// 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 }