birdclaw 0.3.0 → 0.4.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 +25 -0
- package/README.md +37 -9
- package/package.json +4 -6
- package/src/cli.ts +289 -20
- package/src/lib/archive-import.ts +27 -0
- package/src/lib/backup.ts +271 -33
- package/src/lib/bird-actions.ts +17 -0
- package/src/lib/bird.ts +189 -0
- package/src/lib/blocks.ts +3 -3
- package/src/lib/bookmark-sync-job.ts +3 -3
- package/src/lib/db.ts +404 -14
- package/src/lib/dms-live.ts +3 -3
- package/src/lib/identity-search-index.ts +369 -0
- package/src/lib/link-index.ts +716 -0
- package/src/lib/mention-threads-live.ts +5 -9
- package/src/lib/mentions-live.ts +18 -7
- package/src/lib/moderation-target.ts +4 -4
- package/src/lib/moderation-write.ts +3 -3
- package/src/lib/profile-affiliation-hydration.ts +189 -0
- package/src/lib/profile-affiliations.ts +262 -0
- package/src/lib/profile-bio-entities.ts +318 -0
- package/src/lib/profile-history.ts +164 -0
- package/src/lib/profile-hydration.ts +4 -28
- package/src/lib/profile-resolver.ts +428 -0
- package/src/lib/queries.ts +391 -57
- package/src/lib/research.ts +11 -0
- package/src/lib/seed.ts +2 -2
- package/src/lib/sqlite.ts +143 -0
- package/src/lib/timeline-collections-live.ts +7 -7
- package/src/lib/timeline-live.ts +16 -9
- package/src/lib/tweet-account-edges.ts +39 -0
- package/src/lib/types.ts +108 -0
- package/src/lib/url-expansion.ts +155 -0
- package/src/lib/whois.ts +1060 -0
- package/src/lib/x-profile.ts +140 -12
- package/src/lib/xurl.ts +7 -6
package/src/lib/db.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import NativeSqliteDatabase, { type Database } from "./sqlite";
|
|
2
2
|
import { Kysely, SqliteDialect } from "kysely";
|
|
3
3
|
import { ensureBirdclawDirs, getBirdclawPaths } from "./config";
|
|
4
4
|
import { seedDemoData } from "./seed";
|
|
@@ -22,9 +22,69 @@ export interface ProfilesTable {
|
|
|
22
22
|
following_count: number;
|
|
23
23
|
avatar_hue: number;
|
|
24
24
|
avatar_url: string | null;
|
|
25
|
+
location: string | null;
|
|
26
|
+
url: string | null;
|
|
27
|
+
verified_type: string | null;
|
|
28
|
+
entities_json: string;
|
|
29
|
+
raw_json: string;
|
|
25
30
|
created_at: string;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
export interface ProfileAffiliationsTable {
|
|
34
|
+
subject_profile_id: string;
|
|
35
|
+
organization_profile_id: string;
|
|
36
|
+
organization_name: string | null;
|
|
37
|
+
organization_handle: string | null;
|
|
38
|
+
badge_url: string | null;
|
|
39
|
+
url: string | null;
|
|
40
|
+
label: string | null;
|
|
41
|
+
source: string;
|
|
42
|
+
is_active: number;
|
|
43
|
+
first_seen_at: string;
|
|
44
|
+
last_seen_at: string;
|
|
45
|
+
raw_json: string;
|
|
46
|
+
updated_at: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ProfileSnapshotsTable {
|
|
50
|
+
profile_id: string;
|
|
51
|
+
snapshot_hash: string;
|
|
52
|
+
observed_at: string;
|
|
53
|
+
last_seen_at: string;
|
|
54
|
+
source: string;
|
|
55
|
+
handle: string;
|
|
56
|
+
display_name: string;
|
|
57
|
+
bio: string;
|
|
58
|
+
location: string | null;
|
|
59
|
+
url: string | null;
|
|
60
|
+
verified_type: string | null;
|
|
61
|
+
followers_count: number;
|
|
62
|
+
following_count: number;
|
|
63
|
+
affiliations_json: string;
|
|
64
|
+
raw_json: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ProfileBioEntitiesTable {
|
|
68
|
+
profile_id: string;
|
|
69
|
+
kind: string;
|
|
70
|
+
value: string;
|
|
71
|
+
source: string;
|
|
72
|
+
is_active: number;
|
|
73
|
+
first_seen_at: string;
|
|
74
|
+
last_seen_at: string;
|
|
75
|
+
raw_json: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface IdentitySearchIndexTable {
|
|
79
|
+
profile_id: string;
|
|
80
|
+
kind: string;
|
|
81
|
+
value: string;
|
|
82
|
+
normalized_value: string;
|
|
83
|
+
source: string;
|
|
84
|
+
weight: number;
|
|
85
|
+
updated_at: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
28
88
|
export interface TweetsTable {
|
|
29
89
|
id: string;
|
|
30
90
|
account_id: string;
|
|
@@ -53,6 +113,18 @@ export interface TweetCollectionsTable {
|
|
|
53
113
|
updated_at: string;
|
|
54
114
|
}
|
|
55
115
|
|
|
116
|
+
export interface TweetAccountEdgesTable {
|
|
117
|
+
account_id: string;
|
|
118
|
+
tweet_id: string;
|
|
119
|
+
kind: "home" | "mention";
|
|
120
|
+
first_seen_at: string;
|
|
121
|
+
last_seen_at: string;
|
|
122
|
+
seen_count: number;
|
|
123
|
+
source: string;
|
|
124
|
+
raw_json: string;
|
|
125
|
+
updated_at: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
56
128
|
export interface DmConversationsTable {
|
|
57
129
|
id: string;
|
|
58
130
|
account_id: string;
|
|
@@ -113,11 +185,41 @@ export interface SyncCacheTable {
|
|
|
113
185
|
updated_at: string;
|
|
114
186
|
}
|
|
115
187
|
|
|
188
|
+
export interface UrlExpansionsTable {
|
|
189
|
+
short_url: string;
|
|
190
|
+
expanded_url: string;
|
|
191
|
+
final_url: string;
|
|
192
|
+
status: string;
|
|
193
|
+
expanded_tweet_id: string | null;
|
|
194
|
+
expanded_handle: string | null;
|
|
195
|
+
title: string | null;
|
|
196
|
+
description: string | null;
|
|
197
|
+
error: string | null;
|
|
198
|
+
source: string;
|
|
199
|
+
updated_at: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface LinkOccurrencesTable {
|
|
203
|
+
source_kind: "dm" | "tweet";
|
|
204
|
+
source_id: string;
|
|
205
|
+
source_position: number;
|
|
206
|
+
short_url: string;
|
|
207
|
+
account_id: string | null;
|
|
208
|
+
conversation_id: string | null;
|
|
209
|
+
direction: string | null;
|
|
210
|
+
created_at: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
116
213
|
export interface BirdclawDatabase {
|
|
117
214
|
accounts: AccountsTable;
|
|
118
215
|
profiles: ProfilesTable;
|
|
216
|
+
profile_affiliations: ProfileAffiliationsTable;
|
|
217
|
+
profile_snapshots: ProfileSnapshotsTable;
|
|
218
|
+
profile_bio_entities: ProfileBioEntitiesTable;
|
|
219
|
+
identity_search_index: IdentitySearchIndexTable;
|
|
119
220
|
tweets: TweetsTable;
|
|
120
221
|
tweet_collections: TweetCollectionsTable;
|
|
222
|
+
tweet_account_edges: TweetAccountEdgesTable;
|
|
121
223
|
dm_conversations: DmConversationsTable;
|
|
122
224
|
dm_messages: DmMessagesTable;
|
|
123
225
|
tweet_actions: TweetActionsTable;
|
|
@@ -125,9 +227,11 @@ export interface BirdclawDatabase {
|
|
|
125
227
|
mutes: MutesTable;
|
|
126
228
|
ai_scores: AiScoresTable;
|
|
127
229
|
sync_cache: SyncCacheTable;
|
|
230
|
+
url_expansions: UrlExpansionsTable;
|
|
231
|
+
link_occurrences: LinkOccurrencesTable;
|
|
128
232
|
}
|
|
129
233
|
|
|
130
|
-
let nativeDb:
|
|
234
|
+
let nativeDb: Database | undefined;
|
|
131
235
|
let kyselyDb: Kysely<BirdclawDatabase> | undefined;
|
|
132
236
|
|
|
133
237
|
export interface InitDatabaseOptions {
|
|
@@ -158,9 +262,73 @@ const BASE_SCHEMA_SQL = `
|
|
|
158
262
|
following_count integer not null default 0,
|
|
159
263
|
avatar_hue integer not null default 0,
|
|
160
264
|
avatar_url text,
|
|
265
|
+
location text,
|
|
266
|
+
url text,
|
|
267
|
+
verified_type text,
|
|
268
|
+
entities_json text not null default '{}',
|
|
269
|
+
raw_json text not null default '{}',
|
|
161
270
|
created_at text not null
|
|
162
271
|
);
|
|
163
272
|
|
|
273
|
+
create table if not exists profile_affiliations (
|
|
274
|
+
subject_profile_id text not null,
|
|
275
|
+
organization_profile_id text not null,
|
|
276
|
+
organization_name text,
|
|
277
|
+
organization_handle text,
|
|
278
|
+
badge_url text,
|
|
279
|
+
url text,
|
|
280
|
+
label text,
|
|
281
|
+
source text not null,
|
|
282
|
+
is_active integer not null default 1,
|
|
283
|
+
first_seen_at text not null,
|
|
284
|
+
last_seen_at text not null,
|
|
285
|
+
raw_json text not null default '{}',
|
|
286
|
+
updated_at text not null,
|
|
287
|
+
primary key (subject_profile_id, organization_profile_id)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
create table if not exists profile_snapshots (
|
|
291
|
+
profile_id text not null,
|
|
292
|
+
snapshot_hash text not null,
|
|
293
|
+
observed_at text not null,
|
|
294
|
+
last_seen_at text not null,
|
|
295
|
+
source text not null,
|
|
296
|
+
handle text not null,
|
|
297
|
+
display_name text not null,
|
|
298
|
+
bio text not null,
|
|
299
|
+
location text,
|
|
300
|
+
url text,
|
|
301
|
+
verified_type text,
|
|
302
|
+
followers_count integer not null default 0,
|
|
303
|
+
following_count integer not null default 0,
|
|
304
|
+
affiliations_json text not null default '[]',
|
|
305
|
+
raw_json text not null default '{}',
|
|
306
|
+
primary key (profile_id, snapshot_hash)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
create table if not exists profile_bio_entities (
|
|
310
|
+
profile_id text not null,
|
|
311
|
+
kind text not null,
|
|
312
|
+
value text not null,
|
|
313
|
+
source text not null,
|
|
314
|
+
is_active integer not null default 1,
|
|
315
|
+
first_seen_at text not null,
|
|
316
|
+
last_seen_at text not null,
|
|
317
|
+
raw_json text not null default '{}',
|
|
318
|
+
primary key (profile_id, kind, value)
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
create table if not exists identity_search_index (
|
|
322
|
+
profile_id text not null,
|
|
323
|
+
kind text not null,
|
|
324
|
+
value text not null,
|
|
325
|
+
normalized_value text not null,
|
|
326
|
+
source text not null,
|
|
327
|
+
weight integer not null,
|
|
328
|
+
updated_at text not null,
|
|
329
|
+
primary key (profile_id, kind, value, source)
|
|
330
|
+
);
|
|
331
|
+
|
|
164
332
|
create table if not exists tweets (
|
|
165
333
|
id text primary key,
|
|
166
334
|
account_id text not null,
|
|
@@ -190,6 +358,19 @@ const BASE_SCHEMA_SQL = `
|
|
|
190
358
|
primary key (account_id, tweet_id, kind)
|
|
191
359
|
);
|
|
192
360
|
|
|
361
|
+
create table if not exists tweet_account_edges (
|
|
362
|
+
account_id text not null,
|
|
363
|
+
tweet_id text not null,
|
|
364
|
+
kind text not null,
|
|
365
|
+
first_seen_at text not null,
|
|
366
|
+
last_seen_at text not null,
|
|
367
|
+
seen_count integer not null default 1,
|
|
368
|
+
source text not null,
|
|
369
|
+
raw_json text not null default '{}',
|
|
370
|
+
updated_at text not null,
|
|
371
|
+
primary key (account_id, tweet_id, kind)
|
|
372
|
+
);
|
|
373
|
+
|
|
193
374
|
create table if not exists dm_conversations (
|
|
194
375
|
id text primary key,
|
|
195
376
|
account_id text not null,
|
|
@@ -253,6 +434,32 @@ const BASE_SCHEMA_SQL = `
|
|
|
253
434
|
updated_at text not null
|
|
254
435
|
);
|
|
255
436
|
|
|
437
|
+
create table if not exists url_expansions (
|
|
438
|
+
short_url text primary key,
|
|
439
|
+
expanded_url text not null,
|
|
440
|
+
final_url text not null,
|
|
441
|
+
status text not null,
|
|
442
|
+
expanded_tweet_id text,
|
|
443
|
+
expanded_handle text,
|
|
444
|
+
title text,
|
|
445
|
+
description text,
|
|
446
|
+
error text,
|
|
447
|
+
source text not null,
|
|
448
|
+
updated_at text not null
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
create table if not exists link_occurrences (
|
|
452
|
+
source_kind text not null,
|
|
453
|
+
source_id text not null,
|
|
454
|
+
source_position integer not null,
|
|
455
|
+
short_url text not null,
|
|
456
|
+
account_id text,
|
|
457
|
+
conversation_id text,
|
|
458
|
+
direction text,
|
|
459
|
+
created_at text not null,
|
|
460
|
+
primary key (source_kind, source_id, source_position, short_url)
|
|
461
|
+
);
|
|
462
|
+
|
|
256
463
|
create virtual table if not exists tweets_fts using fts5(
|
|
257
464
|
tweet_id unindexed,
|
|
258
465
|
text
|
|
@@ -270,27 +477,40 @@ const INDEX_SQL = `
|
|
|
270
477
|
create index if not exists idx_tweets_quoted on tweets(quoted_tweet_id);
|
|
271
478
|
create index if not exists idx_tweet_collections_kind_account on tweet_collections(kind, account_id, collected_at desc, tweet_id);
|
|
272
479
|
create index if not exists idx_tweet_collections_tweet on tweet_collections(tweet_id);
|
|
480
|
+
create index if not exists idx_tweet_account_edges_kind_account on tweet_account_edges(kind, account_id, last_seen_at desc, tweet_id);
|
|
481
|
+
create index if not exists idx_tweet_account_edges_tweet on tweet_account_edges(tweet_id);
|
|
273
482
|
create index if not exists idx_dm_conversations_account on dm_conversations(account_id, last_message_at desc);
|
|
274
483
|
create index if not exists idx_dm_messages_conversation on dm_messages(conversation_id, created_at asc);
|
|
275
484
|
create index if not exists idx_profiles_followers on profiles(followers_count desc);
|
|
276
485
|
create index if not exists idx_profiles_following on profiles(following_count desc);
|
|
486
|
+
create index if not exists idx_profile_affiliations_subject on profile_affiliations(subject_profile_id, is_active, last_seen_at desc);
|
|
487
|
+
create index if not exists idx_profile_affiliations_org on profile_affiliations(organization_profile_id, is_active, last_seen_at desc);
|
|
488
|
+
create index if not exists idx_profile_snapshots_profile on profile_snapshots(profile_id, last_seen_at desc);
|
|
489
|
+
create index if not exists idx_profile_bio_entities_profile on profile_bio_entities(profile_id, is_active, last_seen_at desc);
|
|
490
|
+
create index if not exists idx_profile_bio_entities_value on profile_bio_entities(kind, value, is_active);
|
|
491
|
+
create index if not exists idx_identity_search_index_profile on identity_search_index(profile_id);
|
|
492
|
+
create index if not exists idx_identity_search_index_value on identity_search_index(normalized_value, kind, weight desc);
|
|
277
493
|
create index if not exists idx_blocks_account_created on blocks(account_id, created_at desc);
|
|
278
494
|
create index if not exists idx_mutes_account_created on mutes(account_id, created_at desc);
|
|
279
495
|
create index if not exists idx_ai_scores_updated on ai_scores(updated_at desc);
|
|
280
496
|
create index if not exists idx_sync_cache_updated on sync_cache(updated_at desc);
|
|
497
|
+
create index if not exists idx_url_expansions_expanded on url_expansions(expanded_url);
|
|
498
|
+
create index if not exists idx_url_expansions_tweet on url_expansions(expanded_tweet_id);
|
|
499
|
+
create index if not exists idx_url_expansions_handle on url_expansions(expanded_handle);
|
|
500
|
+
create index if not exists idx_link_occurrences_url on link_occurrences(short_url);
|
|
501
|
+
create index if not exists idx_link_occurrences_created on link_occurrences(created_at desc);
|
|
502
|
+
create index if not exists idx_link_occurrences_account on link_occurrences(account_id, created_at desc);
|
|
503
|
+
create index if not exists idx_link_occurrences_direction on link_occurrences(direction, created_at desc);
|
|
281
504
|
`;
|
|
282
505
|
|
|
283
|
-
function getColumnNames(
|
|
284
|
-
db: BetterSqlite3.Database,
|
|
285
|
-
tableName: string,
|
|
286
|
-
): Set<string> {
|
|
506
|
+
function getColumnNames(db: Database, tableName: string): Set<string> {
|
|
287
507
|
const rows = db.prepare(`pragma table_info(${tableName})`).all() as Array<{
|
|
288
508
|
name: string;
|
|
289
509
|
}>;
|
|
290
510
|
return new Set(rows.map((row) => row.name));
|
|
291
511
|
}
|
|
292
512
|
|
|
293
|
-
function ensureTweetMetadataColumns(db:
|
|
513
|
+
function ensureTweetMetadataColumns(db: Database) {
|
|
294
514
|
const columnNames = getColumnNames(db, "tweets");
|
|
295
515
|
if (!columnNames.has("entities_json")) {
|
|
296
516
|
db.exec(
|
|
@@ -307,7 +527,7 @@ function ensureTweetMetadataColumns(db: BetterSqlite3.Database) {
|
|
|
307
527
|
}
|
|
308
528
|
}
|
|
309
529
|
|
|
310
|
-
function ensureProfileAvatarColumns(db:
|
|
530
|
+
function ensureProfileAvatarColumns(db: Database) {
|
|
311
531
|
const columnNames = getColumnNames(db, "profiles");
|
|
312
532
|
if (!columnNames.has("following_count")) {
|
|
313
533
|
db.exec(
|
|
@@ -317,16 +537,35 @@ function ensureProfileAvatarColumns(db: BetterSqlite3.Database) {
|
|
|
317
537
|
if (!columnNames.has("avatar_url")) {
|
|
318
538
|
db.exec("alter table profiles add column avatar_url text");
|
|
319
539
|
}
|
|
540
|
+
if (!columnNames.has("location")) {
|
|
541
|
+
db.exec("alter table profiles add column location text");
|
|
542
|
+
}
|
|
543
|
+
if (!columnNames.has("url")) {
|
|
544
|
+
db.exec("alter table profiles add column url text");
|
|
545
|
+
}
|
|
546
|
+
if (!columnNames.has("verified_type")) {
|
|
547
|
+
db.exec("alter table profiles add column verified_type text");
|
|
548
|
+
}
|
|
549
|
+
if (!columnNames.has("entities_json")) {
|
|
550
|
+
db.exec(
|
|
551
|
+
"alter table profiles add column entities_json text not null default '{}'",
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
if (!columnNames.has("raw_json")) {
|
|
555
|
+
db.exec(
|
|
556
|
+
"alter table profiles add column raw_json text not null default '{}'",
|
|
557
|
+
);
|
|
558
|
+
}
|
|
320
559
|
}
|
|
321
560
|
|
|
322
|
-
function ensureAccountExternalUserIdColumn(db:
|
|
561
|
+
function ensureAccountExternalUserIdColumn(db: Database) {
|
|
323
562
|
const columnNames = getColumnNames(db, "accounts");
|
|
324
563
|
if (!columnNames.has("external_user_id")) {
|
|
325
564
|
db.exec("alter table accounts add column external_user_id text");
|
|
326
565
|
}
|
|
327
566
|
}
|
|
328
567
|
|
|
329
|
-
function ensureTweetCollectionsTable(db:
|
|
568
|
+
function ensureTweetCollectionsTable(db: Database) {
|
|
330
569
|
db.exec(`
|
|
331
570
|
create table if not exists tweet_collections (
|
|
332
571
|
account_id text not null,
|
|
@@ -341,7 +580,129 @@ function ensureTweetCollectionsTable(db: BetterSqlite3.Database) {
|
|
|
341
580
|
`);
|
|
342
581
|
}
|
|
343
582
|
|
|
344
|
-
function
|
|
583
|
+
function ensureTweetAccountEdgesTable(db: Database) {
|
|
584
|
+
db.exec(`
|
|
585
|
+
create table if not exists tweet_account_edges (
|
|
586
|
+
account_id text not null,
|
|
587
|
+
tweet_id text not null,
|
|
588
|
+
kind text not null,
|
|
589
|
+
first_seen_at text not null,
|
|
590
|
+
last_seen_at text not null,
|
|
591
|
+
seen_count integer not null default 1,
|
|
592
|
+
source text not null,
|
|
593
|
+
raw_json text not null default '{}',
|
|
594
|
+
updated_at text not null,
|
|
595
|
+
primary key (account_id, tweet_id, kind)
|
|
596
|
+
);
|
|
597
|
+
`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function ensureProfileAffiliationsTable(db: Database) {
|
|
601
|
+
db.exec(`
|
|
602
|
+
create table if not exists profile_affiliations (
|
|
603
|
+
subject_profile_id text not null,
|
|
604
|
+
organization_profile_id text not null,
|
|
605
|
+
organization_name text,
|
|
606
|
+
organization_handle text,
|
|
607
|
+
badge_url text,
|
|
608
|
+
url text,
|
|
609
|
+
label text,
|
|
610
|
+
source text not null,
|
|
611
|
+
is_active integer not null default 1,
|
|
612
|
+
first_seen_at text not null,
|
|
613
|
+
last_seen_at text not null,
|
|
614
|
+
raw_json text not null default '{}',
|
|
615
|
+
updated_at text not null,
|
|
616
|
+
primary key (subject_profile_id, organization_profile_id)
|
|
617
|
+
);
|
|
618
|
+
`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function ensureProfileSnapshotsTable(db: Database) {
|
|
622
|
+
db.exec(`
|
|
623
|
+
create table if not exists profile_snapshots (
|
|
624
|
+
profile_id text not null,
|
|
625
|
+
snapshot_hash text not null,
|
|
626
|
+
observed_at text not null,
|
|
627
|
+
last_seen_at text not null,
|
|
628
|
+
source text not null,
|
|
629
|
+
handle text not null,
|
|
630
|
+
display_name text not null,
|
|
631
|
+
bio text not null,
|
|
632
|
+
location text,
|
|
633
|
+
url text,
|
|
634
|
+
verified_type text,
|
|
635
|
+
followers_count integer not null default 0,
|
|
636
|
+
following_count integer not null default 0,
|
|
637
|
+
affiliations_json text not null default '[]',
|
|
638
|
+
raw_json text not null default '{}',
|
|
639
|
+
primary key (profile_id, snapshot_hash)
|
|
640
|
+
);
|
|
641
|
+
`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function ensureProfileBioEntitiesTable(db: Database) {
|
|
645
|
+
db.exec(`
|
|
646
|
+
create table if not exists profile_bio_entities (
|
|
647
|
+
profile_id text not null,
|
|
648
|
+
kind text not null,
|
|
649
|
+
value text not null,
|
|
650
|
+
source text not null,
|
|
651
|
+
is_active integer not null default 1,
|
|
652
|
+
first_seen_at text not null,
|
|
653
|
+
last_seen_at text not null,
|
|
654
|
+
raw_json text not null default '{}',
|
|
655
|
+
primary key (profile_id, kind, value)
|
|
656
|
+
);
|
|
657
|
+
`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function ensureIdentitySearchIndexTable(db: Database) {
|
|
661
|
+
db.exec(`
|
|
662
|
+
create table if not exists identity_search_index (
|
|
663
|
+
profile_id text not null,
|
|
664
|
+
kind text not null,
|
|
665
|
+
value text not null,
|
|
666
|
+
normalized_value text not null,
|
|
667
|
+
source text not null,
|
|
668
|
+
weight integer not null,
|
|
669
|
+
updated_at text not null,
|
|
670
|
+
primary key (profile_id, kind, value, source)
|
|
671
|
+
);
|
|
672
|
+
`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function ensureLinkIndexTables(db: Database) {
|
|
676
|
+
db.exec(`
|
|
677
|
+
create table if not exists url_expansions (
|
|
678
|
+
short_url text primary key,
|
|
679
|
+
expanded_url text not null,
|
|
680
|
+
final_url text not null,
|
|
681
|
+
status text not null,
|
|
682
|
+
expanded_tweet_id text,
|
|
683
|
+
expanded_handle text,
|
|
684
|
+
title text,
|
|
685
|
+
description text,
|
|
686
|
+
error text,
|
|
687
|
+
source text not null,
|
|
688
|
+
updated_at text not null
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
create table if not exists link_occurrences (
|
|
692
|
+
source_kind text not null,
|
|
693
|
+
source_id text not null,
|
|
694
|
+
source_position integer not null,
|
|
695
|
+
short_url text not null,
|
|
696
|
+
account_id text,
|
|
697
|
+
conversation_id text,
|
|
698
|
+
direction text,
|
|
699
|
+
created_at text not null,
|
|
700
|
+
primary key (source_kind, source_id, source_position, short_url)
|
|
701
|
+
);
|
|
702
|
+
`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function backfillTweetCollections(db: Database) {
|
|
345
706
|
const now = new Date().toISOString();
|
|
346
707
|
const insert = db.prepare(`
|
|
347
708
|
insert or ignore into tweet_collections (
|
|
@@ -362,7 +723,29 @@ function backfillTweetCollections(db: BetterSqlite3.Database) {
|
|
|
362
723
|
})();
|
|
363
724
|
}
|
|
364
725
|
|
|
365
|
-
function
|
|
726
|
+
function backfillTweetAccountEdges(db: Database) {
|
|
727
|
+
const now = new Date().toISOString();
|
|
728
|
+
db.prepare(`
|
|
729
|
+
insert or ignore into tweet_account_edges (
|
|
730
|
+
account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count,
|
|
731
|
+
source, raw_json, updated_at
|
|
732
|
+
)
|
|
733
|
+
select
|
|
734
|
+
account_id,
|
|
735
|
+
id,
|
|
736
|
+
kind,
|
|
737
|
+
created_at,
|
|
738
|
+
created_at,
|
|
739
|
+
1,
|
|
740
|
+
'legacy',
|
|
741
|
+
'{}',
|
|
742
|
+
?
|
|
743
|
+
from tweets
|
|
744
|
+
where kind in ('home', 'mention')
|
|
745
|
+
`).run(now);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function ensureSchemaIndexes(db: Database) {
|
|
366
749
|
db.exec(INDEX_SQL);
|
|
367
750
|
}
|
|
368
751
|
|
|
@@ -371,17 +754,24 @@ function initDatabase(options: InitDatabaseOptions = {}) {
|
|
|
371
754
|
|
|
372
755
|
if (!nativeDb) {
|
|
373
756
|
const { dbPath } = getBirdclawPaths();
|
|
374
|
-
nativeDb = new
|
|
757
|
+
nativeDb = new NativeSqliteDatabase(dbPath);
|
|
375
758
|
nativeDb.exec(BASE_SCHEMA_SQL);
|
|
376
759
|
ensureAccountExternalUserIdColumn(nativeDb);
|
|
377
760
|
ensureTweetMetadataColumns(nativeDb);
|
|
378
761
|
ensureProfileAvatarColumns(nativeDb);
|
|
379
762
|
ensureTweetCollectionsTable(nativeDb);
|
|
763
|
+
ensureTweetAccountEdgesTable(nativeDb);
|
|
764
|
+
ensureProfileAffiliationsTable(nativeDb);
|
|
765
|
+
ensureProfileSnapshotsTable(nativeDb);
|
|
766
|
+
ensureProfileBioEntitiesTable(nativeDb);
|
|
767
|
+
ensureIdentitySearchIndexTable(nativeDb);
|
|
768
|
+
ensureLinkIndexTables(nativeDb);
|
|
380
769
|
ensureSchemaIndexes(nativeDb);
|
|
381
770
|
if (options.seedDemoData !== false) {
|
|
382
771
|
seedDemoData(nativeDb);
|
|
383
772
|
}
|
|
384
773
|
backfillTweetCollections(nativeDb);
|
|
774
|
+
backfillTweetAccountEdges(nativeDb);
|
|
385
775
|
}
|
|
386
776
|
|
|
387
777
|
if (!kyselyDb) {
|
|
@@ -395,7 +785,7 @@ function initDatabase(options: InitDatabaseOptions = {}) {
|
|
|
395
785
|
|
|
396
786
|
export function getNativeDb(options: InitDatabaseOptions = {}) {
|
|
397
787
|
initDatabase(options);
|
|
398
|
-
return nativeDb as
|
|
788
|
+
return nativeDb as Database;
|
|
399
789
|
}
|
|
400
790
|
|
|
401
791
|
export function getDb() {
|
package/src/lib/dms-live.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
2
|
import {
|
|
3
3
|
type BirdDmConversation,
|
|
4
4
|
type BirdDmEvent,
|
|
@@ -25,7 +25,7 @@ function assertBirdLimit(limit: number) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function resolveAccount(db: Database
|
|
28
|
+
function resolveAccount(db: Database, accountId?: string) {
|
|
29
29
|
const row = accountId
|
|
30
30
|
? (db
|
|
31
31
|
.prepare("select id, handle from accounts where id = ?")
|
|
@@ -112,7 +112,7 @@ function getLatestEvent(events: BirdDmEvent[]) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function mergeDirectMessagesIntoLocalStore(
|
|
115
|
-
db: Database
|
|
115
|
+
db: Database,
|
|
116
116
|
accountId: string,
|
|
117
117
|
accountUsername: string,
|
|
118
118
|
payload: {
|