|
| 1 | +var _ = require('underscore'); |
| 2 | +var cheerio = require('cheerio'); |
| 3 | +var request = require('request'); |
| 4 | +var util = require('util'); |
| 5 | + |
| 6 | +var h = require('../helper'); |
| 7 | +var log = require('../log'); |
| 8 | +var Plugin = require('../plugin'); |
| 9 | +var queue = require('../queue'); |
| 10 | +var session = require('../session'); |
| 11 | + |
| 12 | +// Still working in progress! |
| 13 | +// |
| 14 | +// TODO: star/submissions/submission |
| 15 | +// FIXME: why [ERROR] Error: read ECONNRESET [0]?? |
| 16 | +// |
| 17 | +var plugin = new Plugin(15, 'lintcode', '2017.08.04', |
| 18 | + 'Plugin to talk with lintcode APIs.'); |
| 19 | + |
| 20 | +var config = { |
| 21 | + URL_BASE: 'http://www.lintcode.com/en', |
| 22 | + URL_PROBLEMS: 'http://www.lintcode.com/en/problem?page=$page', |
| 23 | + URL_PROBLEM: 'http://www.lintcode.com/en/problem/$slug/', |
| 24 | + URL_PROBLEM_CODE: 'http://www.lintcode.com/en/problem/api/code/?problem_id=$id&language=$lang', |
| 25 | + URL_TEST: 'http://www.lintcode.com/submission/api/submit/', |
| 26 | + URL_TEST_VERIFY: 'http://www.lintcode.com/submission/api/refresh/?id=$id&waiting_time=0&is_test_submission=true', |
| 27 | + URL_SUBMIT_VERIFY: 'http://www.lintcode.com/submission/api/refresh/?id=$id&waiting_time=0', |
| 28 | + URL_LOGIN: 'http://www.lintcode.com/en/accounts/signin/' |
| 29 | +}; |
| 30 | + |
| 31 | +var LANGS = [ |
| 32 | + {value: 'cpp', text: 'C++'}, |
| 33 | + {value: 'java', text: 'Java'}, |
| 34 | + {value: 'python', text: 'Python'} |
| 35 | +]; |
| 36 | + |
| 37 | +function signOpts(opts, user) { |
| 38 | + opts.headers.Cookie = 'sessionid=' + user.sessionId + |
| 39 | + ';csrftoken=' + user.sessionCSRF + ';'; |
| 40 | +} |
| 41 | + |
| 42 | +function makeOpts(url) { |
| 43 | + var opts = {}; |
| 44 | + opts.url = url; |
| 45 | + opts.headers = {}; |
| 46 | + |
| 47 | + if (session.isLogin()) |
| 48 | + signOpts(opts, session.getUser()); |
| 49 | + return opts; |
| 50 | +} |
| 51 | + |
| 52 | +function checkError(e, resp, expectedStatus) { |
| 53 | + if (!e && resp && resp.statusCode !== expectedStatus) { |
| 54 | + var code = resp.statusCode; |
| 55 | + log.debug('http error: ' + code); |
| 56 | + |
| 57 | + if (code === 403 || code === 401) { |
| 58 | + e = session.errors.EXPIRED; |
| 59 | + } else { |
| 60 | + e = {msg: 'http error', statusCode: code}; |
| 61 | + } |
| 62 | + } |
| 63 | + return e; |
| 64 | +} |
| 65 | + |
| 66 | +function _split(s, delim) { |
| 67 | + return (s || '').split(delim).map(function(x) { |
| 68 | + return x.trim(); |
| 69 | + }).filter(function(x) { |
| 70 | + return x.length > 0; |
| 71 | + }); |
| 72 | +} |
| 73 | + |
| 74 | +function _strip(s) { |
| 75 | + s = s.replace(/^<pre><code>/, '').replace(/<\/code><\/pre>$/, ''); |
| 76 | + return util.inspect(s.trim()); |
| 77 | +} |
| 78 | + |
| 79 | +plugin.getProblems = function(cb) { |
| 80 | + log.debug('running lintcode.getProblems'); |
| 81 | + |
| 82 | + var problems = []; |
| 83 | + var doTask = function(page, taskDone) { |
| 84 | + plugin.getPageProblems(page, function(e, _problems) { |
| 85 | + if (!e) problems = problems.concat(_problems); |
| 86 | + return taskDone(e); |
| 87 | + }); |
| 88 | + }; |
| 89 | + |
| 90 | + // FIXME: remove this hardcoded range! |
| 91 | + var pages = [0, 1, 2, 3, 4]; |
| 92 | + queue.run(pages, doTask, function(e) { |
| 93 | + problems = _.sortBy(problems, function(x) { |
| 94 | + return -x.id; |
| 95 | + }); |
| 96 | + return cb(e, problems); |
| 97 | + }); |
| 98 | +}; |
| 99 | + |
| 100 | +plugin.getPageProblems = function(page, cb) { |
| 101 | + log.debug('running lintcode.getPageProblems: ' + page); |
| 102 | + var opts = makeOpts(config.URL_PROBLEMS.replace('$page', page)); |
| 103 | + |
| 104 | + request(opts, function(e, resp, body) { |
| 105 | + e = checkError(e, resp, 200); |
| 106 | + if (e) return cb(e); |
| 107 | + |
| 108 | + var $ = cheerio.load(body); |
| 109 | + var problems = $('div[id=problem_list_pagination] a').map(function(i, a) { |
| 110 | + var problem = { |
| 111 | + locked: false, |
| 112 | + category: 'lintcode', |
| 113 | + state: 'None', |
| 114 | + starred: false, |
| 115 | + companies: [], |
| 116 | + tags: [] |
| 117 | + }; |
| 118 | + problem.slug = $(a).attr('href').split('/').pop(); |
| 119 | + problem.link = config.URL_PROBLEM.replace('$slug', problem.slug); |
| 120 | + |
| 121 | + $(a).children('span').each(function(i, span) { |
| 122 | + var text = $(span).text().trim(); |
| 123 | + var type = _split($(span).attr('class'), ' '); |
| 124 | + type = type.concat(_split($(span).find('i').attr('class'), ' ')); |
| 125 | + |
| 126 | + if (type.indexOf('title') >= 0) { |
| 127 | + problem.id = Number(text.split('.')[0]); |
| 128 | + problem.name = text.split('.')[1].trim(); |
| 129 | + } else if (type.indexOf('difficulty') >= 0) problem.level = text; |
| 130 | + else if (type.indexOf('rate') >= 0) problem.percent = parseInt(text, 10); |
| 131 | + else if (type.indexOf('fa-star') >= 0) problem.starred = true; |
| 132 | + else if (type.indexOf('fa-check') >= 0) problem.state = 'ac'; |
| 133 | + else if (type.indexOf('fa-minus') >= 0) problem.state = 'notac'; |
| 134 | + else if (type.indexOf('fa-briefcase') >= 0) problem.companies = _split($(span).attr('title'), ','); |
| 135 | + }); |
| 136 | + |
| 137 | + return problem; |
| 138 | + }).get(); |
| 139 | + |
| 140 | + return cb(null, problems); |
| 141 | + }); |
| 142 | +}; |
| 143 | + |
| 144 | +plugin.getProblem = function(problem, cb) { |
| 145 | + log.debug('running lintcode.getProblem'); |
| 146 | + var opts = makeOpts(problem.link); |
| 147 | + |
| 148 | + request(opts, function(e, resp, body) { |
| 149 | + e = checkError(e, resp, 200); |
| 150 | + if (e) return cb(e); |
| 151 | + |
| 152 | + var $ = cheerio.load(body); |
| 153 | + problem.testcase = $('textarea[id=input-testcase]').text(); |
| 154 | + problem.testable = problem.testcase.length > 0; |
| 155 | + |
| 156 | + var lines = []; |
| 157 | + $('div[id=description] > div').each(function(i, div) { |
| 158 | + if (i === 0) { |
| 159 | + div = $(div).find('div')[0]; |
| 160 | + lines.push($(div).text().trim()); |
| 161 | + return; |
| 162 | + } |
| 163 | + |
| 164 | + var text = $(div).text().trim(); |
| 165 | + var type = $(div).find('b').text().trim(); |
| 166 | + |
| 167 | + if (type === 'Tags') { |
| 168 | + problem.tags = _split(text, '\n'); |
| 169 | + problem.tags.shift(); |
| 170 | + } else if (type === 'Related Problems') return; |
| 171 | + else lines.push(text); |
| 172 | + }); |
| 173 | + problem.desc = lines.join('\n').replace(/\n{2,}/g, '\n'); |
| 174 | + problem.totalAC = ''; |
| 175 | + problem.totalSubmit = ''; |
| 176 | + problem.templates = []; |
| 177 | + |
| 178 | + var doTask = function(lang, taskDone) { |
| 179 | + plugin.getProblemCode(problem, lang, function(e, code) { |
| 180 | + if (e) return taskDone(e); |
| 181 | + |
| 182 | + lang = _.clone(lang); |
| 183 | + lang.defaultCode = code; |
| 184 | + problem.templates.push(lang); |
| 185 | + return taskDone(); |
| 186 | + }); |
| 187 | + }; |
| 188 | + |
| 189 | + queue.run(LANGS, doTask, function(e) { |
| 190 | + return cb(e, problem); |
| 191 | + }); |
| 192 | + }); |
| 193 | +}; |
| 194 | + |
| 195 | +plugin.getProblemCode = function(problem, lang, cb) { |
| 196 | + log.debug('running lintcode.getProblemCode:' + lang.value); |
| 197 | + var url = config.URL_PROBLEM_CODE |
| 198 | + .replace('$id', problem.id) |
| 199 | + .replace('$lang', lang.text.replace(/\+/g, '%2b')); |
| 200 | + var opts = makeOpts(url); |
| 201 | + |
| 202 | + request(opts, function(e, resp, body) { |
| 203 | + e = checkError(e, resp, 200); |
| 204 | + if (e) return cb(e); |
| 205 | + |
| 206 | + var json = JSON.parse(body); |
| 207 | + return cb(null, json.code); |
| 208 | + }); |
| 209 | +}; |
| 210 | + |
| 211 | +function runCode(problem, isTest, cb) { |
| 212 | + var lang = _.find(LANGS, function(x) { |
| 213 | + return x.value === h.extToLang(problem.file); |
| 214 | + }); |
| 215 | + |
| 216 | + var opts = makeOpts(config.URL_TEST); |
| 217 | + opts.form = { |
| 218 | + problem_id: problem.id, |
| 219 | + code: h.getFileData(problem.file), |
| 220 | + language: lang.text, |
| 221 | + csrfmiddlewaretoken: session.getUser().sessionCSRF |
| 222 | + }; |
| 223 | + if (isTest) { |
| 224 | + opts.form.input = problem.testcase; |
| 225 | + opts.form.is_test_submission = true; |
| 226 | + } |
| 227 | + |
| 228 | + request.post(opts, function(e, resp, body) { |
| 229 | + e = checkError(e, resp, 200); |
| 230 | + if (e) return cb(e); |
| 231 | + |
| 232 | + var json = JSON.parse(body); |
| 233 | + if (!json.id || !json.success) return cb(json.message); |
| 234 | + |
| 235 | + verifyResult(json.id, isTest, cb); |
| 236 | + }); |
| 237 | +} |
| 238 | + |
| 239 | +function verifyResult(id, isTest, cb) { |
| 240 | + log.debug('running verifyResult:' + id); |
| 241 | + var url = isTest ? config.URL_TEST_VERIFY : config.URL_SUBMIT_VERIFY; |
| 242 | + var opts = makeOpts(url.replace('$id', id)); |
| 243 | + |
| 244 | + request(opts, function(e, resp, body) { |
| 245 | + e = checkError(e, resp, 200); |
| 246 | + if (e) return cb(e); |
| 247 | + |
| 248 | + var result = JSON.parse(body); |
| 249 | + if (result.status === 'Compiling' || result.status === 'Running') |
| 250 | + return setTimeout(verifyResult, 1000, id, isTest, cb); |
| 251 | + |
| 252 | + return cb(null, formatResult(result)); |
| 253 | + }); |
| 254 | +} |
| 255 | + |
| 256 | +function formatResult(result) { |
| 257 | + var x = { |
| 258 | + ok: result.status === 'Accepted', |
| 259 | + type: 'Actual', |
| 260 | + state: result.status, |
| 261 | + runtime: result.time_cost + ' ms', |
| 262 | + answer: _strip(result.output), |
| 263 | + stdout: _strip(result.stdout), |
| 264 | + expected_answer: _strip(result.expected), |
| 265 | + testcase: _strip(result.input), |
| 266 | + passed: result.data_accepted_count || 0, |
| 267 | + total: result.data_total_count || 0 |
| 268 | + }; |
| 269 | + |
| 270 | + var error = []; |
| 271 | + if (result.compile_info.length > 0) |
| 272 | + error = error.concat(_split(result.compile_info, '<br>')); |
| 273 | + if (result.error_message.length > 0) |
| 274 | + error = error.concat(_split(result.error_message, '<br>')); |
| 275 | + x.error = error; |
| 276 | + |
| 277 | + // make sure everything is ok |
| 278 | + if (error.length > 0) x.ok = false; |
| 279 | + if (x.passed !== x.total) x.ok = false; |
| 280 | + |
| 281 | + return x; |
| 282 | +} |
| 283 | + |
| 284 | +plugin.testProblem = function(problem, cb) { |
| 285 | + log.debug('running lintcode.testProblem'); |
| 286 | + runCode(problem, true, function(e, result) { |
| 287 | + if (e) return cb(e); |
| 288 | + |
| 289 | + var expected = { |
| 290 | + ok: true, |
| 291 | + type: 'Expected', |
| 292 | + answer: result.expected_answer, |
| 293 | + stdout: "''" |
| 294 | + }; |
| 295 | + return cb(null, [result, expected]); |
| 296 | + }); |
| 297 | +}; |
| 298 | + |
| 299 | +plugin.submitProblem = function(problem, cb) { |
| 300 | + log.debug('running lintcode.submitProblem'); |
| 301 | + runCode(problem, false, function(e, result) { |
| 302 | + if (e) return cb(e); |
| 303 | + return cb(null, [result]); |
| 304 | + }); |
| 305 | +}; |
| 306 | + |
| 307 | +plugin.getSubmission = function(submission, cb) { |
| 308 | + // FIXME |
| 309 | + return cb('Not implemented'); |
| 310 | +}; |
| 311 | + |
| 312 | +plugin.login = function(user, cb) { |
| 313 | + log.debug('running lintcode.login'); |
| 314 | + request(config.URL_LOGIN, function(e, resp, body) { |
| 315 | + e = checkError(e, resp, 200); |
| 316 | + if (e) return cb(e); |
| 317 | + |
| 318 | + user.loginCSRF = h.getSetCookieValue(resp, 'csrftoken'); |
| 319 | + |
| 320 | + var opts = { |
| 321 | + url: config.URL_LOGIN, |
| 322 | + headers: { |
| 323 | + Cookie: 'csrftoken=' + user.loginCSRF + ';' |
| 324 | + }, |
| 325 | + form: { |
| 326 | + csrfmiddlewaretoken: user.loginCSRF, |
| 327 | + username_or_email: user.login, |
| 328 | + password: user.pass |
| 329 | + } |
| 330 | + }; |
| 331 | + request.post(opts, function(e, resp, body) { |
| 332 | + if (e) return cb(e); |
| 333 | + if (resp.statusCode !== 302) return cb('invalid password?'); |
| 334 | + |
| 335 | + user.sessionCSRF = h.getSetCookieValue(resp, 'csrftoken'); |
| 336 | + user.sessionId = h.getSetCookieValue(resp, 'sessionid'); |
| 337 | + user.name = user.login; // FIXME |
| 338 | + |
| 339 | + return cb(null, user); |
| 340 | + }); |
| 341 | + }); |
| 342 | +}; |
| 343 | + |
| 344 | +module.exports = plugin; |
0 commit comments