diff --git a/FyneApp.toml b/FyneApp.toml index c3b12c7..41212ee 100644 --- a/FyneApp.toml +++ b/FyneApp.toml @@ -4,5 +4,5 @@ Website = "https://github.com/beebeeoii/lominus" Icon = "./assets/app-icon.png" Name = "Lominus" ID = "com.beebeeoii.lominus" - Version = "2.0.1" - Build = 200 + Version = "2.0.2" + Build = 202 diff --git a/README.md b/README.md index e8d5cc9..356df99 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
- + @@ -110,7 +110,7 @@ sudo make install ### Prerequisites -1. [Golang](https://go.dev/dl/) +1. [Golang >= 1.18](https://go.dev/dl/) 2. `gcc` @@ -126,7 +126,11 @@ sudo make install ### Build -1. Ensure Go is set in you system env var +1. Ensure `GOPATH` is set in your system env + + ``` bash + export GOPATH=$HOME/go + ``` 2. Install dependencies in the directory where you cloned @@ -137,18 +141,20 @@ sudo make install 3. Install [fyne](https://developer.fyne.io/index.html) ``` bash - go get fyne.io/fyne/v2/cmd/fyne + go install fyne.io/fyne/v2/cmd/fyne@latest ``` -4. Finally, build and compile +4. Ensure that your system `PATH` contains `$GOPATH/bin` before building. ``` bash + export PATH=$GOPATH/bin:$PATH fyne package ``` ## API Lominus can also be used as an API. Please visit [documentations](https://pkg.go.dev/github.com/beebeeoii/lominus) for more details. +However, do note that the documentations are lacking after v2.0.0 update due to lack of time :(. This should be fixed in due time. ### Example: Retrieving your modules diff --git a/go.mod b/go.mod index 80c1fcd..e270cff 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b github.com/mitchellh/mapstructure v1.5.0 github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.8.0 + github.com/sqweek/dialog v0.0.0-20220809060634-e981b270ebbf ) require ( @@ -27,6 +27,7 @@ require ( ) require ( + github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect @@ -37,6 +38,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44 // indirect github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41 // indirect + github.com/stretchr/testify v1.8.0 // indirect github.com/yuin/goldmark v1.4.10 // indirect golang.org/x/image v0.0.0-20220601225756-64ec528b34cd // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect diff --git a/go.sum b/go.sum index af4bd30..669c548 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93/go.mod h1:oM2AQqGJ1AMo4nNq github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I= +github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -266,6 +268,8 @@ github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t6 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/sqweek/dialog v0.0.0-20220809060634-e981b270ebbf h1:pCxn3BCfu8n8VUhYl4zS1BftoZoYY0J4qVF3dqAQ4aU= +github.com/sqweek/dialog v0.0.0-20220809060634-e981b270ebbf/go.mod h1:/qNPSY91qTz/8TgHEMioAUc6q7+3SOybeKczHMXFcXw= github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44 h1:XPYXKIuH/n5zpUoEWk2jWV/SjEMNYmqDYmTgbjmhtaI= github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= diff --git a/internal/app/app.go b/internal/app/app.go index 01e7272..704c4ed 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -9,9 +9,9 @@ import ( appDir "github.com/beebeeoii/lominus/internal/app/dir" appPref "github.com/beebeeoii/lominus/internal/app/pref" + appConstants "github.com/beebeeoii/lominus/internal/constants" "github.com/beebeeoii/lominus/internal/file" logs "github.com/beebeeoii/lominus/internal/log" - "github.com/beebeeoii/lominus/internal/lominus" ) // Init initialises and ensures log and preference files that Lominus requires are available. @@ -65,7 +65,7 @@ func Init() error { } // TODO Consider moving this to its own module in the future. - gradesPath := filepath.Join(baseDir, lominus.GRADES_FILE_NAME) + gradesPath := filepath.Join(baseDir, appConstants.GRADES_FILE_NAME) if !file.Exists(gradesPath) { gradeFileErr := file.EncodeStructToFile(gradesPath, time.Now()) diff --git a/internal/app/auth/auth.go b/internal/app/auth/auth.go index 059b121..04e5139 100644 --- a/internal/app/auth/auth.go +++ b/internal/app/auth/auth.go @@ -5,7 +5,7 @@ import ( "path/filepath" appDir "github.com/beebeeoii/lominus/internal/app/dir" - "github.com/beebeeoii/lominus/internal/lominus" + appConstants "github.com/beebeeoii/lominus/internal/constants" ) // GetJwtPath returns the file path to user's JWT data. @@ -17,7 +17,7 @@ func GetTokensPath() (string, error) { return jwtPath, retrieveBaseDirErr } - jwtPath = filepath.Join(baseDir, lominus.TOKENS_FILE_NAME) + jwtPath = filepath.Join(baseDir, appConstants.TOKENS_FILE_NAME) return jwtPath, nil } @@ -31,7 +31,7 @@ func GetCredentialsPath() (string, error) { return credentialsPath, retrieveBaseDirErr } - credentialsPath = filepath.Join(baseDir, lominus.CREDENTIALS_FILE_NAME) + credentialsPath = filepath.Join(baseDir, appConstants.CREDENTIALS_FILE_NAME) return credentialsPath, nil } diff --git a/internal/app/dir/dir.go b/internal/app/dir/dir.go index 86cd976..e1160a2 100644 --- a/internal/app/dir/dir.go +++ b/internal/app/dir/dir.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/beebeeoii/lominus/internal/lominus" + appConstants "github.com/beebeeoii/lominus/internal/constants" ) // GetBaseDir returns the directory where config files for Lominus will be stored in. @@ -23,7 +23,7 @@ func GetBaseDir() (string, error) { return baseDir, err } - baseDir = filepath.Join(userConfigDir, lominus.APP_NAME) + baseDir = filepath.Join(userConfigDir, appConstants.APP_NAME) return baseDir, nil } diff --git a/internal/app/integrations/telegram/telegram.go b/internal/app/integrations/telegram/telegram.go index 5fa610b..75e60f6 100644 --- a/internal/app/integrations/telegram/telegram.go +++ b/internal/app/integrations/telegram/telegram.go @@ -5,7 +5,7 @@ import ( "path/filepath" appDir "github.com/beebeeoii/lominus/internal/app/dir" - "github.com/beebeeoii/lominus/internal/lominus" + appConstants "github.com/beebeeoii/lominus/internal/constants" ) // GetTelegramInfoPath returns the file path to user's telegram config file. @@ -17,7 +17,7 @@ func GetTelegramInfoPath() (string, error) { return telegramInfoPath, retrieveBaseDirErr } - telegramInfoPath = filepath.Join(baseDir, lominus.TELEGRAM_FILE_NAME) + telegramInfoPath = filepath.Join(baseDir, appConstants.TELEGRAM_FILE_NAME) return telegramInfoPath, nil } diff --git a/internal/app/lock/lock.go b/internal/app/lock/lock.go index 2e8a78f..bd845c5 100644 --- a/internal/app/lock/lock.go +++ b/internal/app/lock/lock.go @@ -5,7 +5,7 @@ import ( "path/filepath" appDir "github.com/beebeeoii/lominus/internal/app/dir" - "github.com/beebeeoii/lominus/internal/lominus" + appConstants "github.com/beebeeoii/lominus/internal/constants" ) // GetLockPath returns the file path to Lominus lock file. @@ -17,7 +17,7 @@ func GetLockPath() (string, error) { return lockPath, retrieveBaseDirErr } - lockPath = filepath.Join(baseDir, lominus.LOCK_FILE_NAME) + lockPath = filepath.Join(baseDir, appConstants.LOCK_FILE_NAME) return lockPath, nil } diff --git a/internal/app/pref/pref.go b/internal/app/pref/pref.go index 58973b4..f52d1cb 100644 --- a/internal/app/pref/pref.go +++ b/internal/app/pref/pref.go @@ -5,11 +5,11 @@ import ( "path/filepath" appDir "github.com/beebeeoii/lominus/internal/app/dir" + appConstants "github.com/beebeeoii/lominus/internal/constants" "github.com/beebeeoii/lominus/internal/file" - "github.com/beebeeoii/lominus/internal/lominus" ) -const PREFERENCES_FILE_NAME = lominus.PREFERENCES_FILE_NAME +const PREFERENCES_FILE_NAME = appConstants.PREFERENCES_FILE_NAME // Preferences struct describes the data being stored in the user's preferences file. type Preferences struct { @@ -27,7 +27,7 @@ func GetPreferencesPath() (string, error) { return preferencesPath, retrieveBaseDirErr } - preferencesPath = filepath.Join(baseDir, lominus.PREFERENCES_FILE_NAME) + preferencesPath = filepath.Join(baseDir, appConstants.PREFERENCES_FILE_NAME) return preferencesPath, nil } diff --git a/internal/lominus/lominus.go b/internal/constants/lominus.go similarity index 67% rename from internal/lominus/lominus.go rename to internal/constants/lominus.go index d11b352..7f6f1b0 100644 --- a/internal/lominus/lominus.go +++ b/internal/constants/lominus.go @@ -1,9 +1,10 @@ -// Package lominus provides app config constants. -package lominus +// Package constants provide constants to be used internally within Lominus (not exported as API) +// such as UI constants. +package constants const APP_NAME = "Lominus" const APP_ID = "com.lominus.beebeeoii" -const APP_VERSION = "2.0.1" +const APP_VERSION = "2.0.2" const LOCK_FILE_NAME = "lominus.lock" diff --git a/internal/constants/ui.go b/internal/constants/ui.go index a47a70d..2111407 100644 --- a/internal/constants/ui.go +++ b/internal/constants/ui.go @@ -1,3 +1,5 @@ +// Package constants provide constants to be used internally within Lominus (not exported as API) +// such as UI constants. package constants const ( @@ -58,7 +60,7 @@ const ( TELEGRAM_DEFAULT_TEST_MESSAGE = "Thank you for using Lominus! You have succesfully integrated Telegram with Lominus!\n\nBy integrating Telegram with Lominus, you will be notified of the following whenever Lominus polls for new update based on the intervals set:\n💥 new grades releases\n💥 new announcements (TBC)" TELEGRAM_TESTING_MESSAGE = "Please wait while we send you a test message..." TELEGRAM_TESTING_SUCCESSFUL_MESSAGE = "Telegram integration successful!" - TELEGRAM_TESTING_FAILED_MESSAGE = "Telegram integration failed. Please ensure that you have chatted with your bot before." + TELEGRAM_TESTING_FAILED_MESSAGE = "Telegram integration failed.\nPlease ensure that you have chatted with your bot before." SAVE_TELEGRAM_DATA_TEXT = "Save Telegram Info" // General diff --git a/internal/cron/cron.go b/internal/cron/cron.go index a9ef76e..1e6c0c0 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -11,10 +11,10 @@ import ( appDir "github.com/beebeeoii/lominus/internal/app/dir" intTelegram "github.com/beebeeoii/lominus/internal/app/integrations/telegram" appPref "github.com/beebeeoii/lominus/internal/app/pref" + appConstants "github.com/beebeeoii/lominus/internal/constants" appFiles "github.com/beebeeoii/lominus/internal/file" "github.com/beebeeoii/lominus/internal/indexing" logs "github.com/beebeeoii/lominus/internal/log" - "github.com/beebeeoii/lominus/internal/lominus" "github.com/beebeeoii/lominus/internal/notifications" "github.com/beebeeoii/lominus/pkg/api" "github.com/beebeeoii/lominus/pkg/auth" @@ -173,64 +173,47 @@ func createJob(frequency int) (*gocron.Job, error) { return } - canvasFolders := []api.Folder{} + lmsFiles := []api.File{} for _, module := range canvasModules { - // TODO Check if it is even possible for files to be in module's root folder - folders, canvasFoldersErr := getFolders(tokensData.CanvasToken.CanvasApiToken, constants.Canvas, module) - if canvasFoldersErr != nil { - // TODO Somehow collate this error and display to user at the end - // notifications.NotificationChannel <- notifications.Notification{Title: "Sync", Content: canvasFoldersErr.Error()} - logs.Logger.Errorln(canvasFoldersErr) + foldersReq, foldersReqErr := api.BuildFoldersRequest( + tokensData.CanvasToken.CanvasApiToken, + constants.Canvas, + module, + ) + if foldersReqErr != nil { + logs.Logger.Errorln(foldersReqErr) } - canvasFolders = append(canvasFolders, folders...) - } - luminusFolders := []api.Folder{} - for _, module := range luminusModules { - // This ensures that files in the module's root folder are downloaded as well. - moduleMainFolder := api.Folder{ - Id: module.Id, - Name: module.Name, - Downloadable: module.IsAccessible, - HasSubFolder: true, // doesn't matter - Ancestors: []string{}, // main folder does not have any ancestors + files, foldersErr := foldersReq.GetRootFiles() + if foldersErr != nil { + logs.Logger.Errorln(foldersErr) } - folders, luminusFoldersErr := getFolders(tokensData.LuminusToken.JwtToken, constants.Luminus, module) - if luminusFoldersErr != nil { - // TODO Somehow collate this error and display to user at the end - // notifications.NotificationChannel <- notifications.Notification{Title: "Sync", Content: luminusFoldersErr.Error()} - logs.Logger.Errorln(luminusFoldersErr) - } - luminusFolders = append(luminusFolders, moduleMainFolder) - luminusFolders = append(luminusFolders, folders...) + + lmsFiles = append(lmsFiles, files...) } - nFilesToUpdate := 0 - filesUpdated := []api.File{} + for _, module := range luminusModules { + foldersReq, foldersReqErr := api.BuildFoldersRequest( + tokensData.LuminusToken.JwtToken, + constants.Luminus, + module, + ) + if foldersReqErr != nil { + logs.Logger.Errorln(foldersReqErr) + } - files := []api.File{} - for _, folder := range canvasFolders { - canvasFiles, canvasFilesErr := getFiles(tokensData.CanvasToken.CanvasApiToken, constants.Canvas, folder) - if canvasFilesErr != nil { - // TODO Somehow collate this error and display to user at the end - // notifications.NotificationChannel <- notifications.Notification{Title: "Sync", Content: canvasFilesErr.Error()} - logs.Logger.Errorln(canvasFilesErr) + files, foldersErr := foldersReq.GetRootFiles() + if foldersErr != nil { + logs.Logger.Errorln(foldersErr) } - files = append(files, canvasFiles...) + lmsFiles = append(lmsFiles, files...) } - for _, folder := range luminusFolders { - luminusFiles, luminusFilesErr := getFiles(tokensData.LuminusToken.JwtToken, constants.Luminus, folder) - if luminusFilesErr != nil { - // TODO Somehow collate this error and display to user at the end - // notifications.NotificationChannel <- notifications.Notification{Title: "Sync", Content: luminusFilesErr.Error()} - logs.Logger.Errorln(luminusFilesErr) - } - files = append(files, luminusFiles...) - } + nFilesToUpdate := 0 + filesUpdated := []api.File{} - for _, file := range files { + for _, file := range lmsFiles { key := fmt.Sprintf("%s/%s", strings.Join(file.Ancestors, "/"), file.Name) localLastUpdated := currentFiles[key].LastUpdated platformLastUpdated := file.LastUpdated @@ -293,6 +276,8 @@ func createJob(frequency int) (*gocron.Job, error) { }) } +// loadTokensData is a helper function that retrieves locally stored Tokens +// data into a TokensData object. func loadTokensData() (auth.TokensData, error) { var tokensData auth.TokensData @@ -309,6 +294,8 @@ func loadTokensData() (auth.TokensData, error) { return tokensData, nil } +// getModules is a helper function that retrieves Module objects based on the platform +// passed in the arguments. func getModules(token string, platform constants.Platform) ([]api.Module, error) { modules := []api.Module{} @@ -325,45 +312,14 @@ func getModules(token string, platform constants.Platform) ([]api.Module, error) return modules, nil } -func getFolders(token string, platform constants.Platform, module api.Module) ([]api.Folder, error) { - folders := []api.Folder{} - - foldersReq, foldersReqErr := api.BuildFoldersRequest(token, platform, module) - if foldersReqErr != nil { - return folders, foldersReqErr - } - - folders, foldersErr := foldersReq.GetFolders() - if foldersErr != nil { - return folders, foldersErr - } - - return folders, nil -} - -func getFiles(token string, platform constants.Platform, folder api.Folder) ([]api.File, error) { - files := []api.File{} - - filesReq, filesReqErr := api.BuildFilesRequest(token, platform, folder) - if filesReqErr != nil { - return files, filesReqErr - } - - files, filesErr := filesReq.GetFiles() - if filesErr != nil { - return files, filesErr - } - - return files, nil -} - +// getGrades is a helper function that retrieves Grade objects for Luminus LMS. func getGrades(modules []api.Module) ([]api.Grade, error) { grades := []api.Grade{} var lastSync time.Time baseDir, _ := appDir.GetBaseDir() - existingGradeErr := appFiles.DecodeStructFromFile(filepath.Join(baseDir, lominus.GRADES_FILE_NAME), &lastSync) + existingGradeErr := appFiles.DecodeStructFromFile(filepath.Join(baseDir, appConstants.GRADES_FILE_NAME), &lastSync) if existingGradeErr != nil { return grades, existingGradeErr } @@ -386,7 +342,7 @@ func getGrades(modules []api.Module) ([]api.Grade, error) { grades = append(grades, allGrades...) } - err := appFiles.EncodeStructToFile(filepath.Join(baseDir, lominus.GRADES_FILE_NAME), time.Now()) + err := appFiles.EncodeStructToFile(filepath.Join(baseDir, appConstants.GRADES_FILE_NAME), time.Now()) if err != nil { return []api.Grade{}, err } diff --git a/internal/log/logs.go b/internal/log/logs.go index ab7d0c7..9f1634f 100644 --- a/internal/log/logs.go +++ b/internal/log/logs.go @@ -9,7 +9,7 @@ import ( appDir "github.com/beebeeoii/lominus/internal/app/dir" appPref "github.com/beebeeoii/lominus/internal/app/pref" - "github.com/beebeeoii/lominus/internal/lominus" + appConstants "github.com/beebeeoii/lominus/internal/constants" log "github.com/sirupsen/logrus" ) @@ -91,7 +91,7 @@ func getLogPath() (string, error) { return logPath, retrieveBaseDirErr } - logPath = filepath.Join(baseDir, lominus.LOG_FILE_NAME) + logPath = filepath.Join(baseDir, appConstants.LOG_FILE_NAME) return logPath, nil } diff --git a/internal/ui/credentials.go b/internal/ui/credentials.go index 540fc74..42f1289 100644 --- a/internal/ui/credentials.go +++ b/internal/ui/credentials.go @@ -1,3 +1,4 @@ +// Package ui provides primitives that initialises the UI. package ui import ( @@ -9,7 +10,6 @@ import ( appConstants "github.com/beebeeoii/lominus/internal/constants" "github.com/beebeeoii/lominus/internal/file" logs "github.com/beebeeoii/lominus/internal/log" - "github.com/beebeeoii/lominus/internal/lominus" "github.com/beebeeoii/lominus/pkg/auth" ) @@ -59,6 +59,7 @@ func getCredentialsTab(parentWindow fyne.Window) (*container.TabItem, error) { return tab, nil } +// getLuminusView builds the view for Luminus credentials placed in the credentials tab. func getLuminusView( parentWindow fyne.Window, defaultCredentials auth.LuminusCredentials, @@ -96,7 +97,7 @@ func getLuminusView( progressBar := widget.NewProgressBarInfinite() mainDialog := dialog.NewCustom( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.CANCEL_TEXT, container.NewVBox(status, progressBar), parentWindow, @@ -109,7 +110,7 @@ func getLuminusView( if err != nil { logs.Logger.Debugln("verfication failed") dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.VERIFICATION_FAILED_MESSAGE, parentWindow, ).Show() @@ -117,7 +118,7 @@ func getLuminusView( logs.Logger.Debugln("verfication succesful - saving credentials") luminusCredentials.Save(credentialsPath) dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.VERIFICATION_SUCCESSFUL_MESSAGE, parentWindow, ).Show() @@ -133,6 +134,7 @@ func getLuminusView( ), nil } +// getCanvasView builds the view for Canvas credentials placed in the credentials tab. func getCanvasView( parentWindow fyne.Window, defaultCredentials auth.CanvasCredentials, @@ -168,7 +170,7 @@ func getCanvasView( progressBar := widget.NewProgressBarInfinite() mainDialog := dialog.NewCustom( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.CANCEL_TEXT, container.NewVBox(status, progressBar), parentWindow, @@ -181,7 +183,7 @@ func getCanvasView( if err != nil { logs.Logger.Debugln("verfication failed") dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.VERIFICATION_FAILED_MESSAGE, parentWindow, ).Show() @@ -190,7 +192,7 @@ func getCanvasView( canvasCredentials.Save(credentialsPath) canvasTokens.Save(tokensPath) dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.VERIFICATION_SUCCESSFUL_MESSAGE, parentWindow, ).Show() diff --git a/internal/ui/integrations.go b/internal/ui/integrations.go index e56005d..df47223 100644 --- a/internal/ui/integrations.go +++ b/internal/ui/integrations.go @@ -1,3 +1,4 @@ +// Package ui provides primitives that initialises the UI. package ui import ( @@ -11,7 +12,6 @@ import ( intTelegram "github.com/beebeeoii/lominus/internal/app/integrations/telegram" "github.com/beebeeoii/lominus/internal/file" logs "github.com/beebeeoii/lominus/internal/log" - "github.com/beebeeoii/lominus/internal/lominus" "github.com/beebeeoii/lominus/pkg/integrations/telegram" appConstants "github.com/beebeeoii/lominus/internal/constants" @@ -63,7 +63,7 @@ func getIntegrationsTab(parentWindow fyne.Window) (*container.TabItem, error) { progressBar := widget.NewProgressBarInfinite() mainDialog := dialog.NewCustom( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.CANCEL_TEXT, container.NewVBox(status, progressBar), parentWindow, @@ -81,7 +81,7 @@ func getIntegrationsTab(parentWindow fyne.Window) (*container.TabItem, error) { ) logs.Logger.Errorln(errMessage) dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.TELEGRAM_TESTING_FAILED_MESSAGE, parentWindow, ).Show() @@ -92,7 +92,7 @@ func getIntegrationsTab(parentWindow fyne.Window) (*container.TabItem, error) { ) logs.Logger.Debugln("telegram test message sent successfully") dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.TELEGRAM_TESTING_SUCCESSFUL_MESSAGE, parentWindow, ).Show() diff --git a/internal/ui/preferences.go b/internal/ui/preferences.go index 4976519..9ff8771 100644 --- a/internal/ui/preferences.go +++ b/internal/ui/preferences.go @@ -1,3 +1,4 @@ +// Package ui provides primitives that initialises the UI. package ui import ( @@ -8,12 +9,12 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" - "github.com/beebeeoii/lominus/internal/lominus" appDir "github.com/beebeeoii/lominus/internal/app/dir" appPref "github.com/beebeeoii/lominus/internal/app/pref" appConstants "github.com/beebeeoii/lominus/internal/constants" logs "github.com/beebeeoii/lominus/internal/log" + fileDialog "github.com/sqweek/dialog" ) var frequencyMap = map[int]string{ @@ -50,6 +51,8 @@ func getPreferencesTab(parentWindow fyne.Window) (*container.TabItem, error) { return tab, nil } +// getFileDirectoryView builds the view for choosing folder directory for LMS files +// to be stored locally. It is placed in the Preferences tab. func getFileDirectoryView(parentWindow fyne.Window) (fyne.CanvasObject, error) { logs.Logger.Debugln("file directory view loaded") @@ -67,63 +70,57 @@ func getFileDirectoryView(parentWindow fyne.Window) (fyne.CanvasObject, error) { folderPathLabel := widget.NewLabel(dir) folderPathLabel.Wrapping = fyne.TextWrapWord chooseDirButton := widget.NewButton(appConstants.FILE_DIRECTORY_SELECT_DIRECTORY_TEXT, func() { - fileDialog := dialog.NewFolderOpen(func(lu fyne.ListableURI, dirErr error) { - if dirErr != nil { + dir, dirErr := fileDialog.Directory().Title( + appConstants.FILE_DIRECTORY_SELECT_DIRECTORY_TEXT, + ).Browse() + + if dirErr != nil { + if dirErr.Error() != "Cancelled" { logs.Logger.Debugln("directory selection cancelled") dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.PREFERENCES_FAILED_MESSAGE, parentWindow, ).Show() logs.Logger.Errorln(dirErr) - return - } - - if lu == nil { - return } + return + } + logs.Logger.Debugf("directory chosen - %s", dir) - dir = lu.Path() - - logs.Logger.Debugf("directory chosen - %s", dir) - - preferences := getPreferences() - preferences.Directory = dir + preferences := getPreferences() + preferences.Directory = dir - preferencesPath, getPreferencesPathErr := appPref.GetPreferencesPath() - if getPreferencesPathErr != nil { - dialog.NewInformation( - lominus.APP_NAME, - appConstants.PREFERENCES_FAILED_MESSAGE, - parentWindow, - ).Show() - logs.Logger.Errorln(getPreferencesPathErr) - return - } + preferencesPath, getPreferencesPathErr := appPref.GetPreferencesPath() + if getPreferencesPathErr != nil { + dialog.NewInformation( + appConstants.APP_NAME, + appConstants.PREFERENCES_FAILED_MESSAGE, + parentWindow, + ).Show() + logs.Logger.Errorln(getPreferencesPathErr) + return + } - savePrefErr := appPref.SavePreferences(preferencesPath, preferences) - if savePrefErr != nil { - dialog.NewInformation( - lominus.APP_NAME, - appConstants.PREFERENCES_FAILED_MESSAGE, - parentWindow, - ).Show() - logs.Logger.Errorln(savePrefErr) - return - } - logs.Logger.Debugln("directory saved") - folderPathLabel.SetText(preferences.Directory) - }, parentWindow) - fileDialog.SetConfirmText(appConstants.FILE_DIRECTORY_CHOOSE_LOCATION_TEXT) - fileDialog.Resize( - w.Canvas().Size().SubtractWidthHeight(appConstants.DIALOG_PADDING, appConstants.DIALOG_PADDING), - ) - fileDialog.Show() + savePrefErr := appPref.SavePreferences(preferencesPath, preferences) + if savePrefErr != nil { + dialog.NewInformation( + appConstants.APP_NAME, + appConstants.PREFERENCES_FAILED_MESSAGE, + parentWindow, + ).Show() + logs.Logger.Errorln(savePrefErr) + return + } + logs.Logger.Debugln("directory saved") + folderPathLabel.SetText(preferences.Directory) }) return container.NewVBox(label, widget.NewSeparator(), folderPathLabel, chooseDirButton), nil } +// getSyncView builds the view for choosing frequency of sync for LMS files. +// It is placed in the Preferences tab. func getSyncView(parentWindow fyne.Window) (fyne.CanvasObject, error) { logs.Logger.Debugln("sync view loaded") @@ -166,7 +163,7 @@ func getSyncView(parentWindow fyne.Window) (fyne.CanvasObject, error) { preferencesPath, getPreferencesPathErr := appPref.GetPreferencesPath() if getPreferencesPathErr != nil { dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.PREFERENCES_FAILED_MESSAGE, parentWindow, ).Show() @@ -177,7 +174,7 @@ func getSyncView(parentWindow fyne.Window) (fyne.CanvasObject, error) { savePrefErr := appPref.SavePreferences(preferencesPath, preferences) if savePrefErr != nil { dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.PREFERENCES_FAILED_MESSAGE, parentWindow, ).Show() @@ -191,6 +188,8 @@ func getSyncView(parentWindow fyne.Window) (fyne.CanvasObject, error) { return container.NewVBox(label, widget.NewSeparator(), description, frequencySelect), nil } +// getAdvancedView builds the view for advanced options such as debug mode. +// It is placed in the Preferences tab. func getAdvancedView(parentWindow fyne.Window) (fyne.CanvasObject, error) { logs.Logger.Debugln("advanced view loaded") @@ -212,7 +211,7 @@ func getAdvancedView(parentWindow fyne.Window) (fyne.CanvasObject, error) { fmt.Sprintf( appConstants.DEBUG_CHECKBOX_W_LINK_DESCRIPTION, filepath.FromSlash( - fmt.Sprintf("file://%s", filepath.Join(baseDir, lominus.LOG_FILE_NAME)), + fmt.Sprintf("file://%s", filepath.Join(baseDir, appConstants.LOG_FILE_NAME)), ), ), ) @@ -225,7 +224,7 @@ func getAdvancedView(parentWindow fyne.Window) (fyne.CanvasObject, error) { preferencesPath, getPreferencesPathErr := appPref.GetPreferencesPath() if getPreferencesPathErr != nil { dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.PREFERENCES_FAILED_MESSAGE, parentWindow, ).Show() @@ -245,7 +244,7 @@ func getAdvancedView(parentWindow fyne.Window) (fyne.CanvasObject, error) { savePrefErr := appPref.SavePreferences(preferencesPath, preferences) if savePrefErr != nil { dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.PREFERENCES_FAILED_MESSAGE, parentWindow, ).Show() @@ -254,7 +253,7 @@ func getAdvancedView(parentWindow fyne.Window) (fyne.CanvasObject, error) { } dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.DEBUG_TOGGLE_SUCCESSFUL_MESSAGE, parentWindow, ).Show() diff --git a/internal/ui/systray.go b/internal/ui/systray.go index 816e972..dcbf9f9 100644 --- a/internal/ui/systray.go +++ b/internal/ui/systray.go @@ -3,14 +3,15 @@ package ui import ( "fyne.io/fyne/v2" + appConstants "github.com/beebeeoii/lominus/internal/constants" "github.com/beebeeoii/lominus/internal/cron" - "github.com/beebeeoii/lominus/internal/lominus" "github.com/beebeeoii/lominus/internal/notifications" ) -// TODO Documentation +// BuildSystemTray creates the system tray icon and its menu options, used to be initialised +// when Lominus starts. func BuildSystemTray() *fyne.Menu { - return fyne.NewMenu(lominus.APP_NAME, + return fyne.NewMenu(appConstants.APP_NAME, fyne.NewMenuItem("Sync Now", func() { preferences := getPreferences() if preferences.Directory == "" { diff --git a/internal/ui/ui.go b/internal/ui/ui.go index e1c7aa9..33dde1f 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -16,7 +16,6 @@ import ( appConstants "github.com/beebeeoii/lominus/internal/constants" "github.com/beebeeoii/lominus/internal/cron" logs "github.com/beebeeoii/lominus/internal/log" - "github.com/beebeeoii/lominus/internal/lominus" "github.com/beebeeoii/lominus/internal/notifications" ) @@ -25,7 +24,7 @@ var w fyne.Window // Init builds and initialises the UI. func Init() error { - mainApp = app.NewWithID(lominus.APP_NAME) + mainApp = app.NewWithID(appConstants.APP_NAME) mainApp.SetIcon(resourceAppIconPng) go func() { @@ -35,7 +34,7 @@ func Init() error { } }() - w = mainApp.NewWindow(fmt.Sprintf("%s v%s", lominus.APP_NAME, lominus.APP_VERSION)) + w = mainApp.NewWindow(fmt.Sprintf("%s v%s", appConstants.APP_NAME, appConstants.APP_VERSION)) if desk, ok := mainApp.(desktop.App); ok { m := BuildSystemTray() @@ -100,7 +99,7 @@ func getSyncButton(parentWindow fyne.Window) *widget.Button { preferences := getPreferences() if preferences.Directory == "" { dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.NO_FOLDER_DIRECTORY_SELECTED, parentWindow, ).Show() @@ -109,7 +108,7 @@ func getSyncButton(parentWindow fyne.Window) *widget.Button { if preferences.Frequency == -1 { dialog.NewInformation( - lominus.APP_NAME, + appConstants.APP_NAME, appConstants.NO_FREQUENCY_SELECTED, parentWindow, ).Show() diff --git a/pkg/api/files.go b/pkg/api/files.go index b642d4e..841539c 100644 --- a/pkg/api/files.go +++ b/pkg/api/files.go @@ -1,4 +1,5 @@ -// Package api provides functions that link up and communicate with Luminus servers. +// Package api provides functions that link up and communicate with LMS servers, +// such as Canvas and Luminus (probably removed in near future). package api import ( @@ -18,20 +19,23 @@ import ( "github.com/mitchellh/mapstructure" ) -// Folder struct is the datapack for containing details about a Folder -// Ancestors the relative folders that precedes the current folder, including itself. -// Eg. Ancestors for a folder with the path: /folder1/folder2/folder3 is ['folder1', 'folder2', 'folder3'] +// Folder struct is the datapack for containing details about a Folder. +// Ancestors describe the relative folders that precedes the current folder, exclusing itself. +// Eg. Ancestors for a folder with the path: /MA2001/Lectures/Week1 is ['MA2001', 'Lectures']. +// IsRootFolder = true if the folder is the root for a module. Root folders are expected to +// be named the module code. type Folder struct { Id string Name string Downloadable bool HasSubFolder bool Ancestors []string + IsRootFolder bool } -// File struct is the datapack for containing details about a File -// Ancestors the relative folders that precedes the current folder, including itself. -// Eg. Ancestors for a file with the path: /folder1/folder2/file.pdf is ['folder1', 'folder2', 'file.pdf'] +// File struct is the datapack for containing details about a File. +// Ancestors describe the relative folders that precedes the current file, excluding itself. +// Eg. Ancestors for a file with the path: /MA2001/Lectures/Lecture1.pdf is ['MA2001', 'Lectures']. type File struct { Id string Name string @@ -44,7 +48,9 @@ const FOLDER_URL_ENDPOINT = "https://luminus.nus.edu.sg/v2/api/files/?populate=t const FILE_URL_ENDPOINT = "https://luminus.nus.edu.sg/v2/api/files/%s/file?populate=Creator,lastUpdatedUser,comment" const DOWNLOAD_URL_ENDPOINT = "https://luminus.nus.edu.sg/v2/api/files/file/%s/downloadurl" -// TODO Documentation +// GetFolders returns a slice of Folder objects from a given FoldersRequest. +// Only the folders in the current Folder/Module (via the builder) provided +// in the FoldersRequest will be returned. In other words, nested folders will not be included. func (foldersRequest FoldersRequest) GetFolders() ([]Folder, error) { folders := []Folder{} ancestors := []string{} @@ -78,20 +84,24 @@ func (foldersRequest FoldersRequest) GetFolders() ([]Folder, error) { for _, folderObject := range response { // All the folders and files of a module are stored under the "course files" folder. - // We do not want to get that folder as we just want the folders and files in that + // We do not want to download that folder as we just want the folders and files in that // folder. // // The "course files" folder resembles the 'home directory' of a module. + downloadable := !folderObject.HiddenForUser + if folderObject.FullName == "course files" { - continue + downloadable = false } folders = append(folders, Folder{ Id: strconv.Itoa(folderObject.Id), Name: appFile.CleanseFolderFileName(folderObject.Name), - Downloadable: !folderObject.HiddenForUser, + Downloadable: downloadable, HasSubFolder: folderObject.FoldersCount > 0, Ancestors: ancestors, + IsRootFolder: folderObject.ParentFolderId == 0 && + folderObject.FullName == "course files", }) } case constants.Luminus: @@ -127,6 +137,7 @@ func (foldersRequest FoldersRequest) GetFolders() ([]Folder, error) { Downloadable: folderObject.IsActive && !folderObject.AllowUpload, HasSubFolder: folderObject.FoldersCount > 0, Ancestors: ancestors, + IsRootFolder: false, }) } default: @@ -136,6 +147,116 @@ func (foldersRequest FoldersRequest) GetFolders() ([]Folder, error) { return folders, nil } +// GetRootFiles is a recursive function that returns a slice of File objects and nested +// File objects that are in a Folder. +// Note that it will traverse all nested folders and return all nested files. +func (foldersRequest FoldersRequest) GetRootFiles() ([]File, error) { + files := []File{} + + if foldersRequest.Request.Token == "" { + return files, nil + } + + switch builder := foldersRequest.Builder.(type) { + case Module: + // Module exists but its contents are restricted to be downloaded. + if !builder.IsAccessible { + return files, nil + } + + // Retrieving of folders in main folder is only required for Luminus + // as Canvas already returns its + if foldersRequest.Request.Url.Platform != constants.Luminus { + break + } + + moduleMainFolder := Folder{ + Id: builder.Id, + Name: builder.Name, + Downloadable: true, + HasSubFolder: true, // doesn't matter + Ancestors: []string{}, // main folder does not have any ancestors + } + subFilesReq, subFilesReqErr := BuildFilesRequest( + foldersRequest.Request.Token, + foldersRequest.Request.Url.Platform, + moduleMainFolder, + ) + if subFilesReqErr != nil { + return files, subFilesReqErr + } + + subFiles, subFilesErr := subFilesReq.GetFiles() + if subFilesErr != nil { + return files, subFilesErr + } + + files = append(files, subFiles...) + case Folder: + // Folder exists but its contents are restricted to be downloaded. + if !builder.Downloadable { + return files, nil + } + + subFilesReq, subFilesReqErr := BuildFilesRequest( + foldersRequest.Request.Token, + foldersRequest.Request.Url.Platform, + builder, + ) + if subFilesReqErr != nil { + return files, subFilesReqErr + } + + subFiles, subFilesErr := subFilesReq.GetFiles() + if subFilesErr != nil { + return files, subFilesErr + } + + files = append(files, subFiles...) + + if !builder.HasSubFolder { + break + } + } + + subFoldersReq, subFoldersReqErr := BuildFoldersRequest( + foldersRequest.Request.Token, + foldersRequest.Request.Url.Platform, + foldersRequest.Builder, + ) + if subFoldersReqErr != nil { + return files, subFoldersReqErr + } + + subFolders, subFoldersErr := subFoldersReq.GetFolders() + if subFoldersErr != nil { + return files, subFoldersErr + } + + for _, subFolder := range subFolders { + nestedFoldersReq, nestedFoldersReqErr := BuildFoldersRequest( + foldersRequest.Request.Token, + foldersRequest.Request.Url.Platform, + subFolder, + ) + if nestedFoldersReqErr != nil { + return files, nestedFoldersReqErr + } + + nestedFiles, nestedFilesErr := nestedFoldersReq.GetRootFiles() + if nestedFilesErr != nil { + return files, nestedFilesErr + } + + files = append(files, nestedFiles...) + } + + return files, nil +} + +// GetFiles returns a slice of File objects from a given FilesRequest. +// Only the files in the current Folder provided in the FilesRequest will be returned. +// In other words, nested files will not be included. func (filesRequest FilesRequest) GetFiles() ([]File, error) { files := []File{} @@ -147,6 +268,15 @@ func (filesRequest FilesRequest) GetFiles() ([]File, error) { switch folderDataType := filesRequest.Request.Url.Platform; folderDataType { case constants.Canvas: + // All the folders and files of a module are stored under the "course files" folder. + // We do not want to get that folder as we just want the folders and files in that + // folder. + // + // The "course files" folder resembles the 'home directory' of a module. + if filesRequest.Folder.IsRootFolder { + ancestors = filesRequest.Folder.Ancestors + } + response := []interfaces.CanvasFileObject{} reqErr := filesRequest.Request.Send(&response) if reqErr != nil { @@ -161,7 +291,7 @@ func (filesRequest FilesRequest) GetFiles() ([]File, error) { files = append(files, File{ Id: strconv.Itoa(fileObject.Id), - Name: appFile.CleanseFolderFileName(fileObject.Name), + Name: appFile.CleanseFolderFileName(fileObject.DisplayName), LastUpdated: lastUpdated, Ancestors: ancestors, DownloadUrl: fileObject.Url, @@ -224,6 +354,8 @@ func (filesRequest FilesRequest) GetFiles() ([]File, error) { return files, nil } +// Download downloads the given file via the DownloadUrl of the File object. +// The downloaded file will be placed in the folderPath specified in the parameter. func (file File) Download(folderPath string) error { if file.DownloadUrl == "" { return errors.New("file.DownloadUrl is empty") diff --git a/pkg/api/grades.go b/pkg/api/grades.go index 3fb0fde..9427b51 100644 --- a/pkg/api/grades.go +++ b/pkg/api/grades.go @@ -1,4 +1,5 @@ -// Package api provides functions that link up and communicate with Luminus servers. +// Package api provides functions that link up and communicate with LMS servers, +// such as Canvas and Luminus (probably removed in near future). package api import ( @@ -31,6 +32,7 @@ func getScoreDetailFieldsRequired() []string { // GetGrades retrieves all grades for a particular module represented by moduleCode specified in GradeRequest. // Find out more about GradeRequests under request.go. +// Note that Grades API works only for Luminus and not supported for Canvas yet. func (req GradeRequest) GetGrades() ([]Grade, error) { var grades []Grade diff --git a/pkg/api/modules.go b/pkg/api/modules.go index 3c1704d..7173e50 100644 --- a/pkg/api/modules.go +++ b/pkg/api/modules.go @@ -1,4 +1,5 @@ -// Package api provides functions that link up and communicate with Luminus servers. +// Package api provides functions that link up and communicate with LMS servers, +// such as Canvas and Luminus (probably removed in near future). package api import ( @@ -22,7 +23,8 @@ type Module struct { const MODULE_URL_ENDPOINT = "https://luminus.nus.edu.sg/v2/api/module/?populate=Creator,termDetail,isMandatory" -// TODO Documentation +// GetModules retrieves all the modules being taken by the user on the specified LMS +// via a ModulesRequest. func (modulesRequest ModulesRequest) GetModules() ([]Module, error) { modules := []Module{} if modulesRequest.Request.Token == "" { @@ -83,7 +85,8 @@ func (modulesRequest ModulesRequest) GetModules() ([]Module, error) { return modules, nil } -// TODO Documentation +// cleanseModuleCode is a helper function that replaces all instances of "/" with "-". +// This is necessary for multi-coded modules like ST2131/MA2216. func cleanseModuleCode(code string) string { return strings.Replace(code, "/", "-", -1) } diff --git a/pkg/api/request.go b/pkg/api/request.go index 4531e0d..404e9f8 100644 --- a/pkg/api/request.go +++ b/pkg/api/request.go @@ -1,4 +1,5 @@ -// Package api provides functions that link up and communicate with Luminus servers. +// Package api provides functions that link up and communicate with LMS servers, +// such as Canvas and Luminus (probably removed in near future). package api import ( @@ -22,33 +23,42 @@ type Request struct { UserAgent string } -// GradeRequest struct is the datapack for containing details about a specific HTTP request used for grades (Luminus Gradebook). +// GradeRequest struct is the datapack for containing details about a specific +// HTTP request used for grades (Luminus Gradebook only). type GradeRequest struct { Module Module Request Request } +// ModulesRequest struct is the datapack for containing details about a specific +// HTTP request used for retrieving all the modules taken by the user. type ModulesRequest struct { Request Request } +// FoldersRequest struct is the datapack for containing details about a specific +// HTTP request used for retrieving folders in a module's uploaded files on Luminus/Canvas. type FoldersRequest struct { Request Request Builder interface{} } +// FilesRequest struct is the datapack for containing details about a specific +// HTTP request used for retrieving files in a module's uploaded files on Luminus/Canvas. type FilesRequest struct { Request Request Folder Folder } -// MultimediaChannelRequest struct is the datapack for containing details about a specific HTTP request used for multimedia channels (Luminus Multimedia). +// MultimediaChannelRequest struct is the datapack for containing details about a +// specific HTTP request used for multimedia channels (Luminus Multimedia). type MultimediaChannelRequest struct { Module Module Request Request } -// MultimediaVideoRequest struct is the datapack for containing details about a specific HTTP request used for multimedia video (Luminus Multimedia). +// MultimediaVideoRequest struct is the datapack for containing details about a +// specific HTTP request used for multimedia video (Luminus Multimedia). type MultimediaVideoRequest struct { MultimediaChannel MultimediaChannel Request Request @@ -60,7 +70,8 @@ const GET_METHOD = "GET" const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded" const CONTENT_TYPE_JSON = "application/json; charset=UTF-8" -// TODO Documentations +// BuildModulesRequest builds and returns a ModulesRequest that can be used to retrieve +// all modules taken by a user. func BuildModulesRequest(token string, platform constants.Platform) (ModulesRequest, error) { var url string @@ -86,26 +97,66 @@ func BuildModulesRequest(token string, platform constants.Platform) (ModulesRequ }, nil } -// TODO Documentations +// BuildFoldersRequest builds and returns a FoldersRequest that can be used for Folder related +// operations such as retrieving folders of a module. func BuildFoldersRequest(token string, platform constants.Platform, builder interface{}) (FoldersRequest, error) { var url string - switch builder := builder.(type) { + switch b := builder.(type) { case Module: switch p := platform; p { case constants.Canvas: - url = fmt.Sprintf(constants.CANVAS_MODULE_FOLDERS_ENDPOINT, builder.Id) + url = fmt.Sprintf(constants.CANVAS_MODULE_FOLDERS_ENDPOINT, b.Id) + folderRequest := FoldersRequest{ + Request: Request{ + Method: GET_METHOD, + Token: token, + Url: interfaces.Url{ + Url: url, + Platform: platform, + }, + UserAgent: USER_AGENT, + }, + Builder: b, + } + + folders, foldersErr := folderRequest.GetFolders() + if foldersErr != nil { + return folderRequest, foldersErr + } + + var rootFolderId string + for _, folder := range folders { + if folder.Name == "course files" { + rootFolderId = folder.Id + break + } + } + + if rootFolderId == "" { + return folderRequest, foldersErr + } + + url = fmt.Sprintf(constants.CANVAS_FOLDERS_ENDPOINT, b.Id) + + builder = Folder{ + Id: rootFolderId, + Name: b.ModuleCode, + Downloadable: b.IsAccessible, + HasSubFolder: true, + Ancestors: []string{}, + } case constants.Luminus: - url = fmt.Sprintf(FOLDER_URL_ENDPOINT, builder.Id) + url = fmt.Sprintf(FOLDER_URL_ENDPOINT, b.Id) default: return FoldersRequest{}, errors.New("invalid platform provided") } case Folder: switch p := platform; p { case constants.Canvas: - url = fmt.Sprintf(constants.CANVAS_FOLDERS_ENDPOINT, builder.Id) + url = fmt.Sprintf(constants.CANVAS_FOLDERS_ENDPOINT, b.Id) case constants.Luminus: - url = fmt.Sprintf(FOLDER_URL_ENDPOINT, builder.Id) + url = fmt.Sprintf(FOLDER_URL_ENDPOINT, b.Id) default: return FoldersRequest{}, errors.New("invalid platform provided") } @@ -129,7 +180,8 @@ func BuildFoldersRequest(token string, platform constants.Platform, builder inte }, nil } -// TODO Documentations +// BuildFilesRequest builds and returns a FilesRequest that can be used for File related operations +// such as retrieving files of a module. func BuildFilesRequest(token string, platform constants.Platform, folder Folder) (FilesRequest, error) { var url string @@ -237,6 +289,9 @@ func retrieveJwtToken() (string, error) { return tokensData.LuminusToken.JwtToken, tokensErr } +// Send takes a Request that encapsulates a HTTP request and sends it. The response body is then +// unmarshalled into the interface{} argument provided. +// Note that the argument parsed must be a pointer. func (req Request) Send(res interface{}) error { request, err := http.NewRequest(req.Method, req.Url.Url, nil) if err != nil { diff --git a/pkg/api/response.go b/pkg/api/response.go index 85e7bbf..f12af5e 100644 --- a/pkg/api/response.go +++ b/pkg/api/response.go @@ -1,4 +1,5 @@ -// Package api provides functions that link up and communicate with Luminus servers. +// Package api provides functions that link up and communicate with LMS servers, +// such as Canvas and Luminus (probably removed in near future). package api import ( @@ -7,7 +8,7 @@ import ( "net/http" ) -// RawResponse struct is the datapack for containing API response raw data. +// RawResponse struct is the datapack for containing API response raw data for Luminus. type RawResponse struct { Status string `json:"status"` Code int `json:"code"` diff --git a/pkg/api/videos.go b/pkg/api/videos.go index 94f02dc..381b081 100644 --- a/pkg/api/videos.go +++ b/pkg/api/videos.go @@ -1,4 +1,5 @@ -// Package api provides functions that link up and communicate with Luminus servers. +// Package api provides functions that link up and communicate with LMS servers, +// such as Canvas and Luminus (probably removed in near future). package api import ( diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 178cdd0..6093a23 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -1,10 +1,11 @@ -// Package auth provides functions that link up and communicate with Luminus authentication server. +// Package auth provides functions that link up and communicate with LMS (Luminus/Canvas) +// authentication server. package auth import ( appAuth "github.com/beebeeoii/lominus/internal/app/auth" + appConstants "github.com/beebeeoii/lominus/internal/constants" file "github.com/beebeeoii/lominus/internal/file" - "github.com/beebeeoii/lominus/internal/lominus" ) // JsonResponse struct is the datapack for containing API authentication response raw data. @@ -14,19 +15,21 @@ type JsonResponse struct { ExpiresIn int `json:"expires_in"` } -// TODO Documentation +// CredentialsData struct is the datapack that contains all the different Credentials for +// available LMS (Luminus, Canvas etc.). type CredentialsData struct { CanvasCredentials CanvasCredentials LuminusCredentials LuminusCredentials } -// TokenData struct is the datapack that describes the user's tokens data. +// TokensData struct is the datapack that contains all the different Tokens for +// available LMS (Luminus, Canvas etc.). type TokensData struct { CanvasToken CanvasTokenData LuminusToken LuminusTokenData } -const CREDENTIALS_FILE_NAME = lominus.CREDENTIALS_FILE_NAME +const CREDENTIALS_FILE_NAME = appConstants.CREDENTIALS_FILE_NAME const CONTENT_TYPE = "application/x-www-form-urlencoded" const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" @@ -81,7 +84,7 @@ func LoadTokensData(tokensPath string, autoRenew bool) (TokensData, error) { return tokensData, err } -// TODO Documentation +// saveCredentialsData saves the user's Credentials data to local storage for future use. func saveCredentialsData(credentialsPath string, credentailsData CredentialsData) error { if file.Exists(credentialsPath) { localCredentialsData, err := LoadCredentialsData(credentialsPath) @@ -95,7 +98,7 @@ func saveCredentialsData(credentialsPath string, credentailsData CredentialsData return file.EncodeStructToFile(credentialsPath, credentailsData) } -// TODO Documentation +// LoadCredentialsData loads the user's Credentials data from local storage. func LoadCredentialsData(credentialsPath string) (CredentialsData, error) { credentialsData := CredentialsData{} if !file.Exists(credentialsPath) { @@ -106,7 +109,10 @@ func LoadCredentialsData(credentialsPath string) (CredentialsData, error) { return credentialsData, err } -// TODO Documentation +// Merge takes n individual Token data encapsulated in TokensData and merge/combine them +// into a TokensData that contains the individual Token data. +// Eg. a := TokensData{CanvasToken} +// a.merge(TokensData{LuminusToken}) // a == TokensData{CanvasToken, LuminusToken} func (t *TokensData) Merge(t2 TokensData) { if t.CanvasToken == (CanvasTokenData{}) { t.CanvasToken = t2.CanvasToken @@ -117,7 +123,10 @@ func (t *TokensData) Merge(t2 TokensData) { } } -// TODO Documentation +// Merge takes n individual Credentials data encapsulated in CredentialsData and merge/combine them +// into a CredentialsData that contains the individual Credentials data. +// Eg. a := CredentialsData{CanvasCredentials} +// a.merge(CredentialsData{LuminusCredentials}) // a == CredentialsData{CanvasCredentials, LuminusCredentials} func (t *CredentialsData) Merge(t2 CredentialsData) { if t.CanvasCredentials == (CanvasCredentials{}) { t.CanvasCredentials = t2.CanvasCredentials diff --git a/pkg/auth/canvas.go b/pkg/auth/canvas.go index 2d11ad9..69db161 100644 --- a/pkg/auth/canvas.go +++ b/pkg/auth/canvas.go @@ -1,3 +1,5 @@ +// Package auth provides functions that link up and communicate with LMS (Luminus/Canvas) +// authentication server. package auth import ( @@ -7,29 +9,36 @@ import ( "github.com/beebeeoii/lominus/pkg/constants" ) -// TODO Documentation +// CanvasTokenData is a struct that encapsulates the token required for authentication. +// In this case, it is the CanvasApiToken which is a string. type CanvasTokenData struct { CanvasApiToken string } +// CanvasCredentials is a struct that encapsulates the credentials required for authentication. +// In this case, it is the CanvasApiToken which is a string. type CanvasCredentials struct { CanvasApiToken string } -// TODO Documentation +// Save takes in the CanvasTokenData and saves it locally with the path provided as arguments. func (canvasTokenData CanvasTokenData) Save(tokensPath string) error { return saveTokenData(tokensPath, TokensData{ CanvasToken: canvasTokenData, }) } -// TODO Documentation +// Save takes in the CanvasCredentials and saves it locally with the path provided as arguments. func (credentials CanvasCredentials) Save(credentialsPath string) error { return saveCredentialsData(credentialsPath, CredentialsData{ CanvasCredentials: credentials, }) } +// Authenticate checks whether the CanvasCredentials provided is valid. +// This is done by sending a HTTP request to the Canvas server to retrieve information +// on the account using the CanvasCredentials. +// If the credentials is valid, the response status code is expected to be 200. func (credentials CanvasCredentials) Authenticate() error { request, err := http.NewRequest("GET", constants.CANVAS_USER_SELF_ENDPOINT, nil) if err != nil { diff --git a/pkg/auth/luminus.go b/pkg/auth/luminus.go index 07e0bec..7966b94 100644 --- a/pkg/auth/luminus.go +++ b/pkg/auth/luminus.go @@ -1,3 +1,5 @@ +// Package auth provides functions that link up and communicate with LMS (Luminus/Canvas) +// authentication server. package auth import ( @@ -19,13 +21,16 @@ const ( EXPIRY_HOURS = 1 ) -// TODO Documentation +// LuminusTokenData is a struct that encapsulates the token required for authentication. +// In this case, it is the JwtToken which is a string, and the corresponding expiry timestamp +// in milliseconds. type LuminusTokenData struct { JwtToken string JwtExpiry int64 } -// Credentials struct is the datapack that describes the user's credentials. +// LuminusCredentials is a struct that encapsulates the credentials required for authentication. +// In this case, it is the username and password. type LuminusCredentials struct { Username string Password string @@ -127,21 +132,21 @@ func RetrieveJwtToken(credentials LuminusCredentials, save bool) (string, error) return jwtToken, nil } -// TODO Documentation +// Save takes in the LuminusTokenData and saves it locally with the path provided as arguments. func (luminusTokenData LuminusTokenData) Save(tokensPath string) error { return saveTokenData(tokensPath, TokensData{ LuminusToken: luminusTokenData, }) } -// SaveCredentials saves the user's Credentials for future authentications or renewals of JWT data. +// Save takes in the LuminusCredentials and saves it locally with the path provided as arguments. func (credentials LuminusCredentials) Save(credentialsPath string) error { return saveCredentialsData(credentialsPath, CredentialsData{ LuminusCredentials: credentials, }) } -// IsExpired is a util function that checks if the user's JWT data has expired. +// IsExpired is a helper function that checks if the user's JWT data has expired. func (luminusTokenData LuminusTokenData) IsExpired() bool { expiry := time.Unix(luminusTokenData.JwtExpiry, 0) return time.Until(expiry).Hours() <= EXPIRY_HOURS diff --git a/pkg/constants/endpoints.go b/pkg/constants/endpoints.go index d31e68c..37c680b 100644 --- a/pkg/constants/endpoints.go +++ b/pkg/constants/endpoints.go @@ -1,3 +1,4 @@ +// Package constants provides constants such as web endpoints. package constants // Canvas Endpoints diff --git a/pkg/constants/platforms.go b/pkg/constants/platforms.go index a503a2f..7d3fd60 100644 --- a/pkg/constants/platforms.go +++ b/pkg/constants/platforms.go @@ -1,13 +1,18 @@ -// TODO Documentations +// Package constants provides constants such as web endpoints. package constants type Platform int +// This is an enum. +// Eg. Canvas = 0, Luminus = 1, ... const ( Canvas Platform = iota Luminus ) +// Platforms is a list of available LMS platforms supported by Lominus. +// It is used to differentiate the various LMS due to the possible different ways +// each platform works. var Platforms = []Platform{ Canvas, Luminus, diff --git a/pkg/interfaces/File.go b/pkg/interfaces/File.go index 126cb9c..9c30292 100644 --- a/pkg/interfaces/File.go +++ b/pkg/interfaces/File.go @@ -1,16 +1,31 @@ +// Package interfaces provide the fundamental blueprint for how each object +// looks like. package interfaces -// TODO Documentation +// CanvasFileObject depicts the actual object return from Canvas. +// There are more fields being returned by Canvas, but these are just the +// relevant ones as of now. type CanvasFileObject struct { - Id int `json:"id"` - Name string `json:"filename"` + Id int `json:"id"` + Name string `json:"filename"` + // DisplayName is the name that is seen on Canvas Web. It can differ + // from the actual name of the file being uploaded. + // Using DisplayName would be more accurate as Professors might + // set the DisplayName to contain more information for the File. + // For eg. filename can be "Tutorial1.pdf" but DisplayName can be + // "Tutorial1_HW1.pdf" to show that Tutorial 1 is to be submitted as + // a graded Homework. + DisplayName string `json:"display_name"` UUID string `json:"uuid"` Url string `json:"url"` HiddenForUser bool `json:"hidden_for_user"` - LastUpdated string `json:"updated_at"` + LastUpdated string `json:"modified_at"` } -// TODO Documentation +// LuminusFileObject depicts the actual object return from Luminus. +// There are more fields being returned by Luminus, but these are just the +// relevant ones as of now. +// For more details on what mapstructure means: https://github.com/mitchellh/mapstructure type LuminusFileObject struct { Id string `json:"id"` Name string `json:"name"` diff --git a/pkg/interfaces/Folder.go b/pkg/interfaces/Folder.go index 765f78a..5d85b98 100644 --- a/pkg/interfaces/Folder.go +++ b/pkg/interfaces/Folder.go @@ -1,19 +1,24 @@ +// Package interfaces provide the fundamental blueprint for how each object +// looks like. package interfaces -type FolderObject interface { - CanvasFolderObject | LuminusFolderObject -} - -// TODO Documentation +// CanvasFolderObject depicts the actual object return from Canvas. +// There are more fields being returned by Canvas, but these are just the +// relevant ones as of now. type CanvasFolderObject struct { - Id int `json:"id"` - Name string `json:"name"` - FullName string `json:"full_name"` - HiddenForUser bool `json:"hidden_for_user"` - FilesCount int `json:"files_count"` - FoldersCount int `json:"folders_count"` + Id int `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + HiddenForUser bool `json:"hidden_for_user"` + FilesCount int `json:"files_count"` + FoldersCount int `json:"folders_count"` + ParentFolderId int `json:"parent_folder_id"` } +// LuminusFolderObject depicts the actual object return from Luminus. +// There are more fields being returned by Luminus, but these are just the +// relevant ones as of now. +// For more details on what mapstructure means: https://github.com/mitchellh/mapstructure type LuminusFolderObject struct { Id string `json:"id" ` Name string `json:"name"` diff --git a/pkg/interfaces/Module.go b/pkg/interfaces/Module.go index d4eee44..8a36b68 100644 --- a/pkg/interfaces/Module.go +++ b/pkg/interfaces/Module.go @@ -1,6 +1,10 @@ +// Package interfaces provide the fundamental blueprint for how each object +// looks like. package interfaces -// TODO Documentation +// CanvasModuleObject depicts the actual object return from Canvas. +// There are more fields being returned by Canvas, but these are just the +// relevant ones as of now. type CanvasModuleObject struct { Id int `json:"id"` UUID string `json:"uuid"` @@ -9,7 +13,10 @@ type CanvasModuleObject struct { IsAccessRestrictedByDate bool `json:"access_restricted_by_date"` } -// TODO Documentation +// LuminusModuleObject depicts the actual object return from Luminus. +// There are more fields being returned by Luminus, but these are just the +// relevant ones as of now. +// For more details on what mapstructure means: https://github.com/mitchellh/mapstructure type LuminusModuleObject struct { Id string `json:"id"` Name string `json:"courseName" mapstructure:"courseName"` diff --git a/pkg/interfaces/Response.go b/pkg/interfaces/Response.go index 287617f..f3c2ce4 100644 --- a/pkg/interfaces/Response.go +++ b/pkg/interfaces/Response.go @@ -1,5 +1,10 @@ +// Package interfaces provide the fundamental blueprint for how each object +// looks like. package interfaces +// LuminusRawResponse depicts the actual object return from Luminus. +// There are more fields being returned by Luminus, but these are just the +// relevant ones as of now. type LuminusRawResponse struct { Status string `json:"status"` Code int `json:"code"` diff --git a/pkg/interfaces/Url.go b/pkg/interfaces/Url.go index 55a0fee..9928671 100644 --- a/pkg/interfaces/Url.go +++ b/pkg/interfaces/Url.go @@ -1,8 +1,11 @@ -// TODO Documentations +// Package interfaces provide the fundamental blueprint for how each object +// looks like. package interfaces import "github.com/beebeeoii/lominus/pkg/constants" +// Url is a struct that encapsulates a Url object. +// It contains the address and the LMS platform which the address belongs to. type Url struct { Url string Platform constants.Platform