Skip to content

Commit

Permalink
add third party github login for future updates in vscode-leetcode (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
yihong0618 authored and jdneo committed Dec 24, 2019
1 parent a5eb30b commit 1f26fbd
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 55 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
98 changes: 69 additions & 29 deletions lib/commands/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
};

Expand Down Expand Up @@ -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;
Expand All @@ -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?');
}
Expand Down
10 changes: 7 additions & 3 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/',
Expand Down Expand Up @@ -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;
Expand Down
131 changes: 115 additions & 16 deletions lib/plugins/leetcode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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';
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

0 comments on commit 1f26fbd

Please sign in to comment.