@cm-growth-hacking/twitter-client 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/twitter.js ADDED
@@ -0,0 +1,1129 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
+
15
+ // src/client/query-ids.ts
16
+ var exports_query_ids = {};
17
+ __export(exports_query_ids, {
18
+ queryIdManager: () => queryIdManager,
19
+ QueryIdManager: () => QueryIdManager
20
+ });
21
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
22
+ import { dirname, join } from "path";
23
+ import { homedir } from "os";
24
+
25
+ class QueryIdManager {
26
+ cache = {};
27
+ constructor() {
28
+ this.loadCache();
29
+ }
30
+ loadCache() {
31
+ try {
32
+ const content = readFileSync(CACHE_PATH, "utf-8");
33
+ const data = JSON.parse(content);
34
+ if (data.version !== CACHE_VERSION) {
35
+ return;
36
+ }
37
+ if (data.timestamp && Date.now() - data.timestamp > CACHE_TTL) {
38
+ return;
39
+ }
40
+ this.cache = data.queryIds || {};
41
+ } catch {
42
+ this.cache = {};
43
+ }
44
+ }
45
+ saveCache(queryIds) {
46
+ try {
47
+ const dir = dirname(CACHE_PATH);
48
+ mkdirSync(dir, { recursive: true });
49
+ const data = {
50
+ version: CACHE_VERSION,
51
+ timestamp: Date.now(),
52
+ queryIds
53
+ };
54
+ writeFileSync(CACHE_PATH, JSON.stringify(data, null, 2));
55
+ } catch (error) {
56
+ console.warn(`Failed to save query ID cache: ${error}`);
57
+ }
58
+ }
59
+ getQueryId(operation) {
60
+ if (this.cache[operation]) {
61
+ return this.cache[operation];
62
+ }
63
+ if (BAKED_IDS[operation]) {
64
+ return BAKED_IDS[operation];
65
+ }
66
+ throw new Error(`Unknown operation: ${operation}`);
67
+ }
68
+ async scrapeQueryIds() {
69
+ const operations = Object.keys(BAKED_IDS);
70
+ const queryIds = {};
71
+ const bundleUrl = "https://abs.twimg.com/responsive-web/client-web/main." + Date.now() + ".js";
72
+ try {
73
+ const response = await fetch(bundleUrl);
74
+ if (!response.ok) {
75
+ throw new Error(`Failed to fetch bundle: ${response.status}`);
76
+ }
77
+ const js = await response.text();
78
+ for (const op of operations) {
79
+ const patterns = [
80
+ new RegExp(`"${op}":"([A-Za-z0-9]+)"`, "g"),
81
+ new RegExp(`queryId:\\s*"([A-Za-z0-9]+)"\\s*,\\s*\\w*:\\s*\\w*:\\s*\\{\\s*"name":\\s*"${op}"`, "g"),
82
+ new RegExp(`"queryId":\\s*"([A-Za-z0-9]+)"`, "g")
83
+ ];
84
+ for (const pattern of patterns) {
85
+ const matches = js.matchAll(pattern);
86
+ const ids = Array.from(matches).map((m) => m[1]).filter((id) => id.length >= 15 && id.length <= 25);
87
+ if (ids.length > 0) {
88
+ queryIds[op] = ids[0];
89
+ break;
90
+ }
91
+ }
92
+ }
93
+ } catch (error) {
94
+ console.warn(`Scraping failed, using baked-in IDs: ${error}`);
95
+ for (const op of operations) {
96
+ const id = BAKED_IDS[op];
97
+ queryIds[op] = Array.isArray(id) ? id[0] : id;
98
+ }
99
+ }
100
+ return queryIds;
101
+ }
102
+ async updateQueryIds(options) {
103
+ const changes = [];
104
+ if (!options?.force) {
105
+ try {
106
+ const content = readFileSync(CACHE_PATH, "utf-8");
107
+ const data = JSON.parse(content);
108
+ const age = Date.now() - (data.timestamp || 0);
109
+ if (age < CACHE_TTL) {
110
+ return { updated: 0, changes: [] };
111
+ }
112
+ } catch {}
113
+ }
114
+ const freshIds = await this.scrapeQueryIds();
115
+ const newCache = {};
116
+ for (const [op, freshId] of Object.entries(freshIds)) {
117
+ const oldId = this.cache[op] || BAKED_IDS[op];
118
+ const oldIdStr = Array.isArray(oldId) ? oldId[0] : oldId;
119
+ if (freshId !== oldIdStr) {
120
+ changes.push(`${op}: ${oldIdStr} \u2192 ${freshId}`);
121
+ }
122
+ if (op === "TweetDetail" || op === "SearchTimeline") {
123
+ const bakedFallbacks = BAKED_IDS[op];
124
+ if (Array.isArray(bakedFallbacks)) {
125
+ newCache[op] = [freshId, ...bakedFallbacks.slice(1)];
126
+ } else {
127
+ newCache[op] = freshId;
128
+ }
129
+ } else {
130
+ newCache[op] = freshId;
131
+ }
132
+ }
133
+ this.saveCache(newCache);
134
+ this.cache = newCache;
135
+ return { updated: changes.length, changes };
136
+ }
137
+ }
138
+ var BAKED_IDS, CACHE_PATH, CACHE_VERSION = "v1", CACHE_TTL, queryIdManager;
139
+ var init_query_ids = __esm(() => {
140
+ BAKED_IDS = {
141
+ CreateTweet: "nmdAQXJDxw6-0KKF2on7eA",
142
+ TweetDetail: ["_NvJCnIjOW__EP5-RF197A", "97JF30KziU00483E_8elBA", "aFvUsJm2c-oDkJV75blV6g"],
143
+ SearchTimeline: ["6AAys3t42mosm_yTI_QENg", "M1jEez78PEfVfbQLvlWMvQ", "5h0kNbk3ii97rmfY6CdgAA"],
144
+ UserByScreenName: ["xc8f1g7BYqr6VTzTbvNlGw", "qW5u-DAuXpMEG0zA1F7UGQ", "sLVLhk0bGj3MVFEKTdax1w"],
145
+ UserTweets: "Wms1GvIiHXAPBaCr9KblaA",
146
+ AboutAccountQuery: ["zs_jFPFT78rBpXv9Z3U2YQ", "cY6EpQYqLfc7LGIJCevPqA"],
147
+ MentionsTimeline: "Wms1GvIiHXAPBaCr9KblaA"
148
+ };
149
+ CACHE_PATH = join(homedir(), ".config", "twitter-client", "query-ids.json");
150
+ CACHE_TTL = 24 * 60 * 60 * 1000;
151
+ queryIdManager = new QueryIdManager;
152
+ });
153
+
154
+ // src/client/parsers.ts
155
+ var exports_parsers = {};
156
+ __export(exports_parsers, {
157
+ parseUser: () => parseUser,
158
+ parseTweet: () => parseTweet,
159
+ extractTweetsFromInstructions: () => extractTweetsFromInstructions
160
+ });
161
+ function parseTweet(tweetData) {
162
+ if (!tweetData)
163
+ return null;
164
+ const legacy = tweetData.legacy || tweetData;
165
+ const core = tweetData.core?.user_results?.result;
166
+ const userLegacy = core?.legacy;
167
+ const userCore = core?.core;
168
+ if (!legacy)
169
+ return null;
170
+ const username = userLegacy?.screen_name || userCore?.screen_name || "";
171
+ const name = userLegacy?.name || userCore?.name || username;
172
+ if (!username) {
173
+ return null;
174
+ }
175
+ const id = legacy.id_str || legacy.rest_id || tweetData.rest_id || "";
176
+ const tweet = {
177
+ id,
178
+ url: `https://x.com/${username}/status/${id}`,
179
+ text: legacy.full_text || legacy.text || "",
180
+ author: {
181
+ id: core?.rest_id || userLegacy?.id_str || "",
182
+ username,
183
+ name
184
+ },
185
+ createdAt: legacy.created_at || "",
186
+ replyCount: legacy.reply_count || 0,
187
+ retweetCount: legacy.retweet_count || 0,
188
+ likeCount: legacy.favorite_count || 0,
189
+ viewCount: tweetData.views?.count ? parseInt(tweetData.views.count, 10) : undefined,
190
+ bookmarkCount: legacy.bookmark_count
191
+ };
192
+ if (legacy.conversation_id_str) {
193
+ tweet.conversationId = legacy.conversation_id_str;
194
+ }
195
+ if (legacy.in_reply_to_status_id_str) {
196
+ tweet.inReplyToStatusId = legacy.in_reply_to_status_id_str;
197
+ }
198
+ if (legacy.extended_entities?.media) {
199
+ tweet.media = legacy.extended_entities.media.map((m) => {
200
+ const type = m.type === "video" || m.type === "animated_gif" ? m.type === "animated_gif" ? "gif" : "video" : "photo";
201
+ const mediaItem = {
202
+ type,
203
+ url: m.media_url_https || "",
204
+ altText: m.alt_text
205
+ };
206
+ const sizes = m.sizes;
207
+ if (sizes?.large) {
208
+ mediaItem.width = sizes.large.w;
209
+ mediaItem.height = sizes.large.h;
210
+ } else if (sizes?.medium) {
211
+ mediaItem.width = sizes.medium.w;
212
+ mediaItem.height = sizes.medium.h;
213
+ }
214
+ if (sizes?.small) {
215
+ mediaItem.previewUrl = `${m.media_url_https}:small`;
216
+ }
217
+ if ((type === "video" || type === "gif") && m.video_info?.variants) {
218
+ const mp4Variants = m.video_info.variants.filter((v) => v.content_type === "video/mp4" && typeof v.url === "string");
219
+ const mp4WithBitrate = mp4Variants.filter((v) => typeof v.bitrate === "number").sort((a, b) => b.bitrate - a.bitrate);
220
+ const selectedVariant = mp4WithBitrate[0] ?? mp4Variants[0];
221
+ if (selectedVariant) {
222
+ mediaItem.videoUrl = selectedVariant.url;
223
+ }
224
+ if (typeof m.video_info.duration_millis === "number") {
225
+ mediaItem.durationMs = m.video_info.duration_millis;
226
+ }
227
+ }
228
+ return mediaItem;
229
+ });
230
+ }
231
+ if (legacy.quoted_status_result?.result) {
232
+ const quoted = parseTweet(legacy.quoted_status_result.result);
233
+ if (quoted)
234
+ tweet.quotedTweet = quoted;
235
+ }
236
+ return tweet;
237
+ }
238
+ function parseUser(userData) {
239
+ if (!userData)
240
+ return null;
241
+ const legacy = userData.legacy || userData;
242
+ const user = {
243
+ id: userData.rest_id || legacy.id_str || "",
244
+ username: legacy.screen_name || "",
245
+ name: legacy.name || "",
246
+ description: legacy.description,
247
+ followersCount: legacy.followers_count,
248
+ followingCount: legacy.friends_count,
249
+ isBlueVerified: legacy.verified || legacy.is_blue_verified,
250
+ profileImageUrl: legacy.profile_image_url_https,
251
+ createdAt: legacy.created_at
252
+ };
253
+ return user;
254
+ }
255
+ function extractTweetsFromInstructions(instructions) {
256
+ const tweets = [];
257
+ const seenIds = new Set;
258
+ for (const instruction of instructions) {
259
+ if (!instruction)
260
+ continue;
261
+ const entries = instruction.entries || instruction.moduleItems || [];
262
+ for (const entry of entries) {
263
+ if (!entry)
264
+ continue;
265
+ const content = entry.content || entry.item?.content;
266
+ if (!content)
267
+ continue;
268
+ const tweetData = content.tweet_results?.result || content.tweet?.tweet_results?.result || content.itemContent?.tweet_results?.result;
269
+ if (tweetData) {
270
+ const tweet = parseTweet(tweetData);
271
+ if (tweet && !seenIds.has(tweet.id)) {
272
+ tweets.push(tweet);
273
+ seenIds.add(tweet.id);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ return tweets;
279
+ }
280
+
281
+ // src/client/graphql.ts
282
+ init_query_ids();
283
+ import { randomUUID, randomBytes } from "crypto";
284
+
285
+ class GraphQLClient {
286
+ authToken;
287
+ ct0;
288
+ userAgent;
289
+ timeout;
290
+ clientUuid;
291
+ clientDeviceId;
292
+ constructor(options) {
293
+ this.authToken = options.authToken;
294
+ this.ct0 = options.ct0;
295
+ this.timeout = options.timeout;
296
+ 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";
297
+ this.clientUuid = randomUUID();
298
+ this.clientDeviceId = randomUUID();
299
+ }
300
+ createTransactionId() {
301
+ return randomBytes(16).toString("hex");
302
+ }
303
+ getHeaders() {
304
+ return {
305
+ accept: "*/*",
306
+ "accept-language": "en-US,en;q=0.9",
307
+ authorization: "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
308
+ "content-type": "application/json",
309
+ "x-csrf-token": this.ct0,
310
+ "x-twitter-auth-type": "OAuth2Session",
311
+ "x-twitter-active-user": "yes",
312
+ "x-twitter-client-language": "en",
313
+ "x-client-uuid": this.clientUuid,
314
+ "x-twitter-client-deviceid": this.clientDeviceId,
315
+ "x-client-transaction-id": this.createTransactionId(),
316
+ cookie: `auth_token=${this.authToken}; ct0=${this.ct0}`,
317
+ "user-agent": this.userAgent,
318
+ origin: "https://x.com",
319
+ referer: "https://x.com/"
320
+ };
321
+ }
322
+ async fetchWithTimeout(url, init) {
323
+ if (!this.timeout || this.timeout <= 0) {
324
+ return fetch(url, init);
325
+ }
326
+ const controller = new AbortController;
327
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
328
+ try {
329
+ return await fetch(url, { ...init, signal: controller.signal });
330
+ } finally {
331
+ clearTimeout(timeoutId);
332
+ }
333
+ }
334
+ getErrorCode(status, errors) {
335
+ if (status === 401 || status === 403) {
336
+ return "AUTH_FAILED";
337
+ }
338
+ if (status === 429) {
339
+ return "RATE_LIMITED";
340
+ }
341
+ if (status === 404) {
342
+ return "STALE_QUERY_ID";
343
+ }
344
+ if (errors?.some((e) => e.code === 226)) {
345
+ return "RATE_LIMITED";
346
+ }
347
+ return "NETWORK_ERROR";
348
+ }
349
+ async validateResponse(response) {
350
+ const data = await response.json();
351
+ if (data.errors && data.errors.length > 0) {
352
+ return {
353
+ success: false,
354
+ error: data.errors.map((e) => e.message).join(", "),
355
+ code: this.getErrorCode(500, data.errors)
356
+ };
357
+ }
358
+ if (!data.data) {
359
+ return {
360
+ success: false,
361
+ error: "No data returned from API",
362
+ code: "NETWORK_ERROR"
363
+ };
364
+ }
365
+ return {
366
+ success: true,
367
+ data: data.data
368
+ };
369
+ }
370
+ async request(operation, variables, features) {
371
+ let queryId = queryIdManager.getQueryId(operation);
372
+ let queryIds = Array.isArray(queryId) ? queryId : [queryId];
373
+ let hasRefreshed = false;
374
+ for (let i = 0;i < queryIds.length; i++) {
375
+ const id = queryIds[i];
376
+ const url = `https://x.com/i/api/graphql/${id}/${operation}`;
377
+ try {
378
+ const body = JSON.stringify({
379
+ variables,
380
+ features,
381
+ queryId: id
382
+ });
383
+ const response = await this.fetchWithTimeout(url, {
384
+ method: "POST",
385
+ headers: this.getHeaders(),
386
+ body
387
+ });
388
+ if (!response.ok) {
389
+ const code = this.getErrorCode(response.status);
390
+ if (response.status === 404 && i < queryIds.length - 1) {
391
+ continue;
392
+ }
393
+ if (response.status === 404 && i === queryIds.length - 1 && !hasRefreshed) {
394
+ console.info(`Query ID stale for ${operation}, refreshing...`);
395
+ hasRefreshed = true;
396
+ try {
397
+ await queryIdManager.updateQueryIds({ force: true });
398
+ queryId = queryIdManager.getQueryId(operation);
399
+ queryIds = Array.isArray(queryId) ? queryId : [queryId];
400
+ i = -1;
401
+ continue;
402
+ } catch (refreshError) {
403
+ return {
404
+ success: false,
405
+ error: `Failed to refresh query IDs: ${refreshError instanceof Error ? refreshError.message : "Unknown error"}`,
406
+ code: "NETWORK_ERROR"
407
+ };
408
+ }
409
+ }
410
+ const text = await response.text();
411
+ return {
412
+ success: false,
413
+ error: `HTTP ${response.status}: ${text.slice(0, 200)}`,
414
+ code
415
+ };
416
+ }
417
+ return await this.validateResponse(response);
418
+ } catch (error) {
419
+ if (error instanceof Error && error.name === "AbortError") {
420
+ return {
421
+ success: false,
422
+ error: "Request timeout",
423
+ code: "NETWORK_ERROR"
424
+ };
425
+ }
426
+ return {
427
+ success: false,
428
+ error: error instanceof Error ? error.message : "Unknown error",
429
+ code: "NETWORK_ERROR"
430
+ };
431
+ }
432
+ }
433
+ return {
434
+ success: false,
435
+ error: "All query IDs failed",
436
+ code: "STALE_QUERY_ID"
437
+ };
438
+ }
439
+ async getCurrentUser() {
440
+ return await this.request("AboutAccountQuery", {}, {
441
+ hidden_profile_subscriptions_enabled: true,
442
+ hidden_profile_likes_enabled: true,
443
+ responsive_web_graphql_exclude_directive_enabled: true,
444
+ verified_phone_label_enabled: false,
445
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
446
+ responsive_web_graphql_timeline_navigation_enabled: true,
447
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
448
+ responsive_web_grok_analyze_post_followups_enabled: false,
449
+ responsive_web_grok_annotations_enabled: false,
450
+ responsive_web_grok_show_grok_translated_post: false,
451
+ responsive_web_jetfuel_frame: true,
452
+ longform_notetweets_consumption_enabled: true,
453
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
454
+ tweet_awards_web_tipping_enabled: false,
455
+ responsive_web_media_download_video_enabled: false,
456
+ c9s_tweet_anatomy_moderator_badge_enabled: true
457
+ });
458
+ }
459
+ }
460
+
461
+ // src/client/rest.ts
462
+ class RestClient {
463
+ authToken;
464
+ ct0;
465
+ userAgent;
466
+ timeout;
467
+ constructor(authToken, ct0, userAgent, timeout) {
468
+ this.authToken = authToken;
469
+ this.ct0 = ct0;
470
+ this.userAgent = userAgent;
471
+ this.timeout = timeout;
472
+ }
473
+ async fetchWithTimeout(url, init) {
474
+ if (!this.timeout || this.timeout <= 0) {
475
+ return fetch(url, init);
476
+ }
477
+ const controller = new AbortController;
478
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
479
+ try {
480
+ return await fetch(url, { ...init, signal: controller.signal });
481
+ } finally {
482
+ clearTimeout(timeoutId);
483
+ }
484
+ }
485
+ getHeaders() {
486
+ return {
487
+ accept: "*/*",
488
+ "accept-language": "en-US,en;q=0.9",
489
+ authorization: "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
490
+ "content-type": "application/json",
491
+ "x-csrf-token": this.ct0,
492
+ "x-twitter-auth-type": "OAuth2Session",
493
+ "x-twitter-active-user": "yes",
494
+ "x-twitter-client-language": "en",
495
+ cookie: `auth_token=${this.authToken}; ct0=${this.ct0}`,
496
+ "user-agent": this.userAgent,
497
+ origin: "https://x.com",
498
+ referer: "https://x.com/"
499
+ };
500
+ }
501
+ }
502
+
503
+ // src/client/TwitterClient.ts
504
+ init_query_ids();
505
+
506
+ class TwitterClient {
507
+ graphql;
508
+ rest;
509
+ constructor(options) {
510
+ this.graphql = new GraphQLClient({
511
+ authToken: options.authToken,
512
+ ct0: options.ct0,
513
+ timeout: options.timeout
514
+ });
515
+ this.rest = new RestClient(options.authToken, options.ct0, "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", options.timeout);
516
+ }
517
+ async whoami() {
518
+ const result = await this.graphql.getCurrentUser();
519
+ if (!result.success) {
520
+ return {
521
+ success: false,
522
+ error: result.error,
523
+ code: result.code
524
+ };
525
+ }
526
+ const userData = result.data;
527
+ if (!userData) {
528
+ return {
529
+ success: false,
530
+ error: "Could not extract user data from GraphQL response",
531
+ code: "NETWORK_ERROR"
532
+ };
533
+ }
534
+ const { parseUser: parseUser2 } = await Promise.resolve().then(() => exports_parsers);
535
+ const user = parseUser2(userData);
536
+ if (!user) {
537
+ return {
538
+ success: false,
539
+ error: "Could not parse user data",
540
+ code: "NETWORK_ERROR"
541
+ };
542
+ }
543
+ return {
544
+ success: true,
545
+ user
546
+ };
547
+ }
548
+ async search(query, count = 20, product = "Top") {
549
+ if (count < 1 || count > 200) {
550
+ return {
551
+ success: false,
552
+ error: "count must be between 1 and 200",
553
+ code: "INVALID_INPUT"
554
+ };
555
+ }
556
+ const features = {
557
+ rweb_video_screen_enabled: true,
558
+ profile_label_improvements_pcf_label_in_post_enabled: true,
559
+ responsive_web_profile_redirect_enabled: true,
560
+ rweb_tipjar_consumption_enabled: true,
561
+ verified_phone_label_enabled: false,
562
+ creator_subscriptions_tweet_preview_api_enabled: true,
563
+ responsive_web_graphql_timeline_navigation_enabled: true,
564
+ responsive_web_graphql_exclude_directive_enabled: true,
565
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
566
+ premium_content_api_read_enabled: false,
567
+ communities_web_enable_tweet_community_results_fetch: true,
568
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
569
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
570
+ responsive_web_grok_analyze_post_followups_enabled: false,
571
+ responsive_web_grok_annotations_enabled: false,
572
+ responsive_web_jetfuel_frame: true,
573
+ post_ctas_fetch_enabled: true,
574
+ responsive_web_grok_share_attachment_enabled: true,
575
+ responsive_web_edit_tweet_api_enabled: true,
576
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
577
+ view_counts_everywhere_api_enabled: true,
578
+ longform_notetweets_consumption_enabled: true,
579
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
580
+ tweet_awards_web_tipping_enabled: false,
581
+ responsive_web_grok_show_grok_translated_post: false,
582
+ responsive_web_grok_analysis_button_from_backend: true,
583
+ creator_subscriptions_quote_tweet_preview_enabled: false,
584
+ freedom_of_speech_not_reach_fetch_enabled: true,
585
+ standardized_nudges_misinfo: true,
586
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
587
+ rweb_video_timestamps_enabled: true,
588
+ longform_notetweets_rich_text_read_enabled: true,
589
+ longform_notetweets_inline_media_enabled: true,
590
+ responsive_web_grok_image_annotation_enabled: true,
591
+ responsive_web_grok_imagine_annotation_enabled: true,
592
+ responsive_web_grok_community_note_auto_translation_is_enabled: false,
593
+ articles_preview_enabled: true,
594
+ responsive_web_enhance_cards_enabled: false
595
+ };
596
+ const result = await this.graphql.request("SearchTimeline", {
597
+ rawQuery: query,
598
+ count: Math.min(count, 20),
599
+ querySource: "typed_query",
600
+ product
601
+ }, features);
602
+ if (!result.success) {
603
+ return {
604
+ success: false,
605
+ error: result.error,
606
+ code: result.code
607
+ };
608
+ }
609
+ const { parseTweet: parseTweet2, extractTweetsFromInstructions: extractTweetsFromInstructions2 } = await Promise.resolve().then(() => exports_parsers);
610
+ const instructions = result.data.search_by_raw_query?.search_timeline?.timeline?.instructions || [];
611
+ const tweets = extractTweetsFromInstructions2(instructions);
612
+ return {
613
+ success: true,
614
+ tweets: tweets.slice(0, count)
615
+ };
616
+ }
617
+ async getTweet(tweetIdOrUrl) {
618
+ let tweetId = tweetIdOrUrl;
619
+ const urlMatch = tweetIdOrUrl.match(/status\/(\d+)/);
620
+ if (urlMatch) {
621
+ tweetId = urlMatch[1];
622
+ }
623
+ const result = await this.graphql.request("TweetDetail", {
624
+ focalTweetId: tweetId,
625
+ with_rux_injections: false,
626
+ includePromotedContent: false,
627
+ withCommunity: true,
628
+ withQuickPromoteEligibilityTweetFields: true,
629
+ withBirdwatchNotes: true,
630
+ withVoice: true,
631
+ withV2Timeline: true
632
+ });
633
+ if (!result.success) {
634
+ return {
635
+ success: false,
636
+ error: result.error,
637
+ code: result.code
638
+ };
639
+ }
640
+ const { parseTweet: parseTweet2 } = await Promise.resolve().then(() => exports_parsers);
641
+ const tweetData = result.data.tweet_results?.result;
642
+ const tweet = parseTweet2(tweetData);
643
+ if (!tweet) {
644
+ return {
645
+ success: false,
646
+ error: "Could not parse tweet",
647
+ code: "NETWORK_ERROR"
648
+ };
649
+ }
650
+ return {
651
+ success: true,
652
+ tweet
653
+ };
654
+ }
655
+ async getThread(tweetIdOrUrl) {
656
+ const tweetResult = await this.getTweet(tweetIdOrUrl);
657
+ if (!tweetResult.success) {
658
+ return tweetResult;
659
+ }
660
+ return {
661
+ success: true,
662
+ tweets: [tweetResult.tweet]
663
+ };
664
+ }
665
+ async getUserTweets(username, count = 20, cursor) {
666
+ if (count < 1 || count > 200) {
667
+ return {
668
+ success: false,
669
+ error: "count must be between 1 and 200",
670
+ code: "INVALID_INPUT"
671
+ };
672
+ }
673
+ const queryId = queryIdManager.getQueryId("UserByScreenName");
674
+ const ids = Array.isArray(queryId) ? queryId : [queryId];
675
+ let userId;
676
+ for (const id of ids) {
677
+ try {
678
+ const variables = {
679
+ screen_name: username,
680
+ withSafetyModeUserFields: true
681
+ };
682
+ const features = {
683
+ hidden_profile_subscriptions_enabled: true,
684
+ hidden_profile_likes_enabled: true,
685
+ responsive_web_graphql_exclude_directive_enabled: true,
686
+ verified_phone_label_enabled: false
687
+ };
688
+ const fieldToggles = {
689
+ withAuxiliaryUserLabels: false
690
+ };
691
+ const params = new URLSearchParams({
692
+ variables: JSON.stringify(variables),
693
+ features: JSON.stringify(features),
694
+ fieldToggles: JSON.stringify(fieldToggles)
695
+ });
696
+ const url = `https://x.com/i/api/graphql/${id}/UserByScreenName?${params.toString()}`;
697
+ const response = await this.graphql.fetchWithTimeout(url, {
698
+ method: "GET",
699
+ headers: this.graphql.getHeaders()
700
+ });
701
+ if (!response.ok) {
702
+ if (response.status === 404) {
703
+ continue;
704
+ }
705
+ return {
706
+ success: false,
707
+ error: `HTTP ${response.status}: User lookup failed`,
708
+ code: "NETWORK_ERROR"
709
+ };
710
+ }
711
+ const data = await response.json();
712
+ if (data.data?.user?.result?.__typename === "UserUnavailable") {
713
+ return {
714
+ success: false,
715
+ error: `User @${username} not found`,
716
+ code: "NETWORK_ERROR"
717
+ };
718
+ }
719
+ userId = data.data?.user?.result?.rest_id;
720
+ if (userId)
721
+ break;
722
+ } catch {
723
+ continue;
724
+ }
725
+ }
726
+ if (!userId) {
727
+ return {
728
+ success: false,
729
+ error: "Failed to resolve user ID",
730
+ code: "NETWORK_ERROR"
731
+ };
732
+ }
733
+ const tweetsResult = await this.graphql.request("UserTweets", {
734
+ userId,
735
+ count: Math.min(count, 40),
736
+ cursor,
737
+ includePromotedContent: false,
738
+ withClientEventToken: false,
739
+ withBirdwatchNotes: false,
740
+ withVoice: true,
741
+ withV2Timeline: true
742
+ });
743
+ if (!tweetsResult.success) {
744
+ return {
745
+ success: false,
746
+ error: tweetsResult.error,
747
+ code: tweetsResult.code
748
+ };
749
+ }
750
+ const { extractTweetsFromInstructions: extractTweetsFromInstructions2 } = await Promise.resolve().then(() => exports_parsers);
751
+ const instructions = tweetsResult.data.user?.result?.timeline_response?.timeline?.instructions || [];
752
+ const tweets = extractTweetsFromInstructions2(instructions);
753
+ return {
754
+ success: true,
755
+ tweets: tweets.slice(0, count)
756
+ };
757
+ }
758
+ async tweet(text) {
759
+ if (!text || text.trim().length === 0) {
760
+ return {
761
+ success: false,
762
+ error: "Tweet text cannot be empty",
763
+ code: "INVALID_INPUT"
764
+ };
765
+ }
766
+ if (text.length > 280) {
767
+ return {
768
+ success: false,
769
+ error: "Tweet text cannot exceed 280 characters",
770
+ code: "INVALID_INPUT"
771
+ };
772
+ }
773
+ const result = await this.graphql.request("CreateTweet", {
774
+ tweet_text: text,
775
+ dark_request: false,
776
+ media: {
777
+ media_entities: [],
778
+ possibly_sensitive: false
779
+ },
780
+ semantic_annotation_ids: []
781
+ }, {
782
+ rweb_video_screen_enabled: true,
783
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
784
+ responsive_web_grok_analyze_post_followups_enabled: false,
785
+ responsive_web_grok_annotations_enabled: false,
786
+ responsive_web_grok_show_grok_translated_post: false,
787
+ responsive_web_jetfuel_frame: true,
788
+ longform_notetweets_consumption_enabled: true,
789
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
790
+ tweet_awards_web_tipping_enabled: false,
791
+ responsive_graphql_exclude_directive_enabled: true,
792
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
793
+ responsive_web_graphql_timeline_navigation_enabled: true,
794
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
795
+ verified_phone_label_enabled: false,
796
+ responsive_web_media_download_video_enabled: false
797
+ });
798
+ if (!result.success) {
799
+ return {
800
+ success: false,
801
+ error: result.error,
802
+ code: result.code
803
+ };
804
+ }
805
+ const tweetId = result.data.create_tweet?.tweet_results?.result?.rest_id;
806
+ if (!tweetId) {
807
+ return {
808
+ success: false,
809
+ error: "Could not parse tweet ID from response",
810
+ code: "NETWORK_ERROR"
811
+ };
812
+ }
813
+ return {
814
+ success: true,
815
+ tweetId
816
+ };
817
+ }
818
+ async reply(tweetId, text) {
819
+ if (!text || text.trim().length === 0) {
820
+ return {
821
+ success: false,
822
+ error: "Reply text cannot be empty",
823
+ code: "INVALID_INPUT"
824
+ };
825
+ }
826
+ if (text.length > 280) {
827
+ return {
828
+ success: false,
829
+ error: "Reply text cannot exceed 280 characters",
830
+ code: "INVALID_INPUT"
831
+ };
832
+ }
833
+ const result = await this.graphql.request("CreateTweet", {
834
+ tweet_text: text,
835
+ media: {
836
+ media_entities: [],
837
+ tagged_users: []
838
+ },
839
+ semantic_annotation_ids: [],
840
+ reply_settings: {
841
+ in_reply_to_tweet_id: tweetId
842
+ }
843
+ });
844
+ if (!result.success) {
845
+ return {
846
+ success: false,
847
+ error: result.error,
848
+ code: result.code
849
+ };
850
+ }
851
+ const newTweetId = result.data.create_tweet?.tweet_results?.result?.rest_id;
852
+ if (!newTweetId) {
853
+ return {
854
+ success: false,
855
+ error: "Could not parse tweet ID from response",
856
+ code: "NETWORK_ERROR"
857
+ };
858
+ }
859
+ return {
860
+ success: true,
861
+ tweetId: newTweetId
862
+ };
863
+ }
864
+ async getNews(count = 20) {
865
+ if (count < 1 || count > 200) {
866
+ return {
867
+ success: false,
868
+ error: "count must be between 1 and 200",
869
+ code: "INVALID_INPUT"
870
+ };
871
+ }
872
+ const result = await this.graphql.request("NewsForYou", {
873
+ count: Math.min(count, 40),
874
+ includePromotedContent: false,
875
+ latestControlAvailable: true,
876
+ requestContext: "launch",
877
+ withClientEventToken: false,
878
+ withBirdwatchNotes: false,
879
+ withVoice: true,
880
+ withV2Timeline: true
881
+ });
882
+ if (!result.success) {
883
+ return {
884
+ success: false,
885
+ error: result.error,
886
+ code: result.code
887
+ };
888
+ }
889
+ const { extractTweetsFromInstructions: extractTweetsFromInstructions2 } = await Promise.resolve().then(() => exports_parsers);
890
+ const instructions = result.data.home?.home_timeline_urt?.instructions || [];
891
+ const tweets = extractTweetsFromInstructions2(instructions);
892
+ const items = tweets.slice(0, count).map((tweet) => ({
893
+ id: tweet.id,
894
+ headline: tweet.text,
895
+ tweets: [tweet]
896
+ }));
897
+ return {
898
+ success: true,
899
+ items
900
+ };
901
+ }
902
+ async getMentions(count = 20) {
903
+ if (count < 1 || count > 200) {
904
+ return {
905
+ success: false,
906
+ error: "count must be between 1 and 200",
907
+ code: "INVALID_INPUT"
908
+ };
909
+ }
910
+ const result = await this.graphql.request("MentionsTimeline", {
911
+ count: Math.min(count, 40),
912
+ includePromotedContent: false,
913
+ latestControlAvailable: true,
914
+ requestContext: "launch",
915
+ withClientEventToken: false,
916
+ withBirdwatchNotes: false,
917
+ withVoice: true,
918
+ withV2Timeline: true
919
+ });
920
+ if (!result.success) {
921
+ return {
922
+ success: false,
923
+ error: result.error,
924
+ code: result.code
925
+ };
926
+ }
927
+ const { extractTweetsFromInstructions: extractTweetsFromInstructions2 } = await Promise.resolve().then(() => exports_parsers);
928
+ const instructions = result.data.home?.home_timeline_urt?.instructions || [];
929
+ const tweets = extractTweetsFromInstructions2(instructions);
930
+ return {
931
+ success: true,
932
+ tweets: tweets.slice(0, count)
933
+ };
934
+ }
935
+ }
936
+ // src/cli.ts
937
+ var AUTH_TOKEN = process.env.TWITTER_AUTH_TOKEN || "";
938
+ var CT0 = process.env.TWITTER_CT0 || "";
939
+ if (!AUTH_TOKEN || !CT0) {
940
+ console.error("Error: TWITTER_AUTH_TOKEN and TWITTER_CT0 environment variables must be set");
941
+ process.exit(1);
942
+ }
943
+ var client = new TwitterClient({
944
+ authToken: AUTH_TOKEN,
945
+ ct0: CT0
946
+ });
947
+ var args = process.argv.slice(2);
948
+ var command = args[0];
949
+ async function main() {
950
+ switch (command) {
951
+ case "whoami": {
952
+ const result = await client.whoami();
953
+ if (!result.success) {
954
+ console.error(`Error: ${result.error}`);
955
+ if (result.code === "AUTH_FAILED") {
956
+ console.error(`
957
+ Authentication failed. Please set these environment variables:`);
958
+ console.error(' export TWITTER_AUTH_TOKEN="your_auth_token"');
959
+ console.error(' export TWITTER_CT0="your_ct0_token"');
960
+ console.error(`
961
+ Get credentials from browser DevTools \u2192 Application \u2192 Cookies \u2192 twitter.com`);
962
+ } else if (result.code === "STALE_QUERY_ID") {
963
+ console.error(`
964
+ Query ID may be stale. Try updating:`);
965
+ console.error(" bird update-query-ids");
966
+ }
967
+ process.exit(1);
968
+ }
969
+ console.log(`Logged in as: @${result.user.username} (${result.user.name})`);
970
+ console.log(`User ID: ${result.user.id}`);
971
+ if (result.user.followersCount !== undefined) {
972
+ console.log(`Followers: ${result.user.followersCount.toLocaleString()}`);
973
+ }
974
+ if (result.user.followingCount !== undefined) {
975
+ console.log(`Following: ${result.user.followingCount.toLocaleString()}`);
976
+ }
977
+ break;
978
+ }
979
+ case "search": {
980
+ const latestFlag = args.includes("--latest");
981
+ const filteredArgs = args.filter((arg) => arg !== "--latest");
982
+ const query = filteredArgs[1];
983
+ if (!query) {
984
+ console.error("Usage: bun run src/cli.ts search [--latest] <query>");
985
+ process.exit(1);
986
+ }
987
+ const result = await client.search(query, 20, latestFlag ? "Latest" : "Top");
988
+ if (result.success) {
989
+ result.tweets.forEach((tweet) => {
990
+ console.log(`@${tweet.author.username}: ${tweet.text}`);
991
+ console.log(`Likes: ${tweet.likeCount} Retweets: ${tweet.retweetCount}
992
+ `);
993
+ });
994
+ } else {
995
+ console.error(`Error: ${result.error}`);
996
+ process.exit(1);
997
+ }
998
+ break;
999
+ }
1000
+ case "tweet": {
1001
+ const text = args.slice(1).join(" ");
1002
+ if (!text) {
1003
+ console.error("Usage: bun run src/cli.ts tweet <text>");
1004
+ process.exit(1);
1005
+ }
1006
+ const result = await client.tweet(text);
1007
+ if (result.success) {
1008
+ console.log(`Tweet posted! ID: ${result.tweetId}`);
1009
+ console.log(`https://x.com/user/status/${result.tweetId}`);
1010
+ } else {
1011
+ if (result.error?.includes("226") || result.error?.includes("automated")) {
1012
+ console.error('\u2717 Tweet rejected by Twitter: "Looks automated"');
1013
+ console.error("");
1014
+ console.error("This usually means:");
1015
+ console.error(" \u2022 Account is new or has low activity");
1016
+ console.error(" \u2022 Tweets need more variation/personal touch");
1017
+ console.error(" \u2022 Try posting from the web interface first");
1018
+ } else {
1019
+ console.error(`\u2717 Error: ${result.error}`);
1020
+ }
1021
+ process.exit(1);
1022
+ }
1023
+ break;
1024
+ }
1025
+ case "get": {
1026
+ const tweetId = args[1];
1027
+ if (!tweetId) {
1028
+ console.error("Usage: bun run src/cli.ts get <tweet_id_or_url>");
1029
+ process.exit(1);
1030
+ }
1031
+ const result = await client.getTweet(tweetId);
1032
+ if (result.success) {
1033
+ const tweet = result.tweet;
1034
+ console.log(`@${tweet.author.username} (@${tweet.author.name})`);
1035
+ console.log(`${tweet.text}`);
1036
+ console.log(`
1037
+ Likes: ${tweet.likeCount} Retweets: ${tweet.retweetCount} Replies: ${tweet.replyCount}`);
1038
+ console.log(`https://x.com/${tweet.author.username}/status/${tweet.id}`);
1039
+ } else {
1040
+ console.error(`Error: ${result.error}`);
1041
+ process.exit(1);
1042
+ }
1043
+ break;
1044
+ }
1045
+ case "user": {
1046
+ const username = args[1];
1047
+ if (!username) {
1048
+ console.error("Usage: bun run src/cli.ts user <username>");
1049
+ process.exit(1);
1050
+ }
1051
+ const result = await client.getUserTweets(username);
1052
+ if (result.success) {
1053
+ result.tweets.forEach((tweet) => {
1054
+ console.log(`@${tweet.author.username}: ${tweet.text}`);
1055
+ console.log(`Likes: ${tweet.likeCount} Retweets: ${tweet.retweetCount}
1056
+ `);
1057
+ });
1058
+ } else {
1059
+ console.error(`Error: ${result.error}`);
1060
+ process.exit(1);
1061
+ }
1062
+ break;
1063
+ }
1064
+ case "news": {
1065
+ const result = await client.getNews();
1066
+ if (result.success) {
1067
+ result.items.forEach((item, i) => {
1068
+ console.log(`${i + 1}. ${item.headline}`);
1069
+ if (item.tweets && item.tweets[0]) {
1070
+ console.log(` @${item.tweets[0].author.username}
1071
+ `);
1072
+ }
1073
+ });
1074
+ } else {
1075
+ console.error(`Error: ${result.error}`);
1076
+ process.exit(1);
1077
+ }
1078
+ break;
1079
+ }
1080
+ case "mentions": {
1081
+ const result = await client.getMentions();
1082
+ if (result.success) {
1083
+ result.tweets.forEach((tweet) => {
1084
+ console.log(`@${tweet.author.username}: ${tweet.text}`);
1085
+ console.log(`Likes: ${tweet.likeCount} Retweets: ${tweet.retweetCount}
1086
+ `);
1087
+ });
1088
+ } else {
1089
+ console.error(`Error: ${result.error}`);
1090
+ process.exit(1);
1091
+ }
1092
+ break;
1093
+ }
1094
+ case "update-query-ids": {
1095
+ const { QueryIdManager: QueryIdManager2 } = await Promise.resolve().then(() => (init_query_ids(), exports_query_ids));
1096
+ const manager = new QueryIdManager2;
1097
+ const result = await manager.updateQueryIds({ force: args.includes("--fresh") });
1098
+ if (result.updated === 0) {
1099
+ console.log("Query IDs are up to date (cache is fresh).");
1100
+ } else {
1101
+ console.log(`\u2713 Updated ${result.updated} query ID(s):`);
1102
+ result.changes.forEach((change) => {
1103
+ console.log(` ${change}`);
1104
+ });
1105
+ }
1106
+ break;
1107
+ }
1108
+ default:
1109
+ console.log("Twitter Client CLI");
1110
+ console.log(`
1111
+ Commands:`);
1112
+ console.log(" whoami - Show current user info");
1113
+ console.log(" search [--latest] <query> - Search for tweets (default: top, use --latest for recent)");
1114
+ console.log(" tweet <text> - Post a tweet");
1115
+ console.log(" get <tweet_id> - Get a tweet by ID or URL");
1116
+ console.log(" user <username> - Get tweets from a user");
1117
+ console.log(" news - Get news timeline");
1118
+ console.log(" mentions - Get mentions timeline");
1119
+ console.log(" update-query-ids [--fresh] - Update query IDs from Twitter");
1120
+ console.log(`
1121
+ Flags:`);
1122
+ console.log(" --fresh - Force refresh even if cache is fresh");
1123
+ break;
1124
+ }
1125
+ }
1126
+ main().catch((err) => {
1127
+ console.error(err);
1128
+ process.exit(1);
1129
+ });