UNPKG

leetcode-query

Version:

Get user profiles, submissions, and problems on LeetCode.

941 lines (910 loc) 40.3 kB
// src/leetcode.ts import EventEmitter2 from "eventemitter3"; // 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 import { useCrossFetch } from "@fetch-impl/cross-fetch"; import { Fetcher } from "@fetch-impl/fetcher"; var fetcher = new Fetcher(); 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 import EventEmitter from "eventemitter3"; var Mutex = class extends EventEmitter { 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 EventEmitter2 { /** * 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 import EventEmitter3 from "eventemitter3"; // 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 EventEmitter3 { /** * 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; export { BASE_URL, BASE_URL_CN, Cache, Credential, leetcode_default as LeetCode, leetcode_cn_default as LeetCodeCN, Mutex, RateLimiter, USER_AGENT, _fetch, cache, caches, src_default as default, _fetch as fetch, fetcher };