X Tutup
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;
X Tutup