diff --git a/.gitignore b/.gitignore index a833ee3d..04dcb7d4 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ jspm_packages tmp/ *.swp .DS_Store +package-lock.json +.vscode/ \ No newline at end of file diff --git a/lib/commands/show.js b/lib/commands/show.js index d930740f..27d65326 100644 --- a/lib/commands/show.js +++ b/lib/commands/show.js @@ -81,17 +81,21 @@ function genFileName(problem, opts) { h.langToExt(opts.lang) ]; + params[0] = ("00" + params[0]).substr(-3) + "-"; // try to use a new filename to avoid overwrite by mistake let i = 0; - let name; - do { - name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.')); - params[2] = i++; - } while (fs.existsSync(name)); + // 转为 001-two-sum.cpp 的文件格式 + let name = path.join(opts.outdir, params.join('').replace(/\.+/g, '.')); + // do { + // name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.')); + // params[2] = i++; + // } while (fs.existsSync(name)); return name; } function showProblem(problem, argv) { + problem.discuss = problem.link.replace("leetcode-cn.com", "leetcode.com").replace("description", "discuss"); + const taglist = [problem.category] .concat(problem.companies || []) .concat(problem.tags || []) @@ -105,11 +109,48 @@ function showProblem(problem, argv) { let code; const needcode = argv.gen || argv.codeonly; if (needcode) { - const template = problem.templates.find(x => x.value === argv.lang); + var template = problem.templates.find(x => x.value === argv.lang); if (!template) { log.fail('Not supported language "' + argv.lang + '"'); log.warn('Supported languages: ' + langlist); return; + } else { + if(argv.lang === 'cpp'){ + var include = ` +#include +#include +using namespace std; + +`; + + template.defaultCode = include + template.defaultCode; + + var main = ` + +int main() +{ + Solution s; + + return 0; +} + ` + template.defaultCode += main; + } else if(argv.lang === 'golang') { + var include = ` +package main + +`; + + template.defaultCode = include + template.defaultCode; + + var main = ` + +func main() { + +} + ` + template.defaultCode += main; + } } const opts = { @@ -124,6 +165,7 @@ function showProblem(problem, argv) { if (argv.gen) { filename = genFileName(problem, argv); h.mkdir(argv.outdir); + code = code.replace(/ \* \r\n/g, ''); fs.writeFileSync(filename, code); if (argv.editor !== undefined) { diff --git a/lib/commands/submit.js b/lib/commands/submit.js index 092f0a53..bc21103d 100644 --- a/lib/commands/submit.js +++ b/lib/commands/submit.js @@ -48,7 +48,10 @@ cmd.handler = function(argv) { // use the 1st section in filename as keyword // e.g. two-sum.cpp, or two-sum.78502271.ac.cpp - const keyword = h.getFilename(argv.filename).split('.')[0]; + // const keyword = h.getFilename(argv.filename).split('.')[0]; + + // 我改为了 001-two-sum.cpp 命名方式,所以用fid去查吧。 + const keyword = parseInt(h.getFilename(argv.filename).split('-')[0]); core.getProblem(keyword, function(e, problem) { if (e) return log.fail(e); diff --git a/lib/config.js b/lib/config.js index f3e40272..8c9e796f 100644 --- a/lib/config.js +++ b/lib/config.js @@ -29,19 +29,19 @@ const DEFAULT_CONFIG = { 'swift' ], urls: { - base: 'https://leetcode.com', - login: 'https://leetcode.com/accounts/login/', - problems: 'https://leetcode.com/api/problems/$category/', - problem: 'https://leetcode.com/problems/$slug/description/', - problem_detail: 'https://leetcode.com/graphql', - test: 'https://leetcode.com/problems/$slug/interpret_solution/', - session: 'https://leetcode.com/session/', - submit: 'https://leetcode.com/problems/$slug/submit/', - submissions: 'https://leetcode.com/api/submissions/$slug', - submission: 'https://leetcode.com/submissions/detail/$id/', - verify: 'https://leetcode.com/submissions/detail/$id/check/', - favorites: 'https://leetcode.com/list/api/questions', - favorite_delete: 'https://leetcode.com/list/api/questions/$hash/$id', + base: 'https://leetcode-cn.com', + login: 'https://leetcode-cn.com/accounts/login/', + problems: 'https://leetcode-cn.com/api/problems/$category/', + problem: 'https://leetcode-cn.com/problems/$slug/description/', + problem_detail: 'https://leetcode-cn.com/graphql', + test: 'https://leetcode-cn.com/problems/$slug/interpret_solution/', + session: 'https://leetcode-cn.com/session/', + submit: 'https://leetcode-cn.com/problems/$slug/submit/', + submissions: 'https://leetcode-cn.com/api/submissions/$slug', + submission: 'https://leetcode-cn.com/submissions/detail/$id/', + verify: 'https://leetcode-cn.com/submissions/detail/$id/check/', + favorites: 'https://leetcode-cn.com/list/api/questions', + favorite_delete: 'https://leetcode-cn.com/list/api/questions/$hash/$id', plugin: 'https://github.com/skygragon/leetcode-cli-plugins/raw/master/plugins/$name.js' } }, diff --git a/lib/core.js b/lib/core.js index a1356a62..6ed1111f 100644 --- a/lib/core.js +++ b/lib/core.js @@ -125,11 +125,12 @@ core.exportProblem = function(problem, opts) { const tplfile = path.join(h.getCodeDir('templates'), opts.tpl + '.tpl'); let output = _.template(h.getFileData(tplfile))(input); - if (h.isWindows()) { - output = output.replace(/\n/g, '\r\n'); - } else { - output = output.replace(/\r\n/g, '\n'); - } + // if (h.isWindows()) { + // output = output.replace(/\n/g, '\r\n'); + // } else { + // output = output.replace(/\r\n/g, '\n'); + // } + return output; }; diff --git a/lib/plugins/leetcode.cn.js b/lib/plugins/leetcode.cn.js new file mode 100644 index 00000000..07d5d2d7 --- /dev/null +++ b/lib/plugins/leetcode.cn.js @@ -0,0 +1,126 @@ +'use strict' +var request = require('request'); + +var config = require('../config'); +var h = require('../helper'); +var log = require('../log'); +var Plugin = require('../plugin'); +var session = require('../session'); + +// +// [Usage] +// +// https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/leetcode.cn.md +// +var plugin = new Plugin(15, 'leetcode.cn', '2018.11.25', + 'Plugin to talk with leetcode-cn APIs.'); + +plugin.init = function() { + config.app = 'leetcode.cn'; + config.sys.urls.base = 'https://leetcode-cn.com'; + config.sys.urls.login = 'https://leetcode-cn.com/accounts/login/'; + config.sys.urls.problems = 'https://leetcode-cn.com/api/problems/$category/'; + config.sys.urls.problem = 'https://leetcode-cn.com/problems/$slug/description/'; + config.sys.urls.graphql = 'https://leetcode-cn.com/graphql'; + config.sys.urls.problem_detail = 'https://leetcode-cn.com/graphql'; + config.sys.urls.test = 'https://leetcode-cn.com/problems/$slug/interpret_solution/'; + config.sys.urls.session = 'https://leetcode-cn.com/session/'; + config.sys.urls.submit = 'https://leetcode-cn.com/problems/$slug/submit/'; + config.sys.urls.submissions = 'https://leetcode-cn.com/api/submissions/$slug'; + config.sys.urls.submission = 'https://leetcode-cn.com/submissions/detail/$id/'; + config.sys.urls.verify = 'https://leetcode-cn.com/submissions/detail/$id/check/'; + config.sys.urls.favorites = 'https://leetcode-cn.com/list/api/questions'; + config.sys.urls.favorite_delete = 'https://leetcode-cn.com/list/api/questions/$hash/$id'; +}; + +// FIXME: refactor those +// update options with user credentials +function signOpts(opts, user) { + opts.headers.Cookie = 'LEETCODE_SESSION=' + user.sessionId + + ';csrftoken=' + user.sessionCSRF + ';'; + opts.headers['X-CSRFToken'] = user.sessionCSRF; + opts.headers['X-Requested-With'] = 'XMLHttpRequest'; +} + +function makeOpts(url) { + const opts = {}; + opts.url = url; + opts.headers = {}; + + if (session.isLogin()) + signOpts(opts, session.getUser()); + return opts; +} + +function checkError(e, resp, expectedStatus) { + if (!e && resp && resp.statusCode !== expectedStatus) { + const code = resp.statusCode; + log.debug('http error: ' + code); + + if (code === 403 || code === 401) { + e = session.errors.EXPIRED; + } else { + e = {msg: 'http error', statusCode: code}; + } + } + return e; +} + +plugin.getProblems = function(cb) { + plugin.next.getProblems(function(e, problems) { + if (e) return cb(e); + + plugin.getProblemsTitle(function(e, titles) { + if (e) return cb(e); + + problems.forEach(function(problem) { + const title = titles[problem.fid]; + if (title) + problem.name = title; + }); + + return cb(null, problems); + }); + }); +}; + +plugin.getProblemsTitle = function(cb) { + log.debug('running leetcode.cn.getProblemNames'); + + const opts = makeOpts(config.sys.urls.graphql); + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = 'https://leetcode-cn.com/api/problems/algorithms/'; + + opts.json = true; + opts.body = { + query: [ + 'query getQuestionTranslation($lang: String) {', + ' translations: allAppliedQuestionTranslations(lang: $lang) {', + ' title', + ' question {', + ' questionId', + ' }', + ' }', + '}', + '' + ].join('\n'), + variables: {}, + operationName: 'getQuestionTranslation' + }; + + const spin = h.spin('Downloading questions titles'); + request.post(opts, function(e, resp, body) { + spin.stop(); + e = checkError(e, resp, 200); + if (e) return cb(e); + + const titles = []; + body.data.translations.forEach(function(x) { + titles[x.question.questionId] = x.title; + }); + + return cb(null, titles); + }); +}; + +module.exports = plugin; diff --git a/lib/plugins/leetcode.js b/lib/plugins/leetcode.js index 07e251e7..01c4dd53 100644 --- a/lib/plugins/leetcode.js +++ b/lib/plugins/leetcode.js @@ -1,476 +1,489 @@ -'use strict'; -var util = require('util'); - -var _ = require('underscore'); -var cheerio = require('cheerio'); -var he = require('he'); -var request = require('request'); - -var config = require('../config'); -var h = require('../helper'); -var log = require('../log'); -var Plugin = require('../plugin'); -var Queue = require('../queue'); -var session = require('../session'); - -const plugin = new Plugin(10, 'leetcode', '', - 'Plugin to talk with leetcode APIs.'); - -var spin; - -// update options with user credentials -function signOpts(opts, user) { - opts.headers.Cookie = 'LEETCODE_SESSION=' + user.sessionId + - ';csrftoken=' + user.sessionCSRF + ';'; - opts.headers['X-CSRFToken'] = user.sessionCSRF; - opts.headers['X-Requested-With'] = 'XMLHttpRequest'; -} - -function makeOpts(url) { - const opts = {}; - opts.url = url; - opts.headers = {}; - - if (session.isLogin()) - signOpts(opts, session.getUser()); - return opts; -} - -function checkError(e, resp, expectedStatus) { - if (!e && resp && resp.statusCode !== expectedStatus) { - const code = resp.statusCode; - log.debug('http error: ' + code); - - if (code === 403 || code === 401) { - e = session.errors.EXPIRED; - } else { - e = {msg: 'http error', statusCode: code}; - } - } - return e; -} - -plugin.getProblems = function(cb) { - log.debug('running leetcode.getProblems'); - let problems = []; - const getCategory = function(category, queue, cb) { - plugin.getCategoryProblems(category, function(e, _problems) { - if (e) { - log.debug(category + ': failed to getProblems: ' + e.msg); - } else { - log.debug(category + ': getProblems got ' + _problems.length + ' problems'); - problems = problems.concat(_problems); - } - return cb(e); - }); - }; - - spin = h.spin('Downloading problems'); - const q = new Queue(config.sys.categories, {}, getCategory); - q.run(null, function(e) { - spin.stop(); - return cb(e, problems); - }); -}; - -plugin.getCategoryProblems = function(category, cb) { - log.debug('running leetcode.getCategoryProblems: ' + category); - const opts = makeOpts(config.sys.urls.problems.replace('$category', category)); - - spin.text = 'Downloading category ' + category; - request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - const json = JSON.parse(body); - - // leetcode permits anonymous access to the problem list - // while we require login first to make a better experience. - if (json.user_name.length === 0) { - log.debug('no user info in list response, maybe session expired...'); - return cb(session.errors.EXPIRED); - } - - const user = session.getUser(); - user.paid = json.is_paid; - session.saveUser(user); - - const problems = json.stat_status_pairs - .filter(p => !p.stat.question__hide) - .map(function(p) { - return { - state: p.status || 'None', - id: p.stat.question_id, - fid: p.stat.frontend_question_id, - name: p.stat.question__title, - slug: p.stat.question__title_slug, - link: config.sys.urls.problem.replace('$slug', p.stat.question__title_slug), - locked: p.paid_only, - percent: p.stat.total_acs * 100 / p.stat.total_submitted, - level: h.levelToName(p.difficulty.level), - starred: p.is_favor, - category: json.category_slug - }; - }); - - return cb(null, problems); - }); -}; - -plugin.getProblem = function(problem, cb) { - log.debug('running leetcode.getProblem'); - const user = session.getUser(); - if (problem.locked && !user.paid) return cb('failed to load locked problem!'); - - const opts = makeOpts(config.sys.urls.problem_detail); - opts.headers.Origin = config.sys.urls.base; - opts.headers.Referer = problem.link; - - opts.json = true; - opts.body = { - query: [ - 'query getQuestionDetail($titleSlug: String!) {', - ' question(titleSlug: $titleSlug) {', - ' content', - ' stats', - ' codeDefinition', - ' sampleTestCase', - ' enableRunCode', - ' metaData', - ' discussCategoryId', - ' }', - '}' - ].join('\n'), - variables: {titleSlug: problem.slug}, - operationName: 'getQuestionDetail' - }; - - const spin = h.spin('Downloading ' + problem.slug); - request.post(opts, function(e, resp, body) { - spin.stop(); - e = checkError(e, resp, 200); - if (e) return cb(e); - - const q = body.data.question; - if (!q) return cb('failed to load problem!'); - - problem.totalAC = JSON.parse(q.stats).totalAccepted; - problem.totalSubmit = JSON.parse(q.stats).totalSubmission; - problem.desc = he.decode(cheerio.load(q.content).root().text()); - problem.templates = JSON.parse(q.codeDefinition); - problem.testcase = q.sampleTestCase; - problem.testable = q.enableRunCode; - problem.templateMeta = JSON.parse(q.metaData); - problem.discuss = q.discussCategoryId; - - return cb(null, problem); - }); -}; - -function runCode(opts, problem, cb) { - opts.method = 'POST'; - opts.headers.Origin = config.sys.urls.base; - opts.headers.Referer = problem.link; - opts.json = true; - opts._delay = opts._delay || 1; // in seconds - - opts.body = opts.body || {}; - _.extendOwn(opts.body, { - lang: h.extToLang(problem.file), - question_id: parseInt(problem.id, 10), - test_mode: false, - typed_code: h.getFileData(problem.file) - }); - - const spin = h.spin('Sending code to judge'); - request(opts, function(e, resp, body) { - spin.stop(); - e = checkError(e, resp, 200); - if (e) return cb(e); - - if (body.error) { - if (!body.error.includes('too soon')) - return cb(body.error); - - // hit 'run code too soon' error, have to wait a bit - log.debug(body.error); - - // linear wait - ++opts._delay; - log.debug('Will retry after %d seconds...', opts._delay); - - const reRun = _.partial(runCode, opts, problem, cb); - return setTimeout(reRun, opts._delay * 1000); - } - - opts.json = false; - opts.body = null; - - return cb(null, body); - }); -} - -function verifyResult(task, queue, cb) { - const opts = queue.ctx.opts; - opts.method = 'GET'; - opts.url = config.sys.urls.verify.replace('$id', task.id); - - const spin = h.spin('Waiting for judge result'); - request(opts, function(e, resp, body) { - spin.stop(); - e = checkError(e, resp, 200); - if (e) return cb(e); - - let result = JSON.parse(body); - if (result.state === 'SUCCESS') { - result = formatResult(result); - _.extendOwn(result, task); - queue.ctx.results.push(result); - } else { - queue.addTask(task); - } - return cb(); - }); -} - -function formatResult(result) { - const x = { - ok: result.run_success, - answer: result.code_answer || '', - runtime: result.status_runtime || '', - state: h.statusToName(result.status_code), - testcase: util.inspect(result.input || result.last_testcase || ''), - passed: result.total_correct || 0, - total: result.total_testcases || 0 - }; - - x.error = _.chain(result) - .pick((v, k) => /_error$/.test(k) && v.length > 0) - .values() - .value(); - - if (result.judge_type === 'large') { - x.answer = result.code_output; - x.expected_answer = result.expected_output; - } else { - x.stdout = util.inspect((result.code_output || []).join('\n')); - } - - // make sure we pass eveything! - if (x.passed !== x.total) x.ok = false; - if (x.state !== 'Accepted') x.ok = false; - if (x.error.length > 0) x.ok = false; - - return x; -} - -plugin.testProblem = function(problem, cb) { - log.debug('running leetcode.testProblem'); - const opts = makeOpts(config.sys.urls.test.replace('$slug', problem.slug)); - opts.body = {data_input: problem.testcase}; - - runCode(opts, problem, function(e, task) { - if (e) return cb(e); - - const tasks = [ - {type: 'Actual', id: task.interpret_id}, - {type: 'Expected', id: task.interpret_expected_id} - ]; - const q = new Queue(tasks, {opts: opts, results: []}, verifyResult); - q.run(null, function(e, ctx) { - return cb(e, ctx.results); - }); - }); -}; - -plugin.submitProblem = function(problem, cb) { - log.debug('running leetcode.submitProblem'); - const opts = makeOpts(config.sys.urls.submit.replace('$slug', problem.slug)); - opts.body = {judge_type: 'large'}; - - runCode(opts, problem, function(e, task) { - if (e) return cb(e); - - const tasks = [{type: 'Actual', id: task.submission_id}]; - const q = new Queue(tasks, {opts: opts, results: []}, verifyResult); - q.run(null, function(e, ctx) { - return cb(e, ctx.results); - }); - }); -}; - -plugin.getSubmissions = function(problem, cb) { - log.debug('running leetcode.getSubmissions'); - const opts = makeOpts(config.sys.urls.submissions.replace('$slug', problem.slug)); - opts.headers.Referer = config.sys.urls.problem.replace('$slug', problem.slug); - - request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - // FIXME: this only return the 1st 20 submissions, we should get next if necessary. - const submissions = JSON.parse(body).submissions_dump; - for (let submission of submissions) - submission.id = _.last(_.compact(submission.url.split('/'))); - - return cb(null, submissions); - }); -}; - -plugin.getSubmission = function(submission, cb) { - log.debug('running leetcode.getSubmission'); - const opts = makeOpts(config.sys.urls.submission.replace('$id', submission.id)); - - request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - let re = body.match(/submissionCode:\s('[^']*')/); - if (re) submission.code = eval(re[1]); - - re = body.match(/distribution_formatted:\s('[^']+')/); - if (re) submission.distributionChart = JSON.parse(eval(re[1])); - return cb(null, submission); - }); -}; - -plugin.starProblem = function(problem, starred, cb) { - log.debug('running leetcode.starProblem'); - const opts = makeOpts(); - opts.headers.Origin = config.sys.urls.base; - opts.headers.Referer = problem.link; - - const user = session.getUser(); - if (starred) { - opts.url = config.sys.urls.favorites; - opts.method = 'POST'; - opts.json = true; - opts.body = { - favorite_id_hash: user.hash, - question_id: problem.id - }; - } else { - opts.url = config.sys.urls.favorite_delete - .replace('$hash', user.hash) - .replace('$id', problem.id); - opts.method = 'DELETE'; - } - - request(opts, function(e, resp, body) { - e = checkError(e, resp, 204); - if (e) return cb(e); - - cb(null, starred); - }); -}; - -plugin.getFavorites = function(cb) { - log.debug('running leetcode.getFavorites'); - const opts = makeOpts(config.sys.urls.favorites); - - request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - const favorites = JSON.parse(body); - return cb(null, favorites); - }); -}; - -function runSession(method, data, cb) { - const opts = makeOpts(config.sys.urls.session); - opts.json = true; - opts.method = method; - opts.body = data; - - const spin = h.spin('Waiting session result'); - request(opts, function(e, resp, body) { - spin.stop(); - e = checkError(e, resp, 200); - if (e && e.statusCode === 302) e = session.errors.EXPIRED; - - return e ? cb(e) : cb(null, body.sessions); - }); -} - -plugin.getSessions = function(cb) { - log.debug('running leetcode.getSessions'); - runSession('POST', {}, cb); -}; - -plugin.activateSession = function(session, cb) { - log.debug('running leetcode.activateSession'); - const data = {func: 'activate', target: session.id}; - runSession('PUT', data, cb); -}; - -plugin.createSession = function(name, cb) { - log.debug('running leetcode.createSession'); - const data = {func: 'create', name: name}; - runSession('PUT', data, cb); -}; - -plugin.deleteSession = function(session, cb) { - log.debug('running leetcode.deleteSession'); - const data = {target: session.id}; - runSession('DELETE', data, cb); -}; - -plugin.signin = function(user, cb) { - log.debug('running leetcode.signin'); - const spin = h.spin('Signing in leetcode.com'); - request(config.sys.urls.login, function(e, resp, body) { - spin.stop(); - e = checkError(e, resp, 200); - if (e) return cb(e); - - user.loginCSRF = h.getSetCookieValue(resp, 'csrftoken'); - - const opts = { - url: config.sys.urls.login, - headers: { - Origin: config.sys.urls.base, - Referer: config.sys.urls.login, - Cookie: 'csrftoken=' + user.loginCSRF + ';' - }, - form: { - csrfmiddlewaretoken: user.loginCSRF, - login: user.login, - password: user.pass - } - }; - request.post(opts, function(e, resp, body) { - if (e) return cb(e); - if (resp.statusCode !== 302) return cb('invalid password?'); - - user.sessionCSRF = h.getSetCookieValue(resp, 'csrftoken'); - user.sessionId = h.getSetCookieValue(resp, 'LEETCODE_SESSION'); - session.saveUser(user); - return cb(null, user); - }); - }); -}; - -plugin.getUser = function(user, cb) { - plugin.getFavorites(function(e, favorites) { - if (e) return cb(e); - - const favorite = favorites.favorites.private_favorites.find(function(f) { - return f.name === 'Favorite'; - }); - user.hash = favorite.id_hash; - user.name = favorites.user_name; - session.saveUser(user); - return cb(null, user); - }); -}; - -plugin.login = function(user, cb) { - log.debug('running leetcode.login'); - plugin.signin(user, function(e, user) { - if (e) return cb(e); - plugin.getUser(user, cb); - }); -}; - -module.exports = plugin; +'use strict'; +var util = require('util'); + +var _ = require('underscore'); +var cheerio = require('cheerio'); +var he = require('he'); +var request = require('request'); + +var config = require('../config'); +var h = require('../helper'); +var log = require('../log'); +var Plugin = require('../plugin'); +var Queue = require('../queue'); +var session = require('../session'); + +const plugin = new Plugin(10, 'leetcode', '', + 'Plugin to talk with leetcode APIs.'); + +var spin; + +// update options with user credentials +function signOpts(opts, user) { + opts.headers.Cookie = 'LEETCODE_SESSION=' + user.sessionId + + ';csrftoken=' + user.sessionCSRF + ';'; + opts.headers['X-CSRFToken'] = user.sessionCSRF; + opts.headers['X-Requested-With'] = 'XMLHttpRequest'; +} + +function makeOpts(url) { + const opts = {}; + opts.url = url; + opts.headers = {}; + + if (session.isLogin()) + signOpts(opts, session.getUser()); + return opts; +} + +function checkError(e, resp, expectedStatus) { + if (!e && resp && resp.statusCode !== expectedStatus) { + const code = resp.statusCode; + log.debug('http error: ' + code); + + if (code === 403 || code === 401) { + e = session.errors.EXPIRED; + } else { + e = {msg: 'http error', statusCode: code}; + } + } + return e; +} + +plugin.getProblems = function(cb) { + log.debug('running leetcode.getProblems'); + let problems = []; + const getCategory = function(category, queue, cb) { + plugin.getCategoryProblems(category, function(e, _problems) { + if (e) { + log.debug(category + ': failed to getProblems: ' + e.msg); + } else { + log.debug(category + ': getProblems got ' + _problems.length + ' problems'); + problems = problems.concat(_problems); + } + return cb(e); + }); + }; + + spin = h.spin('Downloading problems'); + const q = new Queue(config.sys.categories, {}, getCategory); + q.run(null, function(e) { + spin.stop(); + return cb(e, problems); + }); +}; + +plugin.getCategoryProblems = function(category, cb) { + log.debug('running leetcode.getCategoryProblems: ' + category); + const opts = makeOpts(config.sys.urls.problems.replace('$category', category)); + + spin.text = 'Downloading category ' + category; + request(opts, function(e, resp, body) { + e = checkError(e, resp, 200); + if (e) return cb(e); + + const json = JSON.parse(body); + + // leetcode permits anonymous access to the problem list + // while we require login first to make a better experience. + if (json.user_name.length === 0) { + log.debug('no user info in list response, maybe session expired...'); + return cb(session.errors.EXPIRED); + } + + const user = session.getUser(); + user.paid = json.is_paid; + session.saveUser(user); + + const problems = json.stat_status_pairs + .filter(p => !p.stat.question__hide) + .map(function(p) { + return { + state: p.status || 'None', + id: p.stat.question_id, + fid: p.stat.frontend_question_id, + name: p.stat.question__title, + slug: p.stat.question__title_slug, + link: config.sys.urls.problem.replace('$slug', p.stat.question__title_slug), + locked: p.paid_only, + percent: p.stat.total_acs * 100 / p.stat.total_submitted, + level: h.levelToName(p.difficulty.level), + starred: p.is_favor, + category: json.category_slug + }; + }); + + return cb(null, problems); + }); +}; + +plugin.getProblem = function(problem, cb) { + log.debug('running leetcode.getProblem'); + const user = session.getUser(); + if (problem.locked && !user.paid) return cb('failed to load locked problem!'); + + const opts = makeOpts(config.sys.urls.problem_detail); + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = problem.link; + + opts.json = true; + opts.body = { + query: [ + 'query getQuestionDetail($titleSlug: String!) {', + ' question(titleSlug: $titleSlug) {', + ' content', + ' translatedContent', + ' stats', + ' codeDefinition', + ' sampleTestCase', + ' enableRunCode', + ' metaData', + ' }', + '}' + ].join('\n'), + variables: {titleSlug: problem.slug}, + operationName: 'getQuestionDetail' + }; + + const spin = h.spin('Downloading ' + problem.slug); + request.post(opts, function(e, resp, body) { + spin.stop(); + e = checkError(e, resp, 200); + if (e) return cb(e); + + const q = body.data.question; + if (!q) return cb('failed to load problem!'); + + problem.totalAC = JSON.parse(q.stats).totalAccepted; + problem.totalSubmit = JSON.parse(q.stats).totalSubmission; + problem.desc = he.decode(cheerio.load(q.translatedContent).root().text()); + problem.templates = JSON.parse(q.codeDefinition); + problem.testcase = q.sampleTestCase; + problem.testable = q.enableRunCode; + problem.templateMeta = JSON.parse(q.metaData); + + return cb(null, problem); + }); +}; + +function runCode(opts, problem, cb) { + opts.method = 'POST'; + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = problem.link; + opts.json = true; + opts._delay = opts._delay || 1; // in seconds + + opts.body = opts.body || {}; + _.extendOwn(opts.body, { + lang: h.extToLang(problem.file), + question_id: parseInt(problem.id, 10), + test_mode: false, + typed_code: h.getFileData(problem.file) + }); + + if(opts.body.lang === 'cpp') { + var start = opts.body.typed_code.indexOf('class Solution'); + var end = opts.body.typed_code.indexOf('int main()'); + if(start > 0 && end > 0) { + opts.body.typed_code = opts.body.typed_code.substring(start, end); + } + } else if(opts.body.lang === 'golang') { + var start = opts.body.typed_code.indexOf('func'); + var end = opts.body.typed_code.indexOf('func main'); + if(start > 0 && end > 0) { + opts.body.typed_code = opts.body.typed_code.substring(start, end); + } + } + + const spin = h.spin('Sending code to judge'); + request(opts, function(e, resp, body) { + spin.stop(); + e = checkError(e, resp, 200); + if (e) return cb(e); + + if (body.error) { + if (!body.error.includes('too soon')) + return cb(body.error); + + // hit 'run code too soon' error, have to wait a bit + log.debug(body.error); + + // linear wait + ++opts._delay; + log.debug('Will retry after %d seconds...', opts._delay); + + const reRun = _.partial(runCode, opts, problem, cb); + return setTimeout(reRun, opts._delay * 1000); + } + + opts.json = false; + opts.body = null; + + return cb(null, body); + }); +} + +function verifyResult(task, queue, cb) { + const opts = queue.ctx.opts; + opts.method = 'GET'; + opts.url = config.sys.urls.verify.replace('$id', task.id); + + const spin = h.spin('Waiting for judge result'); + request(opts, function(e, resp, body) { + spin.stop(); + e = checkError(e, resp, 200); + if (e) return cb(e); + + let result = JSON.parse(body); + if (result.state === 'SUCCESS') { + result = formatResult(result); + _.extendOwn(result, task); + queue.ctx.results.push(result); + } else { + queue.addTask(task); + } + return cb(); + }); +} + +function formatResult(result) { + const x = { + ok: result.run_success, + answer: result.code_answer || '', + runtime: result.status_runtime || '', + state: h.statusToName(result.status_code), + testcase: util.inspect(result.input || result.last_testcase || ''), + passed: result.total_correct || 0, + total: result.total_testcases || 0 + }; + + x.error = _.chain(result) + .pick((v, k) => /_error$/.test(k) && v.length > 0) + .values() + .value(); + + if (result.judge_type === 'large') { + x.answer = result.code_output; + x.expected_answer = result.expected_output; + } else { + x.stdout = util.inspect((result.code_output || []).join('\n')); + } + + // make sure we pass eveything! + if (x.passed !== x.total) x.ok = false; + if (x.state !== 'Accepted') x.ok = false; + if (x.error.length > 0) x.ok = false; + + return x; +} + +plugin.testProblem = function(problem, cb) { + log.debug('running leetcode.testProblem'); + const opts = makeOpts(config.sys.urls.test.replace('$slug', problem.slug)); + opts.body = {data_input: problem.testcase}; + + runCode(opts, problem, function(e, task) { + if (e) return cb(e); + + const tasks = [ + {type: 'Actual', id: task.interpret_id}, + {type: 'Expected', id: task.interpret_expected_id} + ]; + const q = new Queue(tasks, {opts: opts, results: []}, verifyResult); + q.run(null, function(e, ctx) { + return cb(e, ctx.results); + }); + }); +}; + +plugin.submitProblem = function(problem, cb) { + log.debug('running leetcode.submitProblem'); + const opts = makeOpts(config.sys.urls.submit.replace('$slug', problem.slug)); + opts.body = {judge_type: 'large'}; + + runCode(opts, problem, function(e, task) { + if (e) return cb(e); + + const tasks = [{type: 'Actual', id: task.submission_id}]; + const q = new Queue(tasks, {opts: opts, results: []}, verifyResult); + q.run(null, function(e, ctx) { + return cb(e, ctx.results); + }); + }); +}; + +plugin.getSubmissions = function(problem, cb) { + log.debug('running leetcode.getSubmissions'); + const opts = makeOpts(config.sys.urls.submissions.replace('$slug', problem.slug)); + opts.headers.Referer = config.sys.urls.problem.replace('$slug', problem.slug); + + request(opts, function(e, resp, body) { + e = checkError(e, resp, 200); + if (e) return cb(e); + + // FIXME: this only return the 1st 20 submissions, we should get next if necessary. + const submissions = JSON.parse(body).submissions_dump; + for (let submission of submissions) + submission.id = _.last(_.compact(submission.url.split('/'))); + + return cb(null, submissions); + }); +}; + +plugin.getSubmission = function(submission, cb) { + log.debug('running leetcode.getSubmission'); + const opts = makeOpts(config.sys.urls.submission.replace('$id', submission.id)); + + request(opts, function(e, resp, body) { + e = checkError(e, resp, 200); + if (e) return cb(e); + + let re = body.match(/submissionCode:\s('[^']*')/); + if (re) submission.code = eval(re[1]); + + re = body.match(/distribution_formatted:\s('[^']+')/); + if (re) submission.distributionChart = JSON.parse(eval(re[1])); + return cb(null, submission); + }); +}; + +plugin.starProblem = function(problem, starred, cb) { + log.debug('running leetcode.starProblem'); + const opts = makeOpts(); + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = problem.link; + + const user = session.getUser(); + if (starred) { + opts.url = config.sys.urls.favorites; + opts.method = 'POST'; + opts.json = true; + opts.body = { + favorite_id_hash: user.hash, + question_id: problem.id + }; + } else { + opts.url = config.sys.urls.favorite_delete + .replace('$hash', user.hash) + .replace('$id', problem.id); + opts.method = 'DELETE'; + } + + request(opts, function(e, resp, body) { + e = checkError(e, resp, 204); + if (e) return cb(e); + + cb(null, starred); + }); +}; + +plugin.getFavorites = function(cb) { + log.debug('running leetcode.getFavorites'); + const opts = makeOpts(config.sys.urls.favorites); + + request(opts, function(e, resp, body) { + e = checkError(e, resp, 200); + if (e) return cb(e); + + const favorites = JSON.parse(body); + return cb(null, favorites); + }); +}; + +function runSession(method, data, cb) { + const opts = makeOpts(config.sys.urls.session); + opts.json = true; + opts.method = method; + opts.body = data; + + const spin = h.spin('Waiting session result'); + request(opts, function(e, resp, body) { + spin.stop(); + e = checkError(e, resp, 200); + if (e && e.statusCode === 302) e = session.errors.EXPIRED; + + return e ? cb(e) : cb(null, body.sessions); + }); +} + +plugin.getSessions = function(cb) { + log.debug('running leetcode.getSessions'); + runSession('POST', {}, cb); +}; + +plugin.activateSession = function(session, cb) { + log.debug('running leetcode.activateSession'); + const data = {func: 'activate', target: session.id}; + runSession('PUT', data, cb); +}; + +plugin.createSession = function(name, cb) { + log.debug('running leetcode.createSession'); + const data = {func: 'create', name: name}; + runSession('PUT', data, cb); +}; + +plugin.deleteSession = function(session, cb) { + log.debug('running leetcode.deleteSession'); + const data = {target: session.id}; + runSession('DELETE', data, cb); +}; + +plugin.signin = function(user, cb) { + log.debug('running leetcode.signin'); + const spin = h.spin('Signing in leetcode.com'); + request(config.sys.urls.login, function(e, resp, body) { + spin.stop(); + e = checkError(e, resp, 200); + if (e) return cb(e); + + user.loginCSRF = h.getSetCookieValue(resp, 'csrftoken'); + + const opts = { + url: config.sys.urls.login, + headers: { + Origin: config.sys.urls.base, + Referer: config.sys.urls.login, + Cookie: 'csrftoken=' + user.loginCSRF + ';' + }, + form: { + csrfmiddlewaretoken: user.loginCSRF, + login: user.login, + password: user.pass + } + }; + request.post(opts, function(e, resp, body) { + if (e) return cb(e); + if (resp.statusCode !== 302) return cb('invalid password?'); + + user.sessionCSRF = h.getSetCookieValue(resp, 'csrftoken'); + user.sessionId = h.getSetCookieValue(resp, 'LEETCODE_SESSION'); + session.saveUser(user); + return cb(null, user); + }); + }); +}; + +plugin.getUser = function(user, cb) { + plugin.getFavorites(function(e, favorites) { + if (e) return cb(e); + + const favorite = favorites.favorites.private_favorites.find(function(f) { + return f.name === 'Favorite'; + }); + user.hash = favorite.id_hash; + user.name = favorites.user_name; + session.saveUser(user); + return cb(null, user); + }); +}; + +plugin.login = function(user, cb) { + log.debug('running leetcode.login'); + plugin.signin(user, function(e, user) { + if (e) return cb(e); + plugin.getUser(user, cb); + }); +}; + +module.exports = plugin; diff --git a/templates/detailed.tpl b/templates/detailed.tpl index b8a762e2..134ba80e 100644 --- a/templates/detailed.tpl +++ b/templates/detailed.tpl @@ -2,6 +2,7 @@ <%= comment.line %> [<%= id %>] <%= name %> <%= comment.line %> <%= comment.line %> <%= link %> +<%= comment.line %> <%= discuss %> <%= comment.line %> <%= comment.line %> <%= category %> <%= comment.line %> <%= level %> (<%= percent %>%)