seekia/internal/profiles/myProfileExports/myProfileExports.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
}