@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/LICENSE +21 -0
- package/README.md +360 -0
- package/bin/twitter.js +1129 -0
- package/dist/cli/output.d.ts +22 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +71 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +196 -0
- package/dist/client/TwitterClient.d.ts +47 -0
- package/dist/client/TwitterClient.d.ts.map +1 -0
- package/dist/client/TwitterClient.js +472 -0
- package/dist/client/auth.d.ts +21 -0
- package/dist/client/auth.d.ts.map +1 -0
- package/dist/client/auth.js +42 -0
- package/dist/client/graphql.d.ts +49 -0
- package/dist/client/graphql.d.ts.map +1 -0
- package/dist/client/graphql.js +191 -0
- package/dist/client/parsers.d.ts +14 -0
- package/dist/client/parsers.d.ts.map +1 -0
- package/dist/client/parsers.js +147 -0
- package/dist/client/query-ids.d.ts +22 -0
- package/dist/client/query-ids.d.ts.map +1 -0
- package/dist/client/query-ids.js +157 -0
- package/dist/client/rest.d.ts +10 -0
- package/dist/client/rest.d.ts.map +1 -0
- package/dist/client/rest.js +41 -0
- package/dist/client/types.d.ts +80 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { GraphQLClient } from './graphql.js';
|
|
2
|
+
import { RestClient } from './rest.js';
|
|
3
|
+
import { queryIdManager } from './query-ids.js';
|
|
4
|
+
export class TwitterClient {
|
|
5
|
+
graphql;
|
|
6
|
+
rest;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.graphql = new GraphQLClient({
|
|
9
|
+
authToken: options.authToken,
|
|
10
|
+
ct0: options.ct0,
|
|
11
|
+
timeout: options.timeout,
|
|
12
|
+
});
|
|
13
|
+
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);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Verify authentication and get current user info
|
|
17
|
+
*/
|
|
18
|
+
async whoami() {
|
|
19
|
+
const result = await this.graphql.getCurrentUser();
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: result.error,
|
|
24
|
+
code: result.code,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Parse user data from GraphQL response
|
|
28
|
+
// The response structure is: result.data (which is AboutAccountResponse)
|
|
29
|
+
const userData = result.data;
|
|
30
|
+
if (!userData) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: 'Could not extract user data from GraphQL response',
|
|
34
|
+
code: 'NETWORK_ERROR',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Use existing parseUser function
|
|
38
|
+
const { parseUser } = await import('./parsers.js');
|
|
39
|
+
const user = parseUser(userData);
|
|
40
|
+
if (!user) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
error: 'Could not parse user data',
|
|
44
|
+
code: 'NETWORK_ERROR',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
user,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Search for tweets
|
|
54
|
+
*/
|
|
55
|
+
async search(query, count = 20, product = 'Top') {
|
|
56
|
+
if (count < 1 || count > 200) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: 'count must be between 1 and 200',
|
|
60
|
+
code: 'INVALID_INPUT',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Build features for search (enables view counts and other fields)
|
|
64
|
+
const features = {
|
|
65
|
+
rweb_video_screen_enabled: true,
|
|
66
|
+
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
67
|
+
responsive_web_profile_redirect_enabled: true,
|
|
68
|
+
rweb_tipjar_consumption_enabled: true,
|
|
69
|
+
verified_phone_label_enabled: false,
|
|
70
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
71
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
72
|
+
responsive_web_graphql_exclude_directive_enabled: true,
|
|
73
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
74
|
+
premium_content_api_read_enabled: false,
|
|
75
|
+
communities_web_enable_tweet_community_results_fetch: true,
|
|
76
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
77
|
+
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
|
78
|
+
responsive_web_grok_analyze_post_followups_enabled: false,
|
|
79
|
+
responsive_web_grok_annotations_enabled: false,
|
|
80
|
+
responsive_web_jetfuel_frame: true,
|
|
81
|
+
post_ctas_fetch_enabled: true,
|
|
82
|
+
responsive_web_grok_share_attachment_enabled: true,
|
|
83
|
+
responsive_web_edit_tweet_api_enabled: true,
|
|
84
|
+
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
85
|
+
view_counts_everywhere_api_enabled: true,
|
|
86
|
+
longform_notetweets_consumption_enabled: true,
|
|
87
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
88
|
+
tweet_awards_web_tipping_enabled: false,
|
|
89
|
+
responsive_web_grok_show_grok_translated_post: false,
|
|
90
|
+
responsive_web_grok_analysis_button_from_backend: true,
|
|
91
|
+
creator_subscriptions_quote_tweet_preview_enabled: false,
|
|
92
|
+
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
93
|
+
standardized_nudges_misinfo: true,
|
|
94
|
+
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
95
|
+
rweb_video_timestamps_enabled: true,
|
|
96
|
+
longform_notetweets_rich_text_read_enabled: true,
|
|
97
|
+
longform_notetweets_inline_media_enabled: true,
|
|
98
|
+
responsive_web_grok_image_annotation_enabled: true,
|
|
99
|
+
responsive_web_grok_imagine_annotation_enabled: true,
|
|
100
|
+
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
|
101
|
+
articles_preview_enabled: true,
|
|
102
|
+
responsive_web_enhance_cards_enabled: false,
|
|
103
|
+
};
|
|
104
|
+
const result = await this.graphql.request('SearchTimeline', {
|
|
105
|
+
rawQuery: query,
|
|
106
|
+
count: Math.min(count, 20),
|
|
107
|
+
querySource: 'typed_query',
|
|
108
|
+
product: product,
|
|
109
|
+
}, features);
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: result.error,
|
|
114
|
+
code: result.code,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const { parseTweet, extractTweetsFromInstructions } = await import('./parsers.js');
|
|
118
|
+
const instructions = result.data.search_by_raw_query?.search_timeline?.timeline?.instructions || [];
|
|
119
|
+
const tweets = extractTweetsFromInstructions(instructions);
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
tweets: tweets.slice(0, count),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get a single tweet by ID or URL
|
|
127
|
+
*/
|
|
128
|
+
async getTweet(tweetIdOrUrl) {
|
|
129
|
+
// Extract tweet ID from URL if needed
|
|
130
|
+
let tweetId = tweetIdOrUrl;
|
|
131
|
+
const urlMatch = tweetIdOrUrl.match(/status\/(\d+)/);
|
|
132
|
+
if (urlMatch) {
|
|
133
|
+
tweetId = urlMatch[1];
|
|
134
|
+
}
|
|
135
|
+
const result = await this.graphql.request('TweetDetail', {
|
|
136
|
+
focalTweetId: tweetId,
|
|
137
|
+
with_rux_injections: false,
|
|
138
|
+
includePromotedContent: false,
|
|
139
|
+
withCommunity: true,
|
|
140
|
+
withQuickPromoteEligibilityTweetFields: true,
|
|
141
|
+
withBirdwatchNotes: true,
|
|
142
|
+
withVoice: true,
|
|
143
|
+
withV2Timeline: true,
|
|
144
|
+
});
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: result.error,
|
|
149
|
+
code: result.code,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const { parseTweet } = await import('./parsers.js');
|
|
153
|
+
const tweetData = result.data.tweet_results?.result;
|
|
154
|
+
const tweet = parseTweet(tweetData);
|
|
155
|
+
if (!tweet) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
error: 'Could not parse tweet',
|
|
159
|
+
code: 'NETWORK_ERROR',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
tweet,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get thread (conversation) for a tweet
|
|
169
|
+
*/
|
|
170
|
+
async getThread(tweetIdOrUrl) {
|
|
171
|
+
const tweetResult = await this.getTweet(tweetIdOrUrl);
|
|
172
|
+
if (!tweetResult.success) {
|
|
173
|
+
return tweetResult;
|
|
174
|
+
}
|
|
175
|
+
// For now, just return the single tweet
|
|
176
|
+
// Thread expansion can be added later with cursor-based pagination
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
tweets: [tweetResult.tweet],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get tweets from a user's timeline
|
|
184
|
+
*/
|
|
185
|
+
async getUserTweets(username, count = 20, cursor) {
|
|
186
|
+
if (count < 1 || count > 200) {
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
error: 'count must be between 1 and 200',
|
|
190
|
+
code: 'INVALID_INPUT',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// First, get user ID using UserByScreenName (GET request)
|
|
194
|
+
const queryId = queryIdManager.getQueryId('UserByScreenName');
|
|
195
|
+
const ids = Array.isArray(queryId) ? queryId : [queryId];
|
|
196
|
+
let userId;
|
|
197
|
+
// Try each query ID
|
|
198
|
+
for (const id of ids) {
|
|
199
|
+
try {
|
|
200
|
+
const variables = {
|
|
201
|
+
screen_name: username,
|
|
202
|
+
withSafetyModeUserFields: true,
|
|
203
|
+
};
|
|
204
|
+
const features = {
|
|
205
|
+
hidden_profile_subscriptions_enabled: true,
|
|
206
|
+
hidden_profile_likes_enabled: true,
|
|
207
|
+
responsive_web_graphql_exclude_directive_enabled: true,
|
|
208
|
+
verified_phone_label_enabled: false,
|
|
209
|
+
};
|
|
210
|
+
const fieldToggles = {
|
|
211
|
+
withAuxiliaryUserLabels: false,
|
|
212
|
+
};
|
|
213
|
+
const params = new URLSearchParams({
|
|
214
|
+
variables: JSON.stringify(variables),
|
|
215
|
+
features: JSON.stringify(features),
|
|
216
|
+
fieldToggles: JSON.stringify(fieldToggles),
|
|
217
|
+
});
|
|
218
|
+
const url = `https://x.com/i/api/graphql/${id}/UserByScreenName?${params.toString()}`;
|
|
219
|
+
const response = await this.graphql.fetchWithTimeout(url, {
|
|
220
|
+
method: 'GET',
|
|
221
|
+
headers: this.graphql.getHeaders(),
|
|
222
|
+
});
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
if (response.status === 404) {
|
|
225
|
+
continue; // Try next query ID
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
error: `HTTP ${response.status}: User lookup failed`,
|
|
230
|
+
code: 'NETWORK_ERROR',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const data = await response.json();
|
|
234
|
+
if (data.data?.user?.result?.__typename === 'UserUnavailable') {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
error: `User @${username} not found`,
|
|
238
|
+
code: 'NETWORK_ERROR',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
userId = data.data?.user?.result?.rest_id;
|
|
242
|
+
if (userId)
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!userId) {
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
error: 'Failed to resolve user ID',
|
|
253
|
+
code: 'NETWORK_ERROR',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// Now fetch tweets using UserTweets
|
|
257
|
+
const tweetsResult = await this.graphql.request('UserTweets', {
|
|
258
|
+
userId,
|
|
259
|
+
count: Math.min(count, 40),
|
|
260
|
+
cursor,
|
|
261
|
+
includePromotedContent: false,
|
|
262
|
+
withClientEventToken: false,
|
|
263
|
+
withBirdwatchNotes: false,
|
|
264
|
+
withVoice: true,
|
|
265
|
+
withV2Timeline: true,
|
|
266
|
+
});
|
|
267
|
+
if (!tweetsResult.success) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
error: tweetsResult.error,
|
|
271
|
+
code: tweetsResult.code,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const { extractTweetsFromInstructions } = await import('./parsers.js');
|
|
275
|
+
const instructions = tweetsResult.data.user?.result?.timeline_response?.timeline?.instructions || [];
|
|
276
|
+
const tweets = extractTweetsFromInstructions(instructions);
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
tweets: tweets.slice(0, count),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Post a new tweet
|
|
284
|
+
*/
|
|
285
|
+
async tweet(text) {
|
|
286
|
+
if (!text || text.trim().length === 0) {
|
|
287
|
+
return {
|
|
288
|
+
success: false,
|
|
289
|
+
error: 'Tweet text cannot be empty',
|
|
290
|
+
code: 'INVALID_INPUT',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (text.length > 280) {
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
error: 'Tweet text cannot exceed 280 characters',
|
|
297
|
+
code: 'INVALID_INPUT',
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const result = await this.graphql.request('CreateTweet', {
|
|
301
|
+
tweet_text: text,
|
|
302
|
+
dark_request: false,
|
|
303
|
+
media: {
|
|
304
|
+
media_entities: [],
|
|
305
|
+
possibly_sensitive: false,
|
|
306
|
+
},
|
|
307
|
+
semantic_annotation_ids: [],
|
|
308
|
+
}, {
|
|
309
|
+
rweb_video_screen_enabled: true,
|
|
310
|
+
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
|
311
|
+
responsive_web_grok_analyze_post_followups_enabled: false,
|
|
312
|
+
responsive_web_grok_annotations_enabled: false,
|
|
313
|
+
responsive_web_grok_show_grok_translated_post: false,
|
|
314
|
+
responsive_web_jetfuel_frame: true,
|
|
315
|
+
longform_notetweets_consumption_enabled: true,
|
|
316
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
317
|
+
tweet_awards_web_tipping_enabled: false,
|
|
318
|
+
responsive_graphql_exclude_directive_enabled: true,
|
|
319
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
320
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
321
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
322
|
+
verified_phone_label_enabled: false,
|
|
323
|
+
responsive_web_media_download_video_enabled: false,
|
|
324
|
+
});
|
|
325
|
+
if (!result.success) {
|
|
326
|
+
return {
|
|
327
|
+
success: false,
|
|
328
|
+
error: result.error,
|
|
329
|
+
code: result.code,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const tweetId = result.data.create_tweet?.tweet_results?.result?.rest_id;
|
|
333
|
+
if (!tweetId) {
|
|
334
|
+
return {
|
|
335
|
+
success: false,
|
|
336
|
+
error: 'Could not parse tweet ID from response',
|
|
337
|
+
code: 'NETWORK_ERROR',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
tweetId,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Reply to a tweet
|
|
347
|
+
*/
|
|
348
|
+
async reply(tweetId, text) {
|
|
349
|
+
if (!text || text.trim().length === 0) {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
error: 'Reply text cannot be empty',
|
|
353
|
+
code: 'INVALID_INPUT',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (text.length > 280) {
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
error: 'Reply text cannot exceed 280 characters',
|
|
360
|
+
code: 'INVALID_INPUT',
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const result = await this.graphql.request('CreateTweet', {
|
|
364
|
+
tweet_text: text,
|
|
365
|
+
media: {
|
|
366
|
+
media_entities: [],
|
|
367
|
+
tagged_users: [],
|
|
368
|
+
},
|
|
369
|
+
semantic_annotation_ids: [],
|
|
370
|
+
reply_settings: {
|
|
371
|
+
in_reply_to_tweet_id: tweetId,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
if (!result.success) {
|
|
375
|
+
return {
|
|
376
|
+
success: false,
|
|
377
|
+
error: result.error,
|
|
378
|
+
code: result.code,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const newTweetId = result.data.create_tweet?.tweet_results?.result?.rest_id;
|
|
382
|
+
if (!newTweetId) {
|
|
383
|
+
return {
|
|
384
|
+
success: false,
|
|
385
|
+
error: 'Could not parse tweet ID from response',
|
|
386
|
+
code: 'NETWORK_ERROR',
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
tweetId: newTweetId,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get news/for you timeline
|
|
396
|
+
*/
|
|
397
|
+
async getNews(count = 20) {
|
|
398
|
+
if (count < 1 || count > 200) {
|
|
399
|
+
return {
|
|
400
|
+
success: false,
|
|
401
|
+
error: 'count must be between 1 and 200',
|
|
402
|
+
code: 'INVALID_INPUT',
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const result = await this.graphql.request('NewsForYou', {
|
|
406
|
+
count: Math.min(count, 40),
|
|
407
|
+
includePromotedContent: false,
|
|
408
|
+
latestControlAvailable: true,
|
|
409
|
+
requestContext: 'launch',
|
|
410
|
+
withClientEventToken: false,
|
|
411
|
+
withBirdwatchNotes: false,
|
|
412
|
+
withVoice: true,
|
|
413
|
+
withV2Timeline: true,
|
|
414
|
+
});
|
|
415
|
+
if (!result.success) {
|
|
416
|
+
return {
|
|
417
|
+
success: false,
|
|
418
|
+
error: result.error,
|
|
419
|
+
code: result.code,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const { extractTweetsFromInstructions } = await import('./parsers.js');
|
|
423
|
+
const instructions = result.data.home?.home_timeline_urt?.instructions || [];
|
|
424
|
+
const tweets = extractTweetsFromInstructions(instructions);
|
|
425
|
+
// Transform tweets into news items
|
|
426
|
+
const items = tweets.slice(0, count).map((tweet) => ({
|
|
427
|
+
id: tweet.id,
|
|
428
|
+
headline: tweet.text,
|
|
429
|
+
tweets: [tweet],
|
|
430
|
+
}));
|
|
431
|
+
return {
|
|
432
|
+
success: true,
|
|
433
|
+
items,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Get mentions timeline
|
|
438
|
+
*/
|
|
439
|
+
async getMentions(count = 20) {
|
|
440
|
+
if (count < 1 || count > 200) {
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
error: 'count must be between 1 and 200',
|
|
444
|
+
code: 'INVALID_INPUT',
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const result = await this.graphql.request('MentionsTimeline', {
|
|
448
|
+
count: Math.min(count, 40),
|
|
449
|
+
includePromotedContent: false,
|
|
450
|
+
latestControlAvailable: true,
|
|
451
|
+
requestContext: 'launch',
|
|
452
|
+
withClientEventToken: false,
|
|
453
|
+
withBirdwatchNotes: false,
|
|
454
|
+
withVoice: true,
|
|
455
|
+
withV2Timeline: true,
|
|
456
|
+
});
|
|
457
|
+
if (!result.success) {
|
|
458
|
+
return {
|
|
459
|
+
success: false,
|
|
460
|
+
error: result.error,
|
|
461
|
+
code: result.code,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const { extractTweetsFromInstructions } = await import('./parsers.js');
|
|
465
|
+
const instructions = result.data.home?.home_timeline_urt?.instructions || [];
|
|
466
|
+
const tweets = extractTweetsFromInstructions(instructions);
|
|
467
|
+
return {
|
|
468
|
+
success: true,
|
|
469
|
+
tweets: tweets.slice(0, count),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface TwitterCredentials {
|
|
2
|
+
authToken: string;
|
|
3
|
+
ct0: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ResolvedCredentials {
|
|
6
|
+
authToken: string;
|
|
7
|
+
ct0: string;
|
|
8
|
+
source: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve Twitter credentials from environment variables or config
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveCredentials(): ResolvedCredentials;
|
|
14
|
+
/**
|
|
15
|
+
* Validate credentials format
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateCredentials(creds: TwitterCredentials): {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/client/auth.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,mBAAmB,CA2BxD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAcjG"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve Twitter credentials from environment variables or config
|
|
3
|
+
*/
|
|
4
|
+
export function resolveCredentials() {
|
|
5
|
+
// Check environment variables first
|
|
6
|
+
const authToken = process.env.TWITTER_AUTH_TOKEN ||
|
|
7
|
+
process.env.AUTH_TOKEN ||
|
|
8
|
+
process.env.auth_token;
|
|
9
|
+
const ct0 = process.env.TWITTER_CT0 ||
|
|
10
|
+
process.env.CT0 ||
|
|
11
|
+
process.env.ct0;
|
|
12
|
+
if (!authToken || !ct0) {
|
|
13
|
+
throw new Error('Missing credentials. Set TWITTER_AUTH_TOKEN and TWITTER_CT0 environment variables.\n\n' +
|
|
14
|
+
'Get tokens from browser:\n' +
|
|
15
|
+
'1. Open https://x.com in browser\n' +
|
|
16
|
+
'2. DevTools → Application → Cookies\n' +
|
|
17
|
+
'3. Copy auth_token and ct0 values');
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
authToken,
|
|
21
|
+
ct0,
|
|
22
|
+
source: 'environment',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate credentials format
|
|
27
|
+
*/
|
|
28
|
+
export function validateCredentials(creds) {
|
|
29
|
+
if (!creds.authToken || typeof creds.authToken !== 'string') {
|
|
30
|
+
return { valid: false, error: 'Invalid auth_token' };
|
|
31
|
+
}
|
|
32
|
+
if (!creds.ct0 || typeof creds.ct0 !== 'string') {
|
|
33
|
+
return { valid: false, error: 'Invalid ct0 token' };
|
|
34
|
+
}
|
|
35
|
+
if (creds.authToken.length < 10) {
|
|
36
|
+
return { valid: false, error: 'auth_token too short' };
|
|
37
|
+
}
|
|
38
|
+
if (creds.ct0.length < 10) {
|
|
39
|
+
return { valid: false, error: 'ct0 token too short' };
|
|
40
|
+
}
|
|
41
|
+
return { valid: true };
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Result } from './types.js';
|
|
2
|
+
export interface GraphQLError {
|
|
3
|
+
code?: number;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GraphQLResponse<T> {
|
|
7
|
+
data?: T;
|
|
8
|
+
errors?: GraphQLError[];
|
|
9
|
+
}
|
|
10
|
+
export interface AboutAccountResponse {
|
|
11
|
+
rest_id?: string;
|
|
12
|
+
legacy?: {
|
|
13
|
+
screen_name?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
followers_count?: number;
|
|
16
|
+
friends_count?: number;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export declare class GraphQLClient {
|
|
22
|
+
private authToken;
|
|
23
|
+
private ct0;
|
|
24
|
+
private userAgent;
|
|
25
|
+
private timeout;
|
|
26
|
+
private clientUuid;
|
|
27
|
+
private clientDeviceId;
|
|
28
|
+
constructor(options: {
|
|
29
|
+
authToken: string;
|
|
30
|
+
ct0: string;
|
|
31
|
+
timeout?: number;
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
});
|
|
34
|
+
private createTransactionId;
|
|
35
|
+
getHeaders(): Record<string, string>;
|
|
36
|
+
fetchWithTimeout(url: string, init: RequestInit): Promise<Response>;
|
|
37
|
+
private getErrorCode;
|
|
38
|
+
private validateResponse;
|
|
39
|
+
request<T>(operation: string, variables: Record<string, unknown>, features?: Record<string, boolean | string | number>): Promise<Result<{
|
|
40
|
+
data: T;
|
|
41
|
+
}>>;
|
|
42
|
+
/**
|
|
43
|
+
* Fetch the authenticated user's information using GraphQL
|
|
44
|
+
*/
|
|
45
|
+
getCurrentUser(): Promise<Result<{
|
|
46
|
+
data: AboutAccountResponse;
|
|
47
|
+
}>>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=graphql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/client/graphql.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE;QACP,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAS;gBAEnB,OAAO,EAAE;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;IAWD,OAAO,CAAC,mBAAmB;IAIpB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAoB9B,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAehF,OAAO,CAAC,YAAY;YAgBN,gBAAgB;IA2BxB,OAAO,CAAC,CAAC,EACb,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,GACnD,OAAO,CAAC,MAAM,CAAC;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IAyF/B;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;QAAE,IAAI,EAAE,oBAAoB,CAAA;KAAE,CAAC,CAAC;CAwBxE"}
|