Information freedom does not naturally evolve, it degrades.
—— Open Information Manifesto
Twitter decided in August to fully restrict public access and API interfaces, causing third-party integrations to no longer function properly. Open user data has been hijacked into a private profit-making tool, and the once benchmark of the Open Web, Twitter, has fallen to such a state. Digital slavery appears in the most unexpected places, which is disheartening. This has led many users to migrate to the Fediverse, but once social relationships and habits are formed, it is not easy to change them quickly. More people still choose to endure, and Musk is aware of this, which gives him confidence.
However, we cannot arbitrarily say that Twitter is closed, after all, it still opened a corporate API with a starting price of $40,000 per month, with no upper limit.
What? You say you can't afford it?
Then you can create ten thousand accounts like I did to bypass the restrictions.
Although Twitter has restricted all public access, we found that the newly downloaded Twitter mobile client can still view user dynamics normally. This provides us with a potential method of utilization. By capturing packets, we can see that the client creates a low-permission, strictly rate-limited temporary account by requesting a series of special interfaces. We can use this account to obtain most of the data we need. However, this type of account has very strict limits on request frequency, so a large number of such accounts are needed to meet basic usage needs. At the same time, each IP can only obtain one temporary account within a certain period, so we also need a large number of IP proxies.
For specific unpacking and packet capturing processes, you can refer to BANKA's “How to Crawl Twitter (Android)”. Standing on BANKA's shoulders, we can write a registration script like this (from Nitter - Guest Account Branch Deployment):
#!/bin/bash
guest_token=$(curl -s -XPOST https://api.twitter.com/1.1/guest/activate.json -H 'Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F' | jq -r '.guest_token')
flow_token=$(curl -s -XPOST 'https://api.twitter.com/1.1/onboarding/task.json?flow_name=welcome' \
-H 'Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F' \
-H 'Content-Type: application/json' \
-H "User-Agent: TwitterAndroid/10.10.0" \
-H "X-Guest-Token: ${guest_token}" \
-d '{"flow_token":null,"input_flow_data":{"flow_context":{"start_location":{"location":"splash_screen"}}}}' | jq -r .flow_token)
curl -s -XPOST 'https://api.twitter.com/1.1/onboarding/task.json' \
-H 'Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F' \
-H 'Content-Type: application/json' \
-H "User-Agent: TwitterAndroid/10.10.0" \
-H "X-Guest-Token: ${guest_token}" \
-d "{\"flow_token\":\"${flow_token}\",\"subtask_inputs\":[{\"open_link\":{\"link\":\"next_link\"},\"subtask_id\":\"NextTaskOpenLink\"}]}" | jq -c -r '.subtasks[0]|if(.open_account) then {oauth_token: .open_account.oauth_token, oauth_token_secret: .open_account.oauth_token_secret} else empty end'
And a batch registration script like this (from myself):
const got = require('got');
const { HttpsProxyAgent } = require('https-proxy-agent');
const fs = require('fs');
const path = require('path');
const concurrency = 5; // Please do not set it too large to avoid Twitter discovering our little secret
const proxyUrl = ''; // Add your proxy here
const baseURL = 'https://api.twitter.com/1.1/';
const headers = {
Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F',
'User-Agent': 'TwitterAndroid/10.10.0',
};
const accounts = [];
function generateOne() {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
const timeout = setTimeout(() => {
// eslint-disable-next-line no-console
console.log(`Failed to generate account, continue... timeout`);
resolve();
}, 30000);
const agent = {
https: proxyUrl && new HttpsProxyAgent(proxyUrl),
};
try {
const response = await got.post(`${baseURL}guest/activate.json`, {
headers: {
Authorization: headers.Authorization,
},
agent,
timeout: {
request: 20000,
},
});
const guestToken = JSON.parse(response.body).guest_token;
const flowResponse = await got.post(`${baseURL}onboarding/task.json?flow_name=welcome`, {
json: {
flow_token: null,
input_flow_data: {
flow_context: {
start_location: {
location: 'splash_screen',
},
},
},
},
headers: {
...headers,
'X-Guest-Token': guestToken,
},
agent,
timeout: {
request: 20000,
},
});
const flowToken = JSON.parse(flowResponse.body).flow_token;
const finalResponse = await got.post(`${baseURL}onboarding/task.json`, {
json: {
flow_token: flowToken,
subtask_inputs: [
{
open_link: {
link: 'next_link',
},
subtask_id: 'NextTaskOpenLink',
},
],
},
headers: {
...headers,
'X-Guest-Token': guestToken,
},
agent,
timeout: {
request: 20000,
},
});
const account = JSON.parse(finalResponse.body).subtasks[0].open_account;
if (account) {
accounts.push({
t: account.oauth_token,
s: account.oauth_token_secret,
});
} else {
// eslint-disable-next-line no-console
console.log(`Failed to generate account, continue... no account`);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(`Failed to generate account, continue... ${error}`);
}
clearTimeout(timeout);
resolve();
});
}
(async () => {
const oldAccounts = fs.readFileSync(path.join(__dirname, 'accounts.txt'));
const tokens = oldAccounts.toString().split('\n')[0].split('=')[1].split(',');
const secrets = oldAccounts.toString().split('\n')[1].split('=')[1].split(',');
for (let i = 0; i < tokens.length; i++) {
accounts.push({
t: tokens[i],
s: secrets[i],
});
}
for (let i = 0; i < 1000; i++) {
// eslint-disable-next-line no-console
console.log(`Generating accounts ${i * concurrency}-${(i + 1) * concurrency - 1}, total ${accounts.length}`);
// eslint-disable-next-line no-await-in-loop
await Promise.all(Array.from({ length: concurrency }, () => generateOne()));
fs.writeFileSync(path.join(__dirname, 'accounts.txt'), [`TWITTER_OAUTH_TOKEN=${accounts.map((account) => account.t).join(',')}`, `TWITTER_OAUTH_TOKEN_SECRET=${accounts.map((account) => account.s).join(',')}`].join('\n'));
}
})();
These scripts have been placed in the RSSHub repository: https://github.com/DIYgod/RSSHub/blob/master/scripts/twitter-token/generate.js
Before using, you need to fill in the address of the IP proxy service you purchased. The script will automatically handle timeouts, request errors, and will automatically obtain temporary accounts with 5 concurrent requests. It will stop after obtaining 1000 accounts. Note that do not set the concurrency too high; I have observed that when Twitter detects a large number of requests, it will pause the interface for a while.
I purchased 5 proxy services for testing and found that the effects are similar, so just choose the cheapest service. Usually, the lowest-priced 1G package is enough to obtain about tens of thousands to hundreds of thousands of accounts. The cheapest service I found is proxy-cheap. If you have better options, please let me know.
This method has been running stably on Nitter for a while now and has also been implemented in RSSHub and its official examples. We can announce that the war against the evil Twitter slave master has achieved a phased victory.