// appUsers provides functions to sign Seekia users in and out, and to manage Seekia users // These are the users that are displayed in the gui upon starting Seekia // Each user has its own data folder, allowing each user to operate different identities. package appUsers // The functions in this package are not safe for concurrency // The GUI should prevent any concurrent use of these functions import "seekia/resources/geneticReferences/locusMetadata" import "seekia/resources/geneticReferences/monogenicDiseases" import "seekia/resources/geneticReferences/polygenicDiseases" import "seekia/resources/geneticReferences/traits" import "seekia/resources/trainedPredictionModels" import "seekia/resources/worldLanguages" import "seekia/resources/worldLocations" import "seekia/internal/appMemory" import "seekia/internal/backgroundJobs" import "seekia/internal/desires/myLocalDesires" import "seekia/internal/encoding" import "seekia/internal/genetics/myAnalyses" import "seekia/internal/genetics/myCouples" import "seekia/internal/genetics/myGenomes" import "seekia/internal/genetics/myPeople" import "seekia/internal/localFilesystem" import "seekia/internal/logger" import "seekia/internal/messaging/myChatConversations" import "seekia/internal/messaging/myChatFilters" import "seekia/internal/messaging/myChatKeys" import "seekia/internal/messaging/myChatMessages" import "seekia/internal/messaging/myCipherKeys" import "seekia/internal/messaging/myConversationIndexes" import "seekia/internal/messaging/myMessageQueue" import "seekia/internal/messaging/myReadStatus" import "seekia/internal/messaging/mySecretInboxes" import "seekia/internal/messaging/peerChatKeys" import "seekia/internal/messaging/peerDevices" import "seekia/internal/messaging/peerSecretInboxes" import "seekia/internal/messaging/sendMessages" import "seekia/internal/moderation/myHiddenContent" import "seekia/internal/moderation/mySkippedContent" import "seekia/internal/moderation/viewedContent" import "seekia/internal/moderation/viewedModerators" import "seekia/internal/myBlockedUsers" import "seekia/internal/myContacts" import "seekia/internal/myDatastores/myList" import "seekia/internal/myDatastores/myMap" import "seekia/internal/myDatastores/myMapList" import "seekia/internal/myIgnoredUsers" import "seekia/internal/myLikedUsers" import "seekia/internal/myMatches" import "seekia/internal/myMatchScore" import "seekia/internal/mySeedPhrases" import "seekia/internal/mySettings" import "seekia/internal/network/myBroadcasts" import "seekia/internal/network/myMateCriteria" import "seekia/internal/network/peerServer" import "seekia/internal/network/viewedHosts" import "seekia/internal/profiles/myLocalProfiles" import "seekia/internal/profiles/profileFormat" import "errors" import "os" import "strings" import "sync" import "time" import "unicode" import goFilepath "path/filepath" // This bool is set to true if we have initialized application variables // These only need to be initialized once for all users each time the application is started var applicationVariablesInitialized bool = false func SignInToAppUser(userName string, startBackgroundJobs bool)error{ appMemory.SetMemoryEntry("AppUser", userName) err := createAppUserDataFolders() if (err != nil) { return err } err = initializeAppUserDatastores() if (err != nil) { return err } err = myAnalyses.PruneOldAnalyses() if (err != nil) { return err } if (applicationVariablesInitialized == false){ // This only needs to be done once per application startup err := initializeApplicationVariables() if (err != nil) { return err } applicationVariablesInitialized = true } if (startBackgroundJobs == true){ err = backgroundJobs.StartBackgroundJobs() if (err != nil) { return err } } return nil } func SignOutOfAppUser()error{ var signOutWaitgroup sync.WaitGroup stopBackgroundJobs := func(){ err := backgroundJobs.StopBackgroundJobs() if (err != nil) { //TODO: Log and show to user return } signOutWaitgroup.Done() } waitForSendsToComplete := func(){ sendMessages.WaitForPendingSendsToComplete() signOutWaitgroup.Done() } stopPeerServer := func(){ err := peerServer.StopPeerServer() if (err != nil){ //TODO: Log and show to user } signOutWaitgroup.Done() } //TODO: Wait for manual downloads and manual broadcasts to finish signOutWaitgroup.Add(3) go stopBackgroundJobs() go waitForSendsToComplete() go stopPeerServer() //TODO: Wait for other tasks that use userData folders signOutWaitgroup.Wait() // We simulate some time. This will be removed once the above functions are implemented time.Sleep(time.Second) appMemory.ClearAppMemory() return nil } func VerifyAppUserNameCharactersAreAllowed(userName string)bool{ // The user name becomes the name of a folder on the filesystem // Thus, we only allow letters and numbers const digitsList = "0123456789" for _, character := range userName { isLetter := unicode.IsLetter(character) if (isLetter == true){ continue } isDigit := strings.Contains(digitsList, string(character)) if (isDigit == true){ continue } return false } return true } func GetAppUsersList()([]string, error){ appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath() if (err != nil) { return nil, err } filesystemList, err := os.ReadDir(appUsersDirectory) if (err != nil) { return nil, err } allUsersList := make([]string, 0, len(filesystemList)) for _, filesystemObject := range filesystemList{ filepathIsDirectory := filesystemObject.IsDir() if (filepathIsDirectory == false){ return nil, errors.New("UserData folder is corrupt: Contains non-folder: " + filesystemObject.Name()) } folderName := filesystemObject.Name() isValid := VerifyAppUserNameCharactersAreAllowed(folderName) if (isValid == false){ return nil, errors.New("UserData folder is corrupt: Contains invalid folder name: " + folderName) } allUsersList = append(allUsersList, folderName) } return allUsersList, nil } //Outputs: // -bool: App user exists // -string: User name func GetCurrentAppUserName()(bool, string){ exists, appUserName := appMemory.GetMemoryEntry("AppUser") if (exists == false){ return false, "" } return true, appUserName } //Outputs: // -bool: Name is a duplicate // -error func CreateAppUser(newUserName string)(bool, error){ if (newUserName == ""){ return false, errors.New("CreateAppUser called with empty user name.") } if (len(newUserName) > 30){ return false, errors.New("CreateAppUser called with user name that is too long: " + newUserName) } isAllowed := VerifyAppUserNameCharactersAreAllowed(newUserName) if (isAllowed == false){ return false, errors.New("CreateAppUser called with user name contains unallowed characters: " + newUserName) } appUsersList, err := GetAppUsersList() if (err != nil){ return false, err } for _, existingUser := range appUsersList{ if (existingUser == newUserName){ // User name already exists return true, nil } } appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath() if (err != nil) { return false, err } userFolderpath := goFilepath.Join(appUsersDirectory, newUserName) _, err = localFilesystem.CreateFolder(userFolderpath) if (err != nil) { return false, err } // We set the IsIgnored desire // This desire is initialized upon user creation, so ignored users will be hidden // The user can disable this desire // We have to sign in to do this err = SignInToAppUser(newUserName, false) if (err != nil) { return false, err } err = myLocalDesires.SetDesire("IsIgnored_FilterAll", "Yes") if (err != nil) { return false, err } // Desire value is a "+" delimited list of base64 choices which we desire // "No" == We desire users who are not ignored noBase64 := encoding.EncodeBytesToBase64String([]byte("No")) err = myLocalDesires.SetDesire("IsIgnored", noBase64) if (err != nil) { return false, err } // We clear app memory to "sign out" of app user appMemory.ClearAppMemory() return false, nil } //Outputs: // -bool: User is found // -error func RenameAppUser(userName string, newUserName string)(bool, error){ err := SignOutOfAppUser() if (err != nil) { return false, err } appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath() if (err != nil) { return false, err } currentUserFolderpath := goFilepath.Join(appUsersDirectory, userName) newUserFolderpath := goFilepath.Join(appUsersDirectory, newUserName) err = os.Rename(currentUserFolderpath, newUserFolderpath) if (err != nil){ folderDoesNotExist := os.IsNotExist(err) if (folderDoesNotExist == true){ return false, nil } return false, err } return true, nil } //Outputs: // -bool: User found // -error func DeleteAppUser(userName string)(bool, error){ err := SignOutOfAppUser() if (err != nil) { return false, err } appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath() if (err != nil) { return false, err } userFolderPath := goFilepath.Join(appUsersDirectory, userName) folderExists, err := localFilesystem.DeleteAllFolderContents(userFolderPath) if (err != nil){ return false, err } if (folderExists == false){ return false, nil } folderExists, err = localFilesystem.DeleteFileOrFolder(userFolderPath) if (err != nil) { return false, err } if (folderExists == false){ return false, errors.New("User data folder not found after call to DeleteAllFolderContents") } return true, nil } // We use this to partially sign in to the first app user when testing packages that need it // An example is myMap, which requires an app user to be signed in to use it // We don't need to sign the user out after calling this function, because we only use this for testing func InitializeAppUserForTests(initializeAppDatastores bool, initializeAppVariables bool)error{ appUsersList, err := GetAppUsersList() if (err != nil){ return err } if (len(appUsersList) == 0){ return errors.New("SignInToAppUserForTests called when no app users exist.") } userName := appUsersList[0] appMemory.SetMemoryEntry("AppUser", userName) err = createAppUserDataFolders() if (err != nil) { return err } if (initializeAppDatastores == true){ err := initializeAppUserDatastores() if (err != nil) { return err } } if (initializeAppVariables == true){ err := initializeApplicationVariables() if (err != nil) { return err } } return nil } func initializeApplicationVariables()error{ // This only needs to be done once per application startup err := worldLocations.InitializeWorldLocationsVariables() if (err != nil) { return err } err = worldLanguages.InitializeWorldLanguageVariables() if (err != nil) { return err } err = locusMetadata.InitializeLocusMetadataVariables() if (err != nil) { return err } monogenicDiseases.InitializeMonogenicDiseaseVariables() err = polygenicDiseases.InitializePolygenicDiseaseVariables() if (err != nil) { return err } err = traits.InitializeTraitVariables() if (err != nil) { return err } err = trainedPredictionModels.InitializeTrainedPredictionModels() if (err != nil) { return err } err = profileFormat.InitializeProfileFormatVariables() if (err != nil) { return err } return nil } func createAppUserDataFolders()error{ userDirectoryPath, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } _, err = localFilesystem.CreateFolder(userDirectoryPath) if (err != nil) { return err } err = myMap.InitializeMyMapsFolder() if (err != nil) { return err } err = myList.InitializeMyListsFolder() if (err != nil) { return err } err = myMapList.InitializeMyMapListsFolder() if (err != nil) { return err } err = myBroadcasts.InitializeMyBroadcastsFolders() if (err != nil) { return err } err = myGenomes.CreateUserGenomesFolder() if (err != nil) { return err } err = myAnalyses.CreateMyAnalysesFolder() if (err != nil) { return err } return nil } func initializeAppUserDatastores()error{ err := mySeedPhrases.InitializeMySeedPhrasesDatastore() if (err != nil) { return err } err = myLocalDesires.InitializeMyDesiresDatastore() if (err != nil) { return err } err = myMateCriteria.InitializeMyCriteriaDatastore() if (err != nil) { return err } err = myMatchScore.InitializeMyMatchScorePointsDatastore() if (err != nil) { return err } err = mySettings.InitializeMySettingsDatastore() if (err != nil) { return err } err = myLocalProfiles.InitializeMyLocalProfileDatastores() if (err != nil) { return err } err = myMatches.InitializeMyMatchesDatastores() if (err != nil) { return err } err = myContacts.InitializeMyContactDatastores() if (err != nil) { return err } err = myIgnoredUsers.InitializeMyIgnoredUsersDatastore() if (err != nil) { return err } err = myBlockedUsers.InitializeMyBlockedUsersDatastore() if (err != nil) { return err } err = mySecretInboxes.InitializeMySecretInboxesDatastore() if (err != nil) { return err } err = myChatKeys.InitializeMyChatKeysDatastores() if (err != nil) { return err } err = myCipherKeys.InitializeMyMessageCipherKeysDatastore() if (err != nil) { return err } err = myReadStatus.InitializeMyReadStatusDatastore() if (err != nil) { return err } err = myConversationIndexes.InitializeMyConversationIndexesDatastore() if (err != nil) { return err } err = myChatFilters.InitializeMyChatFiltersDatastores() if (err != nil) { return err } err = myLikedUsers.InitializeMyLikedUsersDatastore() if (err != nil) { return err } err = myChatConversations.InitializeMyChatConversationsDatastores() if (err != nil) { return err } err = myChatMessages.InitializeMyChatMessageDatastores() if (err != nil) { return err } err = myMessageQueue.InitializeMyMessageQueueDatastore() if (err != nil) { return err } err = peerChatKeys.InitializePeerChatKeysDatastores() if (err != nil) { return err } err = peerSecretInboxes.InitializePeerSecretInboxesDatastore() if (err != nil) { return err } err = peerDevices.InitializePeerDevicesDatastore() if (err != nil) { return err } err = sendMessages.InitializeSentMessagesDatastore() if (err != nil) { return err } err = myGenomes.InitializeMyGenomeDatastore() if (err != nil) { return err } err = myPeople.InitializeMyGenomePeopleDatastore() if (err != nil) { return err } err = myCouples.InitializeMyGenomeCouplesDatastore() if (err != nil) { return err } err = myAnalyses.InitializeMyAnalysesDatastores() if (err != nil) { return err } err = viewedContent.InitializeViewedContentDatastores() if (err != nil) { return err } err = logger.InitializeMyLogDatastores() if (err != nil) { return err } err = viewedModerators.InitializeViewedModeratorsDatastores() if (err != nil) { return err } err = viewedHosts.InitializeViewedHostsDatastores() if (err != nil) { return err } err = mySkippedContent.InitializeMySkippedContentDatastores() if (err != nil) { return err } err = myHiddenContent.InitializeMyHiddenContentDatastores() if (err != nil) { return err } return nil }