From 81616feba2177b508028452ea3de4fb18f2e555b Mon Sep 17 00:00:00 2001 From: Vincent Zhou Date: Sun, 23 Aug 2020 08:32:58 +0800 Subject: [PATCH] Added missing plugins from leetcode-tools/leetcode-cli-plugins Signed-off-by: Vincent Zhou --- lib/plugins/cookie.chrome.js | 184 ++++++++++++++++++ lib/plugins/cookie.firefox.js | 83 ++++++++ lib/plugins/cpp.lint.js | 47 +++++ lib/plugins/cpp.run.js | 225 ++++++++++++++++++++++ lib/plugins/github.js | 79 ++++++++ lib/plugins/lintcode.js | 352 ++++++++++++++++++++++++++++++++++ 6 files changed, 970 insertions(+) create mode 100644 lib/plugins/cookie.chrome.js create mode 100644 lib/plugins/cookie.firefox.js create mode 100644 lib/plugins/cpp.lint.js create mode 100644 lib/plugins/cpp.run.js create mode 100644 lib/plugins/github.js create mode 100644 lib/plugins/lintcode.js diff --git a/lib/plugins/cookie.chrome.js b/lib/plugins/cookie.chrome.js new file mode 100644 index 00000000..e2d42780 --- /dev/null +++ b/lib/plugins/cookie.chrome.js @@ -0,0 +1,184 @@ +var path = require('path'); + +var log = require('../log'); +var Plugin = require('../plugin'); +var Queue = require('../queue'); +var session = require('../session'); + +// [Usage] +// +// https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.chrome.md +// +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']); + +plugin.help = function() { + switch (process.platform) { + case 'darwin': + break; + case 'linux': + log.warn('To complete the install: sudo apt install libsecret-tools'); + break; + case 'win32': + break; + } +}; + +var Chrome = {}; + +var ChromeMAC = { + getDBPath: function() { + return `${process.env.HOME}/Library/Application Support/Google/Chrome/${this.profile}/Cookies`; + }, + iterations: 1003, + getPassword: function(cb) { + var keytar = require('keytar'); + keytar.getPassword('Chrome Safe Storage', 'Chrome').then(cb); + } +}; + +var ChromeLinux = { + 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?? + var cmd = 'secret-tool lookup application chrome'; + var password = require('child_process').execSync(cmd).toString(); + return cb(password); + } +}; + +var ChromeWindows = { + getDBPath: function() { + return path.resolve(process.env.APPDATA || '', `../Local/Google/Chrome/User Data/${this.profile}/Cookies`); + }, + getPassword: function(cb) { cb(); } +}; + +Object.setPrototypeOf(ChromeMAC, Chrome); +Object.setPrototypeOf(ChromeLinux, Chrome); +Object.setPrototypeOf(ChromeWindows, Chrome); + +Chrome.getInstance = function() { + switch (process.platform) { + case 'darwin': return ChromeMAC; + case 'linux': return ChromeLinux; + case 'win32': return ChromeWindows; + } +}; +var my = Chrome.getInstance(); + +ChromeWindows.decodeCookie = function(cookie, cb) { + var ref = require('ref'); + var ffi = require('ffi'); + var Struct = require('ref-struct'); + + var DATA_BLOB = Struct({ + cbData: ref.types.uint32, + pbData: ref.refType(ref.types.byte) + }); + var PDATA_BLOB = new ref.refType(DATA_BLOB); + var Crypto = new ffi.Library('Crypt32', { + 'CryptUnprotectData': ['bool', [PDATA_BLOB, 'string', 'string', 'void *', 'string', 'int', PDATA_BLOB]] + }); + + var inBlob = new DATA_BLOB(); + inBlob.pbData = cookie; + inBlob.cbData = cookie.length; + var outBlob = ref.alloc(DATA_BLOB); + + Crypto.CryptUnprotectData(inBlob.ref(), null, null, null, null, 0, outBlob); + var outDeref = outBlob.deref(); + var buf = ref.reinterpret(outDeref.pbData, outDeref.cbData, 0); + + return cb(null, buf.toString('utf8')); +}; + +Chrome.decodeCookie = function(cookie, cb) { + var crypto = require('crypto'); + crypto.pbkdf2(my.password, 'saltysalt', my.iterations, 16, 'sha1', function(e, key) { + if (e) return cb(e); + + var iv = new Buffer(' '.repeat(16)); + var decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); + decipher.setAutoPadding(false); + + var buf = decipher.update(cookie.slice(3)); // remove prefix "v10" or "v11" + var final = decipher.final(); + final.copy(buf, buf.length - 1); + + var padding = buf[buf.length - 1]; + if (padding) buf = buf.slice(0, buf.length - padding); + + return cb(null, buf.toString('utf8')); + }); +}; + +function doDecode(key, queue, cb) { + var ctx = queue.ctx; + var cookie = ctx[key]; + if (!cookie) return cb('Not found cookie: ' + key); + + my.decodeCookie(cookie, function(e, cookie) { + ctx[key] = cookie; + return cb(); + }); +} + +Chrome.getCookies = function(cb) { + var sqlite3 = require('sqlite3'); + var db = new sqlite3.Database(my.getDBPath()); + db.on('error', cb); + var KEYS = ['csrftoken', 'LEETCODE_SESSION']; + + db.serialize(function() { + var cookies = {}; + var sql = 'select name, encrypted_value from cookies where host_key like "%leetcode.com"'; + db.each(sql, function(e, x) { + if (e) return cb(e); + if (KEYS.indexOf(x.name) < 0) return; + cookies[x.name] = x.encrypted_value; + }); + + db.close(function() { + my.getPassword(function(password) { + my.password = password; + var q = new Queue(KEYS, cookies, doDecode); + q.run(null, 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 from profile "${my.profile}"`); + log.error(e); + return plugin.next.signin(user, cb); + } + + log.debug('Successfully copied leetcode cookies!'); + user.sessionId = cookies.LEETCODE_SESSION; + user.sessionCSRF = cookies.csrftoken; + session.saveUser(user); + return cb(null, user); + }); +}; + +plugin.login = function(user, cb) { + log.debug('running cookie.chrome.login'); + plugin.signin(user, function(e, user) { + if (e) return cb(e); + plugin.getUser(user, cb); + }); +}; + +module.exports = plugin; diff --git a/lib/plugins/cookie.firefox.js b/lib/plugins/cookie.firefox.js new file mode 100644 index 00000000..f69f8699 --- /dev/null +++ b/lib/plugins/cookie.firefox.js @@ -0,0 +1,83 @@ +var log = require('../log'); +var Plugin = require('../plugin'); +var session = require('../session'); + +// [Usage] +// +// https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.firefox.md +// +var plugin = new Plugin(13, 'cookie.firefox', '2018.11.19', + 'Plugin to reuse firefox\'s leetcode cookie.', + ['glob', 'sqlite3']); + +function getCookieFile(cb) { + var f; + switch (process.platform) { + case 'darwin': + f = process.env.HOME + '/Library/Application Support/Firefox/Profiles/*.default*/cookies.sqlite'; + break; + case 'linux': + f = process.env.HOME + '/.mozilla/firefox/*.default*/cookies.sqlite'; + break; + case 'win32': + f = (process.env.APPDATA || '') + '/Mozilla/Firefox/Profiles/*.default*/cookies.sqlite'; + break; + } + require('glob')(f, {}, cb); +} + +function getCookies(cb) { + getCookieFile(function(e, files) { + if (e || files.length === 0) return cb('Not found cookie file!'); + + var sqlite3 = require('sqlite3'); + var db = new sqlite3.Database(files[0]); + var KEYS = ['csrftoken', 'LEETCODE_SESSION']; + + db.serialize(function() { + var cookies = {}; + var sql = 'select name, value from moz_cookies where host like "%leetcode.com"'; + db.each(sql, function(e, x) { + if (e) return cb(e); + if (KEYS.indexOf(x.name) < 0) return; + cookies[x.name] = x.value; + }); + + db.close(function() { + return cb(null, cookies); + }); + }); + }); +} + +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); + 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); + } + + log.debug('Successfully copied leetcode cookies!'); + user.sessionId = cookies.LEETCODE_SESSION; + user.sessionCSRF = cookies.csrftoken; + session.saveUser(user); + return cb(null, user); + }); +}; + +plugin.login = function(user, cb) { + log.debug('running cookie.firefox.login'); + plugin.signin(user, function(e, user) { + if (e) return cb(e); + plugin.getUser(user, cb); + }); +}; + +module.exports = plugin; diff --git a/lib/plugins/cpp.lint.js b/lib/plugins/cpp.lint.js new file mode 100644 index 00000000..a892cd54 --- /dev/null +++ b/lib/plugins/cpp.lint.js @@ -0,0 +1,47 @@ +var cp = require('child_process'); + +var log = require('../log'); +var Plugin = require('../plugin'); + +// +// [Usage] +// +// https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cpp.lint.md +// +var plugin = new Plugin(100, 'cpp.lint', '2017.07.27', + 'Plugin to do static code check on c++ code.'); + +var DEFAULT_FLAGS = [ + '-legal/copyright', + '-build/include_what_you_use' +]; + +plugin.testProblem = function(problem, cb) { + // TODO: unify error handling + if (!plugin.config.bin) + return log.error('cpplint.py not configured correctly! (plugins:cpp.lint:bin)'); + + var flags = DEFAULT_FLAGS.concat(plugin.config.flags || []); + + var cmd = [ + plugin.config.bin, + '--filter=' + flags.join(','), + problem.file + ].join(' '); + + log.info('\nRunning cpplint ...'); + log.debug(cmd); + log.info(); + + cp.exec(cmd, function(e, stdout, stderr) { + if (e) { + stderr.split('\n').forEach(function(line) { + if (line.length > 0) log.error(line); + }); + } else { + plugin.next.testProblem(problem, cb); + } + }); +}; + +module.exports = plugin; diff --git a/lib/plugins/cpp.run.js b/lib/plugins/cpp.run.js new file mode 100644 index 00000000..6f206f8f --- /dev/null +++ b/lib/plugins/cpp.run.js @@ -0,0 +1,225 @@ +var cp = require('child_process'); +var fs = require('fs'); + +var h = require('../helper'); +var log = require('../log'); +var Plugin = require('../plugin.js'); +var session = require('../session'); + +// Please note that we DON'T want implement a lightweight judge engine +// here, thus we are NOT going to support all the problems!!! +// +// Only works for those problems could be easily tested. +// +// [Usage] +// +// https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cpp.run.md +// +var plugin = new Plugin(100, 'cpp.run', '2017.07.29', + 'Plugin to run cpp code locally for debugging.'); + +var FILE_SRC = '.tmp.cpp.run.cpp'; +var FILE_EXEC = '.tmp.cpp.run.exe'; + +plugin.testProblem = function(problem, cb) { + if (!session.argv.local || h.extToLang(problem.file) !== 'cpp') + return plugin.next.testProblem(problem, cb); + + log.info('\nTesting locally ...\n'); + + // generate full cpp source code that runnable + var meta = problem.templateMeta; + + var code = fs.readFileSync(problem.file).toString(); + var re = code.match(new RegExp(' ' + meta.name + '\\((.+)\\)')); + if (!re) return cb('failed to generate runnable code!'); + + var types = re[1].split(',').map(function(x) { + var parts = x.trim().split(' '); + parts.pop(); // skip param name + return parts.join(' '); + }); + + var values = problem.testcase.split('\n').map(function(x, i) { + // TODO: handle more special types?? + // array, list, tree, etc + var t = meta.params[i].type; + if (t.indexOf('[]') >= 0 || t === 'ListNode' || t === 'TreeNode') + x = x.replace(/\[/g, '{').replace(/\]/g, '}'); + if (t === 'ListNode') x = 'make_listnode(' + x + ')'; + if (t === 'TreeNode') x = 'make_treenode(' + x + ')'; + + return x; + }); + + var data = DATA.replace('$code', code) + .replace('$method', meta.name) + .replace('$argDefs', values.map(function(x, i) { + return ' decay<' + types[i] + '>::type ' + 'p' + i + ' = ' + x + ';'; + }).join('\n')) + .replace('$args', values.map(function(x, i) { + return 'p' + i; + }).join(',')); + + fs.writeFileSync(FILE_SRC, data); + + // compile and run + var cmd = [ + 'g++', + '-std=c++11', + '-o', + FILE_EXEC, + FILE_SRC, + '&&', + './' + FILE_EXEC + ].join(' '); + cp.exec(cmd, function(e, stdout, stderr) { + if (e) { + stderr.split('\n').forEach(function(line) { + if (line.length > 0) log.error(line); + }); + } else { + stdout.split('\n').forEach(function(line) { + if (line.length > 0) log.info(line); + }); + } + }); +}; + +// FIXME: use file template!! +var DATA = ` +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __cplusplus >= 201103L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +using namespace std; + +/// leetcode defined data types /// +struct ListNode { + int val; + ListNode *next; + ListNode(int x) : val(x), next(NULL) {} +}; + +struct TreeNode { + int val; + TreeNode *left, *right; + TreeNode(int x) : val(x), left(NULL), right(NULL) {} +}; + +ListNode* make_listnode(const vector &v) { + ListNode head(0), *p = &head, *cur; + for (auto x: v) { cur = new ListNode(x); p->next = cur; p = cur; } + return head.next; +} + +constexpr long long null = numeric_limits::min(); + +TreeNode* make_treenode(const vector &v) { + vector cur, next; + TreeNode root(0); cur.push_back(&root); + long long i = 0, n = v.size(), x; + while (i < n) { + for (auto p: cur) { + if ((x = v[i++]) != null) { p->left = new TreeNode(x); next.push_back(p->left); } + if (i == n || p == &root) continue; + if ((x = v[i++]) != null) { p->right = new TreeNode(x); next.push_back(p->right); } + } + cur.swap(next); next.clear(); + } + return root.left; +} + +template +ostream& operator<<(ostream &os, const vector &v) { + os << "["; + for (int i = 0; i < v.size(); ++i) os << (i > 0 ? "," : "") << v[i]; + os << "]"; + return os; +} + +ostream& operator<<(ostream &os, const ListNode *p) { + vector v; + while (p) { v.push_back(p->val); p = p->next; } + return os << v; +} + +ostream& operator<<(ostream &os, const TreeNode *t) { + vector v; + queue cur, next; + if (t) cur.push(t); + + while (!cur.empty()) { + t = cur.front(); cur.pop(); + v.push_back(t ? to_string(t->val) : "null"); + if (t && (t->left || t->right)) { + next.push(t->left); + if (t->right || !cur.empty()) next.push(t->right); + } + if (cur.empty()) cur.swap(next); + } + return os << v; +} + +$code +int main() { + Solution s; +$argDefs + auto res = s.$method($args); + cout << res << endl; + return 0; +} +`; + +module.exports = plugin; diff --git a/lib/plugins/github.js b/lib/plugins/github.js new file mode 100644 index 00000000..6c94e9ec --- /dev/null +++ b/lib/plugins/github.js @@ -0,0 +1,79 @@ +var path = require('path'); +var url = require('url'); + +var h = require('../helper'); +var file = require('../file'); +var log = require('../log'); +var Plugin = require('../plugin'); + +// +// [Usage] +// +// https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/github.md +// +var plugin = new Plugin(100, 'github', '2018.11.18', + 'Plugin to commit accepted code to your own github repo.', + ['github@13']); + +var ctx = {}; + +plugin.submitProblem = function(problem, cb) { + // TODO: unify error handling + if (!plugin.config.repo) + return log.error('GitHub repo not configured correctly! (plugins:github:repo)'); + if (!plugin.config.token) + return log.error('GitHub token not configured correctly! (plugins:github:token)'); + + var parts = url.parse(plugin.config.repo).pathname.split('/'); + var filename = path.basename(problem.file); + parts.push(filename); + + if (parts[0] === '') parts.shift(); + ctx.owner = parts.shift(); + ctx.repo = parts.shift(); + ctx.path = parts.join('/'); + + var GitHubApi = require('github'); + var github = new GitHubApi({host: 'api.github.com'}); + github.authenticate({type: 'token', token: plugin.config.token}); + + plugin.next.submitProblem(problem, function(_e, results) { + cb(_e, results); + if (_e || !results[0].ok) return; + + log.debug('running github.getContent: ' + filename); + github.repos.getContent(ctx, function(e, res) { + if (e && e.code !== 404) { + return log.info(' ' + h.prettyText(' ' + e.message, false)); + } + + ctx.message = 'update ' + filename; + ctx.content = new Buffer(file.data(problem.file)).toString('base64'); + + var onFileDone = function(e, res) { + if (e) + return log.info(' ' + h.prettyText(' ' + e.message, false)); + + log.debug(res.meta.status); + log.debug('updated current file version = ' + res.data.content.sha); + log.debug('updated current commit = ' + res.data.commit.sha); + log.info(' ' + h.prettyText(' Committed to ' + plugin.config.repo, true)); + }; + + if (e) { + log.debug('no previous file version found'); + + log.debug('running github.createFile'); + github.repos.createFile(ctx, onFileDone); + } else { + ctx.sha = res.data.sha; + log.debug('found previous file version = ' + ctx.sha); + + log.debug('running github.updateFile'); + github.repos.updateFile(ctx, onFileDone); + } + }); + }); +}; + +module.exports = plugin; diff --git a/lib/plugins/lintcode.js b/lib/plugins/lintcode.js new file mode 100644 index 00000000..073d12d5 --- /dev/null +++ b/lib/plugins/lintcode.js @@ -0,0 +1,352 @@ +var _ = require('underscore'); +var cheerio = require('cheerio'); +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 session = require('../session'); + +// Still working in progress! +// +// TODO: star/submissions/submission +// FIXME: why [ERROR] Error: read ECONNRESET [0]?? +// +// [Usage] +// +// https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/lintcode.md +// +const plugin = new Plugin(15, 'lintcode', '2018.11.18', + 'Plugin to talk with lintcode APIs.'); + +// 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) { + const opts = { + url: url, + 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; +} + +function _split(s, delim) { + return (s || '').split(delim).map(function(x) { + return x.trim(); + }).filter(function(x) { + return x.length > 0; + }); +} + +function _strip(s) { + s = s.replace(/^
/, '').replace(/<\/code><\/pre>$/, '');
+  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 = [];
+  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);
+    });
+  };
+
+  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);
+  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);
+
+    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',
+        level:     LEVELS[p.level],
+        locked:    false,
+        percent:   p.accepted_rate,
+        starred:   p.is_favorited,
+        companies: p.company_tags,
+        tags:      []
+      };
+      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;
+    });
+
+    ctx.count = json.count;
+    ctx.pages = json.maximum_page;
+    return cb(null, problems, ctx);
+  });
+};
+
+plugin.getProblem = function(problem, cb) {
+  log.debug('running lintcode.getProblem');
+  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);
+
+    const json = JSON.parse(body);
+    problem.testcase = json.testcase_sample;
+    problem.testable = problem.testcase.length > 0;
+    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 = [];
+
+    const getLang = function(lang, queue, cb) {
+      plugin.getProblemCode(problem, lang, function(e, code) {
+        if (!e) {
+          lang = _.clone(lang);
+          lang.defaultCode = code;
+          problem.templates.push(lang);
+        }
+        return cb(e);
+      });
+    };
+
+    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);
+  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);
+
+    var json = JSON.parse(body);
+    return cb(null, json.code);
+  });
+};
+
+function runCode(problem, isTest, cb) {
+  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:       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) 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.sys.urls.test_verify : config.sys.urls.submit_verify;
+  var opts = makeOpts(url.replace('$id', id));
+
+  request(opts, function(e, resp, body) {
+    e = checkError(e, resp, 200);
+    if (e) return cb(e);
+
+    var result = JSON.parse(body);
+    if (result.status === 'Compiling' || result.status === 'Running')
+      return setTimeout(verifyResult, 1000, id, isTest, cb);
+
+    return cb(null, formatResult(result));
+  });
+}
+
+function formatResult(result) {
+  spin.stop();
+  var x = {
+    ok:              result.status === 'Accepted',
+    type:            'Actual',
+    state:           result.status,
+    runtime:         result.time_cost + ' ms',
+    answer:          _strip(result.output),
+    stdout:          _strip(result.stdout),
+    expected_answer: _strip(result.expected),
+    testcase:        _strip(result.input),
+    passed:          result.data_accepted_count || 0,
+    total:           result.data_total_count || 0
+  };
+
+  var error = [];
+  if (result.compile_info.length > 0)
+    error = error.concat(_split(result.compile_info, '
')); + if (result.error_message.length > 0) + error = error.concat(_split(result.error_message, '
')); + x.error = error; + + // make sure everything is ok + if (error.length > 0) x.ok = false; + if (x.passed !== x.total) x.ok = false; + + return x; +} + +plugin.testProblem = function(problem, cb) { + log.debug('running lintcode.testProblem'); + runCode(problem, true, function(e, result) { + if (e) return cb(e); + + const expected = { + ok: true, + type: 'Expected', + answer: result.expected_answer, + stdout: "''" + }; + return cb(null, [result, expected]); + }); +}; + +plugin.submitProblem = function(problem, cb) { + log.debug('running lintcode.submitProblem'); + runCode(problem, false, function(e, result) { + if (e) return cb(e); + return cb(null, [result]); + }); +}; + +plugin.getSubmissions = function(problem, cb) { + return cb('Not implemented'); +}; + +plugin.getSubmission = function(submission, cb) { + return cb('Not implemented'); +}; + +plugin.starProblem = function(problem, starred, cb) { + return cb('Not implemented'); +}; + +plugin.login = function(user, cb) { + log.debug('running lintcode.login'); + const opts = { + url: config.sys.urls.login, + headers: { + 'x-csrftoken': null + }, + form: { + username_or_email: user.login, + password: user.pass + } + }; + + 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 + + return cb(null, user); + }); +}; + +module.exports = plugin;