birdclaw 0.2.1 → 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 +44 -1
- package/README.md +54 -15
- package/package.json +14 -15
- package/playwright.config.ts +5 -2
- package/src/cli.ts +289 -20
- package/src/lib/actions-transport.ts +58 -1
- package/src/lib/archive-import.ts +34 -2
- package/src/lib/backup.ts +271 -33
- package/src/lib/bird-actions.ts +21 -0
- package/src/lib/bird.ts +332 -17
- package/src/lib/blocks.ts +3 -3
- package/src/lib/bookmark-sync-job.ts +3 -3
- package/src/lib/config.ts +34 -1
- package/src/lib/db.ts +321 -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 +267 -0
- package/src/lib/mentions-live.ts +18 -7
- package/src/lib/moderation-target.ts +7 -5
- 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 +8 -24
- package/src/lib/profile-resolver.ts +428 -0
- package/src/lib/queries.ts +522 -68
- package/src/lib/research.ts +607 -0
- package/src/lib/seed.ts +10 -4
- package/src/lib/sqlite.ts +143 -0
- package/src/lib/timeline-collections-live.ts +22 -8
- package/src/lib/timeline-live.ts +182 -0
- package/src/lib/tweet-account-edges.ts +39 -0
- package/src/lib/tweet-lookup.ts +35 -0
- package/src/lib/types.ts +103 -1
- package/src/lib/url-expansion.ts +147 -0
- package/src/lib/whois.ts +1060 -0
- package/src/lib/x-profile.ts +174 -17
- package/src/lib/xurl.ts +41 -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,
|
|
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,16 +1086,92 @@ 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,
|
|
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),
|
|
1010
1159
|
bio = coalesce(nullif(excluded.bio, ''), profiles.bio),
|
|
1011
1160
|
followers_count = max(profiles.followers_count, excluded.followers_count),
|
|
1161
|
+
following_count = max(profiles.following_count, excluded.following_count),
|
|
1012
1162
|
avatar_hue = case when profiles.avatar_hue = 0 then excluded.avatar_hue else profiles.avatar_hue end,
|
|
1013
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,
|
|
1014
1175
|
created_at = min(profiles.created_at, excluded.created_at)
|
|
1015
1176
|
`,
|
|
1016
1177
|
profiles,
|
|
@@ -1020,14 +1181,60 @@ export async function importBackup({
|
|
|
1020
1181
|
"display_name",
|
|
1021
1182
|
"bio",
|
|
1022
1183
|
"followers_count",
|
|
1184
|
+
"following_count",
|
|
1023
1185
|
"avatar_hue",
|
|
1024
1186
|
"avatar_url",
|
|
1187
|
+
"location",
|
|
1188
|
+
"url",
|
|
1189
|
+
"verified_type",
|
|
1190
|
+
"entities_json",
|
|
1191
|
+
"raw_json",
|
|
1025
1192
|
"created_at",
|
|
1026
1193
|
],
|
|
1027
1194
|
);
|
|
1028
1195
|
insertRows(
|
|
1029
1196
|
db,
|
|
1030
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
|
+
`
|
|
1031
1238
|
insert into tweets (
|
|
1032
1239
|
id, account_id, author_profile_id, kind, text, created_at, is_replied,
|
|
1033
1240
|
reply_to_id, like_count, media_count, bookmarked, liked, entities_json,
|
|
@@ -1116,6 +1323,37 @@ export async function importBackup({
|
|
|
1116
1323
|
insertRows(
|
|
1117
1324
|
db,
|
|
1118
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
|
+
`
|
|
1119
1357
|
insert into dm_conversations (
|
|
1120
1358
|
id, account_id, participant_profile_id, title, last_message_at, unread_count, needs_reply
|
|
1121
1359
|
) values (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1249,7 +1487,7 @@ export async function syncBackup({
|
|
|
1249
1487
|
}: {
|
|
1250
1488
|
repoPath: string;
|
|
1251
1489
|
remote?: string;
|
|
1252
|
-
db?: Database
|
|
1490
|
+
db?: Database;
|
|
1253
1491
|
message?: string;
|
|
1254
1492
|
}): Promise<BackupSyncResult> {
|
|
1255
1493
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
@@ -1289,7 +1527,7 @@ export async function updateBackupFromGit({
|
|
|
1289
1527
|
}: {
|
|
1290
1528
|
repoPath: string;
|
|
1291
1529
|
remote?: string;
|
|
1292
|
-
db?: Database
|
|
1530
|
+
db?: Database;
|
|
1293
1531
|
}): Promise<{
|
|
1294
1532
|
ok: true;
|
|
1295
1533
|
repoPath: string;
|
|
@@ -1320,7 +1558,7 @@ export async function updateBackupFromGit({
|
|
|
1320
1558
|
};
|
|
1321
1559
|
}
|
|
1322
1560
|
|
|
1323
|
-
function readAutoSyncState(db: Database
|
|
1561
|
+
function readAutoSyncState(db: Database) {
|
|
1324
1562
|
const row = db
|
|
1325
1563
|
.prepare("select value_json from sync_cache where cache_key = ?")
|
|
1326
1564
|
.get(AUTO_SYNC_CACHE_KEY) as { value_json: string } | undefined;
|
|
@@ -1339,7 +1577,7 @@ function readAutoSyncState(db: Database.Database) {
|
|
|
1339
1577
|
}
|
|
1340
1578
|
|
|
1341
1579
|
function writeAutoSyncState(
|
|
1342
|
-
db: Database
|
|
1580
|
+
db: Database,
|
|
1343
1581
|
value: { checkedAt: string; ok: boolean; error?: string },
|
|
1344
1582
|
) {
|
|
1345
1583
|
db.prepare(
|
|
@@ -1380,7 +1618,7 @@ function resolveAutoSyncConfig() {
|
|
|
1380
1618
|
}
|
|
1381
1619
|
|
|
1382
1620
|
export async function maybeAutoUpdateBackup(
|
|
1383
|
-
db?: Database
|
|
1621
|
+
db?: Database,
|
|
1384
1622
|
): Promise<BackupAutoUpdateResult> {
|
|
1385
1623
|
if (process.env.BIRDCLAW_BACKUP_AUTO_SYNC === "0") {
|
|
1386
1624
|
return {
|
|
@@ -1447,7 +1685,7 @@ export async function maybeAutoUpdateBackup(
|
|
|
1447
1685
|
}
|
|
1448
1686
|
|
|
1449
1687
|
export async function maybeAutoSyncBackup(
|
|
1450
|
-
db?: Database
|
|
1688
|
+
db?: Database,
|
|
1451
1689
|
): Promise<BackupAutoUpdateResult> {
|
|
1452
1690
|
if (process.env.BIRDCLAW_BACKUP_AUTO_SYNC === "0") {
|
|
1453
1691
|
return {
|
package/src/lib/bird-actions.ts
CHANGED
|
@@ -80,6 +80,7 @@ function toBirdLookupUser(payload: Record<string, unknown>): XurlMentionUser {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const followersCount = Number(user.followersCount ?? 0);
|
|
83
|
+
const followingCount = Number(user.followingCount);
|
|
83
84
|
return {
|
|
84
85
|
id,
|
|
85
86
|
name:
|
|
@@ -89,12 +90,32 @@ function toBirdLookupUser(payload: Record<string, unknown>): XurlMentionUser {
|
|
|
89
90
|
username,
|
|
90
91
|
description:
|
|
91
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,
|
|
92
102
|
profile_image_url:
|
|
93
103
|
typeof user.profileImageUrl === "string"
|
|
94
104
|
? user.profileImageUrl
|
|
95
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,
|
|
96
114
|
public_metrics: {
|
|
97
115
|
followers_count: Number.isFinite(followersCount) ? followersCount : 0,
|
|
116
|
+
...(Number.isFinite(followingCount)
|
|
117
|
+
? { following_count: followingCount }
|
|
118
|
+
: {}),
|
|
98
119
|
},
|
|
99
120
|
created_at: typeof user.createdAt === "string" ? user.createdAt : undefined,
|
|
100
121
|
};
|