Use twitter safe relay
CI / test (pull_request) Has been cancelled

This commit is contained in:
2026-06-24 03:39:34 +09:00
parent b771e827aa
commit 1f6e228272
66 changed files with 597 additions and 1720 deletions
+23 -16
View File
@@ -4,6 +4,8 @@ import { type OperationName, QUERY_IDS, TARGET_QUERY_ID_OPERATIONS } from './twi
import type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js';
import { normalizeQuoteDepth } from './twitter-client-utils.js';
const TRAILING_SLASHES = /\/+$/;
// biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement.
export type Constructor<T = object> = new (...args: any[]) => T;
// biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement.
@@ -13,9 +15,7 @@ export type Mixin<TBase extends AbstractConstructor<TwitterClientBase>, TAdded>
) => TwitterClientBase & TAdded;
export abstract class TwitterClientBase {
protected authToken: string;
protected ct0: string;
protected cookieHeader: string;
protected relayBaseUrl: string;
protected userAgent: string;
protected timeoutMs?: number;
protected quoteDepth: number;
@@ -24,12 +24,7 @@ export abstract class TwitterClientBase {
protected clientUserId?: string;
constructor(options: TwitterClientOptions) {
if (!options.cookies.authToken || !options.cookies.ct0) {
throw new Error('Both authToken and ct0 cookies are required');
}
this.authToken = options.cookies.authToken;
this.ct0 = options.cookies.ct0;
this.cookieHeader = options.cookies.cookieHeader || `auth_token=${this.authToken}; ct0=${this.ct0}`;
this.relayBaseUrl = this.normalizeRelayBaseUrl(options.relayBaseUrl ?? process.env.TWITTER_RELAY_BASE_URL);
this.userAgent =
options.userAgent ||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
@@ -72,14 +67,15 @@ export abstract class TwitterClientBase {
}
protected async fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
const relayUrl = this.toRelayUrl(url);
if (!this.timeoutMs || this.timeoutMs <= 0) {
return fetch(url, init);
return fetch(relayUrl, init);
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
try {
return await fetch(url, { ...init, signal: controller.signal });
return await fetch(relayUrl, { ...init, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
@@ -93,20 +89,31 @@ export abstract class TwitterClientBase {
return randomBytes(16).toString('hex');
}
private normalizeRelayBaseUrl(value: string | undefined): string {
const trimmed = value?.trim();
if (!trimmed) {
throw new Error('TWITTER_RELAY_BASE_URL is required');
}
return trimmed.replace(TRAILING_SLASHES, '');
}
private toRelayUrl(url: string): string {
const parsed = new URL(url);
if (!['x.com', 'twitter.com', 'api.twitter.com', 'upload.twitter.com'].includes(parsed.hostname)) {
throw new Error(`Unsupported Twitter API host: ${parsed.hostname}`);
}
return `${this.relayBaseUrl}${parsed.pathname}${parsed.search}`;
}
protected getBaseHeaders(): Record<string, string> {
const headers: Record<string, string> = {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
authorization:
'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
'x-csrf-token': this.ct0,
'x-twitter-auth-type': 'OAuth2Session',
'x-twitter-active-user': 'yes',
'x-twitter-client-language': 'en',
'x-client-uuid': this.clientUuid,
'x-twitter-client-deviceid': this.clientDeviceId,
'x-client-transaction-id': this.createTransactionId(),
cookie: this.cookieHeader,
'user-agent': this.userAgent,
origin: 'https://x.com',
referer: 'https://x.com/',