birdclaw 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -1
- package/README.md +54 -15
- package/package.json +14 -15
- package/playwright.config.ts +5 -2
- package/src/cli.ts +289 -20
- package/src/lib/actions-transport.ts +58 -1
- package/src/lib/archive-import.ts +34 -2
- package/src/lib/backup.ts +271 -33
- package/src/lib/bird-actions.ts +21 -0
- package/src/lib/bird.ts +332 -17
- package/src/lib/blocks.ts +3 -3
- package/src/lib/bookmark-sync-job.ts +3 -3
- package/src/lib/config.ts +34 -1
- package/src/lib/db.ts +321 -14
- package/src/lib/dms-live.ts +3 -3
- package/src/lib/identity-search-index.ts +369 -0
- package/src/lib/mention-threads-live.ts +267 -0
- package/src/lib/mentions-live.ts +18 -7
- package/src/lib/moderation-target.ts +7 -5
- package/src/lib/moderation-write.ts +3 -3
- package/src/lib/profile-affiliation-hydration.ts +189 -0
- package/src/lib/profile-affiliations.ts +262 -0
- package/src/lib/profile-bio-entities.ts +318 -0
- package/src/lib/profile-history.ts +164 -0
- package/src/lib/profile-hydration.ts +8 -24
- package/src/lib/profile-resolver.ts +428 -0
- package/src/lib/queries.ts +522 -68
- package/src/lib/research.ts +607 -0
- package/src/lib/seed.ts +10 -4
- package/src/lib/sqlite.ts +143 -0
- package/src/lib/timeline-collections-live.ts +22 -8
- package/src/lib/timeline-live.ts +182 -0
- package/src/lib/tweet-account-edges.ts +39 -0
- package/src/lib/tweet-lookup.ts +35 -0
- package/src/lib/types.ts +103 -1
- package/src/lib/url-expansion.ts +147 -0
- package/src/lib/whois.ts +1060 -0
- package/src/lib/x-profile.ts +174 -17
- package/src/lib/xurl.ts +41 -6
package/src/cli.ts
CHANGED
|
@@ -29,13 +29,16 @@ import {
|
|
|
29
29
|
} from "#/lib/config";
|
|
30
30
|
import { syncDirectMessagesViaCachedBird } from "#/lib/dms-live";
|
|
31
31
|
import { listInboxItems, scoreInbox } from "#/lib/inbox";
|
|
32
|
+
import { syncMentionThreads } from "#/lib/mention-threads-live";
|
|
32
33
|
import { exportMentionItems } from "#/lib/mentions-export";
|
|
33
34
|
import {
|
|
34
35
|
exportMentionsViaCachedBird,
|
|
35
36
|
exportMentionsViaCachedXurl,
|
|
36
37
|
} from "#/lib/mentions-live";
|
|
37
38
|
import { hydrateProfilesFromX } from "#/lib/profile-hydration";
|
|
39
|
+
import { resolveProfilesForIds } from "#/lib/profile-resolver";
|
|
38
40
|
import { inspectProfileReplies } from "#/lib/profile-replies";
|
|
41
|
+
import { runResearchMode } from "#/lib/research";
|
|
39
42
|
import {
|
|
40
43
|
createDmReply,
|
|
41
44
|
createPost,
|
|
@@ -48,6 +51,9 @@ import {
|
|
|
48
51
|
syncTimelineCollection,
|
|
49
52
|
type TimelineCollectionMode,
|
|
50
53
|
} from "#/lib/timeline-collections-live";
|
|
54
|
+
import { syncHomeTimeline } from "#/lib/timeline-live";
|
|
55
|
+
import { expandUrlsFromTexts } from "#/lib/url-expansion";
|
|
56
|
+
import { formatWhois, runWhois } from "#/lib/whois";
|
|
51
57
|
|
|
52
58
|
const program = new Command();
|
|
53
59
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -63,12 +69,91 @@ function print(data: unknown, asJson: boolean) {
|
|
|
63
69
|
console.log(data);
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
function printError(error: string) {
|
|
73
|
+
console.error(JSON.stringify({ error }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseNonNegativeIntegerOption(
|
|
77
|
+
value: string | undefined,
|
|
78
|
+
option: string,
|
|
79
|
+
) {
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
86
|
+
printError(`${option} must be a non-negative integer`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
92
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
93
|
+
printError(`${option} must be a non-negative integer`);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
function resolveActionOptions(options: { transport?: string }) {
|
|
67
102
|
return {
|
|
68
103
|
transport: options.transport as ActionsTransport | undefined,
|
|
69
104
|
};
|
|
70
105
|
}
|
|
71
106
|
|
|
107
|
+
async function enrichDmItems(
|
|
108
|
+
query: Parameters<typeof listDmConversations>[0],
|
|
109
|
+
options: {
|
|
110
|
+
resolveProfiles?: boolean;
|
|
111
|
+
expandUrls?: boolean;
|
|
112
|
+
refreshProfileCache?: boolean;
|
|
113
|
+
refreshUrlCache?: boolean;
|
|
114
|
+
xurlFallback?: boolean;
|
|
115
|
+
},
|
|
116
|
+
) {
|
|
117
|
+
let items = listDmConversations(query);
|
|
118
|
+
const profileResolution = options.resolveProfiles
|
|
119
|
+
? await resolveProfilesForIds(
|
|
120
|
+
items.map((item) => item.participant.id),
|
|
121
|
+
{
|
|
122
|
+
refresh: options.refreshProfileCache,
|
|
123
|
+
xurlFallback: options.xurlFallback ?? true,
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
: undefined;
|
|
127
|
+
if (profileResolution) {
|
|
128
|
+
items = listDmConversations(query);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const urlExpansions = options.expandUrls
|
|
132
|
+
? await expandUrlsFromTexts(
|
|
133
|
+
items.flatMap((item) => [
|
|
134
|
+
item.lastMessagePreview,
|
|
135
|
+
item.searchSnippet ?? "",
|
|
136
|
+
...(item.matches ?? []).flatMap((match) => [
|
|
137
|
+
...match.before.map((message) => message.text),
|
|
138
|
+
match.message.text,
|
|
139
|
+
...match.after.map((message) => message.text),
|
|
140
|
+
]),
|
|
141
|
+
]),
|
|
142
|
+
{ refresh: options.refreshUrlCache },
|
|
143
|
+
)
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
if (!profileResolution && !urlExpansions) {
|
|
147
|
+
return items;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
items,
|
|
152
|
+
...(profileResolution ? { profileResolution } : {}),
|
|
153
|
+
...(urlExpansions ? { urlExpansions } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
72
157
|
async function autoUpdateBeforeRead() {
|
|
73
158
|
const result = await maybeAutoUpdateBackup();
|
|
74
159
|
if (!result.ok) {
|
|
@@ -171,10 +256,23 @@ searchCommand
|
|
|
171
256
|
.option("--until <date>", "Include tweets created before this date")
|
|
172
257
|
.option("--originals-only", "Exclude authored replies that start with @")
|
|
173
258
|
.option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise")
|
|
259
|
+
.option(
|
|
260
|
+
"--min-likes <n>",
|
|
261
|
+
"Override the low-quality like threshold (default 50)",
|
|
262
|
+
)
|
|
263
|
+
.option("--quality-reason", "Include qualityReason on each row")
|
|
174
264
|
.option("--liked", "Only liked tweets")
|
|
175
265
|
.option("--bookmarked", "Only bookmarked tweets")
|
|
176
266
|
.option("--limit <n>", "Limit results", "20")
|
|
177
267
|
.action(async (query, options) => {
|
|
268
|
+
const minLikes = parseNonNegativeIntegerOption(
|
|
269
|
+
options.minLikes,
|
|
270
|
+
"--min-likes",
|
|
271
|
+
);
|
|
272
|
+
if (options.minLikes !== undefined && minLikes === undefined) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
178
276
|
await autoUpdateBeforeRead();
|
|
179
277
|
const replyFilter = options.replied
|
|
180
278
|
? "replied"
|
|
@@ -189,6 +287,8 @@ searchCommand
|
|
|
189
287
|
until: options.until,
|
|
190
288
|
includeReplies: !options.originalsOnly,
|
|
191
289
|
qualityFilter: options.hideLowQuality ? "summary" : "all",
|
|
290
|
+
lowQualityThreshold: minLikes,
|
|
291
|
+
includeQualityReason: Boolean(options.qualityReason),
|
|
192
292
|
likedOnly: Boolean(options.liked),
|
|
193
293
|
bookmarkedOnly: Boolean(options.bookmarked),
|
|
194
294
|
limit: Number(options.limit),
|
|
@@ -204,17 +304,37 @@ searchCommand
|
|
|
204
304
|
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
205
305
|
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
206
306
|
.option("--sort <mode>", "recent or influence", "recent")
|
|
307
|
+
.option(
|
|
308
|
+
"--context <n>",
|
|
309
|
+
"Include N messages before and after each match",
|
|
310
|
+
"0",
|
|
311
|
+
)
|
|
312
|
+
.option(
|
|
313
|
+
"--resolve-profiles",
|
|
314
|
+
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
315
|
+
)
|
|
316
|
+
.option("--expand-urls", "Expand URLs through the persistent URL cache")
|
|
317
|
+
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
318
|
+
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
319
|
+
.option(
|
|
320
|
+
"--no-xurl-fallback",
|
|
321
|
+
"Do not fall back to xurl after bird profile lookup",
|
|
322
|
+
)
|
|
207
323
|
.option("--replied", "Only replied threads")
|
|
208
324
|
.option("--unreplied", "Only unreplied threads")
|
|
209
325
|
.option("--limit <n>", "Limit results", "20")
|
|
210
326
|
.action(async (query, options) => {
|
|
211
327
|
await autoUpdateBeforeRead();
|
|
328
|
+
const context = parseNonNegativeIntegerOption(options.context, "--context");
|
|
329
|
+
if (context === undefined) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
212
332
|
const replyFilter = options.replied
|
|
213
333
|
? "replied"
|
|
214
334
|
: options.unreplied
|
|
215
335
|
? "unreplied"
|
|
216
336
|
: "all";
|
|
217
|
-
const
|
|
337
|
+
const dmQuery = {
|
|
218
338
|
search: query,
|
|
219
339
|
participant: options.participant,
|
|
220
340
|
minFollowers: options.minFollowers
|
|
@@ -231,11 +351,96 @@ searchCommand
|
|
|
231
351
|
: undefined,
|
|
232
352
|
sort: options.sort === "influence" ? "influence" : "recent",
|
|
233
353
|
replyFilter,
|
|
354
|
+
context,
|
|
234
355
|
limit: Number(options.limit),
|
|
356
|
+
} as const;
|
|
357
|
+
const items = await enrichDmItems(dmQuery, {
|
|
358
|
+
resolveProfiles: Boolean(options.resolveProfiles),
|
|
359
|
+
expandUrls: Boolean(options.expandUrls),
|
|
360
|
+
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
361
|
+
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
362
|
+
xurlFallback: options.xurlFallback,
|
|
235
363
|
});
|
|
236
364
|
print(items, program.opts().json ?? false);
|
|
237
365
|
});
|
|
238
366
|
|
|
367
|
+
program
|
|
368
|
+
.command("whois <query>")
|
|
369
|
+
.description("Identify likely people or orgs from local DMs and tweets")
|
|
370
|
+
.option("--account <accountId>", "Account id")
|
|
371
|
+
.option("--no-dms", "Do not search DMs")
|
|
372
|
+
.option("--tweets", "Include local tweet search evidence")
|
|
373
|
+
.option("--no-resolve-profiles", "Do not resolve placeholder profiles")
|
|
374
|
+
.option("--no-expand-urls", "Do not expand URLs")
|
|
375
|
+
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
376
|
+
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
377
|
+
.option(
|
|
378
|
+
"--no-xurl-fallback",
|
|
379
|
+
"Do not fall back to xurl after bird profile lookup",
|
|
380
|
+
)
|
|
381
|
+
.option(
|
|
382
|
+
"--affiliation <query>",
|
|
383
|
+
"Require affiliation, bio, or history evidence",
|
|
384
|
+
)
|
|
385
|
+
.option(
|
|
386
|
+
"--current-affiliation <query>",
|
|
387
|
+
"Require an active affiliation badge",
|
|
388
|
+
)
|
|
389
|
+
.option(
|
|
390
|
+
"--exclude-domain-only",
|
|
391
|
+
"Drop candidates that only match domains/URLs",
|
|
392
|
+
)
|
|
393
|
+
.option("--context <n>", "DM messages before and after each match", "4")
|
|
394
|
+
.option("--limit <n>", "Limit candidates", "10")
|
|
395
|
+
.action(async (query, options) => {
|
|
396
|
+
await autoUpdateBeforeRead();
|
|
397
|
+
const context = parseNonNegativeIntegerOption(options.context, "--context");
|
|
398
|
+
if (context === undefined) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const result = await runWhois(query, {
|
|
402
|
+
account: options.account,
|
|
403
|
+
dms: options.dms,
|
|
404
|
+
tweets: Boolean(options.tweets),
|
|
405
|
+
resolveProfiles: options.resolveProfiles,
|
|
406
|
+
expandUrls: options.expandUrls,
|
|
407
|
+
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
408
|
+
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
409
|
+
xurlFallback: options.xurlFallback,
|
|
410
|
+
affiliation: options.affiliation,
|
|
411
|
+
currentAffiliation: options.currentAffiliation,
|
|
412
|
+
excludeDomainOnly: Boolean(options.excludeDomainOnly),
|
|
413
|
+
context,
|
|
414
|
+
limit: Number(options.limit),
|
|
415
|
+
});
|
|
416
|
+
print(
|
|
417
|
+
program.opts().json ? result : formatWhois(result),
|
|
418
|
+
program.opts().json ?? false,
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
program
|
|
423
|
+
.command("research [query]")
|
|
424
|
+
.description("Build a markdown research brief from bookmarked threads")
|
|
425
|
+
.option("--account <accountId>", "Account id")
|
|
426
|
+
.option("--limit <n>", "Seed bookmark limit", "20")
|
|
427
|
+
.option("--thread-depth <n>", "Maximum ancestor walk depth", "10")
|
|
428
|
+
.option("--out <path>", "Write the markdown brief to a file")
|
|
429
|
+
.action(async (query, options) => {
|
|
430
|
+
await autoUpdateBeforeRead();
|
|
431
|
+
const report = await runResearchMode({
|
|
432
|
+
account: options.account,
|
|
433
|
+
query,
|
|
434
|
+
limit: Number(options.limit),
|
|
435
|
+
maxThreadDepth: Number(options.threadDepth),
|
|
436
|
+
outPath: options.out,
|
|
437
|
+
});
|
|
438
|
+
print(
|
|
439
|
+
program.opts().json ? report : report.markdown,
|
|
440
|
+
program.opts().json ?? false,
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
|
|
239
444
|
const mentionsCommand = program
|
|
240
445
|
.command("mentions")
|
|
241
446
|
.description("Export local mention tweets for scripts and agents");
|
|
@@ -325,6 +530,50 @@ const syncCommand = program
|
|
|
325
530
|
.command("sync")
|
|
326
531
|
.description("Refresh live Twitter collections into the local store");
|
|
327
532
|
|
|
533
|
+
syncCommand
|
|
534
|
+
.command("timeline")
|
|
535
|
+
.description("Refresh live home timeline through bird")
|
|
536
|
+
.option("--account <accountId>", "Account id")
|
|
537
|
+
.option("--limit <n>", "Result limit", "100")
|
|
538
|
+
.option("--for-you", 'Fetch "For You" instead of chronological Following')
|
|
539
|
+
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
540
|
+
.option("--refresh", "Bypass live-cache freshness window")
|
|
541
|
+
.action(async (options) => {
|
|
542
|
+
const result = await syncHomeTimeline({
|
|
543
|
+
account: options.account,
|
|
544
|
+
limit: Number(options.limit),
|
|
545
|
+
following: !options.forYou,
|
|
546
|
+
refresh: Boolean(options.refresh),
|
|
547
|
+
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
548
|
+
});
|
|
549
|
+
await autoSyncAfterWrite();
|
|
550
|
+
print(result, true);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
syncCommand
|
|
554
|
+
.command("mention-threads")
|
|
555
|
+
.description(
|
|
556
|
+
"Fetch tweet conversation context for recent mentions through bird",
|
|
557
|
+
)
|
|
558
|
+
.option("--account <accountId>", "Account id")
|
|
559
|
+
.option("--limit <n>", "Recent mentions to inspect", "30")
|
|
560
|
+
.option("--delay-ms <n>", "Delay between thread fetches", "1500")
|
|
561
|
+
.option("--timeout-ms <n>", "Per-thread timeout", "15000")
|
|
562
|
+
.option("--all", "Fetch all retrievable thread pages")
|
|
563
|
+
.option("--max-pages <n>", "Stop after N pages")
|
|
564
|
+
.action(async (options) => {
|
|
565
|
+
const result = await syncMentionThreads({
|
|
566
|
+
account: options.account,
|
|
567
|
+
limit: Number(options.limit),
|
|
568
|
+
delayMs: Number(options.delayMs),
|
|
569
|
+
timeoutMs: Number(options.timeoutMs),
|
|
570
|
+
all: Boolean(options.all),
|
|
571
|
+
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
572
|
+
});
|
|
573
|
+
await autoSyncAfterWrite();
|
|
574
|
+
print(result, true);
|
|
575
|
+
});
|
|
576
|
+
|
|
328
577
|
for (const kind of ["likes", "bookmarks"] as const) {
|
|
329
578
|
syncCommand
|
|
330
579
|
.command(kind)
|
|
@@ -434,6 +683,17 @@ dmsCommand
|
|
|
434
683
|
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
435
684
|
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
436
685
|
.option("--sort <mode>", "recent or influence", "recent")
|
|
686
|
+
.option(
|
|
687
|
+
"--resolve-profiles",
|
|
688
|
+
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
689
|
+
)
|
|
690
|
+
.option("--expand-urls", "Expand URLs through the persistent URL cache")
|
|
691
|
+
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
692
|
+
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
693
|
+
.option(
|
|
694
|
+
"--no-xurl-fallback",
|
|
695
|
+
"Do not fall back to xurl after bird profile lookup",
|
|
696
|
+
)
|
|
437
697
|
.option("--replied", "Only replied threads")
|
|
438
698
|
.option("--unreplied", "Only unreplied threads")
|
|
439
699
|
.option("--limit <n>", "Limit results", "20")
|
|
@@ -454,25 +714,34 @@ dmsCommand
|
|
|
454
714
|
} else {
|
|
455
715
|
await autoUpdateBeforeRead();
|
|
456
716
|
}
|
|
457
|
-
const items =
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
717
|
+
const items = await enrichDmItems(
|
|
718
|
+
{
|
|
719
|
+
account: options.account,
|
|
720
|
+
participant: options.participant,
|
|
721
|
+
minFollowers: options.minFollowers
|
|
722
|
+
? Number(options.minFollowers)
|
|
723
|
+
: undefined,
|
|
724
|
+
maxFollowers: options.maxFollowers
|
|
725
|
+
? Number(options.maxFollowers)
|
|
726
|
+
: undefined,
|
|
727
|
+
minInfluenceScore: options.minInfluenceScore
|
|
728
|
+
? Number(options.minInfluenceScore)
|
|
729
|
+
: undefined,
|
|
730
|
+
maxInfluenceScore: options.maxInfluenceScore
|
|
731
|
+
? Number(options.maxInfluenceScore)
|
|
732
|
+
: undefined,
|
|
733
|
+
sort: options.sort === "influence" ? "influence" : "recent",
|
|
734
|
+
replyFilter,
|
|
735
|
+
limit: Number(options.limit),
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
resolveProfiles: Boolean(options.resolveProfiles),
|
|
739
|
+
expandUrls: Boolean(options.expandUrls),
|
|
740
|
+
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
741
|
+
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
742
|
+
xurlFallback: options.xurlFallback,
|
|
743
|
+
},
|
|
744
|
+
);
|
|
476
745
|
print(items, program.opts().json ?? false);
|
|
477
746
|
});
|
|
478
747
|
|
|
@@ -9,7 +9,9 @@ import { type ActionsTransport, resolveActionsTransport } from "./config";
|
|
|
9
9
|
import type {
|
|
10
10
|
ModerationAction,
|
|
11
11
|
ModerationActionTransportResult,
|
|
12
|
+
ModerationTransportKind,
|
|
12
13
|
} from "./types";
|
|
14
|
+
import { blockUserViaXWeb, unblockUserViaXWeb } from "./x-web";
|
|
13
15
|
import {
|
|
14
16
|
blockUserViaXurl,
|
|
15
17
|
lookupAuthenticatedUser,
|
|
@@ -27,7 +29,7 @@ interface RunActionParams {
|
|
|
27
29
|
transport?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
function normalizeFailure(transport:
|
|
32
|
+
function normalizeFailure(transport: ModerationTransportKind, output: string) {
|
|
31
33
|
return `${transport}: ${output}`;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -133,6 +135,37 @@ async function runXurlAction(
|
|
|
133
135
|
};
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
async function runXWebAction(
|
|
139
|
+
action: ModerationAction,
|
|
140
|
+
targetUserId?: string,
|
|
141
|
+
): Promise<ActionTransportResult> {
|
|
142
|
+
if (action !== "block" && action !== "unblock") {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
output: `x-web does not support ${action}`,
|
|
146
|
+
transport: "x-web",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!targetUserId) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
output: "missing target user id for x-web transport",
|
|
154
|
+
transport: "x-web",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result =
|
|
159
|
+
action === "block"
|
|
160
|
+
? await blockUserViaXWeb(targetUserId)
|
|
161
|
+
: await unblockUserViaXWeb(targetUserId);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...result,
|
|
165
|
+
transport: "x-web",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
136
169
|
export async function runModerationAction({
|
|
137
170
|
action,
|
|
138
171
|
query,
|
|
@@ -160,6 +193,30 @@ export async function runModerationAction({
|
|
|
160
193
|
};
|
|
161
194
|
}
|
|
162
195
|
|
|
196
|
+
if (action === "block" || action === "unblock") {
|
|
197
|
+
const xWebResult = await runXWebAction(action, targetUserId);
|
|
198
|
+
if (xWebResult.ok) {
|
|
199
|
+
return {
|
|
200
|
+
...xWebResult,
|
|
201
|
+
output: [
|
|
202
|
+
xWebResult.output,
|
|
203
|
+
`falling back after ${normalizeFailure("bird", birdResult.output)}`,
|
|
204
|
+
`falling back after ${normalizeFailure("xurl", xurlResult.output)}`,
|
|
205
|
+
].join("\n"),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
output: [
|
|
212
|
+
normalizeFailure("bird", birdResult.output),
|
|
213
|
+
normalizeFailure("xurl", xurlResult.output),
|
|
214
|
+
normalizeFailure("x-web", xWebResult.output),
|
|
215
|
+
].join("\n"),
|
|
216
|
+
transport: xWebResult.transport,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
163
220
|
return {
|
|
164
221
|
ok: false,
|
|
165
222
|
output: [
|
|
@@ -286,6 +286,7 @@ function clearImportedData() {
|
|
|
286
286
|
db.exec(`
|
|
287
287
|
delete from ai_scores;
|
|
288
288
|
delete from tweet_actions;
|
|
289
|
+
delete from tweet_account_edges;
|
|
289
290
|
delete from tweet_collections;
|
|
290
291
|
delete from dm_fts;
|
|
291
292
|
delete from tweets_fts;
|
|
@@ -476,6 +477,7 @@ export async function importArchive(
|
|
|
476
477
|
displayName: string;
|
|
477
478
|
bio: string;
|
|
478
479
|
followersCount: number;
|
|
480
|
+
followingCount: number;
|
|
479
481
|
avatarHue: number;
|
|
480
482
|
createdAt: string;
|
|
481
483
|
}
|
|
@@ -500,6 +502,7 @@ export async function importArchive(
|
|
|
500
502
|
displayName: accountPayload.displayName,
|
|
501
503
|
bio: accountPayload.bio,
|
|
502
504
|
followersCount: 0,
|
|
505
|
+
followingCount: 0,
|
|
503
506
|
avatarHue: 18,
|
|
504
507
|
createdAt: accountPayload.createdAt,
|
|
505
508
|
};
|
|
@@ -583,6 +586,7 @@ export async function importArchive(
|
|
|
583
586
|
conversationName || `Group DM ${externalParticipantIds.length}`,
|
|
584
587
|
bio: `Group DM with ${externalParticipantIds.length} participants`,
|
|
585
588
|
followersCount: 0,
|
|
589
|
+
followingCount: 0,
|
|
586
590
|
avatarHue: 220,
|
|
587
591
|
createdAt: accountPayload.createdAt,
|
|
588
592
|
});
|
|
@@ -598,6 +602,7 @@ export async function importArchive(
|
|
|
598
602
|
displayName: inferred.displayName,
|
|
599
603
|
bio: `Imported from archive user ${otherUserId}`,
|
|
600
604
|
followersCount: 0,
|
|
605
|
+
followingCount: 0,
|
|
601
606
|
avatarHue: 210,
|
|
602
607
|
createdAt: accountPayload.createdAt,
|
|
603
608
|
});
|
|
@@ -626,6 +631,7 @@ export async function importArchive(
|
|
|
626
631
|
displayName: inferred.displayName,
|
|
627
632
|
bio: `Imported from archive user ${senderId}`,
|
|
628
633
|
followersCount: 0,
|
|
634
|
+
followingCount: 0,
|
|
629
635
|
avatarHue: 240,
|
|
630
636
|
createdAt: accountPayload.createdAt,
|
|
631
637
|
});
|
|
@@ -745,6 +751,7 @@ export async function importArchive(
|
|
|
745
751
|
displayName: "Unknown",
|
|
746
752
|
bio: "Imported from archive collection metadata",
|
|
747
753
|
followersCount: 0,
|
|
754
|
+
followingCount: 0,
|
|
748
755
|
avatarHue: 210,
|
|
749
756
|
createdAt: accountPayload.createdAt,
|
|
750
757
|
});
|
|
@@ -758,8 +765,8 @@ export async function importArchive(
|
|
|
758
765
|
values (?, ?, ?, ?, ?, 1, ?)
|
|
759
766
|
`);
|
|
760
767
|
const insertProfile = db.prepare(`
|
|
761
|
-
insert into profiles (id, handle, display_name, bio, followers_count, avatar_hue, avatar_url, created_at)
|
|
762
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
768
|
+
insert into profiles (id, handle, display_name, bio, followers_count, following_count, avatar_hue, avatar_url, created_at)
|
|
769
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
763
770
|
`);
|
|
764
771
|
const insertTweet = db.prepare(`
|
|
765
772
|
insert into tweets (
|
|
@@ -770,6 +777,16 @@ export async function importArchive(
|
|
|
770
777
|
const insertTweetFts = db.prepare(
|
|
771
778
|
"insert into tweets_fts (tweet_id, text) values (?, ?)",
|
|
772
779
|
);
|
|
780
|
+
const insertTimelineEdge = db.prepare(`
|
|
781
|
+
insert into tweet_account_edges (
|
|
782
|
+
account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count, source,
|
|
783
|
+
raw_json, updated_at
|
|
784
|
+
) values (?, ?, ?, ?, ?, 1, 'archive', '{}', ?)
|
|
785
|
+
on conflict(account_id, tweet_id, kind) do update set
|
|
786
|
+
first_seen_at = min(tweet_account_edges.first_seen_at, excluded.first_seen_at),
|
|
787
|
+
last_seen_at = max(tweet_account_edges.last_seen_at, excluded.last_seen_at),
|
|
788
|
+
updated_at = max(tweet_account_edges.updated_at, excluded.updated_at)
|
|
789
|
+
`);
|
|
773
790
|
const insertCollection = db.prepare(`
|
|
774
791
|
insert into tweet_collections (
|
|
775
792
|
account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
|
|
@@ -811,6 +828,7 @@ export async function importArchive(
|
|
|
811
828
|
profile.displayName,
|
|
812
829
|
profile.bio,
|
|
813
830
|
profile.followersCount,
|
|
831
|
+
profile.followingCount,
|
|
814
832
|
profile.avatarHue,
|
|
815
833
|
null,
|
|
816
834
|
profile.createdAt,
|
|
@@ -835,6 +853,16 @@ export async function importArchive(
|
|
|
835
853
|
tweet.mediaJson,
|
|
836
854
|
tweet.quotedTweetId,
|
|
837
855
|
);
|
|
856
|
+
if (tweet.kind === "home") {
|
|
857
|
+
insertTimelineEdge.run(
|
|
858
|
+
"acct_primary",
|
|
859
|
+
tweet.id,
|
|
860
|
+
tweet.kind,
|
|
861
|
+
tweet.createdAt,
|
|
862
|
+
tweet.createdAt,
|
|
863
|
+
new Date().toISOString(),
|
|
864
|
+
);
|
|
865
|
+
}
|
|
838
866
|
insertTweetFts.run(tweet.id, tweet.text);
|
|
839
867
|
}
|
|
840
868
|
|
|
@@ -898,8 +926,11 @@ export async function importArchive(
|
|
|
898
926
|
}
|
|
899
927
|
|
|
900
928
|
export const __test__ = {
|
|
929
|
+
normalizeArchivePath,
|
|
901
930
|
extractArchiveJson,
|
|
902
931
|
parseArchiveArray,
|
|
932
|
+
getFirstEntry,
|
|
933
|
+
getMatchingEntries,
|
|
903
934
|
parseTwitterDate,
|
|
904
935
|
asRecord,
|
|
905
936
|
asArray,
|
|
@@ -908,6 +939,7 @@ export const __test__ = {
|
|
|
908
939
|
getTweetMediaCount,
|
|
909
940
|
extractTweetEntities,
|
|
910
941
|
extractTweetMedia,
|
|
942
|
+
extractCollectionTweet,
|
|
911
943
|
buildAccountPayload,
|
|
912
944
|
inferProfileFromDirectory,
|
|
913
945
|
};
|