@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/README.md +277 -133
- package/dist/index.d.ts +102 -169
- package/dist/index.js +588 -1261
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
Role,
|
|
12
12
|
Service,
|
|
13
13
|
createUniqueUuid as createUniqueUuid6,
|
|
14
|
-
logger as
|
|
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
|
|
131
|
+
function parseV2Profile(user) {
|
|
281
132
|
const profile = {
|
|
282
|
-
avatar: getAvatarOriginalSizeUrl(user.
|
|
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.
|
|
287
|
-
friendsCount: user.
|
|
288
|
-
|
|
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.
|
|
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.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
304
|
-
profile.joined = new Date(
|
|
151
|
+
if (user.created_at) {
|
|
152
|
+
profile.joined = new Date(user.created_at);
|
|
305
153
|
}
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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("
|
|
163
|
+
err: new Error("Not authenticated")
|
|
360
164
|
};
|
|
361
165
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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:
|
|
367
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
208
|
+
err: new Error("Not authenticated")
|
|
420
209
|
};
|
|
421
210
|
}
|
|
422
|
-
|
|
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:
|
|
425
|
-
|
|
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
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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*
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
624
|
-
if (!auth
|
|
625
|
-
throw new Error("
|
|
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
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1446
|
-
|
|
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 ??
|
|
1453
|
-
hashtags: tweetV2.entities?.hashtags?.map((tag) => tag.tag) ??
|
|
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
|
-
})) ??
|
|
1458
|
-
urls: tweetV2.entities?.urls?.map((url) => url.url) ??
|
|
1459
|
-
likes: tweetV2.public_metrics?.like_count ??
|
|
1460
|
-
retweets: tweetV2.public_metrics?.retweet_count ??
|
|
1461
|
-
replies: tweetV2.public_metrics?.reply_count ??
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
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
|
|
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
|
|
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 ??
|
|
1511
|
-
parsedTweet.name = user.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 ??
|
|
1522
|
-
country: place.country ??
|
|
1523
|
-
country_code: place.country_code ??
|
|
1524
|
-
name: place.name ??
|
|
1525
|
-
place_type: 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
|
|
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
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
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
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1439
|
+
return this.fetchHomeTimeline(count, seenTweetIds);
|
|
2005
1440
|
}
|
|
2006
1441
|
async getUserTweets(userId, maxTweets = 200, cursor) {
|
|
2007
|
-
if (
|
|
2008
|
-
|
|
2009
|
-
}
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2507
|
+
logger3.log("Successfully posted Tweet");
|
|
3187
2508
|
} catch (error) {
|
|
3188
|
-
|
|
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
|
-
|
|
2521
|
+
logger3.log("Successfully posted a tweet", tweetResult.id);
|
|
3201
2522
|
return tweetResult;
|
|
3202
2523
|
}
|
|
3203
2524
|
} catch (error) {
|
|
3204
|
-
|
|
2525
|
+
logger3.error("Error parsing tweet response:", error);
|
|
3205
2526
|
throw error;
|
|
3206
2527
|
}
|
|
3207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6713
|
+
logger4.log("Finished checking Twitter interactions");
|
|
7393
6714
|
} catch (error) {
|
|
7394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6772
|
+
logger4.log(
|
|
7452
6773
|
`Already replied to tweet ${tweet.id} (found existing reply), skipping`
|
|
7453
6774
|
);
|
|
7454
6775
|
continue;
|
|
7455
6776
|
}
|
|
7456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7003
|
+
logger4.info(
|
|
7683
7004
|
`[DRY RUN] Would have replied to ${tweet.username} with: ${response.text}`
|
|
7684
7005
|
);
|
|
7685
7006
|
return [];
|
|
7686
7007
|
}
|
|
7687
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7063
|
+
logger4.log("No current tweet found for thread building");
|
|
7743
7064
|
return;
|
|
7744
7065
|
}
|
|
7745
7066
|
if (depth >= maxReplies) {
|
|
7746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7123
|
+
logger4.log(
|
|
7803
7124
|
"No parent tweet found for:",
|
|
7804
7125
|
currentTweet.inReplyToStatusId
|
|
7805
7126
|
);
|
|
7806
7127
|
}
|
|
7807
7128
|
} catch (error) {
|
|
7808
|
-
|
|
7129
|
+
logger4.log("Error fetching parent tweet:", {
|
|
7809
7130
|
tweetId: currentTweet.inReplyToStatusId,
|
|
7810
7131
|
error
|
|
7811
7132
|
});
|
|
7812
7133
|
}
|
|
7813
7134
|
} else {
|
|
7814
|
-
|
|
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
|
|
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
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
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
|
-
|
|
7860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7232
|
+
logger5.info(`[DRY RUN] Would post tweet: ${content.text}`);
|
|
7904
7233
|
return [];
|
|
7905
7234
|
}
|
|
7906
7235
|
if (content.text.includes("Error: Missing")) {
|
|
7907
|
-
|
|
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
|
-
|
|
7245
|
+
logger5.info("Skipped posting duplicate tweet");
|
|
7916
7246
|
return [];
|
|
7917
7247
|
}
|
|
7918
|
-
const tweetId = result.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7319
|
+
logger5.warn(
|
|
7987
7320
|
"Media upload not currently supported with the modern Twitter API"
|
|
7988
7321
|
);
|
|
7989
7322
|
} catch (error) {
|
|
7990
|
-
|
|
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
|
-
|
|
7329
|
+
logger5.error("Error sending tweet; Bad response:");
|
|
7997
7330
|
return null;
|
|
7998
7331
|
}
|
|
7999
7332
|
return result;
|
|
8000
7333
|
} catch (error) {
|
|
8001
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7580
|
+
logger6.log(`Liked tweet ${tweet.id}`);
|
|
8271
7581
|
} catch (error) {
|
|
8272
|
-
|
|
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
|
-
|
|
7588
|
+
logger6.log(`Retweeted tweet ${tweet.id}`);
|
|
8279
7589
|
} catch (error) {
|
|
8280
|
-
|
|
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
|
-
|
|
7617
|
+
logger6.log("Successfully posted quote tweet");
|
|
8308
7618
|
} else {
|
|
8309
|
-
|
|
7619
|
+
logger6.error("Quote tweet creation failed:", body);
|
|
8310
7620
|
}
|
|
8311
|
-
const tweetId = tweetResult?.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7857
|
+
logger7.info(`Created Twitter client for ${clientId}`);
|
|
8531
7858
|
return client;
|
|
8532
7859
|
} catch (error) {
|
|
8533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7947
|
+
logger7.info(`Emitted WORLD_JOINED event for Twitter account ${username}`);
|
|
8621
7948
|
} catch (error) {
|
|
8622
|
-
|
|
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
|
-
|
|
7962
|
+
logger7.info(`Stopped Twitter client for ${clientId}`);
|
|
8636
7963
|
} catch (error) {
|
|
8637
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8003
|
+
logger7.error(`Error stopping Twitter client ${key}:`, error);
|
|
8677
8004
|
}
|
|
8678
8005
|
}
|
|
8679
8006
|
}
|