diff --git a/lib/config.ts b/lib/config.ts index 782e831a1ac52e..8833e95cc86847 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -298,6 +298,7 @@ export type Config = { username?: string[]; password?: string[]; authenticationSecret?: string[]; + phoneOrEmail?: string[]; authToken?: string[]; }; uestc: { @@ -701,6 +702,7 @@ const calculateValue = () => { username: envs.TWITTER_USERNAME?.split(','), password: envs.TWITTER_PASSWORD?.split(','), authenticationSecret: envs.TWITTER_AUTHENTICATION_SECRET?.split(','), + phoneOrEmail: envs.TWITTER_PHONE_OR_EMAIL?.split(','), authToken: envs.TWITTER_AUTH_TOKEN?.split(','), }, uestc: { diff --git a/lib/routes/twitter/api/mobile-api/login.ts b/lib/routes/twitter/api/mobile-api/login.ts index ed231e5ec951ef..880ae0b7910a93 100644 --- a/lib/routes/twitter/api/mobile-api/login.ts +++ b/lib/routes/twitter/api/mobile-api/login.ts @@ -8,11 +8,11 @@ import { v5 as uuidv5 } from 'uuid'; import { authenticator } from 'otplib'; import logger from '@/utils/logger'; import cache from '@/utils/cache'; -import { RateLimiterMemory, RateLimiterRedis, RateLimiterQueue } from 'rate-limiter-flexible'; +import { RateLimiterMemory, RateLimiterQueue, RateLimiterRedis } from 'rate-limiter-flexible'; -const NAMESPACE = 'd41d092b-b007-48f7-9129-e9538d2d8fe9'; +const ENDPOINT = 'https://api.x.com/1.1/onboarding/task.json'; -let authentication = null; +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)', @@ -41,7 +41,65 @@ const loginLimiter = cache.clients.redisClient const loginLimiterQueue = new RateLimiterQueue(loginLimiter); -async function login({ username, password, authenticationSecret }) { +const postTask = async (flowToken: string, subtaskId: string, subtaskInput: Record) => + 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 () => { @@ -49,8 +107,8 @@ async function login({ username, password, authenticationSecret }) { await loginLimiterQueue.removeTokens(1); logger.debug('Twitter login start.'); - const android_id = uuidv5(username, NAMESPACE); - headers['X-Twitter-Client-DeviceID'] = android_id; + + headers['X-Twitter-Client-DeviceID'] = uuidv5(username, NAMESPACE); const ct0 = crypto.randomUUID().replaceAll('-', ''); const guestToken = await got(guestActivateUrl, { @@ -61,140 +119,80 @@ async function login({ username, password, authenticationSecret }) { }, method: 'POST', }); - logger.debug('Twitter login 1 finished: guest token.'); + logger.debug('Twitter login: guest token'); headers['x-guest-token'] = guestToken.data.guest_token; - const task1 = await ofetch.raw( - 'https://api.x.com/1.1/onboarding/task.json?' + - 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', + 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, }, - 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; } - ); - logger.debug('Twitter login 2 finished: login flow.'); - - headers.att = task1.headers.get('att'); - - const task2 = await got.post('https://api.x.com/1.1/onboarding/task.json', { - headers, - json: { - flow_token: task1._data.flow_token, - subtask_inputs: [ - { - enter_text: { - suggestion_id: null, - text: username, - link: 'next_link', - }, - subtask_id: 'LoginEnterUserIdentifier', - }, - ], - }, - }); - logger.debug('Twitter login 3 finished: LoginEnterUserIdentifier.'); - - const task3 = await got.post('https://api.x.com/1.1/onboarding/task.json', { - headers, - json: { - flow_token: task2.data.flow_token, - subtask_inputs: [ - { - enter_password: { - password, - link: 'next_link', - }, - subtask_id: 'LoginEnterPassword', - }, - ], - }, - }); - logger.debug('Twitter login 4 finished: LoginEnterPassword.'); - for (const subtask of task3.data?.subtasks || []) { - if (subtask.open_account) { - return subtask.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; } - } - const task4 = await got.post('https://api.x.com/1.1/onboarding/task.json', { - headers, - json: { - flow_token: task3.data.flow_token, - subtask_inputs: [ - { - check_logged_in_account: { - link: 'AccountDuplicationCheck_false', - }, - subtask_id: 'AccountDuplicationCheck', - }, - ], - }, - }); - logger.debug('Twitter login 5 finished: AccountDuplicationCheck.'); + task = await flowTasks[subtask_id]({ + flowToken: data.flow_token, + username, + password, + authenticationSecret, + phoneOrEmail, + }); + logger.debug(`Twitter login flow task finished: subtask: ${subtask_id}.`); - for await (const subtask of task4.data?.subtasks || []) { - if (subtask.open_account) { - authentication = subtask.open_account; - break; - } else if (authenticationSecret && subtask.subtask_id === 'LoginTwoFactorAuthChallenge') { - const token = authenticator.generate(authenticationSecret); + return await runTask(task); + }; + const authentication = await runTask(task); + logger.debug('Twitter login flow finished.'); - const task5 = await got.post('https://api.x.com/1.1/onboarding/task.json', { - headers, - json: { - flow_token: task4.data.flow_token, - subtask_inputs: [ - { - enter_text: { - suggestion_id: null, - text: token, - link: 'next_link', - }, - subtask_id: 'LoginTwoFactorAuthChallenge', - }, - ], - }, - }); - logger.debug('Twitter login 6 finished: LoginTwoFactorAuthChallenge.'); - - for (const subtask of task5.data?.subtasks || []) { - if (subtask.open_account) { - authentication = subtask.open_account; - break; - } - } - break; - } else { - logger.error(`Twitter login 6 failed: unknown subtask: ${subtask.subtask_id}`); - } - } if (authentication) { - logger.debug('Twitter login success.'); + logger.debug('Twitter login success.', authentication); } else { - logger.error(`Twitter login failed. ${JSON.stringify(task4.data?.subtasks, null, 2)}`); + logger.error(`Twitter login failed. ${JSON.stringify(task.data?.subtasks, null, 2)}`); } return authentication; diff --git a/lib/routes/twitter/api/mobile-api/token.ts b/lib/routes/twitter/api/mobile-api/token.ts index 8c8c727d69d901..51d6eba44fc644 100644 --- a/lib/routes/twitter/api/mobile-api/token.ts +++ b/lib/routes/twitter/api/mobile-api/token.ts @@ -11,11 +11,13 @@ async function getToken() { const username = config.twitter.username[index]; const password = config.twitter.password[index]; const authenticationSecret = config.twitter.authenticationSecret?.[index]; + const phoneOrEmail = config.twitter.phoneOrEmail?.[index]; if (username && password) { const authentication = await login({ username, password, authenticationSecret, + phoneOrEmail, }); if (!authentication) { throw new ConfigNotFoundError(`Invalid twitter configs: ${username}`);