Skip to content

Commit

Permalink
feat(twitter): support LoginEnterAlternateIdentifierSubtask task and …
Browse files Browse the repository at this point in the history
…better login flow controll (#17331)

* feat(twitter): support LoginEnterAlternateIdentifierSubtask task and better login flow controll

* chore: lint

* chore: lint

* refactor(twitter): login flow

* fix: var name
  • Loading branch information
ericyzhu authored Oct 29, 2024
1 parent 6d110e0 commit b60ef4d
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 127 deletions.
2 changes: 2 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export type Config = {
username?: string[];
password?: string[];
authenticationSecret?: string[];
phoneOrEmail?: string[];
authToken?: string[];
};
uestc: {
Expand Down Expand Up @@ -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: {
Expand Down
252 changes: 125 additions & 127 deletions lib/routes/twitter/api/mobile-api/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down Expand Up @@ -41,16 +41,74 @@ 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<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.');
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, {
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions lib/routes/twitter/api/mobile-api/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down

0 comments on commit b60ef4d

Please sign in to comment.