Skip to content

Commit 1f26fbd

Browse files
yihong0618jdneo
authored andcommittedDec 24, 2019
add third party github login for future updates in vscode-leetcode (#34)
1 parent a5eb30b commit 1f26fbd

File tree

4 files changed

+200
-55
lines changed

4 files changed

+200
-55
lines changed
 

‎README.md

+9-7
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ Great thanks to leetcode.com, a really awesome website!
3838

3939
## Quick Start
4040

41-
Read help first $ leetcode help
42-
Login with your leetcode account $ leetcode user -l
43-
Cookie login with cookie $ leetcode user -c
44-
Browse all questions $ leetcode list
45-
Choose one question $ leetcode show 1 -g -l cpp
41+
Read help first $ leetcode help
42+
Login with your leetcode account $ leetcode user -l
43+
Login with third party account--GitHub $ leetcode user -g
44+
Login with third party account--LinkedIn $ leetcode user -i
45+
Cookie login with cookie $ leetcode user -c
46+
Browse all questions $ leetcode list
47+
Choose one question $ leetcode show 1 -g -l cpp
4648
Coding it!
47-
Run test(s) and pray... $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7'
48-
Submit final solution! $ leetcode submit ./two-sum.cpp
49+
Run test(s) and pray... $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7'
50+
Submit final solution! $ leetcode submit ./two-sum.cpp

‎lib/commands/user.js

+69-29
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,42 @@ const cmd = {
1515
desc: 'Manage account',
1616
builder: function(yargs) {
1717
return yargs
18-
.option('l', {
19-
alias: 'login',
20-
type: 'boolean',
21-
default: false,
22-
describe: 'Login'
23-
})
24-
.option('c', {
25-
alias: 'cookie',
26-
type: 'boolean',
27-
default: false,
28-
describe: 'cookieLogin'
29-
})
30-
.option('L', {
31-
alias: 'logout',
32-
type: 'boolean',
33-
default: false,
34-
describe: 'Logout'
35-
})
36-
.example(chalk.yellow('leetcode user'), 'Show current user')
37-
.example(chalk.yellow('leetcode user -l'), 'User login')
38-
.example(chalk.yellow('leetcode user -c'), 'User Cookie login')
39-
.example(chalk.yellow('leetcode user -L'), 'User logout');
18+
.option('l', {
19+
alias: 'login',
20+
type: 'boolean',
21+
default: false,
22+
describe: 'Login'
23+
})
24+
.option('c', {
25+
alias: 'cookie',
26+
type: 'boolean',
27+
default: false,
28+
describe: 'cookieLogin'
29+
})
30+
.option('g', {
31+
alias: 'github',
32+
type: 'boolean',
33+
default: false,
34+
describe: 'githubLogin'
35+
})
36+
.option('i', {
37+
alias: 'linkedin',
38+
type: 'boolean',
39+
default: false,
40+
describe: 'linkedinLogin'
41+
})
42+
.option('L', {
43+
alias: 'logout',
44+
type: 'boolean',
45+
default: false,
46+
describe: 'Logout'
47+
})
48+
.example(chalk.yellow('leetcode user'), 'Show current user')
49+
.example(chalk.yellow('leetcode user -l'), 'User login')
50+
.example(chalk.yellow('leetcode user -c'), 'User Cookie login')
51+
.example(chalk.yellow('leetcode user -g'), 'User GitHub login')
52+
.example(chalk.yellow('leetcode user -i'), 'User LinkedIn login')
53+
.example(chalk.yellow('leetcode user -L'), 'User logout');
4054
}
4155
};
4256

@@ -66,6 +80,32 @@ cmd.handler = function(argv) {
6680
log.info('Successfully logout as', chalk.yellow(user.name));
6781
else
6882
log.fail('You are not login yet?');
83+
// third parties
84+
} else if (argv.github || argv.linkedin) {
85+
// add future third parties here
86+
const functionMap = new Map(
87+
[
88+
['g', core.githubLogin],
89+
['github', core.githubLogin],
90+
['i', core.linkedinLogin],
91+
['linkedin', core.linkedinLogin],
92+
]
93+
);
94+
const keyword = Object.entries(argv).filter((i) => (i[1] === true))[0][0];
95+
const coreFunction = functionMap.get(keyword);
96+
prompt.colors = false;
97+
prompt.message = '';
98+
prompt.start();
99+
prompt.get([
100+
{name: 'login', required: true},
101+
{name: 'pass', required: true, hidden: true}
102+
], function(e, user) {
103+
if (e) return log.fail(e);
104+
coreFunction(user, function(e, user) {
105+
if (e) return log.fail(e);
106+
log.info('Successfully third party login as', chalk.yellow(user.name));
107+
});
108+
});
69109
} else if (argv.cookie) {
70110
// session
71111
prompt.colors = false;
@@ -75,22 +115,22 @@ cmd.handler = function(argv) {
75115
{name: 'login', required: true},
76116
{name: 'cookie', required: true}
77117
], function(e, user) {
78-
if (e) return log.fail(e)
79-
core.cookieLogin(user, function(e, user) {
80118
if (e) return log.fail(e);
81-
log.info('Successfully cookie login as', chalk.yellow(user.name));
119+
core.cookieLogin(user, function(e, user) {
120+
if (e) return log.fail(e);
121+
log.info('Successfully cookie login as', chalk.yellow(user.name));
82122
});
83123
});
84-
} else {
124+
} else {
85125
// show current user
86126
user = session.getUser();
87127
if (user) {
88128
log.info(chalk.gray(sprintf(' %-9s %-20s %s', 'Premium', 'User', 'Host')));
89129
log.info(chalk.gray('-'.repeat(60)));
90130
log.printf(' %s %-20s %s',
91-
h.prettyText('', user.paid || false),
92-
chalk.yellow(user.name),
93-
config.sys.urls.base);
131+
h.prettyText('', user.paid || false),
132+
chalk.yellow(user.name),
133+
config.sys.urls.base);
94134
} else
95135
return log.fail('You are not login yet?');
96136
}

‎lib/config.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const DEFAULT_CONFIG = {
3434
base: 'https://leetcode.com',
3535
graphql: 'https://leetcode.com/graphql',
3636
login: 'https://leetcode.com/accounts/login/',
37+
// third part login base urls. TODO facebook google
38+
github_login: 'https://leetcode.com/accounts/github/login/?next=%2F',
39+
facebook_login: 'https://leetcode.com/accounts/facebook/login/?next=%2F',
40+
linkedin_login: 'https://leetcode.com/accounts/linkedin_oauth2/login/?next=%2F',
3741
problems: 'https://leetcode.com/api/problems/$category/',
3842
problem: 'https://leetcode.com/problems/$slug/description/',
3943
test: 'https://leetcode.com/problems/$slug/interpret_solution/',
@@ -79,15 +83,15 @@ function Config() {}
7983

8084
Config.prototype.init = function() {
8185
nconf.file('local', file.configFile())
82-
.add('global', {type: 'literal', store: DEFAULT_CONFIG})
83-
.defaults({});
86+
.add('global', {type: 'literal', store: DEFAULT_CONFIG})
87+
.defaults({});
8488

8589
const cfg = nconf.get();
8690
nconf.remove('local');
8791
nconf.remove('global');
8892

8993
// HACK: remove old style configs
90-
for (let x in cfg) {
94+
for (const x in cfg) {
9195
if (x === x.toUpperCase()) delete cfg[x];
9296
}
9397
delete DEFAULT_CONFIG.type;

‎lib/plugins/leetcode.js

+115-16
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ plugin.checkError = function(e, resp, expectedStatus) {
5151

5252
plugin.init = function() {
5353
config.app = 'leetcode';
54-
}
54+
};
5555

5656
plugin.getProblems = function(cb) {
5757
log.debug('running leetcode.getProblems');
@@ -95,7 +95,7 @@ plugin.getCategoryProblems = function(category, cb) {
9595
}
9696

9797
const problems = json.stat_status_pairs
98-
.filter(p => !p.stat.question__hide)
98+
.filter((p) => !p.stat.question__hide)
9999
.map(function(p) {
100100
return {
101101
state: p.status || 'None',
@@ -167,7 +167,7 @@ plugin.getProblem = function(problem, cb) {
167167
problem.testable = q.enableRunCode;
168168
problem.templateMeta = JSON.parse(q.metaData);
169169
// @si-yao: seems below property is never used.
170-
//problem.discuss = q.discussCategoryId;
170+
// problem.discuss = q.discussCategoryId;
171171

172172
return cb(null, problem);
173173
});
@@ -254,9 +254,9 @@ function formatResult(result) {
254254
};
255255

256256
x.error = _.chain(result)
257-
.pick((v, k) => /_error$/.test(k) && v.length > 0)
258-
.values()
259-
.value();
257+
.pick((v, k) => /_error$/.test(k) && v.length > 0)
258+
.values()
259+
.value();
260260

261261
if (/[runcode|interpret].*/.test(result.submission_id)) {
262262
// It's testing
@@ -374,8 +374,8 @@ plugin.starProblem = function(problem, starred, cb) {
374374
};
375375
} else {
376376
opts.url = config.sys.urls.favorite_delete
377-
.replace('$hash', user.hash)
378-
.replace('$id', problem.id);
377+
.replace('$hash', user.hash)
378+
.replace('$id', problem.id);
379379
opts.method = 'DELETE';
380380
}
381381

@@ -508,7 +508,7 @@ plugin.signin = function(user, cb) {
508508
plugin.getUser = function(user, cb) {
509509
plugin.getFavorites(function(e, favorites) {
510510
if (!e) {
511-
const f = favorites.favorites.private_favorites.find(f => f.name === 'Favorite');
511+
const f = favorites.favorites.private_favorites.find((f) => f.name === 'Favorite');
512512
if (f) {
513513
user.hash = f.id_hash;
514514
user.name = favorites.user_name;
@@ -538,19 +538,118 @@ plugin.login = function(user, cb) {
538538
});
539539
};
540540

541-
plugin.cookieLogin = function(user, cb) {
542-
// re pattern for cookie chrome or firefox
541+
function parseCookie(cookie, cb) {
543542
const SessionPattern = /LEETCODE_SESSION=(.+?)(;|$)/;
544543
const csrfPattern = /csrftoken=(.+?)(;|$)/;
545-
const reSessionResult = SessionPattern.exec(user.cookie);
546-
const reCsrfResult = csrfPattern.exec(user.cookie);
544+
const reSessionResult = SessionPattern.exec(cookie);
545+
const reCsrfResult = csrfPattern.exec(cookie);
547546
if (reSessionResult === null || reCsrfResult === null) {
548-
return cb('invalid cookie?')
547+
return cb('invalid cookie?');
549548
}
550-
user.sessionId = reSessionResult[1];
551-
user.sessionCSRF = reCsrfResult[1];
549+
return {
550+
sessionId: reSessionResult[1],
551+
sessionCSRF: reCsrfResult[1],
552+
};
553+
}
554+
555+
function saveAndGetUser(user, cb, cookieData) {
556+
user.sessionId = cookieData.sessionId;
557+
user.sessionCSRF = cookieData.sessionCSRF;
552558
session.saveUser(user);
553559
plugin.getUser(user, cb);
554560
}
555561

562+
plugin.cookieLogin = function(user, cb) {
563+
const cookieData = parseCookie(user.cookie, cb);
564+
user.sessionId = cookieData.sessionId;
565+
user.sessionCSRF = cookieData.sessionCSRF;
566+
session.saveUser(user);
567+
plugin.getUser(user, cb);
568+
};
569+
570+
plugin.githubLogin = function(user, cb) {
571+
const leetcodeUrl = config.sys.urls.github_login;
572+
const _request = request.defaults({jar: true});
573+
_request('https://github.com/login', function(e, resp, body) {
574+
const authenticityToken = body.match(/name="authenticity_token" value="(.*?)"/);
575+
if (authenticityToken === null) {
576+
return cb('Get GitHub token failed');
577+
}
578+
const options = {
579+
url: 'https://github.com/session',
580+
method: 'POST',
581+
headers: {
582+
'Content-Type': 'application/x-www-form-urlencoded',
583+
},
584+
followAllRedirects: true,
585+
form: {
586+
'login': user.login,
587+
'password': user.pass,
588+
'authenticity_token': authenticityToken[1],
589+
'utf8': encodeURIComponent('✓'),
590+
'commit': encodeURIComponent('Sign in')
591+
},
592+
};
593+
_request(options, function(e, resp, body) {
594+
if (resp.statusCode !== 200) {
595+
return cb('GitHub login failed');
596+
}
597+
_request.get({url: leetcodeUrl}, function(e, resp, body) {
598+
const redirectUri = resp.request.uri.href;
599+
if (redirectUri !== 'https://leetcode.com/') {
600+
return cb('GitHub login failed or GitHub did not link to LeetCode');
601+
}
602+
const cookieData = parseCookie(resp.request.headers.cookie, cb);
603+
saveAndGetUser(user, cb, cookieData);
604+
});
605+
});
606+
});
607+
};
608+
609+
plugin.linkedinLogin = function(user, cb) {
610+
const leetcodeUrl = config.sys.urls.linkedin_login;
611+
const _request = request.defaults({
612+
jar: true,
613+
headers: {
614+
'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'
615+
}
616+
});
617+
_request('https://www.linkedin.com', function(e, resp, body) {
618+
if ( resp.statusCode !== 200) {
619+
return cb('Get LinkedIn session failed');
620+
}
621+
const authenticityToken = body.match(/input name="loginCsrfParam" value="(.*)" /);
622+
if (authenticityToken === null) {
623+
return cb('Get LinkedIn token failed');
624+
}
625+
const options = {
626+
url: 'https://www.linkedin.com/uas/login-submit',
627+
method: 'POST',
628+
headers: {
629+
'Content-Type': 'application/x-www-form-urlencoded',
630+
},
631+
followAllRedirects: true,
632+
form: {
633+
'session_key': user.login,
634+
'session_password': user.pass,
635+
'loginCsrfParam': authenticityToken[1],
636+
'trk': 'guest_homepage-basic_sign-in-submit'
637+
},
638+
};
639+
_request(options, function(e, resp, body) {
640+
if (resp.statusCode !== 200) {
641+
return cb('LinkedIn login failed');
642+
}
643+
_request.get({url: leetcodeUrl}, function(e, resp, body) {
644+
const redirectUri = resp.request.uri.href;
645+
if (redirectUri !== 'https://leetcode.com/') {
646+
return cb('LinkedIn login failed or LinkedIn did not link to LeetCode');
647+
}
648+
const cookieData = parseCookie(resp.request.headers.cookie, cb);
649+
saveAndGetUser(user, cb, cookieData);
650+
});
651+
});
652+
});
653+
};
654+
556655
module.exports = plugin;

0 commit comments

Comments
 (0)
Please sign in to comment.