390 lines
14 KiB
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
|
|
}
|
|
|
|
|
|
|