// 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.ScaleNumberProportionally(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.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) 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 }