563 lines
20 KiB
Go
563 lines
20 KiB
Go
|
|
// 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
|
|
}
|
|
|
|
|