@elizaos/plugin-twitter 1.0.13 → 1.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/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  Role,
12
12
  Service,
13
13
  createUniqueUuid as createUniqueUuid6,
14
- logger as logger6
14
+ logger as logger7
15
15
  } from "@elizaos/core";
16
16
 
17
17
  // src/base.ts
@@ -21,154 +21,6 @@ import {
21
21
  logger
22
22
  } from "@elizaos/core";
23
23
 
24
- // src/client/api.ts
25
- import { Headers } from "headers-polyfill";
26
-
27
- // src/client/errors.ts
28
- var ApiError = class _ApiError extends Error {
29
- /**
30
- * Constructor for creating a new instance of the class.
31
- *
32
- * @param response The response object.
33
- * @param data The data object.
34
- * @param message The message string.
35
- */
36
- constructor(response, data, message) {
37
- super(message);
38
- this.response = response;
39
- this.data = data;
40
- }
41
- /**
42
- * Creates an instance of ApiError based on a Response object.
43
- *
44
- * @param {Response} response The Response object to parse.
45
- * @returns {Promise<ApiError>} A new instance of ApiError with the parsed data and status.
46
- */
47
- static async fromResponse(response) {
48
- let data = void 0;
49
- try {
50
- data = await response.json();
51
- } catch {
52
- try {
53
- data = await response.text();
54
- } catch {
55
- }
56
- }
57
- return new _ApiError(response, data, `Response status: ${response.status}`);
58
- }
59
- };
60
-
61
- // src/client/api.ts
62
- async function requestApi(url, auth, method = "GET", body) {
63
- const headers = new Headers({
64
- "Content-Type": "application/json"
65
- });
66
- let res;
67
- do {
68
- try {
69
- res = await fetch(url, {
70
- method,
71
- headers,
72
- credentials: "include",
73
- ...body && { body: JSON.stringify(body) }
74
- });
75
- } catch (err) {
76
- if (!(err instanceof Error)) {
77
- throw err;
78
- }
79
- return {
80
- success: false,
81
- err: new Error("Failed to perform request.")
82
- };
83
- }
84
- if (res.status === 429) {
85
- const xRateLimitRemaining = res.headers.get("x-rate-limit-remaining");
86
- const xRateLimitReset = res.headers.get("x-rate-limit-reset");
87
- if (xRateLimitRemaining === "0" && xRateLimitReset) {
88
- const currentTime = (/* @__PURE__ */ new Date()).valueOf() / 1e3;
89
- const timeDeltaMs = 1e3 * (Number.parseInt(xRateLimitReset) - currentTime);
90
- await new Promise((resolve) => setTimeout(resolve, timeDeltaMs));
91
- }
92
- }
93
- } while (res.status === 429);
94
- if (!res.ok) {
95
- return {
96
- success: false,
97
- err: await ApiError.fromResponse(res)
98
- };
99
- }
100
- const transferEncoding = res.headers.get("transfer-encoding");
101
- if (transferEncoding === "chunked") {
102
- const reader = typeof res.body?.getReader === "function" ? res.body.getReader() : null;
103
- if (!reader) {
104
- try {
105
- const text = await res.text();
106
- try {
107
- const value = JSON.parse(text);
108
- return { success: true, value };
109
- } catch (_e) {
110
- return { success: true, value: { text } };
111
- }
112
- } catch (_e) {
113
- return {
114
- success: false,
115
- err: new Error("No readable stream available and cant parse")
116
- };
117
- }
118
- }
119
- let chunks = "";
120
- while (true) {
121
- const { done, value } = await reader.read();
122
- if (done) break;
123
- chunks += new TextDecoder().decode(value);
124
- }
125
- try {
126
- const value = JSON.parse(chunks);
127
- return { success: true, value };
128
- } catch (_e) {
129
- return { success: true, value: { text: chunks } };
130
- }
131
- }
132
- const contentType = res.headers.get("content-type");
133
- if (contentType?.includes("application/json")) {
134
- const value = await res.json();
135
- return { success: true, value };
136
- }
137
- return { success: true, value: {} };
138
- }
139
- function addApiFeatures(o) {
140
- return {
141
- ...o,
142
- rweb_lists_timeline_redesign_enabled: true,
143
- responsive_web_graphql_exclude_directive_enabled: true,
144
- verified_phone_label_enabled: false,
145
- creator_subscriptions_tweet_preview_api_enabled: true,
146
- responsive_web_graphql_timeline_navigation_enabled: true,
147
- responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
148
- tweetypie_unmention_optimization_enabled: true,
149
- responsive_web_edit_tweet_api_enabled: true,
150
- graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
151
- view_counts_everywhere_api_enabled: true,
152
- longform_notetweets_consumption_enabled: true,
153
- tweet_awards_web_tipping_enabled: false,
154
- freedom_of_speech_not_reach_fetch_enabled: true,
155
- standardized_nudges_misinfo: true,
156
- longform_notetweets_rich_text_read_enabled: true,
157
- responsive_web_enhance_cards_enabled: false,
158
- subscriptions_verification_info_enabled: true,
159
- subscriptions_verification_info_reason_enabled: true,
160
- subscriptions_verification_info_verified_since_enabled: true,
161
- super_follow_badge_privacy_enabled: false,
162
- super_follow_exclusive_tweet_notifications_enabled: false,
163
- super_follow_tweet_api_enabled: false,
164
- super_follow_user_api_enabled: false,
165
- android_graphql_skip_api_media_color_palette: false,
166
- creator_subscriptions_subscription_count_enabled: false,
167
- blue_business_profile_image_shape_enabled: false,
168
- unified_cards_ad_metadata_container_dynamic_card_content_query_enabled: false
169
- };
170
- }
171
-
172
24
  // src/client/auth.ts
173
25
  import { TwitterApi } from "twitter-api-v2";
174
26
  var TwitterAuth = class {
@@ -273,172 +125,110 @@ var TwitterAuth = class {
273
125
  };
274
126
 
275
127
  // src/client/profile.ts
276
- import stringify from "json-stable-stringify";
277
128
  function getAvatarOriginalSizeUrl(avatarUrl) {
278
129
  return avatarUrl ? avatarUrl.replace("_normal", "") : void 0;
279
130
  }
280
- function parseProfile(user, isBlueVerified) {
131
+ function parseV2Profile(user) {
281
132
  const profile = {
282
- avatar: getAvatarOriginalSizeUrl(user.profile_image_url_https),
283
- banner: user.profile_banner_url,
133
+ avatar: getAvatarOriginalSizeUrl(user.profile_image_url),
284
134
  biography: user.description,
285
- followersCount: user.followers_count,
286
- followingCount: user.friends_count,
287
- friendsCount: user.friends_count,
288
- mediaCount: user.media_count,
135
+ followersCount: user.public_metrics?.followers_count,
136
+ followingCount: user.public_metrics?.following_count,
137
+ friendsCount: user.public_metrics?.following_count,
138
+ tweetsCount: user.public_metrics?.tweet_count,
289
139
  isPrivate: user.protected ?? false,
290
- isVerified: user.verified,
291
- likesCount: user.favourites_count,
292
- listedCount: user.listed_count,
293
- location: user.location,
140
+ isVerified: user.verified ?? false,
141
+ likesCount: user.public_metrics?.like_count,
142
+ listedCount: user.public_metrics?.listed_count,
143
+ location: user.location || "",
294
144
  name: user.name,
295
- pinnedTweetIds: user.pinned_tweet_ids_str,
296
- tweetsCount: user.statuses_count,
297
- url: `https://twitter.com/${user.screen_name}`,
298
- userId: user.id_str,
299
- username: user.screen_name,
300
- isBlueVerified: isBlueVerified ?? false,
301
- canDm: user.can_dm
145
+ pinnedTweetIds: user.pinned_tweet_id ? [user.pinned_tweet_id] : [],
146
+ url: `https://twitter.com/${user.username}`,
147
+ userId: user.id,
148
+ username: user.username,
149
+ isBlueVerified: user.verified_type === "blue"
302
150
  };
303
- if (user.created_at != null) {
304
- profile.joined = new Date(Date.parse(user.created_at));
151
+ if (user.created_at) {
152
+ profile.joined = new Date(user.created_at);
305
153
  }
306
- const urls = user.entities?.url?.urls;
307
- if (urls?.length != null && urls?.length > 0) {
308
- profile.website = urls[0].expanded_url;
154
+ if (user.entities?.url?.urls?.length > 0) {
155
+ profile.website = user.entities.url.urls[0].expanded_url;
309
156
  }
310
157
  return profile;
311
158
  }
312
159
  async function getProfile(username, auth) {
313
- const params = new URLSearchParams();
314
- params.set(
315
- "variables",
316
- stringify({
317
- screen_name: username,
318
- withSafetyModeUserFields: true
319
- }) ?? ""
320
- );
321
- params.set(
322
- "features",
323
- stringify({
324
- hidden_profile_likes_enabled: false,
325
- hidden_profile_subscriptions_enabled: false,
326
- // Auth-restricted
327
- responsive_web_graphql_exclude_directive_enabled: true,
328
- verified_phone_label_enabled: false,
329
- subscriptions_verification_info_is_identity_verified_enabled: false,
330
- subscriptions_verification_info_verified_since_enabled: true,
331
- highlights_tweets_tab_ui_enabled: true,
332
- creator_subscriptions_tweet_preview_api_enabled: true,
333
- responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
334
- responsive_web_graphql_timeline_navigation_enabled: true
335
- }) ?? ""
336
- );
337
- params.set(
338
- "fieldToggles",
339
- stringify({ withAuxiliaryUserLabels: false }) ?? ""
340
- );
341
- const res = await requestApi(
342
- `https://twitter.com/i/api/graphql/G3KGOASz96M-Qu0nwmGXNg/UserByScreenName?${params.toString()}`,
343
- auth
344
- );
345
- if (!res.success) {
346
- return res;
347
- }
348
- const { value } = res;
349
- const { errors } = value;
350
- if (errors != null && errors.length > 0) {
351
- return {
352
- success: false,
353
- err: new Error(errors[0].message)
354
- };
355
- }
356
- if (!value.data || !value.data.user || !value.data.user.result) {
160
+ if (!auth) {
357
161
  return {
358
162
  success: false,
359
- err: new Error("User not found.")
163
+ err: new Error("Not authenticated")
360
164
  };
361
165
  }
362
- const { result: user } = value.data.user;
363
- const { legacy } = user;
364
- if (user.rest_id == null || user.rest_id.length === 0) {
166
+ try {
167
+ const client = auth.getV2Client();
168
+ const user = await client.v2.userByUsername(username, {
169
+ "user.fields": [
170
+ "id",
171
+ "name",
172
+ "username",
173
+ "created_at",
174
+ "description",
175
+ "entities",
176
+ "location",
177
+ "pinned_tweet_id",
178
+ "profile_image_url",
179
+ "protected",
180
+ "public_metrics",
181
+ "url",
182
+ "verified",
183
+ "verified_type"
184
+ ]
185
+ });
186
+ if (!user.data) {
187
+ return {
188
+ success: false,
189
+ err: new Error(`User ${username} not found`)
190
+ };
191
+ }
365
192
  return {
366
- success: false,
367
- err: new Error("rest_id not found.")
193
+ success: true,
194
+ value: parseV2Profile(user.data)
368
195
  };
369
- }
370
- legacy.id_str = user.rest_id;
371
- if (legacy.screen_name == null || legacy.screen_name.length === 0) {
196
+ } catch (error) {
372
197
  return {
373
198
  success: false,
374
- err: new Error(`Either ${username} does not exist or is private.`)
199
+ err: new Error(error.message || "Failed to fetch profile")
375
200
  };
376
201
  }
377
- return {
378
- success: true,
379
- value: parseProfile(user.legacy, user.is_blue_verified)
380
- };
381
202
  }
382
203
  var idCache = /* @__PURE__ */ new Map();
383
204
  async function getScreenNameByUserId(userId, auth) {
384
- const params = new URLSearchParams();
385
- params.set(
386
- "variables",
387
- stringify({
388
- userId,
389
- withSafetyModeUserFields: true
390
- }) ?? ""
391
- );
392
- params.set(
393
- "features",
394
- stringify({
395
- hidden_profile_subscriptions_enabled: true,
396
- rweb_tipjar_consumption_enabled: true,
397
- responsive_web_graphql_exclude_directive_enabled: true,
398
- verified_phone_label_enabled: false,
399
- highlights_tweets_tab_ui_enabled: true,
400
- responsive_web_twitter_article_notes_tab_enabled: true,
401
- subscriptions_feature_can_gift_premium: false,
402
- creator_subscriptions_tweet_preview_api_enabled: true,
403
- responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
404
- responsive_web_graphql_timeline_navigation_enabled: true
405
- }) ?? ""
406
- );
407
- const res = await requestApi(
408
- `https://twitter.com/i/api/graphql/xf3jd90KKBCUxdlI_tNHZw/UserByRestId?${params.toString()}`,
409
- auth
410
- );
411
- if (!res.success) {
412
- return res;
413
- }
414
- const { value } = res;
415
- const { errors } = value;
416
- if (errors != null && errors.length > 0) {
205
+ if (!auth) {
417
206
  return {
418
207
  success: false,
419
- err: new Error(errors[0].message)
208
+ err: new Error("Not authenticated")
420
209
  };
421
210
  }
422
- if (!value.data || !value.data.user || !value.data.user.result) {
211
+ try {
212
+ const client = auth.getV2Client();
213
+ const user = await client.v2.user(userId, {
214
+ "user.fields": ["username"]
215
+ });
216
+ if (!user.data || !user.data.username) {
217
+ return {
218
+ success: false,
219
+ err: new Error(`User with ID ${userId} not found`)
220
+ };
221
+ }
423
222
  return {
424
- success: false,
425
- err: new Error("User not found.")
223
+ success: true,
224
+ value: user.data.username
426
225
  };
427
- }
428
- const { result: user } = value.data.user;
429
- const { legacy } = user;
430
- if (legacy.screen_name == null || legacy.screen_name.length === 0) {
226
+ } catch (error) {
431
227
  return {
432
228
  success: false,
433
- err: new Error(
434
- `Either user with ID ${userId} does not exist or is private.`
435
- )
229
+ err: new Error(error.message || "Failed to fetch user")
436
230
  };
437
231
  }
438
- return {
439
- success: true,
440
- value: legacy.screen_name
441
- };
442
232
  }
443
233
  async function getEntityIdByScreenName(screenName, auth) {
444
234
  const cached = idCache.get(screenName);
@@ -464,202 +254,215 @@ async function getEntityIdByScreenName(screenName, auth) {
464
254
  }
465
255
 
466
256
  // src/client/relationships.ts
467
- import stringify2 from "json-stable-stringify";
468
-
469
- // src/client/timeline-async.ts
470
- async function* getUserTimeline(query, maxProfiles, fetchFunc) {
471
- let nProfiles = 0;
472
- let cursor = void 0;
473
- let consecutiveEmptyBatches = 0;
474
- while (nProfiles < maxProfiles) {
475
- const batch = await fetchFunc(
476
- query,
477
- maxProfiles,
478
- cursor
479
- );
480
- const { profiles, next } = batch;
481
- cursor = next;
482
- if (profiles.length === 0) {
483
- consecutiveEmptyBatches++;
484
- if (consecutiveEmptyBatches > 5) break;
485
- } else consecutiveEmptyBatches = 0;
486
- for (const profile of profiles) {
487
- if (nProfiles < maxProfiles) yield profile;
488
- else break;
489
- nProfiles++;
490
- }
491
- if (!next) break;
492
- }
257
+ import { Headers } from "headers-polyfill";
258
+ function parseV2UserToProfile(user) {
259
+ return {
260
+ avatar: user.profile_image_url?.replace("_normal", ""),
261
+ biography: user.description,
262
+ followersCount: user.public_metrics?.followers_count,
263
+ followingCount: user.public_metrics?.following_count,
264
+ friendsCount: user.public_metrics?.following_count,
265
+ tweetsCount: user.public_metrics?.tweet_count,
266
+ isPrivate: user.protected ?? false,
267
+ isVerified: user.verified ?? false,
268
+ likesCount: user.public_metrics?.like_count,
269
+ listedCount: user.public_metrics?.listed_count,
270
+ location: user.location || "",
271
+ name: user.name,
272
+ pinnedTweetIds: user.pinned_tweet_id ? [user.pinned_tweet_id] : [],
273
+ url: `https://twitter.com/${user.username}`,
274
+ userId: user.id,
275
+ username: user.username,
276
+ isBlueVerified: user.verified_type === "blue",
277
+ joined: user.created_at ? new Date(user.created_at) : void 0,
278
+ website: user.entities?.url?.urls?.[0]?.expanded_url
279
+ };
493
280
  }
494
- async function* getTweetTimeline(query, maxTweets, fetchFunc) {
495
- let nTweets = 0;
496
- let cursor = void 0;
497
- while (nTweets < maxTweets) {
498
- const batch = await fetchFunc(
499
- query,
500
- maxTweets,
501
- cursor
502
- );
503
- const { tweets, next } = batch;
504
- if (tweets.length === 0) {
505
- break;
506
- }
507
- for (const tweet of tweets) {
508
- if (nTweets < maxTweets) {
509
- cursor = next;
510
- yield tweet;
511
- } else {
281
+ async function* getFollowing(userId, maxProfiles, auth) {
282
+ if (!auth) {
283
+ throw new Error("Not authenticated");
284
+ }
285
+ const client = auth.getV2Client();
286
+ let count = 0;
287
+ let paginationToken;
288
+ try {
289
+ while (count < maxProfiles) {
290
+ const response = await client.v2.following(userId, {
291
+ max_results: Math.min(maxProfiles - count, 100),
292
+ pagination_token: paginationToken,
293
+ "user.fields": [
294
+ "id",
295
+ "name",
296
+ "username",
297
+ "created_at",
298
+ "description",
299
+ "entities",
300
+ "location",
301
+ "pinned_tweet_id",
302
+ "profile_image_url",
303
+ "protected",
304
+ "public_metrics",
305
+ "url",
306
+ "verified",
307
+ "verified_type"
308
+ ]
309
+ });
310
+ if (!response.data || response.data.length === 0) {
512
311
  break;
513
312
  }
514
- nTweets++;
313
+ for (const user of response.data) {
314
+ if (count >= maxProfiles) break;
315
+ yield parseV2UserToProfile(user);
316
+ count++;
317
+ }
318
+ paginationToken = response.meta?.next_token;
319
+ if (!paginationToken) break;
515
320
  }
321
+ } catch (error) {
322
+ console.error("Error fetching following:", error);
323
+ throw error;
516
324
  }
517
325
  }
518
-
519
- // src/client/timeline-relationship.ts
520
- function parseRelationshipTimeline(timeline) {
521
- let bottomCursor;
522
- let topCursor;
523
- const profiles = [];
524
- const instructions = timeline.data?.user?.result?.timeline?.timeline?.instructions ?? [];
525
- for (const instruction of instructions) {
526
- if (instruction.type === "TimelineAddEntries" || instruction.type === "TimelineReplaceEntry") {
527
- if (instruction.entry?.content?.cursorType === "Bottom") {
528
- bottomCursor = instruction.entry.content.value;
529
- continue;
530
- }
531
- if (instruction.entry?.content?.cursorType === "Top") {
532
- topCursor = instruction.entry.content.value;
533
- continue;
326
+ async function* getFollowers(userId, maxProfiles, auth) {
327
+ if (!auth) {
328
+ throw new Error("Not authenticated");
329
+ }
330
+ const client = auth.getV2Client();
331
+ let count = 0;
332
+ let paginationToken;
333
+ try {
334
+ while (count < maxProfiles) {
335
+ const response = await client.v2.followers(userId, {
336
+ max_results: Math.min(maxProfiles - count, 100),
337
+ pagination_token: paginationToken,
338
+ "user.fields": [
339
+ "id",
340
+ "name",
341
+ "username",
342
+ "created_at",
343
+ "description",
344
+ "entities",
345
+ "location",
346
+ "pinned_tweet_id",
347
+ "profile_image_url",
348
+ "protected",
349
+ "public_metrics",
350
+ "url",
351
+ "verified",
352
+ "verified_type"
353
+ ]
354
+ });
355
+ if (!response.data || response.data.length === 0) {
356
+ break;
534
357
  }
535
- const entries = instruction.entries ?? [];
536
- for (const entry of entries) {
537
- const itemContent = entry.content?.itemContent;
538
- if (itemContent?.userDisplayType === "User") {
539
- const userResultRaw = itemContent.user_results?.result;
540
- if (userResultRaw?.legacy) {
541
- const profile = parseProfile(
542
- userResultRaw.legacy,
543
- userResultRaw.is_blue_verified
544
- );
545
- if (!profile.userId) {
546
- profile.userId = userResultRaw.rest_id;
547
- }
548
- profiles.push(profile);
549
- }
550
- } else if (entry.content?.cursorType === "Bottom") {
551
- bottomCursor = entry.content.value;
552
- } else if (entry.content?.cursorType === "Top") {
553
- topCursor = entry.content.value;
554
- }
358
+ for (const user of response.data) {
359
+ if (count >= maxProfiles) break;
360
+ yield parseV2UserToProfile(user);
361
+ count++;
555
362
  }
363
+ paginationToken = response.meta?.next_token;
364
+ if (!paginationToken) break;
556
365
  }
366
+ } catch (error) {
367
+ console.error("Error fetching followers:", error);
368
+ throw error;
557
369
  }
558
- return { profiles, next: bottomCursor, previous: topCursor };
559
- }
560
-
561
- // src/client/relationships.ts
562
- function getFollowing(userId, maxProfiles, auth) {
563
- return getUserTimeline(userId, maxProfiles, (q, mt, c) => {
564
- return fetchProfileFollowing(q, mt, auth, c);
565
- });
566
- }
567
- function getFollowers(userId, maxProfiles, auth) {
568
- return getUserTimeline(userId, maxProfiles, (q, mt, c) => {
569
- return fetchProfileFollowers(q, mt, auth, c);
570
- });
571
370
  }
572
371
  async function fetchProfileFollowing(userId, maxProfiles, auth, cursor) {
573
- const timeline = await getFollowingTimeline(
574
- userId,
575
- maxProfiles,
576
- auth,
577
- cursor
578
- );
579
- return parseRelationshipTimeline(timeline);
580
- }
581
- async function fetchProfileFollowers(userId, maxProfiles, auth, cursor) {
582
- const timeline = await getFollowersTimeline(
583
- userId,
584
- maxProfiles,
585
- auth,
586
- cursor
587
- );
588
- return parseRelationshipTimeline(timeline);
589
- }
590
- async function getFollowingTimeline(userId, maxItems, auth, cursor) {
591
- if (!auth.isLoggedIn()) {
592
- throw new Error("Client is not logged-in for profile following.");
593
- }
594
- if (maxItems > 50) {
595
- maxItems = 50;
372
+ if (!auth) {
373
+ throw new Error("Not authenticated");
596
374
  }
597
- const variables = {
598
- userId,
599
- count: maxItems,
600
- includePromotedContent: false
601
- };
602
- const features2 = addApiFeatures({
603
- responsive_web_twitter_article_tweet_consumption_enabled: false,
604
- tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
605
- longform_notetweets_inline_media_enabled: true,
606
- responsive_web_media_download_video_enabled: false
607
- });
608
- if (cursor != null && cursor !== "") {
609
- variables.cursor = cursor;
610
- }
611
- const params = new URLSearchParams();
612
- params.set("features", stringify2(features2) ?? "");
613
- params.set("variables", stringify2(variables) ?? "");
614
- const res = await requestApi(
615
- `https://twitter.com/i/api/graphql/iSicc7LrzWGBgDPL0tM_TQ/Following?${params.toString()}`,
616
- auth
617
- );
618
- if (!res.success) {
619
- throw res.err;
375
+ const client = auth.getV2Client();
376
+ try {
377
+ const response = await client.v2.following(userId, {
378
+ max_results: Math.min(maxProfiles, 100),
379
+ pagination_token: cursor,
380
+ "user.fields": [
381
+ "id",
382
+ "name",
383
+ "username",
384
+ "created_at",
385
+ "description",
386
+ "entities",
387
+ "location",
388
+ "pinned_tweet_id",
389
+ "profile_image_url",
390
+ "protected",
391
+ "public_metrics",
392
+ "url",
393
+ "verified",
394
+ "verified_type"
395
+ ]
396
+ });
397
+ const profiles = response.data?.map(parseV2UserToProfile) || [];
398
+ return {
399
+ profiles,
400
+ next: response.meta?.next_token
401
+ };
402
+ } catch (error) {
403
+ console.error("Error fetching following profiles:", error);
404
+ throw error;
620
405
  }
621
- return res.value;
622
406
  }
623
- async function getFollowersTimeline(userId, maxItems, auth, cursor) {
624
- if (!auth.isLoggedIn()) {
625
- throw new Error("Client is not logged-in for profile followers.");
626
- }
627
- if (maxItems > 50) {
628
- maxItems = 50;
407
+ async function fetchProfileFollowers(userId, maxProfiles, auth, cursor) {
408
+ if (!auth) {
409
+ throw new Error("Not authenticated");
629
410
  }
630
- const variables = {
631
- userId,
632
- count: maxItems,
633
- includePromotedContent: false
634
- };
635
- const features2 = addApiFeatures({
636
- responsive_web_twitter_article_tweet_consumption_enabled: false,
637
- tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
638
- longform_notetweets_inline_media_enabled: true,
639
- responsive_web_media_download_video_enabled: false
640
- });
641
- if (cursor != null && cursor !== "") {
642
- variables.cursor = cursor;
643
- }
644
- const params = new URLSearchParams();
645
- params.set("features", stringify2(features2) ?? "");
646
- params.set("variables", stringify2(variables) ?? "");
647
- const res = await requestApi(
648
- `https://twitter.com/i/api/graphql/rRXFSG5vR6drKr5M37YOTw/Followers?${params.toString()}`,
649
- auth
650
- );
651
- if (!res.success) {
652
- throw res.err;
411
+ const client = auth.getV2Client();
412
+ try {
413
+ const response = await client.v2.followers(userId, {
414
+ max_results: Math.min(maxProfiles, 100),
415
+ pagination_token: cursor,
416
+ "user.fields": [
417
+ "id",
418
+ "name",
419
+ "username",
420
+ "created_at",
421
+ "description",
422
+ "entities",
423
+ "location",
424
+ "pinned_tweet_id",
425
+ "profile_image_url",
426
+ "protected",
427
+ "public_metrics",
428
+ "url",
429
+ "verified",
430
+ "verified_type"
431
+ ]
432
+ });
433
+ const profiles = response.data?.map(parseV2UserToProfile) || [];
434
+ return {
435
+ profiles,
436
+ next: response.meta?.next_token
437
+ };
438
+ } catch (error) {
439
+ console.error("Error fetching follower profiles:", error);
440
+ throw error;
653
441
  }
654
- return res.value;
655
442
  }
656
443
  async function followUser(username, auth) {
657
- console.warn(
658
- "Follow user functionality is not supported in Twitter API v2 client"
659
- );
660
- throw new Error(
661
- "Follow user functionality not implemented for Twitter API v2"
662
- );
444
+ if (!auth) {
445
+ throw new Error("Not authenticated");
446
+ }
447
+ const client = auth.getV2Client();
448
+ try {
449
+ const userResponse = await client.v2.userByUsername(username);
450
+ if (!userResponse.data) {
451
+ throw new Error(`User ${username} not found`);
452
+ }
453
+ const meResponse = await client.v2.me();
454
+ if (!meResponse.data) {
455
+ throw new Error("Failed to get authenticated user");
456
+ }
457
+ const result = await client.v2.follow(meResponse.data.id, userResponse.data.id);
458
+ return new Response(JSON.stringify(result), {
459
+ status: result.data?.following ? 200 : 400,
460
+ headers: new Headers({ "Content-Type": "application/json" })
461
+ });
462
+ } catch (error) {
463
+ console.error("Error following user:", error);
464
+ throw error;
465
+ }
663
466
  }
664
467
 
665
468
  // src/client/search.ts
@@ -701,7 +504,9 @@ async function* searchTweets(query, maxTweets, searchMode, auth) {
701
504
  const convertedTweet = {
702
505
  id: tweet.id,
703
506
  text: tweet.text || "",
704
- timestamp: tweet.created_at ? new Date(tweet.created_at).getTime() : Date.now(),
507
+ // Twitter API returns created_at as ISO string, convert to seconds
508
+ // to match the expected Tweet.timestamp format used throughout the plugin
509
+ timestamp: tweet.created_at ? new Date(tweet.created_at).getTime() / 1e3 : Date.now() / 1e3,
705
510
  timeParsed: tweet.created_at ? new Date(tweet.created_at) : /* @__PURE__ */ new Date(),
706
511
  userId: tweet.author_id || "",
707
512
  name: searchIterator.includes?.users?.find((u) => u.id === tweet.author_id)?.name || "",
@@ -795,449 +600,6 @@ async function* searchQuotedTweets(quotedTweetId, maxTweets, auth) {
795
600
  const query = `url:"twitter.com/*/status/${quotedTweetId}"`;
796
601
  yield* searchTweets(query, maxTweets, 1 /* Latest */, auth);
797
602
  }
798
- var fetchSearchTweets = async (query, maxTweets, searchMode, auth, cursor) => {
799
- throw new Error(
800
- "fetchSearchTweets is deprecated. Use searchTweets generator instead."
801
- );
802
- };
803
- var fetchSearchProfiles = async (query, maxProfiles, auth, cursor) => {
804
- throw new Error(
805
- "fetchSearchProfiles is deprecated. Use searchProfiles generator instead."
806
- );
807
- };
808
-
809
- // src/client/timeline-following.ts
810
- async function fetchFollowingTimeline(count, seenTweetIds, auth) {
811
- const variables = {
812
- count,
813
- includePromotedContent: true,
814
- latestControlAvailable: true,
815
- requestContext: "launch",
816
- seenTweetIds
817
- };
818
- const features2 = {
819
- profile_label_improvements_pcf_label_in_post_enabled: true,
820
- rweb_tipjar_consumption_enabled: true,
821
- responsive_web_graphql_exclude_directive_enabled: true,
822
- verified_phone_label_enabled: false,
823
- creator_subscriptions_tweet_preview_api_enabled: true,
824
- responsive_web_graphql_timeline_navigation_enabled: true,
825
- responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
826
- communities_web_enable_tweet_community_results_fetch: true,
827
- c9s_tweet_anatomy_moderator_badge_enabled: true,
828
- articles_preview_enabled: true,
829
- responsive_web_edit_tweet_api_enabled: true,
830
- graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
831
- view_counts_everywhere_api_enabled: true,
832
- longform_notetweets_consumption_enabled: true,
833
- responsive_web_twitter_article_tweet_consumption_enabled: true,
834
- tweet_awards_web_tipping_enabled: false,
835
- creator_subscriptions_quote_tweet_preview_enabled: false,
836
- freedom_of_speech_not_reach_fetch_enabled: true,
837
- standardized_nudges_misinfo: true,
838
- tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
839
- rweb_video_timestamps_enabled: true,
840
- longform_notetweets_rich_text_read_enabled: true,
841
- longform_notetweets_inline_media_enabled: true,
842
- responsive_web_enhance_cards_enabled: false
843
- };
844
- const res = await requestApi(
845
- `https://x.com/i/api/graphql/K0X1xbCZUjttdK8RazKAlw/HomeLatestTimeline?variables=${encodeURIComponent(
846
- JSON.stringify(variables)
847
- )}&features=${encodeURIComponent(JSON.stringify(features2))}`,
848
- auth,
849
- "GET"
850
- );
851
- if (!res.success) {
852
- if (res.err instanceof ApiError) {
853
- console.error("Error details:", res.err.data);
854
- }
855
- throw res.err;
856
- }
857
- const home = res.value?.data?.home.home_timeline_urt?.instructions;
858
- if (!home) {
859
- return [];
860
- }
861
- const entries = [];
862
- for (const instruction of home) {
863
- if (instruction.type === "TimelineAddEntries") {
864
- for (const entry of instruction.entries ?? []) {
865
- entries.push(entry);
866
- }
867
- }
868
- }
869
- const tweets = entries.map((entry) => entry.content.itemContent?.tweet_results?.result).filter((tweet) => tweet !== void 0);
870
- return tweets;
871
- }
872
-
873
- // src/client/timeline-home.ts
874
- async function fetchHomeTimeline(count, seenTweetIds, auth) {
875
- const variables = {
876
- count,
877
- includePromotedContent: true,
878
- latestControlAvailable: true,
879
- requestContext: "launch",
880
- withCommunity: true,
881
- seenTweetIds
882
- };
883
- const features2 = {
884
- rweb_tipjar_consumption_enabled: true,
885
- responsive_web_graphql_exclude_directive_enabled: true,
886
- verified_phone_label_enabled: false,
887
- creator_subscriptions_tweet_preview_api_enabled: true,
888
- responsive_web_graphql_timeline_navigation_enabled: true,
889
- responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
890
- communities_web_enable_tweet_community_results_fetch: true,
891
- c9s_tweet_anatomy_moderator_badge_enabled: true,
892
- articles_preview_enabled: true,
893
- responsive_web_edit_tweet_api_enabled: true,
894
- graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
895
- view_counts_everywhere_api_enabled: true,
896
- longform_notetweets_consumption_enabled: true,
897
- responsive_web_twitter_article_tweet_consumption_enabled: true,
898
- tweet_awards_web_tipping_enabled: false,
899
- creator_subscriptions_quote_tweet_preview_enabled: false,
900
- freedom_of_speech_not_reach_fetch_enabled: true,
901
- standardized_nudges_misinfo: true,
902
- tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
903
- rweb_video_timestamps_enabled: true,
904
- longform_notetweets_rich_text_read_enabled: true,
905
- longform_notetweets_inline_media_enabled: true,
906
- responsive_web_enhance_cards_enabled: false
907
- };
908
- const res = await requestApi(
909
- `https://x.com/i/api/graphql/HJFjzBgCs16TqxewQOeLNg/HomeTimeline?variables=${encodeURIComponent(
910
- JSON.stringify(variables)
911
- )}&features=${encodeURIComponent(JSON.stringify(features2))}`,
912
- auth,
913
- "GET"
914
- );
915
- if (!res.success) {
916
- if (res.err instanceof ApiError) {
917
- console.error("Error details:", res.err.data);
918
- }
919
- throw res.err;
920
- }
921
- const home = res.value?.data?.home.home_timeline_urt?.instructions;
922
- if (!home) {
923
- return [];
924
- }
925
- const entries = [];
926
- for (const instruction of home) {
927
- if (instruction.type === "TimelineAddEntries") {
928
- for (const entry of instruction.entries ?? []) {
929
- entries.push(entry);
930
- }
931
- }
932
- }
933
- const tweets = entries.map((entry) => entry.content.itemContent?.tweet_results?.result).filter((tweet) => tweet !== void 0);
934
- return tweets;
935
- }
936
-
937
- // src/client/type-util.ts
938
- function isFieldDefined(key) {
939
- return (value) => isDefined(value[key]);
940
- }
941
- function isDefined(value) {
942
- return value != null;
943
- }
944
-
945
- // src/client/timeline-tweet-util.ts
946
- var reHashtag = /\B(\#\S+\b)/g;
947
- var reCashtag = /\B(\$\S+\b)/g;
948
- var reTwitterUrl = /https:(\/\/t\.co\/([A-Za-z0-9]|[A-Za-z]){10})/g;
949
- var reUsername = /\B(\@\S{1,15}\b)/g;
950
- function parseMediaGroups(media) {
951
- const photos = [];
952
- const videos = [];
953
- let sensitiveContent = void 0;
954
- for (const m of media.filter(isFieldDefined("id_str")).filter(isFieldDefined("media_url_https"))) {
955
- if (m.type === "photo") {
956
- photos.push({
957
- id: m.id_str,
958
- url: m.media_url_https,
959
- alt_text: m.ext_alt_text
960
- });
961
- } else if (m.type === "video") {
962
- videos.push(parseVideo(m));
963
- }
964
- const sensitive = m.ext_sensitive_media_warning;
965
- if (sensitive != null) {
966
- sensitiveContent = sensitive.adult_content || sensitive.graphic_violence || sensitive.other;
967
- }
968
- }
969
- return { sensitiveContent, photos, videos };
970
- }
971
- function parseVideo(m) {
972
- const video = {
973
- id: m.id_str,
974
- preview: m.media_url_https
975
- };
976
- let maxBitrate = 0;
977
- const variants = m.video_info?.variants ?? [];
978
- for (const variant of variants) {
979
- const bitrate = variant.bitrate;
980
- if (bitrate != null && bitrate > maxBitrate && variant.url != null) {
981
- let variantUrl = variant.url;
982
- const stringStart = 0;
983
- const tagSuffixIdx = variantUrl.indexOf("?tag=10");
984
- if (tagSuffixIdx !== -1) {
985
- variantUrl = variantUrl.substring(stringStart, tagSuffixIdx + 1);
986
- }
987
- video.url = variantUrl;
988
- maxBitrate = bitrate;
989
- }
990
- }
991
- return video;
992
- }
993
- function reconstructTweetHtml(tweet, photos, videos) {
994
- const media = [];
995
- let html = tweet.full_text ?? "";
996
- html = html.replace(reHashtag, linkHashtagHtml);
997
- html = html.replace(reCashtag, linkCashtagHtml);
998
- html = html.replace(reUsername, linkUsernameHtml);
999
- html = html.replace(reTwitterUrl, unwrapTcoUrlHtml(tweet, media));
1000
- for (const { url } of photos) {
1001
- if (media.indexOf(url) !== -1) {
1002
- continue;
1003
- }
1004
- html += `<br><img src="${url}"/>`;
1005
- }
1006
- for (const { preview: url } of videos) {
1007
- if (media.indexOf(url) !== -1) {
1008
- continue;
1009
- }
1010
- html += `<br><img src="${url}"/>`;
1011
- }
1012
- html = html.replace(/\n/g, "<br>");
1013
- return html;
1014
- }
1015
- function linkHashtagHtml(hashtag) {
1016
- return `<a href="https://twitter.com/hashtag/${hashtag.replace("#", "")}">${hashtag}</a>`;
1017
- }
1018
- function linkCashtagHtml(cashtag) {
1019
- return `<a href="https://twitter.com/search?q=%24${cashtag.replace("$", "")}">${cashtag}</a>`;
1020
- }
1021
- function linkUsernameHtml(username) {
1022
- return `<a href="https://twitter.com/${username.replace("@", "")}">${username}</a>`;
1023
- }
1024
- function unwrapTcoUrlHtml(tweet, foundedMedia) {
1025
- return (tco) => {
1026
- for (const entity of tweet.entities?.urls ?? []) {
1027
- if (tco === entity.url && entity.expanded_url != null) {
1028
- return `<a href="${entity.expanded_url}">${tco}</a>`;
1029
- }
1030
- }
1031
- for (const entity of tweet.extended_entities?.media ?? []) {
1032
- if (tco === entity.url && entity.media_url_https != null) {
1033
- foundedMedia.push(entity.media_url_https);
1034
- return `<br><a href="${tco}"><img src="${entity.media_url_https}"/></a>`;
1035
- }
1036
- }
1037
- return tco;
1038
- };
1039
- }
1040
-
1041
- // src/client/timeline-v2.ts
1042
- function parseLegacyTweet(user, tweet) {
1043
- if (tweet == null) {
1044
- return {
1045
- success: false,
1046
- err: new Error("Tweet was not found in the timeline object.")
1047
- };
1048
- }
1049
- if (user == null) {
1050
- return {
1051
- success: false,
1052
- err: new Error("User was not found in the timeline object.")
1053
- };
1054
- }
1055
- if (!tweet.id_str) {
1056
- if (!tweet.conversation_id_str) {
1057
- return {
1058
- success: false,
1059
- err: new Error("Tweet ID was not found in object.")
1060
- };
1061
- }
1062
- tweet.id_str = tweet.conversation_id_str;
1063
- }
1064
- const hashtags = tweet.entities?.hashtags ?? [];
1065
- const mentions = tweet.entities?.user_mentions ?? [];
1066
- const media = tweet.extended_entities?.media ?? [];
1067
- const pinnedTweets = new Set(
1068
- user.pinned_tweet_ids_str ?? []
1069
- );
1070
- const urls = tweet.entities?.urls ?? [];
1071
- const { photos, videos, sensitiveContent } = parseMediaGroups(media);
1072
- const tw = {
1073
- bookmarkCount: tweet.bookmark_count,
1074
- conversationId: tweet.conversation_id_str,
1075
- id: tweet.id_str,
1076
- hashtags: hashtags.filter(isFieldDefined("text")).map((hashtag) => hashtag.text),
1077
- likes: tweet.favorite_count,
1078
- mentions: mentions.filter(isFieldDefined("id_str")).map((mention) => ({
1079
- id: mention.id_str,
1080
- username: mention.screen_name,
1081
- name: mention.name
1082
- })),
1083
- name: user.name,
1084
- permanentUrl: `https://twitter.com/${user.screen_name}/status/${tweet.id_str}`,
1085
- photos,
1086
- replies: tweet.reply_count,
1087
- retweets: tweet.retweet_count,
1088
- text: tweet.full_text,
1089
- thread: [],
1090
- urls: urls.filter(isFieldDefined("expanded_url")).map((url) => url.expanded_url),
1091
- userId: tweet.user_id_str,
1092
- username: user.screen_name,
1093
- videos,
1094
- isQuoted: false,
1095
- isReply: false,
1096
- isRetweet: false,
1097
- isPin: false,
1098
- sensitiveContent: false
1099
- };
1100
- if (tweet.created_at) {
1101
- tw.timeParsed = new Date(Date.parse(tweet.created_at));
1102
- tw.timestamp = Math.floor(tw.timeParsed.valueOf() / 1e3);
1103
- }
1104
- if (tweet.place?.id) {
1105
- tw.place = tweet.place;
1106
- }
1107
- const quotedStatusIdStr = tweet.quoted_status_id_str;
1108
- const inReplyToStatusIdStr = tweet.in_reply_to_status_id_str;
1109
- const retweetedStatusIdStr = tweet.retweeted_status_id_str;
1110
- const retweetedStatusResult = tweet.retweeted_status_result?.result;
1111
- if (quotedStatusIdStr) {
1112
- tw.isQuoted = true;
1113
- tw.quotedStatusId = quotedStatusIdStr;
1114
- }
1115
- if (inReplyToStatusIdStr) {
1116
- tw.isReply = true;
1117
- tw.inReplyToStatusId = inReplyToStatusIdStr;
1118
- }
1119
- if (retweetedStatusIdStr || retweetedStatusResult) {
1120
- tw.isRetweet = true;
1121
- tw.retweetedStatusId = retweetedStatusIdStr;
1122
- if (retweetedStatusResult) {
1123
- const parsedResult = parseLegacyTweet(
1124
- retweetedStatusResult?.core?.user_results?.result?.legacy,
1125
- retweetedStatusResult?.legacy
1126
- );
1127
- if (parsedResult.success) {
1128
- tw.retweetedStatus = parsedResult.tweet;
1129
- }
1130
- }
1131
- }
1132
- const views = Number.parseInt(tweet.ext_views?.count ?? "");
1133
- if (!Number.isNaN(views)) {
1134
- tw.views = views;
1135
- }
1136
- if (pinnedTweets.has(tweet.id_str)) {
1137
- tw.isPin = true;
1138
- }
1139
- if (sensitiveContent) {
1140
- tw.sensitiveContent = true;
1141
- }
1142
- tw.html = reconstructTweetHtml(tweet, tw.photos, tw.videos);
1143
- return { success: true, tweet: tw };
1144
- }
1145
- function parseResult(result) {
1146
- const noteTweetResultText = result?.note_tweet?.note_tweet_results?.result?.text;
1147
- if (result?.legacy && noteTweetResultText) {
1148
- result.legacy.full_text = noteTweetResultText;
1149
- }
1150
- const tweetResult = parseLegacyTweet(
1151
- result?.core?.user_results?.result?.legacy,
1152
- result?.legacy
1153
- );
1154
- if (!tweetResult.success) {
1155
- return tweetResult;
1156
- }
1157
- if (!tweetResult.tweet.views && result?.views?.count) {
1158
- const views = Number.parseInt(result.views.count);
1159
- if (!Number.isNaN(views)) {
1160
- tweetResult.tweet.views = views;
1161
- }
1162
- }
1163
- const quotedResult = result?.quoted_status_result?.result;
1164
- if (quotedResult) {
1165
- if (quotedResult.legacy && quotedResult.rest_id) {
1166
- quotedResult.legacy.id_str = quotedResult.rest_id;
1167
- }
1168
- const quotedTweetResult = parseResult(quotedResult);
1169
- if (quotedTweetResult.success) {
1170
- tweetResult.tweet.quotedStatus = quotedTweetResult.tweet;
1171
- }
1172
- }
1173
- return tweetResult;
1174
- }
1175
- var expectedEntryTypes = ["tweet", "profile-conversation"];
1176
- function parseTimelineTweetsV2(timeline) {
1177
- let bottomCursor;
1178
- let topCursor;
1179
- const tweets = [];
1180
- const instructions = timeline.data?.user?.result?.timeline_v2?.timeline?.instructions ?? [];
1181
- for (const instruction of instructions) {
1182
- const entries = instruction.entries ?? [];
1183
- for (const entry of entries) {
1184
- const entryContent = entry.content;
1185
- if (!entryContent) continue;
1186
- if (entryContent.cursorType === "Bottom") {
1187
- bottomCursor = entryContent.value;
1188
- continue;
1189
- }
1190
- if (entryContent.cursorType === "Top") {
1191
- topCursor = entryContent.value;
1192
- continue;
1193
- }
1194
- const idStr = entry.entryId;
1195
- if (!expectedEntryTypes.some((entryType) => idStr.startsWith(entryType))) {
1196
- continue;
1197
- }
1198
- if (entryContent.itemContent) {
1199
- parseAndPush(tweets, entryContent.itemContent, idStr);
1200
- } else if (entryContent.items) {
1201
- for (const item of entryContent.items) {
1202
- if (item.item?.itemContent) {
1203
- parseAndPush(tweets, item.item.itemContent, idStr);
1204
- }
1205
- }
1206
- }
1207
- }
1208
- }
1209
- return { tweets, next: bottomCursor, previous: topCursor };
1210
- }
1211
- function parseTimelineEntryItemContentRaw(content, entryId, isConversation = false) {
1212
- let result = content.tweet_results?.result ?? content.tweetResult?.result;
1213
- if (result?.__typename === "Tweet" || result?.__typename === "TweetWithVisibilityResults" && result?.tweet) {
1214
- if (result?.__typename === "TweetWithVisibilityResults")
1215
- result = result.tweet;
1216
- if (result?.legacy) {
1217
- result.legacy.id_str = result.rest_id ?? entryId.replace("conversation-", "").replace("tweet-", "");
1218
- }
1219
- const tweetResult = parseResult(result);
1220
- if (tweetResult.success) {
1221
- if (isConversation) {
1222
- if (content?.tweetDisplayType === "SelfThread") {
1223
- tweetResult.tweet.isSelfThread = true;
1224
- }
1225
- }
1226
- return tweetResult.tweet;
1227
- }
1228
- }
1229
- return null;
1230
- }
1231
- function parseAndPush(tweets, content, entryId, isConversation = false) {
1232
- const tweet = parseTimelineEntryItemContentRaw(
1233
- content,
1234
- entryId,
1235
- isConversation
1236
- );
1237
- if (tweet) {
1238
- tweets.push(tweet);
1239
- }
1240
- }
1241
603
 
1242
604
  // src/client/tweets.ts
1243
605
  var defaultOptions = {
@@ -1318,13 +680,6 @@ var defaultOptions = {
1318
680
  "place_type"
1319
681
  ]
1320
682
  };
1321
- var features = addApiFeatures({
1322
- interactive_text_enabled: true,
1323
- longform_notetweets_inline_media_enabled: false,
1324
- responsive_web_text_conversations_enabled: false,
1325
- tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: false,
1326
- vibe_api_enabled: false
1327
- });
1328
683
  async function fetchTweets(userId, maxTweets, cursor, auth) {
1329
684
  const client = auth.getV2Client();
1330
685
  try {
@@ -1360,7 +715,7 @@ async function fetchTweets(userId, maxTweets, cursor, auth) {
1360
715
  next: response.meta.next_token
1361
716
  };
1362
717
  } catch (error) {
1363
- throw new Error(`Failed to fetch tweets: ${error.message}`);
718
+ throw new Error(`Failed to fetch tweets: ${error?.message || error}`);
1364
719
  }
1365
720
  }
1366
721
  async function fetchTweetsAndReplies(userId, maxTweets, cursor, auth) {
@@ -1397,7 +752,7 @@ async function fetchTweetsAndReplies(userId, maxTweets, cursor, auth) {
1397
752
  next: response.meta.next_token
1398
753
  };
1399
754
  } catch (error) {
1400
- throw new Error(`Failed to fetch tweets and replies: ${error.message}`);
755
+ throw new Error(`Failed to fetch tweets and replies: ${error?.message || error}`);
1401
756
  }
1402
757
  }
1403
758
  async function createCreateTweetRequestV2(text, auth, tweetId, options) {
@@ -1442,45 +797,50 @@ async function createCreateTweetRequestV2(text, auth, tweetId, options) {
1442
797
  }
1443
798
  return await getTweetV2(tweetResponse.data.id, auth, optionsConfig);
1444
799
  }
1445
- function parseTweetV2ToV1(tweetV2, includes, defaultTweetData) {
1446
- let parsedTweet;
1447
- if (defaultTweetData != null) {
1448
- parsedTweet = defaultTweetData;
1449
- }
1450
- parsedTweet = {
800
+ function parseTweetV2ToV1(tweetV2, includes) {
801
+ const parsedTweet = {
1451
802
  id: tweetV2.id,
1452
- text: tweetV2.text ?? defaultTweetData?.text ?? "",
1453
- hashtags: tweetV2.entities?.hashtags?.map((tag) => tag.tag) ?? defaultTweetData?.hashtags ?? [],
803
+ text: tweetV2.text ?? "",
804
+ hashtags: tweetV2.entities?.hashtags?.map((tag) => tag.tag) ?? [],
1454
805
  mentions: tweetV2.entities?.mentions?.map((mention) => ({
1455
806
  id: mention.id,
1456
807
  username: mention.username
1457
- })) ?? defaultTweetData?.mentions ?? [],
1458
- urls: tweetV2.entities?.urls?.map((url) => url.url) ?? defaultTweetData?.urls ?? [],
1459
- likes: tweetV2.public_metrics?.like_count ?? defaultTweetData?.likes ?? 0,
1460
- retweets: tweetV2.public_metrics?.retweet_count ?? defaultTweetData?.retweets ?? 0,
1461
- replies: tweetV2.public_metrics?.reply_count ?? defaultTweetData?.replies ?? 0,
1462
- views: tweetV2.public_metrics?.impression_count ?? defaultTweetData?.views ?? 0,
1463
- userId: tweetV2.author_id ?? defaultTweetData?.userId,
1464
- conversationId: tweetV2.conversation_id ?? defaultTweetData?.conversationId,
1465
- photos: defaultTweetData?.photos ?? [],
1466
- videos: defaultTweetData?.videos ?? [],
1467
- poll: defaultTweetData?.poll ?? null,
1468
- username: defaultTweetData?.username ?? "",
1469
- name: defaultTweetData?.name ?? "",
1470
- place: defaultTweetData?.place,
1471
- thread: defaultTweetData?.thread ?? []
808
+ })) ?? [],
809
+ urls: tweetV2.entities?.urls?.map((url) => url.url) ?? [],
810
+ likes: tweetV2.public_metrics?.like_count ?? 0,
811
+ retweets: tweetV2.public_metrics?.retweet_count ?? 0,
812
+ replies: tweetV2.public_metrics?.reply_count ?? 0,
813
+ quotes: tweetV2.public_metrics?.quote_count ?? 0,
814
+ views: tweetV2.public_metrics?.impression_count ?? 0,
815
+ userId: tweetV2.author_id,
816
+ conversationId: tweetV2.conversation_id,
817
+ photos: [],
818
+ videos: [],
819
+ poll: null,
820
+ username: "",
821
+ name: "",
822
+ thread: [],
823
+ timestamp: tweetV2.created_at ? new Date(tweetV2.created_at).getTime() / 1e3 : Date.now() / 1e3,
824
+ permanentUrl: `https://twitter.com/i/status/${tweetV2.id}`,
825
+ // Check for referenced tweets
826
+ isReply: tweetV2.referenced_tweets?.some((ref) => ref.type === "replied_to") ?? false,
827
+ isRetweet: tweetV2.referenced_tweets?.some((ref) => ref.type === "retweeted") ?? false,
828
+ isQuoted: tweetV2.referenced_tweets?.some((ref) => ref.type === "quoted") ?? false,
829
+ inReplyToStatusId: tweetV2.referenced_tweets?.find((ref) => ref.type === "replied_to")?.id,
830
+ quotedStatusId: tweetV2.referenced_tweets?.find((ref) => ref.type === "quoted")?.id,
831
+ retweetedStatusId: tweetV2.referenced_tweets?.find((ref) => ref.type === "retweeted")?.id
1472
832
  };
1473
833
  if (includes?.polls?.length) {
1474
834
  const poll = includes.polls[0];
1475
835
  parsedTweet.poll = {
1476
836
  id: poll.id,
1477
- end_datetime: poll.end_datetime ? poll.end_datetime : defaultTweetData?.poll?.end_datetime ? defaultTweetData?.poll?.end_datetime : void 0,
837
+ end_datetime: poll.end_datetime,
1478
838
  options: poll.options.map((option) => ({
1479
839
  position: option.position,
1480
840
  label: option.label,
1481
841
  votes: option.votes
1482
842
  })),
1483
- voting_status: poll.voting_status ?? defaultTweetData?.poll?.voting_status
843
+ voting_status: poll.voting_status
1484
844
  };
1485
845
  }
1486
846
  if (includes?.media?.length) {
@@ -1507,8 +867,8 @@ function parseTweetV2ToV1(tweetV2, includes, defaultTweetData) {
1507
867
  (user2) => user2.id === tweetV2.author_id
1508
868
  );
1509
869
  if (user) {
1510
- parsedTweet.username = user.username ?? defaultTweetData?.username ?? "";
1511
- parsedTweet.name = user.name ?? defaultTweetData?.name ?? "";
870
+ parsedTweet.username = user.username ?? "";
871
+ parsedTweet.name = user.name ?? "";
1512
872
  }
1513
873
  }
1514
874
  if (tweetV2?.geo?.place_id && includes?.places?.length) {
@@ -1518,11 +878,11 @@ function parseTweetV2ToV1(tweetV2, includes, defaultTweetData) {
1518
878
  if (place) {
1519
879
  parsedTweet.place = {
1520
880
  id: place.id,
1521
- full_name: place.full_name ?? defaultTweetData?.place?.full_name ?? "",
1522
- country: place.country ?? defaultTweetData?.place?.country ?? "",
1523
- country_code: place.country_code ?? defaultTweetData?.place?.country_code ?? "",
1524
- name: place.name ?? defaultTweetData?.place?.name ?? "",
1525
- place_type: place.place_type ?? defaultTweetData?.place?.place_type
881
+ full_name: place.full_name ?? "",
882
+ country: place.country ?? "",
883
+ country_code: place.country_code ?? "",
884
+ name: place.name ?? "",
885
+ place_type: place.place_type
1526
886
  };
1527
887
  }
1528
888
  }
@@ -1552,7 +912,7 @@ async function createCreateTweetRequest(text, auth, tweetId, mediaData, hideLink
1552
912
  data: result
1553
913
  };
1554
914
  } catch (error) {
1555
- throw new Error(`Failed to create tweet: ${error.message}`);
915
+ throw new Error(`Failed to create tweet: ${error?.message || error}`);
1556
916
  }
1557
917
  }
1558
918
  async function createCreateNoteTweetRequest(text, auth, tweetId, mediaData) {
@@ -1611,35 +971,71 @@ async function deleteTweet(tweetId, auth) {
1611
971
  throw new Error(`Failed to delete tweet: ${error.message}`);
1612
972
  }
1613
973
  }
1614
- function getTweets(user, maxTweets, auth) {
1615
- return getTweetTimeline(user, maxTweets, async (q, mt, c) => {
1616
- const userIdRes = await getEntityIdByScreenName(q, auth);
1617
- if (!userIdRes.success) {
1618
- throw userIdRes.err;
1619
- }
1620
- const { value: userId } = userIdRes;
1621
- return fetchTweets(userId, mt, c, auth);
1622
- });
974
+ async function* getTweets(user, maxTweets, auth) {
975
+ const userIdRes = await getEntityIdByScreenName(user, auth);
976
+ if (!userIdRes.success) {
977
+ throw userIdRes.err;
978
+ }
979
+ const { value: userId } = userIdRes;
980
+ let cursor;
981
+ let totalFetched = 0;
982
+ while (totalFetched < maxTweets) {
983
+ const response = await fetchTweets(userId, maxTweets - totalFetched, cursor, auth);
984
+ for (const tweet of response.tweets) {
985
+ yield tweet;
986
+ totalFetched++;
987
+ if (totalFetched >= maxTweets) break;
988
+ }
989
+ cursor = response.next;
990
+ if (!cursor) break;
991
+ }
1623
992
  }
1624
- function getTweetsByUserId(userId, maxTweets, auth) {
1625
- return getTweetTimeline(userId, maxTweets, (q, mt, c) => {
1626
- return fetchTweets(q, mt, c, auth);
1627
- });
993
+ async function* getTweetsByUserId(userId, maxTweets, auth) {
994
+ let cursor;
995
+ let totalFetched = 0;
996
+ while (totalFetched < maxTweets) {
997
+ const response = await fetchTweets(userId, maxTweets - totalFetched, cursor, auth);
998
+ for (const tweet of response.tweets) {
999
+ yield tweet;
1000
+ totalFetched++;
1001
+ if (totalFetched >= maxTweets) break;
1002
+ }
1003
+ cursor = response.next;
1004
+ if (!cursor) break;
1005
+ }
1628
1006
  }
1629
- function getTweetsAndReplies(user, maxTweets, auth) {
1630
- return getTweetTimeline(user, maxTweets, async (q, mt, c) => {
1631
- const userIdRes = await getEntityIdByScreenName(q, auth);
1632
- if (!userIdRes.success) {
1633
- throw userIdRes.err;
1634
- }
1635
- const { value: userId } = userIdRes;
1636
- return fetchTweetsAndReplies(userId, mt, c, auth);
1637
- });
1007
+ async function* getTweetsAndReplies(user, maxTweets, auth) {
1008
+ const userIdRes = await getEntityIdByScreenName(user, auth);
1009
+ if (!userIdRes.success) {
1010
+ throw userIdRes.err;
1011
+ }
1012
+ const { value: userId } = userIdRes;
1013
+ let cursor;
1014
+ let totalFetched = 0;
1015
+ while (totalFetched < maxTweets) {
1016
+ const response = await fetchTweetsAndReplies(userId, maxTweets - totalFetched, cursor, auth);
1017
+ for (const tweet of response.tweets) {
1018
+ yield tweet;
1019
+ totalFetched++;
1020
+ if (totalFetched >= maxTweets) break;
1021
+ }
1022
+ cursor = response.next;
1023
+ if (!cursor) break;
1024
+ }
1638
1025
  }
1639
- function getTweetsAndRepliesByUserId(userId, maxTweets, auth) {
1640
- return getTweetTimeline(userId, maxTweets, (q, mt, c) => {
1641
- return fetchTweetsAndReplies(q, mt, c, auth);
1642
- });
1026
+ async function* getTweetsAndRepliesByUserId(userId, maxTweets, auth) {
1027
+ let cursor;
1028
+ let totalFetched = 0;
1029
+ while (totalFetched < maxTweets) {
1030
+ const response = await fetchTweetsAndReplies(userId, maxTweets - totalFetched, cursor, auth);
1031
+ for (const tweet of response.tweets) {
1032
+ yield tweet;
1033
+ totalFetched++;
1034
+ if (totalFetched >= maxTweets) break;
1035
+ }
1036
+ cursor = response.next;
1037
+ if (!cursor) break;
1038
+ }
1643
1039
  }
1644
1040
  async function getTweetWhere(tweets, query) {
1645
1041
  const isCallback = typeof query === "function";
@@ -1723,11 +1119,9 @@ async function getTweetV2(id, auth, options = defaultOptions) {
1723
1119
  console.warn(`Tweet data not found for ID: ${id}`);
1724
1120
  return null;
1725
1121
  }
1726
- const defaultTweetData = await getTweet(tweetData.data.id, auth);
1727
1122
  const parsedTweet = parseTweetV2ToV1(
1728
1123
  tweetData.data,
1729
- tweetData?.includes,
1730
- defaultTweetData
1124
+ tweetData?.includes
1731
1125
  );
1732
1126
  return parsedTweet;
1733
1127
  } catch (error) {
@@ -1817,19 +1211,6 @@ async function retweet(tweetId, auth) {
1817
1211
  async function createCreateLongTweetRequest(text, auth, tweetId, mediaData) {
1818
1212
  return createCreateTweetRequest(text, auth, tweetId, mediaData);
1819
1213
  }
1820
- async function getArticle(id, auth) {
1821
- const tweet = await getTweet(id, auth);
1822
- if (!tweet) {
1823
- return null;
1824
- }
1825
- return {
1826
- id: tweet.id || id,
1827
- articleId: id,
1828
- title: "",
1829
- previewText: tweet.text?.substring(0, 100) || "",
1830
- text: tweet.text || ""
1831
- };
1832
- }
1833
1214
  async function fetchRetweetersPage(tweetId, auth, cursor, count = 40) {
1834
1215
  console.warn("Fetching retweeters not implemented for Twitter API v2");
1835
1216
  return {
@@ -1859,7 +1240,6 @@ async function getAllRetweeters(tweetId, auth) {
1859
1240
  }
1860
1241
 
1861
1242
  // src/client/client.ts
1862
- var UserTweetsUrl = "https://twitter.com/i/api/graphql/E3opETHurmVJflFsUBVuUQ/UserTweets";
1863
1243
  var Client = class {
1864
1244
  /**
1865
1245
  * Creates a new Client object.
@@ -1924,8 +1304,17 @@ var Client = class {
1924
1304
  * @param cursor The search cursor, which can be passed into further requests for more results.
1925
1305
  * @returns A page of results, containing a cursor that can be used in further requests.
1926
1306
  */
1927
- fetchSearchTweets(query, maxTweets, searchMode, cursor) {
1928
- return fetchSearchTweets(query, maxTweets, searchMode, this.auth, cursor);
1307
+ async fetchSearchTweets(query, maxTweets, searchMode, cursor) {
1308
+ const tweets = [];
1309
+ const generator = searchTweets(query, maxTweets, searchMode, this.auth);
1310
+ for await (const tweet of generator) {
1311
+ tweets.push(tweet);
1312
+ }
1313
+ return {
1314
+ tweets,
1315
+ // v2 API doesn't provide cursor-based pagination for search
1316
+ next: void 0
1317
+ };
1929
1318
  }
1930
1319
  /**
1931
1320
  * Fetches profiles from Twitter.
@@ -1934,8 +1323,17 @@ var Client = class {
1934
1323
  * @param cursor The search cursor, which can be passed into further requests for more results.
1935
1324
  * @returns A page of results, containing a cursor that can be used in further requests.
1936
1325
  */
1937
- fetchSearchProfiles(query, maxProfiles, cursor) {
1938
- return fetchSearchProfiles(query, maxProfiles, this.auth, cursor);
1326
+ async fetchSearchProfiles(query, maxProfiles, cursor) {
1327
+ const profiles = [];
1328
+ const generator = searchProfiles(query, maxProfiles, this.auth);
1329
+ for await (const profile of generator) {
1330
+ profiles.push(profile);
1331
+ }
1332
+ return {
1333
+ profiles,
1334
+ // v2 API doesn't provide cursor-based pagination for search
1335
+ next: void 0
1336
+ };
1939
1337
  }
1940
1338
  /**
1941
1339
  * Fetches list tweets from Twitter.
@@ -1986,82 +1384,101 @@ var Client = class {
1986
1384
  return fetchProfileFollowers(userId, maxProfiles, this.auth, cursor);
1987
1385
  }
1988
1386
  /**
1989
- * Fetches the home timeline for the current user. (for you feed)
1387
+ * Fetches the home timeline for the current user using Twitter API v2.
1388
+ * Note: Twitter API v2 doesn't distinguish between "For You" and "Following" feeds.
1990
1389
  * @param count The number of tweets to fetch.
1991
- * @param seenTweetIds An array of tweet IDs that have already been seen.
1992
- * @returns A promise that resolves to the home timeline response.
1390
+ * @param seenTweetIds An array of tweet IDs that have already been seen (not used in v2).
1391
+ * @returns A promise that resolves to an array of tweets.
1993
1392
  */
1994
1393
  async fetchHomeTimeline(count, seenTweetIds) {
1995
- return await fetchHomeTimeline(count, seenTweetIds, this.auth);
1394
+ if (!this.auth) {
1395
+ throw new Error("Not authenticated");
1396
+ }
1397
+ const client = this.auth.getV2Client();
1398
+ try {
1399
+ const timeline = await client.v2.homeTimeline({
1400
+ max_results: Math.min(count, 100),
1401
+ "tweet.fields": [
1402
+ "id",
1403
+ "text",
1404
+ "created_at",
1405
+ "author_id",
1406
+ "referenced_tweets",
1407
+ "entities",
1408
+ "public_metrics",
1409
+ "attachments",
1410
+ "conversation_id"
1411
+ ],
1412
+ "user.fields": ["id", "name", "username", "profile_image_url"],
1413
+ "media.fields": ["url", "preview_image_url", "type"],
1414
+ expansions: [
1415
+ "author_id",
1416
+ "attachments.media_keys",
1417
+ "referenced_tweets.id"
1418
+ ]
1419
+ });
1420
+ const tweets = [];
1421
+ for await (const tweet of timeline) {
1422
+ tweets.push(parseTweetV2ToV1(tweet, timeline.includes));
1423
+ if (tweets.length >= count) break;
1424
+ }
1425
+ return tweets;
1426
+ } catch (error) {
1427
+ console.error("Failed to fetch home timeline:", error);
1428
+ throw error;
1429
+ }
1996
1430
  }
1997
1431
  /**
1998
- * Fetches the home timeline for the current user. (following feed)
1432
+ * Fetches the home timeline for the current user (same as fetchHomeTimeline in v2).
1433
+ * Twitter API v2 doesn't provide separate "Following" timeline endpoint.
1999
1434
  * @param count The number of tweets to fetch.
2000
- * @param seenTweetIds An array of tweet IDs that have already been seen.
2001
- * @returns A promise that resolves to the home timeline response.
1435
+ * @param seenTweetIds An array of tweet IDs that have already been seen (not used in v2).
1436
+ * @returns A promise that resolves to an array of tweets.
2002
1437
  */
2003
1438
  async fetchFollowingTimeline(count, seenTweetIds) {
2004
- return await fetchFollowingTimeline(count, seenTweetIds, this.auth);
1439
+ return this.fetchHomeTimeline(count, seenTweetIds);
2005
1440
  }
2006
1441
  async getUserTweets(userId, maxTweets = 200, cursor) {
2007
- if (maxTweets > 200) {
2008
- maxTweets = 200;
2009
- }
2010
- const variables = {
2011
- userId,
2012
- count: maxTweets,
2013
- includePromotedContent: true,
2014
- withQuickPromoteEligibilityTweetFields: true,
2015
- withVoice: true,
2016
- withV2Timeline: true
2017
- };
2018
- if (cursor) {
2019
- variables.cursor = cursor;
2020
- }
2021
- const features2 = {
2022
- rweb_tipjar_consumption_enabled: true,
2023
- responsive_web_graphql_exclude_directive_enabled: true,
2024
- verified_phone_label_enabled: false,
2025
- creator_subscriptions_tweet_preview_api_enabled: true,
2026
- responsive_web_graphql_timeline_navigation_enabled: true,
2027
- responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
2028
- communities_web_enable_tweet_community_results_fetch: true,
2029
- c9s_tweet_anatomy_moderator_badge_enabled: true,
2030
- articles_preview_enabled: true,
2031
- responsive_web_edit_tweet_api_enabled: true,
2032
- graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
2033
- view_counts_everywhere_api_enabled: true,
2034
- longform_notetweets_consumption_enabled: true,
2035
- responsive_web_twitter_article_tweet_consumption_enabled: true,
2036
- tweet_awards_web_tipping_enabled: false,
2037
- creator_subscriptions_quote_tweet_preview_enabled: false,
2038
- freedom_of_speech_not_reach_fetch_enabled: true,
2039
- standardized_nudges_misinfo: true,
2040
- tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
2041
- rweb_video_timestamps_enabled: true,
2042
- longform_notetweets_rich_text_read_enabled: true,
2043
- longform_notetweets_inline_media_enabled: true,
2044
- responsive_web_enhance_cards_enabled: false
2045
- };
2046
- const fieldToggles = {
2047
- withArticlePlainText: false
2048
- };
2049
- const res = await requestApi(
2050
- `${UserTweetsUrl}?variables=${encodeURIComponent(
2051
- JSON.stringify(variables)
2052
- )}&features=${encodeURIComponent(JSON.stringify(features2))}&fieldToggles=${encodeURIComponent(
2053
- JSON.stringify(fieldToggles)
2054
- )}`,
2055
- this.auth
2056
- );
2057
- if (!res.success) {
2058
- throw res.err;
1442
+ if (!this.auth) {
1443
+ throw new Error("Not authenticated");
1444
+ }
1445
+ const client = this.auth.getV2Client();
1446
+ try {
1447
+ const response = await client.v2.userTimeline(userId, {
1448
+ max_results: Math.min(maxTweets, 100),
1449
+ "tweet.fields": [
1450
+ "id",
1451
+ "text",
1452
+ "created_at",
1453
+ "author_id",
1454
+ "referenced_tweets",
1455
+ "entities",
1456
+ "public_metrics",
1457
+ "attachments",
1458
+ "conversation_id"
1459
+ ],
1460
+ "user.fields": ["id", "name", "username", "profile_image_url"],
1461
+ "media.fields": ["url", "preview_image_url", "type"],
1462
+ expansions: [
1463
+ "author_id",
1464
+ "attachments.media_keys",
1465
+ "referenced_tweets.id"
1466
+ ],
1467
+ pagination_token: cursor
1468
+ });
1469
+ const tweets = [];
1470
+ for await (const tweet of response) {
1471
+ tweets.push(parseTweetV2ToV1(tweet, response.includes));
1472
+ if (tweets.length >= maxTweets) break;
1473
+ }
1474
+ return {
1475
+ tweets,
1476
+ next: response.meta?.next_token
1477
+ };
1478
+ } catch (error) {
1479
+ console.error("Failed to fetch user tweets:", error);
1480
+ throw error;
2059
1481
  }
2060
- const timelineV2 = parseTimelineTweetsV2(res.value);
2061
- return {
2062
- tweets: timelineV2.tweets,
2063
- next: timelineV2.next
2064
- };
2065
1482
  }
2066
1483
  async *getUserTweetsIterator(userId, maxTweets = 200) {
2067
1484
  let cursor;
@@ -2344,32 +1761,6 @@ var Client = class {
2344
1761
  "Logout is not applicable when using Twitter API v2 credentials"
2345
1762
  );
2346
1763
  }
2347
- /**
2348
- * Sets the optional cookie to be used in requests.
2349
- * @param _cookie The cookie to be used in requests.
2350
- * @deprecated This function no longer represents any part of Twitter's auth flow.
2351
- * @returns This client instance.
2352
- */
2353
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2354
- withCookie(_cookie) {
2355
- console.warn(
2356
- "Warning: Client#withCookie is deprecated and will be removed in a later version. Use Client#login or Client#setCookies instead."
2357
- );
2358
- return this;
2359
- }
2360
- /**
2361
- * Sets the optional CSRF token to be used in requests.
2362
- * @param _token The CSRF token to be used in requests.
2363
- * @deprecated This function no longer represents any part of Twitter's auth flow.
2364
- * @returns This client instance.
2365
- */
2366
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2367
- withXCsrfToken(_token) {
2368
- console.warn(
2369
- "Warning: Client#withXCsrfToken is deprecated and will be removed in a later version."
2370
- );
2371
- return this;
2372
- }
2373
1764
  /**
2374
1765
  * Sends a quote tweet.
2375
1766
  * @param text The text of the tweet.
@@ -2453,14 +1844,6 @@ var Client = class {
2453
1844
  }
2454
1845
  return res.value;
2455
1846
  }
2456
- /**
2457
- * Fetches a article (long form tweet) by its ID.
2458
- * @param id The ID of the article to fetch. In the format of (http://x.com/i/article/id)
2459
- * @returns The {@link TimelineArticle} object, or `null` if it couldn't be fetched.
2460
- */
2461
- getArticle(id) {
2462
- return getArticle(id, this.auth);
2463
- }
2464
1847
  /**
2465
1848
  * Retrieves all users who retweeted the given tweet.
2466
1849
  * @param tweetId The ID of the tweet.
@@ -2670,72 +2053,6 @@ var _ClientBase = class _ClientBase {
2670
2053
  onReady() {
2671
2054
  throw new Error("Not implemented in base class, please call from subclass");
2672
2055
  }
2673
- /**
2674
- * Parse the raw tweet data into a standardized Tweet object.
2675
- */
2676
- /**
2677
- * Parses a raw tweet object into a structured Tweet object.
2678
- *
2679
- * @param {any} raw - The raw tweet object to parse.
2680
- * @param {number} [depth=0] - The current depth of parsing nested quotes/retweets.
2681
- * @param {number} [maxDepth=3] - The maximum depth allowed for parsing nested quotes/retweets.
2682
- * @returns {Tweet} The parsed Tweet object.
2683
- */
2684
- parseTweet(raw, depth = 0, maxDepth = 3) {
2685
- const canRecurse = depth < maxDepth;
2686
- const quotedStatus = raw.quoted_status_result?.result && canRecurse ? this.parseTweet(raw.quoted_status_result.result, depth + 1, maxDepth) : void 0;
2687
- const retweetedStatus = raw.retweeted_status_result?.result && canRecurse ? this.parseTweet(
2688
- raw.retweeted_status_result.result,
2689
- depth + 1,
2690
- maxDepth
2691
- ) : void 0;
2692
- const t = {
2693
- bookmarkCount: raw.bookmarkCount ?? raw.legacy?.bookmark_count ?? void 0,
2694
- conversationId: raw.conversationId ?? raw.legacy?.conversation_id_str,
2695
- hashtags: raw.hashtags ?? raw.legacy?.entities?.hashtags ?? [],
2696
- html: raw.html,
2697
- id: raw.id ?? raw.rest_id ?? raw.legacy.id_str ?? raw.id_str ?? void 0,
2698
- inReplyToStatus: raw.inReplyToStatus,
2699
- inReplyToStatusId: raw.inReplyToStatusId ?? raw.legacy?.in_reply_to_status_id_str ?? void 0,
2700
- isQuoted: raw.legacy?.is_quote_status === true,
2701
- isPin: raw.isPin,
2702
- isReply: raw.isReply,
2703
- isRetweet: raw.legacy?.retweeted === true,
2704
- isSelfThread: raw.isSelfThread,
2705
- language: raw.legacy?.lang,
2706
- likes: raw.legacy?.favorite_count ?? 0,
2707
- name: raw.name ?? raw?.user_results?.result?.legacy?.name ?? raw.core?.user_results?.result?.legacy?.name,
2708
- mentions: raw.mentions ?? raw.legacy?.entities?.user_mentions ?? [],
2709
- permanentUrl: raw.permanentUrl ?? (raw.core?.user_results?.result?.legacy?.screen_name && raw.rest_id ? `https://x.com/${raw.core?.user_results?.result?.legacy?.screen_name}/status/${raw.rest_id}` : void 0),
2710
- photos: raw.photos ?? (raw.legacy?.entities?.media?.filter((media) => media.type === "photo").map((media) => ({
2711
- id: media.id_str || media.rest_id || media.legacy.id_str,
2712
- url: media.media_url_https,
2713
- alt_text: media.alt_text
2714
- })) || []),
2715
- place: raw.place,
2716
- poll: raw.poll ?? null,
2717
- quotedStatus,
2718
- quotedStatusId: raw.quotedStatusId ?? raw.legacy?.quoted_status_id_str ?? void 0,
2719
- quotes: raw.legacy?.quote_count ?? 0,
2720
- replies: raw.legacy?.reply_count ?? 0,
2721
- retweets: raw.legacy?.retweet_count ?? 0,
2722
- retweetedStatus,
2723
- retweetedStatusId: raw.legacy?.retweeted_status_id_str ?? void 0,
2724
- text: raw.text ?? raw.legacy?.full_text ?? void 0,
2725
- thread: raw.thread || [],
2726
- timeParsed: raw.timeParsed ? new Date(raw.timeParsed) : raw.legacy?.created_at ? new Date(raw.legacy?.created_at) : void 0,
2727
- timestamp: raw.timestamp ?? (raw.legacy?.created_at ? new Date(raw.legacy.created_at).getTime() / 1e3 : void 0),
2728
- urls: raw.urls ?? raw.legacy?.entities?.urls ?? [],
2729
- userId: raw.userId ?? raw.legacy?.user_id_str ?? void 0,
2730
- username: raw.username ?? raw.core?.user_results?.result?.legacy?.screen_name ?? void 0,
2731
- videos: raw.videos ?? raw.legacy?.entities?.media?.filter(
2732
- (media) => media.type === "video"
2733
- ) ?? [],
2734
- views: raw.views?.count ? Number(raw.views.count) : 0,
2735
- sensitiveContent: raw.sensitiveContent
2736
- };
2737
- return t;
2738
- }
2739
2056
  async init() {
2740
2057
  const apiKey = this.state?.TWITTER_API_KEY || this.runtime.getSetting("TWITTER_API_KEY");
2741
2058
  const apiSecretKey = this.state?.TWITTER_API_SECRET_KEY || this.runtime.getSetting("TWITTER_API_SECRET_KEY");
@@ -2845,7 +2162,7 @@ var _ClientBase = class _ClientBase {
2845
2162
  this.profile.id,
2846
2163
  count
2847
2164
  );
2848
- return homeTimeline.tweets.map((t) => this.parseTweet(t));
2165
+ return homeTimeline.tweets;
2849
2166
  }
2850
2167
  /**
2851
2168
  * Fetch timeline for twitter account, optionally only from followed accounts
@@ -2853,8 +2170,7 @@ var _ClientBase = class _ClientBase {
2853
2170
  async fetchHomeTimeline(count, following) {
2854
2171
  logger.debug("fetching home timeline");
2855
2172
  const homeTimeline = following ? await this.twitterClient.fetchFollowingTimeline(count, []) : await this.twitterClient.fetchHomeTimeline(count, []);
2856
- const processedTimeline = homeTimeline.filter((t) => t.__typename !== "TweetWithVisibilityResults").map((tweet) => this.parseTweet(tweet));
2857
- return processedTimeline;
2173
+ return homeTimeline;
2858
2174
  }
2859
2175
  async fetchSearchTweets(query, maxTweets, searchMode, cursor) {
2860
2176
  try {
@@ -3162,7 +2478,7 @@ import {
3162
2478
  ContentType,
3163
2479
  EventType,
3164
2480
  createUniqueUuid as createUniqueUuid3,
3165
- logger as logger3
2481
+ logger as logger4
3166
2482
  } from "@elizaos/core";
3167
2483
 
3168
2484
  // src/utils.ts
@@ -3170,9 +2486,14 @@ import fs from "fs";
3170
2486
  import path from "path";
3171
2487
  import {
3172
2488
  createUniqueUuid as createUniqueUuid2,
3173
- logger as logger2,
2489
+ logger as logger3,
3174
2490
  truncateToCompleteSentence
3175
2491
  } from "@elizaos/core";
2492
+
2493
+ // src/utils/error-handler.ts
2494
+ import { logger as logger2 } from "@elizaos/core";
2495
+
2496
+ // src/utils.ts
3176
2497
  async function sendTweet(client, text, mediaData = [], tweetToReplyTo) {
3177
2498
  const isNoteTweet = text.length > TWEET_MAX_LENGTH;
3178
2499
  const postText = isNoteTweet ? truncateToCompleteSentence(text, TWEET_MAX_LENGTH) : text;
@@ -3183,9 +2504,9 @@ async function sendTweet(client, text, mediaData = [], tweetToReplyTo) {
3183
2504
  tweetToReplyTo,
3184
2505
  mediaData
3185
2506
  );
3186
- logger2.log("Successfully posted Tweet");
2507
+ logger3.log("Successfully posted Tweet");
3187
2508
  } catch (error) {
3188
- logger2.error("Error posting Tweet:", error);
2509
+ logger3.error("Error posting Tweet:", error);
3189
2510
  throw error;
3190
2511
  }
3191
2512
  try {
@@ -3197,14 +2518,14 @@ async function sendTweet(client, text, mediaData = [], tweetToReplyTo) {
3197
2518
  }
3198
2519
  await client.cacheLatestCheckedTweetId();
3199
2520
  await client.cacheTweet(tweetResult);
3200
- logger2.log("Successfully posted a tweet", tweetResult.id);
2521
+ logger3.log("Successfully posted a tweet", tweetResult.id);
3201
2522
  return tweetResult;
3202
2523
  }
3203
2524
  } catch (error) {
3204
- logger2.error("Error parsing tweet response:", error);
2525
+ logger3.error("Error parsing tweet response:", error);
3205
2526
  throw error;
3206
2527
  }
3207
- logger2.error("No valid response from Twitter API");
2528
+ logger3.error("No valid response from Twitter API");
3208
2529
  throw new Error("Failed to send tweet - no valid response");
3209
2530
  }
3210
2531
  var parseActionResponseFromText = (text) => {
@@ -7370,7 +6691,7 @@ var TwitterInteractionClient = class {
7370
6691
  * Asynchronously handles Twitter interactions by checking for mentions, processing tweets, and updating the last checked tweet ID.
7371
6692
  */
7372
6693
  async handleTwitterInteractions() {
7373
- logger3.log("Checking Twitter interactions");
6694
+ logger4.log("Checking Twitter interactions");
7374
6695
  const twitterUsername = this.client.profile?.username;
7375
6696
  try {
7376
6697
  const cursorKey = `twitter/${twitterUsername}/mention_cursor`;
@@ -7389,9 +6710,9 @@ var TwitterInteractionClient = class {
7389
6710
  }
7390
6711
  await this.processMentionTweets(mentionCandidates);
7391
6712
  await this.client.cacheLatestCheckedTweetId();
7392
- logger3.log("Finished checking Twitter interactions");
6713
+ logger4.log("Finished checking Twitter interactions");
7393
6714
  } catch (error) {
7394
- logger3.error("Error handling Twitter interactions:", error);
6715
+ logger4.error("Error handling Twitter interactions:", error);
7395
6716
  }
7396
6717
  }
7397
6718
  /**
@@ -7405,7 +6726,7 @@ var TwitterInteractionClient = class {
7405
6726
  * Note: MENTION_RECEIVED is currently disabled (see TODO below)
7406
6727
  */
7407
6728
  async processMentionTweets(mentionCandidates) {
7408
- logger3.log(
6729
+ logger4.log(
7409
6730
  "Completed checking mentioned tweets:",
7410
6731
  mentionCandidates.length
7411
6732
  );
@@ -7419,7 +6740,7 @@ var TwitterInteractionClient = class {
7419
6740
  targetUsersConfig
7420
6741
  );
7421
6742
  if (!shouldTarget) {
7422
- logger3.log(
6743
+ logger4.log(
7423
6744
  `Skipping tweet from @${tweet.username} - not in target users list`
7424
6745
  );
7425
6746
  }
@@ -7431,7 +6752,7 @@ var TwitterInteractionClient = class {
7431
6752
  const tweetId = createUniqueUuid3(this.runtime, tweet.id);
7432
6753
  const existingResponse = await this.runtime.getMemoryById(tweetId);
7433
6754
  if (existingResponse) {
7434
- logger3.log(`Already responded to tweet ${tweet.id}, skipping`);
6755
+ logger4.log(`Already responded to tweet ${tweet.id}, skipping`);
7435
6756
  continue;
7436
6757
  }
7437
6758
  const conversationRoomId = createUniqueUuid3(
@@ -7448,12 +6769,12 @@ var TwitterInteractionClient = class {
7448
6769
  (memory2) => memory2.content?.inReplyTo === tweetId || memory2.content?.source === "twitter" && memory2.agentId === this.runtime.agentId && memory2.content?.inReplyTo === tweetId
7449
6770
  );
7450
6771
  if (hasExistingReply) {
7451
- logger3.log(
6772
+ logger4.log(
7452
6773
  `Already replied to tweet ${tweet.id} (found existing reply), skipping`
7453
6774
  );
7454
6775
  continue;
7455
6776
  }
7456
- logger3.log("New Tweet found", tweet.permanentUrl);
6777
+ logger4.log("New Tweet found", tweet.permanentUrl);
7457
6778
  const entityId = createUniqueUuid3(
7458
6779
  this.runtime,
7459
6780
  tweet.userId === this.client.profile.id ? this.runtime.agentId : tweet.userId
@@ -7668,23 +6989,23 @@ var TwitterInteractionClient = class {
7668
6989
  thread
7669
6990
  }) {
7670
6991
  if (!message.content.text) {
7671
- logger3.log("Skipping Tweet with no text", tweet.id);
6992
+ logger4.log("Skipping Tweet with no text", tweet.id);
7672
6993
  return { text: "", actions: ["IGNORE"] };
7673
6994
  }
7674
6995
  const callback = async (response, tweetId) => {
7675
6996
  try {
7676
6997
  if (!response.text) {
7677
- logger3.warn("No text content in response, skipping tweet reply");
6998
+ logger4.warn("No text content in response, skipping tweet reply");
7678
6999
  return [];
7679
7000
  }
7680
7001
  const tweetToReplyTo = tweetId || tweet.id;
7681
7002
  if (this.isDryRun) {
7682
- logger3.info(
7003
+ logger4.info(
7683
7004
  `[DRY RUN] Would have replied to ${tweet.username} with: ${response.text}`
7684
7005
  );
7685
7006
  return [];
7686
7007
  }
7687
- logger3.info(`Replying to tweet ${tweetToReplyTo}`);
7008
+ logger4.info(`Replying to tweet ${tweetToReplyTo}`);
7688
7009
  const tweetResult = await sendTweet(
7689
7010
  this.client,
7690
7011
  response.text,
@@ -7694,7 +7015,7 @@ var TwitterInteractionClient = class {
7694
7015
  if (!tweetResult) {
7695
7016
  throw new Error("Failed to get tweet result from response");
7696
7017
  }
7697
- const responseId = createUniqueUuid3(this.runtime, tweetResult.rest_id);
7018
+ const responseId = createUniqueUuid3(this.runtime, tweetResult.id);
7698
7019
  const responseMemory = {
7699
7020
  id: responseId,
7700
7021
  entityId: this.runtime.agentId,
@@ -7710,7 +7031,7 @@ var TwitterInteractionClient = class {
7710
7031
  await this.runtime.createMemory(responseMemory, "messages");
7711
7032
  return [responseMemory];
7712
7033
  } catch (error) {
7713
- logger3.error("Error replying to tweet:", error);
7034
+ logger4.error("Error replying to tweet:", error);
7714
7035
  return [];
7715
7036
  }
7716
7037
  };
@@ -7733,17 +7054,17 @@ var TwitterInteractionClient = class {
7733
7054
  const thread = [];
7734
7055
  const visited = /* @__PURE__ */ new Set();
7735
7056
  async function processThread(currentTweet, depth = 0) {
7736
- logger3.log("Processing tweet:", {
7057
+ logger4.log("Processing tweet:", {
7737
7058
  id: currentTweet.id,
7738
7059
  inReplyToStatusId: currentTweet.inReplyToStatusId,
7739
7060
  depth
7740
7061
  });
7741
7062
  if (!currentTweet) {
7742
- logger3.log("No current tweet found for thread building");
7063
+ logger4.log("No current tweet found for thread building");
7743
7064
  return;
7744
7065
  }
7745
7066
  if (depth >= maxReplies) {
7746
- logger3.log("Reached maximum reply depth", depth);
7067
+ logger4.log("Reached maximum reply depth", depth);
7747
7068
  return;
7748
7069
  }
7749
7070
  const memory = await this.runtime.getMemoryById(
@@ -7781,37 +7102,37 @@ var TwitterInteractionClient = class {
7781
7102
  );
7782
7103
  }
7783
7104
  if (visited.has(currentTweet.id)) {
7784
- logger3.log("Already visited tweet:", currentTweet.id);
7105
+ logger4.log("Already visited tweet:", currentTweet.id);
7785
7106
  return;
7786
7107
  }
7787
7108
  visited.add(currentTweet.id);
7788
7109
  thread.unshift(currentTweet);
7789
7110
  if (currentTweet.inReplyToStatusId) {
7790
- logger3.log("Fetching parent tweet:", currentTweet.inReplyToStatusId);
7111
+ logger4.log("Fetching parent tweet:", currentTweet.inReplyToStatusId);
7791
7112
  try {
7792
7113
  const parentTweet = await this.twitterClient.getTweet(
7793
7114
  currentTweet.inReplyToStatusId
7794
7115
  );
7795
7116
  if (parentTweet) {
7796
- logger3.log("Found parent tweet:", {
7117
+ logger4.log("Found parent tweet:", {
7797
7118
  id: parentTweet.id,
7798
7119
  text: parentTweet.text?.slice(0, 50)
7799
7120
  });
7800
7121
  await processThread(parentTweet, depth + 1);
7801
7122
  } else {
7802
- logger3.log(
7123
+ logger4.log(
7803
7124
  "No parent tweet found for:",
7804
7125
  currentTweet.inReplyToStatusId
7805
7126
  );
7806
7127
  }
7807
7128
  } catch (error) {
7808
- logger3.log("Error fetching parent tweet:", {
7129
+ logger4.log("Error fetching parent tweet:", {
7809
7130
  tweetId: currentTweet.inReplyToStatusId,
7810
7131
  error
7811
7132
  });
7812
7133
  }
7813
7134
  } else {
7814
- logger3.log("Reached end of reply chain at:", currentTweet.id);
7135
+ logger4.log("Reached end of reply chain at:", currentTweet.id);
7815
7136
  }
7816
7137
  }
7817
7138
  await processThread.bind(this)(tweet, 0);
@@ -7837,7 +7158,7 @@ import {
7837
7158
  ChannelType as ChannelType3,
7838
7159
  EventType as EventType2,
7839
7160
  createUniqueUuid as createUniqueUuid4,
7840
- logger as logger4
7161
+ logger as logger5
7841
7162
  } from "@elizaos/core";
7842
7163
  var TwitterPostClient = class {
7843
7164
  /**
@@ -7850,17 +7171,20 @@ var TwitterPostClient = class {
7850
7171
  this.client = client;
7851
7172
  this.state = state;
7852
7173
  this.runtime = runtime;
7853
- this.isDryRun = this.state?.TWITTER_DRY_RUN || this.runtime.getSetting("TWITTER_DRY_RUN");
7854
- logger4.log("Twitter Client Configuration:");
7855
- logger4.log(`- Dry Run Mode: ${this.isDryRun ? "Enabled" : "Disabled"}`);
7856
- logger4.log(
7174
+ const dryRunSetting = this.state?.TWITTER_DRY_RUN ?? this.runtime.getSetting("TWITTER_DRY_RUN");
7175
+ this.isDryRun = dryRunSetting === true || dryRunSetting === "true" || typeof dryRunSetting === "string" && dryRunSetting.toLowerCase() === "true";
7176
+ logger5.log("Twitter Client Configuration:");
7177
+ logger5.log(`- Dry Run Mode: ${this.isDryRun ? "Enabled" : "Disabled"}`);
7178
+ logger5.log(
7857
7179
  `- Post Interval: ${this.state?.TWITTER_POST_INTERVAL_MIN || this.runtime.getSetting("TWITTER_POST_INTERVAL_MIN") || 90}-${this.state?.TWITTER_POST_INTERVAL_MAX || this.runtime.getSetting("TWITTER_POST_INTERVAL_MAX") || 180} minutes`
7858
7180
  );
7859
- logger4.log(
7860
- `- Post Immediately: ${this.state?.TWITTER_POST_IMMEDIATELY || this.runtime.getSetting("TWITTER_POST_IMMEDIATELY") ? "enabled" : "disabled"}`
7181
+ const postImmediatelySetting = this.state?.TWITTER_POST_IMMEDIATELY ?? this.runtime.getSetting("TWITTER_POST_IMMEDIATELY");
7182
+ const isPostImmediately = postImmediatelySetting === true || postImmediatelySetting === "true" || typeof postImmediatelySetting === "string" && postImmediatelySetting.toLowerCase() === "true";
7183
+ logger5.log(
7184
+ `- Post Immediately: ${isPostImmediately ? "enabled" : "disabled"}`
7861
7185
  );
7862
7186
  if (this.isDryRun) {
7863
- logger4.log(
7187
+ logger5.log(
7864
7188
  "Twitter client initialized in dry run mode - no actual tweets should be posted"
7865
7189
  );
7866
7190
  }
@@ -7869,7 +7193,7 @@ var TwitterPostClient = class {
7869
7193
  * Starts the Twitter post client, setting up a loop to periodically generate new tweets.
7870
7194
  */
7871
7195
  async start() {
7872
- logger4.log("Starting Twitter post client...");
7196
+ logger5.log("Starting Twitter post client...");
7873
7197
  const generateNewTweetLoop = async () => {
7874
7198
  const minPostMinutes = this.state?.TWITTER_POST_INTERVAL_MIN || this.runtime.getSetting("TWITTER_POST_INTERVAL_MIN") || 90;
7875
7199
  const maxPostMinutes = this.state?.TWITTER_POST_INTERVAL_MAX || this.runtime.getSetting("TWITTER_POST_INTERVAL_MAX") || 180;
@@ -7879,7 +7203,9 @@ var TwitterPostClient = class {
7879
7203
  setTimeout(generateNewTweetLoop, interval);
7880
7204
  };
7881
7205
  setTimeout(generateNewTweetLoop, 60 * 1e3);
7882
- if (this.state?.TWITTER_POST_IMMEDIATELY || this.runtime.getSetting("TWITTER_POST_IMMEDIATELY")) {
7206
+ const postImmediately = this.state?.TWITTER_POST_IMMEDIATELY ?? this.runtime.getSetting("TWITTER_POST_IMMEDIATELY");
7207
+ const shouldPostImmediately = postImmediately === true || postImmediately === "true" || typeof postImmediately === "string" && postImmediately.toLowerCase() === "true";
7208
+ if (shouldPostImmediately) {
7883
7209
  await new Promise((resolve) => setTimeout(resolve, 1e3));
7884
7210
  await this.generateNewTweet();
7885
7211
  }
@@ -7889,33 +7215,38 @@ var TwitterPostClient = class {
7889
7215
  * This approach aligns with our platform-independent architecture.
7890
7216
  */
7891
7217
  async generateNewTweet() {
7218
+ logger5.info("Attempting to generate new tweet...");
7892
7219
  try {
7893
7220
  const userId = this.client.profile?.id;
7894
7221
  if (!userId) {
7895
- logger4.error("Cannot generate tweet: Twitter profile not available");
7222
+ logger5.error("Cannot generate tweet: Twitter profile not available");
7896
7223
  return;
7897
7224
  }
7225
+ logger5.info(`Generating tweet for user: ${this.client.profile?.username} (${userId})`);
7898
7226
  const worldId = createUniqueUuid4(this.runtime, userId);
7899
7227
  const roomId = createUniqueUuid4(this.runtime, `${userId}-home`);
7900
7228
  const callback = async (content) => {
7229
+ logger5.info("Tweet generation callback triggered");
7901
7230
  try {
7902
7231
  if (this.isDryRun) {
7903
- logger4.info(`[DRY RUN] Would post tweet: ${content.text}`);
7232
+ logger5.info(`[DRY RUN] Would post tweet: ${content.text}`);
7904
7233
  return [];
7905
7234
  }
7906
7235
  if (content.text.includes("Error: Missing")) {
7907
- logger4.error("Error: Missing some context", content);
7236
+ logger5.error("Error: Missing some context", content);
7908
7237
  return [];
7909
7238
  }
7239
+ logger5.info(`Posting tweet: ${content.text}`);
7910
7240
  const result = await this.postToTwitter(
7911
7241
  content.text,
7912
7242
  content.mediaData
7913
7243
  );
7914
7244
  if (result === null) {
7915
- logger4.info("Skipped posting duplicate tweet");
7245
+ logger5.info("Skipped posting duplicate tweet");
7916
7246
  return [];
7917
7247
  }
7918
- const tweetId = result.rest_id || result.id_str || result.legacy?.id_str;
7248
+ const tweetId = result.id;
7249
+ logger5.info(`Tweet posted successfully! ID: ${tweetId}`);
7919
7250
  if (result) {
7920
7251
  const postedTweetId = createUniqueUuid4(this.runtime, tweetId);
7921
7252
  const postedMemory = {
@@ -7940,10 +7271,11 @@ var TwitterPostClient = class {
7940
7271
  }
7941
7272
  return [];
7942
7273
  } catch (error) {
7943
- logger4.error("Error posting tweet:", error, content);
7274
+ logger5.error("Error posting tweet:", error, content);
7944
7275
  return [];
7945
7276
  }
7946
7277
  };
7278
+ logger5.info("Emitting POST_GENERATED event to trigger content generation...");
7947
7279
  this.runtime.emitEvent(
7948
7280
  [EventType2.POST_GENERATED, "TWITTER_POST_GENERATED" /* POST_GENERATED */],
7949
7281
  {
@@ -7955,8 +7287,9 @@ var TwitterPostClient = class {
7955
7287
  source: "twitter"
7956
7288
  }
7957
7289
  );
7290
+ logger5.info("POST_GENERATED event emitted successfully");
7958
7291
  } catch (error) {
7959
- logger4.error("Error generating tweet:", error);
7292
+ logger5.error("Error generating tweet:", error);
7960
7293
  }
7961
7294
  }
7962
7295
  /**
@@ -7973,7 +7306,7 @@ var TwitterPostClient = class {
7973
7306
  if (lastPost) {
7974
7307
  const lastTweet = await this.client.getTweet(lastPost.id);
7975
7308
  if (lastTweet && lastTweet.text === text) {
7976
- logger4.warn(
7309
+ logger5.warn(
7977
7310
  "Tweet is a duplicate of the last post. Skipping to avoid duplicate."
7978
7311
  );
7979
7312
  return null;
@@ -7983,22 +7316,22 @@ var TwitterPostClient = class {
7983
7316
  if (mediaData && mediaData.length > 0) {
7984
7317
  for (const media of mediaData) {
7985
7318
  try {
7986
- logger4.warn(
7319
+ logger5.warn(
7987
7320
  "Media upload not currently supported with the modern Twitter API"
7988
7321
  );
7989
7322
  } catch (error) {
7990
- logger4.error("Error uploading media:", error);
7323
+ logger5.error("Error uploading media:", error);
7991
7324
  }
7992
7325
  }
7993
7326
  }
7994
7327
  const result = await sendTweet(this.client, text, mediaData);
7995
7328
  if (!result) {
7996
- logger4.error("Error sending tweet; Bad response:");
7329
+ logger5.error("Error sending tweet; Bad response:");
7997
7330
  return null;
7998
7331
  }
7999
7332
  return result;
8000
7333
  } catch (error) {
8001
- logger4.error("Error posting to Twitter:", error);
7334
+ logger5.error("Error posting to Twitter:", error);
8002
7335
  throw error;
8003
7336
  }
8004
7337
  }
@@ -8014,7 +7347,7 @@ import {
8014
7347
  ModelType,
8015
7348
  parseKeyValueXml
8016
7349
  } from "@elizaos/core";
8017
- import { logger as logger5 } from "@elizaos/core";
7350
+ import { logger as logger6 } from "@elizaos/core";
8018
7351
 
8019
7352
  // src/templates.ts
8020
7353
  var twitterActionTemplate = `
@@ -8099,30 +7432,7 @@ var TwitterTimelineClient = class {
8099
7432
  async getTimeline(count) {
8100
7433
  const twitterUsername = this.client.profile?.username;
8101
7434
  const homeTimeline = this.timelineType === "following" /* Following */ ? await this.twitterClient.fetchFollowingTimeline(count, []) : await this.twitterClient.fetchHomeTimeline(count, []);
8102
- return homeTimeline.map((tweet) => ({
8103
- id: tweet.rest_id,
8104
- name: tweet.core?.user_results?.result?.legacy?.name,
8105
- username: tweet.core?.user_results?.result?.legacy?.screen_name,
8106
- text: tweet.legacy?.full_text,
8107
- inReplyToStatusId: tweet.legacy?.in_reply_to_status_id_str,
8108
- timestamp: new Date(tweet.legacy?.created_at).getTime() / 1e3,
8109
- userId: tweet.legacy?.user_id_str,
8110
- conversationId: tweet.legacy?.conversation_id_str,
8111
- permanentUrl: `https://twitter.com/${tweet.core?.user_results?.result?.legacy?.screen_name}/status/${tweet.rest_id}`,
8112
- hashtags: tweet.legacy?.entities?.hashtags || [],
8113
- mentions: tweet.legacy?.entities?.user_mentions || [],
8114
- photos: tweet.legacy?.entities?.media?.filter((media) => media.type === "photo").map((media) => ({
8115
- id: media.id_str,
8116
- url: media.media_url_https,
8117
- // Store media_url_https as url
8118
- alt_text: media.alt_text
8119
- })) || [],
8120
- thread: tweet.thread || [],
8121
- urls: tweet.legacy?.entities?.urls || [],
8122
- videos: tweet.legacy?.entities?.media?.filter(
8123
- (media) => media.type === "video"
8124
- ) || []
8125
- })).filter((tweet) => tweet.username !== twitterUsername);
7435
+ return homeTimeline.filter((tweet) => tweet.username !== twitterUsername);
8126
7436
  }
8127
7437
  createTweetId(runtime, tweet) {
8128
7438
  return createUniqueUuid5(runtime, tweet.id);
@@ -8178,7 +7488,7 @@ Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appro
8178
7488
  }
8179
7489
  );
8180
7490
  if (!actionResponse) {
8181
- logger5.log(`No valid actions generated for tweet ${tweet.id}`);
7491
+ logger6.log(`No valid actions generated for tweet ${tweet.id}`);
8182
7492
  continue;
8183
7493
  }
8184
7494
  const { actions } = parseActionResponseFromText(actionResponse.trim());
@@ -8189,7 +7499,7 @@ Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appro
8189
7499
  roomId
8190
7500
  });
8191
7501
  } catch (error) {
8192
- logger5.error(`Error processing tweet ${tweet.id}:`, error);
7502
+ logger6.error(`Error processing tweet ${tweet.id}:`, error);
8193
7503
  continue;
8194
7504
  }
8195
7505
  }
@@ -8236,7 +7546,7 @@ Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appro
8236
7546
  this.handleReplyAction(tweet);
8237
7547
  }
8238
7548
  } catch (error) {
8239
- logger5.error(`Error processing tweet ${tweet.id}:`, error);
7549
+ logger6.error(`Error processing tweet ${tweet.id}:`, error);
8240
7550
  continue;
8241
7551
  }
8242
7552
  }
@@ -8267,17 +7577,17 @@ Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appro
8267
7577
  async handleLikeAction(tweet) {
8268
7578
  try {
8269
7579
  await this.twitterClient.likeTweet(tweet.id);
8270
- logger5.log(`Liked tweet ${tweet.id}`);
7580
+ logger6.log(`Liked tweet ${tweet.id}`);
8271
7581
  } catch (error) {
8272
- logger5.error(`Error liking tweet ${tweet.id}:`, error);
7582
+ logger6.error(`Error liking tweet ${tweet.id}:`, error);
8273
7583
  }
8274
7584
  }
8275
7585
  async handleRetweetAction(tweet) {
8276
7586
  try {
8277
7587
  await this.twitterClient.retweet(tweet.id);
8278
- logger5.log(`Retweeted tweet ${tweet.id}`);
7588
+ logger6.log(`Retweeted tweet ${tweet.id}`);
8279
7589
  } catch (error) {
8280
- logger5.error(`Error retweeting tweet ${tweet.id}:`, error);
7590
+ logger6.error(`Error retweeting tweet ${tweet.id}:`, error);
8281
7591
  }
8282
7592
  }
8283
7593
  async handleQuoteAction(tweet) {
@@ -8304,11 +7614,11 @@ ${tweet.text}`;
8304
7614
  const body = await result.json();
8305
7615
  const tweetResult = body?.data?.create_tweet?.tweet_results?.result || body?.data || body;
8306
7616
  if (tweetResult) {
8307
- logger5.log("Successfully posted quote tweet");
7617
+ logger6.log("Successfully posted quote tweet");
8308
7618
  } else {
8309
- logger5.error("Quote tweet creation failed:", body);
7619
+ logger6.error("Quote tweet creation failed:", body);
8310
7620
  }
8311
- const tweetId = tweetResult?.rest_id || tweetResult?.id || Date.now().toString();
7621
+ const tweetId = tweetResult?.id || Date.now().toString();
8312
7622
  const responseId = createUniqueUuid5(this.runtime, tweetId);
8313
7623
  const responseMemory = {
8314
7624
  id: responseId,
@@ -8324,7 +7634,7 @@ ${tweet.text}`;
8324
7634
  await this.runtime.createMemory(responseMemory, "messages");
8325
7635
  }
8326
7636
  } catch (error) {
8327
- logger5.error("Error in quote tweet generation:", error);
7637
+ logger6.error("Error in quote tweet generation:", error);
8328
7638
  }
8329
7639
  }
8330
7640
  async handleReplyAction(tweet) {
@@ -8351,7 +7661,7 @@ ${tweet.text}`;
8351
7661
  if (!tweetResult) {
8352
7662
  throw new Error("Failed to get tweet result from response");
8353
7663
  }
8354
- const responseId = createUniqueUuid5(this.runtime, tweetResult.rest_id);
7664
+ const responseId = createUniqueUuid5(this.runtime, tweetResult.id);
8355
7665
  const responseMemory = {
8356
7666
  id: responseId,
8357
7667
  entityId: this.runtime.agentId,
@@ -8366,7 +7676,7 @@ ${tweet.text}`;
8366
7676
  await this.runtime.createMemory(responseMemory, "messages");
8367
7677
  }
8368
7678
  } catch (error) {
8369
- logger5.error("Error in quote tweet generation:", error);
7679
+ logger6.error("Error in quote tweet generation:", error);
8370
7680
  }
8371
7681
  }
8372
7682
  };
@@ -8479,18 +7789,35 @@ console.log(`Twitter plugin loaded with service name: ${TWITTER_SERVICE_NAME}`);
8479
7789
  var TwitterClientInstance = class {
8480
7790
  constructor(runtime, state) {
8481
7791
  this.client = new ClientBase(runtime, state);
8482
- if (runtime.getSetting("TWITTER_POST_ENABLE") === "true") {
7792
+ const postEnableSetting = runtime.getSetting("TWITTER_POST_ENABLE");
7793
+ logger7.info(`TWITTER_POST_ENABLE raw value: "${postEnableSetting}"`);
7794
+ logger7.info(`TWITTER_POST_ENABLE type: ${typeof postEnableSetting}`);
7795
+ const postEnabled = postEnableSetting === true || postEnableSetting === "true" || typeof postEnableSetting === "string" && postEnableSetting.toLowerCase() === "true";
7796
+ if (postEnabled) {
7797
+ logger7.info("Twitter posting is ENABLED - creating post client");
8483
7798
  this.post = new TwitterPostClient(this.client, runtime, state);
7799
+ } else {
7800
+ logger7.info("Twitter posting is DISABLED - set TWITTER_POST_ENABLE=true to enable automatic posting");
8484
7801
  }
8485
- if (runtime.getSetting("TWITTER_SEARCH_ENABLE") !== "false") {
7802
+ const searchEnabledSetting = runtime.getSetting("TWITTER_SEARCH_ENABLE");
7803
+ logger7.info(`TWITTER_SEARCH_ENABLE raw value: "${searchEnabledSetting}"`);
7804
+ const searchEnabled = searchEnabledSetting !== false && searchEnabledSetting !== "false";
7805
+ if (searchEnabled) {
7806
+ logger7.info("Twitter search/interactions are ENABLED");
8486
7807
  this.interaction = new TwitterInteractionClient(
8487
7808
  this.client,
8488
7809
  runtime,
8489
7810
  state
8490
7811
  );
7812
+ } else {
7813
+ logger7.info("Twitter search/interactions are DISABLED");
8491
7814
  }
8492
- if (runtime.getSetting("TWITTER_ENABLE_ACTION_PROCESSING") === "true") {
7815
+ const actionProcessingEnabled = runtime.getSetting("TWITTER_ENABLE_ACTION_PROCESSING") === "true";
7816
+ if (actionProcessingEnabled) {
7817
+ logger7.info("Twitter action processing is ENABLED");
8493
7818
  this.timeline = new TwitterTimelineClient(this.client, runtime, state);
7819
+ } else {
7820
+ logger7.info("Twitter action processing is DISABLED");
8494
7821
  }
8495
7822
  this.service = TwitterService.getInstance();
8496
7823
  }
@@ -8511,7 +7838,7 @@ var _TwitterService = class _TwitterService extends Service {
8511
7838
  try {
8512
7839
  const existingClient = this.getClient(clientId, runtime.agentId);
8513
7840
  if (existingClient) {
8514
- logger6.info(`Twitter client already exists for ${clientId}`);
7841
+ logger7.info(`Twitter client already exists for ${clientId}`);
8515
7842
  return existingClient;
8516
7843
  }
8517
7844
  const client = new TwitterClientInstance(runtime, state);
@@ -8527,10 +7854,10 @@ var _TwitterService = class _TwitterService extends Service {
8527
7854
  }
8528
7855
  this.clients.set(this.getClientKey(clientId, runtime.agentId), client);
8529
7856
  await this.emitServerJoinedEvent(runtime, client);
8530
- logger6.info(`Created Twitter client for ${clientId}`);
7857
+ logger7.info(`Created Twitter client for ${clientId}`);
8531
7858
  return client;
8532
7859
  } catch (error) {
8533
- logger6.error(`Failed to create Twitter client for ${clientId}:`, error);
7860
+ logger7.error(`Failed to create Twitter client for ${clientId}:`, error);
8534
7861
  throw error;
8535
7862
  }
8536
7863
  }
@@ -8542,7 +7869,7 @@ var _TwitterService = class _TwitterService extends Service {
8542
7869
  async emitServerJoinedEvent(runtime, client) {
8543
7870
  try {
8544
7871
  if (!client.client.profile) {
8545
- logger6.warn(
7872
+ logger7.warn(
8546
7873
  "Twitter profile not available yet, can't emit WORLD_JOINED event"
8547
7874
  );
8548
7875
  return;
@@ -8617,9 +7944,9 @@ var _TwitterService = class _TwitterService extends Service {
8617
7944
  source: "twitter"
8618
7945
  }
8619
7946
  );
8620
- logger6.info(`Emitted WORLD_JOINED event for Twitter account ${username}`);
7947
+ logger7.info(`Emitted WORLD_JOINED event for Twitter account ${username}`);
8621
7948
  } catch (error) {
8622
- logger6.error("Failed to emit WORLD_JOINED event for Twitter:", error);
7949
+ logger7.error("Failed to emit WORLD_JOINED event for Twitter:", error);
8623
7950
  }
8624
7951
  }
8625
7952
  getClient(clientId, agentId) {
@@ -8632,9 +7959,9 @@ var _TwitterService = class _TwitterService extends Service {
8632
7959
  try {
8633
7960
  await client.service.stop();
8634
7961
  this.clients.delete(key);
8635
- logger6.info(`Stopped Twitter client for ${clientId}`);
7962
+ logger7.info(`Stopped Twitter client for ${clientId}`);
8636
7963
  } catch (error) {
8637
- logger6.error(`Error stopping Twitter client for ${clientId}:`, error);
7964
+ logger7.error(`Error stopping Twitter client for ${clientId}:`, error);
8638
7965
  }
8639
7966
  }
8640
7967
  }
@@ -8651,7 +7978,7 @@ var _TwitterService = class _TwitterService extends Service {
8651
7978
  );
8652
7979
  try {
8653
7980
  if (config.TWITTER_API_KEY && config.TWITTER_API_SECRET_KEY && config.TWITTER_ACCESS_TOKEN && config.TWITTER_ACCESS_TOKEN_SECRET) {
8654
- logger6.info("Creating default Twitter client from character settings");
7981
+ logger7.info("Creating default Twitter client from character settings");
8655
7982
  await twitterClientManager.createClient(
8656
7983
  runtime,
8657
7984
  runtime.agentId,
@@ -8659,7 +7986,7 @@ var _TwitterService = class _TwitterService extends Service {
8659
7986
  );
8660
7987
  }
8661
7988
  } catch (error) {
8662
- logger6.error("Failed to create default Twitter client:", error);
7989
+ logger7.error("Failed to create default Twitter client:", error);
8663
7990
  throw error;
8664
7991
  }
8665
7992
  return twitterClientManager;
@@ -8673,7 +8000,7 @@ var _TwitterService = class _TwitterService extends Service {
8673
8000
  await client.service.stop();
8674
8001
  this.clients.delete(key);
8675
8002
  } catch (error) {
8676
- logger6.error(`Error stopping Twitter client ${key}:`, error);
8003
+ logger7.error(`Error stopping Twitter client ${key}:`, error);
8677
8004
  }
8678
8005
  }
8679
8006
  }