birdclaw 0.8.2 → 0.8.3
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 +16 -0
- package/package.json +2 -1
- package/src/cli/command-context.ts +17 -0
- package/src/cli/register-analysis.ts +500 -0
- package/src/cli/register-compose.ts +40 -0
- package/src/cli/register-graph.ts +132 -0
- package/src/cli/register-inbox.ts +41 -0
- package/src/cli/register-storage.ts +106 -0
- package/src/cli.ts +30 -750
- package/src/components/AccountSwitcher.tsx +7 -15
- package/src/components/AvatarChip.tsx +1 -1
- package/src/components/AvatarPreload.ts +149 -0
- package/src/components/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +8 -674
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +2 -0
- package/src/components/links-controller.ts +162 -0
- package/src/components/links-model.ts +198 -0
- package/src/components/network-map-controller.ts +84 -0
- package/src/components/network-map-model.ts +255 -0
- package/src/components/useTimelineRouteData.ts +105 -235
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -215
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +75 -120
- package/src/lib/backup.ts +335 -424
- package/src/lib/blocks-write.ts +30 -26
- package/src/lib/blocks.ts +18 -20
- package/src/lib/database-metrics.ts +88 -0
- package/src/lib/database-migrations.ts +34 -0
- package/src/lib/database-schema.ts +312 -0
- package/src/lib/database-writer.ts +69 -0
- package/src/lib/db.ts +84 -330
- package/src/lib/dm-read-model.ts +533 -0
- package/src/lib/dms-live.ts +34 -97
- package/src/lib/follow-graph.ts +17 -27
- package/src/lib/import-repository.ts +138 -0
- package/src/lib/inbox.ts +2 -1
- package/src/lib/live-sync-engine.ts +209 -0
- package/src/lib/live-transport-gateway.ts +128 -0
- package/src/lib/mention-threads-live.ts +90 -177
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -181
- package/src/lib/moderation-target.ts +15 -4
- package/src/lib/moderation-write.ts +1 -1
- package/src/lib/mutes-write.ts +30 -26
- package/src/lib/openai-response-runtime.ts +251 -0
- package/src/lib/paginated-sync.ts +93 -0
- package/src/lib/period-digest.ts +116 -304
- package/src/lib/profile-analysis.ts +36 -110
- package/src/lib/queries.ts +6 -2381
- package/src/lib/query-actions.ts +437 -0
- package/src/lib/query-client.tsx +47 -0
- package/src/lib/query-read-model-shared.ts +52 -0
- package/src/lib/query-read-models.ts +5 -0
- package/src/lib/query-resource.ts +41 -0
- package/src/lib/query-status.ts +164 -0
- package/src/lib/research.ts +1 -1
- package/src/lib/runtime-services.ts +20 -0
- package/src/lib/search-discussion.ts +75 -279
- package/src/lib/server-runtime-services.ts +30 -0
- package/src/lib/sqlite.ts +48 -12
- package/src/lib/streaming-ingestion.ts +240 -0
- package/src/lib/sync-cache.ts +6 -1
- package/src/lib/sync-plan.ts +175 -0
- package/src/lib/timeline-collections-live.ts +83 -257
- package/src/lib/timeline-live.ts +86 -236
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -167
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/routes/__root.tsx +11 -8
- package/src/routes/api/action.tsx +1 -1
- package/src/routes/api/conversation.tsx +1 -1
- package/src/routes/api/query.tsx +32 -26
- package/src/routes/api/status.tsx +6 -4
- package/src/routes/api/sync.tsx +5 -2
- package/src/routes/blocks.tsx +97 -131
- package/src/routes/data-sources.tsx +17 -25
- package/src/routes/dms.tsx +167 -184
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -394
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/src/lib/client-cache.ts +0 -109
|
@@ -12,9 +12,20 @@ import path from "node:path";
|
|
|
12
12
|
import { pipeline } from "node:stream/promises";
|
|
13
13
|
import { promisify } from "node:util";
|
|
14
14
|
import { Effect } from "effect";
|
|
15
|
+
import {
|
|
16
|
+
ArchiveImportPlan,
|
|
17
|
+
type ArchiveMessageRow,
|
|
18
|
+
type ArchiveProfileRow,
|
|
19
|
+
} from "./archive-import-plan";
|
|
15
20
|
import { getBirdclawPaths } from "./config";
|
|
16
21
|
import { getNativeDb } from "./db";
|
|
22
|
+
import { databaseWriteEffect } from "./database-writer";
|
|
17
23
|
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
24
|
+
import { getImportRepository } from "./import-repository";
|
|
25
|
+
import {
|
|
26
|
+
ingestSourcesInBatchesEffect,
|
|
27
|
+
streamAssignedJsonArray,
|
|
28
|
+
} from "./streaming-ingestion";
|
|
18
29
|
import { safeHttpUrl } from "./url-safety";
|
|
19
30
|
|
|
20
31
|
const execFileAsync = promisify(execFile);
|
|
@@ -204,6 +215,56 @@ function readArchiveEntryEffect(
|
|
|
204
215
|
return runUnzipEffect(archivePath, ["-p", archivePath, entryPath]);
|
|
205
216
|
}
|
|
206
217
|
|
|
218
|
+
async function* streamArchiveArrayRecords(
|
|
219
|
+
archivePath: string,
|
|
220
|
+
entryPath: string,
|
|
221
|
+
) {
|
|
222
|
+
const child = spawn("unzip", ["-p", archivePath, entryPath], {
|
|
223
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
224
|
+
});
|
|
225
|
+
let stderr = "";
|
|
226
|
+
child.stderr.setEncoding("utf8");
|
|
227
|
+
child.stderr.on("data", (chunk) => {
|
|
228
|
+
stderr += String(chunk);
|
|
229
|
+
});
|
|
230
|
+
const exit = new Promise<number | null>((resolve, reject) => {
|
|
231
|
+
child.on("error", reject);
|
|
232
|
+
child.on("close", resolve);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
yield* streamAssignedJsonArray(child.stdout);
|
|
237
|
+
const exitCode = await exit;
|
|
238
|
+
if (exitCode !== 0) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Failed to extract ${entryPath}: ${
|
|
241
|
+
stderr.trim() || `exit ${String(exitCode)}`
|
|
242
|
+
}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
} finally {
|
|
246
|
+
if (!child.killed) child.kill();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function processArchiveEntryRecordsEffect(
|
|
251
|
+
archivePath: string,
|
|
252
|
+
entryPath: string,
|
|
253
|
+
processRecord: (record: ArchiveRecord) => void,
|
|
254
|
+
) {
|
|
255
|
+
return ingestSourcesInBatchesEffect({
|
|
256
|
+
sources: [
|
|
257
|
+
{
|
|
258
|
+
id: entryPath,
|
|
259
|
+
stream: () => streamArchiveArrayRecords(archivePath, entryPath),
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
processBatch: (batch) => {
|
|
263
|
+
for (const record of batch) processRecord(record);
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
207
268
|
function getFirstEntry(entries: string[], pattern: RegExp) {
|
|
208
269
|
return entries.find((entry) => pattern.test(normalizeArchivePath(entry)));
|
|
209
270
|
}
|
|
@@ -640,58 +701,14 @@ function extractArchiveMediaFilesEffect(
|
|
|
640
701
|
});
|
|
641
702
|
}
|
|
642
703
|
|
|
643
|
-
function
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
rows.push({
|
|
652
|
-
profileId: `profile_user_${externalUserId}`,
|
|
653
|
-
externalUserId,
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return rows;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function clearImportedData(db = getNativeDb()) {
|
|
661
|
-
db.exec(`
|
|
662
|
-
delete from ai_scores;
|
|
663
|
-
delete from tweet_actions;
|
|
664
|
-
delete from tweet_account_edges;
|
|
665
|
-
delete from tweet_collections;
|
|
666
|
-
delete from link_occurrences;
|
|
667
|
-
delete from url_expansions;
|
|
668
|
-
delete from dm_fts;
|
|
669
|
-
delete from tweets_fts;
|
|
670
|
-
delete from dm_messages;
|
|
671
|
-
delete from dm_conversations;
|
|
672
|
-
delete from tweets;
|
|
673
|
-
delete from profiles;
|
|
674
|
-
delete from accounts;
|
|
675
|
-
`);
|
|
676
|
-
clearAuthoredSyncCursors(db);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function clearAuthoredSyncCursors(db = getNativeDb(), accountId?: string) {
|
|
680
|
-
if (accountId) {
|
|
681
|
-
db.prepare("delete from sync_cache where cache_key = ?").run(
|
|
682
|
-
`authored:xurl:${accountId}:cursor`,
|
|
683
|
-
);
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
db.prepare(
|
|
687
|
-
"delete from sync_cache where cache_key like 'authored:xurl:%:cursor'",
|
|
688
|
-
).run();
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function clearMentionSyncState(db = getNativeDb()) {
|
|
692
|
-
db.prepare(
|
|
693
|
-
"delete from sync_cache where cache_key like 'mentions:sync:%'",
|
|
694
|
-
).run();
|
|
704
|
+
function getArchiveFollowRow(wrapper: ArchiveRecord, key: ArchiveFollowKey) {
|
|
705
|
+
const item = asRecord(wrapper[key]);
|
|
706
|
+
const externalUserId = String(item?.accountId ?? "");
|
|
707
|
+
if (!externalUserId) return undefined;
|
|
708
|
+
return {
|
|
709
|
+
profileId: `profile_user_${externalUserId}`,
|
|
710
|
+
externalUserId,
|
|
711
|
+
};
|
|
695
712
|
}
|
|
696
713
|
|
|
697
714
|
function importArchiveInternalEffect(
|
|
@@ -767,47 +784,21 @@ function importArchiveInternalEffect(
|
|
|
767
784
|
parseArchiveArray(accountContent)[0] ?? null,
|
|
768
785
|
parseArchiveArray(profileContent)[0] ?? null,
|
|
769
786
|
);
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
bookmarked: number;
|
|
786
|
-
liked: number;
|
|
787
|
-
entitiesJson: string;
|
|
788
|
-
mediaJson: string;
|
|
789
|
-
quotedTweetId: string | null;
|
|
790
|
-
}> = [];
|
|
791
|
-
const collectionRows: Array<{
|
|
792
|
-
tweetId: string;
|
|
793
|
-
kind: "likes" | "bookmarks";
|
|
794
|
-
collectedAt: string | null;
|
|
795
|
-
source: string;
|
|
796
|
-
rawJson: string;
|
|
797
|
-
}> = [];
|
|
798
|
-
const tweetRowsById = new Map<string, (typeof tweetRows)[number]>();
|
|
799
|
-
|
|
800
|
-
function addTweetRow(row: (typeof tweetRows)[number]) {
|
|
801
|
-
const existing = tweetRowsById.get(row.id);
|
|
802
|
-
if (existing) {
|
|
803
|
-
existing.bookmarked = Math.max(existing.bookmarked, row.bookmarked);
|
|
804
|
-
existing.liked = Math.max(existing.liked, row.liked);
|
|
805
|
-
if (!existing.text && row.text) existing.text = row.text;
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
tweetRows.push(row);
|
|
809
|
-
tweetRowsById.set(row.id, row);
|
|
810
|
-
}
|
|
787
|
+
const db = getNativeDb();
|
|
788
|
+
const repository = getImportRepository(db);
|
|
789
|
+
const plan = new ArchiveImportPlan();
|
|
790
|
+
const {
|
|
791
|
+
mentionDirectory,
|
|
792
|
+
tweets: tweetRows,
|
|
793
|
+
collections: collectionRows,
|
|
794
|
+
profiles,
|
|
795
|
+
conversations,
|
|
796
|
+
dmMessages,
|
|
797
|
+
followers: followerRows,
|
|
798
|
+
following: followingRows,
|
|
799
|
+
followerIds,
|
|
800
|
+
followingIds,
|
|
801
|
+
} = plan;
|
|
811
802
|
|
|
812
803
|
if (tweetEntries.length > 0) {
|
|
813
804
|
onProgress({
|
|
@@ -817,10 +808,9 @@ function importArchiveInternalEffect(
|
|
|
817
808
|
});
|
|
818
809
|
}
|
|
819
810
|
for (const [tweetFileIndex, entry] of tweetEntries.entries()) {
|
|
820
|
-
|
|
821
|
-
for (const wrapper of parseArchiveArray(content)) {
|
|
811
|
+
yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
|
|
822
812
|
const tweet = asRecord(wrapper.tweet);
|
|
823
|
-
if (!tweet)
|
|
813
|
+
if (!tweet) return;
|
|
824
814
|
|
|
825
815
|
for (const mention of asArray<Record<string, unknown>>(
|
|
826
816
|
asRecord(tweet.entities)?.user_mentions,
|
|
@@ -846,7 +836,7 @@ function importArchiveInternalEffect(
|
|
|
846
836
|
});
|
|
847
837
|
}
|
|
848
838
|
|
|
849
|
-
|
|
839
|
+
plan.addTweet({
|
|
850
840
|
id: String(tweet.id_str ?? tweet.id),
|
|
851
841
|
kind: "home",
|
|
852
842
|
authorProfileId: "profile_me",
|
|
@@ -866,7 +856,7 @@ function importArchiveInternalEffect(
|
|
|
866
856
|
? String(tweet.quoted_status_id_str)
|
|
867
857
|
: null,
|
|
868
858
|
});
|
|
869
|
-
}
|
|
859
|
+
});
|
|
870
860
|
onProgress({
|
|
871
861
|
kind: "slice-file",
|
|
872
862
|
slice: "tweets",
|
|
@@ -891,12 +881,11 @@ function importArchiveInternalEffect(
|
|
|
891
881
|
}
|
|
892
882
|
const tweetRowsBeforeNotes = tweetRows.length;
|
|
893
883
|
for (const [noteFileIndex, entry] of noteTweetEntries.entries()) {
|
|
894
|
-
|
|
895
|
-
for (const wrapper of parseArchiveArray(content)) {
|
|
884
|
+
yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
|
|
896
885
|
const noteTweet = asRecord(wrapper.noteTweet);
|
|
897
|
-
if (!noteTweet)
|
|
886
|
+
if (!noteTweet) return;
|
|
898
887
|
const core = asRecord(noteTweet.core);
|
|
899
|
-
|
|
888
|
+
plan.addTweet({
|
|
900
889
|
id: String(noteTweet.noteTweetId ?? noteTweet.id ?? randomUUID()),
|
|
901
890
|
kind: "home",
|
|
902
891
|
authorProfileId: "profile_me",
|
|
@@ -912,7 +901,7 @@ function importArchiveInternalEffect(
|
|
|
912
901
|
mediaJson: "[]",
|
|
913
902
|
quotedTweetId: null,
|
|
914
903
|
});
|
|
915
|
-
}
|
|
904
|
+
});
|
|
916
905
|
onProgress({
|
|
917
906
|
kind: "slice-file",
|
|
918
907
|
slice: "noteTweets",
|
|
@@ -929,38 +918,7 @@ function importArchiveInternalEffect(
|
|
|
929
918
|
}
|
|
930
919
|
const authoredTweetCount = tweetRows.length;
|
|
931
920
|
|
|
932
|
-
type
|
|
933
|
-
id: string;
|
|
934
|
-
conversationId: string;
|
|
935
|
-
senderProfileId: string;
|
|
936
|
-
text: string;
|
|
937
|
-
createdAt: string;
|
|
938
|
-
direction: "inbound" | "outbound";
|
|
939
|
-
mediaCount: number;
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
const profiles = new Map<
|
|
943
|
-
string,
|
|
944
|
-
{
|
|
945
|
-
id: string;
|
|
946
|
-
handle: string;
|
|
947
|
-
displayName: string;
|
|
948
|
-
bio: string;
|
|
949
|
-
followersCount: number;
|
|
950
|
-
followingCount: number;
|
|
951
|
-
publicMetricsJson: string;
|
|
952
|
-
avatarHue: number;
|
|
953
|
-
avatarUrl: string | null;
|
|
954
|
-
location: string | null;
|
|
955
|
-
url: string | null;
|
|
956
|
-
verifiedType: string | null;
|
|
957
|
-
entitiesJson: string;
|
|
958
|
-
rawJson: string;
|
|
959
|
-
createdAt: string;
|
|
960
|
-
}
|
|
961
|
-
>();
|
|
962
|
-
type ProfileRow =
|
|
963
|
-
typeof profiles extends Map<string, infer Value> ? Value : never;
|
|
921
|
+
type ProfileRow = ArchiveProfileRow;
|
|
964
922
|
const defaultProfileMetadata = {
|
|
965
923
|
publicMetricsJson: "{}",
|
|
966
924
|
location: null,
|
|
@@ -969,25 +927,6 @@ function importArchiveInternalEffect(
|
|
|
969
927
|
entitiesJson: "{}",
|
|
970
928
|
rawJson: "{}",
|
|
971
929
|
};
|
|
972
|
-
const conversations = new Map<
|
|
973
|
-
string,
|
|
974
|
-
{
|
|
975
|
-
id: string;
|
|
976
|
-
title: string;
|
|
977
|
-
accountId: string;
|
|
978
|
-
participantProfileId: string;
|
|
979
|
-
lastMessageAt: string;
|
|
980
|
-
unreadCount: number;
|
|
981
|
-
needsReply: number;
|
|
982
|
-
}
|
|
983
|
-
>();
|
|
984
|
-
const dmMessages: MessageRow[] = [];
|
|
985
|
-
const followerRows: Array<{ profileId: string; externalUserId: string }> =
|
|
986
|
-
[];
|
|
987
|
-
const followingRows: Array<{ profileId: string; externalUserId: string }> =
|
|
988
|
-
[];
|
|
989
|
-
const followerIds = new Set<string>();
|
|
990
|
-
const followingIds = new Set<string>();
|
|
991
930
|
type ExistingProfileRow = {
|
|
992
931
|
id: string;
|
|
993
932
|
handle: string;
|
|
@@ -1006,18 +945,14 @@ function importArchiveInternalEffect(
|
|
|
1006
945
|
created_at: string;
|
|
1007
946
|
};
|
|
1008
947
|
const existingProfiles = new Map(
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
.prepare(
|
|
1012
|
-
`
|
|
948
|
+
repository
|
|
949
|
+
.readRows<ExistingProfileRow>(`
|
|
1013
950
|
select id, handle, display_name, bio, followers_count, following_count,
|
|
1014
951
|
public_metrics_json, avatar_hue, avatar_url, location, url,
|
|
1015
952
|
verified_type, entities_json, raw_json, created_at
|
|
1016
953
|
from profiles
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
.all() as ExistingProfileRow[]
|
|
1020
|
-
).map((profile) => [profile.id, profile]),
|
|
954
|
+
`)
|
|
955
|
+
.map((profile) => [profile.id, profile]),
|
|
1021
956
|
);
|
|
1022
957
|
const existingProfilesByHandle = new Map(
|
|
1023
958
|
[...existingProfiles.values()].map((profile) => [
|
|
@@ -1025,11 +960,13 @@ function importArchiveInternalEffect(
|
|
|
1025
960
|
profile,
|
|
1026
961
|
]),
|
|
1027
962
|
);
|
|
1028
|
-
const existingPrimaryAccount =
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
963
|
+
const existingPrimaryAccount = repository.readRow<{
|
|
964
|
+
handle: string;
|
|
965
|
+
external_user_id: string | null;
|
|
966
|
+
}>(
|
|
967
|
+
"select handle, external_user_id from accounts where id = ?",
|
|
968
|
+
"acct_primary",
|
|
969
|
+
);
|
|
1033
970
|
const profileIdAliases = new Map<string, string>();
|
|
1034
971
|
|
|
1035
972
|
type ArchiveProfileTier =
|
|
@@ -1363,13 +1300,12 @@ function importArchiveInternalEffect(
|
|
|
1363
1300
|
});
|
|
1364
1301
|
}
|
|
1365
1302
|
for (const [dmFileIndex, entry] of dmEntries.entries()) {
|
|
1366
|
-
|
|
1367
|
-
for (const wrapper of parseArchiveArray(content)) {
|
|
1303
|
+
yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
|
|
1368
1304
|
const dmConversation = asRecord(wrapper.dmConversation);
|
|
1369
|
-
if (!dmConversation)
|
|
1305
|
+
if (!dmConversation) return;
|
|
1370
1306
|
|
|
1371
1307
|
const rawConversationId = String(dmConversation.conversationId ?? "");
|
|
1372
|
-
if (!rawConversationId)
|
|
1308
|
+
if (!rawConversationId) return;
|
|
1373
1309
|
const conversationId =
|
|
1374
1310
|
resolveArchiveDmConversationId(rawConversationId);
|
|
1375
1311
|
const conversationIdChanged = conversationId !== rawConversationId;
|
|
@@ -1516,18 +1452,18 @@ function importArchiveInternalEffect(
|
|
|
1516
1452
|
direction:
|
|
1517
1453
|
senderId === accountPayload.accountId ? "outbound" : "inbound",
|
|
1518
1454
|
mediaCount: asArray(messageCreate.mediaUrls).length,
|
|
1519
|
-
} satisfies
|
|
1455
|
+
} satisfies ArchiveMessageRow;
|
|
1520
1456
|
})
|
|
1521
1457
|
.sort((left, right) =>
|
|
1522
1458
|
compareIsoTimestamp(left.createdAt, right.createdAt),
|
|
1523
1459
|
);
|
|
1524
1460
|
|
|
1525
1461
|
if (messageEvents.length === 0) {
|
|
1526
|
-
|
|
1462
|
+
return;
|
|
1527
1463
|
}
|
|
1528
1464
|
|
|
1529
1465
|
const lastMessage = messageEvents.at(-1);
|
|
1530
|
-
if (!lastMessage)
|
|
1466
|
+
if (!lastMessage) return;
|
|
1531
1467
|
|
|
1532
1468
|
dmMessages.push(...messageEvents);
|
|
1533
1469
|
const resolvedParticipantProfileId =
|
|
@@ -1544,7 +1480,7 @@ function importArchiveInternalEffect(
|
|
|
1544
1480
|
unreadCount: 0,
|
|
1545
1481
|
needsReply: lastMessage.direction === "inbound" ? 1 : 0,
|
|
1546
1482
|
});
|
|
1547
|
-
}
|
|
1483
|
+
});
|
|
1548
1484
|
onProgress({
|
|
1549
1485
|
kind: "slice-file",
|
|
1550
1486
|
slice: "directMessages",
|
|
@@ -1569,11 +1505,10 @@ function importArchiveInternalEffect(
|
|
|
1569
1505
|
}
|
|
1570
1506
|
let likeCount = 0;
|
|
1571
1507
|
for (const [likeFileIndex, entry] of likeEntries.entries()) {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
for (const like of likes) {
|
|
1508
|
+
yield* processArchiveEntryRecordsEffect(archivePath, entry, (like) => {
|
|
1509
|
+
likeCount += 1;
|
|
1575
1510
|
const tweet = extractCollectionTweet(like, "like");
|
|
1576
|
-
if (!tweet)
|
|
1511
|
+
if (!tweet) return;
|
|
1577
1512
|
collectionRows.push({
|
|
1578
1513
|
tweetId: tweet.id,
|
|
1579
1514
|
kind: "likes",
|
|
@@ -1581,7 +1516,7 @@ function importArchiveInternalEffect(
|
|
|
1581
1516
|
source: "archive",
|
|
1582
1517
|
rawJson: JSON.stringify(like),
|
|
1583
1518
|
});
|
|
1584
|
-
|
|
1519
|
+
plan.addTweet({
|
|
1585
1520
|
id: tweet.id,
|
|
1586
1521
|
kind: "like",
|
|
1587
1522
|
authorProfileId: "profile_unknown",
|
|
@@ -1597,8 +1532,7 @@ function importArchiveInternalEffect(
|
|
|
1597
1532
|
mediaJson: "[]",
|
|
1598
1533
|
quotedTweetId: null,
|
|
1599
1534
|
});
|
|
1600
|
-
}
|
|
1601
|
-
likeCount += likes.length;
|
|
1535
|
+
});
|
|
1602
1536
|
onProgress({
|
|
1603
1537
|
kind: "slice-file",
|
|
1604
1538
|
slice: "likes",
|
|
@@ -1619,36 +1553,38 @@ function importArchiveInternalEffect(
|
|
|
1619
1553
|
}
|
|
1620
1554
|
let bookmarkCount = 0;
|
|
1621
1555
|
for (const [bookmarkFileIndex, entry] of bookmarkEntries.entries()) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1556
|
+
yield* processArchiveEntryRecordsEffect(
|
|
1557
|
+
archivePath,
|
|
1558
|
+
entry,
|
|
1559
|
+
(bookmark) => {
|
|
1560
|
+
bookmarkCount += 1;
|
|
1561
|
+
const tweet = extractCollectionTweet(bookmark, "bookmark");
|
|
1562
|
+
if (!tweet) return;
|
|
1563
|
+
collectionRows.push({
|
|
1564
|
+
tweetId: tweet.id,
|
|
1565
|
+
kind: "bookmarks",
|
|
1566
|
+
collectedAt: tweet.createdAt,
|
|
1567
|
+
source: "archive",
|
|
1568
|
+
rawJson: JSON.stringify(bookmark),
|
|
1569
|
+
});
|
|
1570
|
+
plan.addTweet({
|
|
1571
|
+
id: tweet.id,
|
|
1572
|
+
kind: "bookmark",
|
|
1573
|
+
authorProfileId: "profile_unknown",
|
|
1574
|
+
text: tweet.text,
|
|
1575
|
+
createdAt: tweet.createdAt,
|
|
1576
|
+
isReplied: 0,
|
|
1577
|
+
replyToId: null,
|
|
1578
|
+
likeCount: tweet.likeCount,
|
|
1579
|
+
mediaCount: 0,
|
|
1580
|
+
bookmarked: 1,
|
|
1581
|
+
liked: 0,
|
|
1582
|
+
entitiesJson: "{}",
|
|
1583
|
+
mediaJson: "[]",
|
|
1584
|
+
quotedTweetId: null,
|
|
1585
|
+
});
|
|
1586
|
+
},
|
|
1587
|
+
);
|
|
1652
1588
|
onProgress({
|
|
1653
1589
|
kind: "slice-file",
|
|
1654
1590
|
slice: "bookmarks",
|
|
@@ -1686,12 +1622,12 @@ function importArchiveInternalEffect(
|
|
|
1686
1622
|
});
|
|
1687
1623
|
}
|
|
1688
1624
|
for (const [followerFileIndex, entry] of followerEntries.entries()) {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
if (followerIds.has(row.externalUserId))
|
|
1625
|
+
yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
|
|
1626
|
+
const row = getArchiveFollowRow(wrapper, "follower");
|
|
1627
|
+
if (!row || followerIds.has(row.externalUserId)) return;
|
|
1692
1628
|
followerIds.add(row.externalUserId);
|
|
1693
1629
|
followerRows.push(row);
|
|
1694
|
-
}
|
|
1630
|
+
});
|
|
1695
1631
|
onProgress({
|
|
1696
1632
|
kind: "slice-file",
|
|
1697
1633
|
slice: "followers",
|
|
@@ -1715,12 +1651,12 @@ function importArchiveInternalEffect(
|
|
|
1715
1651
|
});
|
|
1716
1652
|
}
|
|
1717
1653
|
for (const [followingFileIndex, entry] of followingEntries.entries()) {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
if (followingIds.has(row.externalUserId))
|
|
1654
|
+
yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
|
|
1655
|
+
const row = getArchiveFollowRow(wrapper, "following");
|
|
1656
|
+
if (!row || followingIds.has(row.externalUserId)) return;
|
|
1721
1657
|
followingIds.add(row.externalUserId);
|
|
1722
1658
|
followingRows.push(row);
|
|
1723
|
-
}
|
|
1659
|
+
});
|
|
1724
1660
|
onProgress({
|
|
1725
1661
|
kind: "slice-file",
|
|
1726
1662
|
slice: "following",
|
|
@@ -1747,25 +1683,21 @@ function importArchiveInternalEffect(
|
|
|
1747
1683
|
if (includeFollowing && followingEntries.length === 0) {
|
|
1748
1684
|
clearedFollowDirections.add("following");
|
|
1749
1685
|
}
|
|
1750
|
-
const retainedFollowProfiles =
|
|
1751
|
-
.prepare(
|
|
1752
|
-
`
|
|
1753
|
-
select direction, profile_id, external_user_id, source, null as snapshot_id, null as snapshot_source
|
|
1754
|
-
from follow_edges
|
|
1755
|
-
union
|
|
1756
|
-
select ev.direction, ev.profile_id, ev.external_user_id, null as source, ev.snapshot_id, snap.source as snapshot_source
|
|
1757
|
-
from follow_events ev
|
|
1758
|
-
left join follow_snapshots snap on snap.id = ev.snapshot_id
|
|
1759
|
-
`,
|
|
1760
|
-
)
|
|
1761
|
-
.all() as Array<{
|
|
1686
|
+
const retainedFollowProfiles = repository.readRows<{
|
|
1762
1687
|
direction: ArchiveFollowDirection;
|
|
1763
1688
|
profile_id: string;
|
|
1764
1689
|
external_user_id: string;
|
|
1765
1690
|
source: string | null;
|
|
1766
1691
|
snapshot_id: string | null;
|
|
1767
1692
|
snapshot_source: string | null;
|
|
1768
|
-
}
|
|
1693
|
+
}>(`
|
|
1694
|
+
select direction, profile_id, external_user_id, source, null as snapshot_id, null as snapshot_source
|
|
1695
|
+
from follow_edges
|
|
1696
|
+
union
|
|
1697
|
+
select ev.direction, ev.profile_id, ev.external_user_id, null as source, ev.snapshot_id, snap.source as snapshot_source
|
|
1698
|
+
from follow_events ev
|
|
1699
|
+
left join follow_snapshots snap on snap.id = ev.snapshot_id
|
|
1700
|
+
`);
|
|
1769
1701
|
for (const row of retainedFollowProfiles) {
|
|
1770
1702
|
const isClearedArchiveRow =
|
|
1771
1703
|
clearedFollowDirections.has(row.direction) &&
|
|
@@ -1803,7 +1735,6 @@ function importArchiveInternalEffect(
|
|
|
1803
1735
|
);
|
|
1804
1736
|
}
|
|
1805
1737
|
|
|
1806
|
-
const db = getNativeDb();
|
|
1807
1738
|
const insertAccount = db.prepare(`
|
|
1808
1739
|
insert into accounts (id, name, handle, external_user_id, transport, is_default, created_at)
|
|
1809
1740
|
values (?, ?, ?, ?, ?, 1, ?)
|
|
@@ -2451,15 +2382,15 @@ function importArchiveInternalEffect(
|
|
|
2451
2382
|
onProgress({ kind: "write-progress", phase, processed, total });
|
|
2452
2383
|
}
|
|
2453
2384
|
}
|
|
2454
|
-
|
|
2385
|
+
yield* databaseWriteEffect(() => {
|
|
2455
2386
|
if (!selection) {
|
|
2456
|
-
|
|
2457
|
-
clearMentionSyncState(
|
|
2387
|
+
repository.clearArchiveImport();
|
|
2388
|
+
repository.clearMentionSyncState();
|
|
2458
2389
|
}
|
|
2459
2390
|
|
|
2460
2391
|
if (selection) {
|
|
2461
2392
|
if (includeTweets) {
|
|
2462
|
-
clearAuthoredSyncCursors(
|
|
2393
|
+
repository.clearAuthoredSyncCursors("acct_primary");
|
|
2463
2394
|
demoteSelectedArchiveTweetsWithCollections.run(
|
|
2464
2395
|
"acct_primary",
|
|
2465
2396
|
"acct_primary",
|
|
@@ -2704,7 +2635,7 @@ function importArchiveInternalEffect(
|
|
|
2704
2635
|
} else if (includeFollowing) {
|
|
2705
2636
|
clearArchiveFollowRows("following");
|
|
2706
2637
|
}
|
|
2707
|
-
})
|
|
2638
|
+
}, db);
|
|
2708
2639
|
onProgress({ kind: "done" });
|
|
2709
2640
|
|
|
2710
2641
|
return {
|