@leetnotion/leetcode-api
Version:
Get user profiles, submissions, and problems on LeetCode.
1,624 lines (1,592 loc) • 67.4 kB
JavaScript
// 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