// 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, myAnalysisObject, myGenomeIdentifier, _, 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(myAnalysisObject, diseaseName, myGenomeIdentifier) 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 }