diff --git a/README.md b/README.md index 3b9f4680..686755c9 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,13 @@ Great thanks to leetcode.com, a really awesome website! ## Quick Start - Read help first $ leetcode help - Login with your leetcode account $ leetcode user -l - Cookie login with cookie $ leetcode user -c - Browse all questions $ leetcode list - Choose one question $ leetcode show 1 -g -l cpp + Read help first $ leetcode help + Login with your leetcode account $ leetcode user -l + Login with third party account--GitHub $ leetcode user -g + Login with third party account--LinkedIn $ leetcode user -i + Cookie login with cookie $ leetcode user -c + Browse all questions $ leetcode list + Choose one question $ leetcode show 1 -g -l cpp Coding it! - Run test(s) and pray... $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7' - Submit final solution! $ leetcode submit ./two-sum.cpp + Run test(s) and pray... $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7' + Submit final solution! $ leetcode submit ./two-sum.cpp diff --git a/lib/commands/user.js b/lib/commands/user.js index f5ae86b7..a2534202 100644 --- a/lib/commands/user.js +++ b/lib/commands/user.js @@ -15,28 +15,42 @@ const cmd = { desc: 'Manage account', builder: function(yargs) { return yargs - .option('l', { - alias: 'login', - type: 'boolean', - default: false, - describe: 'Login' - }) - .option('c', { - alias: 'cookie', - type: 'boolean', - default: false, - describe: 'cookieLogin' - }) - .option('L', { - alias: 'logout', - type: 'boolean', - default: false, - describe: 'Logout' - }) - .example(chalk.yellow('leetcode user'), 'Show current user') - .example(chalk.yellow('leetcode user -l'), 'User login') - .example(chalk.yellow('leetcode user -c'), 'User Cookie login') - .example(chalk.yellow('leetcode user -L'), 'User logout'); + .option('l', { + alias: 'login', + type: 'boolean', + default: false, + describe: 'Login' + }) + .option('c', { + alias: 'cookie', + type: 'boolean', + default: false, + describe: 'cookieLogin' + }) + .option('g', { + alias: 'github', + type: 'boolean', + default: false, + describe: 'githubLogin' + }) + .option('i', { + alias: 'linkedin', + type: 'boolean', + default: false, + describe: 'linkedinLogin' + }) + .option('L', { + alias: 'logout', + type: 'boolean', + default: false, + describe: 'Logout' + }) + .example(chalk.yellow('leetcode user'), 'Show current user') + .example(chalk.yellow('leetcode user -l'), 'User login') + .example(chalk.yellow('leetcode user -c'), 'User Cookie login') + .example(chalk.yellow('leetcode user -g'), 'User GitHub login') + .example(chalk.yellow('leetcode user -i'), 'User LinkedIn login') + .example(chalk.yellow('leetcode user -L'), 'User logout'); } }; @@ -66,6 +80,32 @@ cmd.handler = function(argv) { log.info('Successfully logout as', chalk.yellow(user.name)); else log.fail('You are not login yet?'); + // third parties + } else if (argv.github || argv.linkedin) { + // add future third parties here + const functionMap = new Map( + [ + ['g', core.githubLogin], + ['github', core.githubLogin], + ['i', core.linkedinLogin], + ['linkedin', core.linkedinLogin], + ] + ); + const keyword = Object.entries(argv).filter((i) => (i[1] === true))[0][0]; + const coreFunction = functionMap.get(keyword); + prompt.colors = false; + prompt.message = ''; + prompt.start(); + prompt.get([ + {name: 'login', required: true}, + {name: 'pass', required: true, hidden: true} + ], function(e, user) { + if (e) return log.fail(e); + coreFunction(user, function(e, user) { + if (e) return log.fail(e); + log.info('Successfully third party login as', chalk.yellow(user.name)); + }); + }); } else if (argv.cookie) { // session prompt.colors = false; @@ -75,22 +115,22 @@ cmd.handler = function(argv) { {name: 'login', required: true}, {name: 'cookie', required: true} ], function(e, user) { - if (e) return log.fail(e) - core.cookieLogin(user, function(e, user) { if (e) return log.fail(e); - log.info('Successfully cookie login as', chalk.yellow(user.name)); + core.cookieLogin(user, function(e, user) { + if (e) return log.fail(e); + log.info('Successfully cookie login as', chalk.yellow(user.name)); }); }); - } else { + } else { // show current user user = session.getUser(); if (user) { log.info(chalk.gray(sprintf(' %-9s %-20s %s', 'Premium', 'User', 'Host'))); log.info(chalk.gray('-'.repeat(60))); log.printf(' %s %-20s %s', - h.prettyText('', user.paid || false), - chalk.yellow(user.name), - config.sys.urls.base); + h.prettyText('', user.paid || false), + chalk.yellow(user.name), + config.sys.urls.base); } else return log.fail('You are not login yet?'); } diff --git a/lib/config.js b/lib/config.js index 9708edfa..049d20d4 100644 --- a/lib/config.js +++ b/lib/config.js @@ -34,6 +34,10 @@ const DEFAULT_CONFIG = { base: 'https://leetcode.com', graphql: 'https://leetcode.com/graphql', login: 'https://leetcode.com/accounts/login/', + // third part login base urls. TODO facebook google + github_login: 'https://leetcode.com/accounts/github/login/?next=%2F', + facebook_login: 'https://leetcode.com/accounts/facebook/login/?next=%2F', + linkedin_login: 'https://leetcode.com/accounts/linkedin_oauth2/login/?next=%2F', problems: 'https://leetcode.com/api/problems/$category/', problem: 'https://leetcode.com/problems/$slug/description/', test: 'https://leetcode.com/problems/$slug/interpret_solution/', @@ -79,15 +83,15 @@ function Config() {} Config.prototype.init = function() { nconf.file('local', file.configFile()) - .add('global', {type: 'literal', store: DEFAULT_CONFIG}) - .defaults({}); + .add('global', {type: 'literal', store: DEFAULT_CONFIG}) + .defaults({}); const cfg = nconf.get(); nconf.remove('local'); nconf.remove('global'); // HACK: remove old style configs - for (let x in cfg) { + for (const x in cfg) { if (x === x.toUpperCase()) delete cfg[x]; } delete DEFAULT_CONFIG.type; diff --git a/lib/plugins/leetcode.js b/lib/plugins/leetcode.js index 924b40d7..004a6034 100644 --- a/lib/plugins/leetcode.js +++ b/lib/plugins/leetcode.js @@ -51,7 +51,7 @@ plugin.checkError = function(e, resp, expectedStatus) { plugin.init = function() { config.app = 'leetcode'; -} +}; plugin.getProblems = function(cb) { log.debug('running leetcode.getProblems'); @@ -95,7 +95,7 @@ plugin.getCategoryProblems = function(category, cb) { } const problems = json.stat_status_pairs - .filter(p => !p.stat.question__hide) + .filter((p) => !p.stat.question__hide) .map(function(p) { return { state: p.status || 'None', @@ -167,7 +167,7 @@ plugin.getProblem = function(problem, cb) { problem.testable = q.enableRunCode; problem.templateMeta = JSON.parse(q.metaData); // @si-yao: seems below property is never used. - //problem.discuss = q.discussCategoryId; + // problem.discuss = q.discussCategoryId; return cb(null, problem); }); @@ -254,9 +254,9 @@ function formatResult(result) { }; x.error = _.chain(result) - .pick((v, k) => /_error$/.test(k) && v.length > 0) - .values() - .value(); + .pick((v, k) => /_error$/.test(k) && v.length > 0) + .values() + .value(); if (/[runcode|interpret].*/.test(result.submission_id)) { // It's testing @@ -374,8 +374,8 @@ plugin.starProblem = function(problem, starred, cb) { }; } else { opts.url = config.sys.urls.favorite_delete - .replace('$hash', user.hash) - .replace('$id', problem.id); + .replace('$hash', user.hash) + .replace('$id', problem.id); opts.method = 'DELETE'; } @@ -508,7 +508,7 @@ plugin.signin = function(user, cb) { plugin.getUser = function(user, cb) { plugin.getFavorites(function(e, favorites) { if (!e) { - const f = favorites.favorites.private_favorites.find(f => f.name === 'Favorite'); + const f = favorites.favorites.private_favorites.find((f) => f.name === 'Favorite'); if (f) { user.hash = f.id_hash; user.name = favorites.user_name; @@ -538,19 +538,118 @@ plugin.login = function(user, cb) { }); }; -plugin.cookieLogin = function(user, cb) { - // re pattern for cookie chrome or firefox +function parseCookie(cookie, cb) { const SessionPattern = /LEETCODE_SESSION=(.+?)(;|$)/; const csrfPattern = /csrftoken=(.+?)(;|$)/; - const reSessionResult = SessionPattern.exec(user.cookie); - const reCsrfResult = csrfPattern.exec(user.cookie); + const reSessionResult = SessionPattern.exec(cookie); + const reCsrfResult = csrfPattern.exec(cookie); if (reSessionResult === null || reCsrfResult === null) { - return cb('invalid cookie?') + return cb('invalid cookie?'); } - user.sessionId = reSessionResult[1]; - user.sessionCSRF = reCsrfResult[1]; + return { + sessionId: reSessionResult[1], + sessionCSRF: reCsrfResult[1], + }; +} + +function saveAndGetUser(user, cb, cookieData) { + user.sessionId = cookieData.sessionId; + user.sessionCSRF = cookieData.sessionCSRF; session.saveUser(user); plugin.getUser(user, cb); } +plugin.cookieLogin = function(user, cb) { + const cookieData = parseCookie(user.cookie, cb); + user.sessionId = cookieData.sessionId; + user.sessionCSRF = cookieData.sessionCSRF; + session.saveUser(user); + plugin.getUser(user, cb); +}; + +plugin.githubLogin = function(user, cb) { + const leetcodeUrl = config.sys.urls.github_login; + const _request = request.defaults({jar: true}); + _request('https://github.com/login', function(e, resp, body) { + const authenticityToken = body.match(/name="authenticity_token" value="(.*?)"/); + if (authenticityToken === null) { + return cb('Get GitHub token failed'); + } + const options = { + url: 'https://github.com/session', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followAllRedirects: true, + form: { + 'login': user.login, + 'password': user.pass, + 'authenticity_token': authenticityToken[1], + 'utf8': encodeURIComponent('✓'), + 'commit': encodeURIComponent('Sign in') + }, + }; + _request(options, function(e, resp, body) { + if (resp.statusCode !== 200) { + return cb('GitHub login failed'); + } + _request.get({url: leetcodeUrl}, function(e, resp, body) { + const redirectUri = resp.request.uri.href; + if (redirectUri !== 'https://leetcode.com/') { + return cb('GitHub login failed or GitHub did not link to LeetCode'); + } + const cookieData = parseCookie(resp.request.headers.cookie, cb); + saveAndGetUser(user, cb, cookieData); + }); + }); + }); +}; + +plugin.linkedinLogin = function(user, cb) { + const leetcodeUrl = config.sys.urls.linkedin_login; + const _request = request.defaults({ + jar: true, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36' + } + }); + _request('https://www.linkedin.com', function(e, resp, body) { + if ( resp.statusCode !== 200) { + return cb('Get LinkedIn session failed'); + } + const authenticityToken = body.match(/input name="loginCsrfParam" value="(.*)" /); + if (authenticityToken === null) { + return cb('Get LinkedIn token failed'); + } + const options = { + url: 'https://www.linkedin.com/uas/login-submit', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followAllRedirects: true, + form: { + 'session_key': user.login, + 'session_password': user.pass, + 'loginCsrfParam': authenticityToken[1], + 'trk': 'guest_homepage-basic_sign-in-submit' + }, + }; + _request(options, function(e, resp, body) { + if (resp.statusCode !== 200) { + return cb('LinkedIn login failed'); + } + _request.get({url: leetcodeUrl}, function(e, resp, body) { + const redirectUri = resp.request.uri.href; + if (redirectUri !== 'https://leetcode.com/') { + return cb('LinkedIn login failed or LinkedIn did not link to LeetCode'); + } + const cookieData = parseCookie(resp.request.headers.cookie, cb); + saveAndGetUser(user, cb, cookieData); + }); + }); + }); +}; + module.exports = plugin;