birdclaw 0.3.0 → 0.4.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 +19 -0
- package/README.md +37 -9
- package/package.json +4 -6
- package/src/cli.ts +176 -20
- package/src/lib/archive-import.ts +25 -0
- package/src/lib/backup.ts +269 -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 +313 -14
- package/src/lib/dms-live.ts +3 -3
- package/src/lib/identity-search-index.ts +369 -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 +74 -0
- package/src/lib/url-expansion.ts +147 -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,10 +965,11 @@ 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;
|
|
900
974
|
delete from blocks;
|
|
901
975
|
delete from mutes;
|
|
@@ -904,6 +978,9 @@ function clearBackupImportData(db: Database.Database) {
|
|
|
904
978
|
delete from dm_messages;
|
|
905
979
|
delete from dm_conversations;
|
|
906
980
|
delete from tweets;
|
|
981
|
+
delete from profile_bio_entities;
|
|
982
|
+
delete from profile_snapshots;
|
|
983
|
+
delete from profile_affiliations;
|
|
907
984
|
delete from profiles;
|
|
908
985
|
delete from accounts;
|
|
909
986
|
delete from sync_cache;
|
|
@@ -917,7 +994,7 @@ export async function importBackup({
|
|
|
917
994
|
mode = "merge",
|
|
918
995
|
}: {
|
|
919
996
|
repoPath: string;
|
|
920
|
-
db?: Database
|
|
997
|
+
db?: Database;
|
|
921
998
|
validate?: boolean;
|
|
922
999
|
mode?: BackupImportMode;
|
|
923
1000
|
}): Promise<BackupImportResult> {
|
|
@@ -938,8 +1015,12 @@ export async function importBackup({
|
|
|
938
1015
|
const [
|
|
939
1016
|
accounts,
|
|
940
1017
|
profiles,
|
|
1018
|
+
profileAffiliations,
|
|
1019
|
+
profileSnapshots,
|
|
1020
|
+
profileBioEntities,
|
|
941
1021
|
tweets,
|
|
942
1022
|
collections,
|
|
1023
|
+
timelineEdges,
|
|
943
1024
|
conversations,
|
|
944
1025
|
messages,
|
|
945
1026
|
blocks,
|
|
@@ -949,8 +1030,12 @@ export async function importBackup({
|
|
|
949
1030
|
] = await Promise.all([
|
|
950
1031
|
readRows((file) => file === "data/accounts.jsonl"),
|
|
951
1032
|
readRows((file) => file === "data/profiles.jsonl"),
|
|
1033
|
+
readRows((file) => file === "data/profile_affiliations.jsonl"),
|
|
1034
|
+
readRows((file) => file === "data/profile_snapshots.jsonl"),
|
|
1035
|
+
readRows((file) => file === "data/profile_bio_entities.jsonl"),
|
|
952
1036
|
readRows((file) => file.startsWith("data/tweets/")),
|
|
953
1037
|
readRows((file) => file.startsWith("data/collections/")),
|
|
1038
|
+
readRows((file) => file.startsWith("data/timeline_edges/")),
|
|
954
1039
|
readRows((file) => file === "data/dms/conversations.jsonl"),
|
|
955
1040
|
readRows(
|
|
956
1041
|
(file) =>
|
|
@@ -1001,9 +1086,73 @@ export async function importBackup({
|
|
|
1001
1086
|
insertRows(
|
|
1002
1087
|
db,
|
|
1003
1088
|
`
|
|
1089
|
+
insert into profile_snapshots (
|
|
1090
|
+
profile_id, snapshot_hash, observed_at, last_seen_at, source, handle,
|
|
1091
|
+
display_name, bio, location, url, verified_type, followers_count,
|
|
1092
|
+
following_count, affiliations_json, raw_json
|
|
1093
|
+
) values (?, ?, ?, ?, coalesce(?, 'backup'), ?, ?, ?, ?, ?, ?, coalesce(?, 0), coalesce(?, 0), coalesce(?, '[]'), coalesce(?, '{}'))
|
|
1094
|
+
on conflict(profile_id, snapshot_hash) do update set
|
|
1095
|
+
last_seen_at = max(profile_snapshots.last_seen_at, excluded.last_seen_at),
|
|
1096
|
+
source = excluded.source,
|
|
1097
|
+
raw_json = case
|
|
1098
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1099
|
+
else profile_snapshots.raw_json
|
|
1100
|
+
end
|
|
1101
|
+
`,
|
|
1102
|
+
profileSnapshots,
|
|
1103
|
+
[
|
|
1104
|
+
"profile_id",
|
|
1105
|
+
"snapshot_hash",
|
|
1106
|
+
"observed_at",
|
|
1107
|
+
"last_seen_at",
|
|
1108
|
+
"source",
|
|
1109
|
+
"handle",
|
|
1110
|
+
"display_name",
|
|
1111
|
+
"bio",
|
|
1112
|
+
"location",
|
|
1113
|
+
"url",
|
|
1114
|
+
"verified_type",
|
|
1115
|
+
"followers_count",
|
|
1116
|
+
"following_count",
|
|
1117
|
+
"affiliations_json",
|
|
1118
|
+
"raw_json",
|
|
1119
|
+
],
|
|
1120
|
+
);
|
|
1121
|
+
insertRows(
|
|
1122
|
+
db,
|
|
1123
|
+
`
|
|
1124
|
+
insert into profile_bio_entities (
|
|
1125
|
+
profile_id, kind, value, source, is_active, first_seen_at, last_seen_at, raw_json
|
|
1126
|
+
) values (?, ?, ?, coalesce(?, 'backup'), coalesce(?, 1), ?, ?, coalesce(?, '{}'))
|
|
1127
|
+
on conflict(profile_id, kind, value) do update set
|
|
1128
|
+
source = excluded.source,
|
|
1129
|
+
is_active = excluded.is_active,
|
|
1130
|
+
last_seen_at = max(profile_bio_entities.last_seen_at, excluded.last_seen_at),
|
|
1131
|
+
raw_json = case
|
|
1132
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1133
|
+
else profile_bio_entities.raw_json
|
|
1134
|
+
end
|
|
1135
|
+
`,
|
|
1136
|
+
profileBioEntities,
|
|
1137
|
+
[
|
|
1138
|
+
"profile_id",
|
|
1139
|
+
"kind",
|
|
1140
|
+
"value",
|
|
1141
|
+
"source",
|
|
1142
|
+
"is_active",
|
|
1143
|
+
"first_seen_at",
|
|
1144
|
+
"last_seen_at",
|
|
1145
|
+
"raw_json",
|
|
1146
|
+
],
|
|
1147
|
+
);
|
|
1148
|
+
insertRows(
|
|
1149
|
+
db,
|
|
1150
|
+
`
|
|
1004
1151
|
insert into profiles (
|
|
1005
|
-
id, handle, display_name, bio, followers_count, following_count,
|
|
1006
|
-
|
|
1152
|
+
id, handle, display_name, bio, followers_count, following_count,
|
|
1153
|
+
avatar_hue, avatar_url, location, url, verified_type, entities_json,
|
|
1154
|
+
raw_json, created_at
|
|
1155
|
+
) values (?, ?, ?, ?, ?, coalesce(?, 0), ?, ?, ?, ?, ?, coalesce(?, '{}'), coalesce(?, '{}'), ?)
|
|
1007
1156
|
on conflict(id) do update set
|
|
1008
1157
|
handle = coalesce(nullif(excluded.handle, ''), profiles.handle),
|
|
1009
1158
|
display_name = coalesce(nullif(excluded.display_name, ''), profiles.display_name),
|
|
@@ -1012,6 +1161,17 @@ export async function importBackup({
|
|
|
1012
1161
|
following_count = max(profiles.following_count, excluded.following_count),
|
|
1013
1162
|
avatar_hue = case when profiles.avatar_hue = 0 then excluded.avatar_hue else profiles.avatar_hue end,
|
|
1014
1163
|
avatar_url = coalesce(excluded.avatar_url, profiles.avatar_url),
|
|
1164
|
+
location = coalesce(excluded.location, profiles.location),
|
|
1165
|
+
url = coalesce(excluded.url, profiles.url),
|
|
1166
|
+
verified_type = coalesce(excluded.verified_type, profiles.verified_type),
|
|
1167
|
+
entities_json = case
|
|
1168
|
+
when excluded.entities_json not in ('', '{}', 'null') then excluded.entities_json
|
|
1169
|
+
else profiles.entities_json
|
|
1170
|
+
end,
|
|
1171
|
+
raw_json = case
|
|
1172
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1173
|
+
else profiles.raw_json
|
|
1174
|
+
end,
|
|
1015
1175
|
created_at = min(profiles.created_at, excluded.created_at)
|
|
1016
1176
|
`,
|
|
1017
1177
|
profiles,
|
|
@@ -1024,12 +1184,57 @@ export async function importBackup({
|
|
|
1024
1184
|
"following_count",
|
|
1025
1185
|
"avatar_hue",
|
|
1026
1186
|
"avatar_url",
|
|
1187
|
+
"location",
|
|
1188
|
+
"url",
|
|
1189
|
+
"verified_type",
|
|
1190
|
+
"entities_json",
|
|
1191
|
+
"raw_json",
|
|
1027
1192
|
"created_at",
|
|
1028
1193
|
],
|
|
1029
1194
|
);
|
|
1030
1195
|
insertRows(
|
|
1031
1196
|
db,
|
|
1032
1197
|
`
|
|
1198
|
+
insert into profile_affiliations (
|
|
1199
|
+
subject_profile_id, organization_profile_id, organization_name,
|
|
1200
|
+
organization_handle, badge_url, url, label, source, is_active,
|
|
1201
|
+
first_seen_at, last_seen_at, raw_json, updated_at
|
|
1202
|
+
) values (?, ?, ?, ?, ?, ?, ?, coalesce(?, 'backup'), coalesce(?, 1), ?, ?, coalesce(?, '{}'), ?)
|
|
1203
|
+
on conflict(subject_profile_id, organization_profile_id) do update set
|
|
1204
|
+
organization_name = coalesce(excluded.organization_name, profile_affiliations.organization_name),
|
|
1205
|
+
organization_handle = coalesce(excluded.organization_handle, profile_affiliations.organization_handle),
|
|
1206
|
+
badge_url = coalesce(excluded.badge_url, profile_affiliations.badge_url),
|
|
1207
|
+
url = coalesce(excluded.url, profile_affiliations.url),
|
|
1208
|
+
label = coalesce(excluded.label, profile_affiliations.label),
|
|
1209
|
+
source = excluded.source,
|
|
1210
|
+
is_active = excluded.is_active,
|
|
1211
|
+
last_seen_at = max(profile_affiliations.last_seen_at, excluded.last_seen_at),
|
|
1212
|
+
raw_json = case
|
|
1213
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1214
|
+
else profile_affiliations.raw_json
|
|
1215
|
+
end,
|
|
1216
|
+
updated_at = excluded.updated_at
|
|
1217
|
+
`,
|
|
1218
|
+
profileAffiliations,
|
|
1219
|
+
[
|
|
1220
|
+
"subject_profile_id",
|
|
1221
|
+
"organization_profile_id",
|
|
1222
|
+
"organization_name",
|
|
1223
|
+
"organization_handle",
|
|
1224
|
+
"badge_url",
|
|
1225
|
+
"url",
|
|
1226
|
+
"label",
|
|
1227
|
+
"source",
|
|
1228
|
+
"is_active",
|
|
1229
|
+
"first_seen_at",
|
|
1230
|
+
"last_seen_at",
|
|
1231
|
+
"raw_json",
|
|
1232
|
+
"updated_at",
|
|
1233
|
+
],
|
|
1234
|
+
);
|
|
1235
|
+
insertRows(
|
|
1236
|
+
db,
|
|
1237
|
+
`
|
|
1033
1238
|
insert into tweets (
|
|
1034
1239
|
id, account_id, author_profile_id, kind, text, created_at, is_replied,
|
|
1035
1240
|
reply_to_id, like_count, media_count, bookmarked, liked, entities_json,
|
|
@@ -1118,6 +1323,37 @@ export async function importBackup({
|
|
|
1118
1323
|
insertRows(
|
|
1119
1324
|
db,
|
|
1120
1325
|
`
|
|
1326
|
+
insert into tweet_account_edges (
|
|
1327
|
+
account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count,
|
|
1328
|
+
source, raw_json, updated_at
|
|
1329
|
+
) values (?, ?, ?, ?, ?, coalesce(?, 1), coalesce(?, 'backup'), coalesce(?, '{}'), ?)
|
|
1330
|
+
on conflict(account_id, tweet_id, kind) do update set
|
|
1331
|
+
first_seen_at = min(tweet_account_edges.first_seen_at, excluded.first_seen_at),
|
|
1332
|
+
last_seen_at = max(tweet_account_edges.last_seen_at, excluded.last_seen_at),
|
|
1333
|
+
seen_count = max(tweet_account_edges.seen_count, excluded.seen_count),
|
|
1334
|
+
source = coalesce(nullif(excluded.source, ''), tweet_account_edges.source),
|
|
1335
|
+
raw_json = case
|
|
1336
|
+
when excluded.raw_json not in ('', '{}', 'null') then excluded.raw_json
|
|
1337
|
+
else tweet_account_edges.raw_json
|
|
1338
|
+
end,
|
|
1339
|
+
updated_at = max(tweet_account_edges.updated_at, excluded.updated_at)
|
|
1340
|
+
`,
|
|
1341
|
+
timelineEdges,
|
|
1342
|
+
[
|
|
1343
|
+
"account_id",
|
|
1344
|
+
"tweet_id",
|
|
1345
|
+
"kind",
|
|
1346
|
+
"first_seen_at",
|
|
1347
|
+
"last_seen_at",
|
|
1348
|
+
"seen_count",
|
|
1349
|
+
"source",
|
|
1350
|
+
"raw_json",
|
|
1351
|
+
"updated_at",
|
|
1352
|
+
],
|
|
1353
|
+
);
|
|
1354
|
+
insertRows(
|
|
1355
|
+
db,
|
|
1356
|
+
`
|
|
1121
1357
|
insert into dm_conversations (
|
|
1122
1358
|
id, account_id, participant_profile_id, title, last_message_at, unread_count, needs_reply
|
|
1123
1359
|
) values (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1251,7 +1487,7 @@ export async function syncBackup({
|
|
|
1251
1487
|
}: {
|
|
1252
1488
|
repoPath: string;
|
|
1253
1489
|
remote?: string;
|
|
1254
|
-
db?: Database
|
|
1490
|
+
db?: Database;
|
|
1255
1491
|
message?: string;
|
|
1256
1492
|
}): Promise<BackupSyncResult> {
|
|
1257
1493
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
@@ -1291,7 +1527,7 @@ export async function updateBackupFromGit({
|
|
|
1291
1527
|
}: {
|
|
1292
1528
|
repoPath: string;
|
|
1293
1529
|
remote?: string;
|
|
1294
|
-
db?: Database
|
|
1530
|
+
db?: Database;
|
|
1295
1531
|
}): Promise<{
|
|
1296
1532
|
ok: true;
|
|
1297
1533
|
repoPath: string;
|
|
@@ -1322,7 +1558,7 @@ export async function updateBackupFromGit({
|
|
|
1322
1558
|
};
|
|
1323
1559
|
}
|
|
1324
1560
|
|
|
1325
|
-
function readAutoSyncState(db: Database
|
|
1561
|
+
function readAutoSyncState(db: Database) {
|
|
1326
1562
|
const row = db
|
|
1327
1563
|
.prepare("select value_json from sync_cache where cache_key = ?")
|
|
1328
1564
|
.get(AUTO_SYNC_CACHE_KEY) as { value_json: string } | undefined;
|
|
@@ -1341,7 +1577,7 @@ function readAutoSyncState(db: Database.Database) {
|
|
|
1341
1577
|
}
|
|
1342
1578
|
|
|
1343
1579
|
function writeAutoSyncState(
|
|
1344
|
-
db: Database
|
|
1580
|
+
db: Database,
|
|
1345
1581
|
value: { checkedAt: string; ok: boolean; error?: string },
|
|
1346
1582
|
) {
|
|
1347
1583
|
db.prepare(
|
|
@@ -1382,7 +1618,7 @@ function resolveAutoSyncConfig() {
|
|
|
1382
1618
|
}
|
|
1383
1619
|
|
|
1384
1620
|
export async function maybeAutoUpdateBackup(
|
|
1385
|
-
db?: Database
|
|
1621
|
+
db?: Database,
|
|
1386
1622
|
): Promise<BackupAutoUpdateResult> {
|
|
1387
1623
|
if (process.env.BIRDCLAW_BACKUP_AUTO_SYNC === "0") {
|
|
1388
1624
|
return {
|
|
@@ -1449,7 +1685,7 @@ export async function maybeAutoUpdateBackup(
|
|
|
1449
1685
|
}
|
|
1450
1686
|
|
|
1451
1687
|
export async function maybeAutoSyncBackup(
|
|
1452
|
-
db?: Database
|
|
1688
|
+
db?: Database,
|
|
1453
1689
|
): Promise<BackupAutoUpdateResult> {
|
|
1454
1690
|
if (process.env.BIRDCLAW_BACKUP_AUTO_SYNC === "0") {
|
|
1455
1691
|
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)
|