seekia/internal/network/myMateCriteria/myMateCriteria.go

564 lines
20 KiB
Go
Raw Normal View History

// myMateCriteria provides functions to retrieve a user's downloads criteria and check if users fulfill their criteria
package myMateCriteria
// Criteria is a way of formatting mate desires for hosts
// This is used when downloading mate profiles from hosts
// The user can choose which desires to share with hosts
// The less information they share, the more profiles they will have to download
// It is a tradeoff between speed and privacy
// Most users should share desires which they would not mind being leaked to the public
// These include things like desired distance, age, and sex
// Malicious nodes could track each requestor's criteria, link the requestor to their user identity, and leak the information online
// TODO: Desires could be altered to make them less specific
// For example, Age desires can be rounded to multiples of 5. Example: 18 -> 20, 26 -> 25
// They should be altered in a different way each time to make it harder to fingerprint the requestor
// I'm unsure if this would increase the requestor anonymity set by much
// Most fingerprints will be very unique, unless the user has few desires and/or shares few desires in their criteria
import "seekia/resources/geneticReferences/monogenicDiseases"
import "seekia/internal/convertCurrencies"
import "seekia/internal/desires/mateDesires"
import "seekia/internal/desires/myLocalDesires"
import "seekia/internal/desires/myMateDesires"
import "seekia/internal/encoding"
import "seekia/internal/genetics/readGeneticAnalysis"
import "seekia/internal/genetics/myChosenAnalysis"
import "seekia/internal/helpers"
import "seekia/internal/myDatastores/myMap"
import "seekia/internal/network/appNetworkType/getAppNetworkType"
import "seekia/internal/profiles/myLocalProfiles"
import "strings"
import "errors"
// This function returns a list of desires that are used in criteria
// This is used when a user is choosing their download desires
// Each desire will have a ShareDesire value associated with it, stored in the myCriteriaMapDatastore
func GetAllMyMateDownloadDesiresList()[]string{
allMyDesiresList := myMateDesires.GetAllMyDesiresList(false)
downloadDesiresList := make([]string, 0)
for _, desireName := range allMyDesiresList{
if (desireName == "HasMessagedMe" || desireName == "IHaveMessaged" || desireName == "HasRejectedMe" || desireName == "IsLiked" || desireName == "IsIgnored" || desireName == "IsMyContact"){
// These desires are not used in criteria
continue
}
downloadDesiresList = append(downloadDesiresList, desireName)
}
return downloadDesiresList
}
// Outputs:
// -bool: Any criteria exists
// -[]byte: My mate downloads criteria
// -error
func GetMyMateDownloadsCriteria()(bool, []byte, error){
allDesiresList := mateDesires.GetAllDesiresList(false)
myCriteriaMap := make(map[string]string)
for _, desireName := range allDesiresList{
if (desireName == "HasMessagedMe" || desireName == "IHaveMessaged" || desireName == "HasRejectedMe" || desireName == "IsLiked" || desireName == "IsIgnored" || desireName == "IsMyContact"){
// These desires are not used in criteria
// We cannot share these desires with hosts
continue
}
if (desireName == "WealthInGold" || desireName == "DistanceFrom"){
// These are desires that we will create using different desires
// Example: Wealth -> WealthInGold, Distance -> DistanceFrom
continue
}
if (desireName == "23andMe_AncestryComposition_Restrictive"){
// This desire exists along with the non-restrictive version
// We will only check either desire once, depending on if restrictive mode is enabled
// Thus, we don't check it twice
continue
}
desireIsMonogenicDisease := strings.HasPrefix(desireName, "MonogenicDisease_")
if (desireIsMonogenicDisease == true){
// These are desires that we create from our OffspringHasAnyDiseaseProbability desire
continue
}
shareDesireBool, err := GetShareMyDesireStatus(desireName)
if (err != nil) { return false, nil, err }
if (shareDesireBool == false){
// We will not share this desire with hosts.
continue
}
// We use this function to add the RequireResponse option to the criteria
// Inputs:
// -string: The name of the desire as it appears in myLocalDesires
// -string: The name of the desire as it appears in the newly created Criteria
// Outputs:
// -error
addDesireRequireResponseOptionToCriteriaMap := func(inputDesireName string, criteriaDesireName string)error{
desireAllowsResponseRequired, _ := mateDesires.CheckIfDesireAllowsRequireResponse(inputDesireName)
if (desireAllowsResponseRequired == false){
// RequireResponse is not allowed for this attribute
return nil
}
getRequireResponseBool := func()(bool, error){
exists, currentResponseRequired, err := myLocalDesires.GetDesire(inputDesireName + "_RequireResponse")
if (err != nil) { return false, err }
if (exists == true && currentResponseRequired == "Yes"){
return true, nil
}
return false, nil
}
requireResponseBool, err := getRequireResponseBool()
if (err != nil) { return err }
if (requireResponseBool == true){
myCriteriaMap[criteriaDesireName + "_RequireResponse"] = "Yes"
}
return nil
}
// We use this function to add the FilterAll option to the criteriaMap
// Inputs:
// -string: The name of the desire as it appears in myLocalDesires
// -string: The name of the desire as it appears in the newly created Criteria
// Outputs:
// -bool: FilterAll is enabled (if true, we need to add the desire value to the criteria map)
// -error
addDesireFilterAllOptionToCriteriaMap := func(inputDesireName string, criteriaDesireName string)(bool, error){
getFilterAllBool := func()(bool, error){
exists, filterAllStatus, err := myLocalDesires.GetDesire(inputDesireName + "_FilterAll")
if (err != nil) { return false, err }
if (exists == true && filterAllStatus == "Yes"){
return true, nil
}
return false, nil
}
filterAllBool, err := getFilterAllBool()
if (err != nil) { return false, err }
if (filterAllBool == false){
// We do not have filterAll enabled
// All users will pass the desire (excluding those without a response, if RequireResponse == true)
return false, nil
}
// FilterAll is enabled
myCriteriaMap[criteriaDesireName + "_FilterAll"] = "Yes"
return true, nil
}
// For some of the desires, we need to convert them to a different form
// This is because some of them are calculated from data within our profile
if (desireName == "Wealth"){
err := addDesireRequireResponseOptionToCriteriaMap("Wealth", "Wealth")
if (err != nil) { return false, nil, err }
// We must convert our currency to gold
// This way we will not leak our currency to any hosts
// Hosts will user a user's WealthInGold attribute for the comparison
currencyExists, currencyCode, err := myLocalDesires.GetDesire("WealthCurrency")
if (err != nil) { return false, nil, err }
if (currencyExists == false){
// We have not selected our desired wealth currency
continue
}
wealthExists, desiredWealthAmount, err := myLocalDesires.GetDesire("Wealth")
if (err != nil) { return false, nil, err }
if (wealthExists == true){
// We have not selected our desired wealth amount
continue
}
filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap("Wealth", "Wealth")
if (err != nil) { return false, nil, err }
if (filterAllIsEnabled == false){
continue
}
desiredWealthAmountFloat64, err := helpers.ConvertStringToFloat64(desiredWealthAmount)
if (err != nil) { return false, nil, err }
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
if (err != nil) { return false, nil, err }
_, convertedDesiredWealth, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(appNetworkType, desiredWealthAmountFloat64, currencyCode)
if (err != nil) { return false, nil, err }
convertedDesiredWealthString := helpers.ConvertFloat64ToString(convertedDesiredWealth)
myCriteriaMap["WealthInGold"] = convertedDesiredWealthString
continue
}
if (desireName == "Distance"){
err := addDesireRequireResponseOptionToCriteriaMap("Distance", "DistanceFrom")
if (err != nil) { return false, nil, err }
//TODO: Randomize location
// We do not want to share our exact location
// This would make it easier to link requestor to user profile
// We want to choose a random distance from our true location, and create a radius that includes our true desired distance
// We must figure out the best way to do this that maximizes privacy
//
// We might want to use only a few random locations that are generated using entropy from the device seed
// This means that, even after making hundreds of requests, hosts would not be able to derive true location
// Otherwise, they could find the average of all requested locations to find the true location of requestor
// This strategy could be combined by using an origin location that is offset from the true location,
// using entropy from the device seed
// We convert Distance to DistanceFrom
minimumExists, desiredDistanceMinimum, err := myLocalDesires.GetDesire("Distance_Minimum")
if (err != nil) { return false, nil, err }
maximumExists, desiredDistanceMaximum, err := myLocalDesires.GetDesire("Distance_Maximum")
if (err != nil) { return false, nil, err }
if (minimumExists == false && maximumExists == false){
// We do not have a desired distance
continue
}
getMinimumBound := func()string{
if (minimumExists == false){
return "0"
}
return desiredDistanceMinimum
}
minimumBound := getMinimumBound()
getMaximumBound := func()string{
if (maximumExists == false){
return "20000"
}
return desiredDistanceMaximum
}
maximumBound := getMaximumBound()
desiredDistanceMinimumFloat64, err := helpers.ConvertStringToFloat64(minimumBound)
if (err != nil){
return false, nil, errors.New("MyLocalDesires contains invalid Distance_Minimum: " + desiredDistanceMinimum)
}
desiredDistanceMaximumFloat64, err := helpers.ConvertStringToFloat64(maximumBound)
if (err != nil){
return false, nil, errors.New("MyLocalDesires contains invalid Distance_Maximum: " + desiredDistanceMaximum)
}
if (desiredDistanceMinimumFloat64 < 0){
return false, nil, errors.New("MyLocalDesires contains invalid Distance_Minimum: " + desiredDistanceMinimum)
}
if (desiredDistanceMaximumFloat64 < desiredDistanceMinimumFloat64){
return false, nil, errors.New("MyLocalDesires contains Distance_Minimum that is larger than Distance_Maximum.")
}
exists, myLocationLatitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLatitude")
if (err != nil) { return false, nil, err }
if (exists == false){
// We have not added a location, we cannot calculate distance.
continue
}
filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap("Distance", "DistanceFrom")
if (err != nil) { return false, nil, err }
if (filterAllIsEnabled == false){
continue
}
exists, myLocationLongitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLongitude")
if (err != nil) { return false, nil, err }
if (exists == false){
return false, nil, errors.New("MyLocalProfiles contains PrimaryLocationLatitude but not PrimaryLocationLongitude")
}
myLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLatitudeString)
if (err != nil) {
return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLatitude: " + myLocationLatitudeString)
}
myLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLongitudeString)
if (err != nil) {
return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLongitude: " + myLocationLongitudeString)
}
isValid := helpers.VerifyLatitude(myLocationLatitudeFloat64)
if (isValid == false){
return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLatitude: " + myLocationLatitudeString)
}
isValid = helpers.VerifyLongitude(myLocationLongitudeFloat64)
if (isValid == false){
return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLongitude: " + myLocationLongitudeString)
}
distanceFromValue := minimumBound + "$" + maximumBound + "@" + myLocationLatitudeString + "+" + myLocationLongitudeString
myCriteriaMap["DistanceFrom"] = distanceFromValue
continue
}
if (desireName == "23andMe_AncestryComposition"){
getDesireToInclude := func()(string, error){
settingExists, restrictiveModeEnabled, err := myLocalDesires.GetDesire("23andMe_AncestryComposition_RestrictiveModeEnabled")
if (err != nil) { return "", err }
if (settingExists == true && restrictiveModeEnabled == "Yes"){
return "23andMe_AncestryComposition_Restrictive", nil
}
return "23andMe_AncestryComposition", nil
}
desireToInclude, err := getDesireToInclude()
if (err != nil) { return false, nil, err }
err = addDesireRequireResponseOptionToCriteriaMap(desireToInclude, desireToInclude)
if (err != nil) { return false, nil, err }
exists, desireValue, err := myLocalDesires.GetDesire(desireToInclude)
if (err != nil) { return false, nil, err }
if (exists == false){
continue
}
filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireToInclude, desireToInclude)
if (err != nil) { return false, nil, err }
if (filterAllIsEnabled == false){
continue
}
myCriteriaMap[desireToInclude] = desireValue
continue
}
if (desireName == "OffspringProbabilityOfAnyMonogenicDisease"){
// We have to determine which mongenic diseases we have a non-zero risk of passing a variant for
desireExists, desiredMaximumRisk, err := myLocalDesires.GetDesire("OffspringProbabilityOfAnyMonogenicDisease_Maximum")
if (err != nil) { return false, nil, err }
if (desireExists == false){
// We have no OffspringProbabilityOfAnyMonogenicDisease desire.
continue
}
myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisMapList, myGenomeIdentifier, iHaveMultipleGenomes, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis()
if (err != nil) { return false, nil, err }
if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){
// We have not linked our genome to our profile
continue
}
monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList()
if (err != nil) { return false, nil, err }
for _, diseaseObject := range monogenicDiseaseObjectsList{
diseaseName := diseaseObject.DiseaseName
diseaseIsDominantOrRecessive := diseaseObject.DominantOrRecessive
myProbabilityIsKnown, _, _, myProbabilityOfPassingAMonogenicDiseaseVariant, _, _, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myAnalysisMapList, diseaseName, myGenomeIdentifier, iHaveMultipleGenomes)
if (err != nil) { return false, nil, err }
if (myProbabilityIsKnown == false){
continue
}
// Outputs:
// -bool: Desire is needed
// -string: Maximum bound of user we desire
// -error
getDiseaseProbabilityMaximumDesiredBound := func()(bool, string, error){
if (diseaseIsDominantOrRecessive == "Dominant"){
if (desiredMaximumRisk == "0"){
// Because disease is dominant, the only way to have a 0% risk is if both users have a 0% risk
return true, "0", nil
}
// desiredMaximumRisk == 99
if (myProbabilityOfPassingAMonogenicDiseaseVariant == 100){
// All users will be filtered, regardless of their probability
// We tried to warn user about this
return true, "0", nil
}
// We might have some probability of passing the disease, but not a 100% probability
// We must make sure the other user does not have a 100% probability of passing the disease
return true, "99", nil
}
// diseaseIsDominantOrRecessive == "Recessive"
if (desiredMaximumRisk == "0"){
if (myProbabilityOfPassingAMonogenicDiseaseVariant == 0){
// Risk is always 0%, because we are 0%
// No desire is needed
return false, "", nil
}
// We have >0% risk
// We must make sure that the other user has a 0% probability of passing a variant
return true, "0", nil
}
// desiredMaximumRisk == "99"
if (myProbabilityOfPassingAMonogenicDiseaseVariant == 100){
// We must make sure other user has a <100% probability of passing a variant
return true, "99", nil
}
// We may have some risk, but not 100% risk
// Thus, offspring risk will never be 100%
// No desire is needed
return false, "", nil
}
desireNeeded, maximumBound, err := getDiseaseProbabilityMaximumDesiredBound()
if (err != nil){ return false, nil, err }
if (desireNeeded == false){
continue
}
diseaseNameWithUnderscores := strings.ReplaceAll(diseaseName, " ", "_")
diseaseDesireName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant_Maximum"
myCriteriaMap[diseaseDesireName] = maximumBound
}
}
err = addDesireRequireResponseOptionToCriteriaMap(desireName, desireName)
if (err != nil) { return false, nil, err }
desireIsNumerical := mateDesires.CheckIfDesireIsNumerical(desireName)
if (desireIsNumerical == true){
minimumExists, minimumValue, err := myLocalDesires.GetDesire(desireName + "_Minimum")
if (err != nil) { return false, nil, err }
maximumExists, maximumValue, err := myLocalDesires.GetDesire(desireName + "_Maximum")
if (err != nil) { return false, nil, err }
if (minimumExists == false && maximumExists == false){
continue
}
filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireName, desireName)
if (err != nil) { return false, nil, err }
if (filterAllIsEnabled == false){
continue
}
myCriteriaMap[desireName + "_Minimum"] = minimumValue
myCriteriaMap[desireName + "_Maximum"] = maximumValue
continue
}
// Desire is not numerical
exists, desireValue, err := myLocalDesires.GetDesire(desireName)
if (err != nil) { return false, nil, err }
if (exists == false){
continue
}
filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireName, desireName)
if (err != nil) { return false, nil, err }
if (filterAllIsEnabled == false){
continue
}
myCriteriaMap[desireName] = desireValue
}
if (len(myCriteriaMap) == 0){
return false, nil, nil
}
myCriteriaBytes, err := encoding.EncodeMessagePackBytes(myCriteriaMap)
if (err != nil) { return false, nil, err }
return true, myCriteriaBytes, nil
}
// This will store the share status of each desire
// This status determines if the user wants to share their desire or not
var myCriteriaMapDatastore *myMap.MyMap
// This function must be called whenever we sign in to an app user
func InitializeMyCriteriaDatastore()error{
newMyCriteriaMapDatastore, err := myMap.CreateNewMap("MyCriteria")
if (err != nil) { return err }
myCriteriaMapDatastore = newMyCriteriaMapDatastore
return nil
}
// This will return whether the user has decided to share this desire with hosts
func GetShareMyDesireStatus(desireName string)(bool, error){
exists, statusValue, err := myCriteriaMapDatastore.GetMapEntry(desireName)
if (err != nil) { return false, err }
if (exists == false){
return false, nil
}
statusValueBool, err := helpers.ConvertYesOrNoStringToBool(statusValue)
if (err != nil) {
return false, errors.New("Invalid myCriteria desire share status: " + statusValue)
}
return statusValueBool, nil
}
func SetShareMyDesireStatus(desireName string, shareStatus bool)error{
newStatusString := helpers.ConvertBoolToYesOrNoString(shareStatus)
err := myCriteriaMapDatastore.SetMapEntry(desireName, newStatusString)
if (err != nil) { return err }
return nil
}