Skip to content

Commit 51d5d88

Browse files
committed
fix guest login
Signed-off-by: ふぁ <yuki@yuki0311.com>
1 parent b7b1cdf commit 51d5d88

File tree

2 files changed

+92
-88
lines changed

2 files changed

+92
-88
lines changed

twitter-openapi-typescript/src/api.ts

Lines changed: 87 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,7 @@ import { DefaultFlag } from '@/models';
1414
import { UsersApiUtils } from './apis/usersApi';
1515

1616
export type TwitterOpenApiParams = {
17-
lang?: string;
1817
fetchApi?: i.FetchAPI;
19-
20-
flag?: DefaultFlag;
21-
accessToken?: string;
22-
userAgent?: string;
23-
};
24-
25-
export type TwitterOpenApiCookie = {
26-
name: string;
27-
value: string;
2818
};
2919

3020
export class TwitterOpenApi {
@@ -36,6 +26,28 @@ export class TwitterOpenApi {
3626
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36';
3727
static bearer =
3828
'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
29+
30+
static browser_headers = {
31+
accept: 'text/plain, */*; q=0.01',
32+
'accept-encoding': 'gzip, deflate, br',
33+
'accept-language': 'en-US,en;q=0.9',
34+
'cache-control': 'no-cache',
35+
pragma: 'no-cache',
36+
'sec-ch-ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"',
37+
'sec-ch-ua-mobile': '?0',
38+
'sec-ch-ua-platform': '"Windows"',
39+
'sec-fetch-dest': 'empty',
40+
'sec-fetch-mode': 'cors',
41+
'sec-fetch-site': 'same-site',
42+
'user-agent': TwitterOpenApi.userAgent,
43+
};
44+
45+
static api_key = {
46+
'x-twitter-client-language': 'en',
47+
'x-twitter-active-user': 'yes',
48+
...TwitterOpenApi.browser_headers,
49+
};
50+
3951
param: TwitterOpenApiParams;
4052

4153
constructor(param: TwitterOpenApiParams = {}) {
@@ -46,103 +58,92 @@ export class TwitterOpenApi {
4658
return this.param.fetchApi || fetch;
4759
}
4860

49-
cookieDecode(cookie: string): TwitterOpenApiCookie[] {
50-
return cookie
51-
.split(', ')
52-
.map((cookie) => cookie.split(';')[0])
53-
.filter((cookie) => cookie.indexOf('=') != -1)
54-
.map((cookie) => cookie.split('='))
55-
.map(([name, value]) => ({
56-
name: name,
57-
value: value,
58-
}));
61+
cookie_normalize(cookie: string[]): { [key: string]: string } {
62+
return cookie.reduce((a, b) => {
63+
const [key, value] = b.split('=');
64+
return { ...a, [key]: value };
65+
}, {});
66+
}
67+
68+
cookieEncode(cookie: { [key: string]: string }): string {
69+
return Object.entries(cookie)
70+
.map(([key, value]) => `${key}=${value}`)
71+
.join('; ');
5972
}
60-
cookieEncode(cookie: TwitterOpenApiCookie[]): string {
61-
return cookie.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
73+
74+
setCookies(context: i.RequestContext, cookies: { [key: string]: string }): i.RequestContext {
75+
if (context.init.headers) {
76+
const headers = context.init.headers as i.HTTPHeaders;
77+
headers['cookie'] = this.cookieEncode(cookies);
78+
}
79+
return context;
6280
}
6381

64-
async getGuestSession(): Promise<TwitterOpenApiCookie[]> {
65-
const response = await this.fetchApi(TwitterOpenApi.twitter, { method: 'GET', redirect: 'manual' });
66-
const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
67-
const csrfToken = [...Array(32)].reduce((a) => a + chars[Math.floor(Math.random() * chars.length)], '');
68-
const cookies = this.cookieDecode(response.headers.get('set-cookie')!);
69-
cookies.push({ name: 'ct0', value: csrfToken });
82+
async getGuestClient(): Promise<TwitterOpenApiClient> {
83+
let cookies: { [key: string]: string } = {};
84+
85+
const response = await this.fetchApi(TwitterOpenApi.twitter, {
86+
method: 'GET',
87+
redirect: 'manual',
88+
headers: { Cookie: this.cookieEncode(cookies) },
89+
});
90+
cookies = { ...cookies, ...this.cookie_normalize(response.headers.get('set-cookie')!.split(', ')) };
7091

7192
const html = await this.fetchApi(TwitterOpenApi.twitter, {
7293
method: 'GET',
7394
headers: { Cookie: this.cookieEncode(cookies) },
7495
}).then((response) => response.text());
7596

76-
const re = new RegExp('<script nonce="([a-zA-Z0-9]{48})">(document.cookie="(.*?)";)+<\\/script>');
97+
const re = new RegExp('document.cookie="(.*?)";', 'g');
98+
99+
const find = [...html.matchAll(re)].map((e) => e[1]);
100+
cookies = { ...cookies, ...this.cookie_normalize(find) };
101+
102+
cookies = Object.entries(cookies)
103+
.filter(([key, value]) => key != 'ct0')
104+
.reduce((a, [key, value]) => ({ ...a, [key]: value }), {});
105+
106+
if (!cookies['gt']) {
107+
const activate_headers = {
108+
...TwitterOpenApi.api_key,
109+
authorization: `Bearer ${TwitterOpenApi.bearer}`,
110+
};
111+
const { guest_token } = await this.fetchApi('https://api.twitter.com/1.1/guest/activate.json', {
112+
method: 'POST',
113+
headers: activate_headers,
114+
}).then((response) => response.json());
115+
cookies['gt'] = guest_token;
116+
}
77117

78-
const script = html.match(re)![0];
79-
script
80-
.split('document.cookie="')
81-
.slice(1)
82-
.map((e) => e.replace('</script>', '').replace('";', ''))
83-
.map((e) => this.cookieDecode(e)[0])
84-
.filter((e) => cookies.findIndex((c) => c.name == e.name) == -1)
85-
.forEach((e) => cookies.push(e));
118+
// const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
119+
// const csrfToken = [...Array(32)].reduce((a) => a + chars[Math.floor(Math.random() * chars.length)], '');
120+
// cookies.push({ name: 'ct0', value: csrfToken });
86121

87-
return cookies;
122+
return this.getClientFromCookies(cookies);
88123
}
89124

90-
setCookies(context: i.RequestContext, cookies: TwitterOpenApiCookie[]): i.RequestContext {
91-
if (context.init.headers) {
92-
const headers = context.init.headers as i.HTTPHeaders;
93-
headers['cookie'] = this.cookieEncode(cookies);
125+
async getClientFromCookies(cookies: { [key: string]: string }): Promise<TwitterOpenApiClient> {
126+
let api_key: { [key: string]: string } = { ...TwitterOpenApi.api_key };
127+
if (cookies['ct0']) {
128+
api_key['x-twitter-auth-type'] = 'OAuth2Session';
129+
api_key['x-csrf-token'] = cookies['ct0'];
130+
}
131+
if (cookies['gt']) {
132+
api_key['x-guest-token'] = cookies['gt'];
94133
}
95-
return context;
96-
}
97134

98-
async getClient(): Promise<TwitterOpenApiClient> {
99-
const cookies: TwitterOpenApiCookie[] = await this.getGuestSession();
100135
const config: i.ConfigurationParameters = {
101136
fetchApi: this.fetchApi,
102137
middleware: [{ pre: async (context) => this.setCookies(context, cookies) }],
103-
apiKey: (key) => {
104-
return {
105-
'user-agent': this.param.userAgent || TwitterOpenApi.userAgent,
106-
'x-twitter-client-language': this.param.lang ?? 'en',
107-
'x-twitter-active-user': 'yes',
108-
'x-csrf-token': cookies.find((cookie) => cookie.name === 'ct0')?.value,
109-
'x-guest-token': cookies.find((cookie) => cookie.name === 'gt')?.value,
110-
}[key]!;
111-
},
112-
accessToken: this.param.accessToken || TwitterOpenApi.bearer,
138+
apiKey: (key) => api_key[key.toLowerCase()],
139+
accessToken: TwitterOpenApi.bearer,
113140
};
114-
const flag =
115-
this.param.flag ||
116-
((await this.fetchApi(TwitterOpenApi.url, { method: 'GET' }).then((res) => res.json())) as DefaultFlag);
117-
return new TwitterOpenApiClient(new i.Configuration(config), flag);
141+
return this.getClient(new i.Configuration(config));
118142
}
119143

120-
async getClientFromCookies(ct0: string, authToken: string): Promise<TwitterOpenApiClient> {
121-
const guestCookies: TwitterOpenApiCookie[] = await this.getGuestSession();
122-
const cookies = [
123-
{ name: 'auth_token', value: authToken },
124-
{ name: 'ct0', value: ct0 },
125-
];
126-
guestCookies.filter((e) => cookies.findIndex((c) => c.name == e.name) == -1).forEach((e) => cookies.push(e));
127-
128-
const config: i.ConfigurationParameters = {
129-
fetchApi: this.param.fetchApi || fetch,
130-
middleware: [{ pre: async (context) => this.setCookies(context, cookies) }],
131-
apiKey: (key) => {
132-
return {
133-
'user-agent': this.param.userAgent || TwitterOpenApi.userAgent,
134-
'x-twitter-client-language': this.param.lang ?? 'en',
135-
'x-twitter-active-user': 'yes',
136-
'x-twitter-auth-type': 'OAuth2Session',
137-
'x-csrf-token': cookies.find((cookie) => cookie.name === 'ct0')?.value,
138-
}[key]!;
139-
},
140-
accessToken: this.param.accessToken || TwitterOpenApi.bearer,
141-
};
142-
const flag =
143-
this.param.flag ||
144-
((await this.fetchApi(TwitterOpenApi.url, { method: 'GET' }).then((res) => res.json())) as DefaultFlag);
145-
return new TwitterOpenApiClient(new i.Configuration(config), flag);
144+
async getClient(api: i.Configuration): Promise<TwitterOpenApiClient> {
145+
const flag = (await this.fetchApi(TwitterOpenApi.url, { method: 'GET' }).then((res) => res.json())) as DefaultFlag;
146+
return new TwitterOpenApiClient(api, flag);
146147
}
147148
}
148149

twitter-openapi-typescript/test/init.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as log4js from 'log4js';
66
dotenv.config();
77

88
const authToken = process.env.AUTH_TOKEN as string;
9-
const ct0 = process.env.CSRF_TOKEN as string;
9+
const CsrfToken = process.env.CSRF_TOKEN as string;
1010

1111
export const logger = log4js
1212
.configure({
@@ -28,7 +28,10 @@ export const logger = log4js
2828

2929
export const getClient = async () => {
3030
const api = new TwitterOpenApi({ fetchApi: fetch as any });
31-
const client = await api.getClientFromCookies(ct0, authToken);
31+
const client = await api.getClientFromCookies({
32+
ct0: CsrfToken,
33+
auth_token: authToken,
34+
});
3235
// const client = await api.getClient();
3336
return client;
3437
};

0 commit comments

Comments
 (0)