leetcode-query
Version:
Get user profiles, submissions, and problems on LeetCode.
991 lines (958 loc) • 42.4 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
BASE_URL: () => BASE_URL,
BASE_URL_CN: () => BASE_URL_CN,
Cache: () => Cache,
Credential: () => Credential,
LeetCode: () => leetcode_default,
LeetCodeCN: () => leetcode_cn_default,
Mutex: () => Mutex,
RateLimiter: () => RateLimiter,
USER_AGENT: () => USER_AGENT,
_fetch: () => _fetch,
cache: () => cache,
caches: () => caches,
default: () => src_default,
fetch: () => _fetch,
fetcher: () => fetcher
});
module.exports = __toCommonJS(src_exports);
// src/leetcode.ts
var import_eventemitter32 = __toESM(require("eventemitter3"), 1);
// src/cache.ts
var Cache = class {
constructor() {
this._table = {};
}
/**
* Get an item from the cache.
* @param key The key of the item.
* @returns {any} The item, or null if it doesn't exist.
*/
get(key) {
const item = this._table[key];
if (item) {
if (item.expires > Date.now()) {
return item.value;
}
this.remove(key);
}
return null;
}
/**
* Set an item in the cache.
* @param key The key of the item.
* @param value The value of the item.
* @param expires The time in milliseconds until the item expires.
*/
set(key, value, expires = 6e4) {
this._table[key] = {
key,
value,
expires: expires > 0 ? Date.now() + expires : 0
};
}
/**
* Remove an item from the cache.
* @param key The key of the item.
*/
remove(key) {
delete this._table[key];
}
/**
* Clear the cache.
*/
clear() {
this._table = {};
}
/**
* Load the cache from a JSON string.
* @param json A {@link CacheTable}-like JSON string.
*/
load(json) {
this._table = JSON.parse(json);
}
};
var cache = new Cache();
var caches = { default: cache };
// src/constants.ts
var BASE_URL = "https://leetcode.com";
var BASE_URL_CN = "https://leetcode.cn";
var USER_AGENT = "Mozilla/5.0 LeetCode API";
// src/fetch.ts
var import_cross_fetch = require("@fetch-impl/cross-fetch");
var import_fetcher = require("@fetch-impl/fetcher");
var fetcher = new import_fetcher.Fetcher();
(0, import_cross_fetch.useCrossFetch)(fetcher);
var _fetch = (...args) => fetcher.fetch(...args);
var fetch_default = _fetch;
// src/utils.ts
function parse_cookie(cookie) {
return cookie.split(";").map((x) => x.trim().split("=")).reduce(
(acc, x) => {
if (x.length !== 2) {
return acc;
}
if (x[0].endsWith("csrftoken")) {
acc["csrftoken"] = x[1];
} else {
acc[x[0]] = x[1];
}
return acc;
},
{}
);
}
// src/credential.ts
async function get_csrf() {
const cookies_raw = await fetch_default(BASE_URL + "/graphql/", {
headers: {
"user-agent": USER_AGENT
}
}).then((res) => res.headers.get("set-cookie"));
if (!cookies_raw) {
return void 0;
}
const csrf_token = parse_cookie(cookies_raw).csrftoken;
return csrf_token;
}
var Credential = class {
constructor(data) {
if (data) {
this.session = data.session;
this.csrf = data.csrf;
}
}
/**
* Init the credential with or without leetcode session cookie.
* @param session
* @returns
*/
async init(session) {
this.csrf = await get_csrf();
if (session)
this.session = session;
return this;
}
};
// src/graphql/contest.graphql?raw
var contest_default = "query ($username: String!) {\n userContestRanking(username: $username) {\n attendedContestsCount\n rating\n globalRanking\n totalParticipants\n topPercentage\n badge {\n name\n }\n }\n userContestRankingHistory(username: $username) {\n attended\n trendDirection\n problemsSolved\n totalProblems\n finishTimeInSeconds\n rating\n ranking\n contest {\n title\n startTime\n }\n }\n}\n";
// src/graphql/daily.graphql?raw
var daily_default = "query {\n activeDailyCodingChallengeQuestion {\n date\n link\n question {\n questionId\n questionFrontendId\n boundTopicId\n title\n titleSlug\n content\n translatedTitle\n translatedContent\n isPaidOnly\n difficulty\n likes\n dislikes\n isLiked\n similarQuestions\n exampleTestcases\n contributors {\n username\n profileUrl\n avatarUrl\n }\n topicTags {\n name\n slug\n translatedName\n }\n companyTagStats\n codeSnippets {\n lang\n langSlug\n code\n }\n stats\n hints\n solution {\n id\n canSeeDetail\n paidOnly\n hasVideoSolution\n paidOnlyVideo\n }\n status\n sampleTestCase\n metaData\n judgerAvailable\n judgeType\n mysqlSchemas\n enableRunCode\n enableTestMode\n enableDebugger\n envInfo\n libraryUrl\n adminUrl\n challengeQuestion {\n id\n date\n incompleteChallengeCount\n streakCount\n type\n }\n note\n }\n }\n}\n";
// src/graphql/leetcode-cn/user-progress-questions.graphql?raw
var user_progress_questions_default = 'query userProgressQuestionList($filters: UserProgressQuestionListInput) {\n userProgressQuestionList(filters: $filters) {\n totalNum\n questions {\n translatedTitle\n frontendId\n title\n titleSlug\n difficulty\n lastSubmittedAt\n numSubmitted\n questionStatus\n lastResult\n topicTags {\n name\n nameTranslated\n slug\n }\n }\n }\n}\n\n# UserProgressQuestionListInput:\n# {\n# "filters": {\n# "skip": 10,\n# "limit": 10,\n# "questionStatus": "SOLVED", // Enums: SOLVED, ATTEMPTED\n# "difficulty": [\n# "EASY",\n# "MEDIUM",\n# "HARD"\n# ]\n# }\n# }\n';
// src/graphql/problem.graphql?raw
var problem_default = "query ($titleSlug: String!) {\n question(titleSlug: $titleSlug) {\n questionId\n questionFrontendId\n boundTopicId\n title\n titleSlug\n content\n translatedTitle\n translatedContent\n isPaidOnly\n difficulty\n likes\n dislikes\n isLiked\n similarQuestions\n exampleTestcases\n contributors {\n username\n profileUrl\n avatarUrl\n }\n topicTags {\n name\n slug\n translatedName\n }\n companyTagStats\n codeSnippets {\n lang\n langSlug\n code\n }\n stats\n hints\n solution {\n id\n canSeeDetail\n paidOnly\n hasVideoSolution\n paidOnlyVideo\n }\n status\n sampleTestCase\n metaData\n judgerAvailable\n judgeType\n mysqlSchemas\n enableRunCode\n enableTestMode\n enableDebugger\n envInfo\n libraryUrl\n adminUrl\n challengeQuestion {\n id\n date\n incompleteChallengeCount\n streakCount\n type\n }\n note\n }\n}\n";
// src/graphql/problems.graphql?raw
var problems_default = "query ($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {\n problemsetQuestionList: questionList(\n categorySlug: $categorySlug\n limit: $limit\n skip: $skip\n filters: $filters\n ) {\n total: totalNum\n questions: data {\n acRate\n difficulty\n freqBar\n questionFrontendId\n isFavor\n isPaidOnly\n status\n title\n titleSlug\n topicTags {\n name\n id\n slug\n }\n hasSolution\n hasVideoSolution\n }\n }\n}\n";
// src/graphql/profile.graphql?raw
var profile_default = "query ($username: String!) {\n allQuestionsCount {\n difficulty\n count\n }\n matchedUser(username: $username) {\n username\n socialAccounts\n githubUrl\n contributions {\n points\n questionCount\n testcaseCount\n }\n profile {\n realName\n websites\n countryName\n skillTags\n company\n school\n starRating\n aboutMe\n userAvatar\n reputation\n ranking\n }\n submissionCalendar\n submitStats {\n acSubmissionNum {\n difficulty\n count\n submissions\n }\n totalSubmissionNum {\n difficulty\n count\n submissions\n }\n }\n badges {\n id\n displayName\n icon\n creationDate\n }\n upcomingBadges {\n name\n icon\n }\n activeBadge {\n id\n }\n }\n recentSubmissionList(username: $username, limit: 20) {\n title\n titleSlug\n timestamp\n statusDisplay\n lang\n }\n}\n";
// src/graphql/recent-submissions.graphql?raw
var recent_submissions_default = "query ($username: String!, $limit: Int) {\n recentSubmissionList(username: $username, limit: $limit) {\n title\n titleSlug\n timestamp\n statusDisplay\n lang\n }\n}\n";
// src/graphql/submission-detail.graphql?raw
var submission_detail_default = "query submissionDetails($id: Int!) {\n submissionDetails(submissionId: $id) {\n id\n runtime\n runtimeDisplay\n runtimePercentile\n runtimeDistribution\n memory\n memoryDisplay\n memoryPercentile\n memoryDistribution\n code\n timestamp\n statusCode\n user {\n username\n profile {\n realName\n userAvatar\n }\n }\n lang {\n name\n verboseName\n }\n question {\n questionId\n titleSlug\n hasFrontendPreview\n }\n notes\n flagType\n topicTags {\n tagId\n slug\n name\n }\n runtimeError\n compileError\n lastTestcase\n codeOutput\n expectedOutput\n totalCorrect\n totalTestcases\n fullCodeOutput\n testDescriptions\n testBodies\n testInfo\n stdOutput\n }\n}\n";
// src/graphql/submissions.graphql?raw
var submissions_default = "query ($offset: Int!, $limit: Int!, $slug: String) {\n submissionList(offset: $offset, limit: $limit, questionSlug: $slug) {\n hasNext\n submissions {\n id\n lang\n time\n timestamp\n statusDisplay\n runtime\n url\n isPending\n title\n memory\n titleSlug\n }\n }\n}\n";
// src/graphql/whoami.graphql?raw
var whoami_default = "query {\n userStatus {\n userId\n username\n avatar\n isSignedIn\n isMockUser\n isPremium\n isAdmin\n isSuperuser\n isTranslator\n permissions\n }\n}\n";
// src/mutex.ts
var import_eventemitter3 = __toESM(require("eventemitter3"), 1);
var Mutex = class extends import_eventemitter3.default {
constructor(space = 1) {
super();
this.space = space;
this.used = 0;
this.releases = [];
}
async lock() {
if (this.used >= this.space) {
const lock = new Promise((r) => this.releases.push(r));
this.emit("wait", {
lock,
release: this.releases[this.releases.length - 1]
});
await lock;
}
this.used++;
this.emit("lock");
return this.used;
}
unlock() {
if (this.used <= 0) {
return 0;
}
if (this.releases.length > 0) {
this.releases.shift()?.();
}
this.used--;
this.emit("unlock");
if (this.used <= 0) {
this.emit("all-clear");
}
return this.used;
}
resize(space) {
this.space = space;
while (this.used < space && this.releases.length > 0) {
this.releases.shift()?.();
}
return this.space;
}
full() {
return this.used >= this.space;
}
waiting() {
return this.releases.length;
}
emit(event, ...args) {
return super.emit(event, ...args);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event, listener) {
return super.on(event, listener);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
once(event, listener) {
return super.once(event, listener);
}
};
var RateLimiter = class extends Mutex {
constructor({ limit = 20, interval = 1e4, concurrent = 2 } = {}) {
super(concurrent);
this.count = 0;
this.last = 0;
this.time_mutex = new Mutex(limit);
this.interval = interval;
this.time_mutex.on("lock", (...args) => this.emit("time-lock", ...args));
this.time_mutex.on("unlock", (...args) => this.emit("time-unlock", ...args));
}
async lock() {
if (this.last + this.interval < Date.now()) {
this.reset();
} else if (this.time_mutex.full() && !this.timer) {
this.cleaner();
}
await this.time_mutex.lock();
this.count++;
return super.lock();
}
reset() {
while (this.count > 0) {
this.time_mutex.unlock();
this.count--;
}
this.last = Date.now();
this.emit("timer-reset");
}
cleaner() {
this.timer = setTimeout(
() => {
this.reset();
setTimeout(() => {
if (this.time_mutex.waiting() > 0) {
this.cleaner();
} else {
this.timer = void 0;
}
}, 0);
},
this.last + this.interval - Date.now()
);
}
set limit(limit) {
this.time_mutex.resize(limit);
}
emit(event, ...args) {
return super.emit(event, ...args);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event, listener) {
return super.on(event, listener);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
once(event, listener) {
return super.once(event, listener);
}
};
// src/leetcode.ts
var LeetCode = class extends import_eventemitter32.default {
/**
* If a credential is provided, the LeetCode API will be authenticated. Otherwise, it will be anonymous.
* @param credential
* @param cache
*/
constructor(credential = null, cache2 = cache) {
super();
/**
* Rate limiter
*/
this.limiter = new RateLimiter();
let initialize;
this.initialized = new Promise((resolve) => {
initialize = resolve;
});
this.cache = cache2;
if (credential) {
this.credential = credential;
setImmediate(() => initialize());
} else {
this.credential = new Credential();
this.credential.init().then(() => initialize());
}
}
/**
* Get public profile of a user.
* @param username
* @returns
*
* ```javascript
* const leetcode = new LeetCode();
* const profile = await leetcode.user("jacoblincool");
* ```
*/
async user(username) {
await this.initialized;
const { data } = await this.graphql({
variables: { username },
query: profile_default
});
return data;
}
/**
* Get public contest info of a user.
* @param username
* @returns
*
* ```javascript
* const leetcode = new LeetCode();
* const profile = await leetcode.user_contest_info("jacoblincool");
* ```
*/
async user_contest_info(username) {
await this.initialized;
const { data } = await this.graphql({
variables: { username },
query: contest_default
});
return data;
}
/**
* Get recent submissions of a user. (max: 20 submissions)
* @param username
* @param limit
* @returns
*
* ```javascript
* const leetcode = new LeetCode();
* const submissions = await leetcode.recent_submissions("jacoblincool");
* ```
*/
async recent_submissions(username, limit = 20) {
await this.initialized;
const { data } = await this.graphql({
variables: { username, limit },
query: recent_submissions_default
});
return data.recentSubmissionList || [];
}
/**
* Get submissions of the credential user. Need to be authenticated.
*
* @returns
*
* ```javascript
* const credential = new Credential();
* await credential.init("SESSION");
* const leetcode = new LeetCode(credential);
* const submissions = await leetcode.submissions({ limit: 100, offset: 0 });
* ```
*/
async submissions({
limit = 20,
offset = 0,
slug
} = {}) {
await this.initialized;
const submissions = [];
const set = /* @__PURE__ */ new Set();
let cursor = offset;
while (submissions.length < limit) {
const { data } = await this.graphql({
variables: {
offset: cursor,
limit: limit - submissions.length > 20 ? 20 : limit - submissions.length,
slug
},
query: submissions_default
});
for (const submission of data.submissionList.submissions) {
submission.id = parseInt(submission.id, 10);
submission.timestamp = parseInt(submission.timestamp, 10) * 1e3;
submission.isPending = submission.isPending !== "Not Pending";
submission.runtime = parseInt(submission.runtime, 10) || 0;
submission.memory = parseFloat(submission.memory) || 0;
if (set.has(submission.id)) {
continue;
}
set.add(submission.id);
submissions.push(submission);
}
if (!data.submissionList.hasNext) {
break;
}
cursor += 20;
}
return submissions;
}
/**
* Get detail of a submission, including the code and percentiles.
* Need to be authenticated.
* @param id Submission ID
* @returns
*/
async submission(id) {
await this.initialized;
const { data } = await this.graphql({
variables: { id },
query: submission_detail_default
});
return data.submissionDetails;
}
/**
* Get user progress questions. Need to be authenticated.
* @returns
*/
async user_progress_questions(filters) {
await this.initialized;
const { data } = await this.graphql({
variables: { filters },
query: user_progress_questions_default
});
return data.userProgressQuestionList;
}
/**
* Get a list of problems by tags and difficulty.
* @param option
* @param option.category
* @param option.offset
* @param option.limit
* @param option.filters
* @returns
*/
async problems({
category = "",
offset = 0,
limit = 100,
filters = {}
} = {}) {
await this.initialized;
const variables = { categorySlug: category, skip: offset, limit, filters };
const { data } = await this.graphql({
variables,
query: problems_default
});
return data.problemsetQuestionList;
}
/**
* Get information of a problem by its slug.
* @param slug Problem slug
* @returns
*
* ```javascript
* const leetcode = new LeetCode();
* const problem = await leetcode.problem("two-sum");
* ```
*/
async problem(slug) {
await this.initialized;
const { data } = await this.graphql({
variables: { titleSlug: slug.toLowerCase().replace(/\s/g, "-") },
query: problem_default
});
return data.question;
}
/**
* Get daily challenge.
* @returns
*
* @example
* ```javascript
* const leetcode = new LeetCode();
* const daily = await leetcode.daily();
* ```
*/
async daily() {
await this.initialized;
const { data } = await this.graphql({
query: daily_default
});
return data.activeDailyCodingChallengeQuestion;
}
/**
* Check the information of the credential owner.
* @returns
*/
async whoami() {
await this.initialized;
const { data } = await this.graphql({
variables: {},
query: whoami_default
});
return data.userStatus;
}
/**
* Use GraphQL to query LeetCode API.
* @param query
* @returns
*/
async graphql(query) {
await this.initialized;
try {
await this.limiter.lock();
const BASE = BASE_URL;
const res = await fetch_default(`${BASE}/graphql`, {
method: "POST",
headers: {
"content-type": "application/json",
origin: BASE,
referer: BASE,
cookie: `csrftoken=${this.credential.csrf || ""}; LEETCODE_SESSION=${this.credential.session || ""};`,
"x-csrftoken": this.credential.csrf || "",
"user-agent": USER_AGENT,
...query.headers
},
body: JSON.stringify(query)
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}: ${await res.text()}`);
}
this.emit("receive-graphql", res);
if (res.headers.has("set-cookie")) {
const cookies = parse_cookie(res.headers.get("set-cookie") || "");
if (cookies["csrftoken"]) {
this.credential.csrf = cookies["csrftoken"];
this.emit("update-csrf", this.credential);
}
}
this.limiter.unlock();
return res.json();
} catch (err) {
this.limiter.unlock();
throw err;
}
}
emit(event, ...args) {
return super.emit(event, ...args);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event, listener) {
return super.on(event, listener);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
once(event, listener) {
return super.once(event, listener);
}
};
var leetcode_default = LeetCode;
// src/leetcode-cn.ts
var import_eventemitter33 = __toESM(require("eventemitter3"), 1);
// src/credential-cn.ts
async function get_csrf2() {
const res = await fetch_default(`${BASE_URL_CN}/graphql/`, {
method: "POST",
headers: {
"content-type": "application/json",
"user-agent": USER_AGENT
},
body: JSON.stringify({
operationName: "nojGlobalData",
variables: {},
query: "query nojGlobalData {\n siteRegion\n chinaHost\n websocketUrl\n}\n"
})
});
const cookies_raw = res.headers.get("set-cookie");
if (!cookies_raw) {
return void 0;
}
const csrf_token = parse_cookie(cookies_raw).csrftoken;
return csrf_token;
}
var Credential2 = class {
constructor(data) {
if (data) {
this.session = data.session;
this.csrf = data.csrf;
}
}
/**
* Init the credential with or without leetcode session cookie.
* @param session
* @returns
*/
async init(session) {
this.csrf = await get_csrf2();
if (session)
this.session = session;
return this;
}
};
// src/graphql/leetcode-cn/problem-set.graphql?raw
var problem_set_default = "query problemsetQuestionList(\n $categorySlug: String\n $limit: Int\n $skip: Int\n $filters: QuestionListFilterInput\n) {\n problemsetQuestionList(\n categorySlug: $categorySlug\n limit: $limit\n skip: $skip\n filters: $filters\n ) {\n hasMore\n total\n questions {\n acRate\n difficulty\n freqBar\n frontendQuestionId\n isFavor\n paidOnly\n solutionNum\n status\n title\n titleCn\n titleSlug\n topicTags {\n name\n nameTranslated\n id\n slug\n }\n }\n }\n}\n";
// src/graphql/leetcode-cn/problem.graphql?raw
var problem_default2 = "query ($titleSlug: String!) {\n question(titleSlug: $titleSlug) {\n questionId\n questionFrontendId\n boundTopicId\n title\n titleSlug\n content\n translatedTitle\n translatedContent\n isPaidOnly\n difficulty\n likes\n dislikes\n isLiked\n similarQuestions\n exampleTestcases\n contributors {\n username\n profileUrl\n avatarUrl\n }\n topicTags {\n name\n slug\n translatedName\n }\n companyTagStats\n codeSnippets {\n lang\n langSlug\n code\n }\n stats\n hints\n solution {\n id\n canSeeDetail\n }\n status\n sampleTestCase\n metaData\n judgerAvailable\n judgeType\n mysqlSchemas\n enableRunCode\n enableTestMode\n libraryUrl\n note\n }\n}\n";
// src/graphql/leetcode-cn/question-of-today.graphql?raw
var question_of_today_default = "query questionOfToday {\n todayRecord {\n date\n userStatus\n question {\n questionId\n frontendQuestionId: questionFrontendId\n difficulty\n title\n titleCn: translatedTitle\n titleSlug\n paidOnly: isPaidOnly\n freqBar\n isFavor\n acRate\n status\n solutionNum\n hasVideoSolution\n topicTags {\n name\n nameTranslated: translatedName\n id\n }\n extra {\n topCompanyTags {\n imgUrl\n slug\n numSubscribed\n }\n }\n }\n lastSubmission {\n id\n }\n }\n}\n";
// src/graphql/leetcode-cn/recent-ac-submissions.graphql?raw
var recent_ac_submissions_default = "query recentAcSubmissions($username: String!) {\n recentACSubmissions(userSlug: $username) {\n submissionId\n submitTime\n question {\n title\n translatedTitle\n titleSlug\n questionFrontendId\n }\n }\n}\n";
// src/graphql/leetcode-cn/submission-detail.graphql?raw
var submission_detail_default2 = "query submissionDetails($submissionId: ID!) {\n submissionDetail(submissionId: $submissionId) {\n id\n code\n timestamp\n statusDisplay\n isMine\n runtimeDisplay: runtime\n memoryDisplay: memory\n memory: rawMemory\n lang\n langVerboseName\n question {\n questionId\n titleSlug\n hasFrontendPreview\n }\n user {\n realName\n userAvatar\n userSlug\n }\n runtimePercentile\n memoryPercentile\n submissionComment {\n flagType\n }\n passedTestCaseCnt\n totalTestCaseCnt\n fullCodeOutput\n testDescriptions\n testInfo\n testBodies\n stdOutput\n ... on GeneralSubmissionNode {\n outputDetail {\n codeOutput\n expectedOutput\n input\n compileError\n runtimeError\n lastTestcase\n }\n }\n ... on ContestSubmissionNode {\n outputDetail {\n codeOutput\n expectedOutput\n input\n compileError\n runtimeError\n lastTestcase\n }\n }\n }\n}\n";
// src/graphql/leetcode-cn/user-contest-ranking.graphql?raw
var user_contest_ranking_default = "query userContestRankingInfo($username: String!) {\n userContestRanking(userSlug: $username) {\n attendedContestsCount\n rating\n globalRanking\n localRanking\n globalTotalParticipants\n localTotalParticipants\n topPercentage\n }\n userContestRankingHistory(userSlug: $username) {\n attended\n totalProblems\n trendingDirection\n finishTimeInSeconds\n rating\n score\n ranking\n contest {\n title\n titleCn\n startTime\n }\n }\n}\n";
// src/graphql/leetcode-cn/user-problem-submissions.graphql?raw
var user_problem_submissions_default = "query submissionList(\n $offset: Int!\n $limit: Int!\n $lastKey: String\n $questionSlug: String\n $lang: String\n $status: SubmissionStatusEnum\n) {\n submissionList(\n offset: $offset\n limit: $limit\n lastKey: $lastKey\n questionSlug: $questionSlug\n lang: $lang\n status: $status\n ) {\n lastKey\n hasNext\n submissions {\n id\n title\n status\n statusDisplay\n lang\n langName: langVerboseName\n runtime\n timestamp\n url\n isPending\n memory\n frontendId\n submissionComment {\n comment\n flagType\n }\n }\n }\n}\n";
// src/graphql/leetcode-cn/user-profile.graphql?raw
var user_profile_default = "query getUserProfile($username: String!) {\n userProfileUserQuestionProgress(userSlug: $username) {\n numAcceptedQuestions {\n count\n difficulty\n }\n numFailedQuestions {\n count\n difficulty\n }\n numUntouchedQuestions {\n count\n difficulty\n }\n }\n userProfilePublicProfile(userSlug: $username) {\n haveFollowed\n siteRanking\n profile {\n userSlug\n realName\n aboutMe\n asciiCode\n userAvatar\n gender\n websites\n skillTags\n ipRegion\n birthday\n location\n useDefaultAvatar\n certificationLevel\n github\n school: schoolV2 {\n schoolId\n logo\n name\n }\n company: companyV2 {\n id\n logo\n name\n }\n job\n globalLocation {\n country\n province\n city\n overseasCity\n }\n socialAccounts {\n provider\n profileUrl\n }\n skillSet {\n langLevels {\n langName\n langVerboseName\n level\n }\n topics {\n slug\n name\n translatedName\n }\n topicAreaScores {\n score\n topicArea {\n name\n slug\n }\n }\n }\n }\n educationRecordList {\n unverifiedOrganizationName\n }\n occupationRecordList {\n unverifiedOrganizationName\n jobTitle\n }\n }\n}\n";
// src/graphql/leetcode-cn/user-status.graphql?raw
var user_status_default = "query userStatus {\n userStatus {\n isSignedIn\n isAdmin\n isStaff\n isSuperuser\n isTranslator\n isVerified\n isPhoneVerified\n isWechatVerified\n checkedInToday\n username\n realName\n userSlug\n avatar\n region\n permissions\n useTranslation\n }\n}\n";
// src/leetcode-cn.ts
var LeetCodeCN = class extends import_eventemitter33.default {
/**
* If a credential is provided, the LeetCodeCN API will be authenticated. Otherwise, it will be anonymous.
* @param credential
* @param cache
*/
constructor(credential = null, cache2 = cache) {
super();
/**
* Rate limiter
*/
this.limiter = new RateLimiter();
let initialize;
this.initialized = new Promise((resolve) => {
initialize = resolve;
});
this.cache = cache2;
if (credential) {
this.credential = credential;
setImmediate(() => initialize());
} else {
this.credential = new Credential2();
this.credential.init().then(() => initialize());
}
}
/**
* Get public profile of a user.
* @param username
* @returns
*
* ```javascript
* const leetcode = new LeetCodeCN();
* const profile = await leetcode.user("leetcode");
* ```
*/
async user(username) {
await this.initialized;
const { data } = await this.graphql({
variables: { username },
query: user_profile_default
});
return data;
}
/**
* Get public contest info of a user.
* @param username
* @returns
*
* ```javascript
* const leetcode = new LeetCodeCN();
* const profile = await leetcode.user_contest_info("username");
* ```
*/
async user_contest_info(username) {
await this.initialized;
const { data } = await this.graphql(
{
operationName: "userContestRankingInfo",
variables: { username },
query: user_contest_ranking_default
},
"/graphql/noj-go/"
);
return data;
}
/**
* Get recent submissions of a user. (max: 20 submissions)
* @param username
* @param limit
* @returns
*
* ```javascript
* const leetcode = new LeetCodeCN();
* const submissions = await leetcode.recent_submissions("username");
* ```
*/
async recent_submissions(username) {
await this.initialized;
const { data } = await this.graphql(
{
variables: { username },
query: recent_ac_submissions_default
},
"/graphql/noj-go/"
);
return data.recentACSubmissions || [];
}
/**
* Get submissions of a problem.
* @param limit The number of submissions to get. Default is 20.
* @param offset The offset of the submissions to get. Default is 0.
* @param slug The slug of the problem. Required.
* @param param0
* @returns
*/
async problem_submissions({
limit = 20,
offset = 0,
slug,
lang,
status
} = {}) {
await this.initialized;
if (!slug) {
throw new Error("LeetCodeCN requires slug parameter for submissions");
}
const submissions = [];
const set = /* @__PURE__ */ new Set();
let cursor = offset;
while (submissions.length < limit) {
const { data } = await this.graphql({
variables: {
offset: cursor,
limit: limit - submissions.length > 20 ? 20 : limit - submissions.length,
questionSlug: slug,
lang,
status
},
query: user_problem_submissions_default
});
for (const submission of data.submissionList.submissions) {
submission.id = parseInt(submission.id, 10);
submission.timestamp = parseInt(submission.timestamp, 10) * 1e3;
submission.isPending = submission.isPending !== "Not Pending";
submission.runtime = parseInt(submission.runtime, 10) || 0;
submission.memory = parseFloat(submission.memory) || 0;
if (set.has(submission.id)) {
continue;
}
set.add(submission.id);
submissions.push(submission);
}
if (!data.submissionList.hasNext) {
break;
}
cursor += 20;
}
return submissions;
}
/**
* Get user progress questions. Need to be authenticated.
* @returns
*/
async user_progress_questions(filters) {
await this.initialized;
const { data } = await this.graphql({
variables: { filters },
query: user_progress_questions_default
});
return data.userProgressQuestionList;
}
/**
* Get a list of problems by tags and difficulty.
* @param option
* @param option.category
* @param option.offset
* @param option.limit
* @param option.filters
* @returns
*/
async problems({
category = "",
offset = 0,
limit = 100,
filters = {}
} = {}) {
await this.initialized;
const variables = { categorySlug: category, skip: offset, limit, filters };
const { data } = await this.graphql({
variables,
query: problem_set_default
});
return data.problemsetQuestionList;
}
/**
* Get information of a problem by its slug.
* @param slug Problem slug
* @returns
*
* ```javascript
* const leetcode = new LeetCodeCN();
* const problem = await leetcode.problem("two-sum");
* ```
*/
async problem(slug) {
await this.initialized;
const { data } = await this.graphql({
variables: { titleSlug: slug.toLowerCase().replace(/\s/g, "-") },
query: problem_default2
});
return data.question;
}
/**
* Get daily challenge.
* @returns
*
* @example
* ```javascript
* const leetcode = new LeetCodeCN();
* const daily = await leetcode.daily();
* ```
*/
async daily() {
await this.initialized;
const { data } = await this.graphql({
query: question_of_today_default
});
return data.todayRecord[0];
}
/**
* Check the status information of the current user.
* @returns User status information including login state and permissions
*/
async userStatus() {
await this.initialized;
const { data } = await this.graphql({
query: user_status_default
});
return data.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");
* ```
*/
async submissionDetail(submissionId) {
await this.initialized;
const { data } = await this.graphql({
variables: { submissionId },
query: submission_detail_default2
});
return data.submissionDetail;
}
/**
* Use GraphQL to query LeetCodeCN API.
* @param query
* @param endpoint Maybe you want to use `/graphql/noj-go/` instead of `/graphql/`.
* @returns
*/
async graphql(query, endpoint = "/graphql/") {
await this.initialized;
try {
await this.limiter.lock();
const BASE = BASE_URL_CN;
const res = await fetch_default(`${BASE}${endpoint}`, {
method: "POST",
headers: {
"content-type": "application/json",
origin: BASE,
referer: BASE,
cookie: `csrftoken=${this.credential.csrf || ""}; LEETCODE_SESSION=${this.credential.session || ""};`,
"x-csrftoken": this.credential.csrf || "",
"user-agent": USER_AGENT,
...query.headers
},
body: JSON.stringify(query)
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}: ${await res.text()}`);
}
this.emit("receive-graphql", res);
if (res.headers.has("set-cookie")) {
const cookies = parse_cookie(res.headers.get("set-cookie") || "");
if (cookies["csrftoken"]) {
this.credential.csrf = cookies["csrftoken"];
this.emit("update-csrf", this.credential);
}
}
this.limiter.unlock();
return res.json();
} catch (err) {
this.limiter.unlock();
throw err;
}
}
emit(event, ...args) {
return super.emit(event, ...args);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event, listener) {
return super.on(event, listener);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
once(event, listener) {
return super.once(event, listener);
}
};
var leetcode_cn_default = LeetCodeCN;
// src/index.ts
var src_default = leetcode_default;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BASE_URL,
BASE_URL_CN,
Cache,
Credential,
LeetCode,
LeetCodeCN,
Mutex,
RateLimiter,
USER_AGENT,
_fetch,
cache,
caches,
fetch,
fetcher
});