From 2c090367aa273781862ab64ccd0be160f7927371 Mon Sep 17 00:00:00 2001 From: Jo <10510431+j178@users.noreply.github.com> Date: Sun, 11 Aug 2024 13:54:41 +0800 Subject: [PATCH] feat: display today streak counter (#305) * feat: display today streak counter * fix streak query --- cmd/contest.go | 2 +- cmd/submit.go | 19 +++++++++++++++++++ cmd/test.go | 13 +++++++++++-- leetcode/client.go | 41 +++++++++++++++++++++++++++++++++++++---- leetcode/models.go | 35 +++++++++++++++++++++-------------- 5 files changed, 89 insertions(+), 21 deletions(-) diff --git a/cmd/contest.go b/cmd/contest.go index 09f745bd..0e404299 100644 --- a/cmd/contest.go +++ b/cmd/contest.go @@ -58,7 +58,7 @@ func selectUpcomingContest(c leetcode.Client, registeredOnly bool) (string, erro for i, ct := range contestList { mark := " " if ct.Registered { - mark = "√" + mark = "✔" } contestNames[i] = fmt.Sprintf( "%s %s at %s", diff --git a/cmd/submit.go b/cmd/submit.go index 16f16232..f46cb9e7 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "strconv" "strings" "github.com/charmbracelet/log" @@ -63,6 +64,11 @@ leetgo submit w330/ } } + err = showTodayStreak(c, cmd) + if err != nil { + log.Debug("failed to show today's streak", "err", err) + } + if hasFailedCase { return exitCode(1) } @@ -145,3 +151,16 @@ func appendToTestCases(q *leetcode.QuestionData, result *leetcode.SubmitCheckRes err = utils.WriteFile(testCasesFile.GetPath(), content) return true, err } + +func showTodayStreak(c leetcode.Client, cmd *cobra.Command) error { + streak, err := c.GetStreakCounter() + if err != nil { + return err + } + today := "" + if streak.TodayCompleted { + today = config.PassedStyle.Render("+1") + } + cmd.Printf("\nTotal streak: %s%s\n", strconv.Itoa(streak.StreakCount-1), today) + return nil +} diff --git a/cmd/test.go b/cmd/test.go index af0d7bc4..1c12407b 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -88,6 +88,7 @@ leetgo test w330/`, submitLimiter := newLimiter(user) var hasFailedCase bool + var hasSubmitted bool for _, q := range qs { var ( localPassed = true @@ -115,6 +116,7 @@ leetgo test w330/`, if autoSubmit && remotePassed && (localPassed || forceSubmit) { log.Info("submitting solution", "user", user.Whoami(c)) + hasSubmitted = true result, err := submitSolution(cmd, q, c, gen, submitLimiter) if err != nil { submitAccepted = false @@ -125,7 +127,7 @@ leetgo test w330/`, submitAccepted = false added, _ := appendToTestCases(q, result) if added { - log.Info("added failed case to testcases.txt") + log.Info("added failed cases to `testcases.txt`") } } } @@ -136,6 +138,13 @@ leetgo test w330/`, } } + if hasSubmitted { + err = showTodayStreak(c, cmd) + if err != nil { + log.Debug("failed to show today's streak", "err", err) + } + } + if hasFailedCase { return exitCode(1) } @@ -213,7 +222,7 @@ func runTestRemotely( if err != nil { log.Debug("failed to update test cases", "err", err) } else { - log.Info("testcases.txt updated") + log.Info("`testcases.txt` updated") } } } diff --git a/leetcode/client.go b/leetcode/client.go index 8e7a5722..d6660774 100644 --- a/leetcode/client.go +++ b/leetcode/client.go @@ -40,8 +40,8 @@ func (e UnexpectedStatusCode) IsError() bool { func (e UnexpectedStatusCode) Error() string { body := "" - if len(e.Body) > 100 { - body = e.Body[:100] + "..." + if len(e.Body) > 500 { + body = e.Body[:500] + "..." } return fmt.Sprintf("[%d %s] %s", e.Code, http.StatusText(e.Code), body) } @@ -81,6 +81,7 @@ type Client interface { GetContestQuestionData(contestSlug string, questionSlug string) (*QuestionData, error) RegisterContest(slug string) error UnregisterContest(slug string) error + GetStreakCounter() (StreakCounter, error) } type cnClient struct { @@ -162,6 +163,7 @@ func nonFollowRedirect(req *http.Request, via []*http.Request) error { } type graphqlRequest struct { + path string query string operationName string variables map[string]any @@ -178,6 +180,7 @@ const ( const ( graphQLPath = "/graphql" + graphQLNoj = "/graphql/noj-go/" accountLoginPath = "/accounts/login/" contestInfoPath = "/contest/api/info/%s/" contestProblemsPath = "/contest/%s/problems/%s/" @@ -265,7 +268,11 @@ func (c *cnClient) graphqlGet(req graphqlRequest, result any) (*http.Response, e v, _ := json.Marshal(req.variables) p.Variables = string(v) } - r, err := c.http.New().Get(graphQLPath).QueryStruct(p).Request() + path := graphQLPath + if req.path != "" { + path = req.path + } + r, err := c.http.New().Get(path).QueryStruct(p).Request() if err != nil { return nil, err } @@ -282,7 +289,11 @@ func (c *cnClient) graphqlPost(req graphqlRequest, result any) (*http.Response, "operationName": req.operationName, "variables": v, } - r, err := c.http.New().Post(graphQLPath).BodyJSON(body).Request() + path := graphQLPath + if req.path != "" { + path = req.path + } + r, err := c.http.New().Post(path).BodyJSON(body).Request() if err != nil { return nil, err } @@ -1012,3 +1023,25 @@ func (c *cnClient) GetQuestionTags() ([]QuestionTag, error) { } return tags, nil } + +func (c *cnClient) GetStreakCounter() (StreakCounter, error) { + query := ` +query getStreakCounter { + problemsetStreakCounter { + today + streakCount + daysSkipped + todayCompleted + } +}` + var resp gjson.Result + _, err := c.graphqlPost( + graphqlRequest{path: graphQLNoj, query: query, authType: requireAuth}, &resp, + ) + if err != nil { + return StreakCounter{}, err + } + var counter StreakCounter + err = json.Unmarshal(utils.StringToBytes(resp.Get("data.problemsetStreakCounter").Raw), &counter) + return counter, err +} diff --git a/leetcode/models.go b/leetcode/models.go index d9fcdb21..47fd2cbe 100644 --- a/leetcode/models.go +++ b/leetcode/models.go @@ -87,7 +87,7 @@ func (r *SubmitCheckResult) Display(q *QuestionData) string { case Accepted: return fmt.Sprintf( "\n%s%s%s%s\n", - config.PassedStyle.Render(fmt.Sprintf(" √ %s\n", r.StatusMsg)), + config.PassedStyle.Render(fmt.Sprintf(" ✔ %s\n", r.StatusMsg)), fmt.Sprintf("\nPassed cases: %d/%d", r.TotalCorrect, r.TotalTestcases), fmt.Sprintf("\nRuntime: %s, better than %.0f%%", r.StatusRuntime, r.RuntimePercentile), fmt.Sprintf("\nMemory: %s, better than %.0f%%", r.StatusMemory, r.MemoryPercentile), @@ -95,7 +95,7 @@ func (r *SubmitCheckResult) Display(q *QuestionData) string { case WrongAnswer: return fmt.Sprintf( "\n%s%s%s%s%s%s\n", - config.FailedStyle.Render(" × Wrong Answer\n"), + config.FailedStyle.Render(" ✘ Wrong Answer\n"), fmt.Sprintf("\nPassed cases: %d/%d", r.TotalCorrect, r.TotalTestcases), fmt.Sprintf("\nLast case: %s", utils.TruncateString(strings.ReplaceAll(r.LastTestcase, "\n", "↩ "), 100)), fmt.Sprintf("\nOutput: %s", utils.TruncateString(strings.ReplaceAll(r.CodeOutput, "\n", "↩ "), 100)), @@ -105,25 +105,25 @@ func (r *SubmitCheckResult) Display(q *QuestionData) string { case MemoryLimitExceeded, TimeLimitExceeded, OutputLimitExceeded: return fmt.Sprintf( "\n%s%s%s\n", - config.ErrorStyle.Render(fmt.Sprintf(" × %s\n", r.StatusMsg)), + config.ErrorStyle.Render(fmt.Sprintf(" ✘ %s\n", r.StatusMsg)), fmt.Sprintf("\nPassed cases: %d/%d", r.TotalCorrect, r.TotalTestcases), fmt.Sprintf("\nLast case: %s", utils.TruncateString(r.LastTestcase, 100)), ) case RuntimeError: return fmt.Sprintf( "\n%s%s%s\n", - config.ErrorStyle.Render(fmt.Sprintf(" × %s\n", r.StatusMsg)), + config.ErrorStyle.Render(fmt.Sprintf(" ✘ %s\n", r.StatusMsg)), fmt.Sprintf("\nPassed cases: %s", formatCompare(r.CompareResult)), "\n"+config.StdoutStyle.Render(r.FullRuntimeError), ) case CompileError: return fmt.Sprintf( "\n%s%s\n", - config.ErrorStyle.Render(fmt.Sprintf(" × %s\n", r.StatusMsg)), + config.ErrorStyle.Render(fmt.Sprintf(" ✘ %s\n", r.StatusMsg)), "\n"+config.StdoutStyle.Render(r.FullCompileError), ) default: - return config.FailedStyle.Render(fmt.Sprintf("\n × %s\n", r.StatusMsg)) + return config.FailedStyle.Render(fmt.Sprintf("\n ✘ %s\n", r.StatusMsg)) } } @@ -181,9 +181,9 @@ func formatCompare(s string) string { var sb strings.Builder for _, c := range s { if c == '1' { - sb.WriteString(config.PassedStyle.Render("√")) + sb.WriteString(config.PassedStyle.Render("✔")) } else { - sb.WriteString(config.ErrorStyle.Render("×")) + sb.WriteString(config.ErrorStyle.Render("✘")) } } return sb.String() @@ -199,7 +199,7 @@ func (r *RunCheckResult) Display(_ *QuestionData) string { if r.CorrectAnswer { return fmt.Sprintf( "\n%s%s%s%s%s%s\n", - config.PassedStyle.Render(fmt.Sprintf(" √ %s\n", r.StatusMsg)), + config.PassedStyle.Render(fmt.Sprintf(" ✔ %s\n", r.StatusMsg)), fmt.Sprintf("\nPassed cases: %s", formatCompare(r.CompareResult)), fmt.Sprintf("\nInput: %s", utils.TruncateString(strings.ReplaceAll(r.InputData, "\n", "↩ "), 100)), fmt.Sprintf("\nOutput: %s", utils.TruncateString(strings.Join(r.CodeAnswer, "↩ "), 100)), @@ -209,7 +209,7 @@ func (r *RunCheckResult) Display(_ *QuestionData) string { } else { return fmt.Sprintf( "\n%s%s%s%s%s%s\n", - config.ErrorStyle.Render("\n × Wrong Answer\n"), + config.ErrorStyle.Render("\n ✘ Wrong Answer\n"), fmt.Sprintf("\nPassed cases: %s", formatCompare(r.CompareResult)), fmt.Sprintf("\nInput: %s", utils.TruncateString(strings.ReplaceAll(r.InputData, "\n", "↩ "), 100)), fmt.Sprintf("\nOutput: %s", utils.TruncateString(strings.Join(r.CodeAnswer, "↩ "), 100)), @@ -218,22 +218,22 @@ func (r *RunCheckResult) Display(_ *QuestionData) string { ) } case MemoryLimitExceeded, TimeLimitExceeded, OutputLimitExceeded: - return config.ErrorStyle.Render(fmt.Sprintf("\n × %s\n", r.StatusMsg)) + return config.ErrorStyle.Render(fmt.Sprintf("\n ✘ %s\n", r.StatusMsg)) case RuntimeError: return fmt.Sprintf( "\n%s%s%s\n", - config.ErrorStyle.Render(fmt.Sprintf(" × %s\n", r.StatusMsg)), + config.ErrorStyle.Render(fmt.Sprintf(" ✘ %s\n", r.StatusMsg)), fmt.Sprintf("Passed cases: %s", formatCompare(r.CompareResult)), "\n"+config.StdoutStyle.Render(r.FullRuntimeError), ) case CompileError: return fmt.Sprintf( "\n%s%s\n", - config.ErrorStyle.Render(fmt.Sprintf(" × %s\n", r.StatusMsg)), + config.ErrorStyle.Render(fmt.Sprintf(" ✘ %s\n", r.StatusMsg)), "\n"+config.StdoutStyle.Render(r.FullCompileError), ) default: - return config.FailedStyle.Render(fmt.Sprintf("\n × %s\n", r.StatusMsg)) + return config.FailedStyle.Render(fmt.Sprintf("\n ✘ %s\n", r.StatusMsg)) } } @@ -259,3 +259,10 @@ type QuestionTag struct { TypeName string `json:"typeName"` TypeTransName string `json:"typeTransName"` } + +type StreakCounter struct { + Today string `json:"today"` + StreakCount int `json:"streakCount"` + DaysSkipped int `json:"daysSkipped"` + TodayCompleted bool `json:"todayCompleted"` +}