From dcd6aedaf98e2dbb46405536fd8311cbeb96e08f Mon Sep 17 00:00:00 2001 From: Adithya Dsilva Date: Mon, 18 May 2020 17:37:04 +0530 Subject: [PATCH 1/5] replace links --- .../CODE_OF_CONDUCT.md | 0 LICENSE | 2 +- README.md | 18 +++++++++--------- client/fetch.go | 1 - cmd/config.go | 2 +- cmd/upgrade.go | 4 ++-- 6 files changed, 13 insertions(+), 14 deletions(-) rename CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md (100%) diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/LICENSE b/LICENSE index 902c7cc..ae88b75 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 infixint943 +Copyright (c) 2020 cp-tools Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a7ccdcd..f2c317f 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@
Don't forget to :star: the project if you liked it!

- - - - - - + + + + + + @@ -37,20 +37,20 @@ # Installation -You may download the latest, compiled, binary files from [here](https://github.com/infixint943/cf/releases). +You may download the latest, compiled, binary files from [here](https://github.com/cp-tools/cf/releases). Place the executable in system **PATH** to invoke the tool from any directory. Alternatively, you can also compile the tool from source. ```bash -git clone https://github.com/infixint943/cf.git +git clone https://github.com/cp-tools/cf.git cd cf/ go build -ldflags "-s -w" ``` # Quick Start -**Note:** For detailed documentation, please head to the [wiki](https://github.com/infixint943/cf/wiki) page. +**Note:** For detailed documentation, please head to the [wiki](https://github.com/cp-tools/cf/wiki) page. > Let's simulate participating in contest `4`. This tutorial assumes you have already configured your login details and added at least one template, through `cf config` diff --git a/client/fetch.go b/client/fetch.go index cbf1eb6..a62bf5f 100644 --- a/client/fetch.go +++ b/client/fetch.go @@ -58,7 +58,6 @@ func FetchProbs(contest string, link url.URL) ([]string, error) { // Returns 2d slice mapping to input and output // If problem == "", fetch all problem test cases // else, only fetch of given problem. -// fix for https://github.com/infixint943/cf/pull/2#issuecomment-626122011 func FetchTests(contest, problem string, link url.URL) ([][]string, [][]string, error) { c := cfg.Session.Client diff --git a/cmd/config.go b/cmd/config.go index 14025f3..c755745 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -83,7 +83,7 @@ func addTmplt() { for name := range cln.LangID { lName = append(lName, name) } - pkg.Log.Info("For detailed instructions, read https://github.com/infixint943/cf/wiki/Configuration") + pkg.Log.Info("For detailed instructions, read https://github.com/cp-tools/cf/wiki/Configuration") tmplt := cfg.Template{} err := survey.Ask([]*survey.Question{ { diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 55bcf7b..c7cb33a 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -18,7 +18,7 @@ func RunUpgrade() { // parse current version cVers := semver.MustParse(Version) // determine latest release version using github API - link := "https://api.github.com/repos/infixint943/cf/releases/latest" + link := "https://api.github.com/repos/cp-tools/cf/releases/latest" resp, err := pkg.GetReqBody(&http.Client{}, link) pkg.PrintError(err, "Failed to fetch latest release") @@ -48,7 +48,7 @@ func RunUpgrade() { return } // url of tar file to download - link = fmt.Sprintf("https://github.com/infixint943/cf/releases/download/%v/cf_%v_%v.tar.gz", + link = fmt.Sprintf("https://github.com/cp-tools/cf/releases/download/%v/cf_%v_%v.tar.gz", latest, runtime.GOOS, runtime.GOARCH) pkg.Log.Info("Downloading update. Please wait.") From 40b3278d0113009e814a598cb94db9457d0422b6 Mon Sep 17 00:00:00 2001 From: Adithya Dsilva Date: Mon, 18 May 2020 18:06:29 +0530 Subject: [PATCH 2/5] Refractor error messages --- client/fetch.go | 3 +- client/langs.go | 105 ------------------------------------------- client/login.go | 4 +- client/misc.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++ client/submit.go | 3 +- client/test.go | 6 +-- client/watch.go | 7 +-- 7 files changed, 122 insertions(+), 121 deletions(-) delete mode 100644 client/langs.go create mode 100644 client/misc.go diff --git a/client/fetch.go b/client/fetch.go index a62bf5f..bdce997 100644 --- a/client/fetch.go +++ b/client/fetch.go @@ -24,8 +24,7 @@ func FindCountdown(contest string, link url.URL) (int64, error) { return 0, err } else if len(body) == 0 { // such page doesn't exist - err = fmt.Errorf("Contest %v doesn't exist", contest) - return 0, err + return 0, ErrContestNotExists } doc, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) diff --git a/client/langs.go b/client/langs.go deleted file mode 100644 index 32d9985..0000000 --- a/client/langs.go +++ /dev/null @@ -1,105 +0,0 @@ -package cln - -// LangID represents all available languages with id's -var LangID = map[string]string{ - "GNU GCC C11 5.1.0": "43", - "Clang++17 Diagnostics": "52", - "GNU G++11 5.1.0": "42", - "GNU G++14 6.4.0": "50", - "GNU G++17 7.3.0": "54", - "Microsoft Visual C++ 2010": "2", - "Microsoft Visual C++ 2017": "59", - "GNU G++17 9.2.0 (64 bit, msys 2)": "61", - "C# Mono 5.18": "9", - "D DMD32 v2.086.0": "28", - "Go 1.12.6": "32", - "Haskell GHC 8.6.3": "12", - "Java 11.0.5": "60", - "Java 1.8.0_162": "36", - "Kotlin 1.3.10": "48", - "OCaml 4.02.1": "19", - "Delphi 7": "3", - "Free Pascal 3.0.2": "4", - "PascalABC.NET 3.4.2": "51", - "Perl 5.20.1": "13", - "PHP 7.2.13": "6", - "Python 2.7.15": "7", - "Python 3.7.2": "31", - "PyPy 2.7 (7.2.0)": "40", - "PyPy 3.6 (7.2.0)": "41", - "Ruby 2.0.0p645": "8", - "Rust 1.35.0": "49", - "Scala 2.12.8": "20", - "JavaScript V8 4.8.0": "34", - "Node.js 9.4.0": "55", - "ActiveTcl 8.5": "14", - "Io-2008-01-07 (Win32)": "15", - "Pike 7.8": "17", - "Befunge": "18", - "OpenCobol 1.0": "22", - "Factor": "25", - "Secret_171": "26", - "Roco": "27", - "Ada GNAT 4": "33", - "Mysterious Language": "38", - "FALSE": "39", - "Picat 0.9": "44", - "GNU C++11 5 ZIP": "45", - "Java 8 ZIP": "46", - "J": "47", - "Microsoft Q#": "56", - "Text": "57", -} - -// LangExt corresponds to file extension of -// given language source code -var LangExt = map[string]string{ - "GNU C11": ".c", - "Clang++17 Diagnostics": ".cpp", - "GNU C++0x": ".cpp", - "GNU C++": ".cpp", - "GNU C++11": ".cpp", - "GNU C++14": ".cpp", - "GNU C++17": ".cpp", - "MS C++": ".cpp", - "MS C++ 2017": ".cpp", - "GNU C++17 (64)": ".cpp", - "Mono C#": ".cs", - "D": ".d", - "Go": ".go", - "Haskell": ".hs", - "Kotlin": ".kt", - "Ocaml": ".ml", - "Delphi": ".pas", - "FPC": ".pas", - "PascalABC.NET": ".pas", - "Perl": ".pl", - "PHP": ".php", - "Python 2": ".py", - "Python 3": ".py", - "PyPy 2": ".py", - "PyPy 3": ".py", - "Ruby": ".rb", - "Rust": ".rs", - "JavaScript": ".js", - "Node.js": ".js", - "Q#": ".qs", - "Java": ".java", - "Java 6": ".java", - "Java 7": ".java", - "Java 8": ".java", - "Java 9": ".java", - "Java 10": ".java", - "Java 11": ".java", - "Tcl": ".tcl", - "F#": ".fs", - "Befunge": ".bf", - "Pike": ".pike", - "Io": ".io", - "Factor": ".factor", - "Cobol": ".cbl", - "Secret_171": ".secret_171", - "Ada": ".adb", - "FALSE": ".f", - "": ".txt", -} \ No newline at end of file diff --git a/client/login.go b/client/login.go index ccda941..99b65de 100644 --- a/client/login.go +++ b/client/login.go @@ -5,7 +5,6 @@ import ( pkg "cf/packages" "encoding/hex" - "fmt" "net/url" "path" @@ -84,8 +83,7 @@ func Relogin() (bool, error) { // decode hex data of encrypted password ciphertext, err := hex.DecodeString(cfg.Session.Passwd) if err != nil { - err := fmt.Errorf("Failed to decode password") - return false, err + return false, ErrDecodePasswdFailed } usr := cfg.Session.Handle dec := aes.NewAES256Decrypter(usr) diff --git a/client/misc.go b/client/misc.go new file mode 100644 index 0000000..2af456c --- /dev/null +++ b/client/misc.go @@ -0,0 +1,115 @@ +package cln + +import "fmt" + +// Some global variables +var ( + ErrContestNotExists = fmt.Errorf("Contest doesn't exist") + ErrDecodePasswdFailed = fmt.Errorf("Failed to decode password") + ErrUnequalSampleTests = fmt.Errorf("Unequal number of input/output test files") + ErrSampleTestsNotExists = fmt.Errorf("No test files found") + + // LangID represents all available languages with id's + LangID = map[string]string{ + "GNU GCC C11 5.1.0": "43", + "Clang++17 Diagnostics": "52", + "GNU G++11 5.1.0": "42", + "GNU G++14 6.4.0": "50", + "GNU G++17 7.3.0": "54", + "Microsoft Visual C++ 2010": "2", + "Microsoft Visual C++ 2017": "59", + "GNU G++17 9.2.0 (64 bit, msys 2)": "61", + "C# Mono 5.18": "9", + "D DMD32 v2.086.0": "28", + "Go 1.12.6": "32", + "Haskell GHC 8.6.3": "12", + "Java 11.0.5": "60", + "Java 1.8.0_162": "36", + "Kotlin 1.3.10": "48", + "OCaml 4.02.1": "19", + "Delphi 7": "3", + "Free Pascal 3.0.2": "4", + "PascalABC.NET 3.4.2": "51", + "Perl 5.20.1": "13", + "PHP 7.2.13": "6", + "Python 2.7.15": "7", + "Python 3.7.2": "31", + "PyPy 2.7 (7.2.0)": "40", + "PyPy 3.6 (7.2.0)": "41", + "Ruby 2.0.0p645": "8", + "Rust 1.35.0": "49", + "Scala 2.12.8": "20", + "JavaScript V8 4.8.0": "34", + "Node.js 9.4.0": "55", + "ActiveTcl 8.5": "14", + "Io-2008-01-07 (Win32)": "15", + "Pike 7.8": "17", + "Befunge": "18", + "OpenCobol 1.0": "22", + "Factor": "25", + "Secret_171": "26", + "Roco": "27", + "Ada GNAT 4": "33", + "Mysterious Language": "38", + "FALSE": "39", + "Picat 0.9": "44", + "GNU C++11 5 ZIP": "45", + "Java 8 ZIP": "46", + "J": "47", + "Microsoft Q#": "56", + "Text": "57", + } + + // LangExt corresponds to file extension of + // given language source code + LangExt = map[string]string{ + "GNU C11": ".c", + "Clang++17 Diagnostics": ".cpp", + "GNU C++0x": ".cpp", + "GNU C++": ".cpp", + "GNU C++11": ".cpp", + "GNU C++14": ".cpp", + "GNU C++17": ".cpp", + "MS C++": ".cpp", + "MS C++ 2017": ".cpp", + "GNU C++17 (64)": ".cpp", + "Mono C#": ".cs", + "D": ".d", + "Go": ".go", + "Haskell": ".hs", + "Kotlin": ".kt", + "Ocaml": ".ml", + "Delphi": ".pas", + "FPC": ".pas", + "PascalABC.NET": ".pas", + "Perl": ".pl", + "PHP": ".php", + "Python 2": ".py", + "Python 3": ".py", + "PyPy 2": ".py", + "PyPy 3": ".py", + "Ruby": ".rb", + "Rust": ".rs", + "JavaScript": ".js", + "Node.js": ".js", + "Q#": ".qs", + "Java": ".java", + "Java 6": ".java", + "Java 7": ".java", + "Java 8": ".java", + "Java 9": ".java", + "Java 10": ".java", + "Java 11": ".java", + "Tcl": ".tcl", + "F#": ".fs", + "Befunge": ".bf", + "Pike": ".pike", + "Io": ".io", + "Factor": ".factor", + "Cobol": ".cbl", + "Secret_171": ".secret_171", + "Ada": ".adb", + "FALSE": ".f", + "": ".txt", + } +) diff --git a/client/submit.go b/client/submit.go index 0fd2473..66246b6 100644 --- a/client/submit.go +++ b/client/submit.go @@ -24,8 +24,7 @@ func Submit(contest, problem, langID, file string, link url.URL) error { return err } else if len(body) == 0 { // such page doesn't exist - err = fmt.Errorf("Contest %v doesn't exist", contest) - return err + return ErrContestNotExists } // read source file diff --git a/client/test.go b/client/test.go index a20e162..06a47c7 100644 --- a/client/test.go +++ b/client/test.go @@ -51,11 +51,9 @@ func FindTests() ([]string, []string, error) { // check for i/o count equality // and existence of non-zero test files if len(inp) != len(out) { - err := fmt.Errorf("Unequal number of input/output test files") - return nil, nil, err + return nil, nil, ErrUnequalSampleTests } else if len(inp) == 0 { - err := fmt.Errorf("No test files found") - return nil, nil, err + return nil, nil, ErrSampleTestsNotExists } return inp, out, nil } diff --git a/client/watch.go b/client/watch.go index 58b0f1c..f3b6a86 100644 --- a/client/watch.go +++ b/client/watch.go @@ -5,7 +5,6 @@ import ( pkg "cf/packages" "bytes" - "fmt" "net/url" "path" "strings" @@ -42,8 +41,7 @@ func WatchSubmissions(contest, query string, link url.URL) ([]Submission, error) return nil, err } else if len(body) == 0 { // such page doesn't exist - err = fmt.Errorf("Contest %v doesn't exist", contest) - return nil, err + return nil, ErrContestNotExists } // to hold all submissions var data []Submission @@ -79,8 +77,7 @@ func WatchContest(contest string, link url.URL) ([]Problem, error) { return nil, err } else if len(body) == 0 { // such page doesn't exist - err = fmt.Errorf("Contest %v doesn't exist", contest) - return nil, err + return nil, ErrContestNotExists } // to hold all problems in contest var data []Problem From 2ffd39504023ecb852514e200797bc5291065861 Mon Sep 17 00:00:00 2001 From: Adithya Dsilva Date: Tue, 19 May 2020 14:32:35 +0530 Subject: [PATCH 3/5] Refractor packages module (WIP) --- client/misc.go | 19 +++++++++- client/test.go | 40 -------------------- client/watch.go | 22 +++++------ cmd/config.go | 49 ++++++++++++------------ cmd/fetch.go | 39 ++++++++++--------- cmd/gen.go | 13 +++---- cmd/misc.go | 85 ++++++++++++++++++++++++++++++++++++++---- cmd/open.go | 4 +- cmd/pull.go | 11 +++--- cmd/submit.go | 39 ++++++++++--------- cmd/test.go | 70 ++++++++++++++++++++++++++-------- cmd/upgrade.go | 18 ++++----- cmd/watch.go | 17 ++++----- config/sessions.go | 61 +++++++++++++++++------------- config/settings.go | 48 +++++++++++++----------- config/templates.go | 36 ++++++++++-------- packages/misc.go | 91 --------------------------------------------- 17 files changed, 335 insertions(+), 327 deletions(-) delete mode 100644 packages/misc.go diff --git a/client/misc.go b/client/misc.go index 2af456c..995ac4a 100644 --- a/client/misc.go +++ b/client/misc.go @@ -1,6 +1,11 @@ package cln -import "fmt" +import ( + "fmt" + "strings" + + "github.com/PuerkitoBio/goquery" +) // Some global variables var ( @@ -113,3 +118,15 @@ var ( "": ".txt", } ) + +// GetText extracts text from particular html data +func GetText(sel *goquery.Selection, query string) string { + str := sel.Find(query).Text() + return strings.TrimSpace(str) +} + +// GetAttr extracts attribute valur of particular html data +func GetAttr(sel *goquery.Selection, query, attr string) string { + str := sel.Find(query).AttrOr(attr, "") + return strings.TrimSpace(str) +} diff --git a/client/test.go b/client/test.go index 06a47c7..3384007 100644 --- a/client/test.go +++ b/client/test.go @@ -2,11 +2,9 @@ package cln import ( cfg "cf/config" - pkg "cf/packages" "bytes" "context" - "fmt" "io" "io/ioutil" "math/big" @@ -16,9 +14,6 @@ import ( "strconv" "strings" "time" - - "github.com/fatih/color" - "github.com/gosuri/uitable" ) // FindTests finds all returns all sample input/output @@ -155,38 +150,3 @@ func Validator(out, ans string, igCase bool, exp int) (string, string) { // return formatted strings return f(out), f(ans) } - -// PrintDiff is run if outputs don't match -// returns input data, and then the diff of => out vs ans -func PrintDiff(inp, out, ans string) string { - // variable to hold diff output - var diff strings.Builder - headerfmt := pkg.Blue.Add(color.Underline).SprintfFunc() - // print input data - fmt.Fprintln(&diff, headerfmt("Input")) - fmt.Fprintln(&diff, inp) - - // break output into lines - str1 := strings.Split(out, "\n") - str2 := strings.Split(ans, "\n") - // equalize string lengths - if len(str1) < len(str2) { - str1 = append(str1, make([]string, len(str2)-len(str1))...) - } else { - str2 = append(str2, make([]string, len(str1)-len(str2))...) - } - - // print output diff data - tbl := uitable.New() - tbl.Separator = " | " - - tbl.AddRow(headerfmt("Actual Output"), headerfmt("Expected Output")) - // iterate over every row of outputs - for i := 0; i < len(str1); i++ { - tbl.AddRow(str1[i], str2[i]) - } - fmt.Fprintln(&diff, tbl) - fmt.Fprintln(&diff) - - return diff.String() -} diff --git a/client/watch.go b/client/watch.go index f3b6a86..d0941d8 100644 --- a/client/watch.go +++ b/client/watch.go @@ -52,14 +52,14 @@ func WatchSubmissions(contest, query string, link url.URL) ([]Submission, error) sel.Each(func(_ int, row *goquery.Selection) { // select cell ...type(x) from row data = append(data, Submission{ - ID: pkg.GetText(row, "td:nth-of-type(1)"), - When: pkg.GetText(row, "td:nth-of-type(2)"), - Name: pkg.GetText(row, "td:nth-of-type(4)"), - Lang: pkg.GetText(row, "td:nth-of-type(5)"), - Waiting: pkg.GetAttr(row, "td:nth-of-type(6)", "waiting"), - Verdict: pkg.GetText(row, "td:nth-of-type(6)"), - Time: pkg.GetText(row, "td:nth-of-type(7)"), - Memory: pkg.GetText(row, "td:nth-of-type(8)"), + ID: GetText(row, "td:nth-of-type(1)"), + When: GetText(row, "td:nth-of-type(2)"), + Name: GetText(row, "td:nth-of-type(4)"), + Lang: GetText(row, "td:nth-of-type(5)"), + Waiting: GetAttr(row, "td:nth-of-type(6)", "waiting"), + Verdict: GetText(row, "td:nth-of-type(6)"), + Time: GetText(row, "td:nth-of-type(7)"), + Memory: GetText(row, "td:nth-of-type(8)"), }) }) @@ -86,9 +86,9 @@ func WatchContest(contest string, link url.URL) ([]Problem, error) { doc.Find(".problems tr").Has("td").Each(func(_ int, row *goquery.Selection) { data = append(data, Problem{ - ID: pkg.GetText(row, "td:nth-of-type(1)"), - Name: pkg.GetText(row, "td:nth-of-type(2) a"), - Count: pkg.GetText(row, "td:nth-of-type(4)"), + ID: GetText(row, "td:nth-of-type(1)"), + Name: GetText(row, "td:nth-of-type(2) a"), + Count: GetText(row, "td:nth-of-type(4)"), Status: row.AttrOr("class", ""), }) }) diff --git a/cmd/config.go b/cmd/config.go index c755745..33db9a9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -3,7 +3,6 @@ package cmd import ( cln "cf/client" cfg "cf/config" - pkg "cf/packages" "fmt" "net/url" @@ -27,7 +26,7 @@ func (opt Opts) RunConfig() { "Other misc preferences", }, }, &choice, survey.WithValidator(survey.Required)) - pkg.PrintError(err, "") + PrintError(err, "") switch choice { case 0: @@ -45,8 +44,8 @@ func (opt Opts) RunConfig() { func login() { // check if logged in user exists if cfg.Session.Handle != "" { - pkg.Log.Success("Current user: " + cfg.Session.Handle) - pkg.Log.Warning("Current session will be overwritten") + Log.Success("Current user: " + cfg.Session.Handle) + Log.Warning("Current session will be overwritten") } // take input of username / password creds := struct{ Usr, Passwd string }{} @@ -61,19 +60,19 @@ func login() { Validate: survey.Required, }, }, &creds) - pkg.PrintError(err, "") + PrintError(err, "") // login and check login status - pkg.Log.Info("Logging in") + Log.Info("Logging in") flag, err := cln.Login(creds.Usr, creds.Passwd) - pkg.PrintError(err, "Login failed") + PrintError(err, "Login failed") // login was successful if flag == true { - pkg.Log.Success("Login successful") - pkg.Log.Notice("Welcome " + cfg.Session.Handle) + Log.Success("Login successful") + Log.Notice("Welcome " + cfg.Session.Handle) } else { // login failed - pkg.Log.Error("Login failed") - pkg.Log.Notice("Check credentials and retry") + Log.Error("Login failed") + Log.Notice("Check credentials and retry") } return } @@ -83,7 +82,7 @@ func addTmplt() { for name := range cln.LangID { lName = append(lName, name) } - pkg.Log.Info("For detailed instructions, read https://github.com/cp-tools/cf/wiki/Configuration") + Log.Info("For detailed instructions, read https://github.com/cp-tools/cf/wiki/Configuration") tmplt := cfg.Template{} err := survey.Ask([]*survey.Question{ { @@ -164,7 +163,7 @@ func addTmplt() { }, }, }, &tmplt) - pkg.PrintError(err, "") + PrintError(err, "") // set ext and langid values manually tmplt.Ext = filepath.Ext(tmplt.Path) tmplt.LangID = cln.LangID[tmplt.LangName] @@ -172,7 +171,7 @@ func addTmplt() { cfg.Templates = append(cfg.Templates, tmplt) cfg.SaveTemplates() - pkg.Log.Success("Template saved successfully") + Log.Success("Template saved successfully") return } @@ -180,7 +179,7 @@ func remTmplt() { // check if any templates are present sz := len(cfg.Templates) if sz == 0 { - pkg.Log.Error("No configured template's exist") + Log.Error("No configured template's exist") return } @@ -189,19 +188,19 @@ func remTmplt() { Message: "Template you want to remove:", Options: cfg.ListTmplts(cfg.Templates...), }, &idx) - pkg.PrintError(err, "") + PrintError(err, "") // delete the template from the slice // and reconfigure default template settings cfg.Templates = append(cfg.Templates[:idx], cfg.Templates[idx+1:]...) if cfg.Settings.DfltTmplt == idx { - pkg.Log.Warning("Default template configurations reset") + Log.Warning("Default template configurations reset") cfg.Settings.DfltTmplt = -1 cfg.Settings.GenOnFetch = false cfg.SaveSettings() } cfg.SaveTemplates() - pkg.Log.Success("Templated removed successfully") + Log.Success("Templated removed successfully") return } @@ -217,7 +216,7 @@ func miscPrefs() { "Set workspace name", }, }, &choice) - pkg.PrintError(err, "") + PrintError(err, "") switch choice { case 0: @@ -227,7 +226,7 @@ func miscPrefs() { Options: append([]string{"None"}, cfg.ListTmplts(cfg.Templates...)...), }, &cfg.Settings.DfltTmplt) cfg.Settings.DfltTmplt-- - pkg.PrintError(err, "") + PrintError(err, "") case 1: // set GenOnFetch @@ -237,7 +236,7 @@ func miscPrefs() { "Default template has to be configured for this feature to work", Default: false, }, &cfg.Settings.GenOnFetch) - pkg.PrintError(err, "") + PrintError(err, "") case 2: // set host domain @@ -250,7 +249,7 @@ func miscPrefs() { _, err := url.ParseRequestURI(ans.(string)) return err })) - pkg.PrintError(err, "") + PrintError(err, "") case 3: // validate and set proxy @@ -268,7 +267,7 @@ func miscPrefs() { _, err := url.ParseRequestURI(ans.(string)) return err })) - pkg.PrintError(err, "") + PrintError(err, "") case 4: err := survey.AskOne(&survey.Input{ @@ -277,10 +276,10 @@ func miscPrefs() { "A root directory will be created of this name, and all problems will be fetched here.\n" + "Current configured workspace name: " + cfg.Settings.WSName, }, &cfg.Settings.WSName, survey.WithValidator(survey.Required)) - pkg.PrintError(err, "") + PrintError(err, "") } cfg.SaveSettings() - pkg.Log.Success("Configurations successfully set") + Log.Success("Configurations successfully set") return } diff --git a/cmd/fetch.go b/cmd/fetch.go index b37d53c..9dc155f 100644 --- a/cmd/fetch.go +++ b/cmd/fetch.go @@ -3,7 +3,6 @@ package cmd import ( cln "cf/client" cfg "cf/config" - pkg "cf/packages" "fmt" "os" @@ -15,37 +14,37 @@ import ( func (opt Opts) RunFetch() { // check if contest id is present if opt.contest == "" { - pkg.Log.Error("No contest id found") + Log.Error("No contest id found") return } // fetch countdown info - pkg.Log.Info("Fetching details of " + opt.contClass + " " + opt.contest) + Log.Info("Fetching details of " + opt.contClass + " " + opt.contest) dur, err := cln.FindCountdown(opt.contest, opt.link) - pkg.PrintError(err, "Extraction of countdown failed") + PrintError(err, "Extraction of countdown failed") // contest not yet started // countdown till it starts if dur > 0 { - pkg.Log.Warning("Contest hasn't started") - pkg.Log.Info("Launching countdown to start") + Log.Warning("Contest hasn't started") + Log.Info("Launching countdown to start") startCountdown(dur) // open problems page (once parsing is over) // page will be opened only for live rounds defer opt.RunOpen() } // Fetch ALL problems from contest page - pkg.Log.Info("Fetching problems...") + Log.Info("Fetching problems...") probs, err := cln.FetchProbs(opt.contest, opt.link) - pkg.PrintError(err, "Extraction of contest problems failed") + PrintError(err, "Extraction of contest problems failed") // Fetch all tests from problems page splInp, splOut, err := cln.FetchTests(opt.contest, "", opt.link) - pkg.PrintError(err, "Failed to extract sample tests") + PrintError(err, "Failed to extract sample tests") // no sample tests found, try parsing from each problem if len(splInp) == 0 { - pkg.Log.Warning("Failed to fetch tests from problems page") - pkg.Log.Info("Fetching from page of every problem") - pkg.Log.Notice("Please be patient") + Log.Warning("Failed to fetch tests from problems page") + Log.Info("Fetching from page of every problem") + Log.Notice("Please be patient") // iterate over all present problems for _, prob := range probs { // Problem isn't specified to be fetched @@ -56,13 +55,13 @@ func (opt Opts) RunFetch() { continue } probInp, probOut, err := cln.FetchTests(opt.contest, prob, opt.link) - pkg.PrintError(err, "Failed to extract sample tests of "+prob) + PrintError(err, "Failed to extract sample tests of "+prob) // append sample tests to slice splInp = append(splInp, probInp...) splOut = append(splOut, probOut...) // if problem is pdf format (can't extract tests) if len(probInp) == 0 { - pkg.Log.Warning("Unable to extract test(s) - " + prob) + Log.Warning("Unable to extract test(s) - " + prob) splInp = append(splInp, make([]string, 0)) splOut = append(splOut, make([]string, 0)) } @@ -86,11 +85,11 @@ func (opt Opts) RunFetch() { // create tests for x := 0; x < len(splInp[i]); x++ { // create input file (form x.in) - pkg.CreateFile(splInp[i][x], fmt.Sprintf("%v/%d.in", path, x)) + CreateFile(splInp[i][x], fmt.Sprintf("%v/%d.in", path, x)) // create output file (form x.ans) - pkg.CreateFile(splOut[i][x], fmt.Sprintf("%v/%d.out", path, x)) + CreateFile(splOut[i][x], fmt.Sprintf("%v/%d.out", path, x)) } - pkg.Log.Success(fmt.Sprintf("Fetched %d test(s) - %v", len(splInp[i]), prob)) + Log.Success(fmt.Sprintf("Fetched %d test(s) - %v", len(splInp[i]), prob)) // generate code files if specified idx := cfg.Settings.DfltTmplt if cfg.Settings.GenOnFetch == true && idx != -1 { @@ -108,15 +107,15 @@ func (opt Opts) RunFetch() { // startCountdown starts countdown of dur seconds func startCountdown(dur int64) { // run timer till it runs out - pkg.LiveUI.Start() + LiveUI.Start() for ; dur > 0; dur-- { h := fmt.Sprintf("%d:", dur/(60*60)) m := fmt.Sprintf("0%d:", (dur/60)%60) s := fmt.Sprintf("0%d", dur%60) - pkg.LiveUI.Print(h + m[len(m)-3:] + s[len(s)-2:]) + LiveUI.Print(h + m[len(m)-3:] + s[len(s)-2:]) time.Sleep(time.Second) } // remove timer data from screen - pkg.LiveUI.Print() + LiveUI.Print() return } diff --git a/cmd/gen.go b/cmd/gen.go index 7d9e103..c2aca1f 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -2,7 +2,6 @@ package cmd import ( cfg "cf/config" - pkg "cf/packages" "fmt" "io/ioutil" @@ -17,7 +16,7 @@ import ( func (opt Opts) RunGen() { // check if any templates exist if len(cfg.Templates) == 0 { - pkg.Log.Error("No configured template's exist") + Log.Error("No configured template's exist") return } // index of template config to use @@ -30,7 +29,7 @@ func (opt Opts) RunGen() { Message: "Select template to generate:", Options: cfg.ListTmplts(cfg.Templates...), }, &idx) - pkg.PrintError(err, "") + PrintError(err, "") } // create template in current folder // leaving path to "" creates file in curr directory @@ -42,7 +41,7 @@ func (opt Opts) RunGen() { func (opt Opts) GenCode(t *cfg.Template, path string) { // read template code file file, err := ioutil.ReadFile(t.Path) - pkg.PrintError(err, "Failed to read template file") + PrintError(err, "Failed to read template file") // clean template code (replace placeholders) e := Env{ Contest: opt.contest, @@ -62,11 +61,11 @@ func (opt Opts) GenCode(t *cfg.Template, path string) { // check if file already exists if _, err := os.Stat(filepath.Join(path, name)); os.IsNotExist(err) { - pkg.CreateFile(source, filepath.Join(path, name)) - pkg.Log.Notice("File " + name + " generated") + CreateFile(source, filepath.Join(path, name)) + Log.Notice("File " + name + " generated") break } - pkg.Log.Warning("File " + name + " exists") + Log.Warning("File " + name + " exists") } return } diff --git a/cmd/misc.go b/cmd/misc.go index de2db06..86e8437 100644 --- a/cmd/misc.go +++ b/cmd/misc.go @@ -2,7 +2,6 @@ package cmd import ( cfg "cf/config" - pkg "cf/packages" "fmt" "net/url" @@ -15,11 +14,58 @@ import ( "time" "github.com/AlecAivazis/survey/v2" + "github.com/fatih/color" + "github.com/k0kubun/go-ansi" ) // Version of the current executable const Version = "1.1.0" +// Global Variables for different UI formatting +var ( + writer = os.Stderr + Green = color.New(color.FgGreen) + Blue = color.New(color.FgBlue) + Red = color.New(color.FgRed) + Yellow = color.New(color.FgYellow) + + Log struct { + Success, Notice, Info, Error, + Warning func(text ...interface{}) + } + // LiveUI to print live data to terminal + LiveUI struct { + count int + Start func() + Print func(text ...string) + } +) + +func init() { + // Initialise colored text output + Log.Success = func(text ...interface{}) { Green.Fprintln(writer, text...) } + Log.Notice = func(text ...interface{}) { fmt.Fprintln(writer, text...) } + Log.Info = func(text ...interface{}) { Blue.Fprintln(writer, text...) } + Log.Error = func(text ...interface{}) { Red.Fprintln(writer, text...) } + Log.Warning = func(text ...interface{}) { Yellow.Fprintln(writer, text...) } + + // Initialise Live rendering output + LiveUI.Start = func() { LiveUI.count = 0 } + LiveUI.Print = func(text ...string) { + // clear last count lines from terminal + for i := 0; i < LiveUI.count; i++ { + ansi.CursorPreviousLine(1) + ansi.EraseInLine(2) + } + // count number of lines in text + LiveUI.count = 1 + for _, str := range text { + LiveUI.count += strings.Count(str, "\n") + fmt.Println(str) + } + } +} + type ( // Opts is struct docopt binds flag data to Opts struct { @@ -216,7 +262,7 @@ func selSourceFile(files []string) (string, error) { Message: "Source file:", Options: files, }, &file) - pkg.PrintError(err, "") + PrintError(err, "") return file, nil } @@ -237,7 +283,7 @@ func selTmpltConfig(tmplt []cfg.Template) (*cfg.Template, error) { Message: "Template configuration:", Options: cfg.ListTmplts(tmplt...), }, &idx) - pkg.PrintError(err, "") + PrintError(err, "") return &tmplt[idx], nil } @@ -250,20 +296,43 @@ func prettyVerdict(verdict string) string { switch { case strings.HasPrefix(verdict, "TLE"): - return pkg.Yellow.Sprint(verdict) + return Yellow.Sprint(verdict) case strings.HasPrefix(verdict, "MLE"): - return pkg.Red.Sprint(verdict) + return Red.Sprint(verdict) case strings.HasPrefix(verdict, "WA"): - return pkg.Red.Sprint(verdict) + return Red.Sprint(verdict) case strings.HasPrefix(verdict, "Pretests passed"): - return pkg.Green.Sprint(verdict) + return Green.Sprint(verdict) case strings.HasPrefix(verdict, "Accepted"): - return pkg.Green.Sprint(verdict) + return Green.Sprint(verdict) default: return verdict } } +// PrintError outputs error (with custom message) +// and exits the program execution (if err != nil) +func PrintError(err error, desc string) { + if err != nil { + if desc != "" { + Log.Error(desc) + } + Log.Error(err.Error()) + os.Exit(0) + } +} + +// CreateFile copies data to dst (create if not exists) +// Returns absolute path to destination file +func CreateFile(data, dst string) string { + out, err := os.Create(dst) + PrintError(err, "File "+dst+" couldn't be created!") + defer out.Close() + + out.WriteString(data) + return dst +} + /* Parsing structure of problems ----------------------------- diff --git a/cmd/open.go b/cmd/open.go index 62b5c4b..e8b5f78 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -1,8 +1,6 @@ package cmd import ( - pkg "cf/packages" - "os/exec" "path" "runtime" @@ -12,7 +10,7 @@ import ( func (opt Opts) RunOpen() { // check if contest id is present if opt.contest == "" { - pkg.Log.Error("No contest id found") + Log.Error("No contest id found") return } link := opt.link diff --git a/cmd/pull.go b/cmd/pull.go index 7b017e8..90e813b 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -2,7 +2,6 @@ package cmd import ( cln "cf/client" - pkg "cf/packages" "fmt" "os" @@ -18,16 +17,16 @@ import ( // RunPull is called on running cf pull func (opt Opts) RunPull() { // fetch all submissions matching criteria - pkg.Log.Success("Pulling submissions of: " + opt.Handle) + Log.Success("Pulling submissions of: " + opt.Handle) // fetch all submissions matching criteria subs, err := cln.FetchSubs(opt.contest, opt.problem, opt.Handle) - pkg.PrintError(err, "Failed to extract submission status") + PrintError(err, "Failed to extract submission status") for _, sub := range subs { // fetch source code source, err := sub.FetchSubSource() if err != nil { - pkg.Log.Error("Failed to pull source code:" + sub.Sid) + Log.Error("Failed to pull source code:" + sub.Sid) continue } @@ -50,8 +49,8 @@ func (opt Opts) RunPull() { cwd, _ := os.Getwd() relPath := strings.TrimPrefix(filepath.Join(path, name), cwd) - pkg.CreateFile(source, filepath.Join(path, name)) - pkg.Log.Success(fmt.Sprintf("Fetched %v %v to .%v", + CreateFile(source, filepath.Join(path, name)) + Log.Success(fmt.Sprintf("Fetched %v %v to .%v", sub.Contest, sub.Problem, relPath)) break } diff --git a/cmd/submit.go b/cmd/submit.go index b9be8d0..f11ebab 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -3,7 +3,6 @@ package cmd import ( cln "cf/client" cfg "cf/config" - pkg "cf/packages" "time" @@ -14,50 +13,50 @@ import ( func (opt Opts) RunSubmit() { // check if problem id is present if opt.problem == "" { - pkg.Log.Error("No problem id found") + Log.Error("No problem id found") return } // find code file to submit file, err := selSourceFile(cln.FindSourceFiles(opt.File)) - pkg.PrintError(err, "Failed to select source file") + PrintError(err, "Failed to select source file") // find template config to use t, err := selTmpltConfig(cln.FindTmpltsConfig(file)) - pkg.PrintError(err, "Failed to select template configuration") + PrintError(err, "Failed to select template configuration") // check login status usr, err := cln.LoggedInUsr() - pkg.PrintError(err, "Failed to check login status") + PrintError(err, "Failed to check login status") if usr == "" { // exit if no saved login configurations found if cfg.Session.Handle == "" || cfg.Session.Passwd == "" { - pkg.Log.Error("No login details configured") - pkg.Log.Notice("Configure login details through cf config") + Log.Error("No login details configured") + Log.Notice("Configure login details through cf config") return } // attempt relogin - pkg.Log.Warning("No logged in user session found") - pkg.Log.Info("Attempting relogin: " + cfg.Session.Handle) + Log.Warning("No logged in user session found") + Log.Info("Attempting relogin: " + cfg.Session.Handle) status, err := cln.Relogin() - pkg.PrintError(err, "Failed to login") + PrintError(err, "Failed to login") if status == true { // logged in successfully - pkg.Log.Success("Login successful") + Log.Success("Login successful") } else { - pkg.Log.Error("Login failed") - pkg.Log.Notice("Configure login details through 'cf config'") + Log.Error("Login failed") + Log.Notice("Configure login details through 'cf config'") return } } else { // output handle details of current user // this is in else loop, since current user is already // being displayed during relogin above - pkg.Log.Notice("Current user: " + usr) + Log.Notice("Current user: " + usr) } // main submit code runs here err = cln.Submit(opt.contest, opt.problem, t.LangID, file, opt.link) - pkg.PrintError(err, "Failed to submit source code") - pkg.Log.Success("Submitted") + PrintError(err, "Failed to submit source code") + Log.Success("Submitted") // watch submission verdict opt.watch() @@ -66,14 +65,14 @@ func (opt Opts) RunSubmit() { func (opt Opts) watch() { // infinite loop till verdicts declared - pkg.LiveUI.Start() + LiveUI.Start() for query := opt.problem; ; { // query param to fetch submitted code verdict and not latest verdict in prob // fetch submission status from contest every second start := time.Now() data, err := cln.WatchSubmissions(opt.contest, query, opt.link) - pkg.PrintError(err, "Failed to extract submissions in contest.") + PrintError(err, "Failed to extract submissions in contest.") sub := data[0] query = sub.ID @@ -85,10 +84,10 @@ func (opt Opts) watch() { if sub.Waiting == "false" { tbl.AddRow("Memory:", sub.Memory) tbl.AddRow("Time:", sub.Time) - pkg.LiveUI.Print(tbl.String()) + LiveUI.Print(tbl.String()) break } - pkg.LiveUI.Print(tbl.String()) + LiveUI.Print(tbl.String()) // sleep for 1 second time.Sleep(time.Second - time.Since(start)) } diff --git a/cmd/test.go b/cmd/test.go index 0a886ad..2ab4d32 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -3,21 +3,24 @@ package cmd import ( cln "cf/client" cfg "cf/config" - pkg "cf/packages" + "fmt" "os" "os/exec" "strings" + + "github.com/fatih/color" + "github.com/gosuri/uitable" ) // RunTest is called on running `cf test` func (opt Opts) RunTest() { // find code file to test file, err := selSourceFile(cln.FindSourceFiles(opt.File)) - pkg.PrintError(err, "Failed to select source file") + PrintError(err, "Failed to select source file") // find template configs to use t, err := selTmpltConfig(cln.FindTmpltsConfig(file)) - pkg.PrintError(err, "Failed to select template configuration") + PrintError(err, "Failed to select template configuration") // main testing starts here!! e := Env{ @@ -32,10 +35,10 @@ func (opt Opts) RunTest() { if t.PreScript != "" { // replace placeholders in script script := e.ReplPlaceholder(t.PreScript) - pkg.Log.Notice(script) + Log.Notice(script) // run script with timer of 20 secs _, _, err := cln.ExecScript(script, "", 1e9) - pkg.PrintError(err, "") + PrintError(err, "") } if opt.Custom == false { @@ -50,10 +53,10 @@ func (opt Opts) RunTest() { if t.PostScript != "" { // replace placeholders in script script := e.ReplPlaceholder(t.PostScript) - pkg.Log.Notice(script) + Log.Notice(script) // run script with timer of 20 secs _, _, err := cln.ExecScript(script, "", 1e9) - pkg.PrintError(err, "") + PrintError(err, "") } return } @@ -63,7 +66,7 @@ func (opt Opts) RunTest() { func (opt Opts) tradJudge(t cfg.Template, e Env) { // fetch test cases from current directory inp, out, err := cln.FindTests() - pkg.PrintError(err, "Failed to parse sample tests") + PrintError(err, "Failed to parse sample tests") // run judge for each test file for i := 0; i < len(inp); i++ { @@ -76,21 +79,21 @@ func (opt Opts) tradJudge(t cfg.Template, e Env) { switch { case elapsed.Seconds() >= float64(opt.Tl): // print TLE message (add support for custom time limit) - pkg.Yellow.Printf("#%d: TLE .... %v\n", i, elapsed.String()) + Yellow.Printf("#%d: TLE .... %v\n", i, elapsed.String()) case err != nil: // print RTE message with error data - pkg.Red.Printf("#%d: RTE .... %v\n", i, err.Error()) + Red.Printf("#%d: RTE .... %v\n", i, err.Error()) case stdout != out[i]: // print WA message and diff output - pkg.Red.Printf("#%d: WA .... %v\n", i, elapsed.String()) - diff := cln.PrintDiff(inp[i], stdout, out[i]) - pkg.Log.Info(diff) + Red.Printf("#%d: WA .... %v\n", i, elapsed.String()) + diff := printDiff(inp[i], stdout, out[i]) + Log.Info(diff) default: // print AC message - pkg.Green.Printf("#%d: AC .... %v\n", i, elapsed.String()) + Green.Printf("#%d: AC .... %v\n", i, elapsed.String()) } } return @@ -114,9 +117,44 @@ func (opt Opts) spclJudge(t cfg.Template, e Env) { */ // inform user that interactive judge has started - pkg.Log.Success("-----Judge begins-----\n") + Log.Success("-----Judge begins-----\n") cmd.Run() - pkg.Log.Success("\n-----Judge closed-----") + Log.Success("\n-----Judge closed-----") return } + +// printDiff is run if outputs don't match +// returns input data, and then the diff of => out vs ans +func printDiff(inp, out, ans string) string { + // variable to hold diff output + var diff strings.Builder + headerfmt := Blue.Add(color.Underline).SprintfFunc() + // print input data + fmt.Fprintln(&diff, headerfmt("Input")) + fmt.Fprintln(&diff, inp) + + // break output into lines + str1 := strings.Split(out, "\n") + str2 := strings.Split(ans, "\n") + // equalize string lengths + if len(str1) < len(str2) { + str1 = append(str1, make([]string, len(str2)-len(str1))...) + } else { + str2 = append(str2, make([]string, len(str1)-len(str2))...) + } + + // print output diff data + tbl := uitable.New() + tbl.Separator = " | " + + tbl.AddRow(headerfmt("Actual Output"), headerfmt("Expected Output")) + // iterate over every row of outputs + for i := 0; i < len(str1); i++ { + tbl.AddRow(str1[i], str2[i]) + } + fmt.Fprintln(&diff, tbl) + fmt.Fprintln(&diff) + + return diff.String() +} diff --git a/cmd/upgrade.go b/cmd/upgrade.go index c7cb33a..2003927 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -20,20 +20,20 @@ func RunUpgrade() { // determine latest release version using github API link := "https://api.github.com/repos/cp-tools/cf/releases/latest" resp, err := pkg.GetReqBody(&http.Client{}, link) - pkg.PrintError(err, "Failed to fetch latest release") + PrintError(err, "Failed to fetch latest release") // check version of latest release from API resp latest := gjson.GetBytes(resp, "tag_name").String() lVers := semver.MustParse(latest[1:]) // check if current release is same as latest release if cVers.GTE(lVers) { - pkg.Log.Success(fmt.Sprintf("Current version (v%v) is the latest", cVers.String())) + Log.Success(fmt.Sprintf("Current version (v%v) is the latest", cVers.String())) return } // new release found (fetch and print release notes) releaseNotes := gjson.GetBytes(resp, "body").String() - pkg.Log.Success(fmt.Sprintf("New release (v%v) found", lVers.String())) - pkg.Log.Notice(releaseNotes) + Log.Success(fmt.Sprintf("New release (v%v) found", lVers.String())) + Log.Notice(releaseNotes) fmt.Println() prompt := true @@ -42,19 +42,19 @@ func RunUpgrade() { cVers.String(), lVers.String()), Default: true, }, &prompt) - pkg.PrintError(err, "") + PrintError(err, "") if prompt == false { - pkg.Log.Info("Tool not upgraded") + Log.Info("Tool not upgraded") return } // url of tar file to download link = fmt.Sprintf("https://github.com/cp-tools/cf/releases/download/%v/cf_%v_%v.tar.gz", latest, runtime.GOOS, runtime.GOARCH) - pkg.Log.Info("Downloading update. Please wait.") + Log.Info("Downloading update. Please wait.") err = cln.SelfUpgrade(link) - pkg.PrintError(err, "Failed to update tool") + PrintError(err, "Failed to update tool") - pkg.Log.Success("Successfully updated to v" + lVers.String()) + Log.Success("Successfully updated to v" + lVers.String()) return } diff --git a/cmd/watch.go b/cmd/watch.go index 350ba29..f683218 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -2,7 +2,6 @@ package cmd import ( cln "cf/client" - pkg "cf/packages" "fmt" "strings" @@ -16,11 +15,11 @@ import ( func (opt Opts) RunWatch() { // check if contest id is present if opt.contest == "" { - pkg.Log.Error("No contest id found") + Log.Error("No contest id found") return } // header formatting for table - headerfmt := pkg.Blue.Add(color.Underline).SprintfFunc() + headerfmt := Blue.Add(color.Underline).SprintfFunc() if opt.SubCnt == 0 { // submissions aren't specified to be parsed @@ -28,7 +27,7 @@ func (opt Opts) RunWatch() { // fetch contest solve status data, err := cln.WatchContest(opt.contest, opt.link) - pkg.PrintError(err, "Failed to extract contest solve status") + PrintError(err, "Failed to extract contest solve status") // init table with header + color tbl := uitable.New() @@ -53,9 +52,9 @@ func (opt Opts) RunWatch() { clean := func(status string) string { switch status { case "accepted-problem": - return pkg.Green.Sprint("AC") + return Green.Sprint("AC") case "rejected-problem": - return pkg.Red.Sprint("RE") + return Red.Sprint("RE") default: return "NA" } @@ -67,13 +66,13 @@ func (opt Opts) RunWatch() { } else { // infinite loop till verdicts declared - pkg.LiveUI.Start() + LiveUI.Start() for { // timer to fetch data in interval of 1 second start := time.Now() // fetch contest submission status data, err := cln.WatchSubmissions(opt.contest, opt.problem, opt.link) - pkg.PrintError(err, "Failed to extract submissions in contest") + PrintError(err, "Failed to extract submissions in contest") // create new table tbl := uitable.New() @@ -97,7 +96,7 @@ func (opt Opts) RunWatch() { isPending = true } } - pkg.LiveUI.Print(tbl.String()) + LiveUI.Print(tbl.String()) if isPending == false { break diff --git a/config/sessions.go b/config/sessions.go index db5c231..f8e20f7 100644 --- a/config/sessions.go +++ b/config/sessions.go @@ -1,8 +1,6 @@ package cfg import ( - pkg "cf/packages" - "encoding/json" "io/ioutil" "net/http" @@ -12,34 +10,42 @@ import ( "github.com/infixint943/cookiejar" ) -// Session holds cookies and other request header data -// password is AES encrypted and stored securely -var Session struct { - Handle string `json:"handle"` - Passwd string `json:"password"` - Cookies *cookiejar.Jar `json:"cookies"` - Client http.Client `json:"-"` -} +var ( + // Session holds cookies and other request header data + // password is AES encrypted and stored securely + Session struct { + Handle string `json:"handle"` + Passwd string `json:"password"` + Cookies *cookiejar.Jar `json:"cookies"` + Client http.Client `json:"-"` + } + // path to sessions.json file + sessPath string +) -var sessPath string +// set default values of Session struct +func init() { + Session.Handle = "" + Session.Passwd = "" + Session.Cookies, _ = cookiejar.New(nil) + Session.Client = *http.DefaultClient +} // InitSession reads data from sessions.json -func InitSession(path string) { +func InitSession(path string) error { // set sessions.json file path sessPath = path - Session.Handle = "" - Session.Cookies, _ = cookiejar.New(nil) - proxyURL := http.ProxyFromEnvironment - - file, err := ioutil.ReadFile(sessPath) + file, err := os.OpenFile(sessPath, os.O_RDWR|os.O_CREATE, 0666) + defer file.Close() if err != nil { - pkg.Log.Warning("File sessions.json doesn't exist") - pkg.Log.Info("Creating sessions.json file...") - SaveSession() + return err } - json.Unmarshal(file, &Session) - // configure proxy if set + + body, _ := ioutil.ReadAll(file) + json.Unmarshal(body, &Session) + // proxy configuration + proxyURL := http.ProxyFromEnvironment if Settings.Proxy != "" { proxy, _ := url.Parse(Settings.Proxy) proxyURL = http.ProxyURL(proxy) @@ -48,14 +54,19 @@ func InitSession(path string) { // instantiate client with proxy configurations Session.Client = http.Client{Jar: Session.Cookies, Transport: &http.Transport{Proxy: proxyURL}} + + return nil } // SaveSession saves the data to sessions.json -func SaveSession() { +func SaveSession() error { // create sessions.json file and log err (if any) - file, err := os.Create(sessPath) - pkg.PrintError(err, "Failed to create sessions.json file") + file, err := os.OpenFile(sessPath, os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return err + } body, _ := json.MarshalIndent(Session, "", "\t") file.Write(body) + return nil } diff --git a/config/settings.go b/config/settings.go index 7e59677..4ff8d33 100644 --- a/config/settings.go +++ b/config/settings.go @@ -1,52 +1,58 @@ package cfg import ( - pkg "cf/packages" - "encoding/json" "io/ioutil" "os" ) -// Settings holds configured settings data of the tool -var Settings struct { - DfltTmplt int `json:"default_template"` - GenOnFetch bool `json:"gen_on_fetch"` - Host string `json:"host"` - Proxy string `json:"proxy"` - WSName string `json:"workspace_name"` -} +var ( + // Settings holds configured settings data of the tool + Settings struct { + DfltTmplt int `json:"default_template"` + GenOnFetch bool `json:"gen_on_fetch"` + Host string `json:"host"` + Proxy string `json:"proxy"` + WSName string `json:"workspace_name"` + } -var settPath string + settPath string +) func init() { // initialise default values of Settings struct Settings.DfltTmplt = -1 Settings.GenOnFetch = false - Settings.Host = "https://codeforces/com" + Settings.Host = "https://codeforces.com" Settings.Proxy = "" Settings.WSName = "codeforces" } // InitSettings reads settings.json file -func InitSettings(path string) { +func InitSettings(path string) error { // set settings.json file path settPath = path - file, err := ioutil.ReadFile(settPath) + file, err := os.OpenFile(settPath, os.O_RDWR|os.O_CREATE, 0666) + defer file.Close() if err != nil { - pkg.Log.Warning("File settings.json doesn't exist") - pkg.Log.Info("Creating settings.json file") - SaveSettings() + return err } - json.Unmarshal(file, &Settings) + + body, _ := ioutil.ReadAll(file) + json.Unmarshal(body, &Settings) + return nil } // SaveSettings to settings.json file -func SaveSettings() { - file, err := os.Create(settPath) - pkg.PrintError(err, "Failed to create settings.json file") +func SaveSettings() error { + // create settings.json file + file, err := os.OpenFile(settPath, os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return err + } body, _ := json.MarshalIndent(Settings, "", "\t") file.Write(body) + return nil } diff --git a/config/templates.go b/config/templates.go index 2320931..af6321c 100644 --- a/config/templates.go +++ b/config/templates.go @@ -1,8 +1,6 @@ package cfg import ( - pkg "cf/packages" - "encoding/json" "io/ioutil" "os" @@ -20,32 +18,40 @@ type Template struct { PostScript string `json:"post_script"` } -// Templates holds all configured templates of user -var Templates []Template - -var tmpltPath string +var ( + // Templates holds all configured templates of user + Templates []Template + tmpltPath string +) // InitTemplates reads data from templates.json -func InitTemplates(path string) { +func InitTemplates(path string) error { // set templates.json file path tmpltPath = path - file, err := ioutil.ReadFile(tmpltPath) + file, err := os.OpenFile(tmpltPath, os.O_RDWR|os.O_CREATE, 0666) + defer file.Close() if err != nil { - pkg.Log.Warning("File templates.json doesn't exist") - pkg.Log.Info("Creating templates.json file...") - SaveTemplates() + return err } - json.Unmarshal(file, &Templates) + + body, _ := ioutil.ReadAll(file) + json.Unmarshal(body, &Templates) + return nil + } // SaveTemplates to settings.json file -func SaveTemplates() { - file, err := os.Create(tmpltPath) - pkg.PrintError(err, "Failed to create templates.json file") +func SaveTemplates() error { + // create templates.json file + file, err := os.OpenFile(tmpltPath, os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return err + } body, _ := json.MarshalIndent(Templates, "", "\t") file.Write(body) + return nil } // ListTmplts returns an array of required template aliases diff --git a/packages/misc.go b/packages/misc.go deleted file mode 100644 index b4743fd..0000000 --- a/packages/misc.go +++ /dev/null @@ -1,91 +0,0 @@ -package pkg - -import ( - "fmt" - "os" - "strings" - - "github.com/PuerkitoBio/goquery" - "github.com/fatih/color" - "github.com/k0kubun/go-ansi" -) - -// Global Variables for different UI formatting -var ( - writer = os.Stderr - Green = color.New(color.FgGreen) - Blue = color.New(color.FgBlue) - Red = color.New(color.FgRed) - Yellow = color.New(color.FgYellow) - - Log struct { - Success, Notice, Info, Error, - Warning func(text ...interface{}) - } - // LiveUI to print live data to terminal - LiveUI struct { - count int - Start func() - Print func(text ...string) - } -) - -func init() { - // Initialise colored text output - Log.Success = func(text ...interface{}) { Green.Fprintln(writer, text...) } - Log.Notice = func(text ...interface{}) { fmt.Fprintln(writer, text...) } - Log.Info = func(text ...interface{}) { Blue.Fprintln(writer, text...) } - Log.Error = func(text ...interface{}) { Red.Fprintln(writer, text...) } - Log.Warning = func(text ...interface{}) { Yellow.Fprintln(writer, text...) } - - // Initialise Live rendering output - LiveUI.Start = func() { LiveUI.count = 0 } - LiveUI.Print = func(text ...string) { - // clear last count lines from terminal - for i := 0; i < LiveUI.count; i++ { - ansi.CursorPreviousLine(1) - ansi.EraseInLine(2) - } - // count number of lines in text - LiveUI.count = 1 - for _, str := range text { - LiveUI.count += strings.Count(str, "\n") - fmt.Println(str) - } - } -} - -// PrintError outputs error (with custom message) -// and exits the program execution (if err != nil) -func PrintError(err error, desc string) { - if err != nil { - if desc != "" { - Log.Error(desc) - } - Log.Error(err.Error()) - os.Exit(0) - } -} - -// CreateFile copies data to dst (create if not exists) -// Returns absolute path to destination file -func CreateFile(data, dst string) string { - out, err := os.Create(dst) - PrintError(err, "File "+dst+" couldn't be created!") - defer out.Close() - - out.WriteString(data) - return dst -} - -// GetText extracts text from particular html data -func GetText(sel *goquery.Selection, query string) string { - str := sel.Find(query).Text() - return strings.TrimSpace(str) -} - -// GetAttr extracts attribute valur of particular html data -func GetAttr(sel *goquery.Selection, query, attr string) string { - str := sel.Find(query).AttrOr(attr, "") - return strings.TrimSpace(str) -} From 633664c857f8bf8bec404f6958a6638c50651cd1 Mon Sep 17 00:00:00 2001 From: Adithya Dsilva Date: Tue, 19 May 2020 18:14:26 +0530 Subject: [PATCH 4/5] Refractor pkg module. Refractor upgrade code --- client/fetch.go | 9 ++++---- client/login.go | 13 +++++------ client/misc.go | 46 +++++++++++++++++++++++++++++++++++++++ client/pull.go | 5 ++--- client/submit.go | 9 ++++---- client/upgrade.go | 29 ++++++++++++++++++++++-- client/watch.go | 9 ++++---- cmd/upgrade.go | 35 ++++++++--------------------- packages/requests.go | 52 -------------------------------------------- 9 files changed, 102 insertions(+), 105 deletions(-) delete mode 100644 packages/requests.go diff --git a/client/fetch.go b/client/fetch.go index bdce997..75c7178 100644 --- a/client/fetch.go +++ b/client/fetch.go @@ -2,7 +2,6 @@ package cln import ( cfg "cf/config" - pkg "cf/packages" "bytes" "fmt" @@ -17,9 +16,9 @@ import ( func FindCountdown(contest string, link url.URL) (int64, error) { // This implementation contains redirection prevention c := cfg.Session.Client - c.CheckRedirect = pkg.RedirectCheck + c.CheckRedirect = RedirectCheck link.Path = path.Join(link.Path, "countdown") - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return 0, err } else if len(body) == 0 { @@ -39,7 +38,7 @@ func FindCountdown(contest string, link url.URL) (int64, error) { func FetchProbs(contest string, link url.URL) ([]string, error) { // no need of modifying link as it already points to dashboard c := cfg.Session.Client - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return nil, err } @@ -68,7 +67,7 @@ func FetchTests(contest, problem string, link url.URL) ([][]string, [][]string, link.Path = path.Join(link.Path, "problem", problem) } - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return nil, nil, err } diff --git a/client/login.go b/client/login.go index 99b65de..e364932 100644 --- a/client/login.go +++ b/client/login.go @@ -2,7 +2,6 @@ package cln import ( cfg "cf/config" - pkg "cf/packages" "encoding/hex" "net/url" @@ -22,18 +21,18 @@ func Login(usr, passwd string) (bool, error) { link, _ := url.Parse(cfg.Settings.Host) link.Path = path.Join(link.Path, "enter") - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return false, err } // Hidden form data - csrf := pkg.FindCsrf(body) + csrf := FindCsrf(body) ftaa := "yzo0kk4bhlbaw83g2q" bfaa := "883b704dbe5c70e1e61de4d8aff2da32" // Post form (aka login using creds) - body, err = pkg.PostReqBody(&c, link.String(), url.Values{ + body, err = PostReqBody(&c, link.String(), url.Values{ "csrf_token": {csrf}, "action": {"enter"}, "ftaa": {ftaa}, @@ -47,7 +46,7 @@ func Login(usr, passwd string) (bool, error) { return false, err } - usr = pkg.FindHandle(body) + usr = FindHandle(body) if usr != "" { // create aes 256 encryption and encode as // hex string and save to sessions.json @@ -69,12 +68,12 @@ func LoggedInUsr() (string, error) { // fetch home page and check if logged in c := cfg.Session.Client link, _ := url.Parse(cfg.Settings.Host) - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return "", err } - return pkg.FindHandle(body), nil + return FindHandle(body), nil } // Relogin extracts handle/passwd from sessions.json diff --git a/client/misc.go b/client/misc.go index 995ac4a..1320064 100644 --- a/client/misc.go +++ b/client/misc.go @@ -1,7 +1,11 @@ package cln import ( + "bytes" "fmt" + "io/ioutil" + "net/http" + "net/url" "strings" "github.com/PuerkitoBio/goquery" @@ -119,6 +123,48 @@ var ( } ) +func parseBody(resp *http.Response) ([]byte, error) { + defer resp.Body.Close() + return ioutil.ReadAll(resp.Body) +} + +// GetReqBody executes a GET request to url and returns the request body +func GetReqBody(client *http.Client, url string) ([]byte, error) { + resp, err := client.Get(url) + if err != nil { + return nil, err + } + return parseBody(resp) +} + +// PostReqBody executes a POST request (with values: data) to url and returns the request body +func PostReqBody(client *http.Client, url string, data url.Values) ([]byte, error) { + resp, err := client.PostForm(url, data) + if err != nil { + return nil, err + } + return parseBody(resp) +} + +// FindHandle scrapes handle from REQUEST body +func FindHandle(body []byte) string { + doc, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) + val := doc.Find("#header").Find("a[href^=\"/profile/\"]").Text() + return val +} + +// FindCsrf extracts Csrf from REQUEST body +func FindCsrf(body []byte) string { + doc, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) + val, _ := doc.Find(".csrf-token").Attr("data-csrf") + return val +} + +// RedirectCheck prevents redirection and returns requested page info +func RedirectCheck(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse +} + // GetText extracts text from particular html data func GetText(sel *goquery.Selection, query string) string { str := sel.Find(query).Text() diff --git a/client/pull.go b/client/pull.go index 43dfba4..284c089 100644 --- a/client/pull.go +++ b/client/pull.go @@ -2,7 +2,6 @@ package cln import ( cfg "cf/config" - pkg "cf/packages" "bytes" "fmt" @@ -35,7 +34,7 @@ func FetchSubs(contest, problem, handle string) ([]Sub, error) { q.Set("handle", handle) link.RawQuery = q.Encode() - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return nil, err } @@ -97,7 +96,7 @@ func (sub *Sub) FetchSubSource() (string, error) { c := cfg.Session.Client link, _ := url.Parse(cfg.Settings.Host) link.Path = path.Join(link.Path, contClass, sub.Contest, "submission", sub.Sid) - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return "", err } diff --git a/client/submit.go b/client/submit.go index 66246b6..7d26ad9 100644 --- a/client/submit.go +++ b/client/submit.go @@ -2,7 +2,6 @@ package cln import ( cfg "cf/config" - pkg "cf/packages" "bytes" "fmt" @@ -17,9 +16,9 @@ import ( func Submit(contest, problem, langID, file string, link url.URL) error { // form redirection prevention is removed while submitting c := cfg.Session.Client - c.CheckRedirect = pkg.RedirectCheck + c.CheckRedirect = RedirectCheck link.Path = path.Join(link.Path, "submit") - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return err } else if len(body) == 0 { @@ -30,12 +29,12 @@ func Submit(contest, problem, langID, file string, link url.URL) error { // read source file data, _ := ioutil.ReadFile(file) // hidden form data - csrf := pkg.FindCsrf(body) + csrf := FindCsrf(body) ftaa := "yzo0kk4bhlbaw83g2q" bfaa := "883b704dbe5c70e1e61de4d8aff2da32" // post form data (remove redirection prevention) c.CheckRedirect = nil - body, err = pkg.PostReqBody(&c, link.String(), url.Values{ + body, err = PostReqBody(&c, link.String(), url.Values{ "csrf_token": {csrf}, "ftaa": {ftaa}, "bfaa": {bfaa}, diff --git a/client/upgrade.go b/client/upgrade.go index 798631e..433ae40 100644 --- a/client/upgrade.go +++ b/client/upgrade.go @@ -3,17 +3,42 @@ package cln import ( "archive/tar" "compress/gzip" + "fmt" "io" "io/ioutil" "net/http" "os" "path" + "runtime" + + "github.com/blang/semver" + "github.com/tidwall/gjson" ) +// FetchLatest determines latest version through API page of github +func FetchLatest(owner, repo string) (semver.Version, string, error) { + // url link of github API to fetch latest version data from + link := fmt.Sprintf("https://api.github.com/repos/%v/%v/releases/latest", owner, repo) + resp, err := GetReqBody(http.DefaultClient, link) + if err != nil { + return semver.Version{}, "", err + } + // gjson is used in pull too. So being used again here! + latest := gjson.GetBytes(resp, "tag_name").String() + lVers, err := semver.ParseTolerant(latest) + releaseNotes := gjson.GetBytes(resp, "body").String() + + return lVers, releaseNotes, err +} + // SelfUpgrade downloads latest release and overwrites current binary // Copied from https://github.com/yitsushi/totp-cli/blob/master/command/update.go -func SelfUpgrade(url string) error { - resp, err := http.Get(url) +func SelfUpgrade(owner, repo, vers string) error { + // compile link from passed parameters to fetch binary matching current build + link := fmt.Sprintf("https://github.com/%v/%v/releases/download/v%v/cf_%v_%v.tar.gz", + owner, repo, vers, runtime.GOOS, runtime.GOARCH) + + resp, err := http.Get(link) if err != nil { return err } diff --git a/client/watch.go b/client/watch.go index d0941d8..997189f 100644 --- a/client/watch.go +++ b/client/watch.go @@ -2,7 +2,6 @@ package cln import ( cfg "cf/config" - pkg "cf/packages" "bytes" "net/url" @@ -33,10 +32,10 @@ type ( func WatchSubmissions(contest, query string, link url.URL) ([]Submission, error) { // This implementation contains redirection prevention c := cfg.Session.Client - c.CheckRedirect = pkg.RedirectCheck + c.CheckRedirect = RedirectCheck // fetch all submissions in contest link.Path = path.Join(link.Path, "my") - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return nil, err } else if len(body) == 0 { @@ -70,9 +69,9 @@ func WatchSubmissions(contest, query string, link url.URL) ([]Submission, error) func WatchContest(contest string, link url.URL) ([]Problem, error) { // This implementation contains redirection prevention c := cfg.Session.Client - c.CheckRedirect = pkg.RedirectCheck + c.CheckRedirect = RedirectCheck // fetch contest dashboard page - body, err := pkg.GetReqBody(&c, link.String()) + body, err := GetReqBody(&c, link.String()) if err != nil { return nil, err } else if len(body) == 0 { diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 2003927..720fc2a 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -2,57 +2,40 @@ package cmd import ( cln "cf/client" - pkg "cf/packages" - - "fmt" - "net/http" - "runtime" "github.com/AlecAivazis/survey/v2" "github.com/blang/semver" - "github.com/tidwall/gjson" ) // RunUpgrade is called on running `cf upgrade` func RunUpgrade() { - // parse current version - cVers := semver.MustParse(Version) - // determine latest release version using github API - link := "https://api.github.com/repos/cp-tools/cf/releases/latest" - resp, err := pkg.GetReqBody(&http.Client{}, link) + // parse current version from set version number + cVers, _ := semver.ParseTolerant(Version) + lVers, releaseNotes, err := cln.FetchLatest("cp-tools", "cf") PrintError(err, "Failed to fetch latest release") - - // check version of latest release from API resp - latest := gjson.GetBytes(resp, "tag_name").String() - lVers := semver.MustParse(latest[1:]) // check if current release is same as latest release if cVers.GTE(lVers) { - Log.Success(fmt.Sprintf("Current version (v%v) is the latest", cVers.String())) + Log.Success("Current version (v" + cVers.String() + ") is the latest") return } // new release found (fetch and print release notes) - releaseNotes := gjson.GetBytes(resp, "body").String() - Log.Success(fmt.Sprintf("New release (v%v) found", lVers.String())) - Log.Notice(releaseNotes) - fmt.Println() + Log.Success("New release (v" + lVers.String() + ") found") + Log.Notice(releaseNotes, "\n") prompt := true err = survey.AskOne(&survey.Confirm{ - Message: fmt.Sprintf("Do you wish to upgrade from v%v to v%v?", - cVers.String(), lVers.String()), + Message: "Upgrade from v" + cVers.String() + " to v" + lVers.String(), Default: true, }, &prompt) PrintError(err, "") + if prompt == false { Log.Info("Tool not upgraded") return } - // url of tar file to download - link = fmt.Sprintf("https://github.com/cp-tools/cf/releases/download/%v/cf_%v_%v.tar.gz", - latest, runtime.GOOS, runtime.GOARCH) Log.Info("Downloading update. Please wait.") - err = cln.SelfUpgrade(link) + err = cln.SelfUpgrade("cp-tools", "cf", lVers.String()) PrintError(err, "Failed to update tool") Log.Success("Successfully updated to v" + lVers.String()) diff --git a/packages/requests.go b/packages/requests.go deleted file mode 100644 index 7664713..0000000 --- a/packages/requests.go +++ /dev/null @@ -1,52 +0,0 @@ -package pkg - -import ( - "bytes" - "io/ioutil" - "net/http" - "net/url" - - "github.com/PuerkitoBio/goquery" -) - -func parseBody(resp *http.Response) ([]byte, error) { - defer resp.Body.Close() - return ioutil.ReadAll(resp.Body) -} - -// GetReqBody executes a GET request to url and returns the request body -func GetReqBody(client *http.Client, url string) ([]byte, error) { - resp, err := client.Get(url) - if err != nil { - return nil, err - } - return parseBody(resp) -} - -// PostReqBody executes a POST request (with values: data) to url and returns the request body -func PostReqBody(client *http.Client, url string, data url.Values) ([]byte, error) { - resp, err := client.PostForm(url, data) - if err != nil { - return nil, err - } - return parseBody(resp) -} - -// FindHandle scrapes handle from REQUEST body -func FindHandle(body []byte) string { - doc, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) - val := doc.Find("#header").Find("a[href^=\"/profile/\"]").Text() - return val -} - -// FindCsrf extracts Csrf from REQUEST body -func FindCsrf(body []byte) string { - doc, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) - val, _ := doc.Find(".csrf-token").Attr("data-csrf") - return val -} - -// RedirectCheck prevents redirection and returns requested page info -func RedirectCheck(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse -} From bcd978a2517b381c6ee79c0f5bdf35c4b7ba412a Mon Sep 17 00:00:00 2001 From: Adithya Dsilva Date: Wed, 20 May 2020 01:56:20 +0530 Subject: [PATCH 5/5] Update global function comments [WIP] --- client/fetch.go | 40 ++++++++++++++++++++++++++++++---------- client/login.go | 42 +++++++++++++++++++++++++++++++----------- client/misc.go | 28 ++++++++++++++-------------- client/pull.go | 28 +++++++++++++++++++++------- client/submit.go | 16 +++++++++++----- client/upgrade.go | 2 +- client/watch.go | 30 +++++++++++++++--------------- 7 files changed, 123 insertions(+), 63 deletions(-) diff --git a/client/fetch.go b/client/fetch.go index 75c7178..b9e0d97 100644 --- a/client/fetch.go +++ b/client/fetch.go @@ -12,13 +12,19 @@ import ( "github.com/PuerkitoBio/goquery" ) -// FindCountdown parses countdown (if exists) from countdown page +/* +FindCountdown parses and returns number of seconds remaining +before contest begins. Returns 0 if countdown has already ended. +Virtual contests (of the current user session) are supported too. + +If countdown page doesn't exsit, returns error ErrContestNotExists +*/ func FindCountdown(contest string, link url.URL) (int64, error) { // This implementation contains redirection prevention c := cfg.Session.Client - c.CheckRedirect = RedirectCheck + c.CheckRedirect = redirectCheck link.Path = path.Join(link.Path, "countdown") - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return 0, err } else if len(body) == 0 { @@ -34,11 +40,17 @@ func FindCountdown(contest string, link url.URL) (int64, error) { return h*3600 + m*60 + s, nil } -// FetchProbs finds all problems present in the contest +/* +FetchProbs parses and returns problem code's of all problems in contest. +Problem codes are returned in their lowercase versions. For example, +A => a, F1 => f1, C2 => c2 etc. + +If contest dashboard doesn't exist, returns error ErrContestNotExists +*/ func FetchProbs(contest string, link url.URL) ([]string, error) { // no need of modifying link as it already points to dashboard c := cfg.Session.Client - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return nil, err } @@ -52,10 +64,18 @@ func FetchProbs(contest string, link url.URL) ([]string, error) { return probs, nil } -// FetchTests extracts test cases of the problem(s) in contest -// Returns 2d slice mapping to input and output -// If problem == "", fetch all problem test cases -// else, only fetch of given problem. +/* +FetchTests parses test cases of problems in the contest and returns +the sample inputs/outputs as a 2d slice of strings. + +If problem parameter is empty, returns test cases of ALL problems in contest. +Samples are fetched from the contest's 'complete problemset' page. + +Otherwise, sample tests of only the specified problem is returned +Here, samples are fetched from the (individual) problem's page + +If problems page doesn't exist, returns error ErrContestNotExists +*/ func FetchTests(contest, problem string, link url.URL) ([][]string, [][]string, error) { c := cfg.Session.Client @@ -67,7 +87,7 @@ func FetchTests(contest, problem string, link url.URL) ([][]string, [][]string, link.Path = path.Join(link.Path, "problem", problem) } - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return nil, nil, err } diff --git a/client/login.go b/client/login.go index e364932..0b983ac 100644 --- a/client/login.go +++ b/client/login.go @@ -11,7 +11,16 @@ import ( "github.com/oleiade/serrure/aes" ) -// Login tries logginging in with user creds +/* +Login attempts logging in to configured host domain +with user credentials passed in the parameters. + +Returns true if login was successful (saves session to sessPath) +and false if login failed due to wrong credentials. + +If login failed for any other reason (other than wrong creds) +the respective hhtp error message is returned. +*/ func Login(usr, passwd string) (bool, error) { // instantiate http client, but remove // past user sessions to prevent redirection @@ -21,18 +30,18 @@ func Login(usr, passwd string) (bool, error) { link, _ := url.Parse(cfg.Settings.Host) link.Path = path.Join(link.Path, "enter") - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return false, err } // Hidden form data - csrf := FindCsrf(body) + csrf := findCsrf(body) ftaa := "yzo0kk4bhlbaw83g2q" bfaa := "883b704dbe5c70e1e61de4d8aff2da32" // Post form (aka login using creds) - body, err = PostReqBody(&c, link.String(), url.Values{ + body, err = postReqBody(&c, link.String(), url.Values{ "csrf_token": {csrf}, "action": {"enter"}, "ftaa": {ftaa}, @@ -46,7 +55,7 @@ func Login(usr, passwd string) (bool, error) { return false, err } - usr = FindHandle(body) + usr = findHandle(body) if usr != "" { // create aes 256 encryption and encode as // hex string and save to sessions.json @@ -62,22 +71,33 @@ func Login(usr, passwd string) (bool, error) { return (usr != ""), nil } -// LoggedInUsr checks and returns whether -// current session is logged in +/* +LoggedInUsr returns handle of currently logged in user +Session uses Session.Client data to pull homepage +and extract the handle of the logged in user. +Returns an empty string if no logged in user is found + +If http request failed, corresponding error is returned +*/ func LoggedInUsr() (string, error) { // fetch home page and check if logged in c := cfg.Session.Client link, _ := url.Parse(cfg.Settings.Host) - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return "", err } - return FindHandle(body), nil + return findHandle(body), nil } -// Relogin extracts handle/passwd from sessions.json -// and log's in with the credentials and returns status +/* +Relogin extracts user handle / passwd from the Session struct +and passes the credentials to function Login() to relogin again. +Returns same return values of function Login() + +If password couldn't be decrypted, returns error ErrDecodePasswdFailed +*/ func Relogin() (bool, error) { // decode hex data of encrypted password ciphertext, err := hex.DecodeString(cfg.Session.Passwd) diff --git a/client/misc.go b/client/misc.go index 1320064..17232a3 100644 --- a/client/misc.go +++ b/client/misc.go @@ -128,8 +128,8 @@ func parseBody(resp *http.Response) ([]byte, error) { return ioutil.ReadAll(resp.Body) } -// GetReqBody executes a GET request to url and returns the request body -func GetReqBody(client *http.Client, url string) ([]byte, error) { +// getReqBody executes a GET request to url and returns the request body +func getReqBody(client *http.Client, url string) ([]byte, error) { resp, err := client.Get(url) if err != nil { return nil, err @@ -137,8 +137,8 @@ func GetReqBody(client *http.Client, url string) ([]byte, error) { return parseBody(resp) } -// PostReqBody executes a POST request (with values: data) to url and returns the request body -func PostReqBody(client *http.Client, url string, data url.Values) ([]byte, error) { +// postReqBody executes a POST request (with values: data) to url and returns the request body +func postReqBody(client *http.Client, url string, data url.Values) ([]byte, error) { resp, err := client.PostForm(url, data) if err != nil { return nil, err @@ -146,33 +146,33 @@ func PostReqBody(client *http.Client, url string, data url.Values) ([]byte, erro return parseBody(resp) } -// FindHandle scrapes handle from REQUEST body -func FindHandle(body []byte) string { +// findHandle scrapes handle from REQUEST body +func findHandle(body []byte) string { doc, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) val := doc.Find("#header").Find("a[href^=\"/profile/\"]").Text() return val } -// FindCsrf extracts Csrf from REQUEST body -func FindCsrf(body []byte) string { +// findCsrf extracts Csrf from REQUEST body +func findCsrf(body []byte) string { doc, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) val, _ := doc.Find(".csrf-token").Attr("data-csrf") return val } -// RedirectCheck prevents redirection and returns requested page info -func RedirectCheck(req *http.Request, via []*http.Request) error { +// redirectCheck prevents redirection and returns requested page info +func redirectCheck(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } -// GetText extracts text from particular html data -func GetText(sel *goquery.Selection, query string) string { +// getText extracts text from particular html data +func getText(sel *goquery.Selection, query string) string { str := sel.Find(query).Text() return strings.TrimSpace(str) } -// GetAttr extracts attribute valur of particular html data -func GetAttr(sel *goquery.Selection, query, attr string) string { +// getAttr extracts attribute valur of particular html data +func getAttr(sel *goquery.Selection, query, attr string) string { str := sel.Find(query).AttrOr(attr, "") return strings.TrimSpace(str) } diff --git a/client/pull.go b/client/pull.go index 284c089..75d8ff3 100644 --- a/client/pull.go +++ b/client/pull.go @@ -23,7 +23,17 @@ type ( } ) -// FetchSubs pulls submissions matching criteria +/* +FetchSubs pulls submission information of each submission +of handle matching contest and problem parameters passed. + +Set contest param to non-empty to filter submissions of particular contest. +Similarly, setting problem param to non-empty filters problems matching criteria. + +Returns slice of struct `Sub` holding details of filtered submissions. + +Returns error is http request fails or API returns non-OK status. +*/ func FetchSubs(contest, problem, handle string) ([]Sub, error) { c := cfg.Session.Client @@ -34,7 +44,7 @@ func FetchSubs(contest, problem, handle string) ([]Sub, error) { q.Set("handle", handle) link.RawQuery = q.Encode() - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return nil, err } @@ -45,7 +55,7 @@ func FetchSubs(contest, problem, handle string) ([]Sub, error) { comm := gjson.GetBytes(body, "comment").String() return nil, fmt.Errorf(comm) } - // is another submission to same problem considered + // is another AC submission to same problem considered isParsed := make(map[string]bool) var Subs []Sub @@ -53,7 +63,6 @@ func FetchSubs(contest, problem, handle string) ([]Sub, error) { // thanks to module tidwall/gjson for the awesome package result := gjson.GetBytes(body, "result") result.ForEach(func(key, value gjson.Result) bool { - // check if result matches search criteria // extract submission data contID := value.Get("problem.contestId").String() probID := value.Get("problem.index").String() @@ -64,7 +73,7 @@ func FetchSubs(contest, problem, handle string) ([]Sub, error) { sid := value.Get("id").String() // ContestId+ProblemId => 1234c2 query := contID + probID - + // check if result matches search criteria if (contID == contest || contest == "") && (probID == problem || problem == "") && (verdict == "OK" && isParsed[query] == false) { // create sub and fetch source code @@ -85,7 +94,12 @@ func FetchSubs(contest, problem, handle string) ([]Sub, error) { return Subs, nil } -// FetchSubSource fetches submission code of Sub +/* +FetchSubSource extracts source code of particular submission +returns a string containing the source code of the submission. + +If fetching submission fails, http error message is returned. +*/ func (sub *Sub) FetchSubSource() (string, error) { // determine contest type (contest/gym) contClass := "contest" @@ -96,7 +110,7 @@ func (sub *Sub) FetchSubSource() (string, error) { c := cfg.Session.Client link, _ := url.Parse(cfg.Settings.Host) link.Path = path.Join(link.Path, contClass, sub.Contest, "submission", sub.Sid) - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return "", err } diff --git a/client/submit.go b/client/submit.go index 7d26ad9..f340804 100644 --- a/client/submit.go +++ b/client/submit.go @@ -12,13 +12,19 @@ import ( "github.com/PuerkitoBio/goquery" ) -// Submit uploads form data and submits user code +/* +Submit reads contents of file and submits it to +specified problem in contest. Returns nil is submission was successful. + +If submission fails (includes failure due to submission of same code) +returns error message of cause of failed submission. +*/ func Submit(contest, problem, langID, file string, link url.URL) error { // form redirection prevention is removed while submitting c := cfg.Session.Client - c.CheckRedirect = RedirectCheck + c.CheckRedirect = redirectCheck link.Path = path.Join(link.Path, "submit") - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return err } else if len(body) == 0 { @@ -29,12 +35,12 @@ func Submit(contest, problem, langID, file string, link url.URL) error { // read source file data, _ := ioutil.ReadFile(file) // hidden form data - csrf := FindCsrf(body) + csrf := findCsrf(body) ftaa := "yzo0kk4bhlbaw83g2q" bfaa := "883b704dbe5c70e1e61de4d8aff2da32" // post form data (remove redirection prevention) c.CheckRedirect = nil - body, err = PostReqBody(&c, link.String(), url.Values{ + body, err = postReqBody(&c, link.String(), url.Values{ "csrf_token": {csrf}, "ftaa": {ftaa}, "bfaa": {bfaa}, diff --git a/client/upgrade.go b/client/upgrade.go index 433ae40..c7a7e46 100644 --- a/client/upgrade.go +++ b/client/upgrade.go @@ -19,7 +19,7 @@ import ( func FetchLatest(owner, repo string) (semver.Version, string, error) { // url link of github API to fetch latest version data from link := fmt.Sprintf("https://api.github.com/repos/%v/%v/releases/latest", owner, repo) - resp, err := GetReqBody(http.DefaultClient, link) + resp, err := getReqBody(http.DefaultClient, link) if err != nil { return semver.Version{}, "", err } diff --git a/client/watch.go b/client/watch.go index 997189f..9cab4bd 100644 --- a/client/watch.go +++ b/client/watch.go @@ -32,10 +32,10 @@ type ( func WatchSubmissions(contest, query string, link url.URL) ([]Submission, error) { // This implementation contains redirection prevention c := cfg.Session.Client - c.CheckRedirect = RedirectCheck + c.CheckRedirect = redirectCheck // fetch all submissions in contest link.Path = path.Join(link.Path, "my") - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return nil, err } else if len(body) == 0 { @@ -51,14 +51,14 @@ func WatchSubmissions(contest, query string, link url.URL) ([]Submission, error) sel.Each(func(_ int, row *goquery.Selection) { // select cell ...type(x) from row data = append(data, Submission{ - ID: GetText(row, "td:nth-of-type(1)"), - When: GetText(row, "td:nth-of-type(2)"), - Name: GetText(row, "td:nth-of-type(4)"), - Lang: GetText(row, "td:nth-of-type(5)"), - Waiting: GetAttr(row, "td:nth-of-type(6)", "waiting"), - Verdict: GetText(row, "td:nth-of-type(6)"), - Time: GetText(row, "td:nth-of-type(7)"), - Memory: GetText(row, "td:nth-of-type(8)"), + ID: getText(row, "td:nth-of-type(1)"), + When: getText(row, "td:nth-of-type(2)"), + Name: getText(row, "td:nth-of-type(4)"), + Lang: getText(row, "td:nth-of-type(5)"), + Waiting: getAttr(row, "td:nth-of-type(6)", "waiting"), + Verdict: getText(row, "td:nth-of-type(6)"), + Time: getText(row, "td:nth-of-type(7)"), + Memory: getText(row, "td:nth-of-type(8)"), }) }) @@ -69,9 +69,9 @@ func WatchSubmissions(contest, query string, link url.URL) ([]Submission, error) func WatchContest(contest string, link url.URL) ([]Problem, error) { // This implementation contains redirection prevention c := cfg.Session.Client - c.CheckRedirect = RedirectCheck + c.CheckRedirect = redirectCheck // fetch contest dashboard page - body, err := GetReqBody(&c, link.String()) + body, err := getReqBody(&c, link.String()) if err != nil { return nil, err } else if len(body) == 0 { @@ -85,9 +85,9 @@ func WatchContest(contest string, link url.URL) ([]Problem, error) { doc.Find(".problems tr").Has("td").Each(func(_ int, row *goquery.Selection) { data = append(data, Problem{ - ID: GetText(row, "td:nth-of-type(1)"), - Name: GetText(row, "td:nth-of-type(2) a"), - Count: GetText(row, "td:nth-of-type(4)"), + ID: getText(row, "td:nth-of-type(1)"), + Name: getText(row, "td:nth-of-type(2) a"), + Count: getText(row, "td:nth-of-type(4)"), Status: row.AttrOr("class", ""), }) })