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/backup.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import type Database from "
|
|
7
|
+
import type { Database } from "./sqlite";
|
|
8
8
|
import { getBirdclawConfig } from "./config";
|
|
9
9
|
import { getNativeDb } from "./db";
|
|
10
10
|
|
|
@@ -160,17 +160,13 @@ function yearFromTimestamp(value: unknown) {
|
|
|
160
160
|
return match[1];
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function rowsForQuery(
|
|
164
|
-
db: Database.Database,
|
|
165
|
-
sql: string,
|
|
166
|
-
params: unknown[] = [],
|
|
167
|
-
) {
|
|
163
|
+
function rowsForQuery(db: Database, sql: string, params: unknown[] = []) {
|
|
168
164
|
return (db.prepare(sql).all(...params) as Record<string, unknown>[]).map(
|
|
169
165
|
toJsonRecord,
|
|
170
166
|
);
|
|
171
167
|
}
|
|
172
168
|
|
|
173
|
-
function getExportRowSets(db: Database
|
|
169
|
+
function getExportRowSets(db: Database) {
|
|
174
170
|
const rowSets: Array<{ logicalName: string; rows: JsonRecord[] }> = [
|
|
175
171
|
{
|
|
176
172
|
logicalName: "accounts",
|
|
@@ -188,9 +184,49 @@ function getExportRowSets(db: Database.Database) {
|
|
|
188
184
|
rows: rowsForQuery(
|
|
189
185
|
db,
|
|
190
186
|
`
|
|
191
|
-
select id, handle, display_name, bio, followers_count, following_count,
|
|
187
|
+
select id, handle, display_name, bio, followers_count, following_count,
|
|
188
|
+
avatar_hue, avatar_url, location, url, verified_type, entities_json,
|
|
189
|
+
raw_json, created_at
|
|
192
190
|
from profiles
|
|
193
191
|
order by id
|
|
192
|
+
`,
|
|
193
|
+
),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
logicalName: "profile_affiliations",
|
|
197
|
+
rows: rowsForQuery(
|
|
198
|
+
db,
|
|
199
|
+
`
|
|
200
|
+
select subject_profile_id, organization_profile_id, organization_name,
|
|
201
|
+
organization_handle, badge_url, url, label, source, is_active,
|
|
202
|
+
first_seen_at, last_seen_at, raw_json, updated_at
|
|
203
|
+
from profile_affiliations
|
|
204
|
+
order by subject_profile_id, organization_profile_id
|
|
205
|
+
`,
|
|
206
|
+
),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
logicalName: "profile_snapshots",
|
|
210
|
+
rows: rowsForQuery(
|
|
211
|
+
db,
|
|
212
|
+
`
|
|
213
|
+
select profile_id, snapshot_hash, observed_at, last_seen_at, source,
|
|
214
|
+
handle, display_name, bio, location, url, verified_type,
|
|
215
|
+
followers_count, following_count, affiliations_json, raw_json
|
|
216
|
+
from profile_snapshots
|
|
217
|
+
order by profile_id, last_seen_at, snapshot_hash
|
|
218
|
+
`,
|
|
219
|
+
),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
logicalName: "profile_bio_entities",
|
|
223
|
+
rows: rowsForQuery(
|
|
224
|
+
db,
|
|
225
|
+
`
|
|
226
|
+
select profile_id, kind, value, source, is_active, first_seen_at,
|
|
227
|
+
last_seen_at, raw_json
|
|
228
|
+
from profile_bio_entities
|
|
229
|
+
order by profile_id, kind, value
|
|
194
230
|
`,
|
|
195
231
|
),
|
|
196
232
|
},
|
|
@@ -215,6 +251,18 @@ function getExportRowSets(db: Database.Database) {
|
|
|
215
251
|
select account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
|
|
216
252
|
from tweet_collections
|
|
217
253
|
order by kind, account_id, coalesce(collected_at, ''), tweet_id
|
|
254
|
+
`,
|
|
255
|
+
),
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
logicalName: "tweet_account_edges",
|
|
259
|
+
rows: rowsForQuery(
|
|
260
|
+
db,
|
|
261
|
+
`
|
|
262
|
+
select account_id, tweet_id, kind, first_seen_at, last_seen_at,
|
|
263
|
+
seen_count, source, raw_json, updated_at
|
|
264
|
+
from tweet_account_edges
|
|
265
|
+
order by kind, account_id, last_seen_at, tweet_id
|
|
218
266
|
`,
|
|
219
267
|
),
|
|
220
268
|
},
|
|
@@ -303,7 +351,7 @@ function addRows(
|
|
|
303
351
|
shards.set(relativePath, existing);
|
|
304
352
|
}
|
|
305
353
|
|
|
306
|
-
function buildShards(db: Database
|
|
354
|
+
function buildShards(db: Database) {
|
|
307
355
|
const shards = new Map<string, JsonRecord[]>();
|
|
308
356
|
const rowSets = getExportRowSets(db);
|
|
309
357
|
|
|
@@ -315,6 +363,15 @@ function buildShards(db: Database.Database) {
|
|
|
315
363
|
case "profiles":
|
|
316
364
|
addRows(shards, "data/profiles.jsonl", rowSet.rows);
|
|
317
365
|
break;
|
|
366
|
+
case "profile_affiliations":
|
|
367
|
+
addRows(shards, "data/profile_affiliations.jsonl", rowSet.rows);
|
|
368
|
+
break;
|
|
369
|
+
case "profile_snapshots":
|
|
370
|
+
addRows(shards, "data/profile_snapshots.jsonl", rowSet.rows);
|
|
371
|
+
break;
|
|
372
|
+
case "profile_bio_entities":
|
|
373
|
+
addRows(shards, "data/profile_bio_entities.jsonl", rowSet.rows);
|
|
374
|
+
break;
|
|
318
375
|
case "tweets":
|
|
319
376
|
for (const row of rowSet.rows) {
|
|
320
377
|
addRows(
|
|
@@ -333,6 +390,15 @@ function buildShards(db: Database.Database) {
|
|
|
333
390
|
addRows(shards, `data/collections/${kind}.jsonl`, [row]);
|
|
334
391
|
}
|
|
335
392
|
break;
|
|
393
|
+
case "tweet_account_edges":
|
|
394
|
+
for (const row of rowSet.rows) {
|
|
395
|
+
const kind =
|
|
396
|
+
row.kind === "home" || row.kind === "mention"
|
|
397
|
+
? row.kind
|
|
398
|
+
: "unknown";
|
|
399
|
+
addRows(shards, `data/timeline_edges/${kind}.jsonl`, [row]);
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
336
402
|
case "dm_conversations":
|
|
337
403
|
addRows(shards, "data/dms/conversations.jsonl", rowSet.rows);
|
|
338
404
|
break;
|
|
@@ -442,15 +508,17 @@ function computeCounts(files: BackupFileManifest[]) {
|
|
|
442
508
|
? "tweets"
|
|
443
509
|
: second === "collections"
|
|
444
510
|
? `collections_${third?.replace(/\.jsonl$/, "") ?? "unknown"}`
|
|
445
|
-
: second === "
|
|
446
|
-
? "
|
|
447
|
-
: second === "dms"
|
|
448
|
-
? "
|
|
449
|
-
: second === "
|
|
450
|
-
?
|
|
451
|
-
: second === "
|
|
452
|
-
? third?.replace(/\.jsonl$/, "") || "
|
|
453
|
-
: second
|
|
511
|
+
: second === "timeline_edges"
|
|
512
|
+
? `timeline_edges_${third?.replace(/\.jsonl$/, "") ?? "unknown"}`
|
|
513
|
+
: second === "dms" && third === "conversations.jsonl"
|
|
514
|
+
? "dm_conversations"
|
|
515
|
+
: second === "dms"
|
|
516
|
+
? "dm_messages"
|
|
517
|
+
: second === "moderation"
|
|
518
|
+
? third?.replace(/\.jsonl$/, "") || "moderation"
|
|
519
|
+
: second === "actions"
|
|
520
|
+
? third?.replace(/\.jsonl$/, "") || "actions"
|
|
521
|
+
: second?.replace(/\.jsonl$/, "") || "unknown";
|
|
454
522
|
counts[key] = (counts[key] ?? 0) + file.rows;
|
|
455
523
|
}
|
|
456
524
|
return counts;
|
|
@@ -473,8 +541,13 @@ Private text backup for Birdclaw data. The committed files are canonical JSONL s
|
|
|
473
541
|
manifest.json
|
|
474
542
|
data/accounts.jsonl
|
|
475
543
|
data/profiles.jsonl
|
|
544
|
+
data/profile_affiliations.jsonl
|
|
545
|
+
data/profile_snapshots.jsonl
|
|
546
|
+
data/profile_bio_entities.jsonl
|
|
476
547
|
data/tweets/YYYY.jsonl
|
|
477
548
|
data/tweets/unknown.jsonl
|
|
549
|
+
data/timeline_edges/home.jsonl
|
|
550
|
+
data/timeline_edges/mention.jsonl
|
|
478
551
|
data/collections/likes.jsonl
|
|
479
552
|
data/collections/bookmarks.jsonl
|
|
480
553
|
data/dms/conversations.jsonl
|
|
@@ -483,7 +556,7 @@ data/moderation/blocks.jsonl
|
|
|
483
556
|
data/moderation/mutes.jsonl
|
|
484
557
|
\`\`\`
|
|
485
558
|
|
|
486
|
-
Tweets are sharded by creation year. Collection-only tweets whose creation date is unknown live in \`data/tweets/unknown.jsonl\`. DMs are sharded by year and keep \`conversation_id\` in each row.
|
|
559
|
+
Tweets are sharded by creation year. Collection-only tweets whose creation date is unknown live in \`data/tweets/unknown.jsonl\`. Timeline edges keep account-scoped home/mention membership separate from canonical tweet content. DMs are sharded by year and keep \`conversation_id\` in each row.
|
|
487
560
|
|
|
488
561
|
Never commit live tokens, browser cookies, raw SQLite WAL/SHM sidecars, or temporary cache files here.
|
|
489
562
|
`,
|
|
@@ -735,7 +808,7 @@ export async function exportBackup({
|
|
|
735
808
|
validate = true,
|
|
736
809
|
}: {
|
|
737
810
|
repoPath: string;
|
|
738
|
-
db?: Database
|
|
811
|
+
db?: Database;
|
|
739
812
|
commit?: boolean;
|
|
740
813
|
push?: boolean;
|
|
741
814
|
message?: string;
|
|
@@ -847,7 +920,7 @@ function rowsForManifestPath(
|
|
|
847
920
|
}
|
|
848
921
|
|
|
849
922
|
function insertRows(
|
|
850
|
-
db: Database
|
|
923
|
+
db: Database,
|
|
851
924
|
sql: string,
|
|
852
925
|
rows: JsonRecord[],
|
|
853
926
|
keys: string[],
|
|
@@ -859,7 +932,7 @@ function insertRows(
|
|
|
859
932
|
}
|
|
860
933
|
|
|
861
934
|
function readFtsIds(
|
|
862
|
-
db: Database
|
|
935
|
+
db: Database,
|
|
863
936
|
tableName: "tweets_fts" | "dm_fts",
|
|
864
937
|
idColumn: "tweet_id" | "message_id",
|
|
865
938
|
) {
|
|
@@ -870,7 +943,7 @@ function readFtsIds(
|
|
|
870
943
|
}
|
|
871
944
|
|
|
872
945
|
function insertFtsRows(
|
|
873
|
-
db: Database
|
|
946
|
+
db: Database,
|
|
874
947
|
tableName: "tweets_fts" | "dm_fts",
|
|
875
948
|
idColumn: "tweet_id" | "message_id",
|
|
876
949
|
rows: JsonRecord[],
|
|
@@ -892,11 +965,14 @@ function insertFtsRows(
|
|
|
892
965
|
}
|
|
893
966
|
}
|
|
894
967
|
|
|
895
|
-
function clearBackupImportData(db: Database
|
|
968
|
+
function clearBackupImportData(db: Database) {
|
|
896
969
|
db.exec(`
|
|
897
970
|
delete from ai_scores;
|
|
898
971
|
delete from tweet_actions;
|
|
972
|
+
delete from tweet_account_edges;
|
|
899
973
|
delete from tweet_collections;
|
|
974
|
+
delete from link_occurrences;
|
|
975
|
+
delete from url_expansions;
|
|
900
976
|
delete from blocks;
|
|
901
977
|
delete from mutes;
|
|
902
978
|
delete from dm_fts;
|
|
@@ -904,6 +980,9 @@ function clearBackupImportData(db: Database.Database) {
|
|
|
904
980
|
delete from dm_messages;
|
|
905
981
|
delete from dm_conversations;
|
|
906
982
|
delete from tweets;
|
|
983
|
+
delete from profile_bio_entities;
|
|
984
|
+
delete from profile_snapshots;
|
|
985
|
+
delete from profile_affiliations;
|
|
907
986
|
delete from profiles;
|
|
908
987
|
delete from accounts;
|
|
909
988
|
delete from sync_cache;
|
|
@@ -917,7 +996,7 @@ export async function importBackup({
|
|
|
917
996
|
mode = "merge",
|
|
918
997
|
}: {
|
|
919
998
|
repoPath: string;
|
|
920
|
-
db?: Database
|
|
999
|
+
db?: Database;
|
|
921
1000
|
validate?: boolean;
|
|
922
1001
|
mode?: BackupImportMode;
|
|
923
1002
|
}): Promise<BackupImportResult> {
|
|
@@ -938,8 +1017,12 @@ export async function importBackup({
|
|
|
938
1017
|
const [
|
|
939
1018
|
accounts,
|
|
940
1019
|
profiles,
|
|
1020
|
+
profileAffiliations,
|
|
1021
|
+
profileSnapshots,
|
|
1022
|
+
profileBioEntities,
|
|
941
1023
|
tweets,
|
|
942
1024
|
collections,
|
|
1025
|
+
timelineEdges,
|
|
943
1026
|
conversations,
|
|
944
1027
|
messages,
|
|
945
1028
|
blocks,
|
|
@@ -949,8 +1032,12 @@ export async function importBackup({
|
|
|
949
1032
|
] = await Promise.all([
|
|
950
1033
|
readRows((file) => file === "data/accounts.jsonl"),
|
|
951
1034
|
readRows((file) => file === "data/profiles.jsonl"),
|
|
1035
|
+
readRows((file) => file === "data/profile_affiliations.jsonl"),
|
|
1036
|
+
readRows((file) => file === "data/profile_snapshots.jsonl"),
|
|
1037
|
+
readRows((file) => file === "data/profile_bio_entities.jsonl"),
|
|
952
1038
|
readRows((file) => file.startsWith("data/tweets/")),
|
|
953
1039
|
readRows((file) => file.startsWith("data/collections/")),
|
|
1040
|
+
readRows((file) => file.startsWith("data/timeline_edges/")),
|
|
954
1041
|
readRows((file) => file === "data/dms/conversations.jsonl"),
|
|
955
1042
|
readRows(
|
|
956
1043
|
(file) =>
|
|
@@ -1001,9 +1088,73 @@ export async function importBackup({
|
|
|
1001
1088
|
insertRows(
|
|
1002
1089
|
db,
|
|
1003
1090
|
`
|
|
1091
|
+
insert into profile_snapshots (
|
|
1092
|
+
profile_id, snapshot_hash, observed_at, last_seen_at, source, handle,
|
|
1093
|
+
display_name, bio, location, url, verified_type, followers_count,
|
|
1094
|
+
following_count, affiliations_json, raw_json
|
|
1095
|
+
) values (?, ?, ?, ?, coalesce(?, 'backup'), ?, ?, ?, ?, ?, ?, coalesce(?, 0), coalesce(?, 0), coalesce(?, '[]'), coalesce(?, '{}'))
|
|
1096
|
+
on conflict(profile_id, snapshot_hash) do update set
|
|
1097
|
+
last_seen_at = max(profile_snapshots.last_seen_at, excluded.last_seen_at),
|
|
1098
|
+
source = excluded.source,
|
|
1099
|
+
raw_json = case
|
|
1100
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1101
|
+
else profile_snapshots.raw_json
|
|
1102
|
+
end
|
|
1103
|
+
`,
|
|
1104
|
+
profileSnapshots,
|
|
1105
|
+
[
|
|
1106
|
+
"profile_id",
|
|
1107
|
+
"snapshot_hash",
|
|
1108
|
+
"observed_at",
|
|
1109
|
+
"last_seen_at",
|
|
1110
|
+
"source",
|
|
1111
|
+
"handle",
|
|
1112
|
+
"display_name",
|
|
1113
|
+
"bio",
|
|
1114
|
+
"location",
|
|
1115
|
+
"url",
|
|
1116
|
+
"verified_type",
|
|
1117
|
+
"followers_count",
|
|
1118
|
+
"following_count",
|
|
1119
|
+
"affiliations_json",
|
|
1120
|
+
"raw_json",
|
|
1121
|
+
],
|
|
1122
|
+
);
|
|
1123
|
+
insertRows(
|
|
1124
|
+
db,
|
|
1125
|
+
`
|
|
1126
|
+
insert into profile_bio_entities (
|
|
1127
|
+
profile_id, kind, value, source, is_active, first_seen_at, last_seen_at, raw_json
|
|
1128
|
+
) values (?, ?, ?, coalesce(?, 'backup'), coalesce(?, 1), ?, ?, coalesce(?, '{}'))
|
|
1129
|
+
on conflict(profile_id, kind, value) do update set
|
|
1130
|
+
source = excluded.source,
|
|
1131
|
+
is_active = excluded.is_active,
|
|
1132
|
+
last_seen_at = max(profile_bio_entities.last_seen_at, excluded.last_seen_at),
|
|
1133
|
+
raw_json = case
|
|
1134
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1135
|
+
else profile_bio_entities.raw_json
|
|
1136
|
+
end
|
|
1137
|
+
`,
|
|
1138
|
+
profileBioEntities,
|
|
1139
|
+
[
|
|
1140
|
+
"profile_id",
|
|
1141
|
+
"kind",
|
|
1142
|
+
"value",
|
|
1143
|
+
"source",
|
|
1144
|
+
"is_active",
|
|
1145
|
+
"first_seen_at",
|
|
1146
|
+
"last_seen_at",
|
|
1147
|
+
"raw_json",
|
|
1148
|
+
],
|
|
1149
|
+
);
|
|
1150
|
+
insertRows(
|
|
1151
|
+
db,
|
|
1152
|
+
`
|
|
1004
1153
|
insert into profiles (
|
|
1005
|
-
id, handle, display_name, bio, followers_count, following_count,
|
|
1006
|
-
|
|
1154
|
+
id, handle, display_name, bio, followers_count, following_count,
|
|
1155
|
+
avatar_hue, avatar_url, location, url, verified_type, entities_json,
|
|
1156
|
+
raw_json, created_at
|
|
1157
|
+
) values (?, ?, ?, ?, ?, coalesce(?, 0), ?, ?, ?, ?, ?, coalesce(?, '{}'), coalesce(?, '{}'), ?)
|
|
1007
1158
|
on conflict(id) do update set
|
|
1008
1159
|
handle = coalesce(nullif(excluded.handle, ''), profiles.handle),
|
|
1009
1160
|
display_name = coalesce(nullif(excluded.display_name, ''), profiles.display_name),
|
|
@@ -1012,6 +1163,17 @@ export async function importBackup({
|
|
|
1012
1163
|
following_count = max(profiles.following_count, excluded.following_count),
|
|
1013
1164
|
avatar_hue = case when profiles.avatar_hue = 0 then excluded.avatar_hue else profiles.avatar_hue end,
|
|
1014
1165
|
avatar_url = coalesce(excluded.avatar_url, profiles.avatar_url),
|
|
1166
|
+
location = coalesce(excluded.location, profiles.location),
|
|
1167
|
+
url = coalesce(excluded.url, profiles.url),
|
|
1168
|
+
verified_type = coalesce(excluded.verified_type, profiles.verified_type),
|
|
1169
|
+
entities_json = case
|
|
1170
|
+
when excluded.entities_json not in ('', '{}', 'null') then excluded.entities_json
|
|
1171
|
+
else profiles.entities_json
|
|
1172
|
+
end,
|
|
1173
|
+
raw_json = case
|
|
1174
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1175
|
+
else profiles.raw_json
|
|
1176
|
+
end,
|
|
1015
1177
|
created_at = min(profiles.created_at, excluded.created_at)
|
|
1016
1178
|
`,
|
|
1017
1179
|
profiles,
|
|
@@ -1024,12 +1186,57 @@ export async function importBackup({
|
|
|
1024
1186
|
"following_count",
|
|
1025
1187
|
"avatar_hue",
|
|
1026
1188
|
"avatar_url",
|
|
1189
|
+
"location",
|
|
1190
|
+
"url",
|
|
1191
|
+
"verified_type",
|
|
1192
|
+
"entities_json",
|
|
1193
|
+
"raw_json",
|
|
1027
1194
|
"created_at",
|
|
1028
1195
|
],
|
|
1029
1196
|
);
|
|
1030
1197
|
insertRows(
|
|
1031
1198
|
db,
|
|
1032
1199
|
`
|
|
1200
|
+
insert into profile_affiliations (
|
|
1201
|
+
subject_profile_id, organization_profile_id, organization_name,
|
|
1202
|
+
organization_handle, badge_url, url, label, source, is_active,
|
|
1203
|
+
first_seen_at, last_seen_at, raw_json, updated_at
|
|
1204
|
+
) values (?, ?, ?, ?, ?, ?, ?, coalesce(?, 'backup'), coalesce(?, 1), ?, ?, coalesce(?, '{}'), ?)
|
|
1205
|
+
on conflict(subject_profile_id, organization_profile_id) do update set
|
|
1206
|
+
organization_name = coalesce(excluded.organization_name, profile_affiliations.organization_name),
|
|
1207
|
+
organization_handle = coalesce(excluded.organization_handle, profile_affiliations.organization_handle),
|
|
1208
|
+
badge_url = coalesce(excluded.badge_url, profile_affiliations.badge_url),
|
|
1209
|
+
url = coalesce(excluded.url, profile_affiliations.url),
|
|
1210
|
+
label = coalesce(excluded.label, profile_affiliations.label),
|
|
1211
|
+
source = excluded.source,
|
|
1212
|
+
is_active = excluded.is_active,
|
|
1213
|
+
last_seen_at = max(profile_affiliations.last_seen_at, excluded.last_seen_at),
|
|
1214
|
+
raw_json = case
|
|
1215
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1216
|
+
else profile_affiliations.raw_json
|
|
1217
|
+
end,
|
|
1218
|
+
updated_at = excluded.updated_at
|
|
1219
|
+
`,
|
|
1220
|
+
profileAffiliations,
|
|
1221
|
+
[
|
|
1222
|
+
"subject_profile_id",
|
|
1223
|
+
"organization_profile_id",
|
|
1224
|
+
"organization_name",
|
|
1225
|
+
"organization_handle",
|
|
1226
|
+
"badge_url",
|
|
1227
|
+
"url",
|
|
1228
|
+
"label",
|
|
1229
|
+
"source",
|
|
1230
|
+
"is_active",
|
|
1231
|
+
"first_seen_at",
|
|
1232
|
+
"last_seen_at",
|
|
1233
|
+
"raw_json",
|
|
1234
|
+
"updated_at",
|
|
1235
|
+
],
|
|
1236
|
+
);
|
|
1237
|
+
insertRows(
|
|
1238
|
+
db,
|
|
1239
|
+
`
|
|
1033
1240
|
insert into tweets (
|
|
1034
1241
|
id, account_id, author_profile_id, kind, text, created_at, is_replied,
|
|
1035
1242
|
reply_to_id, like_count, media_count, bookmarked, liked, entities_json,
|
|
@@ -1118,6 +1325,37 @@ export async function importBackup({
|
|
|
1118
1325
|
insertRows(
|
|
1119
1326
|
db,
|
|
1120
1327
|
`
|
|
1328
|
+
insert into tweet_account_edges (
|
|
1329
|
+
account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count,
|
|
1330
|
+
source, raw_json, updated_at
|
|
1331
|
+
) values (?, ?, ?, ?, ?, coalesce(?, 1), coalesce(?, 'backup'), coalesce(?, '{}'), ?)
|
|
1332
|
+
on conflict(account_id, tweet_id, kind) do update set
|
|
1333
|
+
first_seen_at = min(tweet_account_edges.first_seen_at, excluded.first_seen_at),
|
|
1334
|
+
last_seen_at = max(tweet_account_edges.last_seen_at, excluded.last_seen_at),
|
|
1335
|
+
seen_count = max(tweet_account_edges.seen_count, excluded.seen_count),
|
|
1336
|
+
source = coalesce(nullif(excluded.source, ''), tweet_account_edges.source),
|
|
1337
|
+
raw_json = case
|
|
1338
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1339
|
+
else tweet_account_edges.raw_json
|
|
1340
|
+
end,
|
|
1341
|
+
updated_at = max(tweet_account_edges.updated_at, excluded.updated_at)
|
|
1342
|
+
`,
|
|
1343
|
+
timelineEdges,
|
|
1344
|
+
[
|
|
1345
|
+
"account_id",
|
|
1346
|
+
"tweet_id",
|
|
1347
|
+
"kind",
|
|
1348
|
+
"first_seen_at",
|
|
1349
|
+
"last_seen_at",
|
|
1350
|
+
"seen_count",
|
|
1351
|
+
"source",
|
|
1352
|
+
"raw_json",
|
|
1353
|
+
"updated_at",
|
|
1354
|
+
],
|
|
1355
|
+
);
|
|
1356
|
+
insertRows(
|
|
1357
|
+
db,
|
|
1358
|
+
`
|
|
1121
1359
|
insert into dm_conversations (
|
|
1122
1360
|
id, account_id, participant_profile_id, title, last_message_at, unread_count, needs_reply
|
|
1123
1361
|
) values (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1251,7 +1489,7 @@ export async function syncBackup({
|
|
|
1251
1489
|
}: {
|
|
1252
1490
|
repoPath: string;
|
|
1253
1491
|
remote?: string;
|
|
1254
|
-
db?: Database
|
|
1492
|
+
db?: Database;
|
|
1255
1493
|
message?: string;
|
|
1256
1494
|
}): Promise<BackupSyncResult> {
|
|
1257
1495
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
@@ -1291,7 +1529,7 @@ export async function updateBackupFromGit({
|
|
|
1291
1529
|
}: {
|
|
1292
1530
|
repoPath: string;
|
|
1293
1531
|
remote?: string;
|
|
1294
|
-
db?: Database
|
|
1532
|
+
db?: Database;
|
|
1295
1533
|
}): Promise<{
|
|
1296
1534
|
ok: true;
|
|
1297
1535
|
repoPath: string;
|
|
@@ -1322,7 +1560,7 @@ export async function updateBackupFromGit({
|
|
|
1322
1560
|
};
|
|
1323
1561
|
}
|
|
1324
1562
|
|
|
1325
|
-
function readAutoSyncState(db: Database
|
|
1563
|
+
function readAutoSyncState(db: Database) {
|
|
1326
1564
|
const row = db
|
|
1327
1565
|
.prepare("select value_json from sync_cache where cache_key = ?")
|
|
1328
1566
|
.get(AUTO_SYNC_CACHE_KEY) as { value_json: string } | undefined;
|
|
@@ -1341,7 +1579,7 @@ function readAutoSyncState(db: Database.Database) {
|
|
|
1341
1579
|
}
|
|
1342
1580
|
|
|
1343
1581
|
function writeAutoSyncState(
|
|
1344
|
-
db: Database
|
|
1582
|
+
db: Database,
|
|
1345
1583
|
value: { checkedAt: string; ok: boolean; error?: string },
|
|
1346
1584
|
) {
|
|
1347
1585
|
db.prepare(
|
|
@@ -1382,7 +1620,7 @@ function resolveAutoSyncConfig() {
|
|
|
1382
1620
|
}
|
|
1383
1621
|
|
|
1384
1622
|
export async function maybeAutoUpdateBackup(
|
|
1385
|
-
db?: Database
|
|
1623
|
+
db?: Database,
|
|
1386
1624
|
): Promise<BackupAutoUpdateResult> {
|
|
1387
1625
|
if (process.env.BIRDCLAW_BACKUP_AUTO_SYNC === "0") {
|
|
1388
1626
|
return {
|
|
@@ -1449,7 +1687,7 @@ export async function maybeAutoUpdateBackup(
|
|
|
1449
1687
|
}
|
|
1450
1688
|
|
|
1451
1689
|
export async function maybeAutoSyncBackup(
|
|
1452
|
-
db?: Database
|
|
1690
|
+
db?: Database,
|
|
1453
1691
|
): Promise<BackupAutoUpdateResult> {
|
|
1454
1692
|
if (process.env.BIRDCLAW_BACKUP_AUTO_SYNC === "0") {
|
|
1455
1693
|
return {
|
package/src/lib/bird-actions.ts
CHANGED
|
@@ -90,10 +90,27 @@ function toBirdLookupUser(payload: Record<string, unknown>): XurlMentionUser {
|
|
|
90
90
|
username,
|
|
91
91
|
description:
|
|
92
92
|
typeof user.description === "string" ? user.description : undefined,
|
|
93
|
+
location: typeof user.location === "string" ? user.location : undefined,
|
|
94
|
+
url: typeof user.url === "string" ? user.url : undefined,
|
|
95
|
+
verified: typeof user.verified === "boolean" ? user.verified : undefined,
|
|
96
|
+
verified_type:
|
|
97
|
+
typeof user.verifiedType === "string"
|
|
98
|
+
? user.verifiedType
|
|
99
|
+
: typeof user.verified_type === "string"
|
|
100
|
+
? user.verified_type
|
|
101
|
+
: undefined,
|
|
93
102
|
profile_image_url:
|
|
94
103
|
typeof user.profileImageUrl === "string"
|
|
95
104
|
? user.profileImageUrl
|
|
96
105
|
: undefined,
|
|
106
|
+
entities:
|
|
107
|
+
user.entities && typeof user.entities === "object"
|
|
108
|
+
? (user.entities as Record<string, unknown>)
|
|
109
|
+
: undefined,
|
|
110
|
+
affiliation:
|
|
111
|
+
user.affiliation && typeof user.affiliation === "object"
|
|
112
|
+
? (user.affiliation as Record<string, unknown>)
|
|
113
|
+
: undefined,
|
|
97
114
|
public_metrics: {
|
|
98
115
|
followers_count: Number.isFinite(followersCount) ? followersCount : 0,
|
|
99
116
|
...(Number.isFinite(followingCount)
|