diff --git a/.eslintrc.js b/.eslintrc.js index f274110..0c40bef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,17 +1,36 @@ module.exports = { "env": { "browser": false, - "node": true, - "es6": true + "es6": true, + "mocha": true, + "node": true }, - "extends": "google", + "extends": [ + "google", + "eslint:recommended" + ], "rules": { + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "camelcase": [2, {properties: "never"}], "comma-dangle": 0, "curly": 0, "key-spacing": [2, {align: "value"}], - "max-len": [1, 120], + "max-len": [1, 150], + "no-console": 1, + "no-empty": [2, { "allowEmptyCatch": true }], + "no-eval": 1, // we use it on purpose + "no-loop-func": 1, + "no-multi-spaces": 0, + "no-proto": 1, + "no-unused-expressions": 1, + "no-unused-vars": 1, "no-var": 0, + "no-warning-comments": 0, + "prefer-rest-params": 0, + "prefer-spread": 0, + "quote-props": 1, "quotes": [2, "single", {avoidEscape: true}], - "require-jsdoc": 0 + "require-jsdoc": 0, } }; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a6f5aa8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - 10 +os: + - linux +install: + - npm install +script: + - npm test diff --git a/README.md b/README.md index 41fb9c1..ecda979 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ |[cpp.lint](/docs/cpp.lint.md)|C++ code syntax check|`test`| |[cpp.run](/docs/cpp.run.md)|Test C++ code locally|`test`| |[github](/docs/github.md)|Commit accpeted code to GitHub|`submit`| +|[leetcode.cn](/docs/leetcode.cn.md)|Fight questions from leetcode-cn.com|| |[lintcode](/docs/lintcode.md)|Fight questions from lintcode.com|`list` `show` `test` `submit` `user`| |[solution.discuss](/docs/solution.discuss.md)|Fetch top voted solution|`show`| diff --git a/docs/cookie.chrome.md b/docs/cookie.chrome.md index a4f7593..c5a9dbc 100644 --- a/docs/cookie.chrome.md +++ b/docs/cookie.chrome.md @@ -26,6 +26,28 @@ Make sure build environment is ready before installing plugin: $ npm install -g windows-build-tools $ npm config set msvs_version 2015 -g +## Config + +* `profile`: chrome profile in use, default value is "Default". + +*Set Config* + + $ leetcode config plugins:cookie.chrome:profile "Your Profile" + +*Unset Config* + + $ leetcode config -d plugins:cookie.chrome + +*Example* + + { + "plugins": { + "cookie.chrome": { + "profile": "Profile 2" + } + } + } + ## Usage If enabled, the login will try to reuse existing chrome cookies. You can verify it by printing debug output as below. diff --git a/docs/leetcode.cn.md b/docs/leetcode.cn.md new file mode 100644 index 0000000..3a65ddd --- /dev/null +++ b/docs/leetcode.cn.md @@ -0,0 +1,7 @@ +# leetcode.cn + +Run everything against "leetcode-cn.com" instead of "leetcode.com". + +## Requirement + +You need a valid account on leetcode-cn.com since it uses diffrent user database from leetcode.com. diff --git a/package.json b/package.json index 99fca68..4ea4b95 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/skygragon/leetcode-cli-plugins#readme", "devDependencies": { - "eslint": "^4.4.1", - "eslint-config-google": "^0.9.1" + "eslint": "5.9.0", + "eslint-config-google": "0.11.0" } } diff --git a/plugins/company.js b/plugins/company.js index 711d8a6..ac48974 100644 --- a/plugins/company.js +++ b/plugins/company.js @@ -21,6 +21,7 @@ var COMPONIES = { '5': ['amazon', 'bloomberg', 'microsoft'], '7': ['apple', 'bloomberg'], '8': ['amazon', 'bloomberg', 'microsoft', 'uber'], + '10': ['airbnb', 'facebook', 'google', 'twitter', 'uber'], '11': ['bloomberg'], '12': ['twitter'], @@ -84,6 +85,7 @@ var COMPONIES = { '94': ['microsoft'], '96': ['snapchat'], '98': ['amazon', 'bloomberg', 'facebook', 'microsoft'], + '100': ['bloomberg'], '101': ['bloomberg', 'linkedin', 'microsoft'], '102': ['amazon', 'apple', 'bloomberg', 'facebook', 'linkedin', 'microsoft'], @@ -532,6 +534,7 @@ var TAGS = { '7': ['math'], '8': ['math', 'string'], '9': ['math'], + '10': ['backtracking', 'dynamic-programming', 'string'], '11': ['array', 'two-pointers'], '12': ['math', 'string'], @@ -622,6 +625,7 @@ var TAGS = { '97': ['dynamic-programming', 'string'], '98': ['depth-first-search', 'tree'], '99': ['depth-first-search', 'tree'], + '100': ['depth-first-search', 'tree'], '101': ['breadth-first-search', 'depth-first-search', 'tree'], '102': ['breadth-first-search', 'tree'], @@ -726,6 +730,7 @@ var TAGS = { '217': ['array', 'hash-table'], '218': ['binary-indexed-tree', 'divide-and-conquer', 'heap', 'segment-tree'], '219': ['array', 'hash-table'], + '220': ['binary-search-tree'], '221': ['dynamic-programming'], '222': ['binary-search', 'tree'], '223': ['math'], @@ -817,7 +822,7 @@ var TAGS = { '312': ['divide-and-conquer', 'dynamic-programming'], '313': ['heap', 'math'], '314': ['hash-table'], - '315': ['binary-indexed-tree', 'divide-and-conquer', 'segment-tree'], + '315': ['binary-indexed-tree', 'divide-and-conquer', 'segment-tree', 'binary-search-tree'], '316': ['greedy', 'stack'], '317': ['breadth-first-search'], '318': ['bit-manipulation'], @@ -829,7 +834,7 @@ var TAGS = { '324': ['sort'], '325': ['hash-table'], '326': ['math'], - '327': ['divide-and-conquer'], + '327': ['divide-and-conquer', 'binary-search-tree'], '328': ['linked-list'], '329': ['depth-first-search', 'topological-sort'], '330': ['greedy'], @@ -853,6 +858,7 @@ var TAGS = { '349': ['binary-search', 'hash-table', 'sort', 'two-pointers'], '350': ['binary-search', 'hash-table', 'sort', 'two-pointers'], '351': ['backtracking', 'dynamic-programming'], + '352': ['binary-search-tree'], '353': ['design', 'queue'], '354': ['binary-search', 'dynamic-programming'], '355': ['design', 'hash-table', 'heap'], @@ -963,7 +969,7 @@ var TAGS = { '488': ['depth-first-search'], '490': ['breadth-first-search', 'depth-first-search'], '491': ['depth-first-search'], - '493': ['binary-indexed-tree', 'divide-and-conquer', 'segment-tree'], + '493': ['binary-indexed-tree', 'divide-and-conquer', 'segment-tree', 'binary-search-tree'], '494': ['depth-first-search', 'dynamic-programming'], '495': ['array'], '496': ['stack'], @@ -989,6 +995,7 @@ var TAGS = { '526': ['backtracking'], '527': ['sort', 'string'], '529': ['breadth-first-search', 'depth-first-search'], + '530': ['array', 'depth-first-search', 'binary-search-tree'], '531': ['array', 'depth-first-search'], '532': ['array', 'two-pointers'], '533': ['array', 'depth-first-search'], @@ -1102,7 +1109,7 @@ var TAGS = { '680': ['string'], '681': ['string'], '682': ['stack'], - '683': ['array'], + '683': ['array', 'binary-search-tree'], '684': ['graph', 'tree', 'union-find'], '685': ['depth-first-search', 'graph', 'tree', 'union-find'], '686': ['string'], @@ -1118,12 +1125,12 @@ var TAGS = { '696': ['string'], '697': ['array'], '698': ['dynamic-programming'], - '699': ['segment-tree'], + '699': ['segment-tree', 'binary-search-tree'], '711': ['depth-first-search', 'hash-table'], '712': ['dynamic-programming'], '713': ['array', 'two-pointers'], '714': ['array', 'dynamic-programming', 'greedy'], - '715': ['array', 'segment-tree'], + '715': ['array', 'segment-tree', 'binary-search-tree'], '716': ['design'], '717': ['array'], '718': ['array', 'binary-search', 'dynamic-programming', 'hash-table'], @@ -1139,8 +1146,8 @@ var TAGS = { '728': ['math'], '729': ['array'], '730': ['dynamic-programming', 'string'], - '731': ['array'], - '732': ['segment-tree'], + '731': ['array', 'binary-search-tree'], + '732': ['segment-tree', 'binary-search-tree'], '733': ['depth-first-search'], '734': ['hash-table'], '735': ['stack'], diff --git a/plugins/cookie.chrome.js b/plugins/cookie.chrome.js index c5ac5b6..e2d4278 100644 --- a/plugins/cookie.chrome.js +++ b/plugins/cookie.chrome.js @@ -9,7 +9,7 @@ var session = require('../session'); // // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.chrome.md // -var plugin = new Plugin(13, 'cookie.chrome', '2017.12.23', +var plugin = new Plugin(13, 'cookie.chrome', '2018.11.18', 'Plugin to reuse Chrome\'s leetcode cookie.', ['ffi:win32', 'keytar:darwin', 'ref:win32', 'ref-struct:win32', 'sqlite3']); @@ -18,7 +18,7 @@ plugin.help = function() { case 'darwin': break; case 'linux': - log.info('To complete the install: sudo apt install libsecret-tools'); + log.warn('To complete the install: sudo apt install libsecret-tools'); break; case 'win32': break; @@ -28,7 +28,9 @@ plugin.help = function() { var Chrome = {}; var ChromeMAC = { - db: process.env.HOME + '/Library/Application Support/Google/Chrome/Default/Cookies', + getDBPath: function() { + return `${process.env.HOME}/Library/Application Support/Google/Chrome/${this.profile}/Cookies`; + }, iterations: 1003, getPassword: function(cb) { var keytar = require('keytar'); @@ -37,7 +39,9 @@ var ChromeMAC = { }; var ChromeLinux = { - db: process.env.HOME + '/.config/google-chrome/Default/Cookies', + getDBPath: function() { + return `${process.env.HOME}/.config/google-chrome/${this.profile}/Cookies`; + }, iterations: 1, getPassword: function(cb) { // FIXME: keytar failed to read gnome-keyring on ubuntu?? @@ -48,7 +52,9 @@ var ChromeLinux = { }; var ChromeWindows = { - db: path.resolve(process.env.APPDATA || '', '../Local/Google/Chrome/User Data/Default/Cookies'), + getDBPath: function() { + return path.resolve(process.env.APPDATA || '', `../Local/Google/Chrome/User Data/${this.profile}/Cookies`); + }, getPassword: function(cb) { cb(); } }; @@ -124,7 +130,8 @@ function doDecode(key, queue, cb) { Chrome.getCookies = function(cb) { var sqlite3 = require('sqlite3'); - var db = new sqlite3.Database(my.db); + var db = new sqlite3.Database(my.getDBPath()); + db.on('error', cb); var KEYS = ['csrftoken', 'LEETCODE_SESSION']; db.serialize(function() { @@ -149,9 +156,12 @@ Chrome.getCookies = function(cb) { plugin.signin = function(user, cb) { log.debug('running cookie.chrome.signin'); log.debug('try to copy leetcode cookies from chrome ...'); + + my.profile = plugin.config.profile || 'Default'; my.getCookies(function(e, cookies) { if (e) { - log.error('failed to copy cookies: ' + e); + log.error(`Failed to copy cookies from profile "${my.profile}"`); + log.error(e); return plugin.next.signin(user, cb); } diff --git a/plugins/cookie.firefox.js b/plugins/cookie.firefox.js index 45d8645..f69f869 100644 --- a/plugins/cookie.firefox.js +++ b/plugins/cookie.firefox.js @@ -6,7 +6,7 @@ var session = require('../session'); // // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.firefox.md // -var plugin = new Plugin(13, 'cookie.firefox', '2017.12.28', +var plugin = new Plugin(13, 'cookie.firefox', '2018.11.19', 'Plugin to reuse firefox\'s leetcode cookie.', ['glob', 'sqlite3']); @@ -48,14 +48,19 @@ function getCookies(cb) { }); }); }); -}; +} plugin.signin = function(user, cb) { log.debug('running cookie.firefox.signin'); log.debug('try to copy leetcode cookies from firefox ...'); getCookies(function(e, cookies) { if (e) { - log.error('failed to copy cookies: ' + e); + log.error('Failed to copy cookies: ' + e); + return plugin.next.signin(user, cb); + } + + if (!cookies.LEETCODE_SESSION || !cookies.csrftoken) { + log.error('Got invalid cookies: ' + JSON.stringify(cookies)); return plugin.next.signin(user, cb); } diff --git a/plugins/github.js b/plugins/github.js index 4c00e75..6c94e9e 100644 --- a/plugins/github.js +++ b/plugins/github.js @@ -2,6 +2,7 @@ var path = require('path'); var url = require('url'); var h = require('../helper'); +var file = require('../file'); var log = require('../log'); var Plugin = require('../plugin'); @@ -10,7 +11,7 @@ var Plugin = require('../plugin'); // // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/github.md // -var plugin = new Plugin(100, 'github', '2018.03.24', +var plugin = new Plugin(100, 'github', '2018.11.18', 'Plugin to commit accepted code to your own github repo.', ['github@13']); @@ -47,7 +48,7 @@ plugin.submitProblem = function(problem, cb) { } ctx.message = 'update ' + filename; - ctx.content = new Buffer(h.getFileData(problem.file)).toString('base64'); + ctx.content = new Buffer(file.data(problem.file)).toString('base64'); var onFileDone = function(e, res) { if (e) diff --git a/plugins/leetcode.cn.js b/plugins/leetcode.cn.js new file mode 100644 index 0000000..07d5d2d --- /dev/null +++ b/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/plugins/lintcode.js b/plugins/lintcode.js index 55e3aa4..073d12d 100644 --- a/plugins/lintcode.js +++ b/plugins/lintcode.js @@ -4,9 +4,11 @@ var request = require('request'); var util = require('util'); var h = require('../helper'); +var file = require('../file'); +var config = require('../config'); var log = require('../log'); var Plugin = require('../plugin'); -var queue = require('../queue'); +var Queue = require('../queue'); var session = require('../session'); // Still working in progress! @@ -18,36 +20,37 @@ var session = require('../session'); // // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/lintcode.md // -var plugin = new Plugin(15, 'lintcode', '2017.08.04', +const plugin = new Plugin(15, 'lintcode', '2018.11.18', 'Plugin to talk with lintcode APIs.'); -var config = { - URL_BASE: 'http://www.lintcode.com/en', - URL_PROBLEMS: 'http://www.lintcode.com/en/problem?page=$page', - URL_PROBLEM: 'http://www.lintcode.com/en/problem/$slug/', - URL_PROBLEM_CODE: 'http://www.lintcode.com/en/problem/api/code/?problem_id=$id&language=$lang', - URL_TEST: 'http://www.lintcode.com/submission/api/submit/', - URL_TEST_VERIFY: 'http://www.lintcode.com/submission/api/refresh/?id=$id&waiting_time=0&is_test_submission=true', - URL_SUBMIT_VERIFY: 'http://www.lintcode.com/submission/api/refresh/?id=$id&waiting_time=0', - URL_LOGIN: 'http://www.lintcode.com/en/accounts/signin/' -}; - -var LANGS = [ +// FIXME: add more langs +const LANGS = [ {value: 'cpp', text: 'C++'}, {value: 'java', text: 'Java'}, {value: 'python', text: 'Python'} ]; +const LEVELS = { + 0: 'Naive', + 1: 'Easy', + 2: 'Medium', + 3: 'Hard', + 4: 'Super' +}; + +var spin; + function signOpts(opts, user) { opts.headers.Cookie = 'sessionid=' + user.sessionId + ';csrftoken=' + user.sessionCSRF + ';'; + opts.headers['x-csrftoken'] = user.sessionCSRF; } function makeOpts(url) { - var opts = {}; - opts.url = url; - opts.headers = {}; - + const opts = { + url: url, + headers: {} + }; if (session.isLogin()) signOpts(opts, session.getUser()); return opts; @@ -55,7 +58,7 @@ function makeOpts(url) { function checkError(e, resp, expectedStatus) { if (!e && resp && resp.statusCode !== expectedStatus) { - var code = resp.statusCode; + const code = resp.statusCode; log.debug('http error: ' + code); if (code === 403 || code === 401) { @@ -80,130 +83,128 @@ function _strip(s) { return util.inspect(s.trim()); } +plugin.init = function() { + config.app = 'lintcode'; + config.sys.urls.base = 'https://www.lintcode.com'; + config.sys.urls.problems = 'https://www.lintcode.com/api/problems/?page=$page'; + config.sys.urls.problem = 'https://www.lintcode.com/problem/$slug/description'; + config.sys.urls.problem_detail = 'https://www.lintcode.com/api/problems/detail/?unique_name_or_alias=$slug&_format=detail'; + config.sys.urls.problem_code = 'https://www.lintcode.com/api/problems/$id/reset/?language=$lang'; + config.sys.urls.test = 'https://www.lintcode.com/api/submissions/'; + config.sys.urls.test_verify = 'https://www.lintcode.com/api/submissions/refresh/?id=$id&is_test_submission=true'; + config.sys.urls.submit_verify = 'https://www.lintcode.com/api/submissions/refresh/?id=$id'; + config.sys.urls.login = 'https://www.lintcode.com/api/accounts/signin/?next=%2F'; +}; + plugin.getProblems = function(cb) { log.debug('running lintcode.getProblems'); var problems = []; - var doTask = function(page, taskDone) { - plugin.getPageProblems(page, function(e, _problems) { - if (!e) problems = problems.concat(_problems); - return taskDone(e); + const getPage = function(page, queue, cb) { + plugin.getPageProblems(page, function(e, _problems, ctx) { + if (!e) { + problems = problems.concat(_problems); + queue.tasks = _.reject(queue.tasks, x => ctx.pages > 0 && x > ctx.pages); + } + return cb(e); }); }; - // FIXME: remove this hardcoded range! - var pages = [0, 1, 2, 3, 4]; - queue.run(pages, doTask, function(e) { - problems = _.sortBy(problems, function(x) { - return -x.id; - }); + const pages = _.range(1, 100); + const q = new Queue(pages, {}, getPage); + spin = h.spin('Downloading problems'); + q.run(null, function(e, ctx) { + spin.stop(); + problems = _.sortBy(problems, x => -x.id); return cb(e, problems); }); }; plugin.getPageProblems = function(page, cb) { log.debug('running lintcode.getPageProblems: ' + page); - var opts = makeOpts(config.URL_PROBLEMS.replace('$page', page)); + const opts = makeOpts(config.sys.urls.problems.replace('$page', page)); + spin.text = 'Downloading page ' + page; request(opts, function(e, resp, body) { e = checkError(e, resp, 200); if (e) return cb(e); - var $ = cheerio.load(body); - var problems = $('div[id=problem_list_pagination] a').map(function(i, a) { - var problem = { - locked: false, + const ctx = {}; + const json = JSON.parse(body); + const problems = json.problems.map(function(p, a) { + const problem = { + id: p.id, + fid: p.id, + name: p.title, + slug: p.unique_name, category: 'lintcode', - state: 'None', - starred: false, - companies: [], + level: LEVELS[p.level], + locked: false, + percent: p.accepted_rate, + starred: p.is_favorited, + companies: p.company_tags, tags: [] }; - problem.slug = $(a).attr('href').split('/').pop(); - problem.link = config.URL_PROBLEM.replace('$slug', problem.slug); - - $(a).children('span').each(function(i, span) { - var text = $(span).text().trim(); - var type = _split($(span).attr('class'), ' '); - type = type.concat(_split($(span).find('i').attr('class'), ' ')); - - if (type.indexOf('title') >= 0) { - problem.id = Number(text.split('.')[0]); - problem.name = text.split('.')[1].trim(); - } else if (type.indexOf('difficulty') >= 0) problem.level = text; - else if (type.indexOf('rate') >= 0) problem.percent = parseInt(text, 10); - else if (type.indexOf('fa-star') >= 0) problem.starred = true; - else if (type.indexOf('fa-check') >= 0) problem.state = 'ac'; - else if (type.indexOf('fa-minus') >= 0) problem.state = 'notac'; - else if (type.indexOf('fa-briefcase') >= 0) problem.companies = _split($(span).attr('title'), ','); - }); - + problem.link = config.sys.urls.problem.replace('$slug', problem.slug); + switch (p.user_status) { + case 'Accepted': problem.state = 'ac'; break; + case 'Failed': problem.state = 'notac'; break; + default: problem.state = 'None'; + } return problem; - }).get(); + }); - return cb(null, problems); + ctx.count = json.count; + ctx.pages = json.maximum_page; + return cb(null, problems, ctx); }); }; plugin.getProblem = function(problem, cb) { log.debug('running lintcode.getProblem'); - var opts = makeOpts(problem.link); + const link = config.sys.urls.problem_detail.replace('$slug', problem.slug); + const opts = makeOpts(link); + const spin = h.spin('Downloading ' + problem.slug); request(opts, function(e, resp, body) { + spin.stop(); e = checkError(e, resp, 200); if (e) return cb(e); - var $ = cheerio.load(body); - problem.testcase = $('textarea[id=input-testcase]').text(); + const json = JSON.parse(body); + problem.testcase = json.testcase_sample; problem.testable = problem.testcase.length > 0; - - var lines = []; - $('div[id=description] > div').each(function(i, div) { - if (i === 0) { - div = $(div).find('div')[0]; - lines.push($(div).text().trim()); - return; - } - - var text = $(div).text().trim(); - var type = $(div).find('b').text().trim(); - - if (type === 'Tags') { - problem.tags = _split(text, '\n'); - problem.tags.shift(); - } else if (type === 'Related Problems') return; - else lines.push(text); - }); - problem.desc = lines.join('\n').replace(/\n{2,}/g, '\n'); - problem.totalAC = ''; - problem.totalSubmit = ''; + problem.tags = json.tags.map(x => x.name); + problem.desc = cheerio.load(json.description).root().text(); + problem.totalAC = json.total_accepted; + problem.totalSubmit = json.total_submissions; problem.templates = []; - var doTask = function(lang, taskDone) { + const getLang = function(lang, queue, cb) { plugin.getProblemCode(problem, lang, function(e, code) { - if (e) return taskDone(e); - - lang = _.clone(lang); - lang.defaultCode = code; - problem.templates.push(lang); - return taskDone(); + if (!e) { + lang = _.clone(lang); + lang.defaultCode = code; + problem.templates.push(lang); + } + return cb(e); }); }; - queue.run(LANGS, doTask, function(e) { - return cb(e, problem); - }); + const q = new Queue(LANGS, {}, getLang); + q.run(null, e => cb(e, problem)); }); }; plugin.getProblemCode = function(problem, lang, cb) { log.debug('running lintcode.getProblemCode:' + lang.value); - var url = config.URL_PROBLEM_CODE - .replace('$id', problem.id) - .replace('$lang', lang.text.replace(/\+/g, '%2b')); - var opts = makeOpts(url); + const url = config.sys.urls.problem_code.replace('$id', problem.id) + .replace('$lang', lang.text.replace(/\+/g, '%2B')); + const opts = makeOpts(url); + const spin = h.spin('Downloading code for ' + lang.text); request(opts, function(e, resp, body) { + spin.stop(); e = checkError(e, resp, 200); if (e) return cb(e); @@ -213,36 +214,36 @@ plugin.getProblemCode = function(problem, lang, cb) { }; function runCode(problem, isTest, cb) { - var lang = _.find(LANGS, function(x) { - return x.value === h.extToLang(problem.file); - }); - - var opts = makeOpts(config.URL_TEST); + const lang = _.find(LANGS, x => x.value === h.extToLang(problem.file)); + const opts = makeOpts(config.sys.urls.test); + opts.headers.referer = problem.link; opts.form = { - problem_id: problem.id, - code: h.getFileData(problem.file), - language: lang.text, - csrfmiddlewaretoken: session.getUser().sessionCSRF + problem_id: problem.id, + code: file.data(problem.file), + language: lang.text }; if (isTest) { opts.form.input = problem.testcase; opts.form.is_test_submission = true; } + spin = h.spin('Sending code to judge'); request.post(opts, function(e, resp, body) { + spin.stop(); e = checkError(e, resp, 200); if (e) return cb(e); var json = JSON.parse(body); - if (!json.id || !json.success) return cb(json.message); + if (!json.id) return cb('Failed to start judge!'); + spin = h.spin('Waiting for judge result'); verifyResult(json.id, isTest, cb); }); } function verifyResult(id, isTest, cb) { log.debug('running verifyResult:' + id); - var url = isTest ? config.URL_TEST_VERIFY : config.URL_SUBMIT_VERIFY; + var url = isTest ? config.sys.urls.test_verify : config.sys.urls.submit_verify; var opts = makeOpts(url.replace('$id', id)); request(opts, function(e, resp, body) { @@ -258,6 +259,7 @@ function verifyResult(id, isTest, cb) { } function formatResult(result) { + spin.stop(); var x = { ok: result.status === 'Accepted', type: 'Actual', @@ -290,7 +292,7 @@ plugin.testProblem = function(problem, cb) { runCode(problem, true, function(e, result) { if (e) return cb(e); - var expected = { + const expected = { ok: true, type: 'Expected', answer: result.expected_answer, @@ -322,33 +324,28 @@ plugin.starProblem = function(problem, starred, cb) { plugin.login = function(user, cb) { log.debug('running lintcode.login'); - request(config.URL_LOGIN, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); + const opts = { + url: config.sys.urls.login, + headers: { + 'x-csrftoken': null + }, + form: { + username_or_email: user.login, + password: user.pass + } + }; - user.loginCSRF = h.getSetCookieValue(resp, 'csrftoken'); - - var opts = { - url: config.URL_LOGIN, - headers: { - Cookie: 'csrftoken=' + user.loginCSRF + ';' - }, - form: { - csrfmiddlewaretoken: user.loginCSRF, - username_or_email: 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?'); + const spin = h.spin('Signing in lintcode.com'); + request.post(opts, function(e, resp, body) { + spin.stop(); + if (e) return cb(e); + if (resp.statusCode !== 200) return cb('invalid password?'); - user.sessionCSRF = h.getSetCookieValue(resp, 'csrftoken'); - user.sessionId = h.getSetCookieValue(resp, 'sessionid'); - user.name = user.login; // FIXME + user.sessionCSRF = h.getSetCookieValue(resp, 'csrftoken'); + user.sessionId = h.getSetCookieValue(resp, 'sessionid'); + user.name = user.login; // FIXME - return cb(null, user); - }); + return cb(null, user); }); }; diff --git a/plugins/solution.discuss.js b/plugins/solution.discuss.js index 1289a13..e78b90a 100644 --- a/plugins/solution.discuss.js +++ b/plugins/solution.discuss.js @@ -1,5 +1,3 @@ -var _ = require('underscore'); -var cheerio = require('cheerio'); var request = require('request'); var log = require('../log'); @@ -12,7 +10,7 @@ var session = require('../session'); // // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/solution.discuss.md // -var plugin = new Plugin(200, 'solution.discuss', '2018.04.14', +var plugin = new Plugin(200, 'solution.discuss', '2019.02.03', 'Plugin to fetch most voted solution in discussions.'); var URL_DISCUSSES = 'https://leetcode.com/graphql'; @@ -21,14 +19,22 @@ var URL_DISCUSS = 'https://leetcode.com/problems/$slug/discuss/$id'; function getSolution(problem, lang, cb) { if (!problem) return cb(); + if (lang === 'python3') lang = 'python'; + var opts = { url: URL_DISCUSSES, json: true, - qs: { + body: { query: [ - 'query fetchTopics($questionId: Int!, $pageNo: Int!, $orderBy: String!) {', - ' questionTopics(questionId: $questionId, pageNo: $pageNo, orderBy: $orderBy) {', - ' data {', + 'query questionTopicsList($questionId: String!, $orderBy: TopicSortingOption, $skip: Int, $query: String, $first: Int!, $tags: [String!]) {', + ' questionTopicsList(questionId: $questionId, orderBy: $orderBy, skip: $skip, query: $query, first: $first, tags: $tags) {', + ' ...TopicsList', + ' }', + '}', + 'fragment TopicsList on TopicConnection {', + ' totalNum', + ' edges {', + ' node {', ' id', ' title', ' post {', @@ -42,11 +48,15 @@ function getSolution(problem, lang, cb) { ' }', '}' ].join('\n'), - operationName: 'fetchTopics', - variables: JSON.stringify({ - pageNo: 1, + + operationName: 'questionTopicsList', + variables: JSON.stringify({ + query: '', + first: 1, + skip: 0, orderBy: 'most_votes', - questionId: problem.id + questionId: '' + problem.id, + tags: [lang] }) } }; @@ -55,23 +65,8 @@ function getSolution(problem, lang, cb) { if (resp.statusCode !== 200) return cb({msg: 'http error', statusCode: resp.statusCode}); - var langs = [lang]; - // try to find more compatible langs - if (lang === 'cpp') langs.push('c++'); - if (lang === 'csharp') langs.push('c#'); - if (lang === 'golang') langs.push('go'); - if (lang === 'javascript') langs.push('js'); - if (lang === 'python3') langs.push('python'); - - var solutions = body.data.questionTopics.data; - var solution = _.find(solutions, function(x) { - var keys = x.title.toLowerCase().split(/[^\w+]/); - for (var i = 0; i < keys.length; ++i) { - if (langs.indexOf(keys[i]) >= 0) return true; - } - return false; - }); - + const solutions = body.data.questionTopicsList.edges; + const solution = solutions.length > 0 ? solutions[0].node : null; return cb(null, solution); }); }