seekia/internal/moderation/viewedContent/viewedContent.go

623 lines
20 KiB
Go

// viewedContent provides functions to generate and retrieve the viewedContent list
// This list is used to view content on the View Content page
package viewedContent
import "seekia/internal/appMemory"
import "seekia/internal/badgerDatabase"
import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/moderation/contentControversy"
import "seekia/internal/moderation/reviewStorage"
import "seekia/internal/myDatastores/myList"
import "seekia/internal/myDatastores/myMap"
import "seekia/internal/mySettings"
import "slices"
import "sync"
import "errors"
//TODO: Add more sort by attributes
// Examples: Number of reviews, continuous approval period
// This mutex will be locked whenever we update the viewed content list
var updatingViewedContentMutex sync.Mutex
var viewedContentListDatastore *myList.MyList
var viewedContentFiltersMapDatastore *myMap.MyMap
// This function must be called whenever an app user signs in
func InitializeViewedContentDatastores()error{
updatingViewedContentMutex.Lock()
defer updatingViewedContentMutex.Unlock()
newViewedContentListDatastore, err := myList.CreateNewList("ViewedContent")
if (err != nil) { return err }
newViewedContentFiltersMapDatastore, err := myMap.CreateNewMap("ViewedContentFilters")
if (err != nil){ return err }
viewedContentListDatastore = newViewedContentListDatastore
viewedContentFiltersMapDatastore = newViewedContentFiltersMapDatastore
return nil
}
func GetViewedContentSortByAttribute()(string, error){
exists, currentAttribute, err := mySettings.GetSetting("ViewedContentSortByAttribute")
if (err != nil) { return "", err }
if (exists == false){
return "Controversy", nil
}
return currentAttribute, nil
}
func GetViewedContentSortDirection()(string, error){
exists, sortDirection, err := mySettings.GetSetting("ViewedContentSortDirection")
if (err != nil) { return "", err }
if (exists == false){
return "Descending", nil
}
if (sortDirection != "Ascending" && sortDirection != "Descending"){
return "", errors.New("MySettings malformed: Contains invalid ViewedContentSortDirection: " + sortDirection)
}
return sortDirection, nil
}
func CheckIfViewedContentNeedsRefresh()(bool, error){
exists, needsRefresh, err := mySettings.GetSetting("ViewedContentNeedsRefreshYesNo")
if (err != nil) { return false, err }
if (exists == true && needsRefresh == "No") {
return false, nil
}
return true, nil
}
func GetViewedContentIsReadyStatus(networkType byte)(bool, error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, errors.New("GetViewedContentIsReadyStatus called with invalid networkType: " + networkTypeString)
}
exists, contentGeneratedStatus, err := mySettings.GetSetting("ViewedContentGeneratedStatus")
if (err != nil) { return false, err }
if (exists == false || contentGeneratedStatus != "Yes"){
return false, nil
}
exists, contentSortedStatus, err := mySettings.GetSetting("ViewedContentSortedStatus")
if (err != nil) { return false, err }
if (exists == false || contentSortedStatus != "Yes"){
return false, nil
}
exists, contentNetworkTypeString, err := mySettings.GetSetting("ViewedContentNetworkType")
if (err != nil) { return false, err }
if (exists == false){
// This should not happen, because ViewedContentNetworkType is created whenever content is generated
return false, errors.New("mySettings missing ViewedContentNetworkType when ViewedContentGeneratedStatus exists.")
}
contentNetworkType, err := helpers.ConvertNetworkTypeStringToByte(contentNetworkTypeString)
if (err != nil) {
return false, errors.New("mySettings contains invalid ViewedContentNetworkType: " + contentNetworkTypeString)
}
if (contentNetworkType != networkType){
// Content wes generated for a different networkType
// This should never happen, because we will always set ViewedContentGeneratedStatus to No when we switch network types,
// and we will always call the GetViewedContentIsReadyStatus function with the current appNetworkType
//TODO: Log this.
err := mySettings.SetSetting("ViewedContentGeneratedStatus", "No")
if (err != nil) { return false, err }
return false, nil
}
return true, nil
}
//Outputs:
// -bool: Viewed content map list is ready
// -[]string
// -error
func GetViewedContentList(networkType byte)(bool, []string, error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, nil, errors.New("GetViewedContentList called with invalid networkType: " + networkTypeString)
}
areReady, err := GetViewedContentIsReadyStatus(networkType)
if (err != nil) { return false, nil, err }
if (areReady == false){
return false, nil, nil
}
viewedContentList, err := viewedContentListDatastore.GetList()
if (err != nil) { return false, nil, err }
return true, viewedContentList, nil
}
// This function returns the number of viewed contents
// It can be called before the list is ready (after being generated, but before being sorted)
func GetNumberOfGeneratedViewedContents()(int, error){
currentViewedContentList, err := viewedContentListDatastore.GetList()
if (err != nil) { return 0, err }
lengthInt := len(currentViewedContentList)
return lengthInt, nil
}
//Outputs:
// -bool: Build encountered error
// -string: Error encountered
// -bool: Build is stopped (will be stopped if user went to different page)
// -bool: Viewed content is ready
// -float64: Percentage Progress (0 - 1)
// -error
func GetViewedContentBuildStatus(networkType byte)(bool, string, bool, bool, float64, error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, "", false, false, 0, errors.New("GetViewedContentBuildStatus called with invalid networkType: " + networkTypeString)
}
exists, encounteredError := appMemory.GetMemoryEntry("ViewedContentBuildEncounteredError")
if (exists == false){
// No build exists. A build has not been started since Seekia was started
return false, "", false, false, 0, nil
}
if (encounteredError == "Yes"){
exists, errorEncountered := appMemory.GetMemoryEntry("ViewedContentBuildError")
if (exists == false){
return false, "", false, false, 0, errors.New("Viewed Content build encountered error is yes, but no error exists.")
}
return true, errorEncountered, false, false, 0, nil
}
isStopped := CheckIfBuildViewedContentIsStopped()
if (isStopped == true){
return false, "", true, false, 0, nil
}
contentIsReadyBool, err := GetViewedContentIsReadyStatus(networkType)
if (err != nil) { return false, "", false, false, 0, err }
if (contentIsReadyBool == true){
return false, "", false, true, 1, nil
}
exists, currentPercentageString := appMemory.GetMemoryEntry("ViewedContentReadyProgressStatus")
if (exists == false){
// No build exists. A build has not been started since Seekia was started
return false, "", false, false, 0, nil
}
currentPercentageFloat, err := helpers.ConvertStringToFloat64(currentPercentageString)
if (err != nil){
return false, "", false, false, 0, errors.New("ViewedContentReadyProgressStatus is invalid: Not a float: " + currentPercentageString)
}
return false, "", false, false, currentPercentageFloat, nil
}
func CheckIfBuildViewedContentIsStopped()bool{
exists, buildStoppedStatus := appMemory.GetMemoryEntry("StopBuildViewedContentYesNo")
if (exists == false || buildStoppedStatus != "No") {
return true
}
return false
}
// This function will cancel the current build (if one is running)
// It will then start updating our viewed content
func StartUpdatingViewedContent(networkType byte)error{
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return errors.New("StartUpdatingViewedContent called with invalid networkType: " + networkTypeString)
}
appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "Yes")
// We wait for any existing build to stop
updatingViewedContentMutex.Lock()
appMemory.SetMemoryEntry("ViewedContentBuildEncounteredError", "No")
appMemory.SetMemoryEntry("ViewedContentBuildError", "")
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0")
appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "No")
updateViewedContent := func()error{
getViewedContentNeedsToBeGeneratedStatus := func()(bool, error){
exists, contentGeneratedStatus, err := mySettings.GetSetting("ViewedContentGeneratedStatus")
if (err != nil) { return false, err }
if (exists == false || contentGeneratedStatus != "Yes"){
return true, nil
}
exists, viewedContentNetworkTypeString, err := mySettings.GetSetting("ViewedContentNetworkType")
if (err != nil) { return false, err }
if (exists == false){
// This should not happen, because ViewedContentNetworkType is set to Yes whenever viewed content is generated
return false, errors.New("ViewedContentNetworkType missing when ViewedContentGeneratedStatus exists.")
}
viewedContentNetworkType, err := helpers.ConvertNetworkTypeStringToByte(viewedContentNetworkTypeString)
if (err != nil) {
return false, errors.New("mySettings contains invalid ViewedContentNetworkType: " + viewedContentNetworkTypeString)
}
if (viewedContentNetworkType != networkType){
// This should not happen, because ViewedContentGeneratedStatus should be set to No whenever app network type is changed,
// and StartUpdatingViewedContent should only be called with the current app network type.
return true, nil
}
return false, nil
}
viewedContentNeedsToBeGenerated, err := getViewedContentNeedsToBeGeneratedStatus()
if (err != nil) { return err }
if (viewedContentNeedsToBeGenerated == true){
err := mySettings.SetSetting("ViewedContentSortedStatus", "No")
if (err != nil) { return err }
//TODO: Check if content passes filters
//TODO: Omit disabled profiles
viewedContentList := make([]string, 0)
reportedMessageHashesList, err := badgerDatabase.GetAllReportedMessageHashes()
if (err != nil) { return err }
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.10")
reviewedMessageHashesList, err := badgerDatabase.GetAllReviewedMessageHashes()
if (err != nil) { return err }
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.15")
allMessageHashesList := helpers.CombineTwoListsAndAvoidDuplicates(reportedMessageHashesList, reviewedMessageHashesList)
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.20")
for _, messageHash := range allMessageHashesList{
messageHashString := encoding.EncodeBytesToHexString(messageHash[:])
viewedContentList = append(viewedContentList, messageHashString)
}
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.25")
mateProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Mate")
if (err != nil) { return err }
for _, profileHash := range mateProfileHashesList{
profileHashString := encoding.EncodeBytesToHexString(profileHash[:])
viewedContentList = append(viewedContentList, profileHashString)
}
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.35")
hostProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Host")
if (err != nil) { return err }
for _, profileHash := range hostProfileHashesList{
profileHashString := encoding.EncodeBytesToHexString(profileHash[:])
viewedContentList = append(viewedContentList, profileHashString)
}
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.45")
moderatorProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Moderator")
if (err != nil) { return err }
for _, profileHash := range moderatorProfileHashesList{
profileHashString := encoding.EncodeBytesToHexString(profileHash[:])
viewedContentList = append(viewedContentList, profileHashString)
}
err = viewedContentListDatastore.OverwriteList(viewedContentList)
if (err != nil) { return err }
err = mySettings.SetSetting("ViewedContentGeneratedStatus", "Yes")
if (err != nil) { return err }
networkTypeString := helpers.ConvertByteToString(networkType)
err = mySettings.SetSetting("ViewedContentNetworkType", networkTypeString)
if (err != nil) { return err }
}
isStopped := CheckIfBuildViewedContentIsStopped()
if (isStopped == true){
return nil
}
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.50")
contentReadyStatus, err := GetViewedContentIsReadyStatus(networkType)
if (err != nil) { return err }
if (contentReadyStatus == false){
// Now we sort content.
currentSortByAttribute, err := GetViewedContentSortByAttribute()
if (err != nil) { return err }
currentSortDirection, err := GetViewedContentSortDirection()
if (err != nil) { return err }
currentContentsList, err := viewedContentListDatastore.GetList()
if (err != nil) { return err }
// We use this map to make sure there are no duplicate content hashes
// This should never happen, unless the user's stored list was edited or there is a bug
allContentHashesMap := make(map[string]struct{})
//Map Structure: Content Hash Hex -> Content attribute value
contentAttributeValuesMap := make(map[string]float64)
maximumIndex := len(currentContentsList) - 1
for index, contentHashHex := range currentContentsList{
contentHash, err := encoding.DecodeHexStringToBytes(contentHashHex)
if (err != nil){
return errors.New("viewedContentList contains invalid content hash during sort: " + contentHashHex)
}
_, exists := allContentHashesMap[string(contentHash)]
if (exists == true){
return errors.New("viewedContentList contains duplicate content hash.")
}
allContentHashesMap[string(contentHash)] = struct{}{}
// Outputs:
// -bool: Content attribute value is known
// -float64: Content attribute value
// -error
getContentAttributeValue := func()(bool, float64, error){
if (currentSortByAttribute == "Controversy"){
requiredDataExists, controversyRating, err := contentControversy.GetContentControversyRating(contentHash)
if (err != nil) { return false, 0, err }
if (requiredDataExists == false){
// We do not know the content controversy.
return false, 0, nil
}
return true, float64(controversyRating), nil
}
if (currentSortByAttribute == "NumberOfReviewers"){
contentType, err := helpers.GetContentTypeFromContentHash(contentHash)
if (err != nil) { return false, 0, err }
if (contentType == "Profile"){
if (len(contentHash) != 28){
return false, 0, errors.New("GetContentTypeFromContentHash returning Profile for different length contentHash: " + contentHashHex)
}
profileHash := [28]byte(contentHash)
profileMetadataIsKnown, downloadingRequiredData, numberOfReviewers, err := reviewStorage.GetNumberOfProfileReviewers(profileHash)
if (err != nil) { return false, 0, err }
if (profileMetadataIsKnown == false || downloadingRequiredData == false){
return false, 0, nil
}
return true, float64(numberOfReviewers), nil
} else if (contentType == "Message"){
if (len(contentHash) != 26){
return false, 0, errors.New("GetContentTypeFromContentHash returning Message for different length contentHash: " + contentHashHex)
}
messageHash := [26]byte(contentHash)
messageMetadataIsKnown, downloadingRequiredData, numberOfReviewers, err := reviewStorage.GetNumberOfMessageReviewers(messageHash)
if (err != nil) { return false, 0, err }
if (messageMetadataIsKnown == false || downloadingRequiredData == false){
return false, 0, nil
}
return true, float64(numberOfReviewers), nil
}
return false, 0, errors.New("Viewed content list contains invalid contentHash during sort: " + contentHashHex)
}
//TODO: Add more attributes
return false, 0, errors.New("GetViewedContentSortByAttribute returning unknown attribute: " + currentSortByAttribute)
}
contentAttributeValueIsKnown, contentAttributeValue, err := getContentAttributeValue()
if (err != nil) { return err }
if (contentAttributeValueIsKnown == true){
contentAttributeValuesMap[contentHashHex] = contentAttributeValue
}
isStopped := CheckIfBuildViewedContentIsStopped()
if (isStopped == true){
return nil
}
newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80)
if (err != nil) { return err }
newProgressFloat := float64(newScaledPercentageInt)/100
newProgressString := helpers.ConvertFloat64ToString(newProgressFloat)
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", newProgressString)
}
compareContentsFunction := func(contentHashA string, contentHashB string)int{
if (contentHashA == contentHashB){
panic("Duplicate content hashes called to compare function during sort.")
}
attributeValueA, attributeValueAExists := contentAttributeValuesMap[contentHashA]
attributeValueB, attributeValueBExists := contentAttributeValuesMap[contentHashB]
if (attributeValueAExists == false && attributeValueBExists == false){
// We don't know the attribute value for either content
// We sort contents in unicode order
if (contentHashA < contentHashB){
return -1
}
return 1
} else if (attributeValueAExists == true && attributeValueBExists == false){
// We sort unknown attribute contents to the back of the list
return -1
} else if (attributeValueAExists == false && attributeValueBExists == true){
return 1
}
// Both attribute values exist
if (attributeValueA == attributeValueB){
// We sort content hashes in unicode order
if (contentHashA < contentHashB){
return -1
}
return 1
}
if (attributeValueA < attributeValueB){
if (currentSortDirection == "Ascending"){
return -1
}
return 1
}
if (currentSortDirection == "Ascending"){
return 1
}
return -1
}
slices.SortFunc(currentContentsList, compareContentsFunction)
err = viewedContentListDatastore.OverwriteList(currentContentsList)
if (err != nil) { return err }
err = mySettings.SetSetting("ViewedContentSortedStatus", "Yes")
if (err != nil) { return err }
}
appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "1")
err = mySettings.SetSetting("ViewedContentNeedsRefreshYesNo", "No")
if (err != nil) { return err }
return nil
}
updateFunction := func(){
err := updateViewedContent()
if (err != nil){
appMemory.SetMemoryEntry("ViewedContentBuildEncounteredError", "Yes")
appMemory.SetMemoryEntry("ViewedContentBuildError", err.Error())
}
updatingViewedContentMutex.Unlock()
}
go updateFunction()
return nil
}
func GetNumberOfActiveViewedContentFilters()(int, error){
numberOfActiveFilters := 0
//TODO
return numberOfActiveFilters, nil
}
func SetViewedContentFilterOnOffStatus(filterName string, newFilterStatus bool)error{
filterOnOffStatusString := helpers.ConvertBoolToYesOrNoString(newFilterStatus)
err := viewedContentFiltersMapDatastore.SetMapEntry(filterName, filterOnOffStatusString)
if (err != nil) { return err }
err = mySettings.SetSetting("ViewedContentGeneratedStatus", "No")
if (err != nil) { return err }
return nil
}
func GetViewedContentFilterOnOffStatus(filterName string)(bool, error){
filterStatusExists, currentFilterStatus, err := viewedContentFiltersMapDatastore.GetMapEntry(filterName)
if (err != nil) { return false, err }
if (filterStatusExists == false){
return false, nil
}
filterOnOffStatusBool, err := helpers.ConvertYesOrNoStringToBool(currentFilterStatus)
if (err != nil) { return false, err }
return filterOnOffStatusBool, nil
}