@leetnotion/leetcode-api
Version:
Get user profiles, submissions, and problems on LeetCode.
1,691 lines (1,660 loc) • 50.9 kB
JavaScript
// 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";
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/utils.ts
function parse_cookie(cookie) {
return cookie.split(";").map((x) => x.trim().split("=")).reduce(
(acc, x) => {
acc[x[0]] = x[1];
return acc;
},
{}
);
}
// src/credential.ts
async function get_csrf() {
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;
}
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/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/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
*
*/
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_user_submissions("jacoblincool");
* ```
*/
async recent_user_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
} = {}) {
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;
}
async submissionsApi({ offset = 0, limit = 20 }) {
await this.initialized;
if (limit > 20) limit = 20;
try {
await this.limiter.lock();
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);
}
}
this.limiter.unlock();
return await res.json();
} catch (err) {
this.limiter.unlock();
throw err;
}
}
/**
* 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;
try {
await this.limiter.lock();
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];
const json = new Function("return " + data)();
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
);
this.limiter.unlock();
return result;
} catch (err) {
this.limiter.unlock();
throw err;
}
}
/**
* 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
* @returns
*/
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({
operationName: "globalData",
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/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("jacoblincool");
* ```
*/
async user(username) {
await this.initialized;
const { data } = await this.graphql({
operationName: "getUserProfile",
variables: { username },
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(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/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/question-frontend-ids.graphql?raw
var question_frontend_ids_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 }\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/title-slug-question-number-mapping.graphql?raw
var title_slug_question_number_mapping_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 titleSlug\n }\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: "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: "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: "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() {
await this.initialized;
const { data } = await this.graphql({
query: topic_tags_default,
variables: {
categorySlug: "",
filters: {},
skip: 0,
limit: 1e6
}
});
const problems = data.problemsetQuestionList.questions;
const questionIdToTopicTags = {};
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;
}
async getProblemTypes() {
const problemTypes = {};
for (const category of PROBLEM_CATEGORIES) {
const { data } = await this.graphql({
query: question_frontend_ids_default,
variables: {
categorySlug: category,
filters: {},
skip: 0,
limit: 1e5
}
});
const questions = data.problemsetQuestionList.questions;
for (const question of questions) {
const id = question.questionFrontendId;
if (!problemTypes[id]) {
problemTypes[id] = [];
}
problemTypes[id] = [...problemTypes[id], category];
}
}
return problemTypes;
}
async getLeetcodeProblems(limit = 500, callbackFn = null) {
await this.initialized;
const noOfProblems = await this.noOfProblems();
let problems = [];
for (let skip = 0; skip < noOfProblems; skip += limit) {
const { data } = await this.graphql({
query: custom_problem_default,
variables: {
categorySlug: "",
filters: {},
skip,
limit
}
});
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 { data } = await this.graphql({
variables,
query: this.getProblemsQuery({ requiredProperty: problemProperty.graphql })
});
problems = data.problemsetQuestionList.questions;
} 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() {
await this.initialized;
const { data } = await this.graphql({
query: title_slug_question_number_mapping_default,
variables: {
categorySlug: "",
filters: {},
skip: 0,
limit: 1e5
}
});
const problems = data.problemsetQuestionList.questions;
const mapping = {};
problems.forEach((problem) => {
mapping[problem.titleSlug] = problem.questionFrontendId;
});
return mapping;
}
combineProperties(arr1, arr2) {
const uniquePropertyOfProblem = this.uniquePropertyOfProblem;
const arr1Map = arr1.reduce(
(map, item) => {
const uniqueValue = item[uniquePropertyOfProblem];
map[uniqueValue] = item;
return map;
},
{}
);
return arr2.map((item) => ({
...item,
...arr1Map[item[uniquePropertyOfProblem]]
}));
}
getProblemsQuery({
uniqueProperty = this.uniqueProper