birdclaw 0.2.1 → 0.3.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 +25 -1
- package/README.md +18 -7
- package/package.json +11 -10
- package/playwright.config.ts +5 -2
- package/src/cli.ts +113 -0
- package/src/lib/actions-transport.ts +58 -1
- package/src/lib/archive-import.ts +9 -2
- package/src/lib/backup.ts +5 -3
- package/src/lib/bird-actions.ts +4 -0
- package/src/lib/bird.ts +143 -17
- package/src/lib/config.ts +34 -1
- package/src/lib/db.ts +8 -0
- package/src/lib/mention-threads-live.ts +271 -0
- package/src/lib/moderation-target.ts +3 -1
- package/src/lib/profile-hydration.ts +8 -0
- package/src/lib/queries.ts +135 -15
- package/src/lib/research.ts +596 -0
- package/src/lib/seed.ts +8 -2
- package/src/lib/timeline-collections-live.ts +15 -1
- package/src/lib/timeline-live.ts +175 -0
- package/src/lib/tweet-lookup.ts +35 -0
- package/src/lib/types.ts +29 -1
- package/src/lib/x-profile.ts +41 -12
- package/src/lib/xurl.ts +35 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.3.0 - 2026-05-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add research mode for turning bookmarked Twitter threads into Markdown briefs, with shared `xurl`/`bird` tweet lookup fallback for thread expansion. Thanks @anupamchugh.
|
|
10
|
+
- Add live home timeline and mention-thread sync commands so local triage can pull current `bird` context into the SQLite store.
|
|
11
|
+
- Add search snippets for tweet and DM results, including deterministic DM snippets when multiple messages in a conversation match. Thanks @mvanhorn.
|
|
12
|
+
- Add `--min-likes` and `--quality-reason` controls for tweet search quality filtering. Thanks @mvanhorn.
|
|
13
|
+
- Store Twitter following counts on profiles and include them in JSONL backups.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Use the native TypeScript preview compiler for the `typecheck` script.
|
|
18
|
+
- Refresh TypeScript and related development dependencies.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Use the existing Twitter web cookie fallback as the final `auto` transport for block and unblock actions. Thanks @pejmanjohn.
|
|
23
|
+
- Resolve the `bird` transport from `PATH` before falling back to the local development checkout. Thanks @vyctorbrzezowski.
|
|
24
|
+
- Stabilize the presenter timestamp test across local time zones. Thanks @pejmanjohn.
|
|
25
|
+
- Clean up the DMs route render test so CI does not leave React work running after jsdom teardown.
|
|
26
|
+
- Allow Playwright e2e runs to use an alternate local port when `3000` is already occupied.
|
|
27
|
+
- Replace maintainer-local documentation links with repo-relative links and align the setup docs with the Node version file. Thanks @stainlu.
|
|
4
28
|
|
|
5
29
|
## 0.2.1 - 2026-04-27
|
|
6
30
|
|
package/README.md
CHANGED
|
@@ -203,7 +203,7 @@ pnpm cli search tweets --liked --limit 20 --json
|
|
|
203
203
|
pnpm cli search tweets --bookmarked --limit 20 --json
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
### Sync likes and
|
|
206
|
+
### Sync likes, bookmarks, and home timeline
|
|
207
207
|
|
|
208
208
|
`auto` tries `xurl` first, then falls back to `bird`. Use `bird` directly when the API path is unavailable for the account/token you have locally.
|
|
209
209
|
|
|
@@ -211,6 +211,8 @@ pnpm cli search tweets --bookmarked --limit 20 --json
|
|
|
211
211
|
pnpm cli sync likes --mode auto --limit 100 --refresh --json
|
|
212
212
|
pnpm cli sync bookmarks --mode auto --limit 100 --refresh --json
|
|
213
213
|
pnpm cli sync bookmarks --mode bird --all --max-pages 5 --limit 100 --refresh --json
|
|
214
|
+
pnpm cli sync timeline --limit 100 --refresh --json
|
|
215
|
+
pnpm cli sync mention-threads --limit 30 --delay-ms 1500 --timeout-ms 15000 --json
|
|
214
216
|
```
|
|
215
217
|
|
|
216
218
|
### Export mentions for agents
|
|
@@ -257,7 +259,16 @@ Notes:
|
|
|
257
259
|
- `actions.transport` accepts `auto`, `bird`, or `xurl`
|
|
258
260
|
- `bird` mode uses your local `bird` CLI and caches its mentions output into birdclaw's canonical store
|
|
259
261
|
- filters still work in `xurl` mode; filtered payloads are rebuilt from the local canonical store after sync
|
|
260
|
-
- `sync likes` and `sync
|
|
262
|
+
- `sync likes`, `sync bookmarks`, `sync timeline`, and `sync mention-threads` store live results in the same local timeline table, so `search tweets`, `search tweets --liked`, and `search tweets --bookmarked` work across archive and live data
|
|
263
|
+
|
|
264
|
+
### Research bookmarks and threads
|
|
265
|
+
|
|
266
|
+
`birdclaw research` turns bookmarked tweets into a markdown brief with local thread expansion, live ancestor lookup when needed, and extracted links/handles:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
birdclaw research "codex" --limit 20 --thread-depth 10 --json
|
|
270
|
+
birdclaw research --account acct_primary --out ~/research/codex.md
|
|
271
|
+
```
|
|
261
272
|
|
|
262
273
|
### Search and triage DMs
|
|
263
274
|
|
|
@@ -293,7 +304,7 @@ pnpm cli unban @amelia --account acct_primary --transport bird --json
|
|
|
293
304
|
Notes:
|
|
294
305
|
|
|
295
306
|
- `ban` / `unban` accept `--transport auto|bird|xurl`
|
|
296
|
-
- `auto` tries `bird` first, then falls back to `xurl` when
|
|
307
|
+
- `auto` tries `bird` first, then falls back to `xurl`, then `x-web` cookie-backed block/unblock when both fail
|
|
297
308
|
- forced `xurl` writes still verify through `bird status` before sqlite changes
|
|
298
309
|
- Twitter still rejects pure OAuth2 block writes, so `auto` is the safe default for block/unblock
|
|
299
310
|
- `blocks import` accepts newline-delimited blocklists with comments and markdown bullets
|
|
@@ -487,10 +498,10 @@ GitHub Actions runs:
|
|
|
487
498
|
- `pnpm build`
|
|
488
499
|
- `pnpm e2e`
|
|
489
500
|
|
|
490
|
-
Workflow: [ci.yml](
|
|
501
|
+
Workflow: [ci.yml](.github/workflows/ci.yml)
|
|
491
502
|
|
|
492
503
|
## Docs
|
|
493
504
|
|
|
494
|
-
- [spec.md](
|
|
495
|
-
- [cli.md](
|
|
496
|
-
- [data-architecture.md](
|
|
505
|
+
- [spec.md](docs/spec.md)
|
|
506
|
+
- [cli.md](docs/cli.md)
|
|
507
|
+
- [data-architecture.md](docs/data-architecture.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "birdclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Local Twitter memory in SQLite for archives, DMs, likes, bookmarks, and moderation",
|
|
5
5
|
"homepage": "https://github.com/steipete/birdclaw#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,22 +53,22 @@
|
|
|
53
53
|
"@tailwindcss/vite": "^4.2.4",
|
|
54
54
|
"@tanstack/devtools-vite": "0.6.0",
|
|
55
55
|
"@tanstack/react-devtools": "0.10.2",
|
|
56
|
-
"@tanstack/react-router": "1.
|
|
56
|
+
"@tanstack/react-router": "1.169.1",
|
|
57
57
|
"@tanstack/react-router-devtools": "1.166.13",
|
|
58
58
|
"@tanstack/react-router-ssr-query": "1.166.12",
|
|
59
|
-
"@tanstack/react-start": "1.167.
|
|
60
|
-
"@tanstack/router-plugin": "^1.167.
|
|
59
|
+
"@tanstack/react-start": "1.167.61",
|
|
60
|
+
"@tanstack/router-plugin": "^1.167.32",
|
|
61
61
|
"@vitejs/plugin-react": "^6.0.1",
|
|
62
62
|
"better-sqlite3": "^12.9.0",
|
|
63
63
|
"commander": "^14.0.3",
|
|
64
|
-
"kysely": "^0.28.
|
|
65
|
-
"lucide-react": "^1.
|
|
64
|
+
"kysely": "^0.28.17",
|
|
65
|
+
"lucide-react": "^1.14.0",
|
|
66
66
|
"react": "^19.2.5",
|
|
67
67
|
"react-dom": "^19.2.5",
|
|
68
68
|
"tailwindcss": "^4.2.4",
|
|
69
69
|
"tsx": "^4.21.0",
|
|
70
70
|
"vite": "^8.0.10",
|
|
71
|
-
"zod": "^4.
|
|
71
|
+
"zod": "^4.4.2"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@playwright/test": "^1.59.1",
|
|
@@ -80,10 +80,11 @@
|
|
|
80
80
|
"@types/node": "^25.6.0",
|
|
81
81
|
"@types/react": "^19.2.14",
|
|
82
82
|
"@types/react-dom": "^19.2.3",
|
|
83
|
+
"@typescript/native-preview": "7.0.0-dev.20260505.1",
|
|
83
84
|
"@vitest/coverage-v8": "^4.1.5",
|
|
84
|
-
"jsdom": "^29.1.
|
|
85
|
-
"oxfmt": "^0.
|
|
86
|
-
"oxlint": "^1.
|
|
85
|
+
"jsdom": "^29.1.1",
|
|
86
|
+
"oxfmt": "^0.47.0",
|
|
87
|
+
"oxlint": "^1.62.0",
|
|
87
88
|
"typescript": "^6.0.3",
|
|
88
89
|
"vitest": "^4.1.5"
|
|
89
90
|
},
|
package/playwright.config.ts
CHANGED
|
@@ -2,6 +2,8 @@ import path from "node:path";
|
|
|
2
2
|
import { defineConfig, devices } from "@playwright/test";
|
|
3
3
|
|
|
4
4
|
const testHome = path.join(process.cwd(), ".playwright-home");
|
|
5
|
+
const port = process.env.BIRDCLAW_PLAYWRIGHT_PORT ?? "3000";
|
|
6
|
+
const baseURL = `http://127.0.0.1:${port}`;
|
|
5
7
|
|
|
6
8
|
export default defineConfig({
|
|
7
9
|
testDir: "./playwright",
|
|
@@ -9,7 +11,7 @@ export default defineConfig({
|
|
|
9
11
|
retries: 0,
|
|
10
12
|
workers: 1,
|
|
11
13
|
use: {
|
|
12
|
-
baseURL
|
|
14
|
+
baseURL,
|
|
13
15
|
trace: "on-first-retry",
|
|
14
16
|
},
|
|
15
17
|
projects: [
|
|
@@ -20,10 +22,11 @@ export default defineConfig({
|
|
|
20
22
|
],
|
|
21
23
|
webServer: {
|
|
22
24
|
command: "node ./scripts/start-test-server.mjs",
|
|
23
|
-
url:
|
|
25
|
+
url: baseURL,
|
|
24
26
|
reuseExistingServer: false,
|
|
25
27
|
timeout: 120000,
|
|
26
28
|
env: {
|
|
29
|
+
BIRDCLAW_PLAYWRIGHT_PORT: port,
|
|
27
30
|
BIRDCLAW_HOME: testHome,
|
|
28
31
|
BIRDCLAW_DISABLE_LIVE_WRITES: "1",
|
|
29
32
|
},
|
package/src/cli.ts
CHANGED
|
@@ -29,6 +29,7 @@ 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,
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
} from "#/lib/mentions-live";
|
|
37
38
|
import { hydrateProfilesFromX } from "#/lib/profile-hydration";
|
|
38
39
|
import { inspectProfileReplies } from "#/lib/profile-replies";
|
|
40
|
+
import { runResearchMode } from "#/lib/research";
|
|
39
41
|
import {
|
|
40
42
|
createDmReply,
|
|
41
43
|
createPost,
|
|
@@ -48,6 +50,7 @@ import {
|
|
|
48
50
|
syncTimelineCollection,
|
|
49
51
|
type TimelineCollectionMode,
|
|
50
52
|
} from "#/lib/timeline-collections-live";
|
|
53
|
+
import { syncHomeTimeline } from "#/lib/timeline-live";
|
|
51
54
|
|
|
52
55
|
const program = new Command();
|
|
53
56
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -63,6 +66,35 @@ function print(data: unknown, asJson: boolean) {
|
|
|
63
66
|
console.log(data);
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
function printError(error: string) {
|
|
70
|
+
console.error(JSON.stringify({ error }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseNonNegativeIntegerOption(
|
|
74
|
+
value: string | undefined,
|
|
75
|
+
option: string,
|
|
76
|
+
) {
|
|
77
|
+
if (value === undefined) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
83
|
+
printError(`${option} must be a non-negative integer`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
89
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
90
|
+
printError(`${option} must be a non-negative integer`);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
function resolveActionOptions(options: { transport?: string }) {
|
|
67
99
|
return {
|
|
68
100
|
transport: options.transport as ActionsTransport | undefined,
|
|
@@ -171,10 +203,23 @@ searchCommand
|
|
|
171
203
|
.option("--until <date>", "Include tweets created before this date")
|
|
172
204
|
.option("--originals-only", "Exclude authored replies that start with @")
|
|
173
205
|
.option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise")
|
|
206
|
+
.option(
|
|
207
|
+
"--min-likes <n>",
|
|
208
|
+
"Override the low-quality like threshold (default 50)",
|
|
209
|
+
)
|
|
210
|
+
.option("--quality-reason", "Include qualityReason on each row")
|
|
174
211
|
.option("--liked", "Only liked tweets")
|
|
175
212
|
.option("--bookmarked", "Only bookmarked tweets")
|
|
176
213
|
.option("--limit <n>", "Limit results", "20")
|
|
177
214
|
.action(async (query, options) => {
|
|
215
|
+
const minLikes = parseNonNegativeIntegerOption(
|
|
216
|
+
options.minLikes,
|
|
217
|
+
"--min-likes",
|
|
218
|
+
);
|
|
219
|
+
if (options.minLikes !== undefined && minLikes === undefined) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
178
223
|
await autoUpdateBeforeRead();
|
|
179
224
|
const replyFilter = options.replied
|
|
180
225
|
? "replied"
|
|
@@ -189,6 +234,8 @@ searchCommand
|
|
|
189
234
|
until: options.until,
|
|
190
235
|
includeReplies: !options.originalsOnly,
|
|
191
236
|
qualityFilter: options.hideLowQuality ? "summary" : "all",
|
|
237
|
+
lowQualityThreshold: minLikes,
|
|
238
|
+
includeQualityReason: Boolean(options.qualityReason),
|
|
192
239
|
likedOnly: Boolean(options.liked),
|
|
193
240
|
bookmarkedOnly: Boolean(options.bookmarked),
|
|
194
241
|
limit: Number(options.limit),
|
|
@@ -236,6 +283,28 @@ searchCommand
|
|
|
236
283
|
print(items, program.opts().json ?? false);
|
|
237
284
|
});
|
|
238
285
|
|
|
286
|
+
program
|
|
287
|
+
.command("research [query]")
|
|
288
|
+
.description("Build a markdown research brief from bookmarked threads")
|
|
289
|
+
.option("--account <accountId>", "Account id")
|
|
290
|
+
.option("--limit <n>", "Seed bookmark limit", "20")
|
|
291
|
+
.option("--thread-depth <n>", "Maximum ancestor walk depth", "10")
|
|
292
|
+
.option("--out <path>", "Write the markdown brief to a file")
|
|
293
|
+
.action(async (query, options) => {
|
|
294
|
+
await autoUpdateBeforeRead();
|
|
295
|
+
const report = await runResearchMode({
|
|
296
|
+
account: options.account,
|
|
297
|
+
query,
|
|
298
|
+
limit: Number(options.limit),
|
|
299
|
+
maxThreadDepth: Number(options.threadDepth),
|
|
300
|
+
outPath: options.out,
|
|
301
|
+
});
|
|
302
|
+
print(
|
|
303
|
+
program.opts().json ? report : report.markdown,
|
|
304
|
+
program.opts().json ?? false,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
239
308
|
const mentionsCommand = program
|
|
240
309
|
.command("mentions")
|
|
241
310
|
.description("Export local mention tweets for scripts and agents");
|
|
@@ -325,6 +394,50 @@ const syncCommand = program
|
|
|
325
394
|
.command("sync")
|
|
326
395
|
.description("Refresh live Twitter collections into the local store");
|
|
327
396
|
|
|
397
|
+
syncCommand
|
|
398
|
+
.command("timeline")
|
|
399
|
+
.description("Refresh live home timeline through bird")
|
|
400
|
+
.option("--account <accountId>", "Account id")
|
|
401
|
+
.option("--limit <n>", "Result limit", "100")
|
|
402
|
+
.option("--for-you", 'Fetch "For You" instead of chronological Following')
|
|
403
|
+
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
404
|
+
.option("--refresh", "Bypass live-cache freshness window")
|
|
405
|
+
.action(async (options) => {
|
|
406
|
+
const result = await syncHomeTimeline({
|
|
407
|
+
account: options.account,
|
|
408
|
+
limit: Number(options.limit),
|
|
409
|
+
following: !options.forYou,
|
|
410
|
+
refresh: Boolean(options.refresh),
|
|
411
|
+
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
412
|
+
});
|
|
413
|
+
await autoSyncAfterWrite();
|
|
414
|
+
print(result, true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
syncCommand
|
|
418
|
+
.command("mention-threads")
|
|
419
|
+
.description(
|
|
420
|
+
"Fetch tweet conversation context for recent mentions through bird",
|
|
421
|
+
)
|
|
422
|
+
.option("--account <accountId>", "Account id")
|
|
423
|
+
.option("--limit <n>", "Recent mentions to inspect", "30")
|
|
424
|
+
.option("--delay-ms <n>", "Delay between thread fetches", "1500")
|
|
425
|
+
.option("--timeout-ms <n>", "Per-thread timeout", "15000")
|
|
426
|
+
.option("--all", "Fetch all retrievable thread pages")
|
|
427
|
+
.option("--max-pages <n>", "Stop after N pages")
|
|
428
|
+
.action(async (options) => {
|
|
429
|
+
const result = await syncMentionThreads({
|
|
430
|
+
account: options.account,
|
|
431
|
+
limit: Number(options.limit),
|
|
432
|
+
delayMs: Number(options.delayMs),
|
|
433
|
+
timeoutMs: Number(options.timeoutMs),
|
|
434
|
+
all: Boolean(options.all),
|
|
435
|
+
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
436
|
+
});
|
|
437
|
+
await autoSyncAfterWrite();
|
|
438
|
+
print(result, true);
|
|
439
|
+
});
|
|
440
|
+
|
|
328
441
|
for (const kind of ["likes", "bookmarks"] as const) {
|
|
329
442
|
syncCommand
|
|
330
443
|
.command(kind)
|
|
@@ -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: [
|
|
@@ -476,6 +476,7 @@ export async function importArchive(
|
|
|
476
476
|
displayName: string;
|
|
477
477
|
bio: string;
|
|
478
478
|
followersCount: number;
|
|
479
|
+
followingCount: number;
|
|
479
480
|
avatarHue: number;
|
|
480
481
|
createdAt: string;
|
|
481
482
|
}
|
|
@@ -500,6 +501,7 @@ export async function importArchive(
|
|
|
500
501
|
displayName: accountPayload.displayName,
|
|
501
502
|
bio: accountPayload.bio,
|
|
502
503
|
followersCount: 0,
|
|
504
|
+
followingCount: 0,
|
|
503
505
|
avatarHue: 18,
|
|
504
506
|
createdAt: accountPayload.createdAt,
|
|
505
507
|
};
|
|
@@ -583,6 +585,7 @@ export async function importArchive(
|
|
|
583
585
|
conversationName || `Group DM ${externalParticipantIds.length}`,
|
|
584
586
|
bio: `Group DM with ${externalParticipantIds.length} participants`,
|
|
585
587
|
followersCount: 0,
|
|
588
|
+
followingCount: 0,
|
|
586
589
|
avatarHue: 220,
|
|
587
590
|
createdAt: accountPayload.createdAt,
|
|
588
591
|
});
|
|
@@ -598,6 +601,7 @@ export async function importArchive(
|
|
|
598
601
|
displayName: inferred.displayName,
|
|
599
602
|
bio: `Imported from archive user ${otherUserId}`,
|
|
600
603
|
followersCount: 0,
|
|
604
|
+
followingCount: 0,
|
|
601
605
|
avatarHue: 210,
|
|
602
606
|
createdAt: accountPayload.createdAt,
|
|
603
607
|
});
|
|
@@ -626,6 +630,7 @@ export async function importArchive(
|
|
|
626
630
|
displayName: inferred.displayName,
|
|
627
631
|
bio: `Imported from archive user ${senderId}`,
|
|
628
632
|
followersCount: 0,
|
|
633
|
+
followingCount: 0,
|
|
629
634
|
avatarHue: 240,
|
|
630
635
|
createdAt: accountPayload.createdAt,
|
|
631
636
|
});
|
|
@@ -745,6 +750,7 @@ export async function importArchive(
|
|
|
745
750
|
displayName: "Unknown",
|
|
746
751
|
bio: "Imported from archive collection metadata",
|
|
747
752
|
followersCount: 0,
|
|
753
|
+
followingCount: 0,
|
|
748
754
|
avatarHue: 210,
|
|
749
755
|
createdAt: accountPayload.createdAt,
|
|
750
756
|
});
|
|
@@ -758,8 +764,8 @@ export async function importArchive(
|
|
|
758
764
|
values (?, ?, ?, ?, ?, 1, ?)
|
|
759
765
|
`);
|
|
760
766
|
const insertProfile = db.prepare(`
|
|
761
|
-
insert into profiles (id, handle, display_name, bio, followers_count, avatar_hue, avatar_url, created_at)
|
|
762
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
767
|
+
insert into profiles (id, handle, display_name, bio, followers_count, following_count, avatar_hue, avatar_url, created_at)
|
|
768
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
763
769
|
`);
|
|
764
770
|
const insertTweet = db.prepare(`
|
|
765
771
|
insert into tweets (
|
|
@@ -811,6 +817,7 @@ export async function importArchive(
|
|
|
811
817
|
profile.displayName,
|
|
812
818
|
profile.bio,
|
|
813
819
|
profile.followersCount,
|
|
820
|
+
profile.followingCount,
|
|
814
821
|
profile.avatarHue,
|
|
815
822
|
null,
|
|
816
823
|
profile.createdAt,
|
package/src/lib/backup.ts
CHANGED
|
@@ -188,7 +188,7 @@ function getExportRowSets(db: Database.Database) {
|
|
|
188
188
|
rows: rowsForQuery(
|
|
189
189
|
db,
|
|
190
190
|
`
|
|
191
|
-
select id, handle, display_name, bio, followers_count, avatar_hue, avatar_url, created_at
|
|
191
|
+
select id, handle, display_name, bio, followers_count, following_count, avatar_hue, avatar_url, created_at
|
|
192
192
|
from profiles
|
|
193
193
|
order by id
|
|
194
194
|
`,
|
|
@@ -1002,13 +1002,14 @@ export async function importBackup({
|
|
|
1002
1002
|
db,
|
|
1003
1003
|
`
|
|
1004
1004
|
insert into profiles (
|
|
1005
|
-
id, handle, display_name, bio, followers_count, avatar_hue, avatar_url, created_at
|
|
1006
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1005
|
+
id, handle, display_name, bio, followers_count, following_count, avatar_hue, avatar_url, created_at
|
|
1006
|
+
) values (?, ?, ?, ?, ?, coalesce(?, 0), ?, ?, ?)
|
|
1007
1007
|
on conflict(id) do update set
|
|
1008
1008
|
handle = coalesce(nullif(excluded.handle, ''), profiles.handle),
|
|
1009
1009
|
display_name = coalesce(nullif(excluded.display_name, ''), profiles.display_name),
|
|
1010
1010
|
bio = coalesce(nullif(excluded.bio, ''), profiles.bio),
|
|
1011
1011
|
followers_count = max(profiles.followers_count, excluded.followers_count),
|
|
1012
|
+
following_count = max(profiles.following_count, excluded.following_count),
|
|
1012
1013
|
avatar_hue = case when profiles.avatar_hue = 0 then excluded.avatar_hue else profiles.avatar_hue end,
|
|
1013
1014
|
avatar_url = coalesce(excluded.avatar_url, profiles.avatar_url),
|
|
1014
1015
|
created_at = min(profiles.created_at, excluded.created_at)
|
|
@@ -1020,6 +1021,7 @@ export async function importBackup({
|
|
|
1020
1021
|
"display_name",
|
|
1021
1022
|
"bio",
|
|
1022
1023
|
"followers_count",
|
|
1024
|
+
"following_count",
|
|
1023
1025
|
"avatar_hue",
|
|
1024
1026
|
"avatar_url",
|
|
1025
1027
|
"created_at",
|
package/src/lib/bird-actions.ts
CHANGED
|
@@ -80,6 +80,7 @@ function toBirdLookupUser(payload: Record<string, unknown>): XurlMentionUser {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const followersCount = Number(user.followersCount ?? 0);
|
|
83
|
+
const followingCount = Number(user.followingCount);
|
|
83
84
|
return {
|
|
84
85
|
id,
|
|
85
86
|
name:
|
|
@@ -95,6 +96,9 @@ function toBirdLookupUser(payload: Record<string, unknown>): XurlMentionUser {
|
|
|
95
96
|
: undefined,
|
|
96
97
|
public_metrics: {
|
|
97
98
|
followers_count: Number.isFinite(followersCount) ? followersCount : 0,
|
|
99
|
+
...(Number.isFinite(followingCount)
|
|
100
|
+
? { following_count: followingCount }
|
|
101
|
+
: {}),
|
|
98
102
|
},
|
|
99
103
|
created_at: typeof user.createdAt === "string" ? user.createdAt : undefined,
|
|
100
104
|
};
|