seekia/internal/moderation/myUnreviewed/myUnreviewed.go

660 lines
26 KiB
Go

// myUnreviewed provides functions to get message, profile, and attribute hashes the user has not yet reviewed
package myUnreviewed
// Each content to review has a priority
// Content that has been reviewed by the fewest moderators will have the highest priority
// Content that has been sufficiently reviewed by other moderators will have a lower priority
// Unreviewed content will be retained by the client until it expires from the network
//TODO: Add filter for ProfileLanguage, so user will only review content they understand
//TODO: Add higher priority for reported profiles?
//TODO: Add limit where if it has found enough options to review, break
// -This means we will only need to search through a maximum number of contents before we settle on one that is the highest priority
// -It may not be the highest priority out of all contents, but it will be the highest priority out of a subset of contents
// -This would make the retrieval faster
//TODO: Extract errantProfiles from identity reviews and treat them like profile reports
//TODO: Add a toggle to consider/not consider profiles/messages reviewed if we have banned the author
//TODO: Build a cache to store the highest priority unreviewed content hashes
// -This way we only have to repeat the process after the highest priority unreviewed content has been reviewed
import "seekia/internal/moderation/myReviews"
import "seekia/internal/moderation/verifiedVerdict"
import "seekia/internal/moderation/mySkippedContent"
import "seekia/internal/moderation/myHiddenContent"
import "seekia/internal/profiles/profileStorage"
import "seekia/internal/profiles/readProfiles"
import "seekia/internal/messaging/chatMessageStorage"
import "seekia/internal/myRanges"
import "seekia/internal/badgerDatabase"
import "seekia/internal/helpers"
import "strings"
import "errors"
import "slices"
//Outputs:
// -bool: Any unreviewed message hash exists
// -[26]byte: Message Hash
// -error
func GetMyHighestPriorityUnreviewedMessageHash(networkType byte, imageOrText string)(bool, [26]byte, error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, [26]byte{}, errors.New("GetMyHighestPriorityUnreviewedMessageHash called with invalid networkType: " + networkTypeString)
}
if (imageOrText != "Image" && imageOrText != "Text"){
return false, [26]byte{}, errors.New("GetMyHighestPriorityUnreviewedMessageHash called with invalid imageOrText: " + imageOrText)
}
allReviewedMessageHashesList, err := badgerDatabase.GetAllReviewedMessageHashes()
if (err != nil) { return false, [26]byte{}, err }
allReportedMessageHashesList, err := badgerDatabase.GetAllReportedMessageHashes()
if (err != nil) { return false, [26]byte{}, err }
allMessageHashesList := helpers.CombineTwoListsAndAvoidDuplicates(allReviewedMessageHashesList, allReportedMessageHashesList)
myModeratorIdentityExists, myReviewedMessageHashesList, err := myReviews.GetMyReviewedMessageHashesList(networkType)
if (err != nil) { return false, [26]byte{}, err }
if (myModeratorIdentityExists == false) {
return false, [26]byte{}, errors.New("GetMyHighestPriorityUnreviewedMessageHash called when my moderator identity does not exist.")
}
myIdentityExists, myBannedIdentityHashesList, err := myReviews.GetMyBannedIdentityHashesList(networkType)
if (err != nil) { return false, [26]byte{}, err }
if (myIdentityExists == false) {
return false, [26]byte{}, errors.New("My moderator identity not found after being found already.")
}
leastReviewedMessageFound := false
var leastReviewedMessageHash [26]byte
leastReviewedMessageNumberOfReviews := 0
// We will only serve skipped messages if all messages remaining to review are skipped
// This bool will be true, unless we encounter a non-skipped message
// If we do, we will overwrite the existing least reviewed messageHash and reject all skipped messages going forward
allMessagesAreSkipped := true
for _, messageHash := range allMessageHashesList{
iHaveReviewed := slices.Contains(myReviewedMessageHashesList, messageHash)
if (iHaveReviewed == true){
continue
}
messageMetadataIsKnown, _, messageNetworkType, messageInbox, _, downloadingRequiredReviews, parametersExist, _, numberApprove, numberBan, _, _, _, _, err := verifiedVerdict.GetVerifiedMessageVerdict(messageHash)
if (err != nil) { return false, [26]byte{}, err }
if (messageMetadataIsKnown == false){
// We do not have the message downloaded. It is not eligible for review
continue
}
if (messageNetworkType != networkType){
// Message belongs to a different networkType.
continue
}
if (downloadingRequiredReviews == false){
// Message is not within our moderation range. Skip message
continue
}
if (parametersExist == false){
// Moderation parameters need to be downloaded to moderate content. Stop searching for messages.
break
}
moderatorModeEnabled, isInMyModerationRange, err := myRanges.CheckIfMessageInboxIsWithinMyModerationRange(messageInbox)
if (err != nil) { return false, [26]byte{}, err }
if (moderatorModeEnabled == false){
// Mode must have been disabled during generation.
// No messages to review.
return false, [26]byte{}, nil
}
if (isInMyModerationRange == false){
// Message is outside of our moderation range. Skip it.
continue
}
messageExists, cipherKeyFound, messageIsDecryptable, _, senderIdentityHash, messageCommunication, err := chatMessageStorage.GetDecryptedMessageForModeration(messageHash)
if (err != nil) { return false, [26]byte{}, err }
if (messageExists == false){
// We cannot review messages which we do not have downloaded
continue
}
if (cipherKeyFound == false){
// We do not have a cipher key
// We cannot view the message contents
continue
}
if (messageIsDecryptable == false){
// We cannot decrypt the message
// Message author must be malicious
// We should ban these kinds of messages automatically, and ban anyone who approves them
continue
}
identityIsBanned := slices.Contains(myBannedIdentityHashesList, senderIdentityHash)
if (identityIsBanned == true){
// We already banned the author of the message
// Review is not necessary
continue
}
getImageOrTextStatus := func()string{
isImage := strings.HasPrefix(messageCommunication, ">!>Photo=")
if (isImage == true){
return "Image"
}
return "Text"
}
imageOrTextStatus := getImageOrTextStatus()
if (imageOrTextStatus != imageOrText){
// Message is not the imageOrText that we are interested in.
continue
}
messageIsHidden, _, _, err := myHiddenContent.CheckIfMessageIsHidden(messageHash)
if (err != nil) { return false, [26]byte{}, err }
if (messageIsHidden == true){
// The moderator has hidden this message.
continue
}
messageIsSkipped, _, _, err := mySkippedContent.CheckIfMessageIsSkipped(messageHash)
if (err != nil) { return false, [26]byte{}, err }
if (messageIsSkipped == false){
if (allMessagesAreSkipped == true){
// This is the first non-skipped message we have encountered
// This may not be the first message we have encountered
// Thus, we must clear any previously found content
leastReviewedMessageFound = false
leastReviewedMessageHash = [26]byte{}
leastReviewedMessageNumberOfReviews = 0
allMessagesAreSkipped = false
}
} else {
if (allMessagesAreSkipped == false){
// We have some non-skipped content to review, so we will reject all skipped content
continue
}
}
numberOfReviewers := numberApprove + numberBan
if (numberOfReviewers == 0 && messageIsSkipped == false){
// Message is high priority, it has no reviews. We stop searching.
return true, messageHash, nil
}
if (leastReviewedMessageFound == false || numberOfReviewers < leastReviewedMessageNumberOfReviews){
leastReviewedMessageFound = true
leastReviewedMessageHash = messageHash
leastReviewedMessageNumberOfReviews = numberOfReviewers
}
}
if (leastReviewedMessageFound == false){
// Nothing left to review.
return false, [26]byte{}, nil
}
return true, leastReviewedMessageHash, nil
}
// This function only returns full profiles that have been banned or reported, without reference to the specific attribute
// To approve these profiles, moderators must approve the full profile, which requires viewing all attributes of the profile, including canonical ones
// For profiles whose reviewed attributes have been specified, use GetMyHighestPriorityUnreviewedProfileAttributeHash instead
//Outputs:
// -bool: Unreviewed profile exists
// -[28]byte: Highest priority unreviewed profile hash
// -error
func GetMyHighestPriorityUnreviewedProfileHash(profileType string, networkType byte)(bool, [28]byte, error){
if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){
return false, [28]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileHash called with invalid profileType: " + profileType)
}
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, [28]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileHash called with invalid networkType: " + networkTypeString)
}
// For each profile, we must determine if we have reviewed it
// If we have approved/banned the entire profile, then it is considered reviewed
// If we have banned any attribute of the profile, then it is considered reviewed
moderatorIdentityExists, myReviewedProfileHashesMap, err := myReviews.GetMyReviewedProfileHashesMap(profileType, networkType)
if (err != nil) { return false, [28]byte{}, err }
if (moderatorIdentityExists == false) {
return false, [28]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileHash called when my moderator identity does not exist.")
}
myIdentityExists, myBannedIdentityHashesList, err := myReviews.GetMyBannedIdentityHashesList(networkType)
if (err != nil) { return false, [28]byte{}, err }
if (myIdentityExists == false) {
return false, [28]byte{}, errors.New("My moderator identity not found after being found already.")
}
allReviewedProfileHashesList, err := badgerDatabase.GetAllReviewedProfileHashes()
if (err != nil) { return false, [28]byte{}, err }
allReportedProfileHashesList, err := badgerDatabase.GetAllReportedProfileHashes()
if (err != nil) { return false, [28]byte{}, err }
allProfileHashesList := helpers.CombineTwoListsAndAvoidDuplicates(allReviewedProfileHashesList, allReportedProfileHashesList)
leastReviewedProfileHashFound := false
var leastReviewedProfileHash [28]byte
leastReviewedProfileHashReviewersCount := 0
// We will only serve skipped profiles if all remaining profiles to review are skipped
// This bool will be true, unless we encounter a non-skipped profile
// If we do, we will overwrite the existing least reviewed profileHash and reject all skipped profiles going forward
allProfilesAreSkipped := true
for _, profileHash := range allProfileHashesList{
_, iHaveReviewed := myReviewedProfileHashesMap[profileHash]
if (iHaveReviewed == true){
// We have already reviewed the profile, or banned one of its attributes
continue
}
profileIsHidden, _, _, err := myHiddenContent.CheckIfProfileIsHidden(profileHash)
if (err != nil) { return false, [28]byte{}, err }
if (profileIsHidden == true){
// The moderator has hidden this profile.
continue
}
exists, _, err := profileStorage.GetStoredProfile(profileHash)
if (err != nil) { return false, [28]byte{}, err }
if (exists == false){
// We need the profile bytes to review the profile
continue
}
profileIsDisabled, profileMetadataIsKnown, _, profileNetworkType, profileAuthor, _, downloadingRequiredReviews, parametersExist, _, approveAdvocatesCount, banAdvocatesCount, _, _, _, _, _, _, _, _, fullProfileBanAdvocatesCount, _, _, err := verifiedVerdict.GetVerifiedProfileVerdict(profileHash)
if (err != nil) { return false, [28]byte{}, err }
if (profileIsDisabled == true){
// No review is needed. Skip
continue
}
if (profileMetadataIsKnown == false){
// We do not have the profile downloaded, we cannot review it.
// It must have been deleted after GetAllProfileHashes step.
continue
}
if (profileNetworkType != networkType){
// Profile belongs to a different networkType.
continue
}
if (downloadingRequiredReviews == false){
// Message is not within our moderation range. Skip profile
continue
}
if (parametersExist == false){
// Moderation parameters need to be downloaded to moderate content. Stop searching for profiles.
return false, [28]byte{}, nil
}
profileIsReported := slices.Contains(allReportedProfileHashesList, profileHash)
if (fullProfileBanAdvocatesCount == 0 && profileIsReported == false){
// Nobody has banned/reported the full profile.
// This means that the profile can be reviewed by reviewing attributes only
// We skip this profile
continue
}
moderatorModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyModerationRange(profileAuthor)
if (err != nil) { return false, [28]byte{}, err }
if (moderatorModeEnabled == false){
// Mode must have been disabled after previous check. Return no profiles to review.
return false, [28]byte{}, nil
}
if (isWithinMyRange == false){
// Profile is outside of our moderation range, skip it.
continue
}
iHaveBannedIdentity := slices.Contains(myBannedIdentityHashesList, profileAuthor)
if (iHaveBannedIdentity == true){
// We have banned the profile author
// Reviewing the profile is not necessary
continue
}
profileIsSkipped, _, _, err := mySkippedContent.CheckIfProfileIsSkipped(profileHash)
if (err != nil) { return false, [28]byte{}, err }
if (profileIsSkipped == false){
if (allProfilesAreSkipped == true){
// This is the first non-skipped profile we have encountered
// This may not be the first profile we have encountered
// Thus, we must clear any previously found profiles
leastReviewedProfileHashFound = false
leastReviewedProfileHash = [28]byte{}
leastReviewedProfileHashReviewersCount = 0
allProfilesAreSkipped = false
}
} else {
if (allProfilesAreSkipped == false){
// We have some non-skipped content to review, so we will reject all skipped content
continue
}
}
numberOfEligibleReviewers := approveAdvocatesCount + banAdvocatesCount
if (approveAdvocatesCount == 0 && profileIsSkipped == false){
// This profile has been either been reported or banned, and has no approvals by anyone eligible
// We consider this the highest priority kind of profile.
// Stop searching and return this profile
return true, profileHash, nil
}
if (leastReviewedProfileHashFound == false || leastReviewedProfileHashReviewersCount < numberOfEligibleReviewers){
leastReviewedProfileHashFound = true
leastReviewedProfileHash = profileHash
leastReviewedProfileHashReviewersCount = numberOfEligibleReviewers
}
}
if (leastReviewedProfileHashFound == false){
// No profiles to review
return false, [28]byte{}, nil
}
return true, leastReviewedProfileHash, nil
}
//Outputs:
// -bool: Any unreviewed profile attribute hash exists
// -[27]byte: Profile attribute hash
// -[28]byte: Profile hash that contained this attribute (there may be many, we will return the first one we encounter)
// -[16]byte: Profile identity hash that authored the profile
// -error
func GetMyHighestPriorityUnreviewedProfileAttributeHash(profileType string, attributeIdentifier int, networkType byte)(bool, [27]byte, [28]byte, [16]byte, error){
if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){
return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called with invalid profileType: " + profileType)
}
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called with invalid networkType: " + networkTypeString)
}
myIdentityExists, myReviewedProfileHashesMap, err := myReviews.GetMyReviewedProfileHashesMap(profileType, networkType)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (myIdentityExists == false) {
return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called when my moderator identity does not exist.")
}
myIdentityExists, myReviewedAttributeHashesMap, err := myReviews.GetMyReviewedProfileAttributeHashesMap(networkType)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (myIdentityExists == false) {
return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called when my moderator identity does not exist.")
}
myIdentityExists, myBannedIdentityHashesList, err := myReviews.GetMyBannedIdentityHashesList(networkType)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (myIdentityExists == false) {
return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called when my moderator identity does not exist.")
}
allReportedAttributeHashesList, err := badgerDatabase.GetAllReportedProfileAttributeHashes()
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
allProfileHashesList, err := badgerDatabase.GetAllProfileHashes(profileType)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
// This map will store the attribute hashes we have already checked
// We need this because the same attribute may exist in multiple profiles
encounteredAttributeHashesMap := make(map[[27]byte]struct{})
// We use the below variables to find the least reviewed (highest priority) attribute hash
leastReviewedAttributeHashFound := false
var leastReviewedAttributeHash [27]byte
var leastReviewedAttributeProfileHash [28]byte
var leastReviewedAttributeIdentityHash [16]byte
leastReviewedAttributeHashReviewsCount := 0
// We will only serve skipped attributes if all remaining attributes to review are skipped
// This bool will be true, unless we encounter a non-skipped attribute
// If we do, we will overwrite the existing least reviewed attributeHash and reject all skipped attributes going forward
allAttributesAreSkipped := true
for _, profileHash := range allProfileHashesList{
_, iHaveReviewed := myReviewedProfileHashesMap[profileHash]
if (iHaveReviewed == true){
// We have already reviewed this profile.
// We don't need to review any of its component attributes
continue
}
exists, _, err := profileStorage.GetStoredProfile(profileHash)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (exists == false){
// We need the profile bytes to review the profile
// Profile must have been deleted after we called GetAllProfileHashes
continue
}
profileIsDisabled, profileMetadataIsKnown, _, profileNetworkType, profileAuthor, profileAttributeHashesMap, downloadingRequiredReviews, parametersExist, _, _, _, _, _, attributeApproveAdvocateCountsMap, attributeBanAdvocateCountsMap, _, _, _, _, _, _, allAttributeBanAdvocatesMap, err := verifiedVerdict.GetVerifiedProfileVerdict(profileHash)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (profileIsDisabled == true){
// No review is needed. Skip
continue
}
if (profileMetadataIsKnown == false){
// We do not have the profile downloaded, we cannot review it.
// It must have been deleted after GetAllProfileHashes step.
continue
}
if (profileNetworkType != networkType){
// Profile belongs to a different networkType.
continue
}
if (downloadingRequiredReviews == false){
// Profile is not within our moderation range. Skip message
continue
}
if (parametersExist == false){
// Moderation parameters need to be downloaded to moderate content. Stop searching for profile attributes.
break
}
moderatorModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyModerationRange(profileAuthor)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (moderatorModeEnabled == false){
// Mode must have been disabled after previous check. Return no profiles to review.
return false, [27]byte{}, [28]byte{}, [16]byte{}, nil
}
if (isWithinMyRange == false){
// Profile is outside of our moderation range, skip it.
continue
}
iHaveBannedIdentity := slices.Contains(myBannedIdentityHashesList, profileAuthor)
if (iHaveBannedIdentity == true){
// We have already banned the profile author
continue
}
attributeHash, exists := profileAttributeHashesMap[attributeIdentifier]
if (exists == false){
// This profile does not have the attribute which we want to review
continue
}
_, hasBeenEncountered := encounteredAttributeHashesMap[attributeHash]
if (hasBeenEncountered == true){
// We already dealt with this attribute
continue
}
encounteredAttributeHashesMap[attributeHash] = struct{}{}
_, iHaveReviewed = myReviewedAttributeHashesMap[attributeHash]
if (iHaveReviewed == true){
// We have already reviewed the attribute.
continue
}
attributeIsHidden, _, _, err := myHiddenContent.CheckIfAttributeIsHidden(attributeHash)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (attributeIsHidden == true){
// The moderator has hidden this attribute.
continue
}
// This will tell us if any moderators, banned or not, have banned the attribute
checkIfAnyAttributeBanAdvocatesExist := func()bool{
allAttributeBanAdvocatesList, exists := allAttributeBanAdvocatesMap[attributeIdentifier]
if (exists == false){
return false
}
if (len(allAttributeBanAdvocatesList) == 0){
return false
}
return true
}
anyAttributeBanAdvocatesExist := checkIfAnyAttributeBanAdvocatesExist()
if (anyAttributeBanAdvocatesExist == false){
attributeIsReported := slices.Contains(allReportedAttributeHashesList, attributeHash)
if (attributeIsReported == false){
// Nobody has banned/reported this attribute
if (profileType != "Mate"){
// We do not need to review attributes that are not banned for non-mate profiles
continue
}
attributeProfileType, attributeIsCanonical, err := readProfiles.ReadAttributeHashMetadata(attributeHash)
if (err != nil){ return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (attributeProfileType != profileType){
return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetVerifiedProfileVerdict returning attribute hash with different profileType than author.")
}
if (attributeIsCanonical == true){
// We don't need to review it, even if profileType == Mate
continue
}
// We see if the user has a newer profile
// If they do, we don't need to review this profile
anyProfileExists, _, newestProfileHash, _, _, _, err := profileStorage.GetNewestUserProfile(profileAuthor, networkType)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (anyProfileExists == false){
// The user's profile must have been deleted after we performed GetAllProfileHashes
continue
}
if (profileHash != newestProfileHash){
// There is a newer profile by this same Mate user
// We don't need to review the current profile, because it is not banned by anyone, and none of its attributes are banned
// We will review the newer profile instead
continue
}
// We still need to review this attribute, because it is a non-canonical mate attribute
}
}
// The attribute is worthy of review
// We see if it is less reviewed than our current leastReviewedAttributeHash
// This will tell us the number of eligible attribute approve advocates
getAttributeApproveAdvocatesCount := func()int{
attributeApproveAdvocatesCount, exists := attributeApproveAdvocateCountsMap[attributeIdentifier]
if (exists == false){
return 0
}
return attributeApproveAdvocatesCount
}
// This will tell us the number of eligible attribute ban advocates
getAttributeBanAdvocatesCount := func()int{
attributeBanAdvocatesCount, exists := attributeBanAdvocateCountsMap[attributeIdentifier]
if (exists == false){
return 0
}
return attributeBanAdvocatesCount
}
attributeApproveAdvocatesCount := getAttributeApproveAdvocatesCount()
attributeBanAdvocatesCount := getAttributeBanAdvocatesCount()
numberOfReviewers := attributeApproveAdvocatesCount + attributeBanAdvocatesCount
attributeIsSkipped, _, _, err := mySkippedContent.CheckIfAttributeIsSkipped(attributeHash)
if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err }
if (attributeIsSkipped == false){
if (allAttributesAreSkipped == true){
// This is the first non-skipped attribute we have encountered
// It may not be the first attribute we have encountered
// Thus, we must clear any previously found attributes
leastReviewedAttributeHashFound = false
leastReviewedAttributeHash = [27]byte{}
leastReviewedAttributeProfileHash = [28]byte{}
leastReviewedAttributeIdentityHash = [16]byte{}
leastReviewedAttributeHashReviewsCount = 0
allAttributesAreSkipped = false
}
} else {
if (allAttributesAreSkipped == false){
// We have some non-skipped attributes, so we will reject all skipped attributes
continue
}
}
if (attributeApproveAdvocatesCount == 0 && attributeIsSkipped == false){
// This is the highest priority kind of profile attribute
// It has no approvers, and needs to be reviewed
// We stop searching and return the attribute
return true, attributeHash, profileHash, profileAuthor, nil
}
if (leastReviewedAttributeHashFound == false || numberOfReviewers < leastReviewedAttributeHashReviewsCount){
leastReviewedAttributeHashFound = true
leastReviewedAttributeHash = attributeHash
leastReviewedAttributeProfileHash = profileHash
leastReviewedAttributeIdentityHash = profileAuthor
leastReviewedAttributeHashReviewsCount = numberOfReviewers
}
}
if (leastReviewedAttributeHashFound == true){
return true, leastReviewedAttributeHash, leastReviewedAttributeProfileHash, leastReviewedAttributeIdentityHash, nil
}
// Nothing left to review
return false, [27]byte{}, [28]byte{}, [16]byte{}, nil
}