UNPKG

rsshub

Version:
212 lines (189 loc) • 8.28 kB
// https://github.com/BANKA2017/twitter-monitor/blob/node/apps/open_account/scripts/login.mjs import { bearerToken, guestActivateUrl } from './constants'; import got from '@/utils/got'; import ofetch from '@/utils/ofetch'; import crypto from 'node:crypto'; import { v5 as uuidv5 } from 'uuid'; import { authenticator } from 'otplib'; import logger from '@/utils/logger'; import cache from '@/utils/cache'; import { RateLimiterMemory, RateLimiterQueue, RateLimiterRedis } from 'rate-limiter-flexible'; const ENDPOINT = 'https://api.x.com/1.1/onboarding/task.json'; const NAMESPACE = 'd41d092b-b007-48f7-9129-e9538d2d8fe9'; const headers = { 'User-Agent': 'TwitterAndroid/10.21.0-release.0 (310210000-r-0) ONEPLUS+A3010/9 (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)', 'X-Twitter-API-Version': '5', 'X-Twitter-Client': 'TwitterAndroid', 'X-Twitter-Client-Version': '10.21.0-release.0', 'OS-Version': '28', 'System-User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3010 Build/PKQ1.181203.001)', 'X-Twitter-Active-User': 'yes', 'Content-Type': 'application/json', Authorization: bearerToken, }; const loginLimiter = cache.clients.redisClient ? new RateLimiterRedis({ points: 1, duration: 20, execEvenly: true, storeClient: cache.clients.redisClient, }) : new RateLimiterMemory({ points: 1, duration: 20, execEvenly: true, }); const loginLimiterQueue = new RateLimiterQueue(loginLimiter); const postTask = async (flowToken: string, subtaskId: string, subtaskInput: Record<string, unknown>) => await got.post(ENDPOINT, { headers, json: { flow_token: flowToken, subtask_inputs: [Object.assign({ subtask_id: subtaskId }, subtaskInput)], }, }); // In the Twitter login flow, each task successfully requested will respond with a 'subtask_id' to determine what the next task is, and the execution sequence of the tasks is non-fixed. // So abstract these tasks out into a map so that they can be dynamically executed during the login flow. // If there are missing tasks in the future, simply add the implementation of that task to it. const flowTasks = { async LoginEnterUserIdentifier({ flowToken, username }) { return await postTask(flowToken, 'LoginEnterUserIdentifier', { enter_text: { suggestion_id: null, text: username, link: 'next_link', }, }); }, async LoginEnterPassword({ flowToken, password }) { return await postTask(flowToken, 'LoginEnterPassword', { enter_password: { password, link: 'next_link', }, }); }, async LoginEnterAlternateIdentifierSubtask({ flowToken, phoneOrEmail }) { return await postTask(flowToken, 'LoginEnterAlternateIdentifierSubtask', { enter_text: { suggestion_id: null, text: phoneOrEmail, link: 'next_link', }, }); }, async AccountDuplicationCheck({ flowToken }) { return await postTask(flowToken, 'AccountDuplicationCheck', { check_logged_in_account: { link: 'AccountDuplicationCheck_false', }, }); }, async LoginTwoFactorAuthChallenge({ flowToken, authenticationSecret }) { const token = authenticator.generate(authenticationSecret); return await postTask(flowToken, 'LoginTwoFactorAuthChallenge', { enter_text: { suggestion_id: null, text: token, link: 'next_link', }, }); }, }; async function login({ username, password, authenticationSecret, phoneOrEmail }) { return (await cache.tryGet( `twitter:authentication:${username}`, async () => { try { await loginLimiterQueue.removeTokens(1); logger.debug('Twitter login start.'); headers['X-Twitter-Client-DeviceID'] = uuidv5(username, NAMESPACE); const ct0 = crypto.randomUUID().replaceAll('-', ''); const guestToken = await got(guestActivateUrl, { headers: { authorization: bearerToken, 'x-csrf-token': ct0, cookie: 'ct0=' + ct0, }, method: 'POST', }); logger.debug('Twitter login: guest token'); headers['x-guest-token'] = guestToken.data.guest_token; let task = await ofetch .raw( ENDPOINT + '?' + new URLSearchParams({ flow_name: 'login', api_version: '1', known_device_token: '', sim_country_code: 'us', }).toString(), { method: 'POST', headers, body: { flow_token: null, input_flow_data: { country_code: null, flow_context: { referrer_context: { referral_details: 'utm_source=google-play&utm_medium=organic', referrer_url: '', }, start_location: { location: 'deeplink', }, }, requested_variant: null, target_user_id: 0, }, }, } ) .then(({ headers: _headers, _data }) => { headers.att = _headers.get('att'); return { data: _data }; }); logger.debug('Twitter login flow start.'); const runTask = async ({ data }) => { const { subtask_id, open_account } = data.subtasks.shift(); // If `open_account` exists (and 'subtask_id' is `LoginSuccessSubtask`), it means the login was successful. if (open_account) { return open_account; } // If task does not exist in `flowTasks`, we need to implement it. if (!(subtask_id in flowTasks)) { logger.error(`Twitter login flow task failed: unknown subtask: ${subtask_id}`); return; } task = await flowTasks[subtask_id]({ flowToken: data.flow_token, username, password, authenticationSecret, phoneOrEmail, }); logger.debug(`Twitter login flow task finished: subtask: ${subtask_id}.`); return await runTask(task); }; const authentication = await runTask(task); logger.debug('Twitter login flow finished.'); if (authentication) { logger.debug('Twitter login success.', authentication); } else { logger.error(`Twitter login failed. ${JSON.stringify(task.data?.subtasks, null, 2)}`); } return authentication; } catch (error) { logger.error(`Twitter username ${username} login failed:`, error); } }, 60 * 60 * 24 * 30, // 30 days false )) as { oauth_token: string; oauth_token_secret: string; } | null; } export default login;