448 lines
17 KiB
Go
448 lines
17 KiB
Go
|
|
// 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.")
|
|
}
|
|
|
|
_, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisObject)
|
|
if (err != nil) { return err }
|
|
|
|
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 }
|
|
|
|
// This map stores the rsIDs to share in our profile
|
|
// We use a map to avoid duplicates
|
|
myLociToShareMap := make(map[int64]struct{})
|
|
|
|
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
|
|
}
|
|
|
|
lociList := diseaseObject.LociList
|
|
|
|
for _, locusObject := range lociList{
|
|
|
|
locusRSID := locusObject.LocusRSID
|
|
|
|
myLociToShareMap[locusRSID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
lociList := traitObject.LociList
|
|
|
|
for _, rsID := range lociList{
|
|
|
|
myLociToShareMap[rsID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
myGenomeLocusValuesMap, exists := myGenomesMap[genomeIdentifierToShare]
|
|
if (exists == false){
|
|
return errors.New("GetMyChosenMateGeneticAnalysis returning genetic analysis which has GenomesMap which is missing my genome identifier.")
|
|
}
|
|
|
|
for rsID, _ := range myLociToShareMap{
|
|
|
|
locusValueObject, exists := myGenomeLocusValuesMap[rsID]
|
|
if (exists == false){
|
|
continue
|
|
}
|
|
|
|
rsIDString := helpers.ConvertInt64ToString(rsID)
|
|
|
|
locusBase1 := locusValueObject.Base1Value
|
|
locusBase2 := locusValueObject.Base2Value
|
|
locusIsPhased := locusValueObject.LocusIsPhased
|
|
|
|
basePairValue := locusBase1 + ";" + locusBase2
|
|
locisIsPhasedString := helpers.ConvertBoolToYesOrNoString(locusIsPhased)
|
|
|
|
locusValueAttributeName := "LocusValue_rs" + rsIDString
|
|
locusIsPhasedAttributeName := "LocusIsPhased_rs" + rsIDString
|
|
|
|
profileMap[locusValueAttributeName] = basePairValue
|
|
profileMap[locusIsPhasedAttributeName] = locisIsPhasedString
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
|
|
|
|
|
|
|