diff --git a/lib/commands/contest.js b/lib/commands/contest.js new file mode 100644 index 00000000..3769456c --- /dev/null +++ b/lib/commands/contest.js @@ -0,0 +1,56 @@ +'use strict'; +var _ = require('underscore'); + +var h = require('../helper'); +var chalk = require('../chalk'); +var icon = require('../icon'); +var log = require('../log'); +var core = require('../core'); + +const cmd = { + command: 'contest [keyword]', + aliases: ['contests'], + desc: 'Solve questions from a contest', + builder: function(yargs) { + return yargs + .positional('keyword', { + type: 'string', + default: '', + describe: 'Filter contests by keyword' + }) + .example(chalk.yellow('leetcode contest'), 'List all contests') + .example(chalk.yellow('leetcode contest weekly-contest-155'), 'List questions from a contest'); + } +}; + +cmd.handler = function(argv) { + let keyword = argv.keyword; + log.debug('argv: ' + keyword); + if (keyword.length > 0) { + // Show questions of a specific contest + core.getContestQuestions(keyword, function(e, questions) { + if (e) return log.fail(e); + + for (let q of questions) { + log.printf('%-60s (%s)', + q.name, + q.slug + ); + } + }); + } else { + // List all contests + core.getContests(function(e, contests) { + if (e) return log.fail(e); + + for (let c of contests) { + log.printf('%s (%s)', + c.title, + c.titleSlug + ); + } + }); + } +}; + +module.exports = cmd; diff --git a/lib/config.js b/lib/config.js index a427b8f3..c29586f4 100644 --- a/lib/config.js +++ b/lib/config.js @@ -33,6 +33,7 @@ const DEFAULT_CONFIG = { base: 'https://leetcode.com', graphql: 'https://leetcode.com/graphql', login: 'https://leetcode.com/accounts/login/', + contest: 'https://leetcode.com/contest/api/info/$slug', problems: 'https://leetcode.com/api/problems/$category/', problem: 'https://leetcode.com/problems/$slug/description/', test: 'https://leetcode.com/problems/$slug/interpret_solution/', diff --git a/lib/core.js b/lib/core.js index 74362f78..806d4f22 100644 --- a/lib/core.js +++ b/lib/core.js @@ -128,4 +128,12 @@ core.exportProblem = function(problem, opts) { return file.render(opts.tpl, data); }; +core.getContests = function(cb) { + core.next.getContests(cb); +}; + +core.getContestQuestions = function(titleSlug, cb) { + core.next.getContestQuestions(titleSlug, cb); +}; + module.exports = core; diff --git a/lib/plugins/leetcode.js b/lib/plugins/leetcode.js index 0ce3a66e..abfb2c65 100644 --- a/lib/plugins/leetcode.js +++ b/lib/plugins/leetcode.js @@ -55,6 +55,69 @@ plugin.init = function() { config.app = 'leetcode'; } +plugin.getContests = function(cb) { + log.debug('running leetcode.getContests'); + + const opts = plugin.makeOpts(config.sys.urls.graphql); + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = config.sys.urls.base; + opts.json = true; + opts.body = { + query: [ + '{', + ' allContests {', + ' containsPremium', + ' title', + ' titleSlug', + ' }', + '}', + ].join('\n'), + variables: {} + }; + + const spin = h.spin('Fetching contests'); + request.post(opts, function(e, resp, body) { + spin.stop(); + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + const contests = body.data.allContests; + return cb(null, contests.reverse()); + }); +}; + +plugin.getContestQuestions = function(titleSlug, cb) { + log.debug('running leetcode.getContest'); + const opts = plugin.makeOpts(config.sys.urls.contest.replace('$slug', titleSlug)); + + // TODO add spin text + request(opts, function(e, resp, body) { + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + const json = JSON.parse(body); + const questions = json.questions + .map(function(p) { + return { + id: p.id, + fid: p.question_id, + name: p.title, + slug: p.title_slug, + credit: p.credit, + }; + }); + + return cb(null, questions); + }); + + // 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.getProblems = function(cb) { log.debug('running leetcode.getProblems'); let problems = []; diff --git a/test/mock/weekly-contest-sample.20190926.json b/test/mock/weekly-contest-sample.20190926.json new file mode 100644 index 00000000..f180d118 --- /dev/null +++ b/test/mock/weekly-contest-sample.20190926.json @@ -0,0 +1 @@ +{"contest":{"id":245,"title":"Weekly Contest 155","title_slug":"weekly-contest-155","description":"\r\n\r\n
\r\n
\r\n
\r\n

\r\n Welcome to the 155th LeetCode Weekly Contest\r\n

\r\n
\r\n

\r\n \r\n  Important Note\r\n

\r\n
    \r\n
  1. \r\n The penalty time has been changed from 10 minutes to 5 minutes for each wrong submission.\r\n
  2. \r\n
  3. \r\n All submissions will be run through a plagiarism checker. Any detected plagiarism will result in immediate disqualification + 3 weeks account ban. Cheating will NOT be tolerated in any way, shape, or form. Please read the Contest FAQ for more information.\r\n
  4. \r\n
\r\n

\r\n

\r\n \r\n  Announcement\r\n

\r\n

\r\n Users must register to participate. We hope you enjoy this contest!\r\n

\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n

\r\n \r\n  Prize\r\n

\r\n
    \r\n
  • \r\n 1st\r\n \r\n 5,000 \"LeetCoin\"\r\n \r\n
  • \r\n
  • \r\n 2nd\r\n \r\n 2,500 \"LeetCoin\"\r\n \r\n
  • \r\n
  • \r\n 3rd\r\n \r\n 1,000 \"LeetCoin\"\r\n \r\n
  • \r\n
  • \r\n 4 - 50th\r\n \r\n 300 \"LeetCoin\"\r\n \r\n
  • \r\n
  • \r\n 51 - 100th\r\n \r\n 100 \"LeetCoin\"\r\n \r\n
  • \r\n
  • \r\n 101 - 200th\r\n \r\n 50 \"LeetCoin\"\r\n \r\n
  • \r\n
  • \r\n Participate\r\n \r\n 5 \"LeetCoin\"\r\n \r\n
  • \r\n
  • \r\n First Time Participate\r\n \r\n 200 \"LeetCoin\"\r\n \r\n
  • \r\n
  • \r\nNEW\r\n Participate Biweekly + Weekly Contests in Same Week\r\n \r\n 100 \"LeetCoin\"\r\n \r\n
  • \r\n
\r\n
\r\n
\r\n
\r\n
\r\n

Want millions of LeetCode users to recognize your company? Contact us to sponsor a contest.

","duration":5400,"start_time":1569119400,"is_virtual":false,"origin_start_time":1569119400,"is_private":false,"related_contest_title":null},"questions":[{"id":974,"question_id":1306,"credit":3,"title":"Minimum Absolute Difference","title_slug":"minimum-absolute-difference"},{"id":975,"question_id":1307,"credit":4,"title":"Ugly Number III","title_slug":"ugly-number-iii"},{"id":976,"question_id":1308,"credit":5,"title":"Smallest String With Swaps","title_slug":"smallest-string-with-swaps"},{"id":977,"question_id":1309,"credit":8,"title":"Sort Items by Groups Respecting Dependencies","title_slug":"sort-items-by-groups-respecting-dependencies"}],"user_num":4979,"has_chosen_contact":false,"company":{"name":"LeetCode","description":"LeetCode's mission is to create the best code learning platform helping students and engineers to land on their dream jobs. For more information please see our Jobs page.","logo":"https://assets.leetcode.com/contest/LeetCode/company_logo"},"registered":false,"containsPremium":false} \ No newline at end of file diff --git a/test/plugins/test_leetcode.js b/test/plugins/test_leetcode.js index ef099b9b..6b9159d3 100644 --- a/test/plugins/test_leetcode.js +++ b/test/plugins/test_leetcode.js @@ -107,6 +107,24 @@ describe('plugin:leetcode', function() { }); }); // #login + describe('#getContests', function() { + // TODO + }); // #getContests + + describe('#getContestQuestions', function() { + it('should ok', function(done) { + nock('https://leetcode.com') + .get('/contest/api/info/weekly-contest-155') + .replyWithFile(200, './test/mock/weekly-contest-sample.20190926.json'); + + plugin.getContestQuestions('weekly-contest-155', function(e, questions) { + assert.equal(e, null); + assert.equal(questions.length, 4); + done(); + }); + }); + }); + describe('#getProblems', function() { it('should ok', function(done) { nock('https://leetcode.com')