diff --git a/cmd/debug.go b/cmd/debug.go index bd99fdf7..87739777 100644 --- a/cmd/debug.go +++ b/cmd/debug.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "os" "strings" @@ -42,7 +43,7 @@ var whoamiCmd = &cobra.Command{ return err } if !user.IsSignedIn { - return leetcode.ErrForbidden + return errors.New("user not signed in") } cmd.Println(user.Whoami(c)) return nil diff --git a/leetcode/cache_json.go b/leetcode/cache_json.go index 486425e2..9deb29c7 100644 --- a/leetcode/cache_json.go +++ b/leetcode/cache_json.go @@ -108,7 +108,7 @@ func (c *jsonCache) Update() error { if err != nil { return err } - log.Info("cache updated", "path", c.path) + log.Info("questions cache updated", "count", len(all), "path", c.path) return nil } diff --git a/leetcode/cache_sqlite.go b/leetcode/cache_sqlite.go index daaacd8f..92f366e9 100644 --- a/leetcode/cache_sqlite.go +++ b/leetcode/cache_sqlite.go @@ -337,6 +337,7 @@ func (c *sqliteCache) Update() error { if err != nil { return err } + count := len(all) placeholder := "(" + strings.Repeat("?,", 19) + "?)" batch := 100 for len(all) > 0 { @@ -367,6 +368,6 @@ func (c *sqliteCache) Update() error { if err != nil { return err } - log.Info("cache updated", "path", c.path) + log.Info("questions cache updated", "count", count, "path", c.path) return nil } diff --git a/leetcode/client.go b/leetcode/client.go index 093f771d..b508556e 100644 --- a/leetcode/client.go +++ b/leetcode/client.go @@ -27,14 +27,6 @@ var ( ErrPaidOnlyQuestion = errors.New("this is paid only question, you need to subscribe to LeetCode Premium") ErrQuestionNotFound = errors.New("no such question") ErrContestNotStarted = errors.New("contest has not started") - ErrForbidden = UnexpectedStatusCode{ - Code: 403, - Body: "access is forbidden, your cookies may have expired or LeetCode has restricted its API access", - } - ErrTooManyRequests = UnexpectedStatusCode{ - Code: 429, - Body: "LeetCode limited you access rate, you may be submitting too frequently", - } ) type UnexpectedStatusCode struct { @@ -43,13 +35,27 @@ type UnexpectedStatusCode struct { } func (e UnexpectedStatusCode) Error() string { - body := "" - if len(e.Body) > 1024 { - body = e.Body[:1024] + body := "" + if len(e.Body) > 100 { + body = e.Body[:100] + "..." } return fmt.Sprintf("[%d %s] %s", e.Code, http.StatusText(e.Code), body) } +func NewUnexpectedStatusCode(resp *http.Response) UnexpectedStatusCode { + err := UnexpectedStatusCode{Code: resp.StatusCode} + switch resp.StatusCode { + case http.StatusTooManyRequests: + err.Body = "LeetCode limited you access rate, you may be submitting too frequently" + case http.StatusForbidden: + err.Body = "Access is forbidden, your cookies may have expired or LeetCode has restricted its API access" + default: + body, _ := io.ReadAll(resp.Body) + err.Body = string(body) + } + return err +} + type Client interface { BaseURI() string Inspect(typ string) (map[string]any, error) @@ -182,7 +188,7 @@ const ( problemsApiTagsPath = "/problems/api/tags/" ) -func (c *cnClient) send(req *http.Request, authType authType, result any, failure any) (*http.Response, error) { +func (c *cnClient) send(req *http.Request, authType authType, result any) (*http.Response, error) { switch authType { case withoutAuth: case withAuth: @@ -204,31 +210,29 @@ func (c *cnClient) send(req *http.Request, authType authType, result any, failur log.Debug("request", "method", req.Method, "url", req.URL.String(), "body", utils.BytesToString(bodyStr)) } - var resp *http.Response err := retry.Do( func() error { - var err error - resp, err = c.http.Do(req, result, failure) + var ( + err error + respErr UnexpectedStatusCode + ) + _, err = c.http.Do(req, result, &respErr) if err != nil { return err } - if !(200 <= resp.StatusCode && resp.StatusCode <= 299) { - switch resp.StatusCode { - case http.StatusTooManyRequests: - return ErrTooManyRequests - case http.StatusForbidden: - return ErrForbidden - default: - body, _ := io.ReadAll(resp.Body) - return UnexpectedStatusCode{Code: resp.StatusCode, Body: string(body)} - } + if respErr.Code != 0 { + return respErr } return nil }, retry.RetryIf( func(err error) bool { // Do not retry on 429 - return !errors.Is(err, ErrTooManyRequests) + var e UnexpectedStatusCode + if errors.As(err, &e) && e.Code == http.StatusTooManyRequests { + return false + } + return true }, ), retry.Attempts(3), @@ -244,7 +248,7 @@ func (c *cnClient) send(req *http.Request, authType authType, result any, failur } //nolint:unused -func (c *cnClient) graphqlGet(req graphqlRequest, result any, failure any) (*http.Response, error) { +func (c *cnClient) graphqlGet(req graphqlRequest, result any) (*http.Response, error) { type params struct { Query string `url:"query"` OperationName string `url:"operationName"` @@ -262,10 +266,10 @@ func (c *cnClient) graphqlGet(req graphqlRequest, result any, failure any) (*htt if err != nil { return nil, err } - return c.send(r, req.authType, result, failure) + return c.send(r, req.authType, result) } -func (c *cnClient) graphqlPost(req graphqlRequest, result any, failure any) (*http.Response, error) { +func (c *cnClient) graphqlPost(req graphqlRequest, result any) (*http.Response, error) { v := req.variables if v == nil { v = make(map[string]any) @@ -279,23 +283,23 @@ func (c *cnClient) graphqlPost(req graphqlRequest, result any, failure any) (*ht if err != nil { return nil, err } - return c.send(r, req.authType, result, failure) + return c.send(r, req.authType, result) } -func (c *cnClient) jsonGet(url string, query any, authType authType, result any, failure any) (*http.Response, error) { +func (c *cnClient) jsonGet(url string, query any, authType authType, result any) (*http.Response, error) { r, err := c.http.New().Get(url).QueryStruct(query).Request() if err != nil { return nil, err } - return c.send(r, authType, result, failure) + return c.send(r, authType, result) } -func (c *cnClient) jsonPost(url string, json any, authType authType, result any, failure any) (*http.Response, error) { +func (c *cnClient) jsonPost(url string, json any, authType authType, result any) (*http.Response, error) { r, err := c.http.New().Post(url).BodyJSON(json).Request() if err != nil { return nil, err } - return c.send(r, authType, result, failure) + return c.send(r, authType, result) } func (c *cnClient) BaseURI() string { @@ -339,7 +343,6 @@ query a { _, err := c.graphqlGet( graphqlRequest{query: query}, &resp, - nil, ) return resp, err } @@ -407,7 +410,7 @@ query globalData { } `json:"data"` } _, err := c.graphqlPost( - graphqlRequest{query: query, authType: requireAuth}, &resp, nil, + graphqlRequest{query: query, authType: requireAuth}, &resp, ) if err != nil { return nil, err @@ -428,7 +431,7 @@ func (c *cnClient) getQuestionData(slug string, query string, authType authType) operationName: "questionData", variables: map[string]any{"titleSlug": slug}, authType: authType, - }, &resp, nil, + }, &resp, ) if err != nil { return nil, err @@ -500,7 +503,7 @@ func (c *cnClient) GetAllQuestions() ([]*QuestionData, error) { graphqlRequest{ query: query, operationName: "AllQuestionUrls", - }, &resp, nil, + }, &resp, ) if err != nil { return nil, err @@ -552,7 +555,7 @@ func (c *cnClient) GetTodayQuestion() (*QuestionData, error) { query: query, operationName: "questionOfToday", authType: withoutAuth, - }, &resp, nil, + }, &resp, ) if err != nil { return nil, err @@ -582,7 +585,7 @@ func (c *cnClient) GetQuestionOfDate(date time.Time) (*QuestionData, error) { }, authType: withAuth, }, - &resp, nil, + &resp, ) if err != nil { return nil, err @@ -601,7 +604,7 @@ func (c *cnClient) GetQuestionOfDate(date time.Time) (*QuestionData, error) { func (c *cnClient) getContest(contestSlug string) (*Contest, error) { path := fmt.Sprintf(contestInfoPath, contestSlug) var resp gjson.Result - _, err := c.jsonGet(path, nil, withAuth, &resp, nil) + _, err := c.jsonGet(path, nil, withAuth, &resp) if err != nil { return nil, err } @@ -653,7 +656,7 @@ func (c *cnClient) GetContestQuestionData(contestSlug string, questionSlug strin path := fmt.Sprintf(contestProblemsPath, contestSlug, questionSlug) var html []byte req, _ := c.http.New().Get(path).Request() - _, err := c.send(req, requireAuth, &html, nil) + _, err := c.send(req, requireAuth, &html) if err != nil { var e UnexpectedStatusCode if errors.As(err, &e) && e.Code == 302 { @@ -800,7 +803,7 @@ func (c *cnClient) RunCode(q *QuestionData, lang string, code string, dataInput "question_id": q.QuestionId, "typed_code": code, "data_input": dataInput, - }, requireAuth, &resp, nil, + }, requireAuth, &resp, ) if err != nil { return nil, err @@ -828,7 +831,7 @@ func (c *cnClient) SubmitCode(q *QuestionData, lang string, code string) (string "questionSlug": q.TitleSlug, "question_id": q.QuestionId, "typed_code": code, - }, requireAuth, &resp, nil, + }, requireAuth, &resp, ) return resp.Get("submission_id").String(), err } @@ -839,7 +842,7 @@ func (c *cnClient) CheckResult(submissionId string) ( ) { path := fmt.Sprintf(checkResultPath, submissionId) var result gjson.Result - _, err := c.jsonGet(path, nil, requireAuth, &result, nil) + _, err := c.jsonGet(path, nil, requireAuth, &result) if err != nil { return nil, err } @@ -871,7 +874,7 @@ func (c *cnClient) GetUpcomingContests() ([]*Contest, error) { ` var resp gjson.Result _, err := c.graphqlPost( - graphqlRequest{query: query, authType: withAuth}, &resp, nil, + graphqlRequest{query: query, authType: withAuth}, &resp, ) if err != nil { return nil, err @@ -903,7 +906,7 @@ func (c *cnClient) GetUpcomingContests() ([]*Contest, error) { func (c *cnClient) RegisterContest(slug string) error { path := fmt.Sprintf(contestRegisterPath, slug) - _, err := c.jsonPost(path, nil, requireAuth, nil, nil) + _, err := c.jsonPost(path, nil, requireAuth, nil) var e UnexpectedStatusCode if errors.As(err, &e) && e.Code == http.StatusFound { err = nil @@ -914,7 +917,7 @@ func (c *cnClient) RegisterContest(slug string) error { func (c *cnClient) UnregisterContest(slug string) error { path := fmt.Sprintf(contestRegisterPath, slug) req, _ := c.http.New().Delete(path).Request() - _, err := c.send(req, requireAuth, nil, nil) + _, err := c.send(req, requireAuth, nil) return err } @@ -964,7 +967,7 @@ query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $fi graphqlRequest{ query: query, variables: vars, - }, &resp, nil, + }, &resp, ) if err != nil { return QuestionList{}, err @@ -986,7 +989,7 @@ query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $fi func (c *cnClient) GetQuestionTags() ([]QuestionTag, error) { var resp gjson.Result - _, err := c.jsonGet(problemsApiTagsPath, nil, withAuth, &resp, nil) + _, err := c.jsonGet(problemsApiTagsPath, nil, withAuth, &resp) if err != nil { return nil, err } diff --git a/leetcode/client_us.go b/leetcode/client_us.go index 8a9b3513..72192501 100644 --- a/leetcode/client_us.go +++ b/leetcode/client_us.go @@ -71,7 +71,7 @@ func (c *usClient) GetQuestionData(slug string) (*QuestionData, error) { } func (c *usClient) GetAllQuestions() ([]*QuestionData, error) { - var resp struct { + var respData struct { UserName string `json:"user_name"` NumSolved int `json:"num_solved"` NumTotal int `json:"num_total"` @@ -92,12 +92,13 @@ func (c *usClient) GetAllQuestions() ([]*QuestionData, error) { PaidOnly bool `json:"paid_only"` } `json:"stat_status_pairs"` } - _, err := c.http.New().Get(problemsAllPath).ReceiveSuccess(&resp) + + _, err := c.jsonGet(problemsAllPath, nil, withoutAuth, &respData) if err != nil { return nil, err } - qs := make([]*QuestionData, 0, len(resp.StatStatusPairs)) - for _, pair := range resp.StatStatusPairs { + qs := make([]*QuestionData, 0, len(respData.StatStatusPairs)) + for _, pair := range respData.StatStatusPairs { difficulty := "" switch pair.Difficulty.Level { case 1: @@ -134,7 +135,7 @@ func (c *usClient) GetTodayQuestion() (*QuestionData, error) { }` var resp gjson.Result _, err := c.graphqlPost( - graphqlRequest{query: query, authType: withoutAuth}, &resp, nil, + graphqlRequest{query: query, authType: withoutAuth}, &resp, ) if err != nil { return nil, err @@ -166,7 +167,7 @@ func (c *usClient) GetQuestionOfDate(date time.Time) (*QuestionData, error) { }, authType: withAuth, }, - &resp, nil, + &resp, ) if err != nil { return nil, err @@ -198,7 +199,7 @@ func (c *usClient) GetContestQuestionData(contestSlug string, questionSlug strin path := fmt.Sprintf(contestProblemsPath, contestSlug, questionSlug) var html []byte req, _ := c.http.New().Get(path).Request() - _, err := c.send(req, requireAuth, &html, nil) + _, err := c.send(req, requireAuth, &html) if err != nil { var e UnexpectedStatusCode if errors.As(err, &e) && e.Code == 302 { @@ -240,7 +241,7 @@ func (c *usClient) GetUpcomingContests() ([]*Contest, error) { path := "_next/data/" + buildId + "/contest.json" var resp gjson.Result - _, err = c.jsonGet(path, nil, withAuth, &resp, nil) + _, err = c.jsonGet(path, nil, withAuth, &resp) if err != nil { return nil, err } @@ -324,7 +325,7 @@ query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $fi Post(graphQLPath). Set("Cookie", "NEW_PROBLEMLIST_PAGE=1"). BodyJSON(body).Request() - _, err := c.send(req, withoutAuth, &resp, nil) + _, err := c.send(req, withoutAuth, &resp) if err != nil { return QuestionList{}, err } @@ -345,7 +346,7 @@ query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $fi func (c *usClient) GetQuestionTags() ([]QuestionTag, error) { var resp gjson.Result - _, err := c.jsonGet(problemsApiTagsPath, nil, withoutAuth, &resp, nil) + _, err := c.jsonGet(problemsApiTagsPath, nil, withoutAuth, &resp) if err != nil { return nil, err } diff --git a/leetcode/decoder.go b/leetcode/decoder.go index f901b0fe..ee2a611e 100644 --- a/leetcode/decoder.go +++ b/leetcode/decoder.go @@ -17,9 +17,10 @@ import ( ) var ( - gjsonType = reflect.TypeOf(gjson.Result{}) - bytesType = reflect.TypeOf([]byte{}) - stringType = reflect.TypeOf("") + gjsonType = reflect.TypeFor[gjson.Result]() + bytesType = reflect.TypeFor[[]byte]() + stringType = reflect.TypeFor[string]() + errorType = reflect.TypeFor[UnexpectedStatusCode]() ) type smartDecoder struct { @@ -64,10 +65,9 @@ func (d smartDecoder) Decode(resp *http.Response, v interface{}) error { if d.LogResponse { dataStr = utils.BytesToString(data) limit := d.LogLimit - if len(data) < limit { - limit = len(data) + if len(data) > limit { + dataStr = dataStr[:limit] + "..." } - dataStr = dataStr[:limit] } log.Debug( "response", @@ -91,6 +91,8 @@ func (d smartDecoder) Decode(resp *http.Response, v interface{}) error { ele.SetBytes(data) case stringType: ele.SetString(utils.BytesToString(data)) + case errorType: + ele.Set(reflect.ValueOf(NewUnexpectedStatusCode(resp))) default: return json.Unmarshal(data, v) }