// myProfileExports provides functions to manage a user's exported profiles // Exported profiles exist so users can view their profiles before broadcasting them // They are stored in their own folder, waiting to be broadcasted package myProfileExports import "seekia/resources/geneticReferences/monogenicDiseases" import "seekia/resources/geneticReferences/polygenicDiseases" import "seekia/resources/geneticReferences/traits" import "seekia/internal/genetics/readGeneticAnalysis" import "seekia/internal/encoding" import "seekia/internal/genetics/myChosenAnalysis" import "seekia/internal/helpers" import "seekia/internal/localFilesystem" import "seekia/internal/messaging/myChatKeys" import "seekia/internal/myDevice" import "seekia/internal/myIdentity" import "seekia/internal/profiles/createProfiles" import "seekia/internal/profiles/myLocalProfiles" import "seekia/internal/profiles/profileFormat" import "seekia/internal/profiles/readProfiles" import messagepack "github.com/vmihailenco/msgpack/v5" import "strings" import "time" import "errors" import "path/filepath" import "sync" import "slices" // We lock this when we are writing any exported profile files var writingExportedProfileFilesMutex sync.Mutex func UpdateMyExportedProfile(myProfileType string, networkType byte)error{ if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){ return errors.New("UpdateMyExportedProfile called with invalid myProfileType: " + myProfileType) } isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return errors.New("UpdateMyExportedProfile called with invalid networkType: " + networkTypeString) } networkTypeString := helpers.ConvertByteToString(networkType) myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) if (err != nil) { return err } if (myIdentityFound == false) { return errors.New("UpdateMyExportedProfile called when my identity is missing.") } getNewProfileMap := func()(map[string]string, error){ profileMap := map[string]string{ "ProfileType": myProfileType, "NetworkType": networkTypeString, } attributeExists, profileIsDisabled, err := myLocalProfiles.GetProfileData(myProfileType, "Disabled") if (err != nil) { return nil, err } if (attributeExists == true && profileIsDisabled == "Yes"){ profileMap["Disabled"] = "Yes" return profileMap, nil } // This is a map of all possible profile attributes // This map contains some attributes that are calculated, and thus will not be found when querying the user profile // An example is a user's genetic analysis, which is added in its own function // Map Structure: Attribute name -> Attribute object profileAttributeObjectsList, err := profileFormat.GetProfileAttributeObjectsList() if (err != nil) { return nil, err } for _, attributeObject := range profileAttributeObjectsList{ attributeName := attributeObject.AttributeName if (attributeName == "ProfileType" || attributeName == "CreationTime" || attributeName == "IdentityKey" || attributeName == "ProfileVersion" || attributeName == "Disabled"){ continue } profileVersion := 1 attributeProfileTypesList, err := attributeObject.GetProfileTypes(profileVersion) if (err != nil) { return nil, err } attributeIsRelevant := slices.Contains(attributeProfileTypesList, myProfileType) if (attributeIsRelevant == false){ continue } attributeValueExists, attributeValue, err := myLocalProfiles.GetProfileData(myProfileType, attributeName) if (err != nil){ return nil, err } if (attributeValueExists == false){ continue } if (attributeName == "23andMe_AncestryComposition"){ // The user has the option of hiding this attribute from their profile // The user can then view offspring ancestry compositions without sharing that information on their profile // This is the only attribute (other than genetic analysis attributes) where hiding the attribute is possible exists, visibilityStatus, err := myLocalProfiles.GetProfileData("Mate", "VisibilityStatus_23andMe_AncestryComposition") if (err != nil) { return nil, err } if (exists == true && visibilityStatus == "No"){ // The user has opted to not share this attribute continue } } if (attributeName == "PrimaryLocationLatitude" || attributeName == "PrimaryLocationLongitude" || attributeName == "PrimaryLocationCountry" || attributeName == "SecondaryLocationLatitude" || attributeName == "SecondaryLocationLongitude" || attributeName == "SecondaryLocationCountry"){ exists, visibilityStatus, err := myLocalProfiles.GetProfileData("Mate", "VisibilityStatus_Location") if (err != nil){ return nil, err } if (exists == true && visibilityStatus == "No"){ // The user has opted to not share their location continue } } profileMap[attributeName] = attributeValue } addGeneticAnalysisToProfileMap := func()error{ if (myProfileType != "Mate"){ // Only mate profiles can contain a genetic analysis return nil } myGenomePersonChosen, anyGenomesExist, personAnalysisIsReady, myGeneticAnalysisObject, genomeIdentifierToShare, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() if (err != nil) { return err } if (myGenomePersonChosen == false){ // User has not linked a genome person. // No genetic analysis needs to be added. return nil } if (anyGenomesExist == false){ // The profile has a genome person, but the genome person has no genomes return nil } if (personAnalysisIsReady == false){ return errors.New("UpdateMyExportedProfile called when profile genetic analysis is not ready.") } monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() if (err != nil) { return err } for _, monogenicDiseaseName := range monogenicDiseaseNamesList{ shareDiseaseInfoAttributeName := "ShareMonogenicDiseaseInfo_" + monogenicDiseaseName currentShareDiseaseInfoAttributeExists, currentShareDiseaseInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareDiseaseInfoAttributeName) if (err != nil) { return err } if (currentShareDiseaseInfoAttributeExists == false){ continue } if (currentShareDiseaseInfoAttribute != "Yes"){ continue } probabilitiesKnown, _, probabilityOfPassingADiseaseVariant, _, numberOfVariantsTested, _, _, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisObject, monogenicDiseaseName, genomeIdentifierToShare) if (err != nil) { return err } if (probabilitiesKnown == false){ continue } diseaseNameWithUnderscores := strings.ReplaceAll(monogenicDiseaseName, " ", "_") probabilityOfPassingVariantAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant" probabilityOfPassingADiseaseVariantString := helpers.ConvertIntToString(probabilityOfPassingADiseaseVariant) variantsTestedAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_NumberOfVariantsTested" numberOfVariantsTestedString := helpers.ConvertIntToString(numberOfVariantsTested) profileMap[probabilityOfPassingVariantAttributeName] = probabilityOfPassingADiseaseVariantString profileMap[variantsTestedAttributeName] = numberOfVariantsTestedString } polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() if (err != nil) { return err } for _, diseaseObject := range polygenicDiseaseObjectsList{ diseaseName := diseaseObject.DiseaseName shareDiseaseInfoAttributeName := "SharePolygenicDiseaseInfo_" + diseaseName currentShareDiseaseInfoAttributeExists, currentShareDiseaseInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareDiseaseInfoAttributeName) if (err != nil) { return err } if (currentShareDiseaseInfoAttributeExists == false){ continue } if (currentShareDiseaseInfoAttribute != "Yes"){ continue } _, _, _, myDiseaseLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisObject, diseaseName, genomeIdentifierToShare) if (err != nil) { return err } for rsID, locusValueObject := range myDiseaseLocusValuesMap{ rsIDString := helpers.ConvertInt64ToString(rsID) locusBase1 := locusValueObject.Base1Value locusBase2 := locusValueObject.Base2Value basePairValue := locusBase1 + ";" + locusBase2 profileMap["LocusValue_rs" + rsIDString] = basePairValue } } traitObjectsList, err := traits.GetTraitObjectsList() if (err != nil) { return err } for _, traitObject := range traitObjectsList{ traitName := traitObject.TraitName shareTraitInfoAttributeName := "ShareTraitInfo_" + traitName currentShareTraitInfoAttributeExists, currentShareTraitInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareTraitInfoAttributeName) if (err != nil) { return err } if (currentShareTraitInfoAttributeExists == false){ continue } if (currentShareTraitInfoAttribute != "Yes"){ continue } myTraitLocusValuesMap, _, _, _, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(myGeneticAnalysisObject, traitName, genomeIdentifierToShare) if (err != nil) { return err } for rsID, locusValueObject := range myTraitLocusValuesMap{ rsIDString := helpers.ConvertInt64ToString(rsID) locusBase1 := locusValueObject.Base1Value locusBase2 := locusValueObject.Base2Value basePairValue := locusBase1 + ";" + locusBase2 profileMap["LocusValue_rs" + rsIDString] = basePairValue } } return nil } err = addGeneticAnalysisToProfileMap() if (err != nil) { return nil, err } if (myProfileType != "Host"){ // We add the chat keys to the Mate/Moderator profile // Host profiles do not have chat keys identityExists, myNaclKey, myKyberKey, err := myChatKeys.GetMyNewestPublicChatKeys(myIdentityHash, networkType) if (err != nil) { return nil, err } if (identityExists == false){ return nil, errors.New("My identity not found after being found already.") } myIdentityFound, myDeviceIdentifier, err := myDevice.GetMyDeviceIdentifier(myIdentityHash, networkType) if (err != nil) { return nil, err } if (myIdentityFound == false){ return nil, errors.New("My identity not found after being found already.") } myDeviceIdentifierHex := encoding.EncodeBytesToHexString(myDeviceIdentifier[:]) myNaclKeyString := encoding.EncodeBytesToBase64String(myNaclKey[:]) myKyberKeyString := encoding.EncodeBytesToBase64String(myKyberKey[:]) profileMap["DeviceIdentifier"] = myDeviceIdentifierHex profileMap["NaclKey"] = myNaclKeyString profileMap["KyberKey"] = myKyberKeyString // We get the chatKeysLatestUpdateTime, assuming that this profile will be broadcast // We will only update the chatKeysLatestUpdateTime we have saved locally if the profile is actually broadcast getChatKeysLatestUpdateTime := func()(int64, error){ myIdentityExists, anyKeysExist, existingNaclKey, existingKyberKey, err := myChatKeys.GetMyNewestBroadcastPublicChatKeys(myIdentityHash, networkType) if (err != nil){ return 0, err } if (myIdentityExists == false){ return 0, errors.New("My identity not found after being found already.") } if (anyKeysExist == true){ if (existingNaclKey != myNaclKey || existingKyberKey != myKyberKey){ // This profile contains new keys. // Our latestChatKeysUpdateTime is now the creation time of the profile we are currently creating // We will get the time now, which may be a second older than the creationTime of the profile, which doesn't matter currentTime := time.Now().Unix() return currentTime, nil } } updateTimeExists, existingUpdateTime, err := myChatKeys.GetMyChatKeysLatestUpdateTime(myIdentityHash, networkType) if (err != nil) { return 0, err } if (updateTimeExists == false){ // We have not broadcast a profile yet. // Our current exported profile will contain the newest chat keys currentTime := time.Now().Unix() return currentTime, nil } // We have broadcasted a profile before, and the current exported profile is not broadcasting any novel chat keys return existingUpdateTime, nil } chatKeysLatestUpdateTime, err := getChatKeysLatestUpdateTime() if (err != nil) { return nil, err } chatKeysLatestUpdateTimeString := helpers.ConvertInt64ToString(chatKeysLatestUpdateTime) profileMap["ChatKeysLatestUpdateTime"] = chatKeysLatestUpdateTimeString } return profileMap, nil } profileMap, err := getNewProfileMap() if (err != nil){ return err } myIdentityExists, myIdentityPublicKey, myIdentityPrivateKey, err := myIdentity.GetMyPublicPrivateIdentityKeys(myProfileType) if (err != nil) { return err } if (myIdentityExists == false){ return errors.New("UpdateMyExportedProfile called when my identity is missing.") } profileBytes, err := createProfiles.CreateProfile(myIdentityPublicKey, myIdentityPrivateKey, profileMap) if (err != nil) { return err } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil ) { return err } exportedProfilesDirectory := filepath.Join(userDirectory, "MyExportedProfiles") networkTypeExportedProfilesDirectory := filepath.Join(exportedProfilesDirectory, "Network" + networkTypeString) writingExportedProfileFilesMutex.Lock() defer writingExportedProfileFilesMutex.Unlock() _, err = localFilesystem.CreateFolder(exportedProfilesDirectory) if (err != nil) {return err } _, err = localFilesystem.CreateFolder(networkTypeExportedProfilesDirectory) if (err != nil) {return err } profileFileName := myProfileType + "Profile.messagepack" err = localFilesystem.CreateOrOverwriteFile(profileBytes, networkTypeExportedProfilesDirectory, profileFileName) if (err != nil) { return err } return nil } //Outputs: // -bool: Profile found // -[28]byte: Profile hash // -[]byte: Profile bytes // -map[int]messagepack.RawMessage: Raw profile map // -error func GetMyExportedProfile(myProfileType string, networkType byte)(bool, [28]byte, []byte, map[int]messagepack.RawMessage, error){ if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){ return false, [28]byte{}, nil, nil, errors.New("GetMyExportedProfile called with invalid profileType: " + myProfileType) } isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, [28]byte{}, nil, nil, errors.New("GetMyExportedProfile called with invalid networkType: " + networkTypeString) } myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) if (err != nil) { return false, [28]byte{}, nil, nil, err } if (myIdentityFound == false) { return false, [28]byte{}, nil, nil, errors.New("GetMyExportedProfile called when my identity is missing.") } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil ) { return false, [28]byte{}, nil, nil, err } networkTypeString := helpers.ConvertByteToString(networkType) profileFilePath := filepath.Join(userDirectory, "MyExportedProfiles", "Network" + networkTypeString, myProfileType + "Profile.messagepack") exists, profileBytes, err := localFilesystem.GetFileContents(profileFilePath) if (err != nil) { return false, [28]byte{}, nil, nil, err } if (exists == false) { return false, [28]byte{}, nil, nil, nil } ableToRead, profileHash, _, profileNetworkType, profileAuthor, _, _, rawProfileMap, err := readProfiles.ReadProfileAndHash(true, profileBytes) if (err != nil) { return false, [28]byte{}, nil, nil, err } if (ableToRead == false){ return false, [28]byte{}, nil, nil, errors.New("MyExportedProfiles folder contains invalid profile.") } if (profileNetworkType != networkType){ return false, [28]byte{}, nil, nil, errors.New("MyExportedProfiles folder contains profile for different networkType.") } if (profileAuthor != myIdentityHash){ // This profile must be authored by an old identity. // We will not return error, instead just return profile missing // Once we update the exported profile again, this old profile will be overwritten return false, [28]byte{}, nil, nil, nil } return true, profileHash, profileBytes, rawProfileMap, nil }