信息自由並不會自然演變,而是會退化。
—— 開放信息宣言
Twitter 在 8 月決定了全面限制公開訪問和 API 接口,導致第三方集成均無法再正常工作。開放用戶數據被綁架成私人攫取財富的工具,曾經的 Open Web 標杆 Twitter 竟淪落到這種境地,數字奴隸制在最不應該的地方出現,令人唏噓。這也致使許多用戶流向 Fediverse,但社交關係和習慣一旦形成,要讓其迅速改變並不易,更多人還是選擇了忍受,Musk 也是看穿了這一點才有恃無恐
然而,我們也不能武斷地說 Twitter 封閉,畢竟它仍然開放了一個起步價為每月 4 萬美元,上限不設的企業 API
什麼?你說用不起?
那麼你可以像我一樣,通過創建一萬個賬號以繞開封鎖
儘管 Twitter 限制了所有公開訪問,但我們發現新下載的 Twitter 移動客戶端仍可以正常查看用戶動態。這為我們提供了潛在的利用方法,通過抓包,我們可以看到客戶端是通過請求一系列特殊接口來創建一個權限較低、頻率限制嚴格的臨時賬號。我們可以用這個賬號獲取我們需要的大部分數據。然而,這種賬號對請求頻率的限制非常嚴格,因此需要大量的這樣的賬號才能滿足基本的使用需求。同時,每個 IP 在一段時間內只能獲取一個臨時賬號,因此我們也需要大量的 IP 代理
具體拆包和抓包過程可以參考 BANKA 的《怎麼爬 Twitter(Android)》。站在 BANKA 肩膀上,我們可以寫出一個這樣的註冊腳本(來自 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'
以及這樣的批量註冊腳本(來自我自己):
const got = require('got');
const { HttpsProxyAgent } = require('https-proxy-agent');
const fs = require('fs');
const path = require('path');
const concurrency = 5; // 請不要設置得太大以避免 Twitter 發現我們的小秘密
const proxyUrl = ''; // 在這裡添加你的代理
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(`生成賬號失敗,繼續... 超時`);
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(`生成賬號失敗,繼續... 沒有賬號`);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(`生成賬號失敗,繼續... ${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(`生成賬號 ${i * concurrency}-${(i + 1) * concurrency - 1}, 總計 ${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'));
}
})();
這些腳本已放到了 RSSHub 倉庫: https://github.com/DIYgod/RSSHub/blob/master/scripts/twitter-token/generate.js
在使用前,你需要填入你購買的 IP 代理服務地址。腳本會自動處理超時、請求等錯誤,並且以 5 並發來自動獲取臨時賬號,當獲取到 1000 個賬號後將會停止。需注意並發不要設置得過高,我從觀察發現,當 Twitter 發現大量請求時會暫停接口一段時間
我購買了 5 家代理服務以進行測試,感覺效果相差無幾,選擇一個最便宜的服務就可以。通常,最低價的 1G 套餐就足夠獲取大約幾萬到十幾萬個賬號了。我目前找到的最便宜的服務是 proxy-cheap,如果你有更好的選擇請告知我
這種方法已經在 Nitter 上穩定運行了一段時間,現在也已實裝到了 RSSHub 及其官方示例上,我們可以宣布與邪惡 Twitter 奴隸主的戰爭已經階段性勝利