Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dull-cats-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"leetcode-query": minor
---

Add APIs for leetcode.cn endpoints
6,655 changes: 6,655 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions src/_tests/credential-cn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { Credential } from "../credential-cn";

describe("Credential", () => {
it("should be able to pass session and csrf directly", async () => {
const credential = new Credential({
session: "test_session",
csrf: "test_csrf",
});
expect(credential.csrf).toBe("test_csrf");
expect(credential.session).toBe("test_session");
});

it("should be able to init without session", async () => {
const credential = new Credential();
await credential.init();
expect(credential.csrf).toBeDefined();
expect(credential.session).toBeUndefined();
});

it("should be able to init with session", async () => {
const credential = new Credential();
await credential.init("test_session");
expect(credential.csrf).toBeDefined();
expect(credential.session).toBe("test_session");
});
});
120 changes: 81 additions & 39 deletions src/_tests/leetcode-cn.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect, it } from "vitest";
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";

describe("LeetCode", { timeout: 15_000 }, () => {
describe("LeetCodeCN", { timeout: 15_000 }, () => {
describe("General", () => {
it("should be an instance of LeetCodeCN", () => {
const lc = new LeetCodeCN();
Expand All @@ -16,6 +18,48 @@ describe("LeetCode", { timeout: 15_000 }, () => {
});
});

describe("Authenticated", () => {
dotenv.config();
const credential = new Credential();
let lc: LeetCodeCN;

beforeAll(async () => {
await credential.init(process.env["TEST_CN_LEETCODE_SESSION"]);
lc = new LeetCodeCN(credential);
});

it.skipIf(!process.env["TEST_CN_LEETCODE_SESSION"])(
"should be able to get own submissions with slug",
async () => {
const submissions = await lc.problem_submissions({
limit: 30,
offset: 0,
slug: "two-sum",
});
expect(Array.isArray(submissions)).toBe(true);
},
);

it.skipIf(!process.env["TEST_CN_LEETCODE_SESSION"])(
"should be able to get user progress questions",
async () => {
const progress = await lc.user_progress_questions({
skip: 0,
limit: 20,
});
expect(progress).toBeDefined();
},
);

it.skipIf(!process.env["TEST_CN_LEETCODE_SESSION"])(
"should be able to get user signed in status",
async () => {
const user = await lc.userStatus();
expect(user.isSignedIn).toBe(true);
},
);
});

describe("Unauthenticated", () => {
const lc = new LeetCodeCN();
lc.limiter.limit = 100;
Expand All @@ -26,45 +70,43 @@ describe("LeetCode", { timeout: 15_000 }, () => {

it("should be able to get user profile", async () => {
const user = await lc.user("LeetCode");
expect(user.userProfilePublicProfile.username).toBe("LeetCode");
expect(user.userProfilePublicProfile.profile.realName).toBe("LeetCode");
});

it("should be able to use graphql", async () => {
const { data } = await lc.graphql({
operationName: "data",
variables: { username: "LeetCode" },
query: `
query data($username: String!) {
progress: userProfileUserQuestionProgress(userSlug: $username) {
ac: numAcceptedQuestions { difficulty count }
wa: numFailedQuestions { difficulty count }
un: numUntouchedQuestions { difficulty count }
}
user: userProfilePublicProfile(userSlug: $username) {
username
ranking: siteRanking
profile {
realname: realName
about: aboutMe
avatar: userAvatar
skills: skillTags
country: countryName
}
}
submissions: recentSubmitted(userSlug: $username) {
id: submissionId
status
lang
time: submitTime
question {
title: translatedTitle
slug: titleSlug
}
}
}
`,
});
expect(data.user.username).toBe("LeetCode");
it("should be able to get user's contest info", async () => {
const contest = await lc.user_contest_info("LeetCode");
expect(contest).toBeDefined();
});

it("should be able to get user's recent submissions", async () => {
const submissions = await lc.recent_submissions("LeetCode");
expect(Array.isArray(submissions)).toBe(true);
});

it("should be able to get problems list", async () => {
const problems = await lc.problems({ limit: 10 });
expect(problems.questions.length).toBe(10);
});

it("should be able to get problem by slug", async () => {
const problem = await lc.problem("two-sum");
expect(problem.titleSlug).toBe("two-sum");
});

it("should be able to get daily challenge", async () => {
const daily = await lc.daily();
expect(daily.question).toBeDefined();
});

it("should be able to get user status", async () => {
const user = await lc.userStatus();
expect(user.isSignedIn).toBe(false);
});

it("should throw error when trying to get submissions without slug", async () => {
await expect(lc.problem_submissions({ limit: 30, offset: 0 })).rejects.toThrow(
"LeetCodeCN requires slug parameter for submissions",
);
});

it("should be able to use graphql noj-go", async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/_tests/leetcode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe("LeetCode", { timeout: 15_000 }, () => {
"should be able to get own submissions",
async () => {
const submissions = await lc.submissions({ limit: 100, offset: 0 });
expect(submissions.length).toBe(100);
expect(submissions.length).greaterThan(0).lessThanOrEqual(100);
},
);

Expand Down
2 changes: 1 addition & 1 deletion src/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ICredential } from "./types";
import { parse_cookie } from "./utils";

async function get_csrf() {
const cookies_raw = await fetch(BASE_URL, {
const cookies_raw = await fetch(BASE_URL + "/graphql/", {
headers: {
"user-agent": USER_AGENT,
},
Expand Down
35 changes: 35 additions & 0 deletions src/graphql/leetcode-cn/problem-set.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
query problemsetQuestionList(
$categorySlug: String
$limit: Int
$skip: Int
$filters: QuestionListFilterInput
) {
problemsetQuestionList(
categorySlug: $categorySlug
limit: $limit
skip: $skip
filters: $filters
) {
hasMore
total
questions {
acRate
difficulty
freqBar
frontendQuestionId
isFavor
paidOnly
solutionNum
status
title
titleCn
titleSlug
topicTags {
name
nameTranslated
id
slug
}
}
}
}
51 changes: 51 additions & 0 deletions src/graphql/leetcode-cn/problem.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
query ($titleSlug: String!) {
question(titleSlug: $titleSlug) {
questionId
questionFrontendId
boundTopicId
title
titleSlug
content
translatedTitle
translatedContent
isPaidOnly
difficulty
likes
dislikes
isLiked
similarQuestions
exampleTestcases
contributors {
username
profileUrl
avatarUrl
}
topicTags {
name
slug
translatedName
}
companyTagStats
codeSnippets {
lang
langSlug
code
}
stats
hints
solution {
id
canSeeDetail
}
status
sampleTestCase
metaData
judgerAvailable
judgeType
mysqlSchemas
enableRunCode
enableTestMode
libraryUrl
note
}
}
36 changes: 36 additions & 0 deletions src/graphql/leetcode-cn/question-of-today.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
query questionOfToday {
todayRecord {
date
userStatus
question {
questionId
frontendQuestionId: questionFrontendId
difficulty
title
titleCn: translatedTitle
titleSlug
paidOnly: isPaidOnly
freqBar
isFavor
acRate
status
solutionNum
hasVideoSolution
topicTags {
name
nameTranslated: translatedName
id
}
extra {
topCompanyTags {
imgUrl
slug
numSubscribed
}
}
}
lastSubmission {
id
}
}
}
12 changes: 12 additions & 0 deletions src/graphql/leetcode-cn/recent-ac-submissions.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
query recentAcSubmissions($username: String!) {
recentACSubmissions(userSlug: $username) {
submissionId
submitTime
question {
title
translatedTitle
titleSlug
questionFrontendId
}
}
}
25 changes: 25 additions & 0 deletions src/graphql/leetcode-cn/user-contest-ranking.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
query userContestRankingInfo($username: String!) {
userContestRanking(userSlug: $username) {
attendedContestsCount
rating
globalRanking
localRanking
globalTotalParticipants
localTotalParticipants
topPercentage
}
userContestRankingHistory(userSlug: $username) {
attended
totalProblems
trendingDirection
finishTimeInSeconds
rating
score
ranking
contest {
title
titleCn
startTime
}
}
}
38 changes: 38 additions & 0 deletions src/graphql/leetcode-cn/user-problem-submissions.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
query submissionList(
$offset: Int!
$limit: Int!
$lastKey: String
$questionSlug: String!
$lang: String
$status: SubmissionStatusEnum
) {
submissionList(
offset: $offset
limit: $limit
lastKey: $lastKey
questionSlug: $questionSlug
lang: $lang
status: $status
) {
lastKey
hasNext
submissions {
id
title
status
statusDisplay
lang
langName: langVerboseName
runtime
timestamp
url
isPending
memory
frontendId
submissionComment {
comment
flagType
}
}
}
}
Loading