birdclaw 0.4.1 → 0.5.1
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 +52 -0
- package/README.md +113 -7
- package/bin/birdclaw.mjs +50 -11
- package/package.json +30 -28
- package/playwright.config.ts +1 -0
- package/public/birdclaw-mark.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +2 -2
- package/scripts/browser-perf.mjs +399 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +29 -0
- package/src/cli.ts +496 -19
- package/src/components/AppNav.tsx +66 -29
- package/src/components/AvatarChip.tsx +10 -5
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +126 -0
- package/src/components/DmWorkspace.tsx +118 -105
- package/src/components/EmbeddedTweetCard.tsx +20 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +104 -90
- package/src/components/LinkPreviewCard.tsx +270 -0
- package/src/components/ProfilePreview.tsx +8 -3
- package/src/components/SavedTimelineView.tsx +89 -71
- package/src/components/SyncNowButton.tsx +105 -0
- package/src/components/ThemeSlider.tsx +10 -59
- package/src/components/TimelineCard.tsx +326 -86
- package/src/components/TimelineRouteFrame.tsx +156 -0
- package/src/components/TweetMediaGrid.tsx +120 -23
- package/src/components/TweetRichText.tsx +19 -4
- package/src/components/useTimelineRouteData.ts +137 -0
- package/src/lib/api-client.ts +229 -0
- package/src/lib/archive-finder.ts +24 -20
- package/src/lib/archive-import.ts +1582 -67
- package/src/lib/authored-live.ts +1074 -0
- package/src/lib/backup.ts +316 -14
- package/src/lib/bird-actions.ts +1 -10
- package/src/lib/bird-command.ts +57 -0
- package/src/lib/bird.ts +89 -5
- package/src/lib/config.ts +1 -1
- package/src/lib/conversation-surface.ts +174 -0
- package/src/lib/db.ts +193 -4
- package/src/lib/follow-graph.ts +1053 -0
- package/src/lib/link-index.ts +11 -98
- package/src/lib/link-insights.ts +834 -0
- package/src/lib/link-preview-metadata.ts +334 -0
- package/src/lib/media-fetch.ts +787 -0
- package/src/lib/media-includes.ts +165 -0
- package/src/lib/mention-threads-live.ts +535 -43
- package/src/lib/mentions-live.ts +623 -19
- package/src/lib/moderation-target.ts +6 -0
- package/src/lib/profile-hydration.ts +1 -1
- package/src/lib/profile-resolver.ts +115 -1
- package/src/lib/queries.ts +326 -35
- package/src/lib/seed.ts +127 -8
- package/src/lib/timeline-collections-live.ts +145 -22
- package/src/lib/timeline-live.ts +10 -15
- package/src/lib/tweet-account-edges.ts +9 -2
- package/src/lib/tweet-render.ts +97 -0
- package/src/lib/types.ts +185 -2
- package/src/lib/ui.ts +383 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +20 -3
- package/src/lib/web-sync.ts +443 -0
- package/src/lib/x-profile.ts +7 -2
- package/src/lib/xurl.ts +296 -12
- package/src/routeTree.gen.ts +126 -0
- package/src/routes/__root.tsx +7 -3
- package/src/routes/api/conversation.tsx +34 -0
- package/src/routes/api/link-insights.tsx +79 -0
- package/src/routes/api/link-preview.tsx +43 -0
- package/src/routes/api/profile-hydrate.tsx +51 -0
- package/src/routes/api/sync.tsx +59 -0
- package/src/routes/blocks.tsx +111 -86
- package/src/routes/dms.tsx +172 -87
- package/src/routes/inbox.tsx +98 -86
- package/src/routes/index.tsx +22 -115
- package/src/routes/links.tsx +928 -0
- package/src/routes/mentions.tsx +22 -115
- package/src/styles.css +169 -43
- package/vite.config.ts +8 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { EmbeddedTweet } from "#/lib/types";
|
|
4
|
+
|
|
5
|
+
type ConversationStatus = "idle" | "loading" | "ready" | "error";
|
|
6
|
+
|
|
7
|
+
interface ConversationRecord {
|
|
8
|
+
error: string | null;
|
|
9
|
+
items: EmbeddedTweet[];
|
|
10
|
+
status: ConversationStatus;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ConversationSurfaceSnapshot {
|
|
14
|
+
expandedTweetId: string | null;
|
|
15
|
+
records: ReadonlyMap<string, ConversationRecord>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Listener = () => void;
|
|
19
|
+
|
|
20
|
+
const emptyRecord: ConversationRecord = {
|
|
21
|
+
error: null,
|
|
22
|
+
items: [],
|
|
23
|
+
status: "idle",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let snapshot: ConversationSurfaceSnapshot = {
|
|
27
|
+
expandedTweetId: null,
|
|
28
|
+
records: new Map(),
|
|
29
|
+
};
|
|
30
|
+
const listeners = new Set<Listener>();
|
|
31
|
+
const inFlight = new Set<string>();
|
|
32
|
+
let activeScopes = 0;
|
|
33
|
+
let generation = 0;
|
|
34
|
+
|
|
35
|
+
function emit() {
|
|
36
|
+
for (const listener of listeners) {
|
|
37
|
+
listener();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setSnapshot(next: ConversationSurfaceSnapshot) {
|
|
42
|
+
snapshot = next;
|
|
43
|
+
emit();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function updateRecord(tweetId: string, record: ConversationRecord) {
|
|
47
|
+
const records = new Map(snapshot.records);
|
|
48
|
+
records.set(tweetId, record);
|
|
49
|
+
setSnapshot({ ...snapshot, records });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function subscribe(listener: Listener) {
|
|
53
|
+
listeners.add(listener);
|
|
54
|
+
return () => {
|
|
55
|
+
listeners.delete(listener);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getSnapshot() {
|
|
60
|
+
return snapshot;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadConversation(tweetId: string) {
|
|
64
|
+
const current = snapshot.records.get(tweetId);
|
|
65
|
+
if (current?.status === "ready" || inFlight.has(tweetId)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const loadGeneration = generation;
|
|
70
|
+
inFlight.add(tweetId);
|
|
71
|
+
updateRecord(tweetId, {
|
|
72
|
+
error: null,
|
|
73
|
+
items: current?.items ?? [],
|
|
74
|
+
status: "loading",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(
|
|
79
|
+
`/api/conversation?tweetId=${encodeURIComponent(tweetId)}`,
|
|
80
|
+
);
|
|
81
|
+
const data = (await response.json()) as {
|
|
82
|
+
error?: string;
|
|
83
|
+
items?: EmbeddedTweet[];
|
|
84
|
+
ok?: boolean;
|
|
85
|
+
};
|
|
86
|
+
if (!response.ok || data.ok === false) {
|
|
87
|
+
throw new Error(data.error ?? "Conversation unavailable");
|
|
88
|
+
}
|
|
89
|
+
if (loadGeneration !== generation) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
updateRecord(tweetId, {
|
|
93
|
+
error: null,
|
|
94
|
+
items: (data.items ?? []).filter(Boolean),
|
|
95
|
+
status: "ready",
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (loadGeneration !== generation) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
updateRecord(tweetId, {
|
|
102
|
+
error:
|
|
103
|
+
error instanceof Error ? error.message : "Conversation unavailable",
|
|
104
|
+
items: [],
|
|
105
|
+
status: "error",
|
|
106
|
+
});
|
|
107
|
+
} finally {
|
|
108
|
+
inFlight.delete(tweetId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function retainConversationSurfaceScope() {
|
|
113
|
+
activeScopes += 1;
|
|
114
|
+
return () => {
|
|
115
|
+
activeScopes = Math.max(0, activeScopes - 1);
|
|
116
|
+
if (activeScopes === 0) {
|
|
117
|
+
resetConversationSurface();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function resetConversationSurface() {
|
|
123
|
+
generation += 1;
|
|
124
|
+
inFlight.clear();
|
|
125
|
+
setSnapshot({
|
|
126
|
+
expandedTweetId: null,
|
|
127
|
+
records: new Map(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function ConversationSurfaceScope({
|
|
132
|
+
children,
|
|
133
|
+
}: {
|
|
134
|
+
children: ReactNode;
|
|
135
|
+
}) {
|
|
136
|
+
useEffect(() => retainConversationSurfaceScope(), []);
|
|
137
|
+
return children;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function useConversationSurface(tweetId: string) {
|
|
141
|
+
const currentSnapshot = useSyncExternalStore(
|
|
142
|
+
subscribe,
|
|
143
|
+
getSnapshot,
|
|
144
|
+
getSnapshot,
|
|
145
|
+
);
|
|
146
|
+
const record = currentSnapshot.records.get(tweetId) ?? emptyRecord;
|
|
147
|
+
const isOpen = currentSnapshot.expandedTweetId === tweetId;
|
|
148
|
+
|
|
149
|
+
const toggle = useCallback(() => {
|
|
150
|
+
const nextExpanded = snapshot.expandedTweetId === tweetId ? null : tweetId;
|
|
151
|
+
setSnapshot({ ...snapshot, expandedTweetId: nextExpanded });
|
|
152
|
+
if (nextExpanded) {
|
|
153
|
+
void loadConversation(tweetId);
|
|
154
|
+
}
|
|
155
|
+
}, [tweetId]);
|
|
156
|
+
|
|
157
|
+
const prefetch = useCallback(() => {
|
|
158
|
+
const current = snapshot.records.get(tweetId);
|
|
159
|
+
if (current?.status === "ready" || current?.status === "loading") {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
void loadConversation(tweetId);
|
|
163
|
+
}, [tweetId]);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
error: record.error,
|
|
167
|
+
isOpen,
|
|
168
|
+
items: record.items,
|
|
169
|
+
loading: record.status === "loading",
|
|
170
|
+
prefetch,
|
|
171
|
+
status: record.status,
|
|
172
|
+
toggle,
|
|
173
|
+
};
|
|
174
|
+
}
|
package/src/lib/db.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface ProfilesTable {
|
|
|
20
20
|
bio: string;
|
|
21
21
|
followers_count: number;
|
|
22
22
|
following_count: number;
|
|
23
|
+
public_metrics_json: string;
|
|
23
24
|
avatar_hue: number;
|
|
24
25
|
avatar_url: string | null;
|
|
25
26
|
location: string | null;
|
|
@@ -116,7 +117,7 @@ export interface TweetCollectionsTable {
|
|
|
116
117
|
export interface TweetAccountEdgesTable {
|
|
117
118
|
account_id: string;
|
|
118
119
|
tweet_id: string;
|
|
119
|
-
kind: "home" | "mention";
|
|
120
|
+
kind: "home" | "mention" | "authored" | "thread_context";
|
|
120
121
|
first_seen_at: string;
|
|
121
122
|
last_seen_at: string;
|
|
122
123
|
seen_count: number;
|
|
@@ -194,6 +195,8 @@ export interface UrlExpansionsTable {
|
|
|
194
195
|
expanded_handle: string | null;
|
|
195
196
|
title: string | null;
|
|
196
197
|
description: string | null;
|
|
198
|
+
image_url: string | null;
|
|
199
|
+
site_name: string | null;
|
|
197
200
|
error: string | null;
|
|
198
201
|
source: string;
|
|
199
202
|
updated_at: string;
|
|
@@ -210,6 +213,50 @@ export interface LinkOccurrencesTable {
|
|
|
210
213
|
created_at: string;
|
|
211
214
|
}
|
|
212
215
|
|
|
216
|
+
export interface FollowEdgesTable {
|
|
217
|
+
account_id: string;
|
|
218
|
+
direction: "followers" | "following";
|
|
219
|
+
profile_id: string;
|
|
220
|
+
external_user_id: string;
|
|
221
|
+
source: string;
|
|
222
|
+
current: number;
|
|
223
|
+
first_seen_at: string;
|
|
224
|
+
last_seen_at: string;
|
|
225
|
+
ended_at: string | null;
|
|
226
|
+
updated_at: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface FollowSnapshotsTable {
|
|
230
|
+
id: string;
|
|
231
|
+
account_id: string;
|
|
232
|
+
direction: "followers" | "following";
|
|
233
|
+
source: string;
|
|
234
|
+
status: "complete" | "incomplete";
|
|
235
|
+
page_count: number;
|
|
236
|
+
result_count: number;
|
|
237
|
+
started_at: string;
|
|
238
|
+
completed_at: string;
|
|
239
|
+
raw_meta_json: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface FollowSnapshotMembersTable {
|
|
243
|
+
snapshot_id: string;
|
|
244
|
+
profile_id: string;
|
|
245
|
+
external_user_id: string;
|
|
246
|
+
position: number;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface FollowEventsTable {
|
|
250
|
+
id: string;
|
|
251
|
+
account_id: string;
|
|
252
|
+
direction: "followers" | "following";
|
|
253
|
+
profile_id: string;
|
|
254
|
+
external_user_id: string;
|
|
255
|
+
kind: "started" | "ended";
|
|
256
|
+
event_at: string;
|
|
257
|
+
snapshot_id: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
213
260
|
export interface BirdclawDatabase {
|
|
214
261
|
accounts: AccountsTable;
|
|
215
262
|
profiles: ProfilesTable;
|
|
@@ -229,10 +276,15 @@ export interface BirdclawDatabase {
|
|
|
229
276
|
sync_cache: SyncCacheTable;
|
|
230
277
|
url_expansions: UrlExpansionsTable;
|
|
231
278
|
link_occurrences: LinkOccurrencesTable;
|
|
279
|
+
follow_edges: FollowEdgesTable;
|
|
280
|
+
follow_snapshots: FollowSnapshotsTable;
|
|
281
|
+
follow_snapshot_members: FollowSnapshotMembersTable;
|
|
282
|
+
follow_events: FollowEventsTable;
|
|
232
283
|
}
|
|
233
284
|
|
|
234
285
|
let nativeDb: Database | undefined;
|
|
235
286
|
let kyselyDb: Kysely<BirdclawDatabase> | undefined;
|
|
287
|
+
let demoSeedAttempted = false;
|
|
236
288
|
|
|
237
289
|
export interface InitDatabaseOptions {
|
|
238
290
|
seedDemoData?: boolean;
|
|
@@ -260,6 +312,7 @@ const BASE_SCHEMA_SQL = `
|
|
|
260
312
|
bio text not null,
|
|
261
313
|
followers_count integer not null default 0,
|
|
262
314
|
following_count integer not null default 0,
|
|
315
|
+
public_metrics_json text not null default '{}',
|
|
263
316
|
avatar_hue integer not null default 0,
|
|
264
317
|
avatar_url text,
|
|
265
318
|
location text,
|
|
@@ -443,6 +496,8 @@ const BASE_SCHEMA_SQL = `
|
|
|
443
496
|
expanded_handle text,
|
|
444
497
|
title text,
|
|
445
498
|
description text,
|
|
499
|
+
image_url text,
|
|
500
|
+
site_name text,
|
|
446
501
|
error text,
|
|
447
502
|
source text not null,
|
|
448
503
|
updated_at text not null
|
|
@@ -460,6 +515,52 @@ const BASE_SCHEMA_SQL = `
|
|
|
460
515
|
primary key (source_kind, source_id, source_position, short_url)
|
|
461
516
|
);
|
|
462
517
|
|
|
518
|
+
create table if not exists follow_snapshots (
|
|
519
|
+
id text primary key,
|
|
520
|
+
account_id text not null,
|
|
521
|
+
direction text not null,
|
|
522
|
+
source text not null,
|
|
523
|
+
status text not null,
|
|
524
|
+
page_count integer not null default 0,
|
|
525
|
+
result_count integer not null default 0,
|
|
526
|
+
started_at text not null,
|
|
527
|
+
completed_at text not null,
|
|
528
|
+
raw_meta_json text not null default '{}'
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
create table if not exists follow_snapshot_members (
|
|
532
|
+
snapshot_id text not null,
|
|
533
|
+
profile_id text not null,
|
|
534
|
+
external_user_id text not null,
|
|
535
|
+
position integer not null,
|
|
536
|
+
primary key (snapshot_id, profile_id)
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
create table if not exists follow_edges (
|
|
540
|
+
account_id text not null,
|
|
541
|
+
direction text not null,
|
|
542
|
+
profile_id text not null,
|
|
543
|
+
external_user_id text not null,
|
|
544
|
+
source text not null,
|
|
545
|
+
current integer not null default 1,
|
|
546
|
+
first_seen_at text not null,
|
|
547
|
+
last_seen_at text not null,
|
|
548
|
+
ended_at text,
|
|
549
|
+
updated_at text not null,
|
|
550
|
+
primary key (account_id, direction, profile_id)
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
create table if not exists follow_events (
|
|
554
|
+
id text primary key,
|
|
555
|
+
account_id text not null,
|
|
556
|
+
direction text not null,
|
|
557
|
+
profile_id text not null,
|
|
558
|
+
external_user_id text not null,
|
|
559
|
+
kind text not null,
|
|
560
|
+
event_at text not null,
|
|
561
|
+
snapshot_id text not null
|
|
562
|
+
);
|
|
563
|
+
|
|
463
564
|
create virtual table if not exists tweets_fts using fts5(
|
|
464
565
|
tweet_id unindexed,
|
|
465
566
|
text
|
|
@@ -473,16 +574,19 @@ const BASE_SCHEMA_SQL = `
|
|
|
473
574
|
|
|
474
575
|
const INDEX_SQL = `
|
|
475
576
|
create index if not exists idx_tweets_kind_created on tweets(kind, created_at desc);
|
|
577
|
+
create index if not exists idx_tweets_created on tweets(created_at desc);
|
|
476
578
|
create index if not exists idx_tweets_account_created on tweets(account_id, created_at desc);
|
|
477
579
|
create index if not exists idx_tweets_quoted on tweets(quoted_tweet_id);
|
|
478
580
|
create index if not exists idx_tweet_collections_kind_account on tweet_collections(kind, account_id, collected_at desc, tweet_id);
|
|
479
581
|
create index if not exists idx_tweet_collections_tweet on tweet_collections(tweet_id);
|
|
480
582
|
create index if not exists idx_tweet_account_edges_kind_account on tweet_account_edges(kind, account_id, last_seen_at desc, tweet_id);
|
|
583
|
+
create index if not exists idx_tweet_account_edges_kind_tweet on tweet_account_edges(kind, tweet_id, account_id);
|
|
481
584
|
create index if not exists idx_tweet_account_edges_tweet on tweet_account_edges(tweet_id);
|
|
482
585
|
create index if not exists idx_dm_conversations_account on dm_conversations(account_id, last_message_at desc);
|
|
483
586
|
create index if not exists idx_dm_messages_conversation on dm_messages(conversation_id, created_at asc);
|
|
484
587
|
create index if not exists idx_profiles_followers on profiles(followers_count desc);
|
|
485
588
|
create index if not exists idx_profiles_following on profiles(following_count desc);
|
|
589
|
+
create index if not exists idx_profiles_handle on profiles(handle);
|
|
486
590
|
create index if not exists idx_profile_affiliations_subject on profile_affiliations(subject_profile_id, is_active, last_seen_at desc);
|
|
487
591
|
create index if not exists idx_profile_affiliations_org on profile_affiliations(organization_profile_id, is_active, last_seen_at desc);
|
|
488
592
|
create index if not exists idx_profile_snapshots_profile on profile_snapshots(profile_id, last_seen_at desc);
|
|
@@ -501,6 +605,10 @@ const INDEX_SQL = `
|
|
|
501
605
|
create index if not exists idx_link_occurrences_created on link_occurrences(created_at desc);
|
|
502
606
|
create index if not exists idx_link_occurrences_account on link_occurrences(account_id, created_at desc);
|
|
503
607
|
create index if not exists idx_link_occurrences_direction on link_occurrences(direction, created_at desc);
|
|
608
|
+
create index if not exists idx_follow_edges_current on follow_edges(account_id, direction, current, last_seen_at desc);
|
|
609
|
+
create index if not exists idx_follow_edges_profile on follow_edges(profile_id, current);
|
|
610
|
+
create index if not exists idx_follow_snapshots_account on follow_snapshots(account_id, direction, completed_at desc);
|
|
611
|
+
create index if not exists idx_follow_events_account on follow_events(account_id, direction, kind, event_at desc);
|
|
504
612
|
`;
|
|
505
613
|
|
|
506
614
|
function getColumnNames(db: Database, tableName: string): Set<string> {
|
|
@@ -556,6 +664,11 @@ function ensureProfileAvatarColumns(db: Database) {
|
|
|
556
664
|
"alter table profiles add column raw_json text not null default '{}'",
|
|
557
665
|
);
|
|
558
666
|
}
|
|
667
|
+
if (!columnNames.has("public_metrics_json")) {
|
|
668
|
+
db.exec(
|
|
669
|
+
"alter table profiles add column public_metrics_json text not null default '{}'",
|
|
670
|
+
);
|
|
671
|
+
}
|
|
559
672
|
}
|
|
560
673
|
|
|
561
674
|
function ensureAccountExternalUserIdColumn(db: Database) {
|
|
@@ -683,6 +796,8 @@ function ensureLinkIndexTables(db: Database) {
|
|
|
683
796
|
expanded_handle text,
|
|
684
797
|
title text,
|
|
685
798
|
description text,
|
|
799
|
+
image_url text,
|
|
800
|
+
site_name text,
|
|
686
801
|
error text,
|
|
687
802
|
source text not null,
|
|
688
803
|
updated_at text not null
|
|
@@ -700,6 +815,64 @@ function ensureLinkIndexTables(db: Database) {
|
|
|
700
815
|
primary key (source_kind, source_id, source_position, short_url)
|
|
701
816
|
);
|
|
702
817
|
`);
|
|
818
|
+
|
|
819
|
+
const urlExpansionColumns = getColumnNames(db, "url_expansions");
|
|
820
|
+
if (!urlExpansionColumns.has("image_url")) {
|
|
821
|
+
db.exec("alter table url_expansions add column image_url text");
|
|
822
|
+
}
|
|
823
|
+
if (!urlExpansionColumns.has("site_name")) {
|
|
824
|
+
db.exec("alter table url_expansions add column site_name text");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function ensureFollowGraphTables(db: Database) {
|
|
829
|
+
db.exec(`
|
|
830
|
+
create table if not exists follow_snapshots (
|
|
831
|
+
id text primary key,
|
|
832
|
+
account_id text not null,
|
|
833
|
+
direction text not null,
|
|
834
|
+
source text not null,
|
|
835
|
+
status text not null,
|
|
836
|
+
page_count integer not null default 0,
|
|
837
|
+
result_count integer not null default 0,
|
|
838
|
+
started_at text not null,
|
|
839
|
+
completed_at text not null,
|
|
840
|
+
raw_meta_json text not null default '{}'
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
create table if not exists follow_snapshot_members (
|
|
844
|
+
snapshot_id text not null,
|
|
845
|
+
profile_id text not null,
|
|
846
|
+
external_user_id text not null,
|
|
847
|
+
position integer not null,
|
|
848
|
+
primary key (snapshot_id, profile_id)
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
create table if not exists follow_edges (
|
|
852
|
+
account_id text not null,
|
|
853
|
+
direction text not null,
|
|
854
|
+
profile_id text not null,
|
|
855
|
+
external_user_id text not null,
|
|
856
|
+
source text not null,
|
|
857
|
+
current integer not null default 1,
|
|
858
|
+
first_seen_at text not null,
|
|
859
|
+
last_seen_at text not null,
|
|
860
|
+
ended_at text,
|
|
861
|
+
updated_at text not null,
|
|
862
|
+
primary key (account_id, direction, profile_id)
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
create table if not exists follow_events (
|
|
866
|
+
id text primary key,
|
|
867
|
+
account_id text not null,
|
|
868
|
+
direction text not null,
|
|
869
|
+
profile_id text not null,
|
|
870
|
+
external_user_id text not null,
|
|
871
|
+
kind text not null,
|
|
872
|
+
event_at text not null,
|
|
873
|
+
snapshot_id text not null
|
|
874
|
+
);
|
|
875
|
+
`);
|
|
703
876
|
}
|
|
704
877
|
|
|
705
878
|
function backfillTweetCollections(db: Database) {
|
|
@@ -749,6 +922,17 @@ function ensureSchemaIndexes(db: Database) {
|
|
|
749
922
|
db.exec(INDEX_SQL);
|
|
750
923
|
}
|
|
751
924
|
|
|
925
|
+
function ensureDemoData(db: Database) {
|
|
926
|
+
if (demoSeedAttempted) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
seedDemoData(db);
|
|
931
|
+
backfillTweetCollections(db);
|
|
932
|
+
backfillTweetAccountEdges(db);
|
|
933
|
+
demoSeedAttempted = true;
|
|
934
|
+
}
|
|
935
|
+
|
|
752
936
|
function initDatabase(options: InitDatabaseOptions = {}) {
|
|
753
937
|
ensureBirdclawDirs();
|
|
754
938
|
|
|
@@ -766,12 +950,16 @@ function initDatabase(options: InitDatabaseOptions = {}) {
|
|
|
766
950
|
ensureProfileBioEntitiesTable(nativeDb);
|
|
767
951
|
ensureIdentitySearchIndexTable(nativeDb);
|
|
768
952
|
ensureLinkIndexTables(nativeDb);
|
|
953
|
+
ensureFollowGraphTables(nativeDb);
|
|
769
954
|
ensureSchemaIndexes(nativeDb);
|
|
770
955
|
if (options.seedDemoData !== false) {
|
|
771
|
-
|
|
956
|
+
ensureDemoData(nativeDb);
|
|
957
|
+
} else {
|
|
958
|
+
backfillTweetCollections(nativeDb);
|
|
959
|
+
backfillTweetAccountEdges(nativeDb);
|
|
772
960
|
}
|
|
773
|
-
|
|
774
|
-
|
|
961
|
+
} else if (options.seedDemoData !== false) {
|
|
962
|
+
ensureDemoData(nativeDb);
|
|
775
963
|
}
|
|
776
964
|
|
|
777
965
|
if (!kyselyDb) {
|
|
@@ -799,4 +987,5 @@ export function resetDatabaseForTests() {
|
|
|
799
987
|
|
|
800
988
|
nativeDb?.close();
|
|
801
989
|
nativeDb = undefined;
|
|
990
|
+
demoSeedAttempted = false;
|
|
802
991
|
}
|