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