birdclaw 0.4.0 → 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 +39 -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 +563 -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 +1567 -65
- package/src/lib/authored-live.ts +1074 -0
- package/src/lib/backup.ts +318 -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 +282 -4
- package/src/lib/follow-graph.ts +1053 -0
- package/src/lib/link-index.ts +629 -0
- 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 +219 -2
- package/src/lib/ui.ts +375 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +33 -8
- 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;
|
|
@@ -185,6 +186,77 @@ export interface SyncCacheTable {
|
|
|
185
186
|
updated_at: string;
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
export interface UrlExpansionsTable {
|
|
190
|
+
short_url: string;
|
|
191
|
+
expanded_url: string;
|
|
192
|
+
final_url: string;
|
|
193
|
+
status: string;
|
|
194
|
+
expanded_tweet_id: string | null;
|
|
195
|
+
expanded_handle: string | null;
|
|
196
|
+
title: string | null;
|
|
197
|
+
description: string | null;
|
|
198
|
+
image_url: string | null;
|
|
199
|
+
site_name: string | null;
|
|
200
|
+
error: string | null;
|
|
201
|
+
source: string;
|
|
202
|
+
updated_at: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface LinkOccurrencesTable {
|
|
206
|
+
source_kind: "dm" | "tweet";
|
|
207
|
+
source_id: string;
|
|
208
|
+
source_position: number;
|
|
209
|
+
short_url: string;
|
|
210
|
+
account_id: string | null;
|
|
211
|
+
conversation_id: string | null;
|
|
212
|
+
direction: string | null;
|
|
213
|
+
created_at: string;
|
|
214
|
+
}
|
|
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
|
+
|
|
188
260
|
export interface BirdclawDatabase {
|
|
189
261
|
accounts: AccountsTable;
|
|
190
262
|
profiles: ProfilesTable;
|
|
@@ -202,10 +274,17 @@ export interface BirdclawDatabase {
|
|
|
202
274
|
mutes: MutesTable;
|
|
203
275
|
ai_scores: AiScoresTable;
|
|
204
276
|
sync_cache: SyncCacheTable;
|
|
277
|
+
url_expansions: UrlExpansionsTable;
|
|
278
|
+
link_occurrences: LinkOccurrencesTable;
|
|
279
|
+
follow_edges: FollowEdgesTable;
|
|
280
|
+
follow_snapshots: FollowSnapshotsTable;
|
|
281
|
+
follow_snapshot_members: FollowSnapshotMembersTable;
|
|
282
|
+
follow_events: FollowEventsTable;
|
|
205
283
|
}
|
|
206
284
|
|
|
207
285
|
let nativeDb: Database | undefined;
|
|
208
286
|
let kyselyDb: Kysely<BirdclawDatabase> | undefined;
|
|
287
|
+
let demoSeedAttempted = false;
|
|
209
288
|
|
|
210
289
|
export interface InitDatabaseOptions {
|
|
211
290
|
seedDemoData?: boolean;
|
|
@@ -233,6 +312,7 @@ const BASE_SCHEMA_SQL = `
|
|
|
233
312
|
bio text not null,
|
|
234
313
|
followers_count integer not null default 0,
|
|
235
314
|
following_count integer not null default 0,
|
|
315
|
+
public_metrics_json text not null default '{}',
|
|
236
316
|
avatar_hue integer not null default 0,
|
|
237
317
|
avatar_url text,
|
|
238
318
|
location text,
|
|
@@ -407,6 +487,80 @@ const BASE_SCHEMA_SQL = `
|
|
|
407
487
|
updated_at text not null
|
|
408
488
|
);
|
|
409
489
|
|
|
490
|
+
create table if not exists url_expansions (
|
|
491
|
+
short_url text primary key,
|
|
492
|
+
expanded_url text not null,
|
|
493
|
+
final_url text not null,
|
|
494
|
+
status text not null,
|
|
495
|
+
expanded_tweet_id text,
|
|
496
|
+
expanded_handle text,
|
|
497
|
+
title text,
|
|
498
|
+
description text,
|
|
499
|
+
image_url text,
|
|
500
|
+
site_name text,
|
|
501
|
+
error text,
|
|
502
|
+
source text not null,
|
|
503
|
+
updated_at text not null
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
create table if not exists link_occurrences (
|
|
507
|
+
source_kind text not null,
|
|
508
|
+
source_id text not null,
|
|
509
|
+
source_position integer not null,
|
|
510
|
+
short_url text not null,
|
|
511
|
+
account_id text,
|
|
512
|
+
conversation_id text,
|
|
513
|
+
direction text,
|
|
514
|
+
created_at text not null,
|
|
515
|
+
primary key (source_kind, source_id, source_position, short_url)
|
|
516
|
+
);
|
|
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
|
+
|
|
410
564
|
create virtual table if not exists tweets_fts using fts5(
|
|
411
565
|
tweet_id unindexed,
|
|
412
566
|
text
|
|
@@ -430,6 +584,7 @@ const INDEX_SQL = `
|
|
|
430
584
|
create index if not exists idx_dm_messages_conversation on dm_messages(conversation_id, created_at asc);
|
|
431
585
|
create index if not exists idx_profiles_followers on profiles(followers_count desc);
|
|
432
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);
|
|
433
588
|
create index if not exists idx_profile_affiliations_subject on profile_affiliations(subject_profile_id, is_active, last_seen_at desc);
|
|
434
589
|
create index if not exists idx_profile_affiliations_org on profile_affiliations(organization_profile_id, is_active, last_seen_at desc);
|
|
435
590
|
create index if not exists idx_profile_snapshots_profile on profile_snapshots(profile_id, last_seen_at desc);
|
|
@@ -441,6 +596,17 @@ const INDEX_SQL = `
|
|
|
441
596
|
create index if not exists idx_mutes_account_created on mutes(account_id, created_at desc);
|
|
442
597
|
create index if not exists idx_ai_scores_updated on ai_scores(updated_at desc);
|
|
443
598
|
create index if not exists idx_sync_cache_updated on sync_cache(updated_at desc);
|
|
599
|
+
create index if not exists idx_url_expansions_expanded on url_expansions(expanded_url);
|
|
600
|
+
create index if not exists idx_url_expansions_tweet on url_expansions(expanded_tweet_id);
|
|
601
|
+
create index if not exists idx_url_expansions_handle on url_expansions(expanded_handle);
|
|
602
|
+
create index if not exists idx_link_occurrences_url on link_occurrences(short_url);
|
|
603
|
+
create index if not exists idx_link_occurrences_created on link_occurrences(created_at desc);
|
|
604
|
+
create index if not exists idx_link_occurrences_account on link_occurrences(account_id, created_at desc);
|
|
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);
|
|
444
610
|
`;
|
|
445
611
|
|
|
446
612
|
function getColumnNames(db: Database, tableName: string): Set<string> {
|
|
@@ -496,6 +662,11 @@ function ensureProfileAvatarColumns(db: Database) {
|
|
|
496
662
|
"alter table profiles add column raw_json text not null default '{}'",
|
|
497
663
|
);
|
|
498
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
|
+
}
|
|
499
670
|
}
|
|
500
671
|
|
|
501
672
|
function ensureAccountExternalUserIdColumn(db: Database) {
|
|
@@ -612,6 +783,96 @@ function ensureIdentitySearchIndexTable(db: Database) {
|
|
|
612
783
|
`);
|
|
613
784
|
}
|
|
614
785
|
|
|
786
|
+
function ensureLinkIndexTables(db: Database) {
|
|
787
|
+
db.exec(`
|
|
788
|
+
create table if not exists url_expansions (
|
|
789
|
+
short_url text primary key,
|
|
790
|
+
expanded_url text not null,
|
|
791
|
+
final_url text not null,
|
|
792
|
+
status text not null,
|
|
793
|
+
expanded_tweet_id text,
|
|
794
|
+
expanded_handle text,
|
|
795
|
+
title text,
|
|
796
|
+
description text,
|
|
797
|
+
image_url text,
|
|
798
|
+
site_name text,
|
|
799
|
+
error text,
|
|
800
|
+
source text not null,
|
|
801
|
+
updated_at text not null
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
create table if not exists link_occurrences (
|
|
805
|
+
source_kind text not null,
|
|
806
|
+
source_id text not null,
|
|
807
|
+
source_position integer not null,
|
|
808
|
+
short_url text not null,
|
|
809
|
+
account_id text,
|
|
810
|
+
conversation_id text,
|
|
811
|
+
direction text,
|
|
812
|
+
created_at text not null,
|
|
813
|
+
primary key (source_kind, source_id, source_position, short_url)
|
|
814
|
+
);
|
|
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
|
+
`);
|
|
874
|
+
}
|
|
875
|
+
|
|
615
876
|
function backfillTweetCollections(db: Database) {
|
|
616
877
|
const now = new Date().toISOString();
|
|
617
878
|
const insert = db.prepare(`
|
|
@@ -659,6 +920,17 @@ function ensureSchemaIndexes(db: Database) {
|
|
|
659
920
|
db.exec(INDEX_SQL);
|
|
660
921
|
}
|
|
661
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
|
+
|
|
662
934
|
function initDatabase(options: InitDatabaseOptions = {}) {
|
|
663
935
|
ensureBirdclawDirs();
|
|
664
936
|
|
|
@@ -675,12 +947,17 @@ function initDatabase(options: InitDatabaseOptions = {}) {
|
|
|
675
947
|
ensureProfileSnapshotsTable(nativeDb);
|
|
676
948
|
ensureProfileBioEntitiesTable(nativeDb);
|
|
677
949
|
ensureIdentitySearchIndexTable(nativeDb);
|
|
950
|
+
ensureLinkIndexTables(nativeDb);
|
|
951
|
+
ensureFollowGraphTables(nativeDb);
|
|
678
952
|
ensureSchemaIndexes(nativeDb);
|
|
679
953
|
if (options.seedDemoData !== false) {
|
|
680
|
-
|
|
954
|
+
ensureDemoData(nativeDb);
|
|
955
|
+
} else {
|
|
956
|
+
backfillTweetCollections(nativeDb);
|
|
957
|
+
backfillTweetAccountEdges(nativeDb);
|
|
681
958
|
}
|
|
682
|
-
|
|
683
|
-
|
|
959
|
+
} else if (options.seedDemoData !== false) {
|
|
960
|
+
ensureDemoData(nativeDb);
|
|
684
961
|
}
|
|
685
962
|
|
|
686
963
|
if (!kyselyDb) {
|
|
@@ -708,4 +985,5 @@ export function resetDatabaseForTests() {
|
|
|
708
985
|
|
|
709
986
|
nativeDb?.close();
|
|
710
987
|
nativeDb = undefined;
|
|
988
|
+
demoSeedAttempted = false;
|
|
711
989
|
}
|