Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions scripts/update-query-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import fs from 'node:fs/promises';
import https from 'node:https';
import path from 'node:path';

const TARGET_OPERATIONS = [
Expand Down Expand Up @@ -101,12 +102,36 @@ async function readExistingIds(): Promise<Record<OperationName, string>> {
}

async function fetchText(url: string): Promise<string> {
const response = await fetch(url, { headers: HEADERS });
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(`HTTP ${response.status} for ${url}: ${body.slice(0, 120)}`);
}
return response.text();
// Use native https module to bypass TLS verification issues
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || 443,
path: parsedUrl.pathname + parsedUrl.search,
method: 'GET',
headers: HEADERS as Record<string, string>,
rejectUnauthorized: false // Ignore TLS/certificate errors
};

const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode && res.statusCode >= 400) {
reject(new Error(`HTTP ${res.statusCode} for ${url}: ${data.slice(0, 120)}`));
} else {
resolve(data);
}
});
});
req.on('error', reject);
req.setTimeout(30000, () => {
req.destroy();
reject(new Error('Timeout'));
});
req.end();
});
}

async function discoverBundles(): Promise<string[]> {
Expand Down
8 changes: 7 additions & 1 deletion src/lib/features.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
"responsive_web_text_conversations_enabled": false,
"interactive_text_enabled": true,
"vibe_api_enabled": true,
"responsive_web_twitter_blue_verified_badge_is_enabled": true
"responsive_web_twitter_blue_verified_badge_is_enabled": true,
"subscriptions_feature_can_gift_premium": false,
"hidden_profile_subscriptions_enabled": false,
"highlights_tweets_tab_ui_enabled": false,
"subscriptions_verification_info_is_identity_verified_enabled": false,
"responsive_web_twitter_article_notes_tab_enabled": false,
"subscriptions_verification_info_verified_since_enabled": false
}
}
}
23 changes: 13 additions & 10 deletions src/lib/query-ids.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
{
"CreateTweet": "nmdAQXJDxw6-0KKF2on7eA",
"CreateTweet": "z0m4Q8u_67R9VOSMXU_MWg",
"CreateRetweet": "LFho5rIi4xcKO90p9jwG7A",
"DeleteRetweet": "G4MoqBiE6aqyo4QWAgCy4w",
"CreateFriendship": "8h9JVdV8dlSyqyRDJEPCsA",
"DestroyFriendship": "ppXWuagMNXgvzx6WoXBW0Q",
"FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
"UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA",
"CreateBookmark": "aoDbu3RHznuiSkQ9aNM67Q",
"DeleteBookmark": "Wlmlj2-xzyS1GN3a6cj-mQ",
"TweetDetail": "_NvJCnIjOW__EP5-RF197A",
"SearchTimeline": "6AAys3t42mosm_yTI_QENg",
"TweetDetail": "Kzfv17rukSzjT96BerOWZA",
"SearchTimeline": "f_A-Gyo204PRxixpkrchJg",
"Bookmarks": "RV1g3b8n_SGOHwkqKYSCFw",
"BookmarkFolderTimeline": "KJIQpsvxrTfRIlbaRIySHQ",
"Following": "mWYeougg_ocJS2Vr1Vt28w",
"Followers": "SFYY3WsgwjlXSLlfnEUE4A",
"Likes": "ETJflBunfqNa1uE1mBPCaw",
"ExploreSidebar": "lpSN4M6qpimkF4nRFPE3nQ",
"ExplorePage": "kheAINB_4pzRDqkzG3K-ng",
"GenericTimelineById": "uGSr7alSjR9v6QJAIaqSKQ",
"TrendHistory": "Sj4T-jSB9pr0Mxtsc1UKZQ",
"Following": "i2GOldCH2D3OUEhAdimLrA",
"Followers": "oQWxG6XdR5SPvMBsPiKUPQ",
"Likes": "fuBEtiFu3uQFuPDTsv4bfg",
"ExploreSidebar": "K7oS8hOBGcK7eXoV0MXAZQ",
"ExplorePage": "fIgAQhnH-MiqWGZ8YyIyJQ",
"GenericTimelineById": "qpouD2Cm-kJhe1IdAQeEFA",
"TrendHistory": "mzmMuPqcnQLWuMbdvIuSCA",
"AboutAccountQuery": "zs_jFPFT78rBpXv9Z3U2YQ"
}
76 changes: 65 additions & 11 deletions src/lib/twitter-client-base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { randomBytes, randomUUID } from 'node:crypto';
import https from 'node:https';
import http from 'node:http';
import type { IncomingMessage, ServerResponse } from 'node:http';
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';
Expand Down Expand Up @@ -84,17 +87,68 @@ export abstract class TwitterClientBase {
}

protected async fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
if (!this.timeoutMs || this.timeoutMs <= 0) {
return fetch(url, init);
}

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
try {
return await fetch(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
// Use custom https.request to bypass TLS certificate verification issues
const parsedUrl = new URL(url);
const isHttps = parsedUrl.protocol === 'https:';

const initCopy = { ...init } as RequestInit & { timeoutMs?: number; body?: any };
const timeoutMs = initCopy.timeoutMs || this.timeoutMs;

return new Promise<Response>((resolve, reject) => {
const options: https.RequestOptions | http.RequestOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isHttps ? 443 : 80),
path: parsedUrl.pathname + parsedUrl.search,
method: (init.method || 'GET') as string,
headers: init.headers as Record<string, string>,
rejectUnauthorized: false // Ignore TLS/certificate errors
};

const requestLib = isHttps ? https : http;
const req = requestLib.request(options, (res: IncomingMessage) => {
let data = '';
res.on('data', (chunk: Buffer | string) => {
data += chunk.toString();
});
res.on('end', () => {
const headers: Record<string, string> = {};
if (res.headers) {
// Convert raw headers to Record<string, string>
for (const [key, value] of Object.entries(res.headers)) {
if (Array.isArray(value)) {
headers[key] = value[0];
} else if (typeof value === 'string') {
headers[key] = value;
}
}
}

const responseInit: ResponseInit = {
status: res.statusCode || 200,
statusText: res.statusMessage || 'OK',
headers
};
resolve(new Response(data, responseInit));
});
});

req.on('error', (err: Error) => {
reject(err);
});

if (timeoutMs && timeoutMs > 0) {
req.setTimeout(timeoutMs, () => {
req.destroy();
reject(new Error('Request timeout'));
});
}

if (init.body) {
req.write(init.body);
}

req.end();
});
}

protected getHeaders(): Record<string, string> {
Expand Down
14 changes: 14 additions & 0 deletions src/lib/twitter-client-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ export function buildTimelineFeatures(): Record<string, boolean> {
interactive_text_enabled: true,
longform_notetweets_richtext_consumption_enabled: true,
responsive_web_media_download_video_enabled: false,
// Added for x.com API changes (2026-02-01)
subscriptions_feature_can_gift_premium: false,
hidden_profile_subscriptions_enabled: false,
highlights_tweets_tab_ui_enabled: false,
subscriptions_verification_info_is_identity_verified_enabled: false,
responsive_web_twitter_article_notes_tab_enabled: false,
subscriptions_verification_info_verified_since_enabled: false,
});
}

Expand Down Expand Up @@ -311,6 +318,13 @@ export function buildFollowingFeatures(): Record<string, boolean> {
responsive_web_grok_imagine_annotation_enabled: false,
responsive_web_grok_community_note_auto_translation_is_enabled: false,
responsive_web_enhance_cards_enabled: false,
// Added for x.com API changes (2026-02-01)
subscriptions_feature_can_gift_premium: false,
hidden_profile_subscriptions_enabled: false,
highlights_tweets_tab_ui_enabled: false,
subscriptions_verification_info_is_identity_verified_enabled: false,
responsive_web_twitter_article_notes_tab_enabled: false,
subscriptions_verification_info_verified_since_enabled: false,
});
}

Expand Down