@elizaos/plugin-twitter 1.0.13 → 1.0.14
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 +585 -1260
- 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
|
|
@@ -795,449 +598,6 @@ async function* searchQuotedTweets(quotedTweetId, maxTweets, auth) {
|
|
|
795
598
|
const query = `url:"twitter.com/*/status/${quotedTweetId}"`;
|
|
796
599
|
yield* searchTweets(query, maxTweets, 1 /* Latest */, auth);
|
|
797
600
|
}
|
|
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
601
|
|
|
1242
602
|
// src/client/tweets.ts
|
|
1243
603
|
var defaultOptions = {
|
|
@@ -1318,13 +678,6 @@ var defaultOptions = {
|
|
|
1318
678
|
"place_type"
|
|
1319
679
|
]
|
|
1320
680
|
};
|
|
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
681
|
async function fetchTweets(userId, maxTweets, cursor, auth) {
|
|
1329
682
|
const client = auth.getV2Client();
|
|
1330
683
|
try {
|
|
@@ -1360,7 +713,7 @@ async function fetchTweets(userId, maxTweets, cursor, auth) {
|
|
|
1360
713
|
next: response.meta.next_token
|
|
1361
714
|
};
|
|
1362
715
|
} catch (error) {
|
|
1363
|
-
throw new Error(`Failed to fetch tweets: ${error
|
|
716
|
+
throw new Error(`Failed to fetch tweets: ${error?.message || error}`);
|
|
1364
717
|
}
|
|
1365
718
|
}
|
|
1366
719
|
async function fetchTweetsAndReplies(userId, maxTweets, cursor, auth) {
|
|
@@ -1397,7 +750,7 @@ async function fetchTweetsAndReplies(userId, maxTweets, cursor, auth) {
|
|
|
1397
750
|
next: response.meta.next_token
|
|
1398
751
|
};
|
|
1399
752
|
} catch (error) {
|
|
1400
|
-
throw new Error(`Failed to fetch tweets and replies: ${error
|
|
753
|
+
throw new Error(`Failed to fetch tweets and replies: ${error?.message || error}`);
|
|
1401
754
|
}
|
|
1402
755
|
}
|
|
1403
756
|
async function createCreateTweetRequestV2(text, auth, tweetId, options) {
|
|
@@ -1442,45 +795,50 @@ async function createCreateTweetRequestV2(text, auth, tweetId, options) {
|
|
|
1442
795
|
}
|
|
1443
796
|
return await getTweetV2(tweetResponse.data.id, auth, optionsConfig);
|
|
1444
797
|
}
|
|
1445
|
-
function parseTweetV2ToV1(tweetV2, includes
|
|
1446
|
-
|
|
1447
|
-
if (defaultTweetData != null) {
|
|
1448
|
-
parsedTweet = defaultTweetData;
|
|
1449
|
-
}
|
|
1450
|
-
parsedTweet = {
|
|
798
|
+
function parseTweetV2ToV1(tweetV2, includes) {
|
|
799
|
+
const parsedTweet = {
|
|
1451
800
|
id: tweetV2.id,
|
|
1452
|
-
text: tweetV2.text ??
|
|
1453
|
-
hashtags: tweetV2.entities?.hashtags?.map((tag) => tag.tag) ??
|
|
801
|
+
text: tweetV2.text ?? "",
|
|
802
|
+
hashtags: tweetV2.entities?.hashtags?.map((tag) => tag.tag) ?? [],
|
|
1454
803
|
mentions: tweetV2.entities?.mentions?.map((mention) => ({
|
|
1455
804
|
id: mention.id,
|
|
1456
805
|
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:
|
|
806
|
+
})) ?? [],
|
|
807
|
+
urls: tweetV2.entities?.urls?.map((url) => url.url) ?? [],
|
|
808
|
+
likes: tweetV2.public_metrics?.like_count ?? 0,
|
|
809
|
+
retweets: tweetV2.public_metrics?.retweet_count ?? 0,
|
|
810
|
+
replies: tweetV2.public_metrics?.reply_count ?? 0,
|
|
811
|
+
quotes: tweetV2.public_metrics?.quote_count ?? 0,
|
|
812
|
+
views: tweetV2.public_metrics?.impression_count ?? 0,
|
|
813
|
+
userId: tweetV2.author_id,
|
|
814
|
+
conversationId: tweetV2.conversation_id,
|
|
815
|
+
photos: [],
|
|
816
|
+
videos: [],
|
|
817
|
+
poll: null,
|
|
818
|
+
username: "",
|
|
819
|
+
name: "",
|
|
820
|
+
thread: [],
|
|
821
|
+
timestamp: tweetV2.created_at ? new Date(tweetV2.created_at).getTime() / 1e3 : Date.now() / 1e3,
|
|
822
|
+
permanentUrl: `https://twitter.com/i/status/${tweetV2.id}`,
|
|
823
|
+
// Check for referenced tweets
|
|
824
|
+
isReply: tweetV2.referenced_tweets?.some((ref) => ref.type === "replied_to") ?? false,
|
|
825
|
+
isRetweet: tweetV2.referenced_tweets?.some((ref) => ref.type === "retweeted") ?? false,
|
|
826
|
+
isQuoted: tweetV2.referenced_tweets?.some((ref) => ref.type === "quoted") ?? false,
|
|
827
|
+
inReplyToStatusId: tweetV2.referenced_tweets?.find((ref) => ref.type === "replied_to")?.id,
|
|
828
|
+
quotedStatusId: tweetV2.referenced_tweets?.find((ref) => ref.type === "quoted")?.id,
|
|
829
|
+
retweetedStatusId: tweetV2.referenced_tweets?.find((ref) => ref.type === "retweeted")?.id
|
|
1472
830
|
};
|
|
1473
831
|
if (includes?.polls?.length) {
|
|
1474
832
|
const poll = includes.polls[0];
|
|
1475
833
|
parsedTweet.poll = {
|
|
1476
834
|
id: poll.id,
|
|
1477
|
-
end_datetime: poll.end_datetime
|
|
835
|
+
end_datetime: poll.end_datetime,
|
|
1478
836
|
options: poll.options.map((option) => ({
|
|
1479
837
|
position: option.position,
|
|
1480
838
|
label: option.label,
|
|
1481
839
|
votes: option.votes
|
|
1482
840
|
})),
|
|
1483
|
-
voting_status: poll.voting_status
|
|
841
|
+
voting_status: poll.voting_status
|
|
1484
842
|
};
|
|
1485
843
|
}
|
|
1486
844
|
if (includes?.media?.length) {
|
|
@@ -1507,8 +865,8 @@ function parseTweetV2ToV1(tweetV2, includes, defaultTweetData) {
|
|
|
1507
865
|
(user2) => user2.id === tweetV2.author_id
|
|
1508
866
|
);
|
|
1509
867
|
if (user) {
|
|
1510
|
-
parsedTweet.username = user.username ??
|
|
1511
|
-
parsedTweet.name = user.name ??
|
|
868
|
+
parsedTweet.username = user.username ?? "";
|
|
869
|
+
parsedTweet.name = user.name ?? "";
|
|
1512
870
|
}
|
|
1513
871
|
}
|
|
1514
872
|
if (tweetV2?.geo?.place_id && includes?.places?.length) {
|
|
@@ -1518,11 +876,11 @@ function parseTweetV2ToV1(tweetV2, includes, defaultTweetData) {
|
|
|
1518
876
|
if (place) {
|
|
1519
877
|
parsedTweet.place = {
|
|
1520
878
|
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
|
|
879
|
+
full_name: place.full_name ?? "",
|
|
880
|
+
country: place.country ?? "",
|
|
881
|
+
country_code: place.country_code ?? "",
|
|
882
|
+
name: place.name ?? "",
|
|
883
|
+
place_type: place.place_type
|
|
1526
884
|
};
|
|
1527
885
|
}
|
|
1528
886
|
}
|
|
@@ -1552,7 +910,7 @@ async function createCreateTweetRequest(text, auth, tweetId, mediaData, hideLink
|
|
|
1552
910
|
data: result
|
|
1553
911
|
};
|
|
1554
912
|
} catch (error) {
|
|
1555
|
-
throw new Error(`Failed to create tweet: ${error
|
|
913
|
+
throw new Error(`Failed to create tweet: ${error?.message || error}`);
|
|
1556
914
|
}
|
|
1557
915
|
}
|
|
1558
916
|
async function createCreateNoteTweetRequest(text, auth, tweetId, mediaData) {
|
|
@@ -1611,35 +969,71 @@ async function deleteTweet(tweetId, auth) {
|
|
|
1611
969
|
throw new Error(`Failed to delete tweet: ${error.message}`);
|
|
1612
970
|
}
|
|
1613
971
|
}
|
|
1614
|
-
function getTweets(user, maxTweets, auth) {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
972
|
+
async function* getTweets(user, maxTweets, auth) {
|
|
973
|
+
const userIdRes = await getEntityIdByScreenName(user, auth);
|
|
974
|
+
if (!userIdRes.success) {
|
|
975
|
+
throw userIdRes.err;
|
|
976
|
+
}
|
|
977
|
+
const { value: userId } = userIdRes;
|
|
978
|
+
let cursor;
|
|
979
|
+
let totalFetched = 0;
|
|
980
|
+
while (totalFetched < maxTweets) {
|
|
981
|
+
const response = await fetchTweets(userId, maxTweets - totalFetched, cursor, auth);
|
|
982
|
+
for (const tweet of response.tweets) {
|
|
983
|
+
yield tweet;
|
|
984
|
+
totalFetched++;
|
|
985
|
+
if (totalFetched >= maxTweets) break;
|
|
986
|
+
}
|
|
987
|
+
cursor = response.next;
|
|
988
|
+
if (!cursor) break;
|
|
989
|
+
}
|
|
1623
990
|
}
|
|
1624
|
-
function getTweetsByUserId(userId, maxTweets, auth) {
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
991
|
+
async function* getTweetsByUserId(userId, maxTweets, auth) {
|
|
992
|
+
let cursor;
|
|
993
|
+
let totalFetched = 0;
|
|
994
|
+
while (totalFetched < maxTweets) {
|
|
995
|
+
const response = await fetchTweets(userId, maxTweets - totalFetched, cursor, auth);
|
|
996
|
+
for (const tweet of response.tweets) {
|
|
997
|
+
yield tweet;
|
|
998
|
+
totalFetched++;
|
|
999
|
+
if (totalFetched >= maxTweets) break;
|
|
1000
|
+
}
|
|
1001
|
+
cursor = response.next;
|
|
1002
|
+
if (!cursor) break;
|
|
1003
|
+
}
|
|
1628
1004
|
}
|
|
1629
|
-
function getTweetsAndReplies(user, maxTweets, auth) {
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1005
|
+
async function* getTweetsAndReplies(user, maxTweets, auth) {
|
|
1006
|
+
const userIdRes = await getEntityIdByScreenName(user, auth);
|
|
1007
|
+
if (!userIdRes.success) {
|
|
1008
|
+
throw userIdRes.err;
|
|
1009
|
+
}
|
|
1010
|
+
const { value: userId } = userIdRes;
|
|
1011
|
+
let cursor;
|
|
1012
|
+
let totalFetched = 0;
|
|
1013
|
+
while (totalFetched < maxTweets) {
|
|
1014
|
+
const response = await fetchTweetsAndReplies(userId, maxTweets - totalFetched, cursor, auth);
|
|
1015
|
+
for (const tweet of response.tweets) {
|
|
1016
|
+
yield tweet;
|
|
1017
|
+
totalFetched++;
|
|
1018
|
+
if (totalFetched >= maxTweets) break;
|
|
1019
|
+
}
|
|
1020
|
+
cursor = response.next;
|
|
1021
|
+
if (!cursor) break;
|
|
1022
|
+
}
|
|
1638
1023
|
}
|
|
1639
|
-
function getTweetsAndRepliesByUserId(userId, maxTweets, auth) {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1024
|
+
async function* getTweetsAndRepliesByUserId(userId, maxTweets, auth) {
|
|
1025
|
+
let cursor;
|
|
1026
|
+
let totalFetched = 0;
|
|
1027
|
+
while (totalFetched < maxTweets) {
|
|
1028
|
+
const response = await fetchTweetsAndReplies(userId, maxTweets - totalFetched, cursor, auth);
|
|
1029
|
+
for (const tweet of response.tweets) {
|
|
1030
|
+
yield tweet;
|
|
1031
|
+
totalFetched++;
|
|
1032
|
+
if (totalFetched >= maxTweets) break;
|
|
1033
|
+
}
|
|
1034
|
+
cursor = response.next;
|
|
1035
|
+
if (!cursor) break;
|
|
1036
|
+
}
|
|
1643
1037
|
}
|
|
1644
1038
|
async function getTweetWhere(tweets, query) {
|
|
1645
1039
|
const isCallback = typeof query === "function";
|
|
@@ -1723,11 +1117,9 @@ async function getTweetV2(id, auth, options = defaultOptions) {
|
|
|
1723
1117
|
console.warn(`Tweet data not found for ID: ${id}`);
|
|
1724
1118
|
return null;
|
|
1725
1119
|
}
|
|
1726
|
-
const defaultTweetData = await getTweet(tweetData.data.id, auth);
|
|
1727
1120
|
const parsedTweet = parseTweetV2ToV1(
|
|
1728
1121
|
tweetData.data,
|
|
1729
|
-
tweetData?.includes
|
|
1730
|
-
defaultTweetData
|
|
1122
|
+
tweetData?.includes
|
|
1731
1123
|
);
|
|
1732
1124
|
return parsedTweet;
|
|
1733
1125
|
} catch (error) {
|
|
@@ -1817,19 +1209,6 @@ async function retweet(tweetId, auth) {
|
|
|
1817
1209
|
async function createCreateLongTweetRequest(text, auth, tweetId, mediaData) {
|
|
1818
1210
|
return createCreateTweetRequest(text, auth, tweetId, mediaData);
|
|
1819
1211
|
}
|
|
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
1212
|
async function fetchRetweetersPage(tweetId, auth, cursor, count = 40) {
|
|
1834
1213
|
console.warn("Fetching retweeters not implemented for Twitter API v2");
|
|
1835
1214
|
return {
|
|
@@ -1859,7 +1238,6 @@ async function getAllRetweeters(tweetId, auth) {
|
|
|
1859
1238
|
}
|
|
1860
1239
|
|
|
1861
1240
|
// src/client/client.ts
|
|
1862
|
-
var UserTweetsUrl = "https://twitter.com/i/api/graphql/E3opETHurmVJflFsUBVuUQ/UserTweets";
|
|
1863
1241
|
var Client = class {
|
|
1864
1242
|
/**
|
|
1865
1243
|
* Creates a new Client object.
|
|
@@ -1924,8 +1302,17 @@ var Client = class {
|
|
|
1924
1302
|
* @param cursor The search cursor, which can be passed into further requests for more results.
|
|
1925
1303
|
* @returns A page of results, containing a cursor that can be used in further requests.
|
|
1926
1304
|
*/
|
|
1927
|
-
fetchSearchTweets(query, maxTweets, searchMode, cursor) {
|
|
1928
|
-
|
|
1305
|
+
async fetchSearchTweets(query, maxTweets, searchMode, cursor) {
|
|
1306
|
+
const tweets = [];
|
|
1307
|
+
const generator = searchTweets(query, maxTweets, searchMode, this.auth);
|
|
1308
|
+
for await (const tweet of generator) {
|
|
1309
|
+
tweets.push(tweet);
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
tweets,
|
|
1313
|
+
// v2 API doesn't provide cursor-based pagination for search
|
|
1314
|
+
next: void 0
|
|
1315
|
+
};
|
|
1929
1316
|
}
|
|
1930
1317
|
/**
|
|
1931
1318
|
* Fetches profiles from Twitter.
|
|
@@ -1934,8 +1321,17 @@ var Client = class {
|
|
|
1934
1321
|
* @param cursor The search cursor, which can be passed into further requests for more results.
|
|
1935
1322
|
* @returns A page of results, containing a cursor that can be used in further requests.
|
|
1936
1323
|
*/
|
|
1937
|
-
fetchSearchProfiles(query, maxProfiles, cursor) {
|
|
1938
|
-
|
|
1324
|
+
async fetchSearchProfiles(query, maxProfiles, cursor) {
|
|
1325
|
+
const profiles = [];
|
|
1326
|
+
const generator = searchProfiles(query, maxProfiles, this.auth);
|
|
1327
|
+
for await (const profile of generator) {
|
|
1328
|
+
profiles.push(profile);
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
profiles,
|
|
1332
|
+
// v2 API doesn't provide cursor-based pagination for search
|
|
1333
|
+
next: void 0
|
|
1334
|
+
};
|
|
1939
1335
|
}
|
|
1940
1336
|
/**
|
|
1941
1337
|
* Fetches list tweets from Twitter.
|
|
@@ -1986,82 +1382,101 @@ var Client = class {
|
|
|
1986
1382
|
return fetchProfileFollowers(userId, maxProfiles, this.auth, cursor);
|
|
1987
1383
|
}
|
|
1988
1384
|
/**
|
|
1989
|
-
* Fetches the home timeline for the current user
|
|
1385
|
+
* Fetches the home timeline for the current user using Twitter API v2.
|
|
1386
|
+
* Note: Twitter API v2 doesn't distinguish between "For You" and "Following" feeds.
|
|
1990
1387
|
* @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
|
|
1388
|
+
* @param seenTweetIds An array of tweet IDs that have already been seen (not used in v2).
|
|
1389
|
+
* @returns A promise that resolves to an array of tweets.
|
|
1993
1390
|
*/
|
|
1994
1391
|
async fetchHomeTimeline(count, seenTweetIds) {
|
|
1995
|
-
|
|
1392
|
+
if (!this.auth) {
|
|
1393
|
+
throw new Error("Not authenticated");
|
|
1394
|
+
}
|
|
1395
|
+
const client = this.auth.getV2Client();
|
|
1396
|
+
try {
|
|
1397
|
+
const timeline = await client.v2.homeTimeline({
|
|
1398
|
+
max_results: Math.min(count, 100),
|
|
1399
|
+
"tweet.fields": [
|
|
1400
|
+
"id",
|
|
1401
|
+
"text",
|
|
1402
|
+
"created_at",
|
|
1403
|
+
"author_id",
|
|
1404
|
+
"referenced_tweets",
|
|
1405
|
+
"entities",
|
|
1406
|
+
"public_metrics",
|
|
1407
|
+
"attachments",
|
|
1408
|
+
"conversation_id"
|
|
1409
|
+
],
|
|
1410
|
+
"user.fields": ["id", "name", "username", "profile_image_url"],
|
|
1411
|
+
"media.fields": ["url", "preview_image_url", "type"],
|
|
1412
|
+
expansions: [
|
|
1413
|
+
"author_id",
|
|
1414
|
+
"attachments.media_keys",
|
|
1415
|
+
"referenced_tweets.id"
|
|
1416
|
+
]
|
|
1417
|
+
});
|
|
1418
|
+
const tweets = [];
|
|
1419
|
+
for await (const tweet of timeline) {
|
|
1420
|
+
tweets.push(parseTweetV2ToV1(tweet, timeline.includes));
|
|
1421
|
+
if (tweets.length >= count) break;
|
|
1422
|
+
}
|
|
1423
|
+
return tweets;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
console.error("Failed to fetch home timeline:", error);
|
|
1426
|
+
throw error;
|
|
1427
|
+
}
|
|
1996
1428
|
}
|
|
1997
1429
|
/**
|
|
1998
|
-
* Fetches the home timeline for the current user
|
|
1430
|
+
* Fetches the home timeline for the current user (same as fetchHomeTimeline in v2).
|
|
1431
|
+
* Twitter API v2 doesn't provide separate "Following" timeline endpoint.
|
|
1999
1432
|
* @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
|
|
1433
|
+
* @param seenTweetIds An array of tweet IDs that have already been seen (not used in v2).
|
|
1434
|
+
* @returns A promise that resolves to an array of tweets.
|
|
2002
1435
|
*/
|
|
2003
1436
|
async fetchFollowingTimeline(count, seenTweetIds) {
|
|
2004
|
-
return
|
|
1437
|
+
return this.fetchHomeTimeline(count, seenTweetIds);
|
|
2005
1438
|
}
|
|
2006
1439
|
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;
|
|
1440
|
+
if (!this.auth) {
|
|
1441
|
+
throw new Error("Not authenticated");
|
|
1442
|
+
}
|
|
1443
|
+
const client = this.auth.getV2Client();
|
|
1444
|
+
try {
|
|
1445
|
+
const response = await client.v2.userTimeline(userId, {
|
|
1446
|
+
max_results: Math.min(maxTweets, 100),
|
|
1447
|
+
"tweet.fields": [
|
|
1448
|
+
"id",
|
|
1449
|
+
"text",
|
|
1450
|
+
"created_at",
|
|
1451
|
+
"author_id",
|
|
1452
|
+
"referenced_tweets",
|
|
1453
|
+
"entities",
|
|
1454
|
+
"public_metrics",
|
|
1455
|
+
"attachments",
|
|
1456
|
+
"conversation_id"
|
|
1457
|
+
],
|
|
1458
|
+
"user.fields": ["id", "name", "username", "profile_image_url"],
|
|
1459
|
+
"media.fields": ["url", "preview_image_url", "type"],
|
|
1460
|
+
expansions: [
|
|
1461
|
+
"author_id",
|
|
1462
|
+
"attachments.media_keys",
|
|
1463
|
+
"referenced_tweets.id"
|
|
1464
|
+
],
|
|
1465
|
+
pagination_token: cursor
|
|
1466
|
+
});
|
|
1467
|
+
const tweets = [];
|
|
1468
|
+
for await (const tweet of response) {
|
|
1469
|
+
tweets.push(parseTweetV2ToV1(tweet, response.includes));
|
|
1470
|
+
if (tweets.length >= maxTweets) break;
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
tweets,
|
|
1474
|
+
next: response.meta?.next_token
|
|
1475
|
+
};
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
console.error("Failed to fetch user tweets:", error);
|
|
1478
|
+
throw error;
|
|
2059
1479
|
}
|
|
2060
|
-
const timelineV2 = parseTimelineTweetsV2(res.value);
|
|
2061
|
-
return {
|
|
2062
|
-
tweets: timelineV2.tweets,
|
|
2063
|
-
next: timelineV2.next
|
|
2064
|
-
};
|
|
2065
1480
|
}
|
|
2066
1481
|
async *getUserTweetsIterator(userId, maxTweets = 200) {
|
|
2067
1482
|
let cursor;
|
|
@@ -2344,32 +1759,6 @@ var Client = class {
|
|
|
2344
1759
|
"Logout is not applicable when using Twitter API v2 credentials"
|
|
2345
1760
|
);
|
|
2346
1761
|
}
|
|
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
1762
|
/**
|
|
2374
1763
|
* Sends a quote tweet.
|
|
2375
1764
|
* @param text The text of the tweet.
|
|
@@ -2453,14 +1842,6 @@ var Client = class {
|
|
|
2453
1842
|
}
|
|
2454
1843
|
return res.value;
|
|
2455
1844
|
}
|
|
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
1845
|
/**
|
|
2465
1846
|
* Retrieves all users who retweeted the given tweet.
|
|
2466
1847
|
* @param tweetId The ID of the tweet.
|
|
@@ -2670,72 +2051,6 @@ var _ClientBase = class _ClientBase {
|
|
|
2670
2051
|
onReady() {
|
|
2671
2052
|
throw new Error("Not implemented in base class, please call from subclass");
|
|
2672
2053
|
}
|
|
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
2054
|
async init() {
|
|
2740
2055
|
const apiKey = this.state?.TWITTER_API_KEY || this.runtime.getSetting("TWITTER_API_KEY");
|
|
2741
2056
|
const apiSecretKey = this.state?.TWITTER_API_SECRET_KEY || this.runtime.getSetting("TWITTER_API_SECRET_KEY");
|
|
@@ -2845,7 +2160,7 @@ var _ClientBase = class _ClientBase {
|
|
|
2845
2160
|
this.profile.id,
|
|
2846
2161
|
count
|
|
2847
2162
|
);
|
|
2848
|
-
return homeTimeline.tweets
|
|
2163
|
+
return homeTimeline.tweets;
|
|
2849
2164
|
}
|
|
2850
2165
|
/**
|
|
2851
2166
|
* Fetch timeline for twitter account, optionally only from followed accounts
|
|
@@ -2853,8 +2168,7 @@ var _ClientBase = class _ClientBase {
|
|
|
2853
2168
|
async fetchHomeTimeline(count, following) {
|
|
2854
2169
|
logger.debug("fetching home timeline");
|
|
2855
2170
|
const homeTimeline = following ? await this.twitterClient.fetchFollowingTimeline(count, []) : await this.twitterClient.fetchHomeTimeline(count, []);
|
|
2856
|
-
|
|
2857
|
-
return processedTimeline;
|
|
2171
|
+
return homeTimeline;
|
|
2858
2172
|
}
|
|
2859
2173
|
async fetchSearchTweets(query, maxTweets, searchMode, cursor) {
|
|
2860
2174
|
try {
|
|
@@ -3162,7 +2476,7 @@ import {
|
|
|
3162
2476
|
ContentType,
|
|
3163
2477
|
EventType,
|
|
3164
2478
|
createUniqueUuid as createUniqueUuid3,
|
|
3165
|
-
logger as
|
|
2479
|
+
logger as logger4
|
|
3166
2480
|
} from "@elizaos/core";
|
|
3167
2481
|
|
|
3168
2482
|
// src/utils.ts
|
|
@@ -3170,9 +2484,14 @@ import fs from "fs";
|
|
|
3170
2484
|
import path from "path";
|
|
3171
2485
|
import {
|
|
3172
2486
|
createUniqueUuid as createUniqueUuid2,
|
|
3173
|
-
logger as
|
|
2487
|
+
logger as logger3,
|
|
3174
2488
|
truncateToCompleteSentence
|
|
3175
2489
|
} from "@elizaos/core";
|
|
2490
|
+
|
|
2491
|
+
// src/utils/error-handler.ts
|
|
2492
|
+
import { logger as logger2 } from "@elizaos/core";
|
|
2493
|
+
|
|
2494
|
+
// src/utils.ts
|
|
3176
2495
|
async function sendTweet(client, text, mediaData = [], tweetToReplyTo) {
|
|
3177
2496
|
const isNoteTweet = text.length > TWEET_MAX_LENGTH;
|
|
3178
2497
|
const postText = isNoteTweet ? truncateToCompleteSentence(text, TWEET_MAX_LENGTH) : text;
|
|
@@ -3183,9 +2502,9 @@ async function sendTweet(client, text, mediaData = [], tweetToReplyTo) {
|
|
|
3183
2502
|
tweetToReplyTo,
|
|
3184
2503
|
mediaData
|
|
3185
2504
|
);
|
|
3186
|
-
|
|
2505
|
+
logger3.log("Successfully posted Tweet");
|
|
3187
2506
|
} catch (error) {
|
|
3188
|
-
|
|
2507
|
+
logger3.error("Error posting Tweet:", error);
|
|
3189
2508
|
throw error;
|
|
3190
2509
|
}
|
|
3191
2510
|
try {
|
|
@@ -3197,14 +2516,14 @@ async function sendTweet(client, text, mediaData = [], tweetToReplyTo) {
|
|
|
3197
2516
|
}
|
|
3198
2517
|
await client.cacheLatestCheckedTweetId();
|
|
3199
2518
|
await client.cacheTweet(tweetResult);
|
|
3200
|
-
|
|
2519
|
+
logger3.log("Successfully posted a tweet", tweetResult.id);
|
|
3201
2520
|
return tweetResult;
|
|
3202
2521
|
}
|
|
3203
2522
|
} catch (error) {
|
|
3204
|
-
|
|
2523
|
+
logger3.error("Error parsing tweet response:", error);
|
|
3205
2524
|
throw error;
|
|
3206
2525
|
}
|
|
3207
|
-
|
|
2526
|
+
logger3.error("No valid response from Twitter API");
|
|
3208
2527
|
throw new Error("Failed to send tweet - no valid response");
|
|
3209
2528
|
}
|
|
3210
2529
|
var parseActionResponseFromText = (text) => {
|
|
@@ -7370,7 +6689,7 @@ var TwitterInteractionClient = class {
|
|
|
7370
6689
|
* Asynchronously handles Twitter interactions by checking for mentions, processing tweets, and updating the last checked tweet ID.
|
|
7371
6690
|
*/
|
|
7372
6691
|
async handleTwitterInteractions() {
|
|
7373
|
-
|
|
6692
|
+
logger4.log("Checking Twitter interactions");
|
|
7374
6693
|
const twitterUsername = this.client.profile?.username;
|
|
7375
6694
|
try {
|
|
7376
6695
|
const cursorKey = `twitter/${twitterUsername}/mention_cursor`;
|
|
@@ -7389,9 +6708,9 @@ var TwitterInteractionClient = class {
|
|
|
7389
6708
|
}
|
|
7390
6709
|
await this.processMentionTweets(mentionCandidates);
|
|
7391
6710
|
await this.client.cacheLatestCheckedTweetId();
|
|
7392
|
-
|
|
6711
|
+
logger4.log("Finished checking Twitter interactions");
|
|
7393
6712
|
} catch (error) {
|
|
7394
|
-
|
|
6713
|
+
logger4.error("Error handling Twitter interactions:", error);
|
|
7395
6714
|
}
|
|
7396
6715
|
}
|
|
7397
6716
|
/**
|
|
@@ -7405,7 +6724,7 @@ var TwitterInteractionClient = class {
|
|
|
7405
6724
|
* Note: MENTION_RECEIVED is currently disabled (see TODO below)
|
|
7406
6725
|
*/
|
|
7407
6726
|
async processMentionTweets(mentionCandidates) {
|
|
7408
|
-
|
|
6727
|
+
logger4.log(
|
|
7409
6728
|
"Completed checking mentioned tweets:",
|
|
7410
6729
|
mentionCandidates.length
|
|
7411
6730
|
);
|
|
@@ -7419,7 +6738,7 @@ var TwitterInteractionClient = class {
|
|
|
7419
6738
|
targetUsersConfig
|
|
7420
6739
|
);
|
|
7421
6740
|
if (!shouldTarget) {
|
|
7422
|
-
|
|
6741
|
+
logger4.log(
|
|
7423
6742
|
`Skipping tweet from @${tweet.username} - not in target users list`
|
|
7424
6743
|
);
|
|
7425
6744
|
}
|
|
@@ -7431,7 +6750,7 @@ var TwitterInteractionClient = class {
|
|
|
7431
6750
|
const tweetId = createUniqueUuid3(this.runtime, tweet.id);
|
|
7432
6751
|
const existingResponse = await this.runtime.getMemoryById(tweetId);
|
|
7433
6752
|
if (existingResponse) {
|
|
7434
|
-
|
|
6753
|
+
logger4.log(`Already responded to tweet ${tweet.id}, skipping`);
|
|
7435
6754
|
continue;
|
|
7436
6755
|
}
|
|
7437
6756
|
const conversationRoomId = createUniqueUuid3(
|
|
@@ -7448,12 +6767,12 @@ var TwitterInteractionClient = class {
|
|
|
7448
6767
|
(memory2) => memory2.content?.inReplyTo === tweetId || memory2.content?.source === "twitter" && memory2.agentId === this.runtime.agentId && memory2.content?.inReplyTo === tweetId
|
|
7449
6768
|
);
|
|
7450
6769
|
if (hasExistingReply) {
|
|
7451
|
-
|
|
6770
|
+
logger4.log(
|
|
7452
6771
|
`Already replied to tweet ${tweet.id} (found existing reply), skipping`
|
|
7453
6772
|
);
|
|
7454
6773
|
continue;
|
|
7455
6774
|
}
|
|
7456
|
-
|
|
6775
|
+
logger4.log("New Tweet found", tweet.permanentUrl);
|
|
7457
6776
|
const entityId = createUniqueUuid3(
|
|
7458
6777
|
this.runtime,
|
|
7459
6778
|
tweet.userId === this.client.profile.id ? this.runtime.agentId : tweet.userId
|
|
@@ -7668,23 +6987,23 @@ var TwitterInteractionClient = class {
|
|
|
7668
6987
|
thread
|
|
7669
6988
|
}) {
|
|
7670
6989
|
if (!message.content.text) {
|
|
7671
|
-
|
|
6990
|
+
logger4.log("Skipping Tweet with no text", tweet.id);
|
|
7672
6991
|
return { text: "", actions: ["IGNORE"] };
|
|
7673
6992
|
}
|
|
7674
6993
|
const callback = async (response, tweetId) => {
|
|
7675
6994
|
try {
|
|
7676
6995
|
if (!response.text) {
|
|
7677
|
-
|
|
6996
|
+
logger4.warn("No text content in response, skipping tweet reply");
|
|
7678
6997
|
return [];
|
|
7679
6998
|
}
|
|
7680
6999
|
const tweetToReplyTo = tweetId || tweet.id;
|
|
7681
7000
|
if (this.isDryRun) {
|
|
7682
|
-
|
|
7001
|
+
logger4.info(
|
|
7683
7002
|
`[DRY RUN] Would have replied to ${tweet.username} with: ${response.text}`
|
|
7684
7003
|
);
|
|
7685
7004
|
return [];
|
|
7686
7005
|
}
|
|
7687
|
-
|
|
7006
|
+
logger4.info(`Replying to tweet ${tweetToReplyTo}`);
|
|
7688
7007
|
const tweetResult = await sendTweet(
|
|
7689
7008
|
this.client,
|
|
7690
7009
|
response.text,
|
|
@@ -7694,7 +7013,7 @@ var TwitterInteractionClient = class {
|
|
|
7694
7013
|
if (!tweetResult) {
|
|
7695
7014
|
throw new Error("Failed to get tweet result from response");
|
|
7696
7015
|
}
|
|
7697
|
-
const responseId = createUniqueUuid3(this.runtime, tweetResult.
|
|
7016
|
+
const responseId = createUniqueUuid3(this.runtime, tweetResult.id);
|
|
7698
7017
|
const responseMemory = {
|
|
7699
7018
|
id: responseId,
|
|
7700
7019
|
entityId: this.runtime.agentId,
|
|
@@ -7710,7 +7029,7 @@ var TwitterInteractionClient = class {
|
|
|
7710
7029
|
await this.runtime.createMemory(responseMemory, "messages");
|
|
7711
7030
|
return [responseMemory];
|
|
7712
7031
|
} catch (error) {
|
|
7713
|
-
|
|
7032
|
+
logger4.error("Error replying to tweet:", error);
|
|
7714
7033
|
return [];
|
|
7715
7034
|
}
|
|
7716
7035
|
};
|
|
@@ -7733,17 +7052,17 @@ var TwitterInteractionClient = class {
|
|
|
7733
7052
|
const thread = [];
|
|
7734
7053
|
const visited = /* @__PURE__ */ new Set();
|
|
7735
7054
|
async function processThread(currentTweet, depth = 0) {
|
|
7736
|
-
|
|
7055
|
+
logger4.log("Processing tweet:", {
|
|
7737
7056
|
id: currentTweet.id,
|
|
7738
7057
|
inReplyToStatusId: currentTweet.inReplyToStatusId,
|
|
7739
7058
|
depth
|
|
7740
7059
|
});
|
|
7741
7060
|
if (!currentTweet) {
|
|
7742
|
-
|
|
7061
|
+
logger4.log("No current tweet found for thread building");
|
|
7743
7062
|
return;
|
|
7744
7063
|
}
|
|
7745
7064
|
if (depth >= maxReplies) {
|
|
7746
|
-
|
|
7065
|
+
logger4.log("Reached maximum reply depth", depth);
|
|
7747
7066
|
return;
|
|
7748
7067
|
}
|
|
7749
7068
|
const memory = await this.runtime.getMemoryById(
|
|
@@ -7781,37 +7100,37 @@ var TwitterInteractionClient = class {
|
|
|
7781
7100
|
);
|
|
7782
7101
|
}
|
|
7783
7102
|
if (visited.has(currentTweet.id)) {
|
|
7784
|
-
|
|
7103
|
+
logger4.log("Already visited tweet:", currentTweet.id);
|
|
7785
7104
|
return;
|
|
7786
7105
|
}
|
|
7787
7106
|
visited.add(currentTweet.id);
|
|
7788
7107
|
thread.unshift(currentTweet);
|
|
7789
7108
|
if (currentTweet.inReplyToStatusId) {
|
|
7790
|
-
|
|
7109
|
+
logger4.log("Fetching parent tweet:", currentTweet.inReplyToStatusId);
|
|
7791
7110
|
try {
|
|
7792
7111
|
const parentTweet = await this.twitterClient.getTweet(
|
|
7793
7112
|
currentTweet.inReplyToStatusId
|
|
7794
7113
|
);
|
|
7795
7114
|
if (parentTweet) {
|
|
7796
|
-
|
|
7115
|
+
logger4.log("Found parent tweet:", {
|
|
7797
7116
|
id: parentTweet.id,
|
|
7798
7117
|
text: parentTweet.text?.slice(0, 50)
|
|
7799
7118
|
});
|
|
7800
7119
|
await processThread(parentTweet, depth + 1);
|
|
7801
7120
|
} else {
|
|
7802
|
-
|
|
7121
|
+
logger4.log(
|
|
7803
7122
|
"No parent tweet found for:",
|
|
7804
7123
|
currentTweet.inReplyToStatusId
|
|
7805
7124
|
);
|
|
7806
7125
|
}
|
|
7807
7126
|
} catch (error) {
|
|
7808
|
-
|
|
7127
|
+
logger4.log("Error fetching parent tweet:", {
|
|
7809
7128
|
tweetId: currentTweet.inReplyToStatusId,
|
|
7810
7129
|
error
|
|
7811
7130
|
});
|
|
7812
7131
|
}
|
|
7813
7132
|
} else {
|
|
7814
|
-
|
|
7133
|
+
logger4.log("Reached end of reply chain at:", currentTweet.id);
|
|
7815
7134
|
}
|
|
7816
7135
|
}
|
|
7817
7136
|
await processThread.bind(this)(tweet, 0);
|
|
@@ -7837,7 +7156,7 @@ import {
|
|
|
7837
7156
|
ChannelType as ChannelType3,
|
|
7838
7157
|
EventType as EventType2,
|
|
7839
7158
|
createUniqueUuid as createUniqueUuid4,
|
|
7840
|
-
logger as
|
|
7159
|
+
logger as logger5
|
|
7841
7160
|
} from "@elizaos/core";
|
|
7842
7161
|
var TwitterPostClient = class {
|
|
7843
7162
|
/**
|
|
@@ -7850,17 +7169,20 @@ var TwitterPostClient = class {
|
|
|
7850
7169
|
this.client = client;
|
|
7851
7170
|
this.state = state;
|
|
7852
7171
|
this.runtime = runtime;
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7172
|
+
const dryRunSetting = this.state?.TWITTER_DRY_RUN ?? this.runtime.getSetting("TWITTER_DRY_RUN");
|
|
7173
|
+
this.isDryRun = dryRunSetting === true || dryRunSetting === "true" || typeof dryRunSetting === "string" && dryRunSetting.toLowerCase() === "true";
|
|
7174
|
+
logger5.log("Twitter Client Configuration:");
|
|
7175
|
+
logger5.log(`- Dry Run Mode: ${this.isDryRun ? "Enabled" : "Disabled"}`);
|
|
7176
|
+
logger5.log(
|
|
7857
7177
|
`- 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
7178
|
);
|
|
7859
|
-
|
|
7860
|
-
|
|
7179
|
+
const postImmediatelySetting = this.state?.TWITTER_POST_IMMEDIATELY ?? this.runtime.getSetting("TWITTER_POST_IMMEDIATELY");
|
|
7180
|
+
const isPostImmediately = postImmediatelySetting === true || postImmediatelySetting === "true" || typeof postImmediatelySetting === "string" && postImmediatelySetting.toLowerCase() === "true";
|
|
7181
|
+
logger5.log(
|
|
7182
|
+
`- Post Immediately: ${isPostImmediately ? "enabled" : "disabled"}`
|
|
7861
7183
|
);
|
|
7862
7184
|
if (this.isDryRun) {
|
|
7863
|
-
|
|
7185
|
+
logger5.log(
|
|
7864
7186
|
"Twitter client initialized in dry run mode - no actual tweets should be posted"
|
|
7865
7187
|
);
|
|
7866
7188
|
}
|
|
@@ -7869,7 +7191,7 @@ var TwitterPostClient = class {
|
|
|
7869
7191
|
* Starts the Twitter post client, setting up a loop to periodically generate new tweets.
|
|
7870
7192
|
*/
|
|
7871
7193
|
async start() {
|
|
7872
|
-
|
|
7194
|
+
logger5.log("Starting Twitter post client...");
|
|
7873
7195
|
const generateNewTweetLoop = async () => {
|
|
7874
7196
|
const minPostMinutes = this.state?.TWITTER_POST_INTERVAL_MIN || this.runtime.getSetting("TWITTER_POST_INTERVAL_MIN") || 90;
|
|
7875
7197
|
const maxPostMinutes = this.state?.TWITTER_POST_INTERVAL_MAX || this.runtime.getSetting("TWITTER_POST_INTERVAL_MAX") || 180;
|
|
@@ -7879,7 +7201,9 @@ var TwitterPostClient = class {
|
|
|
7879
7201
|
setTimeout(generateNewTweetLoop, interval);
|
|
7880
7202
|
};
|
|
7881
7203
|
setTimeout(generateNewTweetLoop, 60 * 1e3);
|
|
7882
|
-
|
|
7204
|
+
const postImmediately = this.state?.TWITTER_POST_IMMEDIATELY ?? this.runtime.getSetting("TWITTER_POST_IMMEDIATELY");
|
|
7205
|
+
const shouldPostImmediately = postImmediately === true || postImmediately === "true" || typeof postImmediately === "string" && postImmediately.toLowerCase() === "true";
|
|
7206
|
+
if (shouldPostImmediately) {
|
|
7883
7207
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
7884
7208
|
await this.generateNewTweet();
|
|
7885
7209
|
}
|
|
@@ -7889,33 +7213,38 @@ var TwitterPostClient = class {
|
|
|
7889
7213
|
* This approach aligns with our platform-independent architecture.
|
|
7890
7214
|
*/
|
|
7891
7215
|
async generateNewTweet() {
|
|
7216
|
+
logger5.info("Attempting to generate new tweet...");
|
|
7892
7217
|
try {
|
|
7893
7218
|
const userId = this.client.profile?.id;
|
|
7894
7219
|
if (!userId) {
|
|
7895
|
-
|
|
7220
|
+
logger5.error("Cannot generate tweet: Twitter profile not available");
|
|
7896
7221
|
return;
|
|
7897
7222
|
}
|
|
7223
|
+
logger5.info(`Generating tweet for user: ${this.client.profile?.username} (${userId})`);
|
|
7898
7224
|
const worldId = createUniqueUuid4(this.runtime, userId);
|
|
7899
7225
|
const roomId = createUniqueUuid4(this.runtime, `${userId}-home`);
|
|
7900
7226
|
const callback = async (content) => {
|
|
7227
|
+
logger5.info("Tweet generation callback triggered");
|
|
7901
7228
|
try {
|
|
7902
7229
|
if (this.isDryRun) {
|
|
7903
|
-
|
|
7230
|
+
logger5.info(`[DRY RUN] Would post tweet: ${content.text}`);
|
|
7904
7231
|
return [];
|
|
7905
7232
|
}
|
|
7906
7233
|
if (content.text.includes("Error: Missing")) {
|
|
7907
|
-
|
|
7234
|
+
logger5.error("Error: Missing some context", content);
|
|
7908
7235
|
return [];
|
|
7909
7236
|
}
|
|
7237
|
+
logger5.info(`Posting tweet: ${content.text}`);
|
|
7910
7238
|
const result = await this.postToTwitter(
|
|
7911
7239
|
content.text,
|
|
7912
7240
|
content.mediaData
|
|
7913
7241
|
);
|
|
7914
7242
|
if (result === null) {
|
|
7915
|
-
|
|
7243
|
+
logger5.info("Skipped posting duplicate tweet");
|
|
7916
7244
|
return [];
|
|
7917
7245
|
}
|
|
7918
|
-
const tweetId = result.
|
|
7246
|
+
const tweetId = result.id;
|
|
7247
|
+
logger5.info(`Tweet posted successfully! ID: ${tweetId}`);
|
|
7919
7248
|
if (result) {
|
|
7920
7249
|
const postedTweetId = createUniqueUuid4(this.runtime, tweetId);
|
|
7921
7250
|
const postedMemory = {
|
|
@@ -7940,10 +7269,11 @@ var TwitterPostClient = class {
|
|
|
7940
7269
|
}
|
|
7941
7270
|
return [];
|
|
7942
7271
|
} catch (error) {
|
|
7943
|
-
|
|
7272
|
+
logger5.error("Error posting tweet:", error, content);
|
|
7944
7273
|
return [];
|
|
7945
7274
|
}
|
|
7946
7275
|
};
|
|
7276
|
+
logger5.info("Emitting POST_GENERATED event to trigger content generation...");
|
|
7947
7277
|
this.runtime.emitEvent(
|
|
7948
7278
|
[EventType2.POST_GENERATED, "TWITTER_POST_GENERATED" /* POST_GENERATED */],
|
|
7949
7279
|
{
|
|
@@ -7955,8 +7285,9 @@ var TwitterPostClient = class {
|
|
|
7955
7285
|
source: "twitter"
|
|
7956
7286
|
}
|
|
7957
7287
|
);
|
|
7288
|
+
logger5.info("POST_GENERATED event emitted successfully");
|
|
7958
7289
|
} catch (error) {
|
|
7959
|
-
|
|
7290
|
+
logger5.error("Error generating tweet:", error);
|
|
7960
7291
|
}
|
|
7961
7292
|
}
|
|
7962
7293
|
/**
|
|
@@ -7973,7 +7304,7 @@ var TwitterPostClient = class {
|
|
|
7973
7304
|
if (lastPost) {
|
|
7974
7305
|
const lastTweet = await this.client.getTweet(lastPost.id);
|
|
7975
7306
|
if (lastTweet && lastTweet.text === text) {
|
|
7976
|
-
|
|
7307
|
+
logger5.warn(
|
|
7977
7308
|
"Tweet is a duplicate of the last post. Skipping to avoid duplicate."
|
|
7978
7309
|
);
|
|
7979
7310
|
return null;
|
|
@@ -7983,22 +7314,22 @@ var TwitterPostClient = class {
|
|
|
7983
7314
|
if (mediaData && mediaData.length > 0) {
|
|
7984
7315
|
for (const media of mediaData) {
|
|
7985
7316
|
try {
|
|
7986
|
-
|
|
7317
|
+
logger5.warn(
|
|
7987
7318
|
"Media upload not currently supported with the modern Twitter API"
|
|
7988
7319
|
);
|
|
7989
7320
|
} catch (error) {
|
|
7990
|
-
|
|
7321
|
+
logger5.error("Error uploading media:", error);
|
|
7991
7322
|
}
|
|
7992
7323
|
}
|
|
7993
7324
|
}
|
|
7994
7325
|
const result = await sendTweet(this.client, text, mediaData);
|
|
7995
7326
|
if (!result) {
|
|
7996
|
-
|
|
7327
|
+
logger5.error("Error sending tweet; Bad response:");
|
|
7997
7328
|
return null;
|
|
7998
7329
|
}
|
|
7999
7330
|
return result;
|
|
8000
7331
|
} catch (error) {
|
|
8001
|
-
|
|
7332
|
+
logger5.error("Error posting to Twitter:", error);
|
|
8002
7333
|
throw error;
|
|
8003
7334
|
}
|
|
8004
7335
|
}
|
|
@@ -8014,7 +7345,7 @@ import {
|
|
|
8014
7345
|
ModelType,
|
|
8015
7346
|
parseKeyValueXml
|
|
8016
7347
|
} from "@elizaos/core";
|
|
8017
|
-
import { logger as
|
|
7348
|
+
import { logger as logger6 } from "@elizaos/core";
|
|
8018
7349
|
|
|
8019
7350
|
// src/templates.ts
|
|
8020
7351
|
var twitterActionTemplate = `
|
|
@@ -8099,30 +7430,7 @@ var TwitterTimelineClient = class {
|
|
|
8099
7430
|
async getTimeline(count) {
|
|
8100
7431
|
const twitterUsername = this.client.profile?.username;
|
|
8101
7432
|
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);
|
|
7433
|
+
return homeTimeline.filter((tweet) => tweet.username !== twitterUsername);
|
|
8126
7434
|
}
|
|
8127
7435
|
createTweetId(runtime, tweet) {
|
|
8128
7436
|
return createUniqueUuid5(runtime, tweet.id);
|
|
@@ -8178,7 +7486,7 @@ Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appro
|
|
|
8178
7486
|
}
|
|
8179
7487
|
);
|
|
8180
7488
|
if (!actionResponse) {
|
|
8181
|
-
|
|
7489
|
+
logger6.log(`No valid actions generated for tweet ${tweet.id}`);
|
|
8182
7490
|
continue;
|
|
8183
7491
|
}
|
|
8184
7492
|
const { actions } = parseActionResponseFromText(actionResponse.trim());
|
|
@@ -8189,7 +7497,7 @@ Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appro
|
|
|
8189
7497
|
roomId
|
|
8190
7498
|
});
|
|
8191
7499
|
} catch (error) {
|
|
8192
|
-
|
|
7500
|
+
logger6.error(`Error processing tweet ${tweet.id}:`, error);
|
|
8193
7501
|
continue;
|
|
8194
7502
|
}
|
|
8195
7503
|
}
|
|
@@ -8236,7 +7544,7 @@ Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appro
|
|
|
8236
7544
|
this.handleReplyAction(tweet);
|
|
8237
7545
|
}
|
|
8238
7546
|
} catch (error) {
|
|
8239
|
-
|
|
7547
|
+
logger6.error(`Error processing tweet ${tweet.id}:`, error);
|
|
8240
7548
|
continue;
|
|
8241
7549
|
}
|
|
8242
7550
|
}
|
|
@@ -8267,17 +7575,17 @@ Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appro
|
|
|
8267
7575
|
async handleLikeAction(tweet) {
|
|
8268
7576
|
try {
|
|
8269
7577
|
await this.twitterClient.likeTweet(tweet.id);
|
|
8270
|
-
|
|
7578
|
+
logger6.log(`Liked tweet ${tweet.id}`);
|
|
8271
7579
|
} catch (error) {
|
|
8272
|
-
|
|
7580
|
+
logger6.error(`Error liking tweet ${tweet.id}:`, error);
|
|
8273
7581
|
}
|
|
8274
7582
|
}
|
|
8275
7583
|
async handleRetweetAction(tweet) {
|
|
8276
7584
|
try {
|
|
8277
7585
|
await this.twitterClient.retweet(tweet.id);
|
|
8278
|
-
|
|
7586
|
+
logger6.log(`Retweeted tweet ${tweet.id}`);
|
|
8279
7587
|
} catch (error) {
|
|
8280
|
-
|
|
7588
|
+
logger6.error(`Error retweeting tweet ${tweet.id}:`, error);
|
|
8281
7589
|
}
|
|
8282
7590
|
}
|
|
8283
7591
|
async handleQuoteAction(tweet) {
|
|
@@ -8304,11 +7612,11 @@ ${tweet.text}`;
|
|
|
8304
7612
|
const body = await result.json();
|
|
8305
7613
|
const tweetResult = body?.data?.create_tweet?.tweet_results?.result || body?.data || body;
|
|
8306
7614
|
if (tweetResult) {
|
|
8307
|
-
|
|
7615
|
+
logger6.log("Successfully posted quote tweet");
|
|
8308
7616
|
} else {
|
|
8309
|
-
|
|
7617
|
+
logger6.error("Quote tweet creation failed:", body);
|
|
8310
7618
|
}
|
|
8311
|
-
const tweetId = tweetResult?.
|
|
7619
|
+
const tweetId = tweetResult?.id || Date.now().toString();
|
|
8312
7620
|
const responseId = createUniqueUuid5(this.runtime, tweetId);
|
|
8313
7621
|
const responseMemory = {
|
|
8314
7622
|
id: responseId,
|
|
@@ -8324,7 +7632,7 @@ ${tweet.text}`;
|
|
|
8324
7632
|
await this.runtime.createMemory(responseMemory, "messages");
|
|
8325
7633
|
}
|
|
8326
7634
|
} catch (error) {
|
|
8327
|
-
|
|
7635
|
+
logger6.error("Error in quote tweet generation:", error);
|
|
8328
7636
|
}
|
|
8329
7637
|
}
|
|
8330
7638
|
async handleReplyAction(tweet) {
|
|
@@ -8351,7 +7659,7 @@ ${tweet.text}`;
|
|
|
8351
7659
|
if (!tweetResult) {
|
|
8352
7660
|
throw new Error("Failed to get tweet result from response");
|
|
8353
7661
|
}
|
|
8354
|
-
const responseId = createUniqueUuid5(this.runtime, tweetResult.
|
|
7662
|
+
const responseId = createUniqueUuid5(this.runtime, tweetResult.id);
|
|
8355
7663
|
const responseMemory = {
|
|
8356
7664
|
id: responseId,
|
|
8357
7665
|
entityId: this.runtime.agentId,
|
|
@@ -8366,7 +7674,7 @@ ${tweet.text}`;
|
|
|
8366
7674
|
await this.runtime.createMemory(responseMemory, "messages");
|
|
8367
7675
|
}
|
|
8368
7676
|
} catch (error) {
|
|
8369
|
-
|
|
7677
|
+
logger6.error("Error in quote tweet generation:", error);
|
|
8370
7678
|
}
|
|
8371
7679
|
}
|
|
8372
7680
|
};
|
|
@@ -8479,18 +7787,35 @@ console.log(`Twitter plugin loaded with service name: ${TWITTER_SERVICE_NAME}`);
|
|
|
8479
7787
|
var TwitterClientInstance = class {
|
|
8480
7788
|
constructor(runtime, state) {
|
|
8481
7789
|
this.client = new ClientBase(runtime, state);
|
|
8482
|
-
|
|
7790
|
+
const postEnableSetting = runtime.getSetting("TWITTER_POST_ENABLE");
|
|
7791
|
+
logger7.info(`TWITTER_POST_ENABLE raw value: "${postEnableSetting}"`);
|
|
7792
|
+
logger7.info(`TWITTER_POST_ENABLE type: ${typeof postEnableSetting}`);
|
|
7793
|
+
const postEnabled = postEnableSetting === true || postEnableSetting === "true" || typeof postEnableSetting === "string" && postEnableSetting.toLowerCase() === "true";
|
|
7794
|
+
if (postEnabled) {
|
|
7795
|
+
logger7.info("Twitter posting is ENABLED - creating post client");
|
|
8483
7796
|
this.post = new TwitterPostClient(this.client, runtime, state);
|
|
7797
|
+
} else {
|
|
7798
|
+
logger7.info("Twitter posting is DISABLED - set TWITTER_POST_ENABLE=true to enable automatic posting");
|
|
8484
7799
|
}
|
|
8485
|
-
|
|
7800
|
+
const searchEnabledSetting = runtime.getSetting("TWITTER_SEARCH_ENABLE");
|
|
7801
|
+
logger7.info(`TWITTER_SEARCH_ENABLE raw value: "${searchEnabledSetting}"`);
|
|
7802
|
+
const searchEnabled = searchEnabledSetting !== false && searchEnabledSetting !== "false";
|
|
7803
|
+
if (searchEnabled) {
|
|
7804
|
+
logger7.info("Twitter search/interactions are ENABLED");
|
|
8486
7805
|
this.interaction = new TwitterInteractionClient(
|
|
8487
7806
|
this.client,
|
|
8488
7807
|
runtime,
|
|
8489
7808
|
state
|
|
8490
7809
|
);
|
|
7810
|
+
} else {
|
|
7811
|
+
logger7.info("Twitter search/interactions are DISABLED");
|
|
8491
7812
|
}
|
|
8492
|
-
|
|
7813
|
+
const actionProcessingEnabled = runtime.getSetting("TWITTER_ENABLE_ACTION_PROCESSING") === "true";
|
|
7814
|
+
if (actionProcessingEnabled) {
|
|
7815
|
+
logger7.info("Twitter action processing is ENABLED");
|
|
8493
7816
|
this.timeline = new TwitterTimelineClient(this.client, runtime, state);
|
|
7817
|
+
} else {
|
|
7818
|
+
logger7.info("Twitter action processing is DISABLED");
|
|
8494
7819
|
}
|
|
8495
7820
|
this.service = TwitterService.getInstance();
|
|
8496
7821
|
}
|
|
@@ -8511,7 +7836,7 @@ var _TwitterService = class _TwitterService extends Service {
|
|
|
8511
7836
|
try {
|
|
8512
7837
|
const existingClient = this.getClient(clientId, runtime.agentId);
|
|
8513
7838
|
if (existingClient) {
|
|
8514
|
-
|
|
7839
|
+
logger7.info(`Twitter client already exists for ${clientId}`);
|
|
8515
7840
|
return existingClient;
|
|
8516
7841
|
}
|
|
8517
7842
|
const client = new TwitterClientInstance(runtime, state);
|
|
@@ -8527,10 +7852,10 @@ var _TwitterService = class _TwitterService extends Service {
|
|
|
8527
7852
|
}
|
|
8528
7853
|
this.clients.set(this.getClientKey(clientId, runtime.agentId), client);
|
|
8529
7854
|
await this.emitServerJoinedEvent(runtime, client);
|
|
8530
|
-
|
|
7855
|
+
logger7.info(`Created Twitter client for ${clientId}`);
|
|
8531
7856
|
return client;
|
|
8532
7857
|
} catch (error) {
|
|
8533
|
-
|
|
7858
|
+
logger7.error(`Failed to create Twitter client for ${clientId}:`, error);
|
|
8534
7859
|
throw error;
|
|
8535
7860
|
}
|
|
8536
7861
|
}
|
|
@@ -8542,7 +7867,7 @@ var _TwitterService = class _TwitterService extends Service {
|
|
|
8542
7867
|
async emitServerJoinedEvent(runtime, client) {
|
|
8543
7868
|
try {
|
|
8544
7869
|
if (!client.client.profile) {
|
|
8545
|
-
|
|
7870
|
+
logger7.warn(
|
|
8546
7871
|
"Twitter profile not available yet, can't emit WORLD_JOINED event"
|
|
8547
7872
|
);
|
|
8548
7873
|
return;
|
|
@@ -8617,9 +7942,9 @@ var _TwitterService = class _TwitterService extends Service {
|
|
|
8617
7942
|
source: "twitter"
|
|
8618
7943
|
}
|
|
8619
7944
|
);
|
|
8620
|
-
|
|
7945
|
+
logger7.info(`Emitted WORLD_JOINED event for Twitter account ${username}`);
|
|
8621
7946
|
} catch (error) {
|
|
8622
|
-
|
|
7947
|
+
logger7.error("Failed to emit WORLD_JOINED event for Twitter:", error);
|
|
8623
7948
|
}
|
|
8624
7949
|
}
|
|
8625
7950
|
getClient(clientId, agentId) {
|
|
@@ -8632,9 +7957,9 @@ var _TwitterService = class _TwitterService extends Service {
|
|
|
8632
7957
|
try {
|
|
8633
7958
|
await client.service.stop();
|
|
8634
7959
|
this.clients.delete(key);
|
|
8635
|
-
|
|
7960
|
+
logger7.info(`Stopped Twitter client for ${clientId}`);
|
|
8636
7961
|
} catch (error) {
|
|
8637
|
-
|
|
7962
|
+
logger7.error(`Error stopping Twitter client for ${clientId}:`, error);
|
|
8638
7963
|
}
|
|
8639
7964
|
}
|
|
8640
7965
|
}
|
|
@@ -8651,7 +7976,7 @@ var _TwitterService = class _TwitterService extends Service {
|
|
|
8651
7976
|
);
|
|
8652
7977
|
try {
|
|
8653
7978
|
if (config.TWITTER_API_KEY && config.TWITTER_API_SECRET_KEY && config.TWITTER_ACCESS_TOKEN && config.TWITTER_ACCESS_TOKEN_SECRET) {
|
|
8654
|
-
|
|
7979
|
+
logger7.info("Creating default Twitter client from character settings");
|
|
8655
7980
|
await twitterClientManager.createClient(
|
|
8656
7981
|
runtime,
|
|
8657
7982
|
runtime.agentId,
|
|
@@ -8659,7 +7984,7 @@ var _TwitterService = class _TwitterService extends Service {
|
|
|
8659
7984
|
);
|
|
8660
7985
|
}
|
|
8661
7986
|
} catch (error) {
|
|
8662
|
-
|
|
7987
|
+
logger7.error("Failed to create default Twitter client:", error);
|
|
8663
7988
|
throw error;
|
|
8664
7989
|
}
|
|
8665
7990
|
return twitterClientManager;
|
|
@@ -8673,7 +7998,7 @@ var _TwitterService = class _TwitterService extends Service {
|
|
|
8673
7998
|
await client.service.stop();
|
|
8674
7999
|
this.clients.delete(key);
|
|
8675
8000
|
} catch (error) {
|
|
8676
|
-
|
|
8001
|
+
logger7.error(`Error stopping Twitter client ${key}:`, error);
|
|
8677
8002
|
}
|
|
8678
8003
|
}
|
|
8679
8004
|
}
|