diff --git a/src/lib/twitter-client-base.ts b/src/lib/twitter-client-base.ts index f586785..f9d0ec4 100644 --- a/src/lib/twitter-client-base.ts +++ b/src/lib/twitter-client-base.ts @@ -1,4 +1,3 @@ -import { randomBytes, randomUUID } from 'node:crypto'; import { runtimeQueryIds } from './runtime-query-ids.js'; import { type OperationName, QUERY_IDS, TARGET_QUERY_ID_OPERATIONS } from './twitter-client-constants.js'; import type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js'; @@ -16,22 +15,14 @@ export type Mixin, TAdded> export abstract class TwitterClientBase { protected relayBaseUrl: string; - protected userAgent: string; protected timeoutMs?: number; protected quoteDepth: number; - protected clientUuid: string; - protected clientDeviceId: string; protected clientUserId?: string; constructor(options: TwitterClientOptions) { 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'; this.timeoutMs = options.timeoutMs; this.quoteDepth = normalizeQuoteDepth(options.quoteDepth); - this.clientUuid = randomUUID(); - this.clientDeviceId = randomUUID(); } protected abstract getCurrentUser(): Promise; @@ -82,11 +73,7 @@ export abstract class TwitterClientBase { } protected getHeaders(): Record { - return this.getJsonHeaders(); - } - - protected createTransactionId(): string { - return randomBytes(16).toString('hex'); + return {}; } private normalizeRelayBaseUrl(value: string | undefined): string { @@ -105,37 +92,12 @@ export abstract class TwitterClientBase { return `${this.relayBaseUrl}${parsed.pathname}${parsed.search}`; } - protected getBaseHeaders(): Record { - const headers: Record = { - accept: '*/*', - 'accept-language': 'en-US,en;q=0.9', - '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(), - 'user-agent': this.userAgent, - origin: 'https://x.com', - referer: 'https://x.com/', - }; - - if (this.clientUserId) { - headers['x-twitter-client-user-id'] = this.clientUserId; - } - - return headers; - } - protected getJsonHeaders(): Record { - return { - ...this.getBaseHeaders(), - 'content-type': 'application/json', - }; + return { 'content-type': 'application/json' }; } protected getUploadHeaders(): Record { - // Note: do not set content-type; URLSearchParams/FormData need to set it (incl boundary) themselves. - return this.getBaseHeaders(); + return {}; } protected async ensureClientUserId(): Promise { diff --git a/src/lib/twitter-client-bookmarks.ts b/src/lib/twitter-client-bookmarks.ts index 49830a3..b16f38a 100644 --- a/src/lib/twitter-client-bookmarks.ts +++ b/src/lib/twitter-client-bookmarks.ts @@ -22,7 +22,6 @@ export function withBookmarks JSON.stringify({ variables, queryId }); - const buildHeaders = () => ({ ...this.getHeaders(), referer: `https://x.com/i/status/${tweetId}` }); let body = buildBody(); const parseResponse = async (response: Response): Promise => { @@ -42,7 +41,7 @@ export function withBookmarks let body = buildBody(); try { - const headers = { ...this.getHeaders(), referer: 'https://x.com/compose/post' }; + const headers = this.getJsonHeaders(); let response = await this.fetchWithTimeout(urlWithOperation, { method: 'POST', headers, @@ -88,14 +88,14 @@ export function withPosting response = await this.fetchWithTimeout(urlWithOperation, { method: 'POST', - headers: { ...this.getHeaders(), referer: 'https://x.com/compose/post' }, + headers, body, }); if (response.status === 404) { const retry = await this.fetchWithTimeout(TWITTER_GRAPHQL_POST_URL, { method: 'POST', - headers: { ...this.getHeaders(), referer: 'https://x.com/compose/post' }, + headers, body, }); @@ -224,12 +224,8 @@ export function withPosting try { const response = await this.fetchWithTimeout(TWITTER_STATUS_UPDATE_URL, { method: 'POST', - headers: { - ...this.getBaseHeaders(), - 'content-type': 'application/x-www-form-urlencoded', - referer: 'https://x.com/compose/post', - }, - body: params.toString(), + headers: this.getHeaders(), + body: params, }); if (!response.ok) { diff --git a/src/lib/twitter-client-search.ts b/src/lib/twitter-client-search.ts index 7848d69..d096643 100644 --- a/src/lib/twitter-client-search.ts +++ b/src/lib/twitter-client-search.ts @@ -105,7 +105,7 @@ export function withSearch> try { const response = await this.fetchWithTimeout(url, { method: 'POST', - headers: this.getHeaders(), + headers: this.getJsonHeaders(), body: JSON.stringify({ features, queryId }), }); diff --git a/src/lib/twitter-client-tweet-detail.ts b/src/lib/twitter-client-tweet-detail.ts index a70884c..db63b22 100644 --- a/src/lib/twitter-client-tweet-detail.ts +++ b/src/lib/twitter-client-tweet-detail.ts @@ -251,7 +251,7 @@ export function withTweetDetails Promise; fetchWithTimeout: (url: string, init: RequestInit) => Promise; ensureClientUserId: () => Promise; - getBaseHeaders: () => Record; + getHeaders: () => Record; fetchUserArticlePlainText: (userId: string, tweetId: string) => Promise<{ title?: string; plainText?: string }>; fetchTweetDetail: (tweetId: string) => Promise<{ success: true } | { success: false; error: string }>; postStatusUpdate: (input: { text: string }) => Promise<{ success: boolean; error?: string }>; @@ -108,7 +108,7 @@ describe('TwitterClient core coverage', () => { expect(result.error).toBe('Tweet not found in response'); }); - it('adds client user id header after ensureClientUserId', async () => { + it('does not add Twitter headers after ensureClientUserId', async () => { const client = new TwitterClient({ relayBaseUrl: testRelayBaseUrl }); process.env.NODE_ENV = 'production'; @@ -119,9 +119,9 @@ describe('TwitterClient core coverage', () => { const clientPrivate = client as unknown as TwitterClientPrivate; await clientPrivate.ensureClientUserId(); - const headers = clientPrivate.getBaseHeaders(); + const headers = clientPrivate.getHeaders(); - expect(headers['x-twitter-client-user-id']).toBe('42'); + expect(headers).toEqual({}); }); describe('fetchUserArticlePlainText failures', () => { diff --git a/tests/twitter-client.constructor-tweet-reply.test.ts b/tests/twitter-client.constructor-tweet-reply.test.ts index ba2175d..c74c319 100644 --- a/tests/twitter-client.constructor-tweet-reply.test.ts +++ b/tests/twitter-client.constructor-tweet-reply.test.ts @@ -68,9 +68,7 @@ describe('TwitterClient tweet', () => { expect(String(url)).toContain(`${testRelayBaseUrl}/i/api/graphql/`); expect(String(url)).toContain('CreateTweet'); expect(options.method).toBe('POST'); - expect(options.headers).not.toHaveProperty('cookie'); - expect(options.headers).not.toHaveProperty('authorization'); - expect(options.headers).not.toHaveProperty('x-csrf-token'); + expect(options.headers).toEqual({ 'content-type': 'application/json' }); const body = JSON.parse(options.body); expect(body.variables.tweet_text).toBe('Hello world!'); @@ -392,9 +390,10 @@ describe('TwitterClient reply', () => { const [, options] = mockFetch.mock.calls[1]; expect(String(mockFetch.mock.calls[1][0])).toContain('statuses/update.json'); expect(options.method).toBe('POST'); - expect(options.body).toContain('status=This+is+a+reply'); - expect(options.body).toContain('in_reply_to_status_id=1234567890'); - expect(options.body).toContain('auto_populate_reply_metadata=true'); - expect(options.body).toContain('media_ids=111%2C222'); + const body = String(options.body); + expect(body).toContain('status=This+is+a+reply'); + expect(body).toContain('in_reply_to_status_id=1234567890'); + expect(body).toContain('auto_populate_reply_metadata=true'); + expect(body).toContain('media_ids=111%2C222'); }); });