seekia/internal/myMatches/myMatches.go

551 lines
18 KiB
Go

// myMatches provides functions to generate and retrieve a user's mate matches
// Matches are the users whom fulfill a user's desires.
package myMatches
import "seekia/internal/appMemory"
import "seekia/internal/badgerDatabase"
import "seekia/internal/desires/myMateDesires"
import "seekia/internal/helpers"
import "seekia/internal/identity"
import "seekia/internal/myBlockedUsers"
import "seekia/internal/myDatastores/myList"
import "seekia/internal/myIdentity"
import "seekia/internal/mySettings"
import "seekia/internal/profiles/viewableProfiles"
import "slices"
import "sync"
import "errors"
// This mutex will be locked whenever we are updating matches.
var updatingMatchesMutex sync.Mutex
var myMatchesListDatastore *myList.MyList
// This function must be called whenever an app user signs in
func InitializeMyMatchesDatastores()error{
updatingMatchesMutex.Lock()
defer updatingMatchesMutex.Unlock()
newMyMatchesListDatastore, err := myList.CreateNewList("MyMatches")
if (err != nil) { return err }
myMatchesListDatastore = newMyMatchesListDatastore
return nil
}
func GetMatchesSortByAttribute()(string, error){
exists, currentAttribute, err := mySettings.GetSetting("MatchesSortByAttribute")
if (err != nil) { return "", err }
if (exists == false){
return "MatchScore", nil
}
return currentAttribute, nil
}
func GetMatchesSortDirection()(string, error){
exists, currentDirection, err := mySettings.GetSetting("MatchesSortDirection")
if (err != nil) { return "", err }
if (exists == false){
return "Descending", nil
}
if (currentDirection != "Ascending" && currentDirection != "Descending"){
return "", errors.New("MySettings malformed: Invalid MatchesSortDirection: " + currentDirection)
}
return currentDirection, nil
}
func GetMatchesReadyStatus(networkType byte)(bool, error) {
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, errors.New("GetMatchesReadyStatus called with invalid networkType: " + networkTypeString)
}
exists, matchesGeneratedStatus, err := mySettings.GetSetting("MatchesGeneratedStatus")
if (err != nil) { return false, err }
if (exists == false || matchesGeneratedStatus != "Yes") {
return false, nil
}
exists, matchesSortedStatus, err := mySettings.GetSetting("MatchesSortedStatus")
if (err != nil) { return false, err }
if (exists == false || matchesSortedStatus != "Yes") {
return false, nil
}
exists, matchesNetworkTypeString, err := mySettings.GetSetting("MatchesNetworkType")
if (err != nil) { return false, err }
if (exists == false){
// This should not happen, because Matches network type is created whenever matches are generated
return false, errors.New("mySettings missing MatchesNetworkType when MatchesGeneratedStatus exists.")
}
matchesNetworkType, err := helpers.ConvertNetworkTypeStringToByte(matchesNetworkTypeString)
if (err != nil) {
return false, errors.New("mySettings contains invalid MatchesNetworkType: " + matchesNetworkTypeString)
}
if (matchesNetworkType != networkType){
// Matches were generated for a different networkType
// This should never happen, because we will always set MatchesGeneratedStatus to No when we switch network types,
// and we will always call the GetMatchesReadyStatus function with the current appNetworkType
//TODO: Log this.
err := mySettings.SetSetting("MatchesGeneratedStatus", "No")
if (err != nil) { return false, err }
return false, nil
}
return true, nil
}
// Will need a refresh any time a new mate profile is downloaded
func CheckIfMyMatchesNeedRefresh()(bool, error){
exists, needsRefresh, err := mySettings.GetSetting("MatchesNeedRefreshYesNo")
if (err != nil) { return true, err }
if (exists == true && needsRefresh == "No"){
return false, nil
}
return true, nil
}
// This function should only be called once matches are ready (generated and sorted)
//Outputs:
// -bool: Matches are ready
// -[][16]byte: List of sorted matches
// -error
func GetMyMatchesList(networkType byte)(bool, [][16]byte, error) {
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, nil, errors.New("GetMyMatchesList called with invalid networkType: " + networkTypeString)
}
matchesReady, err := GetMatchesReadyStatus(networkType)
if (err != nil) { return false, nil, err }
if (matchesReady == false){
return false, nil, nil
}
myMatchIdentityHashesList, err := myMatchesListDatastore.GetList()
if (err != nil) { return false, nil, err }
myMatchesList := make([][16]byte, 0, len(myMatchIdentityHashesList))
for _, matchIdentityHashString := range myMatchIdentityHashesList{
matchIdentityHash, identityType, err := identity.ReadIdentityHashString(matchIdentityHashString)
if (err != nil){
return false, nil, errors.New("myMatchesListDatastore contains invalid match identity hash: " + matchIdentityHashString)
}
if (identityType != "Mate"){
return false, nil, errors.New("myMatchesListDatastore contains non-Mate match identity hash.")
}
myMatchesList = append(myMatchesList, matchIdentityHash)
}
return true, myMatchesList, nil
}
// This function returns the number of matches
// It can be called before the matches are ready (after being generated, but before being sorted)
func GetNumberOfGeneratedMatches()(int, error){
myMatchesList, err := myMatchesListDatastore.GetList()
if (err != nil) { return 0, err }
numberOfGeneratedMatches := len(myMatchesList)
return numberOfGeneratedMatches, nil
}
//Outputs:
// -bool: Build encountered error
// -string: Error encountered
// -bool: Build is stopped (will be stopped if user went to different page)
// -bool: Matches are ready
// -float64: Progress status (0 - 1)
// -error
func GetMyMatchesBuildStatus(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("GetMyMatchesBuildStatus called with invalid networkType: " + networkTypeString)
}
exists, encounteredError := appMemory.GetMemoryEntry("MatchesBuildEncounteredError")
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("MatchesBuildError")
if (exists == false){
return false, "", false, false, 0, errors.New("Matches build encountered error is yes, but no error exists.")
}
return true, errorEncountered, false, false, 0, nil
}
isStopped := CheckIfBuildMatchesIsStopped()
if (isStopped == true){
return false, "", true, false, 0, nil
}
matchesReadyBool, err := GetMatchesReadyStatus(networkType)
if (err != nil) { return false, "", false, false, 0, err }
if (matchesReadyBool == true){
return false, "", false, true, 1, nil
}
exists, currentPercentageString := appMemory.GetMemoryEntry("MatchesReadyProgressPercentage")
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("MatchesReadyProgressPercentage is not a float: " + currentPercentageString)
}
return false, "", false, false, currentPercentageFloat, nil
}
// True == Stop building matches
// False = Don't stop building matches
func CheckIfBuildMatchesIsStopped()bool{
exists, stopBuildMatchesYesNo := appMemory.GetMemoryEntry("StopBuildMatchesYesNo")
if (exists == false || stopBuildMatchesYesNo != "No"){
return true
}
return false
}
// This function will cancel the current build (if one is running)
// It will then start updating our matches
func StartUpdatingMyMatches(networkType byte)error{
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return errors.New("StartUpdatingMyMatches called with invalid networkType: " + networkTypeString)
}
appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "Yes")
// We wait for any existing builds to stop.
updatingMatchesMutex.Lock()
appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0")
appMemory.SetMemoryEntry("MatchesBuildEncounteredError", "No")
appMemory.SetMemoryEntry("MatchesBuildError", "")
appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "No")
updateMatches := func()error{
appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0")
getMatchesNeedToBeGeneratedStatus := func()(bool, error){
exists, matchesGeneratedStatus, err := mySettings.GetSetting("MatchesGeneratedStatus")
if (err != nil) { return false, err }
if (exists == false || matchesGeneratedStatus != "Yes"){
return true, nil
}
exists, matchesNetworkTypeString, err := mySettings.GetSetting("MatchesNetworkType")
if (err != nil) { return false, err }
if (exists == false){
// This should not happen, because MatchesNetworkType is set to Yes whenever matches are generated
return false, errors.New("MatchesNetworkType missing when MatchesGeneratedStatus exists.")
}
matchesNetworkType, err := helpers.ConvertNetworkTypeStringToByte(matchesNetworkTypeString)
if (err != nil) {
return false, errors.New("mySettings contains invalid MatchesNetworkType: " + matchesNetworkTypeString)
}
if (matchesNetworkType != networkType){
// This should not happen, because MatchesGeneratedStatus should be set to No whenever app network type is changed,
// and StartUpdatingMyMatches should only be called with the current app network type.
return true, nil
}
return false, nil
}
matchesNeedToBeGenerated, err := getMatchesNeedToBeGeneratedStatus()
if (err != nil) { return err }
if (matchesNeedToBeGenerated == true){
err := mySettings.SetSetting("MatchesSortedStatus", "No")
if (err != nil) { return err }
// We use below to make sure we do not add ourselves as a match
myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate")
if (err != nil) { return err }
mateIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate")
if (err != nil) { return err }
maximumIndex := len(mateIdentityHashesList) - 1
// This is a list of match identity hashes
matchesList := make([]string, 0)
for index, peerIdentityHash := range mateIdentityHashesList{
stopBuildMatchesYesNo := CheckIfBuildMatchesIsStopped()
if (stopBuildMatchesYesNo == true){
// User has gone to a different page, match generation was stopped
return nil
}
progressPercentage, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 0, 50)
if (err != nil) { return err }
progressFloat := float64(progressPercentage)/100
matchesReadyProgressPercentage := helpers.ConvertFloat64ToString(progressFloat)
appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", matchesReadyProgressPercentage)
if (myIdentityExists == true && peerIdentityHash == myIdentityHash){
// We never show ourselves as a match
continue
}
userIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(peerIdentityHash)
if (err != nil) { return err }
if (userIsBlocked == true){
continue
}
profileExists, _, getAnyUserProfileAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(peerIdentityHash, networkType, true, false, true)
if (err != nil) { return err }
if (profileExists == false) {
// Profile must have been deleted or it is not viewable
// User cannot be a match, because they have no viewable profile.
continue
}
exists, _, userIsDisabled, err := getAnyUserProfileAttributeFunction("Disabled")
if (err != nil) { return err }
if (exists == true && userIsDisabled == "Yes"){
// User's newest viewable profile is disabled.
continue
}
profilePassesDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyUserProfileAttributeFunction)
if (err != nil) { return err }
if (profilePassesDesires == true){
peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash)
if (err != nil) { return err }
matchesList = append(matchesList, peerIdentityHashString)
}
}
err = myMatchesListDatastore.OverwriteList(matchesList)
if (err != nil) { return err }
err = mySettings.SetSetting("MatchesGeneratedStatus", "Yes")
if (err != nil) { return err }
networkTypeString := helpers.ConvertByteToString(networkType)
err = mySettings.SetSetting("MatchesNetworkType", networkTypeString)
if (err != nil) { return err }
err = mySettings.SetSetting("MatchesViewIndex", "0")
if (err != nil) { return err }
}
stopBuildMatchesBool := CheckIfBuildMatchesIsStopped()
if (stopBuildMatchesBool == true){
return nil
}
appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0.50")
exists, matchesSortedStatus, err := mySettings.GetSetting("MatchesSortedStatus")
if (err != nil) { return err }
if (exists == false || matchesSortedStatus != "Yes"){
// Now we sort matches.
myMatchesList, err := myMatchesListDatastore.GetList()
if (err != nil) { return err }
currentSortBy, err := GetMatchesSortByAttribute()
if (err != nil) { return err }
currentSortDirection, err := GetMatchesSortDirection()
if (err != nil) { return err }
// We use this map to make sure there are no duplicate matches
// This should never happen, unless the user's stored list was edited or there is a bug
allMatchesMap := make(map[[16]byte]struct{})
// Map structure: Match Identity Hash -> Sort By Attribute Value
matchAttributeValuesMap := make(map[string]float64)
maximumIndex := len(myMatchesList) - 1
for index, matchIdentityHashString := range myMatchesList{
matchIdentityHash, _, err := identity.ReadIdentityHashString(matchIdentityHashString)
if (err != nil){
return errors.New("myMatchesList contains invalid identity hash during sort: " + matchIdentityHashString)
}
_, exists := allMatchesMap[matchIdentityHash]
if (exists == true){
return errors.New("myMatchesList contains duplicate match.")
}
allMatchesMap[matchIdentityHash] = struct{}{}
profileExists, _, attributeExists, attributeValue, err := viewableProfiles.GetAnyAttributeFromNewestViewableUserProfile(matchIdentityHash, networkType, currentSortBy, true, false, false)
if (err != nil) { return err }
if (profileExists == false){
// Profile must have been deleted, or it became unviewable
// The gui will let the user know this when they navigate to the profile
// The user will be placed towards the end of the sort, with all of the other No Response users
continue
}
if (attributeExists == true){
attributeValueFloat, err := helpers.ConvertStringToFloat64(attributeValue)
if (err != nil) {
return errors.New("Mate profile attribute cannot be converted to float during myMatches sort: " + attributeValue)
}
matchAttributeValuesMap[matchIdentityHashString] = attributeValueFloat
}
isStopped := CheckIfBuildMatchesIsStopped()
if (isStopped == true){
// User has gone to a different page, match generation was stopped
return nil
}
newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 95)
if (err != nil) { return err }
newProgressFloat := float64(newScaledPercentageInt)/100
newProgressString := helpers.ConvertFloat64ToString(newProgressFloat)
appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", newProgressString)
}
compareMatchesFunction := func(identityHashA string, identityHashB string) int {
if (identityHashA == identityHashB){
panic("Duplicate match identity hashes called during sort.")
}
attributeValueA, attributeValueAExists := matchAttributeValuesMap[identityHashA]
attributeValueB, attributeValueBExists := matchAttributeValuesMap[identityHashB]
if (attributeValueAExists == false && attributeValueBExists == false){
// We don't know the attribute value for either match
// We sort matches in unicode order
if (identityHashA < identityHashB){
return -1
}
return 1
} else if (attributeValueAExists == true && attributeValueBExists == false){
// We sort unknown attribute matches 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 identity hashes in unicode order
if (identityHashA < identityHashB){
return -1
}
return 1
}
if (attributeValueA < attributeValueB){
if (currentSortDirection == "Ascending"){
return -1
}
return 1
}
if (currentSortDirection == "Ascending"){
return 1
}
return -1
}
slices.SortFunc(myMatchesList, compareMatchesFunction)
err = myMatchesListDatastore.OverwriteList(myMatchesList)
if (err != nil) { return err }
err = mySettings.SetSetting("MatchesSortedStatus", "Yes")
if (err != nil) { return err }
}
appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "1")
err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "No")
if (err != nil) { return err }
return nil
}
updateFunction := func(){
err := updateMatches()
if (err != nil) {
appMemory.SetMemoryEntry("MatchesBuildEncounteredError", "Yes")
appMemory.SetMemoryEntry("MatchesBuildError", err.Error())
}
updatingMatchesMutex.Unlock()
}
go updateFunction()
return nil
}