leetcode-fetcher-cli
Version:
A CLi Application for local fetching of leetcode problems
246 lines (203 loc) • 8.32 kB
text/typescript
/**
* @author Riccardo La Marca
*
* @brief User-releated commands:
* - login [Login into leetcode to start a session]
* - inspect [Inspect the overall statistics of a user, or the current logged one]
*/
import { randomBytes } from 'crypto';
import { HTTPRequest, HTTPResponse } from 'puppeteer';
import { Spinner } from '../pprint';
import { HashPassword, OpenLoginBrowser } from '../utils/general';
import { FormatString, FormatCookies } from '../utils/formatter';
import { PrintUserSummary } from '../utils/printer';
import * as types from '../types';
import * as lc from '../leetcode';
import constants from '../constants';
import chalk from 'chalk';
const SetLoginDetails = (state: types.AppStateData, params: URLSearchParams, user_str: string, pwd_str: string) =>
{
let content: {[key: string]: string} = {};
for (const [key, value] of params.entries()) {
if ([user_str, pwd_str].includes(key)) {
content[key] = value;
}
}
// Creates the salt and hash the password
const salt = randomBytes(16).toString('hex');
const hash_pwd = HashPassword(content[pwd_str], salt);
state.userLogin ={username: content[user_str], password: hash_pwd, salt: salt};
state.selectedUser = content[user_str];
}
const HandleResponse = async (state: types.AppStateData, response: HTTPResponse) =>
{
// Classical login through LeetCode
if (response.url().startsWith(constants.SITES.LOGIN_PAGE.URL))
{
const request = response.request(); // Get the corresponding request
// We dont want the navigation request since most likely it would
// be the naviagation request we have done when opening the page.
// Moreover, we also need the response to be successful and the
// request to be a POST request, thus with POST data.
if (request.isNavigationRequest() || !request.hasPostData() || !response.ok()) return;
// Get the username and the password provided and compute the hash
const result = await response.json();
const username = result.form.fields.login.value;
const password = result.form.fields.password.value;
const salt = randomBytes(16).toString('hex');
const hash_pawd = HashPassword(password, salt);
state.userLogin = {username: username, password: hash_pawd, salt: salt};
state.selectedUser = username;
const headers = response.headers();
const matches = headers['set-cookie'].matchAll(/^([\w_]+)=([^;]+)/gm);
state.cookies = { csrftoken: "", messages: "", LEETCODE_SESSION: "" };
Array.from(matches).forEach((value: RegExpExecArray) => {
state.cookies![value[1] as keyof types.LeetcodeSessionCookies] = value[2];
});
return;
}
// Login using Github
if (response.url().startsWith(constants.SITES.THIRD_PARTY.GITHUB.SESSION)) {
const response_status_code = response.status();
const request = response.request();
// If the response is not a redirect or the request has no post data
if (response_status_code !== 302 || !request.hasPostData()) return;
const request_post_data = request.postData();
const params = new URLSearchParams(request_post_data);
// From the (key, value) pairs select those corresponding to username and password
SetLoginDetails(state, params, "login", "password");
return;
}
if (
(
response.url().startsWith(constants.SITES.THIRD_PARTY.GITHUB.CALLBACK)
|| response.url().startsWith(constants.SITES.THIRD_PARTY.LINKEDIN.CALLBACK)
)
) {
const headers = response.headers();
const matches = headers['set-cookie'].matchAll(/^([\w_]+)=([^;]+)/gm);
state.cookies = { csrftoken: "", messages: "", LEETCODE_SESSION: "" };
Array.from(matches).forEach((value: RegExpExecArray) => {
state.cookies![value[1] as keyof types.LeetcodeSessionCookies] = value[2];
});
return;
}
// Login using Linkedin
if (response.url().startsWith(constants.SITES.THIRD_PARTY.LINKEDIN.SESSION)) {
const response_status_code = response.status();
const request = response.request();
// If the response is not a redirect or the request has no post data
if (response_status_code !== 303 || !request.hasPostData()) return;
const request_post_data = request.postData();
const params = new URLSearchParams(request_post_data);
SetLoginDetails(state, params, "session_key", "session_password");
return;
}
}
const HandleRequest = async (request: HTTPRequest) =>
{
if (request.isInterceptResolutionHandled()) return;
if (request.url() !== constants.SITES.HOME_PAGE.URL) {
request.continue();
return;
}
request.respond(constants.SITES.HOME_PAGE.REDIRECT);
}
const CheckUserSession = async (spinner: Spinner, state: types.AppStateData)
: Promise<void> =>
{
if (state.userLogin !== undefined && state.cookies !== undefined) {
spinner.changeMessage("Session found. Checking validity");
spinner.start();
const result = await lc.CheckUserSession(state.cookies);
spinner.stop();
if (result) {
console.log(FormatString("\rThe current {0} is still {1}!",
chalk.bold("session"), chalk.greenBright("active")
));
} else {
state.userLogin = undefined;
state.cookies = undefined;
}
}
}
const LoginCommand = async (data: string[], state: types.AppStateData)
: Promise<types.AppStateData> =>
{
const spinner = new Spinner("User Logging in");
const forced = data[0] !== undefined;
if (!forced) await CheckUserSession(spinner, state);
else {
console.warn(chalk.yellowBright("[WARNING] Login has been forced. No check on current session."));
}
if (!(state.userLogin && state.cookies) || forced) {
spinner.changeMessage("User Logging in");
spinner.start();
try {
await OpenLoginBrowser(
(r: HTTPResponse) => HandleResponse(state, r),
HandleRequest
);
} catch (error) {
}
spinner.stop();
}
if (!(state.userLogin && state.cookies)) {
console.log(chalk.red('User login has been stopped.'));
state.userLogin = undefined;
state.cookies = undefined;
return state;
}
let headers = FormatCookies(state.cookies);
const username = await lc.FetchUsername(headers);
if (!username) {
console.error(chalk.redBright(`User ${state.userLogin.username} does not have access.`));
state.userLogin = undefined;
state.cookies = undefined;
return state;
}
state.userLogin.username = username!;
const success = chalk.greenBright("successfully");
console.log(`User ${username} has ${success} logged.`);
// Fetch user details
const user_data = await lc.GetUserData(state.userLogin.username!, state);
state.profile = user_data;
if (user_data) PrintUserSummary(user_data);
return state;
}
// Login command - Login into leetcode to start a session
export const login_command: types.AppCommandData = {
group : 'User',
name : 'Login Command',
command : 'login',
syntax : /^login(?:\s+(force))?$/,
callback : LoginCommand,
help: 'login [force] - Login into leetcode to start a session. `force` parameter\n' +
'forces the application to attempt the login instead of checking the\n' +
'current session validity (if any).'
};
const InspectCommand = async (data: string[], state: types.AppStateData)
: Promise<types.AppStateData> =>
{
if (data.length < 1) {
// Check if a user is logged
if (!state.selectedUser) {
console.error(chalk.redBright("No current logged user. A username must be provided."));
return state;
}
data = [state.selectedUser];
}
const result = await lc.GetUserData(data[0], state);
if (result) PrintUserSummary(result);
return state;
}
// Inspect command - Inspect a user
export const inspect_command: types.AppCommandData = {
group : 'User',
name : 'Inspect Command',
command : 'inspect',
syntax : /^inspect(?:\s(.*?))$/,
callback : InspectCommand,
help: 'inspect [USERNAME] - Inspect a given username if it exists' +
', or the current logged one.\n'
};