birdclaw 0.8.2 → 0.8.3
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 +16 -0
- package/package.json +2 -1
- package/src/cli/command-context.ts +17 -0
- package/src/cli/register-analysis.ts +500 -0
- package/src/cli/register-compose.ts +40 -0
- package/src/cli/register-graph.ts +132 -0
- package/src/cli/register-inbox.ts +41 -0
- package/src/cli/register-storage.ts +106 -0
- package/src/cli.ts +30 -750
- package/src/components/AccountSwitcher.tsx +7 -15
- package/src/components/AvatarChip.tsx +1 -1
- package/src/components/AvatarPreload.ts +149 -0
- package/src/components/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +8 -674
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +2 -0
- package/src/components/links-controller.ts +162 -0
- package/src/components/links-model.ts +198 -0
- package/src/components/network-map-controller.ts +84 -0
- package/src/components/network-map-model.ts +255 -0
- package/src/components/useTimelineRouteData.ts +105 -235
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -215
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +75 -120
- package/src/lib/backup.ts +335 -424
- package/src/lib/blocks-write.ts +30 -26
- package/src/lib/blocks.ts +18 -20
- package/src/lib/database-metrics.ts +88 -0
- package/src/lib/database-migrations.ts +34 -0
- package/src/lib/database-schema.ts +312 -0
- package/src/lib/database-writer.ts +69 -0
- package/src/lib/db.ts +84 -330
- package/src/lib/dm-read-model.ts +533 -0
- package/src/lib/dms-live.ts +34 -97
- package/src/lib/follow-graph.ts +17 -27
- package/src/lib/import-repository.ts +138 -0
- package/src/lib/inbox.ts +2 -1
- package/src/lib/live-sync-engine.ts +209 -0
- package/src/lib/live-transport-gateway.ts +128 -0
- package/src/lib/mention-threads-live.ts +90 -177
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -181
- package/src/lib/moderation-target.ts +15 -4
- package/src/lib/moderation-write.ts +1 -1
- package/src/lib/mutes-write.ts +30 -26
- package/src/lib/openai-response-runtime.ts +251 -0
- package/src/lib/paginated-sync.ts +93 -0
- package/src/lib/period-digest.ts +116 -304
- package/src/lib/profile-analysis.ts +36 -110
- package/src/lib/queries.ts +6 -2381
- package/src/lib/query-actions.ts +437 -0
- package/src/lib/query-client.tsx +47 -0
- package/src/lib/query-read-model-shared.ts +52 -0
- package/src/lib/query-read-models.ts +5 -0
- package/src/lib/query-resource.ts +41 -0
- package/src/lib/query-status.ts +164 -0
- package/src/lib/research.ts +1 -1
- package/src/lib/runtime-services.ts +20 -0
- package/src/lib/search-discussion.ts +75 -279
- package/src/lib/server-runtime-services.ts +30 -0
- package/src/lib/sqlite.ts +48 -12
- package/src/lib/streaming-ingestion.ts +240 -0
- package/src/lib/sync-cache.ts +6 -1
- package/src/lib/sync-plan.ts +175 -0
- package/src/lib/timeline-collections-live.ts +83 -257
- package/src/lib/timeline-live.ts +86 -236
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -167
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/routes/__root.tsx +11 -8
- package/src/routes/api/action.tsx +1 -1
- package/src/routes/api/conversation.tsx +1 -1
- package/src/routes/api/query.tsx +32 -26
- package/src/routes/api/status.tsx +6 -4
- package/src/routes/api/sync.tsx +5 -2
- package/src/routes/blocks.tsx +97 -131
- package/src/routes/data-sources.tsx +17 -25
- package/src/routes/dms.tsx +167 -184
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -394
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/src/lib/client-cache.ts +0 -109
package/src/lib/dms-live.ts
CHANGED
|
@@ -4,17 +4,18 @@ import {
|
|
|
4
4
|
type BirdDmConversation,
|
|
5
5
|
type BirdDmEvent,
|
|
6
6
|
type BirdDmUser,
|
|
7
|
-
getAuthenticatedBirdAccountEffect,
|
|
8
|
-
listDirectMessagesViaBirdEffect,
|
|
9
7
|
} from "./bird";
|
|
10
8
|
import { getNativeDb } from "./db";
|
|
11
9
|
import { runEffectPromise } from "./effect-runtime";
|
|
10
|
+
import { liveTransportGateway } from "./live-transport-gateway";
|
|
11
|
+
import {
|
|
12
|
+
assertLiveAccountMatches,
|
|
13
|
+
resolveLiveSyncAccount,
|
|
14
|
+
type LiveSyncAccount,
|
|
15
|
+
} from "./live-sync-engine";
|
|
12
16
|
import { readSyncCache, writeSyncCache } from "./sync-cache";
|
|
17
|
+
import { runSyncPlanEffect } from "./sync-plan";
|
|
13
18
|
import type { XurlDmEventsResponse, XurlMentionUser } from "./types";
|
|
14
|
-
import {
|
|
15
|
-
listDirectMessageEventsViaXurlEffect,
|
|
16
|
-
lookupAuthenticatedOAuth2UserEffect,
|
|
17
|
-
} from "./xurl";
|
|
18
19
|
import {
|
|
19
20
|
buildExternalProfileId,
|
|
20
21
|
randomAvatarHue,
|
|
@@ -65,11 +66,6 @@ function parseSyncMode(mode: DirectMessagesSyncMode | undefined) {
|
|
|
65
66
|
throw new Error("--mode must be auto, bird, or xurl");
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
function normalizeExternalUserId(value: string | null | undefined) {
|
|
69
|
-
const trimmed = value?.trim();
|
|
70
|
-
return trimmed ? trimmed : undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
69
|
function makePreviewMessageId(conversationId: string): string {
|
|
74
70
|
return `${PREVIEW_MESSAGE_ID_PREFIX}${conversationId}`;
|
|
75
71
|
}
|
|
@@ -85,39 +81,6 @@ function deleteDmFtsRows(db: Database, messageIds: string[]) {
|
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
function resolveAccount(db: Database, accountId?: string) {
|
|
89
|
-
const row = accountId
|
|
90
|
-
? (db
|
|
91
|
-
.prepare(
|
|
92
|
-
"select id, handle, external_user_id from accounts where id = ?",
|
|
93
|
-
)
|
|
94
|
-
.get(accountId) as
|
|
95
|
-
| { id: string; handle: string; external_user_id: string | null }
|
|
96
|
-
| undefined)
|
|
97
|
-
: (db
|
|
98
|
-
.prepare(
|
|
99
|
-
`
|
|
100
|
-
select id, handle, external_user_id
|
|
101
|
-
from accounts
|
|
102
|
-
order by is_default desc, created_at asc
|
|
103
|
-
limit 1
|
|
104
|
-
`,
|
|
105
|
-
)
|
|
106
|
-
.get() as
|
|
107
|
-
| { id: string; handle: string; external_user_id: string | null }
|
|
108
|
-
| undefined);
|
|
109
|
-
|
|
110
|
-
if (!row) {
|
|
111
|
-
throw new Error(`Unknown account: ${accountId ?? "default"}`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
accountId: row.id,
|
|
116
|
-
username: row.handle.replace(/^@/, ""),
|
|
117
|
-
externalUserId: normalizeExternalUserId(row.external_user_id),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
84
|
function toIsoTimestamp(value?: string) {
|
|
122
85
|
if (!value) {
|
|
123
86
|
return new Date().toISOString();
|
|
@@ -202,36 +165,21 @@ function getLatestEvent(events: BirdDmEvent[]) {
|
|
|
202
165
|
|
|
203
166
|
function assertAuthenticatedBirdAccountMatches({
|
|
204
167
|
source,
|
|
205
|
-
|
|
206
|
-
username,
|
|
207
|
-
externalUserId,
|
|
168
|
+
account,
|
|
208
169
|
liveUsername,
|
|
209
170
|
liveExternalUserId,
|
|
210
171
|
}: {
|
|
211
172
|
source: "bird" | "xurl";
|
|
212
|
-
|
|
213
|
-
username: string;
|
|
214
|
-
externalUserId?: string;
|
|
173
|
+
account: LiveSyncAccount;
|
|
215
174
|
liveUsername: string;
|
|
216
175
|
liveExternalUserId?: string;
|
|
217
176
|
}) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
if (externalUserId && liveExternalUserId) {
|
|
226
|
-
throw new Error(
|
|
227
|
-
`${source} is authenticated as user ${liveExternalUserId}; refusing to sync into ${accountId} (${externalUserId})`,
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
if (liveUsername.toLowerCase() !== username.toLowerCase()) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`${source} is authenticated as @${liveUsername}; refusing to sync into ${accountId} (@${username})`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
177
|
+
assertLiveAccountMatches({
|
|
178
|
+
source,
|
|
179
|
+
account,
|
|
180
|
+
liveUsername,
|
|
181
|
+
liveExternalUserId,
|
|
182
|
+
});
|
|
235
183
|
}
|
|
236
184
|
|
|
237
185
|
function getAuthenticatedXurlAccount(payload: Record<string, unknown> | null): {
|
|
@@ -707,10 +655,6 @@ function mergeXurlDmPages(pages: XurlDmEventsResponse[]): XurlDmEventsResponse {
|
|
|
707
655
|
};
|
|
708
656
|
}
|
|
709
657
|
|
|
710
|
-
function sleepEffect(ms: number | undefined) {
|
|
711
|
-
return typeof ms === "number" && ms > 0 ? Effect.sleep(ms) : Effect.void;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
658
|
function fetchDirectMessagesViaXurlEffect({
|
|
715
659
|
limit,
|
|
716
660
|
username,
|
|
@@ -725,25 +669,21 @@ function fetchDirectMessagesViaXurlEffect({
|
|
|
725
669
|
pageDelayMs?: number;
|
|
726
670
|
}) {
|
|
727
671
|
return Effect.gen(function* () {
|
|
728
|
-
const pages: XurlDmEventsResponse[] = [];
|
|
729
|
-
let paginationToken: string | undefined;
|
|
730
|
-
let pageIndex = 0;
|
|
731
672
|
const pageLimit = allPages
|
|
732
673
|
? Number.POSITIVE_INFINITY
|
|
733
674
|
: Math.max(1, (maxPages ?? 0) + 1);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return mergeXurlDmPages(pages);
|
|
675
|
+
const result = yield* runSyncPlanEffect({
|
|
676
|
+
fetchPage: ({ cursor }) =>
|
|
677
|
+
liveTransportGateway.xurl.listDirectMessages({
|
|
678
|
+
maxResults: limit,
|
|
679
|
+
username,
|
|
680
|
+
...(cursor ? { paginationToken: cursor } : {}),
|
|
681
|
+
}),
|
|
682
|
+
getNextCursor: (page) => getMetaNextToken(page.meta),
|
|
683
|
+
maxPages: pageLimit,
|
|
684
|
+
pageDelayMs,
|
|
685
|
+
});
|
|
686
|
+
return mergeXurlDmPages(result.pages);
|
|
747
687
|
});
|
|
748
688
|
}
|
|
749
689
|
|
|
@@ -780,7 +720,7 @@ export function syncDirectMessagesViaCachedBirdEffect({
|
|
|
780
720
|
);
|
|
781
721
|
}
|
|
782
722
|
const db = getNativeDb();
|
|
783
|
-
const resolvedAccount =
|
|
723
|
+
const resolvedAccount = resolveLiveSyncAccount(db, account);
|
|
784
724
|
const pageKey = allPages
|
|
785
725
|
? "all-pages"
|
|
786
726
|
: `max-pages:${String(maxPages ?? 0)}`;
|
|
@@ -813,7 +753,7 @@ export function syncDirectMessagesViaCachedBirdEffect({
|
|
|
813
753
|
if (tryXurl) {
|
|
814
754
|
const xurlPayload = yield* Effect.gen(function* () {
|
|
815
755
|
const authenticated = getAuthenticatedXurlAccount(
|
|
816
|
-
yield*
|
|
756
|
+
yield* liveTransportGateway.xurl.lookupAuthenticatedOAuth2User(
|
|
817
757
|
resolvedAccount.username,
|
|
818
758
|
),
|
|
819
759
|
);
|
|
@@ -824,9 +764,7 @@ export function syncDirectMessagesViaCachedBirdEffect({
|
|
|
824
764
|
}
|
|
825
765
|
assertAuthenticatedBirdAccountMatches({
|
|
826
766
|
source: "xurl",
|
|
827
|
-
|
|
828
|
-
username: resolvedAccount.username,
|
|
829
|
-
externalUserId: resolvedAccount.externalUserId,
|
|
767
|
+
account: resolvedAccount,
|
|
830
768
|
liveUsername: authenticated.username ?? resolvedAccount.username,
|
|
831
769
|
liveExternalUserId: authenticated.id,
|
|
832
770
|
});
|
|
@@ -874,12 +812,11 @@ export function syncDirectMessagesViaCachedBirdEffect({
|
|
|
874
812
|
}
|
|
875
813
|
}
|
|
876
814
|
if (!payload) {
|
|
877
|
-
const authenticated =
|
|
815
|
+
const authenticated =
|
|
816
|
+
yield* liveTransportGateway.bird.getAuthenticatedAccount();
|
|
878
817
|
assertAuthenticatedBirdAccountMatches({
|
|
879
818
|
source: "bird",
|
|
880
|
-
|
|
881
|
-
username: resolvedAccount.username,
|
|
882
|
-
externalUserId: resolvedAccount.externalUserId,
|
|
819
|
+
account: resolvedAccount,
|
|
883
820
|
liveUsername: authenticated.username,
|
|
884
821
|
liveExternalUserId: authenticated.id,
|
|
885
822
|
});
|
|
@@ -891,7 +828,7 @@ export function syncDirectMessagesViaCachedBirdEffect({
|
|
|
891
828
|
accountExternalUserId,
|
|
892
829
|
);
|
|
893
830
|
}
|
|
894
|
-
payload = yield*
|
|
831
|
+
payload = yield* liveTransportGateway.bird.listDirectMessages({
|
|
895
832
|
maxResults: limit,
|
|
896
833
|
...(inbox !== "all" ? { inbox } : {}),
|
|
897
834
|
...(typeof maxPages === "number" ? { maxPages } : {}),
|
package/src/lib/follow-graph.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import { getNativeDb } from "./db";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { runEffectPromise } from "./effect-runtime";
|
|
5
|
+
import { liveTransportGateway } from "./live-transport-gateway";
|
|
6
6
|
import type { Database } from "./sqlite";
|
|
7
7
|
import { readSyncCache, writeSyncCache } from "./sync-cache";
|
|
8
|
+
import { runSyncPlanEffect } from "./sync-plan";
|
|
8
9
|
import type {
|
|
9
10
|
FollowEventKind,
|
|
10
11
|
FollowDirection,
|
|
@@ -16,7 +17,6 @@ import type {
|
|
|
16
17
|
XurlPublicMetrics,
|
|
17
18
|
} from "./types";
|
|
18
19
|
import { upsertProfileFromXUser } from "./x-profile";
|
|
19
|
-
import { listFollowUsersViaXurl } from "./xurl";
|
|
20
20
|
|
|
21
21
|
const DEFAULT_FOLLOW_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
22
22
|
const DEFAULT_FOLLOW_PAGE_LIMIT = 1000;
|
|
@@ -267,35 +267,25 @@ function fetchFollowGraphViaXurlEffect({
|
|
|
267
267
|
maxResources?: number;
|
|
268
268
|
}): Effect.Effect<MergedFollowPayload, unknown> {
|
|
269
269
|
return Effect.gen(function* () {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let collectedCount = 0;
|
|
274
|
-
|
|
275
|
-
do {
|
|
276
|
-
const payload = yield* tryPromise(() =>
|
|
277
|
-
listFollowUsersViaXurl({
|
|
270
|
+
const result = yield* runSyncPlanEffect({
|
|
271
|
+
fetchPage: ({ cursor }) =>
|
|
272
|
+
liveTransportGateway.xurl.listFollowUsers({
|
|
278
273
|
direction,
|
|
279
274
|
username,
|
|
280
275
|
userId,
|
|
281
276
|
maxResults: limit,
|
|
282
|
-
paginationToken:
|
|
277
|
+
...(cursor ? { paginationToken: cursor } : {}),
|
|
283
278
|
}),
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
} while (
|
|
293
|
-
nextToken &&
|
|
294
|
-
(maxPages === undefined || pageCount < maxPages) &&
|
|
295
|
-
(maxResources === undefined || collectedCount < maxResources)
|
|
296
|
-
);
|
|
279
|
+
getItemCount: (page) => page.data.length,
|
|
280
|
+
getNextCursor: (page) =>
|
|
281
|
+
typeof page.meta?.next_token === "string"
|
|
282
|
+
? String(page.meta.next_token)
|
|
283
|
+
: undefined,
|
|
284
|
+
maxItems: maxResources,
|
|
285
|
+
maxPages,
|
|
286
|
+
});
|
|
297
287
|
|
|
298
|
-
return mergePages(pages,
|
|
288
|
+
return mergePages(result.pages, result.nextCursor, maxResources);
|
|
299
289
|
});
|
|
300
290
|
}
|
|
301
291
|
|
|
@@ -321,7 +311,7 @@ function fetchFollowGraphViaBirdEffect({
|
|
|
321
311
|
maxPages ?? Number.POSITIVE_INFINITY,
|
|
322
312
|
Math.ceil(maxResources / birdLimit),
|
|
323
313
|
);
|
|
324
|
-
const payload = yield*
|
|
314
|
+
const payload = yield* liveTransportGateway.bird.listFollowUsers({
|
|
325
315
|
direction,
|
|
326
316
|
userId,
|
|
327
317
|
maxResults: Math.min(birdLimit, maxResources ?? birdLimit),
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
|
+
|
|
3
|
+
export type ImportRow = Record<string, unknown>;
|
|
4
|
+
export type ImportFtsTable =
|
|
5
|
+
| { table: "tweets_fts"; idColumn: "tweet_id" }
|
|
6
|
+
| { table: "dm_fts"; idColumn: "message_id" };
|
|
7
|
+
|
|
8
|
+
export class ImportRepository {
|
|
9
|
+
constructor(readonly db: Database) {}
|
|
10
|
+
|
|
11
|
+
readRows<T>(sql: string, ...parameters: unknown[]) {
|
|
12
|
+
return this.db.prepare(sql).all(...parameters) as T[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
readRow<T>(sql: string, ...parameters: unknown[]) {
|
|
16
|
+
return this.db.prepare(sql).get(...parameters) as T | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
insertRows(sql: string, rows: readonly ImportRow[], keys: readonly string[]) {
|
|
20
|
+
const statement = this.db.prepare(sql);
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
statement.run(...keys.map((key) => row[key] ?? null));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
readFtsIds({ table, idColumn }: ImportFtsTable) {
|
|
27
|
+
const rows = this.db
|
|
28
|
+
.prepare(`select ${idColumn} as id from ${table}`)
|
|
29
|
+
.all() as { id: string }[];
|
|
30
|
+
return new Set(rows.map((row) => row.id));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
insertFtsRows({
|
|
34
|
+
target,
|
|
35
|
+
rows,
|
|
36
|
+
idKey,
|
|
37
|
+
textKey,
|
|
38
|
+
existingIds = new Set<string>(),
|
|
39
|
+
}: {
|
|
40
|
+
target: ImportFtsTable;
|
|
41
|
+
rows: readonly ImportRow[];
|
|
42
|
+
idKey: string;
|
|
43
|
+
textKey: string;
|
|
44
|
+
existingIds?: Set<string>;
|
|
45
|
+
}) {
|
|
46
|
+
const statement = this.db.prepare(
|
|
47
|
+
`insert into ${target.table} (${target.idColumn}, text) values (?, ?)`,
|
|
48
|
+
);
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
const id = row[idKey];
|
|
51
|
+
if (typeof id !== "string" || existingIds.has(id)) continue;
|
|
52
|
+
const text = row[textKey];
|
|
53
|
+
statement.run(id, typeof text === "string" ? text : "");
|
|
54
|
+
existingIds.add(id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clearArchiveImport() {
|
|
59
|
+
this.db.exec(`
|
|
60
|
+
delete from ai_scores;
|
|
61
|
+
delete from tweet_actions;
|
|
62
|
+
delete from tweet_account_edges;
|
|
63
|
+
delete from tweet_collections;
|
|
64
|
+
delete from link_occurrences;
|
|
65
|
+
delete from url_expansions;
|
|
66
|
+
delete from dm_fts;
|
|
67
|
+
delete from tweets_fts;
|
|
68
|
+
delete from dm_messages;
|
|
69
|
+
delete from dm_conversations;
|
|
70
|
+
delete from tweets;
|
|
71
|
+
delete from profiles;
|
|
72
|
+
delete from accounts;
|
|
73
|
+
`);
|
|
74
|
+
this.clearAuthoredSyncCursors();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
clearAuthoredSyncCursors(accountId?: string) {
|
|
78
|
+
if (accountId) {
|
|
79
|
+
this.db
|
|
80
|
+
.prepare("delete from sync_cache where cache_key = ?")
|
|
81
|
+
.run(`authored:xurl:${accountId}:cursor`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.db
|
|
85
|
+
.prepare(
|
|
86
|
+
"delete from sync_cache where cache_key like 'authored:xurl:%:cursor'",
|
|
87
|
+
)
|
|
88
|
+
.run();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
clearMentionSyncState() {
|
|
92
|
+
this.db
|
|
93
|
+
.prepare("delete from sync_cache where cache_key like 'mentions:sync:%'")
|
|
94
|
+
.run();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
clearBackupImport() {
|
|
98
|
+
this.db.exec(`
|
|
99
|
+
delete from follow_events;
|
|
100
|
+
delete from follow_edges;
|
|
101
|
+
delete from follow_snapshot_members;
|
|
102
|
+
delete from follow_snapshots;
|
|
103
|
+
delete from ai_scores;
|
|
104
|
+
delete from tweet_actions;
|
|
105
|
+
delete from tweet_account_edges;
|
|
106
|
+
delete from tweet_collections;
|
|
107
|
+
delete from link_occurrences;
|
|
108
|
+
delete from url_expansions;
|
|
109
|
+
delete from blocks;
|
|
110
|
+
delete from mutes;
|
|
111
|
+
delete from dm_fts;
|
|
112
|
+
delete from tweets_fts;
|
|
113
|
+
delete from dm_messages;
|
|
114
|
+
delete from dm_conversations;
|
|
115
|
+
delete from tweets;
|
|
116
|
+
delete from profile_bio_entities;
|
|
117
|
+
delete from profile_snapshots;
|
|
118
|
+
delete from profile_affiliations;
|
|
119
|
+
delete from profiles;
|
|
120
|
+
delete from accounts;
|
|
121
|
+
delete from sync_cache;
|
|
122
|
+
`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let repositories = new WeakMap<Database, ImportRepository>();
|
|
127
|
+
|
|
128
|
+
export function getImportRepository(db: Database) {
|
|
129
|
+
const existing = repositories.get(db);
|
|
130
|
+
if (existing) return existing;
|
|
131
|
+
const repository = new ImportRepository(db);
|
|
132
|
+
repositories.set(db, repository);
|
|
133
|
+
return repository;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function resetImportRepositoriesForTests() {
|
|
137
|
+
repositories = new WeakMap();
|
|
138
|
+
}
|
package/src/lib/inbox.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { Effect } from "effect";
|
|
|
2
2
|
import { getNativeDb } from "./db";
|
|
3
3
|
import { runEffectPromise } from "./effect-runtime";
|
|
4
4
|
import { scoreInboxItemWithOpenAIEffect } from "./openai";
|
|
5
|
-
import { listDmConversations
|
|
5
|
+
import { listDmConversations } from "./dm-read-model";
|
|
6
|
+
import { listTimelineItems } from "./timeline-read-model";
|
|
6
7
|
import type { InboxItem, InboxQuery, InboxResponse } from "./types";
|
|
7
8
|
|
|
8
9
|
function heuristicSummary(kind: InboxItem["entityKind"]) {
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { databaseWriteEffect } from "./database-writer";
|
|
3
|
+
import type { Database } from "./sqlite";
|
|
4
|
+
import { readSyncCache, writeSyncCache } from "./sync-cache";
|
|
5
|
+
|
|
6
|
+
export interface LiveTransportAdapter<Source extends string, Payload> {
|
|
7
|
+
source: Source;
|
|
8
|
+
fetch: Effect.Effect<Payload, Error>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LiveSyncAccount {
|
|
12
|
+
accountId: string;
|
|
13
|
+
username: string;
|
|
14
|
+
externalUserId?: string;
|
|
15
|
+
isDefault: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CachedLiveSyncOptions<Source extends string, Payload, Persisted> {
|
|
19
|
+
db: Database;
|
|
20
|
+
cacheKey: string;
|
|
21
|
+
refresh: boolean;
|
|
22
|
+
cacheTtlMs: number;
|
|
23
|
+
transports: readonly LiveTransportAdapter<Source, Payload>[];
|
|
24
|
+
persistLive: (db: Database, payload: Payload, source: Source) => Persisted;
|
|
25
|
+
persistCached?: (db: Database, payload: Payload) => Persisted;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CachedLiveSyncResult<
|
|
29
|
+
Source extends string,
|
|
30
|
+
Payload,
|
|
31
|
+
Persisted,
|
|
32
|
+
> {
|
|
33
|
+
source: Source | "cache";
|
|
34
|
+
payload: Payload;
|
|
35
|
+
persisted: Persisted | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toError(error: unknown) {
|
|
39
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveLiveSyncAccount(
|
|
43
|
+
db: Database,
|
|
44
|
+
accountId?: string,
|
|
45
|
+
): LiveSyncAccount {
|
|
46
|
+
const row = accountId
|
|
47
|
+
? (db
|
|
48
|
+
.prepare(
|
|
49
|
+
"select id, handle, external_user_id, is_default from accounts where id = ?",
|
|
50
|
+
)
|
|
51
|
+
.get(accountId) as
|
|
52
|
+
| {
|
|
53
|
+
id: string;
|
|
54
|
+
handle: string;
|
|
55
|
+
external_user_id: string | null;
|
|
56
|
+
is_default: number;
|
|
57
|
+
}
|
|
58
|
+
| undefined)
|
|
59
|
+
: (db
|
|
60
|
+
.prepare(
|
|
61
|
+
`
|
|
62
|
+
select id, handle, external_user_id, is_default
|
|
63
|
+
from accounts
|
|
64
|
+
order by is_default desc, created_at asc
|
|
65
|
+
limit 1
|
|
66
|
+
`,
|
|
67
|
+
)
|
|
68
|
+
.get() as
|
|
69
|
+
| {
|
|
70
|
+
id: string;
|
|
71
|
+
handle: string;
|
|
72
|
+
external_user_id: string | null;
|
|
73
|
+
is_default: number;
|
|
74
|
+
}
|
|
75
|
+
| undefined);
|
|
76
|
+
|
|
77
|
+
if (!row) {
|
|
78
|
+
throw new Error(`Unknown account: ${accountId ?? "default"}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const externalUserId = row.external_user_id?.trim();
|
|
82
|
+
return {
|
|
83
|
+
accountId: row.id,
|
|
84
|
+
username: row.handle.replace(/^@/, ""),
|
|
85
|
+
...(externalUserId ? { externalUserId } : {}),
|
|
86
|
+
isDefault: row.is_default === 1,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createLiveTransportAdapter<Source extends string, Payload>(
|
|
91
|
+
source: Source,
|
|
92
|
+
fetch: Effect.Effect<Payload, unknown>,
|
|
93
|
+
): LiveTransportAdapter<Source, Payload> {
|
|
94
|
+
return {
|
|
95
|
+
source,
|
|
96
|
+
fetch: fetch.pipe(Effect.mapError(toError)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function assertLiveAccountMatches({
|
|
101
|
+
source,
|
|
102
|
+
account,
|
|
103
|
+
liveUsername,
|
|
104
|
+
liveExternalUserId,
|
|
105
|
+
}: {
|
|
106
|
+
source: string;
|
|
107
|
+
account: LiveSyncAccount;
|
|
108
|
+
liveUsername: string;
|
|
109
|
+
liveExternalUserId?: string;
|
|
110
|
+
}) {
|
|
111
|
+
if (
|
|
112
|
+
account.externalUserId &&
|
|
113
|
+
liveExternalUserId &&
|
|
114
|
+
account.externalUserId === liveExternalUserId
|
|
115
|
+
) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (account.externalUserId && liveExternalUserId) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`${source} is authenticated as user ${liveExternalUserId}; refusing to sync into ${account.accountId} (${account.externalUserId})`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (liveUsername.toLowerCase() !== account.username.toLowerCase()) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`${source} is authenticated as @${liveUsername}; refusing to sync into ${account.accountId} (@${account.username})`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeCacheTtlMs(
|
|
131
|
+
value: number | undefined,
|
|
132
|
+
defaultValue: number,
|
|
133
|
+
) {
|
|
134
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
135
|
+
return defaultValue;
|
|
136
|
+
}
|
|
137
|
+
return Math.floor(value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function fetchWithTransportFallbackEffect<
|
|
141
|
+
Source extends string,
|
|
142
|
+
Payload,
|
|
143
|
+
>(
|
|
144
|
+
transports: readonly LiveTransportAdapter<Source, Payload>[],
|
|
145
|
+
): Effect.Effect<{ source: Source; payload: Payload }, Error> {
|
|
146
|
+
const [first, ...rest] = transports;
|
|
147
|
+
if (!first) {
|
|
148
|
+
return Effect.fail(new Error("No live transport adapters configured"));
|
|
149
|
+
}
|
|
150
|
+
return first.fetch.pipe(
|
|
151
|
+
Effect.map((payload) => ({ source: first.source, payload })),
|
|
152
|
+
Effect.catchAll((error) =>
|
|
153
|
+
rest.length > 0
|
|
154
|
+
? fetchWithTransportFallbackEffect(rest)
|
|
155
|
+
: Effect.fail(toError(error)),
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function runCachedLiveSyncEffect<
|
|
161
|
+
Source extends string,
|
|
162
|
+
Payload,
|
|
163
|
+
Persisted,
|
|
164
|
+
>({
|
|
165
|
+
db,
|
|
166
|
+
cacheKey,
|
|
167
|
+
refresh,
|
|
168
|
+
cacheTtlMs,
|
|
169
|
+
transports,
|
|
170
|
+
persistLive,
|
|
171
|
+
persistCached,
|
|
172
|
+
}: CachedLiveSyncOptions<Source, Payload, Persisted>): Effect.Effect<
|
|
173
|
+
CachedLiveSyncResult<Source, Payload, Persisted>,
|
|
174
|
+
Error
|
|
175
|
+
> {
|
|
176
|
+
return Effect.gen(function* () {
|
|
177
|
+
const cached = yield* Effect.try({
|
|
178
|
+
try: () => readSyncCache<Payload>(cacheKey, db),
|
|
179
|
+
catch: toError,
|
|
180
|
+
});
|
|
181
|
+
const cacheAgeMs = cached
|
|
182
|
+
? Date.now() - new Date(cached.updatedAt).getTime()
|
|
183
|
+
: Number.POSITIVE_INFINITY;
|
|
184
|
+
if (!refresh && cached && cacheAgeMs <= cacheTtlMs) {
|
|
185
|
+
const persisted = persistCached
|
|
186
|
+
? yield* databaseWriteEffect((writeDb) =>
|
|
187
|
+
persistCached(writeDb, cached.value),
|
|
188
|
+
)
|
|
189
|
+
: undefined;
|
|
190
|
+
return {
|
|
191
|
+
source: "cache",
|
|
192
|
+
payload: cached.value,
|
|
193
|
+
persisted,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const fetched = yield* fetchWithTransportFallbackEffect(transports);
|
|
198
|
+
const persisted = yield* databaseWriteEffect((writeDb) => {
|
|
199
|
+
const value = persistLive(writeDb, fetched.payload, fetched.source);
|
|
200
|
+
writeSyncCache(cacheKey, fetched.payload, writeDb);
|
|
201
|
+
return value;
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
source: fetched.source,
|
|
205
|
+
payload: fetched.payload,
|
|
206
|
+
persisted,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|