seekia/internal/moderation/moderatorControversy/moderatorControversy.go

390 lines
14 KiB
Go

// moderatorControversy provides functions for calculating a moderator's controversy rating
// Controversy ratings are used by moderators to find controversial moderators
// These are moderators who might be worthy of being banned or debated with
package moderatorControversy
// Controversy calculation can be tuned in several ways:
//
// 1. Weight controversy by moderator scores
// -If we turn off, we use number of moderators.
// -If we turn on, we use combined score of moderators
// -When toggled on, controversy is lower if disagreement is among low-score moderators
// 2. Exclude banned moderators in calculation
// -When enabled, banned moderator verdicts will not be included
// 3. TODO: Only include from a specific time range (Time min- Time Max)
// -This is useful to see moderators who only recently became controversial
// 4. Omit agree with consensus in calculation
// -When enabled, moderator controversy will only count when moderators disagree with the majority
// -This prevents moderators from making their controversy scores seem lower by creating many agreement reviews
// -When enabled, the controversy score is a measure of how much the moderator disagrees, only when they dissent from the majority
//TODO: Reduce the calculation to a random sampling to make it less expensive
import "seekia/internal/badgerDatabase"
import "seekia/internal/contentMetadata"
import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/identity"
import "seekia/internal/moderation/bannedModeratorConsensus"
import "seekia/internal/moderation/enabledModerators"
import "seekia/internal/moderation/moderatorScores"
import "seekia/internal/moderation/readReviews"
import "seekia/internal/moderation/reviewStorage"
import "bytes"
import "errors"
//Outputs:
// -bool: Score is known (parameters exist, downloading any reviews)
// -int64: Moderator controversy score
// -error
func GetModeratorControversyRating(moderatorIdentityHash [16]byte, networkType byte)(bool, int64, error){
isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator")
if (err != nil) { return false, 0, err }
if (isValid == false){
moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:])
return false, 0, errors.New("GetModeratorControversyRating called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex)
}
isValid = helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, 0, errors.New("GetModeratorControversyRating called with invalid networkType: " + networkTypeString)
}
//TODO: Make sure we are actually downloading any reviews (moderator mode/host mode is enabled)
// We may only be hosting a small range of total reviews. This is fine.
// Controversy will be calculated for reviews of content within the range
// There is no controversy for identity reviews
// This is because there can be no identity approve reviews, only ban reviews.
reviewTypesList := []string{"Profile", "Attribute", "Message"}
numberOfRatingsIncluded := 0
controversyRatingsSum := float64(0)
for _, reviewType := range reviewTypesList{
// These are all reviews created by this moderator
exists, reviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(moderatorIdentityHash, reviewType)
if (err != nil) { return false, 0, err }
if (exists == false){
// No reviews exist by this moderator
// No controversy exists
return true, 0, nil
}
reviewsList := make([]readReviews.ReviewWithHash, 0)
for _, reviewHash := range reviewHashesList{
exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash)
if (err != nil) { return false, 0, err }
if (exists == false){
// Review must have been deleted, backgroundJobs will prune reviewHashesList
continue
}
reviewObject := readReviews.ReviewWithHash{
ReviewHash: reviewHash,
ReviewBytes: reviewBytes,
}
reviewsList = append(reviewsList, reviewObject)
}
// This returns a map of the newest reviews for each reviewedHash in the list
newestReviewsMap, err := readReviews.GetNewestModeratorReviewsMapFromReviewsList(reviewsList, moderatorIdentityHash, networkType, true, reviewType)
if (err != nil) { return false, 0, err }
for reviewedHashString, reviewBytes := range newestReviewsMap{
reviewedHash := []byte(reviewedHashString)
ableToRead, _, reviewNetworkType, reviewerIdentityHash, _, currentReviewType, currentReviewedHash, reviewVerdict, reviewMap, err := readReviews.ReadReview(false, reviewBytes)
if (err != nil) { return false, 0, err }
if (ableToRead == false){
return false, 0, errors.New("Database corrupt: Contains invalid review.")
}
if (reviewNetworkType != networkType){
return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning different networkType review.")
}
if (reviewerIdentityHash != moderatorIdentityHash){
return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review by different reviewer.")
}
areEqual := bytes.Equal(reviewedHash, currentReviewedHash)
if (areEqual == false){
return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning different reviewedHash review")
}
if (currentReviewType != reviewType){
return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review of different reviewType.")
}
if (reviewVerdict == "None"){
return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review with None verdict.")
}
//Outputs:
// -bool: Required data exists
// -map[[16]byte]int64: Content Approve advocates map map (identity hash -> Time of approval)
// -map[[16]byte]int64: Content Ban advocates map (identity hash -> Time of ban)
// -error
getApproveAndBanAdvocateMaps := func()(bool, map[[16]byte]int64, map[[16]byte]int64, error){
if (reviewType == "Profile"){
if (len(reviewedHash) != 28){
reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash)
return false, nil, nil, errors.New("ReadReview returning invalid reviewed profileHash: " + reviewedHashHex)
}
reviewedProfileHash := [28]byte(reviewedHash)
metadataExists, _, profileNetworkType, _, _, profileIsDisabled, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(reviewedProfileHash)
if (err != nil) { return false, nil, nil, err }
if (metadataExists == false){
// We can't verify attribute reviews
return false, nil, nil, nil
}
if (profileNetworkType != reviewNetworkType){
// This moderator has reviewed a profile from a different network as the review network
// This moderator must be malicious
// The Seekia app should automatically ban these moderators
// We will not count this profile in the calculation of this moderator's controversy
return false, nil, nil, nil
}
if (profileIsDisabled == true){
// Disabled profiles can't be reviewed.
// The review is invalid. This moderator must be malicious.
// The moderator should automatically ban them in the background.
return false, nil, nil, nil
}
// We will find all moderators who have approved/banned the profile
// This requires finding users who have banned any attribute within the profile
// This could override their full profile approval, if the attribute ban was submitted after the full profile approval
// profileAttributeHashesMap is a map whose values are the attribute hashes of this profile
attributeHashesList := helpers.GetListOfMapValues(profileAttributeHashesMap)
approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileVerdictMaps(reviewedProfileHash, profileNetworkType, true, attributeHashesList)
if (err != nil) { return false, nil, nil, err }
return true, approveAdvocatesMap, banAdvocatesMap, nil
}
if (reviewType == "Attribute"){
// We want to find all moderators who have approved/banned this particular attribute
// This requires checking for full profile approvals for all profiles which contain this attribute
if (len(reviewedHash) != 27){
reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash)
return false, nil, nil, errors.New("ReadReview returning invalid reviewed attributeHash: " + reviewedHashHex)
}
reviewedAttributeHash := [27]byte(reviewedHash)
getAttributeProfileHashesList := func()([][28]byte, error){
anyExist, attributeProfilesList, err := badgerDatabase.GetAttributeProfilesList(reviewedAttributeHash)
if (err != nil) { return nil, err }
if (anyExist == false){
emptyList := make([][28]byte, 0)
return emptyList, nil
}
return attributeProfilesList, nil
}
attributeProfileHashesList, err := getAttributeProfileHashesList()
if (err != nil) { return false, nil, nil, err }
approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileAttributeVerdictMaps(reviewedAttributeHash, networkType, true, attributeProfileHashesList)
if (err != nil) { return false, nil, nil, err }
return true, approveAdvocatesMap, banAdvocatesMap, nil
}
if (reviewType == "Message"){
if (len(reviewedHash) != 26){
reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash)
return false, nil, nil, errors.New("ReadReview returning invalid reviewed messageHash: " + reviewedHashHex)
}
reviewedMessageHash := [26]byte(reviewedHash)
metadataExists, _, messageNetworkType, _, _, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(reviewedMessageHash)
if (err != nil){ return false, nil, nil, err }
if (metadataExists == false){
// We cannot verify reviews, so no approve or ban advocates can be determined
return false, nil, nil, nil
}
if (messageNetworkType != reviewNetworkType){
// Review author must be malicious
// They created a review for a message on a different network
// The app should ban them automatically in the background
return false, nil, nil, nil
}
reviewIsValid, err := readReviews.VerifyMessageReviewCipherKey(reviewMap, messageCipherKeyHash)
if (err != nil){ return false, nil, nil, err }
if (reviewIsValid == false){
// Moderator must be malicious.
// They should be banned automatically in the background
return false, nil, nil, nil
}
approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetMessageVerdictMaps(reviewedMessageHash, messageNetworkType, messageCipherKeyHash)
if (err != nil) { return false, nil, nil, err }
return true, approveAdvocatesMap, banAdvocatesMap, nil
}
return false, nil, nil, errors.New("reviewTypesList contains invalid reviewType: " + reviewType)
}
requiredDataExists, approveAdvocatesMap, banAdvocatesMap, err := getApproveAndBanAdvocateMaps()
if (err != nil) { return false, 0, err }
if (requiredDataExists == false){
continue
}
if (len(approveAdvocatesMap) == 0 && len(banAdvocatesMap) == 0){
continue
}
//TODO: Fix below to retrieve from settings
includeBannedModerators := true
multiplyIdentityScores := true
excludeConsensusAgreement := true
// Outputs:
// -bool: Required data exists
// -float64: Moderators value
// -error
getModeratorsValue := func(inputModeratorsMap map[[16]byte]int64)(bool, float64, error){
moderatorsValue := float64(0)
for identityHash, _ := range inputModeratorsMap{
if (identityHash == reviewerIdentityHash){
// Exclude current reviewer
continue
}
moderatorIsEnabled, err := enabledModerators.CheckIfModeratorIsEnabled(true, identityHash, networkType)
if (err != nil) { return false, 0, err }
if (moderatorIsEnabled == false){
// Skip this moderator
continue
}
if (includeBannedModerators == false){
requiredDataBeingDownloaded, parametersExist, isBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(true, identityHash, networkType)
if (err != nil){ return false, 0, err }
if (requiredDataBeingDownloaded == false || parametersExist == false){
return false, 0, nil
}
if (isBanned == true){
continue
}
}
if (multiplyIdentityScores == false){
moderatorsValue += 1
continue
}
statusIsKnown, moderatorScore, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(identityHash)
if (err != nil) { return false, 0, err }
if (statusIsKnown == false){
// Client has not downloaded moderator score. It will do so automatically.
continue
}
if (scoreIsSufficient == false){
// Moderator cannot participate in consensus
continue
}
moderatorsValue += moderatorScore
}
return true, moderatorsValue, nil
}
requiredDataExists, approveModeratorsValue, err := getModeratorsValue(approveAdvocatesMap)
if (requiredDataExists == false){
// We need the parameters and required data to calculate if moderators are banned
// This is required for all moderators, and is required to calculate controversy
// Thus, we cannot calculate controversy for this moderator
return false, 0, nil
}
requiredDataExists, banModeratorsValue, err := getModeratorsValue(banAdvocatesMap)
if (requiredDataExists == false){
return false, 0, nil
}
if (approveModeratorsValue == 0 && banModeratorsValue == 0){
continue
}
getAgreeDisagreeValues := func()(float64, float64, error){
if (reviewVerdict == "Approve"){
return approveModeratorsValue, banModeratorsValue, nil
}
return banModeratorsValue, approveModeratorsValue, nil
}
agreeValue, disagreeValue, err := getAgreeDisagreeValues()
if (err != nil) { return false, 0, err }
if (excludeConsensusAgreement == true){
if (agreeValue > disagreeValue){
continue
}
}
getCurrentControversyRating := func()float64{
if (disagreeValue == 0){
return 0
}
currentControversyRating := (agreeValue/disagreeValue) * disagreeValue
return currentControversyRating
}
currentControversyRating := getCurrentControversyRating()
controversyRatingsSum += currentControversyRating
numberOfRatingsIncluded += 1
}
}
if (numberOfRatingsIncluded == 0){
// There is no controversy
return true, 0, nil
}
moderatorControversyRating := controversyRatingsSum/float64(numberOfRatingsIncluded)
scoreInt, err := helpers.FloorFloat64ToInt64(moderatorControversyRating)
if (err != nil) { return false, 0, err }
return true, scoreInt, nil
}