UNPKG

@leetnotion/leetcode-api

Version:

Get user profiles, submissions, and problems on LeetCode.

1,624 lines (1,592 loc) 67.4 kB
// src/base-leetcode.ts import EventEmitter2 from "eventemitter3"; // src/constants.ts var BASE_URL = "https://leetcode.com"; var BASE_URL_CN = "https://leetcode.cn"; var USER_AGENT = "Mozilla/5.0 LeetCode API"; var PROBLEM_CATEGORIES = [ "algorithms", "database", "javascript", "shell", "concurrency", "pandas" ]; // 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/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/utils.ts function parse_cookie(cookie) { return cookie.split(";").map((x) => { const trimmed = x.trim(); const idx = trimmed.indexOf("="); if (idx === -1) return [trimmed, ""]; return [trimmed.slice(0, idx), trimmed.slice(idx + 1)]; }).reduce( (acc, x) => { acc[x[0]] = x[1]; return acc; }, {} ); } // src/base-leetcode.ts var BaseLeetCode = class extends EventEmitter2 { constructor(credential, createCredential, cache2) { super(); /** * Rate limiter */ this.limiter = new RateLimiter(); let initialize; this.initialized = new Promise((resolve) => { initialize = () => resolve(true); }); this.cache = cache2; if (credential) { this.credential = credential; setImmediate(() => initialize()); } else { this.credential = createCredential(); this.credential.init().then(() => initialize()); } } /** * Use GraphQL to query LeetCode API. * @param query * @param endpoint The GraphQL endpoint path. Default is `/graphql`. * @returns */ async graphql(query, endpoint = "/graphql") { await this.initialized; const { cacheTime, headers: queryHeaders, ...queryBody } = query; if (cacheTime) { const cacheKey = JSON.stringify({ query: queryBody.query, variables: queryBody.variables, endpoint }); const cached = this.cache.get(cacheKey); if (cached) { return cached; } } await this.limiter.lock(); try { const BASE = this.baseUrl; 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, ...queryHeaders }, body: JSON.stringify(queryBody) }); 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); } } const result = await res.json(); if (cacheTime) { const cacheKey = JSON.stringify({ query: queryBody.query, variables: queryBody.variables, endpoint }); this.cache.set(cacheKey, result, cacheTime); } return result; } finally { this.limiter.unlock(); } } 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/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/base-credential.ts var BaseCredential = 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 this.fetchCsrf(); if (session) this.session = session; return this; } }; // src/credential.ts var Credential = class extends BaseCredential { async fetchCsrf() { const cookies_raw = await fetch_default(BASE_URL, { 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; } }; // 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 userStatus\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 id\n isPending\n lang\n memory\n runtime\n statusDisplay\n time\n timestamp\n title\n titleSlug\n url\n }\n}\n"; // src/graphql/whoami.graphql?raw var whoami_default = "query globalData {\n userStatus {\n userId\n username\n avatar\n isSignedIn\n isMockUser\n isPremium\n isAdmin\n isSuperuser\n isTranslator\n activeSessionId\n checkedInToday\n permissions\n }\n}\n"; // src/leetcode.ts var LeetCode = class extends BaseLeetCode { /** * If a credential is provided, the LeetCode API will be authenticated. Otherwise, it will be anonymous. * @param credential * @param cache */ constructor(credential = null, cache2 = new Cache()) { super(credential, () => new Credential(), cache2); this.baseUrl = BASE_URL; } /** * Get public profile of a user. * @param username * @returns * * ```javascript * const leetcode = new LeetCode(); * const profile = await leetcode.user("codewithsathya"); * ``` */ async user(username) { await this.initialized; const { data } = await this.graphql({ variables: { username }, query: profile_default, cacheTime: 3e5 // 5 minutes }); return data; } /** * Get public contest info of a user. * @param username * @returns * */ async user_contest_info(username) { await this.initialized; const { data } = await this.graphql({ variables: { username }, query: contest_default, cacheTime: 3e5 // 5 minutes }); 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_user_submissions("codewitsathya"); * ``` */ async recent_user_submissions(username, limit = 20) { await this.initialized; const { data } = await this.graphql({ variables: { username, limit }, query: recent_submissions_default, cacheTime: 3e4 // 30 seconds }); 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 } = {}) { let allSubmissions = []; let cursor = offset; while (allSubmissions.length < limit) { const { submissions_dump: submissions, has_next } = await this.submissionsApi({ offset: cursor, limit: limit <= 20 ? limit : 20 }); allSubmissions = [...allSubmissions, ...submissions]; if (!has_next) { break; } cursor += 20; } return allSubmissions.slice(0, limit); } async submissionsApi({ offset = 0, limit = 20 }) { await this.initialized; if (limit > 20) limit = 20; await this.limiter.lock(); try { const res = await fetch_default(`${BASE_URL}/api/submissions/?offset=${offset}&limit=${limit}`, { method: "GET", headers: { "content-type": "application/json", origin: BASE_URL, referer: BASE_URL, cookie: `csrftoken=${this.credential.csrf || ""}; LEETCODE_SESSION=${this.credential.session || ""};`, "x-csrftoken": this.credential.csrf || "", "user-agent": USER_AGENT } }); if (!res.ok) { throw new Error(`HTTP ${res.status} ${res.statusText}: ${await res.text()}`); } 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); } } return await res.json(); } finally { this.limiter.unlock(); } } /** * Get detail of a submission, including the code and percentiles. * Need to be authenticated. * @param id Submission ID * @returns * @deprecated * */ async submission(id) { await this.initialized; await this.limiter.lock(); try { const res = await fetch_default(`${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]; if (!data) { throw new Error("Failed to parse submission page data"); } const jsonStr = data.replace(/'/g, '"').replace(/(\w+)\s*:/g, '"$1":').replace(/,\s*}/g, "}").replace(/,\s*]/g, "]"); const json = JSON.parse(jsonStr); 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) => [+item[0], item[1]] ) : [], runtime_percentile: 0, memory: parseInt(json.memory), memory_distribution: json.memoryDistributionFormatted ? JSON.parse(json.memoryDistributionFormatted).distribution.map( (item) => [+item[0], item[1]] ) : [], 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 / 1e3 ? p : 0), 0 ); return result; } finally { this.limiter.unlock(); } } /** * 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, cacheTime: 6e5 // 10 minutes }); return data.problemsetQuestionList; } /** * Get information of a problem by its slug. * @param slug * @returns */ async problem(slug) { await this.initialized; const { data } = await this.graphql({ variables: { titleSlug: slug.toLowerCase().replace(/\s/g, "-") }, query: problem_default, cacheTime: 6e5 // 10 minutes }); 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, cacheTime: 0 }); return data.activeDailyCodingChallengeQuestion; } /** * Check the information of the credential owner. * @returns */ async whoami() { await this.initialized; const { data } = await this.graphql({ operationName: "globalData", variables: {}, query: whoami_default }); return data.userStatus; } }; var leetcode_default = LeetCode; // src/credential-cn.ts var CredentialCN = class extends BaseCredential { async fetchCsrf() { 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; } }; // src/leetcode-cn.ts var LeetCodeCN = class extends BaseLeetCode { /** * If a credential is provided, the LeetCodeCN API will be authenticated. Otherwise, it will be anonymous. * @param credential * @param cache */ constructor(credential = null, cache2 = new Cache()) { super(credential, () => new CredentialCN(), cache2); this.baseUrl = BASE_URL_CN; } /** * Get public profile of a user. * @param username * @returns * * ```javascript * const leetcode = new LeetCodeCN(); * const profile = await leetcode.user("codewithsathya"); * ``` */ async user(username) { await this.initialized; const { data } = await this.graphql({ operationName: "getUserProfile", variables: { username }, cacheTime: 3e5, // 5 minutes query: ` query getUserProfile($username: String!) { userProfileUserQuestionProgress(userSlug: $username) { numAcceptedQuestions { difficulty count } numFailedQuestions { difficulty count } numUntouchedQuestions { difficulty count } } userProfilePublicProfile(userSlug: $username) { username haveFollowed siteRanking profile { userSlug realName aboutMe userAvatar location gender websites skillTags contestCount asciiCode medals { name year month category } ranking { currentLocalRanking currentGlobalRanking currentRating totalLocalUsers totalGlobalUsers } socialAccounts { provider profileUrl } } } } ` }); return data; } /** * Use GraphQL to query LeetCodeCN API. * @param query * @param endpoint Maybe you want to use `/graphql/noj-go/` instead of `/graphql/`. * @returns */ async graphql(...args) { if (args.length < 2 || args[1] === void 0) { args[1] = "/graphql/"; } return super.graphql(...args); } }; var leetcode_cn_default = LeetCodeCN; // src/graphql/checkin.graphql?raw var checkin_default = "mutation checkin {\n checkin {\n checkedIn\n ok\n error\n __typename\n }\n}\n"; // src/graphql/collect-easter-egg.graphql?raw var collect_easter_egg_default = "mutation collectContestEasterEgg {\n collectContestEasterEgg {\n ok\n }\n}\n"; // src/graphql/company-tags.graphql?raw var company_tags_default = "query questionCompanyTags {\n companyTags {\n id\n imgUrl\n name\n slug\n questionCount\n questionIds\n frequencies\n }\n}\n"; // src/graphql/custom-problem.graphql?raw var custom_problem_default = "query problemsetQuestionList(\n $categorySlug: String\n $limit: Int\n $skip: Int\n $filters: QuestionListFilterInput\n) {\n problemsetQuestionList: questionList(\n categorySlug: $categorySlug\n limit: $limit\n skip: $skip\n filters: $filters\n ) {\n total: totalNum\n questions: data {\n title\n difficulty\n topicTags {\n name\n }\n companyTagStats\n frequency\n similarQuestions\n questionFrontendId\n isPaidOnly\n solution {\n url\n paidOnly\n hasVideoSolution\n }\n questionId\n likes\n dislikes\n stats\n titleSlug\n }\n }\n}\n"; // src/graphql/is-easter-egg-collected.graphql?raw var is_easter_egg_collected_default = "query easterEgg {\n isEasterEggCollected\n}\n"; // src/graphql/lists.graphql?raw var lists_default = "query myFavoriteList {\n myCreatedFavoriteList {\n favorites {\n name\n slug\n }\n hasMore\n totalLength\n }\n}\n"; // src/graphql/minimal-company-tags.graphql?raw var minimal_company_tags_default = "query questionCompanyTags {\n companyTags {\n name\n questions {\n questionFrontendId\n }\n }\n}\n"; // src/graphql/no-of-problems.graphql?raw var no_of_problems_default = "query problemsetQuestionList($categorySlug: String, $filters: QuestionListFilterInput) {\n problemsetQuestionList: questionList(categorySlug: $categorySlug, filters: $filters) {\n total: totalNum\n }\n}\n"; // src/graphql/past-contests.graphql?raw var past_contests_default = "query contestV2HistoryContests($skip: Int!, $limit: Int!) {\n contestV2HistoryContests(skip: $skip, limit: $limit) {\n totalNum\n contests {\n titleSlug\n title\n titleCn\n startTime\n duration\n cardImg\n cardImgApp\n companyWatermark\n solved\n totalQuestions\n }\n }\n}\n"; // src/graphql/question-detail.graphql?raw var question_detail_default = "query questionDetail($titleSlug: String!) {\n languageList {\n id\n name\n }\n submittableLanguageList {\n id\n name\n verboseName\n }\n statusList {\n id\n name\n }\n questionDiscussionTopic(questionSlug: $titleSlug) {\n id\n commentCount\n topLevelCommentCount\n }\n ugcArticleOfficialSolutionArticle(questionSlug: $titleSlug) {\n uuid\n chargeType\n canSee\n hasVideoArticle\n }\n question(titleSlug: $titleSlug) {\n title\n titleSlug\n questionId\n questionFrontendId\n questionTitle\n translatedTitle\n content\n translatedContent\n categoryTitle\n difficulty\n stats\n companyTagStatsV2\n topicTags {\n name\n slug\n translatedName\n }\n positionLevelTags {\n name\n nameTranslated\n slug\n }\n similarQuestionList {\n difficulty\n titleSlug\n title\n translatedTitle\n isPaidOnly\n }\n mysqlSchemas\n dataSchemas\n frontendPreviews\n likes\n dislikes\n isPaidOnly\n status\n canSeeQuestion\n enableTestMode\n metaData\n enableRunCode\n enableSubmit\n enableDebugger\n envInfo\n isLiked\n nextChallenges {\n difficulty\n title\n titleSlug\n questionFrontendId\n }\n libraryUrl\n adminUrl\n hints\n codeSnippets {\n code\n lang\n langSlug\n }\n exampleTestcaseList\n hasFrontendPreview\n featuredContests {\n titleSlug\n title\n }\n }\n}\n"; // src/graphql/questions-of-list.graphql?raw var questions_of_list_default = "query favoriteQuestionList($favoriteSlug: String!, $filter: FavoriteQuestionFilterInput) {\n favoriteQuestionList(favoriteSlug: $favoriteSlug, filter: $filter) {\n questions {\n questionFrontendId\n status\n title\n titleSlug\n translatedTitle\n isInMyFavorites\n frequency\n topicTags {\n name\n slug\n }\n }\n totalLength\n hasMore\n }\n}\n"; // src/graphql/topic-tags.graphql?raw var topic_tags_default = "query problemsetQuestionList(\n $categorySlug: String\n $limit: Int\n $skip: Int\n $filters: QuestionListFilterInput\n) {\n problemsetQuestionList: questionList(\n categorySlug: $categorySlug\n limit: $limit\n skip: $skip\n filters: $filters\n ) {\n total: totalNum\n questions: data {\n questionFrontendId\n topicTags {\n name\n }\n }\n }\n}\n"; // src/problem-properties.ts var problemProperties = [ { title: "Allow discussion", property: "allowDiscuss", graphql: "allowDiscuss", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Article", property: "article", graphql: "article", enable: false, private: false, isPremium: false, needParsing: true, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Category title", property: "categoryTitle", graphql: "categoryTitle", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Code definition", property: "codeDefinition", graphql: "codeDefinition", enable: false, private: false, isPremium: false, needParsing: true, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Code snippets", property: "codeSnippets", graphql: `codeSnippets { code lang langSlug }`, enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: true, problemsPerRequest: 200 }, { title: "Company tag stats", property: "companyTagStats", graphql: "companyTagStats", enable: true, private: true, isPremium: true, needParsing: true, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Content", property: "content", graphql: "content", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: true, problemsPerRequest: 500 }, { title: "Difficulty", property: "difficulty", graphql: "difficulty", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Dislikes", property: "dislikes", graphql: "dislikes", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Enable run code", property: "enableRunCode", graphql: "enableRunCode", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Enable submit", property: "enableSubmit", graphql: "enableSubmit", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Enable test mode", property: "enableTestMode", graphql: "enableTestMode", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Frequency", property: "frequency", graphql: "frequency", enable: true, private: true, isPremium: true, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Hints", property: "hints", graphql: "hints", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Info verified", property: "infoVerified", graphql: "infoVerified", enable: false, private: true, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Interpret url", property: "interpretUrl", graphql: "interpretUrl", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Is liked", property: "isLiked", graphql: "isLiked", enable: false, private: true, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Is paid only", property: "isPaidOnly", graphql: "isPaidOnly", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Judge type", property: "judgeType", graphql: "judgeType", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Judger available", property: "judgerAvailable", graphql: "judgerAvailable", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Lang to valid playground", property: "langToValidPlayground", graphql: "langToValidPlayground", enable: false, private: false, isPremium: false, needParsing: true, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Library URL", property: "libraryUrl", graphql: "libraryUrl", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Likes", property: "likes", graphql: "likes", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Metadata", property: "metaData", graphql: "metaData", enable: false, private: false, isPremium: false, // This can also be parsed. needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Mysql schemas", property: "mysqlSchemas", graphql: "mysqlSchemas", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Next challenge pairs", property: "nextChallengePairs", graphql: "nextChallengePairs", enable: false, private: true, isPremium: false, needParsing: true, needRequestChunking: true, problemsPerRequest: 500 }, { title: "Note", property: "note", graphql: "note", enable: false, private: true, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Question detail URL", property: "questionDetailUrl", graphql: "questionDetailUrl", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Question frontend ID", property: "questionFrontendId", graphql: "questionFrontendId", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Question ID", property: "questionId", graphql: "questionId", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Question title", property: "questionTitle", graphql: "questionTitle", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Question title slug", property: "questionTitleSlug", graphql: "questionTitleSlug", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Question type", property: "questionType", graphql: "questionType", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Sample testcase", property: "sampleTestCase", graphql: "sampleTestCase", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: true, problemsPerRequest: 500 }, { title: "Session ID", property: "sessionId", graphql: "sessionId", enable: false, private: true, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Similar questions", property: "similarQuestions", graphql: "similarQuestions", enable: true, private: false, isPremium: false, needParsing: true, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Solution", property: "solution", graphql: `solution { canSeeDetail content contentTypeId id rating { average count id userRating { id score } } title url paidOnly hasVideoSolution paidOnlyVideo }`, enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: true, problemsPerRequest: 100 }, { title: "Stats", property: "stats", graphql: "stats", enable: true, private: false, isPremium: false, needParsing: true, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Status", property: "status", graphql: "status", enable: false, private: true, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Submit URL", property: "submitUrl", graphql: "submitUrl", enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Title", property: "title", graphql: "title", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Title slug", property: "titleSlug", graphql: "titleSlug", enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Topic tags", property: "topicTags", graphql: `topicTags { name slug translatedName }`, enable: true, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Translated content", property: `translatedContent`, graphql: `translatedContent`, enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 }, { title: "Translated title", property: "translatedTitle", graphql: `translatedTitle`, enable: false, private: false, isPremium: false, needParsing: false, needRequestChunking: false, problemsPerRequest: 1e5 } ]; var problem_properties_default = problemProperties; // src/leetcode-advanced.ts var LeetCodeAdvanced = class extends LeetCode { constructor(credential = null, cache2 = cache) { super(credential, cache2); this.problemProperties = problem_properties_default; this.uniquePropertyOfProblem = "questionFrontendId"; } setUniquePropertyOfProblem(property) { this.uniquePropertyOfProblem = property; } setCustomProblemProperties(problemProperties2) { this.problemProperties = problemProperties2; } /** * Checks if easter egg is already collected. * Need to be authenticated. * @returns boolean */ async isEasterEggCollected() { await this.initialized; const { data } = await this.graphql({ query: is_easter_egg_collected_default }); return data.isEasterEggCollected; } /** * Collects easter egg if available. * Need to be authenticated. */ async collectEasterEgg() { await this.initialized; const easterEggCollected = await this.isEasterEggCollected(); if (easterEggCollected) { return false; } await this.graphql({ operationName: "collectContestEasterEgg", variables: {}, query: collect_easter_egg_default }); return true; } /** * Get all topic tags for each question with question frontend id as key * @returns */ async topicTags({ limit = 1e4, problemsPerRequest = 100, skip = 0 } = {}) { await this.initialized; const questionIdToTopicTags = {}; const noOfProblems = Math.min(skip + limit, await this.noOfProblems()); for (let offset = skip; offset < noOfProblems; offset += problemsPerRequest) { const { data } = await this.graphql({ query: topic_tags_default, variables: { categorySlug: "", filters: {}, skip: offset, limit: problemsPerRequest } }); const problems = data.problemsetQuestionList.questions; for (const problem of problems) { questionIdToTopicTags[problem.questionFrontendId] = problem.topicTags.map( ({ name }) => name ); } } return questionIdToTopicTags; } /** * Get all company tags with their details. * For company wise question details, need to be authenticated and should be premium user. * @returns */ async companyTags() { await this.initialized; const { data } = await this.graphql({ query: company_tags_default }); return data.companyTags; } /** * Get question frontend id to company tags mapping. * Need to be authenticated and should be premium user * @returns */ async getQuestionIdCompanyTagsMapping() { await this.initialized; const { data } = await this.graphql({ query: minimal_company_tags_default }); const companyTags = data.companyTags; if (companyTags === null) { throw new Error(`You should have leetcode premium to access company tags information`); } const questionIdToCompanyTags = {}; for (const companyTag of companyTags) { for (const { questionFrontendId: id } of companyTag.questions) { if (!questionIdToCompanyTags[id]) { questionIdToCompanyTags[id] = []; } questionIdToCompanyTags[id].push(companyTag.name); } } return questionIdToCompanyTags; } /** * Checkin to collect a coin * Need to be authenticated */ async checkIn() { await this.initialized; const { data } = await this.graphql({ query: checkin_default }); return data.checkin.checkedIn; } /** * Get recent submission of current user. * Need to be authenticated * @returns Submission * @returns null if there are no recent submissions */ async recentSubmission() { const submissions = await this.submissions({ limit: 1, offset: 0 }); if (submissions.length == 0) return null; return submissions[0]; } /** * Get recent submission of a user by username * Need to be authenticated * @param username * @returns Submission * @returns null if there are no recent submissions */ async recentSubmissionOfUser(username) { const recentSubmissions = await this.recent_user_submissions(username, 1); if (recentSubmissions.length == 0) return null; return recentSubmissions[0]; } /** * Get no of total problems in leetcode right now. * @returns number */ async noOfProblems() { await this.initialized; const { data } = await this.graphql({ query: no_of_problems_default, variables: { categorySlug: "", filters: {} } }); return data.problemsetQuestionList.total; } /** * Get leetcode lists of the user * Need to be authenticated * @returns array of leetcode lists */ async getLists() { await this.initialized; const { data } = await this.graphql({ query: lists_default }); return data.myCreatedFavoriteList.favorites; } /** * Get all questions of a leetcode list * Need to be authenticated * @param slug slug id of the leetcode list * @returns array of questions */ async getQuestionsOfList(slug) { await this.initialized; const { data } = await this.graphql({ query: questions_of_list_default, variables: { favoriteSlug: slug } }); return data.favoriteQuestionList.questions; } /** * Get problem types for all questions. * @returns Record<string, string> - A mapping of question frontend IDs to their problem type. */ async getProblemTypes() { const problemTypes = {}; for (const category of PROBLEM_CATEGORIES) { const pairs = await this.fetchCategoryQuestions(category); for (const pair of pairs) { const id = String(pair.stat.frontend_question_id); problemTypes[id] = category; } } return problemTypes; } /** * Get leetcode problems with optional parameters. * @param option * @param option.limit - Total number of problems to fetch. Default is 10000. * @param option.problemsPerRequest - Number of problems to fetch per request. Default is 100. * @param option.skip - Number of problems to skip from the start. Default is 0. * @param option.callbackFn - Optional callback function that will be called after each request with the currently fetched problems. * @returns Array of LeetcodeProblem */ async getLeetcodeProblems({ limit = 1e4, problemsPerRequest = 100, skip = 0, callbackFn = null } = {}) { await this.initialized; const noOfProblems = Math.min(skip + limit, await this.noOfProblems()); let problems = []; for (let offset = skip; offset < noOfProblems; offset += problemsPerRequest) { const { data } = await this.graphql({ query: custom_problem_default, variables: { categorySlug: "", filters: {}, skip: offset, limit: problemsPerRequest } }); const consolidatedProblems = data.problemsetQuestionList.questions; problems = [...problems, ...consolidatedProblems]; if (callbackFn) { await callbackFn(problems); } } return this.parseProblems(problems); } async parseProblems(problems) { const parsingDetails = {}; for (const fieldDetails of problem_properties_default) { parsingDetails[fieldDetails.property] = fieldDetails.needParsing; } for (const problem of problems) { for (const field of Object.keys(problem)) { if (parsingDetails[field]) { problem[field] = JSON.parse( problem[field] ); } } } return problems; } /** * Get list of detailed problems by tags and difficulty. * This will collect details according to the problemProperties configuration and this is slow. * @param option * @param option.category * @param option.offset * @param option.limit * @param option.filters * @returns DetailedProblem[] */ async detailedProblems({ category = "", offset = 0, limit = 1e5, filters = {} } = {}) { await this.initialized; let problems = []; for (const problemProperty of this.problemProperties.filter(({ enable }) => enable)) { const consolidatedProblems = await this.problemsOfProperty(problemProperty, { category, offset, limit, filters }); if (problems.length === 0) { problems = consolidatedProblems; } else { problems = this.combineProperties(problems, consolidatedProblems); } } return problems; } /** * Get problems with a particular property and requests are sent according to the configuration. * @param problemProperty * @param QueryParams * @returns DetailedProblem[] */ async problemsOfProperty(problemProperty, { category = "", offset = 0, limit = 1e5, filters = {} } = {}) { if (!problemProperty.enable) { throw new Error(`${problemProperty.title} is not enabled.`); } await this.initialized; const variables = { categorySlug: category, skip: offset, limit, filters }; let problems = []; if (!problemProperty.needRequestChunking) { const chunkSize = 100; const noOfProblems = Math.min(offset + limit, await this.noOfProblems()); variables.limit = Math.min(chunkSize, limit); while (variables.skip < noOfProblems) { const { data } = await this.graphql({ variables, query: this.getProblemsQuery({ requiredProperty: problemProperty.graphql }) }); problems = [...problems, ...data.problemsetQuestionList.questions]; variables.skip += variables.limit; variables.limit = Math.min(chunkSize, noOfProblems - variables.skip); } } else { variables.limit = problemProperty.problemsPerRequest; const noOfProblems = await this.noOfProblems(); while (variables.skip < noOfProblems) { const { data } = await this.graphql({ variables, query: this.getProblemsQuery({ requiredProperty: problemProperty.graphql }) }); problems = [...problems, ...data.problemsetQuestionList.questions]; variables.skip += variables.limit; } } if (problemProperty.needParsing) { const property = problemProperty.property; problems.forEach((problem) => { problem[property] = JSON.parse(problem[property]); }); } return problems; } /** * Get title slug question number mapping for all leetcode questions * @returns Mapping */ async getTitleSlugQuestionNumberMapping() { const mapping = {}; for (const category of PROBLEM_CATEGORIES) { const pairs = a