seekia/internal/appUsers/appUsers.go

551 lines
15 KiB
Go

// 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
}