@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.
@@ -0,0 +1,191 @@
1
+ import { randomUUID, randomBytes } from 'node:crypto';
2
+ import { queryIdManager } from './query-ids.js';
3
+ export class GraphQLClient {
4
+ authToken;
5
+ ct0;
6
+ userAgent;
7
+ timeout;
8
+ clientUuid;
9
+ clientDeviceId;
10
+ constructor(options) {
11
+ this.authToken = options.authToken;
12
+ this.ct0 = options.ct0;
13
+ this.timeout = options.timeout;
14
+ this.userAgent =
15
+ options.userAgent ||
16
+ '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';
17
+ this.clientUuid = randomUUID();
18
+ this.clientDeviceId = randomUUID();
19
+ }
20
+ createTransactionId() {
21
+ return randomBytes(16).toString('hex');
22
+ }
23
+ getHeaders() {
24
+ return {
25
+ accept: '*/*',
26
+ 'accept-language': 'en-US,en;q=0.9',
27
+ authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
28
+ 'content-type': 'application/json',
29
+ 'x-csrf-token': this.ct0,
30
+ 'x-twitter-auth-type': 'OAuth2Session',
31
+ 'x-twitter-active-user': 'yes',
32
+ 'x-twitter-client-language': 'en',
33
+ 'x-client-uuid': this.clientUuid,
34
+ 'x-twitter-client-deviceid': this.clientDeviceId,
35
+ 'x-client-transaction-id': this.createTransactionId(),
36
+ cookie: `auth_token=${this.authToken}; ct0=${this.ct0}`,
37
+ 'user-agent': this.userAgent,
38
+ origin: 'https://x.com',
39
+ referer: 'https://x.com/',
40
+ };
41
+ }
42
+ async fetchWithTimeout(url, init) {
43
+ if (!this.timeout || this.timeout <= 0) {
44
+ return fetch(url, init);
45
+ }
46
+ const controller = new AbortController();
47
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
48
+ try {
49
+ return await fetch(url, { ...init, signal: controller.signal });
50
+ }
51
+ finally {
52
+ clearTimeout(timeoutId);
53
+ }
54
+ }
55
+ getErrorCode(status, errors) {
56
+ if (status === 401 || status === 403) {
57
+ return 'AUTH_FAILED';
58
+ }
59
+ if (status === 429) {
60
+ return 'RATE_LIMITED';
61
+ }
62
+ if (status === 404) {
63
+ return 'STALE_QUERY_ID';
64
+ }
65
+ if (errors?.some((e) => e.code === 226)) {
66
+ return 'RATE_LIMITED';
67
+ }
68
+ return 'NETWORK_ERROR';
69
+ }
70
+ async validateResponse(response) {
71
+ const data = await response.json();
72
+ if (data.errors && data.errors.length > 0) {
73
+ return {
74
+ success: false,
75
+ error: data.errors.map((e) => e.message).join(', '),
76
+ code: this.getErrorCode(500, data.errors),
77
+ };
78
+ }
79
+ if (!data.data) {
80
+ return {
81
+ success: false,
82
+ error: 'No data returned from API',
83
+ code: 'NETWORK_ERROR',
84
+ };
85
+ }
86
+ return {
87
+ success: true,
88
+ data: data.data,
89
+ };
90
+ }
91
+ async request(operation, variables, features) {
92
+ let queryId = queryIdManager.getQueryId(operation);
93
+ let queryIds = Array.isArray(queryId) ? queryId : [queryId];
94
+ let hasRefreshed = false; // Track if we've already attempted refresh
95
+ // Try each query ID in order
96
+ for (let i = 0; i < queryIds.length; i++) {
97
+ const id = queryIds[i];
98
+ const url = `https://x.com/i/api/graphql/${id}/${operation}`;
99
+ try {
100
+ const body = JSON.stringify({
101
+ variables,
102
+ features,
103
+ queryId: id,
104
+ });
105
+ const response = await this.fetchWithTimeout(url, {
106
+ method: 'POST',
107
+ headers: this.getHeaders(),
108
+ body,
109
+ });
110
+ if (!response.ok) {
111
+ const code = this.getErrorCode(response.status);
112
+ // Auto-refresh on 404 (except for last query ID)
113
+ if (response.status === 404 && i < queryIds.length - 1) {
114
+ continue; // Try next fallback ID
115
+ }
116
+ // If 404 and we have no more fallbacks, try refreshing once (but only once per request)
117
+ if (response.status === 404 && i === queryIds.length - 1 && !hasRefreshed) {
118
+ console.info(`Query ID stale for ${operation}, refreshing...`);
119
+ hasRefreshed = true; // Mark that we've attempted refresh
120
+ try {
121
+ await queryIdManager.updateQueryIds({ force: true });
122
+ // Get fresh query IDs and continue the loop with them
123
+ queryId = queryIdManager.getQueryId(operation);
124
+ queryIds = Array.isArray(queryId) ? queryId : [queryId];
125
+ // Reset index to try fresh IDs from the beginning
126
+ i = -1;
127
+ continue;
128
+ }
129
+ catch (refreshError) {
130
+ return {
131
+ success: false,
132
+ error: `Failed to refresh query IDs: ${refreshError instanceof Error ? refreshError.message : 'Unknown error'}`,
133
+ code: 'NETWORK_ERROR',
134
+ };
135
+ }
136
+ }
137
+ // For other errors or if we've already refreshed, return error
138
+ const text = await response.text();
139
+ return {
140
+ success: false,
141
+ error: `HTTP ${response.status}: ${text.slice(0, 200)}`,
142
+ code,
143
+ };
144
+ }
145
+ return await this.validateResponse(response);
146
+ }
147
+ catch (error) {
148
+ if (error instanceof Error && error.name === 'AbortError') {
149
+ return {
150
+ success: false,
151
+ error: 'Request timeout',
152
+ code: 'NETWORK_ERROR',
153
+ };
154
+ }
155
+ return {
156
+ success: false,
157
+ error: error instanceof Error ? error.message : 'Unknown error',
158
+ code: 'NETWORK_ERROR',
159
+ };
160
+ }
161
+ }
162
+ return {
163
+ success: false,
164
+ error: 'All query IDs failed',
165
+ code: 'STALE_QUERY_ID',
166
+ };
167
+ }
168
+ /**
169
+ * Fetch the authenticated user's information using GraphQL
170
+ */
171
+ async getCurrentUser() {
172
+ return await this.request('AboutAccountQuery', {}, {
173
+ hidden_profile_subscriptions_enabled: true,
174
+ hidden_profile_likes_enabled: true,
175
+ responsive_web_graphql_exclude_directive_enabled: true,
176
+ verified_phone_label_enabled: false,
177
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
178
+ responsive_web_graphql_timeline_navigation_enabled: true,
179
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
180
+ responsive_web_grok_analyze_post_followups_enabled: false,
181
+ responsive_web_grok_annotations_enabled: false,
182
+ responsive_web_grok_show_grok_translated_post: false,
183
+ responsive_web_jetfuel_frame: true,
184
+ longform_notetweets_consumption_enabled: true,
185
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
186
+ tweet_awards_web_tipping_enabled: false,
187
+ responsive_web_media_download_video_enabled: false,
188
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
189
+ });
190
+ }
191
+ }
@@ -0,0 +1,14 @@
1
+ import type { Tweet, User } from './types.js';
2
+ /**
3
+ * Parse tweet object from GraphQL response
4
+ */
5
+ export declare function parseTweet(tweetData: any): Tweet | null;
6
+ /**
7
+ * Parse user object from GraphQL response
8
+ */
9
+ export declare function parseUser(userData: any): User | null;
10
+ /**
11
+ * Extract tweets from timeline instructions
12
+ */
13
+ export declare function extractTweetsFromInstructions(instructions: any[]): Tweet[];
14
+ //# sourceMappingURL=parsers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsers.d.ts","sourceRoot":"","sources":["../../src/client/parsers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAa,MAAM,YAAY,CAAC;AAEzD;;GAEG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,GAAG,GAAG,KAAK,GAAG,IAAI,CAoGvD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,CAkBpD;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,CAoC1E"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Parse tweet object from GraphQL response
3
+ */
4
+ export function parseTweet(tweetData) {
5
+ if (!tweetData)
6
+ return null;
7
+ const legacy = tweetData.legacy || tweetData;
8
+ const core = tweetData.core?.user_results?.result;
9
+ const userLegacy = core?.legacy;
10
+ const userCore = core?.core;
11
+ if (!legacy)
12
+ return null;
13
+ // Extract username from either legacy or core (Twitter uses different locations)
14
+ const username = userLegacy?.screen_name || userCore?.screen_name || '';
15
+ const name = userLegacy?.name || userCore?.name || username;
16
+ if (!username) {
17
+ // Skip tweets without identifiable author
18
+ return null;
19
+ }
20
+ const id = legacy.id_str || legacy.rest_id || tweetData.rest_id || '';
21
+ const tweet = {
22
+ id,
23
+ url: `https://x.com/${username}/status/${id}`,
24
+ text: legacy.full_text || legacy.text || '',
25
+ author: {
26
+ id: core?.rest_id || userLegacy?.id_str || '',
27
+ username,
28
+ name,
29
+ },
30
+ createdAt: legacy.created_at || '',
31
+ replyCount: legacy.reply_count || 0,
32
+ retweetCount: legacy.retweet_count || 0,
33
+ likeCount: legacy.favorite_count || 0,
34
+ viewCount: tweetData.views?.count ? parseInt(tweetData.views.count, 10) : undefined,
35
+ bookmarkCount: legacy.bookmark_count,
36
+ };
37
+ // Optional fields
38
+ if (legacy.conversation_id_str) {
39
+ tweet.conversationId = legacy.conversation_id_str;
40
+ }
41
+ if (legacy.in_reply_to_status_id_str) {
42
+ tweet.inReplyToStatusId = legacy.in_reply_to_status_id_str;
43
+ }
44
+ // Parse media with enhanced fields (width, height, preview, video info)
45
+ if (legacy.extended_entities?.media) {
46
+ tweet.media = legacy.extended_entities.media.map((m) => {
47
+ const type = m.type === 'video' || m.type === 'animated_gif' ? (m.type === 'animated_gif' ? 'gif' : 'video') : 'photo';
48
+ const mediaItem = {
49
+ type,
50
+ url: m.media_url_https || '',
51
+ altText: m.alt_text,
52
+ };
53
+ // Add dimensions from largest available size
54
+ const sizes = m.sizes;
55
+ if (sizes?.large) {
56
+ mediaItem.width = sizes.large.w;
57
+ mediaItem.height = sizes.large.h;
58
+ }
59
+ else if (sizes?.medium) {
60
+ mediaItem.width = sizes.medium.w;
61
+ mediaItem.height = sizes.medium.h;
62
+ }
63
+ // Add thumbnail/preview URL
64
+ if (sizes?.small) {
65
+ mediaItem.previewUrl = `${m.media_url_https}:small`;
66
+ }
67
+ // Extract video URL and duration for videos/gifs
68
+ if ((type === 'video' || type === 'gif') && m.video_info?.variants) {
69
+ // Prefer highest bitrate MP4, fall back to first MP4
70
+ const mp4Variants = m.video_info.variants.filter((v) => v.content_type === 'video/mp4' && typeof v.url === 'string');
71
+ const mp4WithBitrate = mp4Variants
72
+ .filter((v) => typeof v.bitrate === 'number')
73
+ .sort((a, b) => b.bitrate - a.bitrate);
74
+ const selectedVariant = mp4WithBitrate[0] ?? mp4Variants[0];
75
+ if (selectedVariant) {
76
+ mediaItem.videoUrl = selectedVariant.url;
77
+ }
78
+ // Add video duration
79
+ if (typeof m.video_info.duration_millis === 'number') {
80
+ mediaItem.durationMs = m.video_info.duration_millis;
81
+ }
82
+ }
83
+ return mediaItem;
84
+ });
85
+ }
86
+ // Parse quoted tweet
87
+ if (legacy.quoted_status_result?.result) {
88
+ const quoted = parseTweet(legacy.quoted_status_result.result);
89
+ if (quoted)
90
+ tweet.quotedTweet = quoted;
91
+ }
92
+ return tweet;
93
+ }
94
+ /**
95
+ * Parse user object from GraphQL response
96
+ */
97
+ export function parseUser(userData) {
98
+ if (!userData)
99
+ return null;
100
+ const legacy = userData.legacy || userData;
101
+ const user = {
102
+ id: userData.rest_id || legacy.id_str || '',
103
+ username: legacy.screen_name || '',
104
+ name: legacy.name || '',
105
+ description: legacy.description,
106
+ followersCount: legacy.followers_count,
107
+ followingCount: legacy.friends_count,
108
+ isBlueVerified: legacy.verified || legacy.is_blue_verified,
109
+ profileImageUrl: legacy.profile_image_url_https,
110
+ createdAt: legacy.created_at,
111
+ };
112
+ return user;
113
+ }
114
+ /**
115
+ * Extract tweets from timeline instructions
116
+ */
117
+ export function extractTweetsFromInstructions(instructions) {
118
+ const tweets = [];
119
+ const seenIds = new Set();
120
+ for (const instruction of instructions) {
121
+ if (!instruction)
122
+ continue;
123
+ // Handle different instruction types
124
+ const entries = instruction.entries ||
125
+ instruction.moduleItems ||
126
+ [];
127
+ for (const entry of entries) {
128
+ if (!entry)
129
+ continue;
130
+ const content = entry.content || entry.item?.content;
131
+ if (!content)
132
+ continue;
133
+ // Try to find tweet data in various possible locations
134
+ const tweetData = content.tweet_results?.result ||
135
+ content.tweet?.tweet_results?.result ||
136
+ content.itemContent?.tweet_results?.result;
137
+ if (tweetData) {
138
+ const tweet = parseTweet(tweetData);
139
+ if (tweet && !seenIds.has(tweet.id)) {
140
+ tweets.push(tweet);
141
+ seenIds.add(tweet.id);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return tweets;
147
+ }
@@ -0,0 +1,22 @@
1
+ export declare class QueryIdManager {
2
+ private cache;
3
+ constructor();
4
+ private loadCache;
5
+ private saveCache;
6
+ getQueryId(operation: string): string | string[];
7
+ /**
8
+ * Fetch fresh query IDs from Twitter's web client bundle
9
+ */
10
+ scrapeQueryIds(): Promise<Record<string, string>>;
11
+ /**
12
+ * Update query IDs by scraping Twitter's web client
13
+ */
14
+ updateQueryIds(options?: {
15
+ force?: boolean;
16
+ }): Promise<{
17
+ updated: number;
18
+ changes: string[];
19
+ }>;
20
+ }
21
+ export declare const queryIdManager: QueryIdManager;
22
+ //# sourceMappingURL=query-ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-ids.d.ts","sourceRoot":"","sources":["../../src/client/query-ids.ts"],"names":[],"mappings":"AAkBA,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAyC;;IAMtD,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,SAAS;IAiBjB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE;IAYhD;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IA+CvD;;OAEG;IACG,cAAc,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAkDrG;AAED,eAAO,MAAM,cAAc,gBAAuB,CAAC"}
@@ -0,0 +1,157 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ const BAKED_IDS = {
5
+ CreateTweet: 'nmdAQXJDxw6-0KKF2on7eA',
6
+ TweetDetail: ['_NvJCnIjOW__EP5-RF197A', '97JF30KziU00483E_8elBA', 'aFvUsJm2c-oDkJV75blV6g'],
7
+ SearchTimeline: ['6AAys3t42mosm_yTI_QENg', 'M1jEez78PEfVfbQLvlWMvQ', '5h0kNbk3ii97rmfY6CdgAA'],
8
+ UserByScreenName: ['xc8f1g7BYqr6VTzTbvNlGw', 'qW5u-DAuXpMEG0zA1F7UGQ', 'sLVLhk0bGj3MVFEKTdax1w'],
9
+ UserTweets: 'Wms1GvIiHXAPBaCr9KblaA',
10
+ AboutAccountQuery: ['zs_jFPFT78rBpXv9Z3U2YQ', 'cY6EpQYqLfc7LGIJCevPqA'],
11
+ MentionsTimeline: 'Wms1GvIiHXAPBaCr9KblaA',
12
+ };
13
+ const CACHE_PATH = join(homedir(), '.config', 'twitter-client', 'query-ids.json');
14
+ const CACHE_VERSION = 'v1';
15
+ const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
16
+ export class QueryIdManager {
17
+ cache = {};
18
+ constructor() {
19
+ this.loadCache();
20
+ }
21
+ loadCache() {
22
+ try {
23
+ const content = readFileSync(CACHE_PATH, 'utf-8');
24
+ const data = JSON.parse(content);
25
+ // Check cache version and TTL
26
+ if (data.version !== CACHE_VERSION) {
27
+ return; // Version mismatch, use baked-in
28
+ }
29
+ if (data.timestamp && Date.now() - data.timestamp > CACHE_TTL) {
30
+ return; // Cache expired, use baked-in
31
+ }
32
+ this.cache = data.queryIds || {};
33
+ }
34
+ catch {
35
+ // Cache doesn't exist or is invalid, use baked-in
36
+ this.cache = {};
37
+ }
38
+ }
39
+ saveCache(queryIds) {
40
+ try {
41
+ const dir = dirname(CACHE_PATH);
42
+ mkdirSync(dir, { recursive: true });
43
+ const data = {
44
+ version: CACHE_VERSION,
45
+ timestamp: Date.now(),
46
+ queryIds,
47
+ };
48
+ writeFileSync(CACHE_PATH, JSON.stringify(data, null, 2));
49
+ }
50
+ catch (error) {
51
+ console.warn(`Failed to save query ID cache: ${error}`);
52
+ }
53
+ }
54
+ getQueryId(operation) {
55
+ // Check cache first
56
+ if (this.cache[operation]) {
57
+ return this.cache[operation];
58
+ }
59
+ // Fall back to baked-in
60
+ if (BAKED_IDS[operation]) {
61
+ return BAKED_IDS[operation];
62
+ }
63
+ throw new Error(`Unknown operation: ${operation}`);
64
+ }
65
+ /**
66
+ * Fetch fresh query IDs from Twitter's web client bundle
67
+ */
68
+ async scrapeQueryIds() {
69
+ const operations = Object.keys(BAKED_IDS);
70
+ const queryIds = {};
71
+ // Fetch Twitter's main client JS bundle
72
+ const bundleUrl = 'https://abs.twimg.com/responsive-web/client-web/main.' + Date.now() + '.js';
73
+ try {
74
+ const response = await fetch(bundleUrl);
75
+ if (!response.ok) {
76
+ throw new Error(`Failed to fetch bundle: ${response.status}`);
77
+ }
78
+ const js = await response.text();
79
+ // Extract query IDs using regex
80
+ // Pattern: operationName:"queryId"
81
+ for (const op of operations) {
82
+ // Try multiple regex patterns
83
+ const patterns = [
84
+ new RegExp(`"${op}":"([A-Za-z0-9]+)"`, 'g'),
85
+ new RegExp(`queryId:\\s*"([A-Za-z0-9]+)"\\s*,\\s*\\w*:\\s*\\w*:\\s*\\{\\s*"name":\\s*"${op}"`, 'g'),
86
+ new RegExp(`"queryId":\\s*"([A-Za-z0-9]+)"`, 'g'),
87
+ ];
88
+ for (const pattern of patterns) {
89
+ const matches = js.matchAll(pattern);
90
+ const ids = Array.from(matches).map(m => m[1]).filter(id => id.length >= 15 && id.length <= 25);
91
+ if (ids.length > 0) {
92
+ queryIds[op] = ids[0]; // Use first match
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ catch (error) {
99
+ console.warn(`Scraping failed, using baked-in IDs: ${error}`);
100
+ // Return baked-in IDs as fallback
101
+ for (const op of operations) {
102
+ const id = BAKED_IDS[op];
103
+ queryIds[op] = Array.isArray(id) ? id[0] : id;
104
+ }
105
+ }
106
+ return queryIds;
107
+ }
108
+ /**
109
+ * Update query IDs by scraping Twitter's web client
110
+ */
111
+ async updateQueryIds(options) {
112
+ const changes = [];
113
+ // Check if cache is still fresh (unless forced)
114
+ if (!options?.force) {
115
+ try {
116
+ const content = readFileSync(CACHE_PATH, 'utf-8');
117
+ const data = JSON.parse(content);
118
+ const age = Date.now() - (data.timestamp || 0);
119
+ if (age < CACHE_TTL) {
120
+ return { updated: 0, changes: [] };
121
+ }
122
+ }
123
+ catch {
124
+ // Cache doesn't exist, proceed with update
125
+ }
126
+ }
127
+ // Scrape fresh IDs
128
+ const freshIds = await this.scrapeQueryIds();
129
+ // Build new cache with fresh IDs
130
+ const newCache = {};
131
+ for (const [op, freshId] of Object.entries(freshIds)) {
132
+ const oldId = this.cache[op] || BAKED_IDS[op];
133
+ const oldIdStr = Array.isArray(oldId) ? oldId[0] : oldId;
134
+ if (freshId !== oldIdStr) {
135
+ changes.push(`${op}: ${oldIdStr} → ${freshId}`);
136
+ }
137
+ // For operations with fallbacks, include multiple IDs
138
+ if (op === 'TweetDetail' || op === 'SearchTimeline') {
139
+ const bakedFallbacks = BAKED_IDS[op];
140
+ if (Array.isArray(bakedFallbacks)) {
141
+ newCache[op] = [freshId, ...bakedFallbacks.slice(1)];
142
+ }
143
+ else {
144
+ newCache[op] = freshId;
145
+ }
146
+ }
147
+ else {
148
+ newCache[op] = freshId;
149
+ }
150
+ }
151
+ // Save to cache
152
+ this.saveCache(newCache);
153
+ this.cache = newCache;
154
+ return { updated: changes.length, changes };
155
+ }
156
+ }
157
+ export const queryIdManager = new QueryIdManager();
@@ -0,0 +1,10 @@
1
+ export declare class RestClient {
2
+ private authToken;
3
+ private ct0;
4
+ private userAgent;
5
+ private timeout;
6
+ constructor(authToken: string, ct0: string, userAgent: string, timeout?: number);
7
+ private fetchWithTimeout;
8
+ private getHeaders;
9
+ }
10
+ //# sourceMappingURL=rest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/client/rest.ts"],"names":[],"mappings":"AAEA,qBAAa,UAAU;IAInB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,SAAS;IALnB,OAAO,CAAC,OAAO,CAAqB;gBAG1B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACzB,OAAO,CAAC,EAAE,MAAM;YAKJ,gBAAgB;IAe9B,OAAO,CAAC,UAAU;CAgBnB"}
@@ -0,0 +1,41 @@
1
+ export class RestClient {
2
+ authToken;
3
+ ct0;
4
+ userAgent;
5
+ timeout;
6
+ constructor(authToken, ct0, userAgent, timeout) {
7
+ this.authToken = authToken;
8
+ this.ct0 = ct0;
9
+ this.userAgent = userAgent;
10
+ this.timeout = timeout;
11
+ }
12
+ async fetchWithTimeout(url, init) {
13
+ if (!this.timeout || this.timeout <= 0) {
14
+ return fetch(url, init);
15
+ }
16
+ const controller = new AbortController();
17
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
18
+ try {
19
+ return await fetch(url, { ...init, signal: controller.signal });
20
+ }
21
+ finally {
22
+ clearTimeout(timeoutId);
23
+ }
24
+ }
25
+ getHeaders() {
26
+ return {
27
+ 'accept': '*/*',
28
+ 'accept-language': 'en-US,en;q=0.9',
29
+ 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
30
+ 'content-type': 'application/json',
31
+ 'x-csrf-token': this.ct0,
32
+ 'x-twitter-auth-type': 'OAuth2Session',
33
+ 'x-twitter-active-user': 'yes',
34
+ 'x-twitter-client-language': 'en',
35
+ 'cookie': `auth_token=${this.authToken}; ct0=${this.ct0}`,
36
+ 'user-agent': this.userAgent,
37
+ 'origin': 'https://x.com',
38
+ 'referer': 'https://x.com/',
39
+ };
40
+ }
41
+ }
@@ -0,0 +1,80 @@
1
+ export interface Tweet {
2
+ id: string;
3
+ url: string;
4
+ text: string;
5
+ author: {
6
+ username: string;
7
+ name: string;
8
+ id: string;
9
+ };
10
+ createdAt: string;
11
+ replyCount: number;
12
+ retweetCount: number;
13
+ likeCount: number;
14
+ viewCount?: number;
15
+ bookmarkCount?: number;
16
+ conversationId?: string;
17
+ inReplyToStatusId?: string;
18
+ quotedTweet?: Tweet;
19
+ media?: MediaItem[];
20
+ }
21
+ export interface MediaItem {
22
+ type: 'photo' | 'video' | 'gif';
23
+ url: string;
24
+ altText?: string;
25
+ width?: number;
26
+ height?: number;
27
+ previewUrl?: string;
28
+ videoUrl?: string;
29
+ durationMs?: number;
30
+ }
31
+ export interface User {
32
+ id: string;
33
+ username: string;
34
+ name: string;
35
+ description?: string;
36
+ followersCount?: number;
37
+ followingCount?: number;
38
+ isBlueVerified?: boolean;
39
+ profileImageUrl?: string;
40
+ createdAt?: string;
41
+ }
42
+ export interface NewsItem {
43
+ id: string;
44
+ headline: string;
45
+ category?: string;
46
+ timeAgo?: string;
47
+ postCount?: number;
48
+ description?: string;
49
+ url?: string;
50
+ tweets?: Tweet[];
51
+ }
52
+ export type SuccessResult<T> = {
53
+ success: true;
54
+ } & T;
55
+ export type ErrorResult = {
56
+ success: false;
57
+ error: string;
58
+ code?: ErrorCode;
59
+ };
60
+ export type Result<T> = SuccessResult<T> | ErrorResult;
61
+ export type ErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'INVALID_INPUT' | 'NETWORK_ERROR' | 'STALE_QUERY_ID';
62
+ export type TweetResult = Result<{
63
+ tweet: Tweet;
64
+ }>;
65
+ export type TweetsResult = Result<{
66
+ tweets: Tweet[];
67
+ nextCursor?: string;
68
+ }>;
69
+ export type PostResult = Result<{
70
+ tweetId: string;
71
+ }>;
72
+ export type UserResult = Result<{
73
+ user: User;
74
+ }>;
75
+ export type NewsResult = Result<{
76
+ items: NewsItem[];
77
+ }>;
78
+ export type SearchResult = TweetsResult;
79
+ export type ThreadResult = TweetsResult;
80
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"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"}