birdclaw 0.4.1 → 0.5.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 +33 -1
- package/README.md +108 -7
- package/package.json +24 -23
- package/playwright.config.ts +1 -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/src/cli.ts +450 -18
- package/src/components/AppNav.tsx +66 -29
- package/src/components/AvatarChip.tsx +10 -5
- package/src/components/ConversationThread.tsx +125 -0
- package/src/components/DmWorkspace.tsx +96 -98
- package/src/components/EmbeddedTweetCard.tsx +20 -14
- package/src/components/InboxCard.tsx +92 -90
- package/src/components/LinkPreviewCard.tsx +270 -0
- package/src/components/ProfilePreview.tsx +8 -3
- package/src/components/SavedTimelineView.tsx +48 -38
- package/src/components/TimelineCard.tsx +228 -84
- package/src/components/TweetRichText.tsx +6 -2
- package/src/lib/archive-import.ts +1565 -65
- 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/db.ts +191 -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 +233 -7
- 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 +375 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +20 -3
- package/src/lib/x-profile.ts +7 -2
- package/src/lib/xurl.ts +296 -12
- package/src/routeTree.gen.ts +105 -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/blocks.tsx +111 -86
- package/src/routes/dms.tsx +90 -78
- package/src/routes/inbox.tsx +98 -86
- package/src/routes/index.tsx +63 -50
- package/src/routes/links.tsx +883 -0
- package/src/routes/mentions.tsx +63 -50
- package/src/styles.css +106 -43
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
|
|
@@ -483,6 +584,7 @@ const INDEX_SQL = `
|
|
|
483
584
|
create index if not exists idx_dm_messages_conversation on dm_messages(conversation_id, created_at asc);
|
|
484
585
|
create index if not exists idx_profiles_followers on profiles(followers_count desc);
|
|
485
586
|
create index if not exists idx_profiles_following on profiles(following_count desc);
|
|
587
|
+
create index if not exists idx_profiles_handle on profiles(handle);
|
|
486
588
|
create index if not exists idx_profile_affiliations_subject on profile_affiliations(subject_profile_id, is_active, last_seen_at desc);
|
|
487
589
|
create index if not exists idx_profile_affiliations_org on profile_affiliations(organization_profile_id, is_active, last_seen_at desc);
|
|
488
590
|
create index if not exists idx_profile_snapshots_profile on profile_snapshots(profile_id, last_seen_at desc);
|
|
@@ -501,6 +603,10 @@ const INDEX_SQL = `
|
|
|
501
603
|
create index if not exists idx_link_occurrences_created on link_occurrences(created_at desc);
|
|
502
604
|
create index if not exists idx_link_occurrences_account on link_occurrences(account_id, created_at desc);
|
|
503
605
|
create index if not exists idx_link_occurrences_direction on link_occurrences(direction, created_at desc);
|
|
606
|
+
create index if not exists idx_follow_edges_current on follow_edges(account_id, direction, current, last_seen_at desc);
|
|
607
|
+
create index if not exists idx_follow_edges_profile on follow_edges(profile_id, current);
|
|
608
|
+
create index if not exists idx_follow_snapshots_account on follow_snapshots(account_id, direction, completed_at desc);
|
|
609
|
+
create index if not exists idx_follow_events_account on follow_events(account_id, direction, kind, event_at desc);
|
|
504
610
|
`;
|
|
505
611
|
|
|
506
612
|
function getColumnNames(db: Database, tableName: string): Set<string> {
|
|
@@ -556,6 +662,11 @@ function ensureProfileAvatarColumns(db: Database) {
|
|
|
556
662
|
"alter table profiles add column raw_json text not null default '{}'",
|
|
557
663
|
);
|
|
558
664
|
}
|
|
665
|
+
if (!columnNames.has("public_metrics_json")) {
|
|
666
|
+
db.exec(
|
|
667
|
+
"alter table profiles add column public_metrics_json text not null default '{}'",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
559
670
|
}
|
|
560
671
|
|
|
561
672
|
function ensureAccountExternalUserIdColumn(db: Database) {
|
|
@@ -683,6 +794,8 @@ function ensureLinkIndexTables(db: Database) {
|
|
|
683
794
|
expanded_handle text,
|
|
684
795
|
title text,
|
|
685
796
|
description text,
|
|
797
|
+
image_url text,
|
|
798
|
+
site_name text,
|
|
686
799
|
error text,
|
|
687
800
|
source text not null,
|
|
688
801
|
updated_at text not null
|
|
@@ -700,6 +813,64 @@ function ensureLinkIndexTables(db: Database) {
|
|
|
700
813
|
primary key (source_kind, source_id, source_position, short_url)
|
|
701
814
|
);
|
|
702
815
|
`);
|
|
816
|
+
|
|
817
|
+
const urlExpansionColumns = getColumnNames(db, "url_expansions");
|
|
818
|
+
if (!urlExpansionColumns.has("image_url")) {
|
|
819
|
+
db.exec("alter table url_expansions add column image_url text");
|
|
820
|
+
}
|
|
821
|
+
if (!urlExpansionColumns.has("site_name")) {
|
|
822
|
+
db.exec("alter table url_expansions add column site_name text");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function ensureFollowGraphTables(db: Database) {
|
|
827
|
+
db.exec(`
|
|
828
|
+
create table if not exists follow_snapshots (
|
|
829
|
+
id text primary key,
|
|
830
|
+
account_id text not null,
|
|
831
|
+
direction text not null,
|
|
832
|
+
source text not null,
|
|
833
|
+
status text not null,
|
|
834
|
+
page_count integer not null default 0,
|
|
835
|
+
result_count integer not null default 0,
|
|
836
|
+
started_at text not null,
|
|
837
|
+
completed_at text not null,
|
|
838
|
+
raw_meta_json text not null default '{}'
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
create table if not exists follow_snapshot_members (
|
|
842
|
+
snapshot_id text not null,
|
|
843
|
+
profile_id text not null,
|
|
844
|
+
external_user_id text not null,
|
|
845
|
+
position integer not null,
|
|
846
|
+
primary key (snapshot_id, profile_id)
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
create table if not exists follow_edges (
|
|
850
|
+
account_id text not null,
|
|
851
|
+
direction text not null,
|
|
852
|
+
profile_id text not null,
|
|
853
|
+
external_user_id text not null,
|
|
854
|
+
source text not null,
|
|
855
|
+
current integer not null default 1,
|
|
856
|
+
first_seen_at text not null,
|
|
857
|
+
last_seen_at text not null,
|
|
858
|
+
ended_at text,
|
|
859
|
+
updated_at text not null,
|
|
860
|
+
primary key (account_id, direction, profile_id)
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
create table if not exists follow_events (
|
|
864
|
+
id text primary key,
|
|
865
|
+
account_id text not null,
|
|
866
|
+
direction text not null,
|
|
867
|
+
profile_id text not null,
|
|
868
|
+
external_user_id text not null,
|
|
869
|
+
kind text not null,
|
|
870
|
+
event_at text not null,
|
|
871
|
+
snapshot_id text not null
|
|
872
|
+
);
|
|
873
|
+
`);
|
|
703
874
|
}
|
|
704
875
|
|
|
705
876
|
function backfillTweetCollections(db: Database) {
|
|
@@ -749,6 +920,17 @@ function ensureSchemaIndexes(db: Database) {
|
|
|
749
920
|
db.exec(INDEX_SQL);
|
|
750
921
|
}
|
|
751
922
|
|
|
923
|
+
function ensureDemoData(db: Database) {
|
|
924
|
+
if (demoSeedAttempted) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
seedDemoData(db);
|
|
929
|
+
backfillTweetCollections(db);
|
|
930
|
+
backfillTweetAccountEdges(db);
|
|
931
|
+
demoSeedAttempted = true;
|
|
932
|
+
}
|
|
933
|
+
|
|
752
934
|
function initDatabase(options: InitDatabaseOptions = {}) {
|
|
753
935
|
ensureBirdclawDirs();
|
|
754
936
|
|
|
@@ -766,12 +948,16 @@ function initDatabase(options: InitDatabaseOptions = {}) {
|
|
|
766
948
|
ensureProfileBioEntitiesTable(nativeDb);
|
|
767
949
|
ensureIdentitySearchIndexTable(nativeDb);
|
|
768
950
|
ensureLinkIndexTables(nativeDb);
|
|
951
|
+
ensureFollowGraphTables(nativeDb);
|
|
769
952
|
ensureSchemaIndexes(nativeDb);
|
|
770
953
|
if (options.seedDemoData !== false) {
|
|
771
|
-
|
|
954
|
+
ensureDemoData(nativeDb);
|
|
955
|
+
} else {
|
|
956
|
+
backfillTweetCollections(nativeDb);
|
|
957
|
+
backfillTweetAccountEdges(nativeDb);
|
|
772
958
|
}
|
|
773
|
-
|
|
774
|
-
|
|
959
|
+
} else if (options.seedDemoData !== false) {
|
|
960
|
+
ensureDemoData(nativeDb);
|
|
775
961
|
}
|
|
776
962
|
|
|
777
963
|
if (!kyselyDb) {
|
|
@@ -799,4 +985,5 @@ export function resetDatabaseForTests() {
|
|
|
799
985
|
|
|
800
986
|
nativeDb?.close();
|
|
801
987
|
nativeDb = undefined;
|
|
988
|
+
demoSeedAttempted = false;
|
|
802
989
|
}
|