From 52ebf7bac44162cb000a0c7698ccce79584318af Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 16:28:23 +0800 Subject: [PATCH 01/14] feat(leetcode.cn): add submission detail query and corresponding test --- src/_tests/leetcode-cn.test.ts | 11 +++ .../leetcode-cn/submission-detail.graphql | 55 ++++++++++++++ src/leetcode-cn.ts | 75 ++++++++++++++++++- 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/graphql/leetcode-cn/submission-detail.graphql diff --git a/src/_tests/leetcode-cn.test.ts b/src/_tests/leetcode-cn.test.ts index 4687152..463cc88 100644 --- a/src/_tests/leetcode-cn.test.ts +++ b/src/_tests/leetcode-cn.test.ts @@ -58,6 +58,17 @@ describe("LeetCodeCN", { timeout: 15_000 }, () => { expect(user.isSignedIn).toBe(true); }, ); + + it.skipIf( + !process.env["TEST_CN_LEETCODE_SESSION"] || !process.env["TEST_CN_SUBMISSION_ID"], + )("should be able to get submission detail", async () => { + const submissionId = process.env["TEST_CN_SUBMISSION_ID"]; + if (submissionId) { + const detail = await lc.submissionDetail(submissionId); + expect(detail.submissionDetail).toBeDefined(); + expect(detail.submissionDetail.code).toBeDefined(); + } + }); }); describe("Unauthenticated", () => { diff --git a/src/graphql/leetcode-cn/submission-detail.graphql b/src/graphql/leetcode-cn/submission-detail.graphql new file mode 100644 index 0000000..3782c4b --- /dev/null +++ b/src/graphql/leetcode-cn/submission-detail.graphql @@ -0,0 +1,55 @@ +query submissionDetails($submissionId: ID!) { + submissionDetail(submissionId: $submissionId) { + code + timestamp + statusDisplay + isMine + runtimeDisplay: runtime + memoryDisplay: memory + memory: rawMemory + lang + langVerboseName + question { + questionId + titleSlug + hasFrontendPreview + } + user { + realName + userAvatar + userSlug + } + runtimePercentile + memoryPercentile + submissionComment { + flagType + } + passedTestCaseCnt + totalTestCaseCnt + fullCodeOutput + testDescriptions + testInfo + testBodies + stdOutput + ... on GeneralSubmissionNode { + outputDetail { + codeOutput + expectedOutput + input + compileError + runtimeError + lastTestcase + } + } + ... on ContestSubmissionNode { + outputDetail { + codeOutput + expectedOutput + input + compileError + runtimeError + lastTestcase + } + } + } +} diff --git a/src/leetcode-cn.ts b/src/leetcode-cn.ts index f8e8a91..3652ad0 100644 --- a/src/leetcode-cn.ts +++ b/src/leetcode-cn.ts @@ -7,6 +7,7 @@ import PROBLEM_SET from "./graphql/leetcode-cn/problem-set.graphql?raw"; import PROBLEM from "./graphql/leetcode-cn/problem.graphql?raw"; import QUESTION_OF_TODAY from "./graphql/leetcode-cn/question-of-today.graphql?raw"; import RECENT_AC_SUBMISSIONS from "./graphql/leetcode-cn/recent-ac-submissions.graphql?raw"; +import SUBMISSION_DETAIL from "./graphql/leetcode-cn/submission-detail.graphql?raw"; import USER_CONTEST from "./graphql/leetcode-cn/user-contest-ranking.graphql?raw"; import USER_PROBLEM_SUBMISSIONS from "./graphql/leetcode-cn/user-problem-submissions.graphql?raw"; import USER_PROFILE from "./graphql/leetcode-cn/user-profile.graphql?raw"; @@ -67,7 +68,7 @@ export class LeetCodeCN extends EventEmitter { * * ```javascript * const leetcode = new LeetCodeCN(); - * const profile = await leetcode.user("jacoblincool"); + * const profile = await leetcode.user("leetcode"); * ``` */ public async user(username: string): Promise { @@ -286,6 +287,26 @@ export class LeetCodeCN extends EventEmitter { return data.userStatus as UserStatus; } + /** + * Get detailed information about a submission. + * @param submissionId The ID of the submission + * @returns Detailed information about the submission + * + * ```javascript + * const leetcode = new LeetCodeCN(); + * const detail = await leetcode.submissionDetail("123456789"); + * ``` + */ + public async submissionDetail(submissionId: string): Promise { + await this.initialized; + const { data } = await this.graphql({ + operationName: "submissionDetails", + variables: { submissionId }, + query: SUBMISSION_DETAIL, + }); + return data; + } + /** * Use GraphQL to query LeetCodeCN API. * @param query @@ -657,3 +678,55 @@ export interface UserProgressQuestionListInput { skip: number; limit: number; } + +export interface SubmissionQuestion { + questionId: string; + titleSlug: string; + hasFrontendPreview: boolean; +} + +export interface SubmissionUser { + realName: string; + userAvatar: string; + userSlug: string; +} + +export interface OutputDetail { + codeOutput: string; + expectedOutput: string; + input: string; + compileError: string; + runtimeError: string; + lastTestcase: string; +} + +export interface SubmissionDetail { + code: string; + timestamp: number; + statusDisplay: string; + isMine: boolean; + runtimeDisplay: string; + memoryDisplay: string; + memory: string; + lang: string; + langVerboseName: string; + question: SubmissionQuestion; + user: SubmissionUser; + runtimePercentile: number; + memoryPercentile: number; + submissionComment: null | { + flagType: string; + }; + passedTestCaseCnt: number; + totalTestCaseCnt: number; + fullCodeOutput: null | string; + testDescriptions: null | string; + testInfo: null | string; + testBodies: null | string; + stdOutput: string; + outputDetail: OutputDetail; +} + +export interface SubmissionDetailResult { + submissionDetail: SubmissionDetail; +} From a8fd3201176362e334d17598a7a75b355dfd9c27 Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 16:38:55 +0800 Subject: [PATCH 02/14] feat(leetcode.cn): add `lang` and `status` parameters to user's submissions query --- src/_tests/leetcode-cn.test.ts | 5 +++++ src/leetcode-cn.ts | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/_tests/leetcode-cn.test.ts b/src/_tests/leetcode-cn.test.ts index 463cc88..38aebfc 100644 --- a/src/_tests/leetcode-cn.test.ts +++ b/src/_tests/leetcode-cn.test.ts @@ -35,8 +35,13 @@ describe("LeetCodeCN", { timeout: 15_000 }, () => { limit: 30, offset: 0, slug: "two-sum", + lang: "cpp", + status: "AC", }); expect(Array.isArray(submissions)).toBe(true); + if (submissions.length > 0) { + expect(submissions[0].status).toBe("AC"); + } }, ); diff --git a/src/leetcode-cn.ts b/src/leetcode-cn.ts index 3652ad0..5304f71 100644 --- a/src/leetcode-cn.ts +++ b/src/leetcode-cn.ts @@ -138,7 +138,15 @@ export class LeetCodeCN extends EventEmitter { limit = 20, offset = 0, slug, - }: { limit?: number; offset?: number; slug?: string } = {}): Promise { + lang, + status, + }: { + limit?: number; + offset?: number; + slug?: string; + lang?: string; + status?: string; + } = {}): Promise { await this.initialized; if (!slug) { @@ -155,6 +163,8 @@ export class LeetCodeCN extends EventEmitter { offset: cursor, limit: limit - submissions.length > 20 ? 20 : limit - submissions.length, questionSlug: slug, + lang, + status, }, query: USER_PROBLEM_SUBMISSIONS, }); From 6c815eff3cdf9fdb8704b7ea18a3af22d2e39edb Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 16:57:54 +0800 Subject: [PATCH 03/14] docs: update README for leetcode.cn endpoint - fix the error in the example for `leetcode.submissions` api --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba7ff76..2f22584 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The API to get user profiles, submissions, and problems on LeetCode, with highly - [x] Get Public User Profile. - [x] Get User's Recent Submissions. (Public, Max: 20) - [x] Get User Contest Records. (thanks to [@laporchen](https://github.com/laporchen)) +- [x] Get All of User's Submissions. (Only for `leetcode.cn` endpoint) - [x] Get All Problem List, or with filter of difficulty and tags. - [x] Get Problem Detail. - [x] Get Daily Challenge. @@ -35,6 +36,14 @@ import { LeetCode } from "leetcode-query"; const leetcode = new LeetCode(); const user = await leetcode.user("username"); + +/* +// An Example for leetcode.cn endpoint +import { LeetCodeCN } from "leetcode-query"; + +const leetcodeCN = new LeetCodeCN(); +const user = await leetcodeCN.user("leetcode"); +*/ ``` ### Get All Of Your Submissions @@ -46,7 +55,7 @@ const credential = new Credential(); await credential.init("YOUR-LEETCODE-SESSION-COOKIE"); const leetcode = new LeetCode(credential); -console.log(await leetcode.submissions(100, 0)); +console.log(await leetcode.submissions({ limit: 10, offset: 0 })); ``` ### Use Custom Fetcher From 1ba3dc8998f2aea4524d1f0b03a7a193dafadc14 Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 20:32:12 +0800 Subject: [PATCH 04/14] fix(leetcode.cn): fix operation name for user contest query --- src/leetcode-cn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/leetcode-cn.ts b/src/leetcode-cn.ts index 5304f71..bf773e6 100644 --- a/src/leetcode-cn.ts +++ b/src/leetcode-cn.ts @@ -94,7 +94,7 @@ export class LeetCodeCN extends EventEmitter { await this.initialized; const { data } = await this.graphql( { - operationName: "userContest", + operationName: "userContestRankingInfo", variables: { username }, query: USER_CONTEST, }, From 381236fa65cbd26501403ac4ea1f52cc6ff981ce Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 20:33:58 +0800 Subject: [PATCH 05/14] chore: add changeset --- .changeset/curvy-planets-pay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curvy-planets-pay.md diff --git a/.changeset/curvy-planets-pay.md b/.changeset/curvy-planets-pay.md new file mode 100644 index 0000000..c399b7d --- /dev/null +++ b/.changeset/curvy-planets-pay.md @@ -0,0 +1,5 @@ +--- +"leetcode-query": minor +--- + +Add the `submission_detail` API to leetcode.cn \ No newline at end of file From 714d2a7d2e8a0bea3758e982fba2fcd656cdaede Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 22:15:57 +0800 Subject: [PATCH 06/14] fix(tests): increase wait time to ensure cache expiration in default cache test --- src/_tests/cache.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_tests/cache.test.ts b/src/_tests/cache.test.ts index 29a3417..0394757 100644 --- a/src/_tests/cache.test.ts +++ b/src/_tests/cache.test.ts @@ -66,7 +66,8 @@ describe("Cache", () => { it("should expire after 300ms", async () => { caches.default.set("test", "test", 300); - await sleep(300); + // wait for 305ms to ensure the cache is expired + await sleep(305); expect(caches.default.get("test")).toBeNull(); caches.default.clear(); }); From c4207094f00e894f9445f7d2b31ef796b5fbdde7 Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 22:56:14 +0800 Subject: [PATCH 07/14] feat(leetcode.com): add submission details query and update SubmissionDetail interface --- src/graphql/submission-detail.graphql | 51 +++++++++++++++++++++++ src/leetcode-types.ts | 55 +++++++++++++++++-------- src/leetcode.ts | 58 +++------------------------ 3 files changed, 95 insertions(+), 69 deletions(-) create mode 100644 src/graphql/submission-detail.graphql diff --git a/src/graphql/submission-detail.graphql b/src/graphql/submission-detail.graphql new file mode 100644 index 0000000..40fa299 --- /dev/null +++ b/src/graphql/submission-detail.graphql @@ -0,0 +1,51 @@ +query submissionDetails($id: Int!) { + submissionDetails(submissionId: $id) { + id + runtime + runtimeDisplay + runtimePercentile + runtimeDistribution + memory + memoryDisplay + memoryPercentile + memoryDistribution + code + timestamp + statusCode + user { + username + profile { + realName + userAvatar + } + } + lang { + name + verboseName + } + question { + questionId + titleSlug + hasFrontendPreview + } + notes + flagType + topicTags { + tagId + slug + name + } + runtimeError + compileError + lastTestcase + codeOutput + expectedOutput + totalCorrect + totalTestcases + fullCodeOutput + testDescriptions + testBodies + testInfo + stdOutput + } +} diff --git a/src/leetcode-types.ts b/src/leetcode-types.ts index 5b527f8..567c2f6 100644 --- a/src/leetcode-types.ts +++ b/src/leetcode-types.ts @@ -260,27 +260,48 @@ export interface Whoami { export interface SubmissionDetail { id: number; - problem_id: number; runtime: number; - runtime_distribution: [number, number][]; - runtime_percentile: number; + runtimeDisplay: string; + runtimePercentile: number; + runtimeDistribution: string; memory: number; - memory_distribution: [number, number][]; - memory_percentile: number; + memoryDisplay: string; + memoryPercentile: number; + memoryDistribution: string; code: string; - details: { - status_code: number; - runtime: string; - memory: string; - total_correct: string; - total_testcases: string; - compare_result: string; - input_formatted: string; - input: string; - expected_output: string; - code_output: string; - last_testcase: string; + timestamp: number; + statusCode: number; + user: { + username: string; + profile: { + realName: string; + userAvatar: string; + }; + }; + lang: { + name: string; + verboseName: string; + }; + question: { + questionId: string; + titleSlug: string; + hasFrontendPreview: boolean; }; + notes: string; + flagType: string; + topicTags: string[]; + runtimeError: string | null; + compileError: string | null; + lastTestcase: string; + codeOutput: string; + expectedOutput: string; + totalCorrect: number; + totalTestcases: number; + fullCodeOutput: string | null; + testDescriptions: string | null; + testBodies: string | null; + testInfo: string | null; + stdOutput: string | null; } export interface ProblemList { diff --git a/src/leetcode.ts b/src/leetcode.ts index b25c7b9..595db98 100644 --- a/src/leetcode.ts +++ b/src/leetcode.ts @@ -9,6 +9,7 @@ import PROBLEM from "./graphql/problem.graphql?raw"; import PROBLEMS from "./graphql/problems.graphql?raw"; import PROFILE from "./graphql/profile.graphql?raw"; import RECENT_SUBMISSIONS from "./graphql/recent-submissions.graphql?raw"; +import SUBMISSION_DETAIL from "./graphql/submission-detail.graphql?raw"; import SUBMISSIONS from "./graphql/submissions.graphql?raw"; import WHOAMI from "./graphql/whoami.graphql?raw"; import type { @@ -194,59 +195,12 @@ export class LeetCode extends EventEmitter { */ public async submission(id: number): Promise { await this.initialized; + const { data } = await this.graphql({ + variables: { id }, + query: SUBMISSION_DETAIL, + }); - try { - await this.limiter.lock(); - - const res = await fetch(`${BASE_URL}/submissions/detail/${id}/`, { - headers: { - origin: BASE_URL, - referer: BASE_URL, - cookie: `csrftoken=${this.credential.csrf || ""}; LEETCODE_SESSION=${ - this.credential.session || "" - };`, - "user-agent": USER_AGENT, - }, - }); - const raw = await res.text(); - const data = raw.match(/var pageData = ({[^]+?});/)?.[1]; - const json = new Function("return " + data)(); - const result = { - id: parseInt(json.submissionId), - problem_id: parseInt(json.questionId), - runtime: parseInt(json.runtime), - runtime_distribution: json.runtimeDistributionFormatted - ? (JSON.parse(json.runtimeDistributionFormatted).distribution.map( - (item: [string, number]) => [+item[0], item[1]], - ) as [number, number][]) - : [], - runtime_percentile: 0, - memory: parseInt(json.memory), - memory_distribution: json.memoryDistributionFormatted - ? (JSON.parse(json.memoryDistributionFormatted).distribution.map( - (item: [string, number]) => [+item[0], item[1]], - ) as [number, number][]) - : [], - memory_percentile: 0, - code: json.submissionCode, - details: json.submissionData, - }; - - result.runtime_percentile = result.runtime_distribution.reduce( - (acc, [usage, p]) => acc + (usage >= result.runtime ? p : 0), - 0, - ); - result.memory_percentile = result.memory_distribution.reduce( - (acc, [usage, p]) => acc + (usage >= result.memory / 1000 ? p : 0), - 0, - ); - - this.limiter.unlock(); - return result; - } catch (err) { - this.limiter.unlock(); - throw err; - } + return data.submissionDetails as SubmissionDetail; } /** From 34ca8cc49bab92cc1708c8084ed91799e12c5dbd Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 23:06:06 +0800 Subject: [PATCH 08/14] feat(leetcode.cn): update submissionDetail method and interface to include submission ID --- src/_tests/leetcode-cn.test.ts | 7 ++++--- src/graphql/leetcode-cn/submission-detail.graphql | 1 + src/leetcode-cn.ts | 10 +++------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/_tests/leetcode-cn.test.ts b/src/_tests/leetcode-cn.test.ts index 38aebfc..7ecd74e 100644 --- a/src/_tests/leetcode-cn.test.ts +++ b/src/_tests/leetcode-cn.test.ts @@ -69,9 +69,10 @@ describe("LeetCodeCN", { timeout: 15_000 }, () => { )("should be able to get submission detail", async () => { const submissionId = process.env["TEST_CN_SUBMISSION_ID"]; if (submissionId) { - const detail = await lc.submissionDetail(submissionId); - expect(detail.submissionDetail).toBeDefined(); - expect(detail.submissionDetail.code).toBeDefined(); + const submissionDetail = await lc.submissionDetail(submissionId); + expect(submissionDetail).toBeDefined(); + expect(submissionDetail.id).toBe(submissionId); + expect(submissionDetail.code).toBeDefined(); } }); }); diff --git a/src/graphql/leetcode-cn/submission-detail.graphql b/src/graphql/leetcode-cn/submission-detail.graphql index 3782c4b..33d5966 100644 --- a/src/graphql/leetcode-cn/submission-detail.graphql +++ b/src/graphql/leetcode-cn/submission-detail.graphql @@ -1,5 +1,6 @@ query submissionDetails($submissionId: ID!) { submissionDetail(submissionId: $submissionId) { + id code timestamp statusDisplay diff --git a/src/leetcode-cn.ts b/src/leetcode-cn.ts index bf773e6..e3cecde 100644 --- a/src/leetcode-cn.ts +++ b/src/leetcode-cn.ts @@ -307,14 +307,13 @@ export class LeetCodeCN extends EventEmitter { * const detail = await leetcode.submissionDetail("123456789"); * ``` */ - public async submissionDetail(submissionId: string): Promise { + public async submissionDetail(submissionId: string): Promise { await this.initialized; const { data } = await this.graphql({ - operationName: "submissionDetails", variables: { submissionId }, query: SUBMISSION_DETAIL, }); - return data; + return data.submissionDetail as SubmissionDetail; } /** @@ -711,6 +710,7 @@ export interface OutputDetail { } export interface SubmissionDetail { + id: string; code: string; timestamp: number; statusDisplay: string; @@ -736,7 +736,3 @@ export interface SubmissionDetail { stdOutput: string; outputDetail: OutputDetail; } - -export interface SubmissionDetailResult { - submissionDetail: SubmissionDetail; -} From 2a777b1759431d3cdadfa4021e98b9a04e9e15fd Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Thu, 10 Apr 2025 23:13:17 +0800 Subject: [PATCH 09/14] chore: add changeset --- .changeset/warm-lobsters-act.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/warm-lobsters-act.md diff --git a/.changeset/warm-lobsters-act.md b/.changeset/warm-lobsters-act.md new file mode 100644 index 0000000..0ec7fc9 --- /dev/null +++ b/.changeset/warm-lobsters-act.md @@ -0,0 +1,6 @@ +--- +"leetcode-query": major +--- + +Add the `submission_detail` API to leetcode.cn +Add GraphQL query support for the submission_detail interface of the leetcode.com to fix API errors From 9218b926bc32a8b1697434710323f2845bfdf31f Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Fri, 11 Apr 2025 16:36:32 +0800 Subject: [PATCH 10/14] fix(graphql): make questionSlug optional in submissionList query --- src/graphql/leetcode-cn/user-problem-submissions.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphql/leetcode-cn/user-problem-submissions.graphql b/src/graphql/leetcode-cn/user-problem-submissions.graphql index dec2624..6aeef8a 100644 --- a/src/graphql/leetcode-cn/user-problem-submissions.graphql +++ b/src/graphql/leetcode-cn/user-problem-submissions.graphql @@ -2,7 +2,7 @@ query submissionList( $offset: Int! $limit: Int! $lastKey: String - $questionSlug: String! + $questionSlug: String $lang: String $status: SubmissionStatusEnum ) { From 66ddd8cfd64286ca8b53990f1ba5175d72671226 Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Fri, 11 Apr 2025 17:31:21 +0800 Subject: [PATCH 11/14] feat(leetcode.cn): enhance user_progress_questions to support question status and difficulty filters --- src/_tests/leetcode-cn.test.ts | 17 ++++++++++++-- .../user-progress-questions.graphql | 12 ++++++++-- src/leetcode-cn.ts | 22 ++++++++++++++----- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/_tests/leetcode-cn.test.ts b/src/_tests/leetcode-cn.test.ts index 7ecd74e..aad7d3b 100644 --- a/src/_tests/leetcode-cn.test.ts +++ b/src/_tests/leetcode-cn.test.ts @@ -2,7 +2,7 @@ import dotenv from "dotenv"; import { beforeAll, describe, expect, it } from "vitest"; import { Cache } from "../cache"; import Credential from "../credential-cn"; -import { LeetCodeCN } from "../leetcode-cn"; +import { LeetCodeCN, QuestionStatusEnum } from "../leetcode-cn"; describe("LeetCodeCN", { timeout: 15_000 }, () => { describe("General", () => { @@ -50,9 +50,22 @@ describe("LeetCodeCN", { timeout: 15_000 }, () => { async () => { const progress = await lc.user_progress_questions({ skip: 0, - limit: 20, + limit: 10, }); expect(progress).toBeDefined(); + expect(progress.questions.length).toBeLessThanOrEqual(10); + + const progressWithQuestionStatus = await lc.user_progress_questions({ + skip: 0, + limit: 10, + questionStatus: QuestionStatusEnum.ATTEMPTED, + }); + expect(progressWithQuestionStatus).toBeDefined(); + if (progressWithQuestionStatus.questions.length > 0) { + expect(progressWithQuestionStatus.questions[0].questionStatus).toBe( + QuestionStatusEnum.ATTEMPTED, + ); + } }, ); diff --git a/src/graphql/leetcode-cn/user-progress-questions.graphql b/src/graphql/leetcode-cn/user-progress-questions.graphql index 8d16928..34b2b40 100644 --- a/src/graphql/leetcode-cn/user-progress-questions.graphql +++ b/src/graphql/leetcode-cn/user-progress-questions.graphql @@ -22,6 +22,14 @@ query userProgressQuestionList($filters: UserProgressQuestionListInput) { # UserProgressQuestionListInput: # { -# "skip": 50, -# "limit": 50 +# "filters": { +# "skip": 10, +# "limit": 10, +# "questionStatus": "SOLVED", // Enums: SOLVED, ATTEMPTED +# "difficulty": [ +# "EASY", +# "MEDIUM", +# "HARD" +# ] +# } # } diff --git a/src/leetcode-cn.ts b/src/leetcode-cn.ts index e3cecde..8dbad42 100644 --- a/src/leetcode-cn.ts +++ b/src/leetcode-cn.ts @@ -198,13 +198,12 @@ export class LeetCodeCN extends EventEmitter { * Get user progress questions. Need to be authenticated. * @returns */ - public async user_progress_questions(filters: { - skip: number; - limit: number; - }): Promise { + public async user_progress_questions( + filters: UserProgressQuestionListInput, + ): Promise { await this.initialized; const { data } = await this.graphql({ - variables: { filter: filters }, + variables: { filters: filters }, query: USER_PROGRESS_QUESTIONS, }); return data.userProgressQuestionList as UserProgressQuestionList; @@ -684,10 +683,23 @@ export interface UserProgressQuestionList { } export interface UserProgressQuestionListInput { + difficulty?: Array; + questionStatus?: QuestionStatusEnum; skip: number; limit: number; } +export enum DifficultyEnum { + EASY = "EASY", + MEDIUM = "MEDIUM", + HARD = "HARD", +} + +export enum QuestionStatusEnum { + ATTEMPTED = "ATTEMPTED", + SOLVED = "SOLVED", +} + export interface SubmissionQuestion { questionId: string; titleSlug: string; From aa67fd72ba88fdee54217bf62822353c98e4c18d Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Fri, 11 Apr 2025 17:36:31 +0800 Subject: [PATCH 12/14] feat(leetcode.com): add user_progress_questions method to retrieve user progress with filters --- src/_tests/leetcode.test.ts | 25 +++++++++++++++++++++++++ src/leetcode.ts | 17 +++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/_tests/leetcode.test.ts b/src/_tests/leetcode.test.ts index f5dd2f4..11fc20a 100644 --- a/src/_tests/leetcode.test.ts +++ b/src/_tests/leetcode.test.ts @@ -3,6 +3,7 @@ import { beforeAll, describe, expect, it } from "vitest"; import { Cache } from "../cache"; import { Credential } from "../credential"; import { LeetCode } from "../leetcode"; +import { QuestionStatusEnum } from "../leetcode-cn"; describe("LeetCode", { timeout: 15_000 }, () => { describe("General", () => { @@ -125,5 +126,29 @@ describe("LeetCode", { timeout: 15_000 }, () => { expect(submission.runtime).toBe(200); }, ); + + it.skipIf(!process.env["TEST_LEETCODE_SESSION"])( + "should be able to get user progress questions", + async () => { + const progress = await lc.user_progress_questions({ + skip: 0, + limit: 10, + }); + expect(progress).toBeDefined(); + expect(progress.questions.length).toBeLessThanOrEqual(10); + + const progressWithQuestionStatus = await lc.user_progress_questions({ + skip: 0, + limit: 10, + questionStatus: QuestionStatusEnum.SOLVED, + }); + expect(progressWithQuestionStatus).toBeDefined(); + if (progressWithQuestionStatus.questions.length > 0) { + expect(progressWithQuestionStatus.questions[0].questionStatus).toBe( + QuestionStatusEnum.SOLVED, + ); + } + }, + ); }); }); diff --git a/src/leetcode.ts b/src/leetcode.ts index 595db98..c9de989 100644 --- a/src/leetcode.ts +++ b/src/leetcode.ts @@ -5,6 +5,7 @@ import { Credential } from "./credential"; import fetch from "./fetch"; import CONTEST from "./graphql/contest.graphql?raw"; import DAILY from "./graphql/daily.graphql?raw"; +import USER_PROGRESS_QUESTIONS from "./graphql/leetcode-cn/user-progress-questions.graphql?raw"; import PROBLEM from "./graphql/problem.graphql?raw"; import PROBLEMS from "./graphql/problems.graphql?raw"; import PROFILE from "./graphql/profile.graphql?raw"; @@ -12,6 +13,7 @@ import RECENT_SUBMISSIONS from "./graphql/recent-submissions.graphql?raw"; import SUBMISSION_DETAIL from "./graphql/submission-detail.graphql?raw"; import SUBMISSIONS from "./graphql/submissions.graphql?raw"; import WHOAMI from "./graphql/whoami.graphql?raw"; +import { UserProgressQuestionList, UserProgressQuestionListInput } from "./leetcode-cn"; import type { DailyChallenge, Problem, @@ -203,6 +205,21 @@ export class LeetCode extends EventEmitter { return data.submissionDetails as SubmissionDetail; } + /** + * Get user progress questions. Need to be authenticated. + * @returns + */ + public async user_progress_questions( + filters: UserProgressQuestionListInput, + ): Promise { + await this.initialized; + const { data } = await this.graphql({ + variables: { filters: filters }, + query: USER_PROGRESS_QUESTIONS, + }); + return data.userProgressQuestionList as UserProgressQuestionList; + } + /** * Get a list of problems by tags and difficulty. * @param option From 44b27a45e321d5835e2574b6c0ce84fea18f92f4 Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Fri, 11 Apr 2025 17:38:52 +0800 Subject: [PATCH 13/14] chore: add changeset --- .changeset/warm-lobsters-act.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/warm-lobsters-act.md b/.changeset/warm-lobsters-act.md index 0ec7fc9..afaffef 100644 --- a/.changeset/warm-lobsters-act.md +++ b/.changeset/warm-lobsters-act.md @@ -3,4 +3,5 @@ --- Add the `submission_detail` API to leetcode.cn -Add GraphQL query support for the submission_detail interface of the leetcode.com to fix API errors +Add GraphQL query support for the `submission_detail` interface of the leetcode.com to fix API errors +Add `user_progress_questions` method to retrieve user progress with filters to the leetcode.com From 0c58c96ff27f138a8505496a386d661157f65fd9 Mon Sep 17 00:00:00 2001 From: jinzcdev Date: Fri, 11 Apr 2025 18:57:59 +0800 Subject: [PATCH 14/14] chore: update the changeset --- .changeset/curvy-planets-pay.md | 5 ----- .changeset/warm-lobsters-act.md | 13 ++++++++++--- 2 files changed, 10 insertions(+), 8 deletions(-) delete mode 100644 .changeset/curvy-planets-pay.md diff --git a/.changeset/curvy-planets-pay.md b/.changeset/curvy-planets-pay.md deleted file mode 100644 index c399b7d..0000000 --- a/.changeset/curvy-planets-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"leetcode-query": minor ---- - -Add the `submission_detail` API to leetcode.cn \ No newline at end of file diff --git a/.changeset/warm-lobsters-act.md b/.changeset/warm-lobsters-act.md index afaffef..6b39d78 100644 --- a/.changeset/warm-lobsters-act.md +++ b/.changeset/warm-lobsters-act.md @@ -2,6 +2,13 @@ "leetcode-query": major --- -Add the `submission_detail` API to leetcode.cn -Add GraphQL query support for the `submission_detail` interface of the leetcode.com to fix API errors -Add `user_progress_questions` method to retrieve user progress with filters to the leetcode.com +## Breaking Changes +- **`submission` method**: Now uses GraphQL query to fetch submission details, resulting in significant changes to return structure: + - Removed `problem_id` field, replaced by `question.questionId` + - Removed manually calculated percentiles (`runtime_percentile` and `memory_percentile`), replaced by API-provided `runtimePercentile` and `memoryPercentile` values + - Removed `details` field with submission data + - Return structure now directly matches GraphQL response format instead of the previous custom format + +## New Features +- Added `submission_detail` GraphQL API query support, fixing API errors for leetcode.com +- Added `user_progress_questions` method to retrieve user progress with filters for leetcode.com