birdclaw 0.5.1 → 0.7.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 +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- package/src/styles.css +22 -0
|
@@ -11,8 +11,11 @@ import {
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { pipeline } from "node:stream/promises";
|
|
13
13
|
import { promisify } from "node:util";
|
|
14
|
+
import { Effect } from "effect";
|
|
14
15
|
import { getBirdclawPaths } from "./config";
|
|
15
16
|
import { getNativeDb } from "./db";
|
|
17
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
18
|
+
import { safeHttpUrl } from "./url-safety";
|
|
16
19
|
|
|
17
20
|
const execFileAsync = promisify(execFile);
|
|
18
21
|
const ARCHIVE_JSON_PAYLOAD = /=\s*(\[[\s\S]*\]|\{[\s\S]*\})/s;
|
|
@@ -58,8 +61,45 @@ export const ARCHIVE_IMPORT_SLICES = [
|
|
|
58
61
|
|
|
59
62
|
export type ArchiveImportSlice = (typeof ARCHIVE_IMPORT_SLICES)[number];
|
|
60
63
|
|
|
64
|
+
export type ImportProgressSlice =
|
|
65
|
+
| "tweets"
|
|
66
|
+
| "noteTweets"
|
|
67
|
+
| "directMessages"
|
|
68
|
+
| "likes"
|
|
69
|
+
| "bookmarks"
|
|
70
|
+
| "media"
|
|
71
|
+
| "followers"
|
|
72
|
+
| "following";
|
|
73
|
+
|
|
74
|
+
export type ImportWritePhase =
|
|
75
|
+
| "profiles"
|
|
76
|
+
| "tweets"
|
|
77
|
+
| "collections"
|
|
78
|
+
| "dmMessages";
|
|
79
|
+
|
|
80
|
+
export type ImportProgressEvent =
|
|
81
|
+
| { kind: "scanned"; entryCount: number }
|
|
82
|
+
| { kind: "slice-start"; slice: ImportProgressSlice; files: number }
|
|
83
|
+
| {
|
|
84
|
+
kind: "slice-file";
|
|
85
|
+
slice: ImportProgressSlice;
|
|
86
|
+
processed: number;
|
|
87
|
+
files: number;
|
|
88
|
+
}
|
|
89
|
+
| { kind: "slice-done"; slice: ImportProgressSlice; count: number }
|
|
90
|
+
| { kind: "writing" }
|
|
91
|
+
| { kind: "write-start"; phase: ImportWritePhase; total: number }
|
|
92
|
+
| {
|
|
93
|
+
kind: "write-progress";
|
|
94
|
+
phase: ImportWritePhase;
|
|
95
|
+
processed: number;
|
|
96
|
+
total: number;
|
|
97
|
+
}
|
|
98
|
+
| { kind: "done" };
|
|
99
|
+
|
|
61
100
|
export interface ImportArchiveOptions {
|
|
62
101
|
select?: ArchiveImportSlice[];
|
|
102
|
+
onProgress?: (event: ImportProgressEvent) => void;
|
|
63
103
|
}
|
|
64
104
|
|
|
65
105
|
type ArchiveRecord = Record<string, unknown>;
|
|
@@ -108,48 +148,60 @@ function parseArchiveArray(content: string): ArchiveRecord[] {
|
|
|
108
148
|
: [];
|
|
109
149
|
}
|
|
110
150
|
|
|
111
|
-
|
|
151
|
+
function runUnzipEffect(
|
|
112
152
|
_archivePath: string,
|
|
113
153
|
args: string[],
|
|
114
154
|
maxBuffer = 1024 * 1024 * 256,
|
|
115
155
|
) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
156
|
+
return tryPromise(() =>
|
|
157
|
+
execFileAsync("unzip", args, {
|
|
158
|
+
maxBuffer,
|
|
159
|
+
}),
|
|
160
|
+
).pipe(Effect.map(({ stdout }) => stdout));
|
|
120
161
|
}
|
|
121
162
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
163
|
+
function listArchiveEntriesEffect(
|
|
164
|
+
archivePath: string,
|
|
165
|
+
): Effect.Effect<string[], unknown> {
|
|
166
|
+
return Effect.gen(function* () {
|
|
167
|
+
const stdout = yield* runUnzipEffect(
|
|
168
|
+
archivePath,
|
|
169
|
+
["-Z1", archivePath],
|
|
170
|
+
1024 * 1024 * 64,
|
|
171
|
+
);
|
|
172
|
+
return stdout
|
|
173
|
+
.split("\n")
|
|
174
|
+
.map((item) => item.trim())
|
|
175
|
+
.filter((item) => item.length > 0);
|
|
176
|
+
});
|
|
132
177
|
}
|
|
133
178
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
function listArchiveEntryDetailsEffect(
|
|
180
|
+
archivePath: string,
|
|
181
|
+
): Effect.Effect<Array<{ path: string; size: number }>, unknown> {
|
|
182
|
+
return Effect.gen(function* () {
|
|
183
|
+
const stdout = yield* runUnzipEffect(
|
|
184
|
+
archivePath,
|
|
185
|
+
["-Z", "-l", archivePath],
|
|
186
|
+
1024 * 1024 * 64,
|
|
187
|
+
);
|
|
188
|
+
return stdout
|
|
189
|
+
.split("\n")
|
|
190
|
+
.map((line) => line.trim().split(/\s+/))
|
|
191
|
+
.filter((parts) => parts.length >= 10 && /^[-d]/.test(parts[0] ?? ""))
|
|
192
|
+
.map((parts) => ({
|
|
193
|
+
path: parts.slice(9).join(" "),
|
|
194
|
+
size: Number(parts[3] ?? 0),
|
|
195
|
+
}))
|
|
196
|
+
.filter((entry) => entry.path.length > 0 && Number.isFinite(entry.size));
|
|
197
|
+
});
|
|
149
198
|
}
|
|
150
199
|
|
|
151
|
-
|
|
152
|
-
|
|
200
|
+
function readArchiveEntryEffect(
|
|
201
|
+
archivePath: string,
|
|
202
|
+
entryPath: string,
|
|
203
|
+
): Effect.Effect<string, unknown> {
|
|
204
|
+
return runUnzipEffect(archivePath, ["-p", archivePath, entryPath]);
|
|
153
205
|
}
|
|
154
206
|
|
|
155
207
|
function getFirstEntry(entries: string[], pattern: RegExp) {
|
|
@@ -216,6 +268,10 @@ function toFiniteNumber(value: unknown) {
|
|
|
216
268
|
return Number.isFinite(number) ? number : undefined;
|
|
217
269
|
}
|
|
218
270
|
|
|
271
|
+
function archiveHttpUrl(value: unknown) {
|
|
272
|
+
return safeHttpUrl(typeof value === "string" ? value : String(value ?? ""));
|
|
273
|
+
}
|
|
274
|
+
|
|
219
275
|
function extractTweetEntities(tweet: Record<string, unknown>) {
|
|
220
276
|
const entities = asRecord(tweet.entities);
|
|
221
277
|
const urlEntries = [
|
|
@@ -225,10 +281,10 @@ function extractTweetEntities(tweet: Record<string, unknown>) {
|
|
|
225
281
|
const seenUrls = new Set<string>();
|
|
226
282
|
const urls = urlEntries
|
|
227
283
|
.map((entry) => ({
|
|
228
|
-
url:
|
|
229
|
-
expandedUrl:
|
|
230
|
-
entry.expanded_url ?? entry.expandedUrl ?? entry.url ??
|
|
231
|
-
|
|
284
|
+
url: archiveHttpUrl(entry.url) ?? "",
|
|
285
|
+
expandedUrl:
|
|
286
|
+
archiveHttpUrl(entry.expanded_url ?? entry.expandedUrl ?? entry.url) ??
|
|
287
|
+
"",
|
|
232
288
|
displayUrl: String(
|
|
233
289
|
entry.display_url ??
|
|
234
290
|
entry.displayUrl ??
|
|
@@ -242,17 +298,13 @@ function extractTweetEntities(tweet: Record<string, unknown>) {
|
|
|
242
298
|
description:
|
|
243
299
|
typeof entry.description === "string" ? entry.description : null,
|
|
244
300
|
imageUrl:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
? entry.media_url_https
|
|
253
|
-
: typeof entry.media_url === "string"
|
|
254
|
-
? entry.media_url
|
|
255
|
-
: undefined,
|
|
301
|
+
archiveHttpUrl(
|
|
302
|
+
entry.image_url ??
|
|
303
|
+
entry.imageUrl ??
|
|
304
|
+
entry.thumbnail_url ??
|
|
305
|
+
entry.media_url_https ??
|
|
306
|
+
entry.media_url,
|
|
307
|
+
) ?? undefined,
|
|
256
308
|
siteName:
|
|
257
309
|
typeof entry.site_name === "string"
|
|
258
310
|
? entry.site_name
|
|
@@ -356,12 +408,11 @@ function extractTweetMedia(tweet: Record<string, unknown>) {
|
|
|
356
408
|
|
|
357
409
|
return sourceMedia
|
|
358
410
|
.map((entry) => {
|
|
359
|
-
const url =
|
|
360
|
-
entry.media_url_https ?? entry.media_url ?? entry.url ??
|
|
361
|
-
|
|
362
|
-
const thumbnailUrl =
|
|
363
|
-
entry.media_url_https ?? entry.media_url ?? url
|
|
364
|
-
);
|
|
411
|
+
const url =
|
|
412
|
+
archiveHttpUrl(entry.media_url_https ?? entry.media_url ?? entry.url) ??
|
|
413
|
+
"";
|
|
414
|
+
const thumbnailUrl =
|
|
415
|
+
archiveHttpUrl(entry.media_url_https ?? entry.media_url ?? url) ?? url;
|
|
365
416
|
const videoInfo = asRecord(entry.video_info);
|
|
366
417
|
const durationMs = toFiniteNumber(videoInfo?.duration_millis);
|
|
367
418
|
const variants = archiveMp4Variants(entry);
|
|
@@ -515,66 +566,78 @@ function needsArchiveMediaCopy(destinationPath: string, size: number) {
|
|
|
515
566
|
return statSync(destinationPath).size !== size;
|
|
516
567
|
}
|
|
517
568
|
|
|
518
|
-
|
|
569
|
+
function copyArchiveEntryToFileEffect(
|
|
519
570
|
archivePath: string,
|
|
520
571
|
entryPath: string,
|
|
521
572
|
destinationPath: string,
|
|
522
573
|
) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
stderr
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
574
|
+
return tryPromise(() => {
|
|
575
|
+
mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
576
|
+
const temporaryPath = `${destinationPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
577
|
+
const child = spawn("unzip", ["-p", archivePath, entryPath], {
|
|
578
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
579
|
+
});
|
|
580
|
+
let stderr = "";
|
|
581
|
+
child.stderr.setEncoding("utf8");
|
|
582
|
+
child.stderr.on("data", (chunk) => {
|
|
583
|
+
stderr += String(chunk);
|
|
584
|
+
});
|
|
585
|
+
const exit = new Promise<number | null>((resolve, reject) => {
|
|
586
|
+
child.on("error", reject);
|
|
587
|
+
child.on("close", resolve);
|
|
588
|
+
});
|
|
537
589
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
590
|
+
return pipeline(child.stdout, createWriteStream(temporaryPath))
|
|
591
|
+
.then(() => exit)
|
|
592
|
+
.then((exitCode) => {
|
|
593
|
+
if (exitCode !== 0) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Failed to extract ${entryPath}: ${
|
|
596
|
+
stderr.trim() || `exit ${String(exitCode)}`
|
|
597
|
+
}`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
renameSync(temporaryPath, destinationPath);
|
|
601
|
+
})
|
|
602
|
+
.catch((error: unknown) => {
|
|
603
|
+
child.kill();
|
|
604
|
+
if (existsSync(temporaryPath)) {
|
|
605
|
+
unlinkSync(temporaryPath);
|
|
606
|
+
}
|
|
607
|
+
throw error;
|
|
608
|
+
});
|
|
609
|
+
});
|
|
554
610
|
}
|
|
555
611
|
|
|
556
|
-
|
|
612
|
+
function extractArchiveMediaFilesEffect(
|
|
557
613
|
archivePath: string,
|
|
558
614
|
selectedKinds: Set<ArchiveMediaKind> | null,
|
|
559
|
-
) {
|
|
560
|
-
|
|
561
|
-
|
|
615
|
+
): Effect.Effect<ArchiveMediaFileCounts, unknown> {
|
|
616
|
+
return Effect.gen(function* () {
|
|
617
|
+
const counts = createArchiveMediaFileCounts();
|
|
618
|
+
if (selectedKinds?.size === 0) {
|
|
619
|
+
return counts;
|
|
620
|
+
}
|
|
621
|
+
const entries = yield* listArchiveEntryDetailsEffect(archivePath);
|
|
622
|
+
for (const entry of entries) {
|
|
623
|
+
const mediaKind = getArchiveMediaKind(entry.path);
|
|
624
|
+
if (!mediaKind) continue;
|
|
625
|
+
if (selectedKinds && !selectedKinds.has(mediaKind.kind)) continue;
|
|
626
|
+
|
|
627
|
+
counts[mediaKind.kind] += 1;
|
|
628
|
+
const destinationPath = getArchiveMediaDestination(
|
|
629
|
+
entry.path,
|
|
630
|
+
mediaKind.kind,
|
|
631
|
+
);
|
|
632
|
+
if (!needsArchiveMediaCopy(destinationPath, entry.size)) continue;
|
|
633
|
+
yield* copyArchiveEntryToFileEffect(
|
|
634
|
+
archivePath,
|
|
635
|
+
entry.path,
|
|
636
|
+
destinationPath,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
562
639
|
return counts;
|
|
563
|
-
}
|
|
564
|
-
for (const entry of await listArchiveEntryDetails(archivePath)) {
|
|
565
|
-
const mediaKind = getArchiveMediaKind(entry.path);
|
|
566
|
-
if (!mediaKind) continue;
|
|
567
|
-
if (selectedKinds && !selectedKinds.has(mediaKind.kind)) continue;
|
|
568
|
-
|
|
569
|
-
counts[mediaKind.kind] += 1;
|
|
570
|
-
const destinationPath = getArchiveMediaDestination(
|
|
571
|
-
entry.path,
|
|
572
|
-
mediaKind.kind,
|
|
573
|
-
);
|
|
574
|
-
if (!needsArchiveMediaCopy(destinationPath, entry.size)) continue;
|
|
575
|
-
await copyArchiveEntryToFile(archivePath, entry.path, destinationPath);
|
|
576
|
-
}
|
|
577
|
-
return counts;
|
|
640
|
+
});
|
|
578
641
|
}
|
|
579
642
|
|
|
580
643
|
function getArchiveFollowRows(content: string, key: ArchiveFollowKey) {
|
|
@@ -594,8 +657,7 @@ function getArchiveFollowRows(content: string, key: ArchiveFollowKey) {
|
|
|
594
657
|
return rows;
|
|
595
658
|
}
|
|
596
659
|
|
|
597
|
-
function clearImportedData() {
|
|
598
|
-
const db = getNativeDb();
|
|
660
|
+
function clearImportedData(db = getNativeDb()) {
|
|
599
661
|
db.exec(`
|
|
600
662
|
delete from ai_scores;
|
|
601
663
|
delete from tweet_actions;
|
|
@@ -632,894 +694,1062 @@ function clearMentionSyncState(db = getNativeDb()) {
|
|
|
632
694
|
).run();
|
|
633
695
|
}
|
|
634
696
|
|
|
635
|
-
|
|
697
|
+
function importArchiveInternalEffect(
|
|
636
698
|
archivePath: string,
|
|
637
699
|
options: ImportArchiveOptions = {},
|
|
638
|
-
):
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
700
|
+
): Effect.Effect<ImportedArchiveSummary, unknown> {
|
|
701
|
+
return Effect.gen(function* () {
|
|
702
|
+
const onProgress = options.onProgress ?? (() => {});
|
|
703
|
+
const entries = yield* listArchiveEntriesEffect(archivePath);
|
|
704
|
+
onProgress({ kind: "scanned", entryCount: entries.length });
|
|
705
|
+
const selection = selectedSlices(options);
|
|
706
|
+
const includeTweets = includesSlice(selection, "tweets");
|
|
707
|
+
const includeLikes = includesSlice(selection, "likes");
|
|
708
|
+
const includeBookmarks = includesSlice(selection, "bookmarks");
|
|
709
|
+
const includeDirectMessages = includesSlice(selection, "directMessages");
|
|
710
|
+
const includeProfiles = includesSlice(selection, "profiles");
|
|
711
|
+
const includeFollowers = includesSlice(selection, "followers");
|
|
712
|
+
const includeFollowing = includesSlice(selection, "following");
|
|
713
|
+
const accountEntry = getFirstEntry(entries, /(?:^|\/)data\/account\.js$/i);
|
|
714
|
+
const profileEntry = getFirstEntry(entries, /(?:^|\/)data\/profile\.js$/i);
|
|
715
|
+
const tweetEntries = includeTweets
|
|
716
|
+
? getMatchingEntries(
|
|
717
|
+
entries,
|
|
718
|
+
/(?:^|\/)data\/(?:tweets|community-tweet)(?:-part\d+)?\.js$/i,
|
|
719
|
+
)
|
|
720
|
+
: [];
|
|
721
|
+
const noteTweetEntries = includeTweets
|
|
722
|
+
? getMatchingEntries(
|
|
723
|
+
entries,
|
|
724
|
+
/(?:^|\/)data\/note-tweet(?:-part\d+)?\.js$/i,
|
|
725
|
+
)
|
|
726
|
+
: [];
|
|
727
|
+
const likeEntries = includeLikes
|
|
728
|
+
? getMatchingEntries(
|
|
729
|
+
entries,
|
|
730
|
+
/(?:^|\/)data\/(?:like|likes)(?:-part\d+)?\.js$/i,
|
|
731
|
+
)
|
|
732
|
+
: [];
|
|
733
|
+
const bookmarkEntries = includeBookmarks
|
|
734
|
+
? getMatchingEntries(
|
|
735
|
+
entries,
|
|
736
|
+
/(?:^|\/)data\/(?:bookmark|bookmarks)(?:-part\d+)?\.js$/i,
|
|
737
|
+
)
|
|
738
|
+
: [];
|
|
739
|
+
const dmEntries = includeDirectMessages
|
|
740
|
+
? getMatchingEntries(
|
|
741
|
+
entries,
|
|
742
|
+
/(?:^|\/)data\/direct-messages(?:-group)?(?:-part\d+)?\.js$/i,
|
|
743
|
+
)
|
|
744
|
+
: [];
|
|
745
|
+
const followerEntries = includeFollowers
|
|
746
|
+
? getMatchingEntries(entries, /(?:^|\/)data\/follower(?:-part\d+)?\.js$/i)
|
|
747
|
+
: [];
|
|
748
|
+
const followingEntries = includeFollowing
|
|
749
|
+
? getMatchingEntries(
|
|
750
|
+
entries,
|
|
751
|
+
/(?:^|\/)data\/following(?:-part\d+)?\.js$/i,
|
|
752
|
+
)
|
|
753
|
+
: [];
|
|
683
754
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
755
|
+
if (!accountEntry) {
|
|
756
|
+
return yield* Effect.fail(new Error("Archive missing data/account.js"));
|
|
757
|
+
}
|
|
687
758
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
759
|
+
const [accountContent, profileContent] = yield* Effect.all([
|
|
760
|
+
readArchiveEntryEffect(archivePath, accountEntry),
|
|
761
|
+
profileEntry
|
|
762
|
+
? readArchiveEntryEffect(archivePath, profileEntry)
|
|
763
|
+
: Effect.succeed("[]"),
|
|
764
|
+
]);
|
|
694
765
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
766
|
+
const accountPayload = buildAccountPayload(
|
|
767
|
+
parseArchiveArray(accountContent)[0] ?? null,
|
|
768
|
+
parseArchiveArray(profileContent)[0] ?? null,
|
|
769
|
+
);
|
|
699
770
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
771
|
+
const mentionDirectory = new Map<
|
|
772
|
+
string,
|
|
773
|
+
{ handle?: string; displayName?: string }
|
|
774
|
+
>();
|
|
775
|
+
const tweetRows: Array<{
|
|
776
|
+
id: string;
|
|
777
|
+
kind: "home" | "like" | "bookmark";
|
|
778
|
+
authorProfileId: string;
|
|
779
|
+
text: string;
|
|
780
|
+
createdAt: string;
|
|
781
|
+
isReplied: number;
|
|
782
|
+
replyToId: string | null;
|
|
783
|
+
likeCount: number;
|
|
784
|
+
mediaCount: number;
|
|
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);
|
|
736
810
|
}
|
|
737
|
-
tweetRows.push(row);
|
|
738
|
-
tweetRowsById.set(row.id, row);
|
|
739
|
-
}
|
|
740
811
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
812
|
+
if (tweetEntries.length > 0) {
|
|
813
|
+
onProgress({
|
|
814
|
+
kind: "slice-start",
|
|
815
|
+
slice: "tweets",
|
|
816
|
+
files: tweetEntries.length,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
for (const [tweetFileIndex, entry] of tweetEntries.entries()) {
|
|
820
|
+
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
821
|
+
for (const wrapper of parseArchiveArray(content)) {
|
|
822
|
+
const tweet = asRecord(wrapper.tweet);
|
|
823
|
+
if (!tweet) continue;
|
|
824
|
+
|
|
825
|
+
for (const mention of asArray<Record<string, unknown>>(
|
|
826
|
+
asRecord(tweet.entities)?.user_mentions,
|
|
827
|
+
)) {
|
|
828
|
+
const mentionId = String(mention.id_str ?? mention.id ?? "");
|
|
829
|
+
if (!mentionId) continue;
|
|
830
|
+
mentionDirectory.set(mentionId, {
|
|
831
|
+
handle: String(mention.screen_name ?? ""),
|
|
832
|
+
displayName: String(
|
|
833
|
+
mention.name ?? mention.screen_name ?? mentionId,
|
|
834
|
+
),
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const replyUserId = String(
|
|
839
|
+
tweet.in_reply_to_user_id_str ?? tweet.in_reply_to_user_id ?? "",
|
|
840
|
+
);
|
|
841
|
+
const replyScreenName = String(tweet.in_reply_to_screen_name ?? "");
|
|
842
|
+
if (replyUserId && replyScreenName) {
|
|
843
|
+
mentionDirectory.set(replyUserId, {
|
|
844
|
+
handle: replyScreenName,
|
|
845
|
+
displayName: replyScreenName,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
addTweetRow({
|
|
850
|
+
id: String(tweet.id_str ?? tweet.id),
|
|
851
|
+
kind: "home",
|
|
852
|
+
authorProfileId: "profile_me",
|
|
853
|
+
text: String(tweet.full_text ?? tweet.text ?? ""),
|
|
854
|
+
createdAt: parseTwitterDate(tweet.created_at),
|
|
855
|
+
isReplied: tweet.in_reply_to_status_id_str ? 1 : 0,
|
|
856
|
+
replyToId: tweet.in_reply_to_status_id_str
|
|
857
|
+
? String(tweet.in_reply_to_status_id_str)
|
|
858
|
+
: null,
|
|
859
|
+
likeCount: toInt(tweet.favorite_count),
|
|
860
|
+
mediaCount: getTweetMediaCount(tweet),
|
|
861
|
+
bookmarked: 0,
|
|
862
|
+
liked: 0,
|
|
863
|
+
entitiesJson: JSON.stringify(extractTweetEntities(tweet)),
|
|
864
|
+
mediaJson: JSON.stringify(extractTweetMedia(tweet)),
|
|
865
|
+
quotedTweetId: tweet.quoted_status_id_str
|
|
866
|
+
? String(tweet.quoted_status_id_str)
|
|
867
|
+
: null,
|
|
755
868
|
});
|
|
756
869
|
}
|
|
870
|
+
onProgress({
|
|
871
|
+
kind: "slice-file",
|
|
872
|
+
slice: "tweets",
|
|
873
|
+
processed: tweetFileIndex + 1,
|
|
874
|
+
files: tweetEntries.length,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (tweetEntries.length > 0) {
|
|
878
|
+
onProgress({
|
|
879
|
+
kind: "slice-done",
|
|
880
|
+
slice: "tweets",
|
|
881
|
+
count: tweetRows.length,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
757
884
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
885
|
+
if (noteTweetEntries.length > 0) {
|
|
886
|
+
onProgress({
|
|
887
|
+
kind: "slice-start",
|
|
888
|
+
slice: "noteTweets",
|
|
889
|
+
files: noteTweetEntries.length,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const tweetRowsBeforeNotes = tweetRows.length;
|
|
893
|
+
for (const [noteFileIndex, entry] of noteTweetEntries.entries()) {
|
|
894
|
+
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
895
|
+
for (const wrapper of parseArchiveArray(content)) {
|
|
896
|
+
const noteTweet = asRecord(wrapper.noteTweet);
|
|
897
|
+
if (!noteTweet) continue;
|
|
898
|
+
const core = asRecord(noteTweet.core);
|
|
899
|
+
addTweetRow({
|
|
900
|
+
id: String(noteTweet.noteTweetId ?? noteTweet.id ?? randomUUID()),
|
|
901
|
+
kind: "home",
|
|
902
|
+
authorProfileId: "profile_me",
|
|
903
|
+
text: String(core?.text ?? ""),
|
|
904
|
+
createdAt: parseTwitterDate(noteTweet.createdAt),
|
|
905
|
+
isReplied: 0,
|
|
906
|
+
replyToId: null,
|
|
907
|
+
likeCount: 0,
|
|
908
|
+
mediaCount: 0,
|
|
909
|
+
bookmarked: 0,
|
|
910
|
+
liked: 0,
|
|
911
|
+
entitiesJson: "{}",
|
|
912
|
+
mediaJson: "[]",
|
|
913
|
+
quotedTweetId: null,
|
|
766
914
|
});
|
|
767
915
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
text: String(tweet.full_text ?? tweet.text ?? ""),
|
|
774
|
-
createdAt: parseTwitterDate(tweet.created_at),
|
|
775
|
-
isReplied: tweet.in_reply_to_status_id_str ? 1 : 0,
|
|
776
|
-
replyToId: tweet.in_reply_to_status_id_str
|
|
777
|
-
? String(tweet.in_reply_to_status_id_str)
|
|
778
|
-
: null,
|
|
779
|
-
likeCount: toInt(tweet.favorite_count),
|
|
780
|
-
mediaCount: getTweetMediaCount(tweet),
|
|
781
|
-
bookmarked: 0,
|
|
782
|
-
liked: 0,
|
|
783
|
-
entitiesJson: JSON.stringify(extractTweetEntities(tweet)),
|
|
784
|
-
mediaJson: JSON.stringify(extractTweetMedia(tweet)),
|
|
785
|
-
quotedTweetId: tweet.quoted_status_id_str
|
|
786
|
-
? String(tweet.quoted_status_id_str)
|
|
787
|
-
: null,
|
|
916
|
+
onProgress({
|
|
917
|
+
kind: "slice-file",
|
|
918
|
+
slice: "noteTweets",
|
|
919
|
+
processed: noteFileIndex + 1,
|
|
920
|
+
files: noteTweetEntries.length,
|
|
788
921
|
});
|
|
789
922
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const noteTweet = asRecord(wrapper.noteTweet);
|
|
796
|
-
if (!noteTweet) continue;
|
|
797
|
-
const core = asRecord(noteTweet.core);
|
|
798
|
-
addTweetRow({
|
|
799
|
-
id: String(noteTweet.noteTweetId ?? noteTweet.id ?? randomUUID()),
|
|
800
|
-
kind: "home",
|
|
801
|
-
authorProfileId: "profile_me",
|
|
802
|
-
text: String(core?.text ?? ""),
|
|
803
|
-
createdAt: parseTwitterDate(noteTweet.createdAt),
|
|
804
|
-
isReplied: 0,
|
|
805
|
-
replyToId: null,
|
|
806
|
-
likeCount: 0,
|
|
807
|
-
mediaCount: 0,
|
|
808
|
-
bookmarked: 0,
|
|
809
|
-
liked: 0,
|
|
810
|
-
entitiesJson: "{}",
|
|
811
|
-
mediaJson: "[]",
|
|
812
|
-
quotedTweetId: null,
|
|
923
|
+
if (noteTweetEntries.length > 0) {
|
|
924
|
+
onProgress({
|
|
925
|
+
kind: "slice-done",
|
|
926
|
+
slice: "noteTweets",
|
|
927
|
+
count: tweetRows.length - tweetRowsBeforeNotes,
|
|
813
928
|
});
|
|
814
929
|
}
|
|
815
|
-
|
|
816
|
-
const authoredTweetCount = tweetRows.length;
|
|
930
|
+
const authoredTweetCount = tweetRows.length;
|
|
817
931
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
932
|
+
type MessageRow = {
|
|
933
|
+
id: string;
|
|
934
|
+
conversationId: string;
|
|
935
|
+
senderProfileId: string;
|
|
936
|
+
text: string;
|
|
937
|
+
createdAt: string;
|
|
938
|
+
direction: "inbound" | "outbound";
|
|
939
|
+
mediaCount: number;
|
|
940
|
+
};
|
|
827
941
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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;
|
|
964
|
+
const defaultProfileMetadata = {
|
|
965
|
+
publicMetricsJson: "{}",
|
|
966
|
+
location: null,
|
|
967
|
+
url: null,
|
|
968
|
+
verifiedType: null,
|
|
969
|
+
entitiesJson: "{}",
|
|
970
|
+
rawJson: "{}",
|
|
971
|
+
};
|
|
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
|
+
type ExistingProfileRow = {
|
|
831
992
|
id: string;
|
|
832
993
|
handle: string;
|
|
833
|
-
|
|
994
|
+
display_name: string;
|
|
834
995
|
bio: string;
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
996
|
+
followers_count: number;
|
|
997
|
+
following_count: number;
|
|
998
|
+
public_metrics_json: string;
|
|
999
|
+
avatar_hue: number;
|
|
1000
|
+
avatar_url: string | null;
|
|
840
1001
|
location: string | null;
|
|
841
1002
|
url: string | null;
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
location: null,
|
|
853
|
-
url: null,
|
|
854
|
-
verifiedType: null,
|
|
855
|
-
entitiesJson: "{}",
|
|
856
|
-
rawJson: "{}",
|
|
857
|
-
};
|
|
858
|
-
const conversations = new Map<
|
|
859
|
-
string,
|
|
860
|
-
{
|
|
861
|
-
id: string;
|
|
862
|
-
title: string;
|
|
863
|
-
accountId: string;
|
|
864
|
-
participantProfileId: string;
|
|
865
|
-
lastMessageAt: string;
|
|
866
|
-
unreadCount: number;
|
|
867
|
-
needsReply: number;
|
|
868
|
-
}
|
|
869
|
-
>();
|
|
870
|
-
const dmMessages: MessageRow[] = [];
|
|
871
|
-
const followerRows: Array<{ profileId: string; externalUserId: string }> = [];
|
|
872
|
-
const followingRows: Array<{ profileId: string; externalUserId: string }> =
|
|
873
|
-
[];
|
|
874
|
-
const followerIds = new Set<string>();
|
|
875
|
-
const followingIds = new Set<string>();
|
|
876
|
-
type ExistingProfileRow = {
|
|
877
|
-
id: string;
|
|
878
|
-
handle: string;
|
|
879
|
-
display_name: string;
|
|
880
|
-
bio: string;
|
|
881
|
-
followers_count: number;
|
|
882
|
-
following_count: number;
|
|
883
|
-
public_metrics_json: string;
|
|
884
|
-
avatar_hue: number;
|
|
885
|
-
avatar_url: string | null;
|
|
886
|
-
location: string | null;
|
|
887
|
-
url: string | null;
|
|
888
|
-
verified_type: string | null;
|
|
889
|
-
entities_json: string;
|
|
890
|
-
raw_json: string;
|
|
891
|
-
created_at: string;
|
|
892
|
-
};
|
|
893
|
-
const existingProfiles = new Map(
|
|
894
|
-
(
|
|
895
|
-
getNativeDb()
|
|
896
|
-
.prepare(
|
|
897
|
-
`
|
|
1003
|
+
verified_type: string | null;
|
|
1004
|
+
entities_json: string;
|
|
1005
|
+
raw_json: string;
|
|
1006
|
+
created_at: string;
|
|
1007
|
+
};
|
|
1008
|
+
const existingProfiles = new Map(
|
|
1009
|
+
(
|
|
1010
|
+
getNativeDb()
|
|
1011
|
+
.prepare(
|
|
1012
|
+
`
|
|
898
1013
|
select id, handle, display_name, bio, followers_count, following_count,
|
|
899
1014
|
public_metrics_json, avatar_hue, avatar_url, location, url,
|
|
900
1015
|
verified_type, entities_json, raw_json, created_at
|
|
901
1016
|
from profiles
|
|
902
1017
|
`,
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
);
|
|
907
|
-
const existingProfilesByHandle = new Map(
|
|
908
|
-
[...existingProfiles.values()].map((profile) => [
|
|
909
|
-
profile.handle.toLowerCase(),
|
|
910
|
-
profile,
|
|
911
|
-
]),
|
|
912
|
-
);
|
|
913
|
-
const existingPrimaryAccount = getNativeDb()
|
|
914
|
-
.prepare("select handle, external_user_id from accounts where id = ?")
|
|
915
|
-
.get("acct_primary") as
|
|
916
|
-
| { handle: string; external_user_id: string | null }
|
|
917
|
-
| undefined;
|
|
918
|
-
const profileIdAliases = new Map<string, string>();
|
|
919
|
-
|
|
920
|
-
type ArchiveProfileTier =
|
|
921
|
-
| "archive_follow_stub"
|
|
922
|
-
| "archive_dm_stub"
|
|
923
|
-
| "archive_mention_inferred"
|
|
924
|
-
| "live_or_hydrated";
|
|
925
|
-
const archiveProfileTierRank: Record<ArchiveProfileTier, number> = {
|
|
926
|
-
archive_follow_stub: 0,
|
|
927
|
-
archive_dm_stub: 1,
|
|
928
|
-
archive_mention_inferred: 2,
|
|
929
|
-
live_or_hydrated: 3,
|
|
930
|
-
};
|
|
931
|
-
|
|
932
|
-
function classifyExistingProfile(profile: ProfileRow): ArchiveProfileTier {
|
|
933
|
-
const externalUserId = profile.id.startsWith("profile_user_")
|
|
934
|
-
? profile.id.slice("profile_user_".length)
|
|
935
|
-
: "";
|
|
936
|
-
const fallbackHandle = externalUserId ? `id${externalUserId}` : profile.id;
|
|
937
|
-
const hasLiveSignals =
|
|
938
|
-
profile.followersCount > 0 ||
|
|
939
|
-
profile.followingCount > 0 ||
|
|
940
|
-
profile.publicMetricsJson.trim() !== "{}" ||
|
|
941
|
-
profile.avatarUrl !== null ||
|
|
942
|
-
profile.location !== null ||
|
|
943
|
-
profile.url !== null ||
|
|
944
|
-
profile.verifiedType !== null ||
|
|
945
|
-
profile.entitiesJson.trim() !== "{}" ||
|
|
946
|
-
profile.rawJson.trim() !== "{}";
|
|
947
|
-
|
|
948
|
-
if (hasLiveSignals) return "live_or_hydrated";
|
|
949
|
-
if (
|
|
950
|
-
profile.handle === fallbackHandle &&
|
|
951
|
-
profile.displayName === "" &&
|
|
952
|
-
profile.bio === ""
|
|
953
|
-
) {
|
|
954
|
-
return "archive_follow_stub";
|
|
955
|
-
}
|
|
956
|
-
if (profile.bio.startsWith("Imported from archive user ")) {
|
|
957
|
-
return profile.handle === fallbackHandle &&
|
|
958
|
-
profile.displayName === fallbackHandle
|
|
959
|
-
? "archive_dm_stub"
|
|
960
|
-
: "archive_mention_inferred";
|
|
961
|
-
}
|
|
962
|
-
return profile.handle === fallbackHandle && profile.displayName === ""
|
|
963
|
-
? "archive_follow_stub"
|
|
964
|
-
: "archive_mention_inferred";
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
function shouldPreserveProfile(
|
|
968
|
-
existingTier: ArchiveProfileTier,
|
|
969
|
-
incomingTier: ArchiveProfileTier,
|
|
970
|
-
) {
|
|
971
|
-
return (
|
|
972
|
-
archiveProfileTierRank[existingTier] >=
|
|
973
|
-
archiveProfileTierRank[incomingTier]
|
|
1018
|
+
)
|
|
1019
|
+
.all() as ExistingProfileRow[]
|
|
1020
|
+
).map((profile) => [profile.id, profile]),
|
|
974
1021
|
);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1022
|
+
const existingProfilesByHandle = new Map(
|
|
1023
|
+
[...existingProfiles.values()].map((profile) => [
|
|
1024
|
+
profile.handle.toLowerCase(),
|
|
1025
|
+
profile,
|
|
1026
|
+
]),
|
|
1027
|
+
);
|
|
1028
|
+
const existingPrimaryAccount = getNativeDb()
|
|
1029
|
+
.prepare("select handle, external_user_id from accounts where id = ?")
|
|
1030
|
+
.get("acct_primary") as
|
|
1031
|
+
| { handle: string; external_user_id: string | null }
|
|
1032
|
+
| undefined;
|
|
1033
|
+
const profileIdAliases = new Map<string, string>();
|
|
1034
|
+
|
|
1035
|
+
type ArchiveProfileTier =
|
|
1036
|
+
| "archive_follow_stub"
|
|
1037
|
+
| "archive_dm_stub"
|
|
1038
|
+
| "archive_mention_inferred"
|
|
1039
|
+
| "live_or_hydrated";
|
|
1040
|
+
const archiveProfileTierRank: Record<ArchiveProfileTier, number> = {
|
|
1041
|
+
archive_follow_stub: 0,
|
|
1042
|
+
archive_dm_stub: 1,
|
|
1043
|
+
archive_mention_inferred: 2,
|
|
1044
|
+
live_or_hydrated: 3,
|
|
996
1045
|
};
|
|
997
|
-
}
|
|
998
1046
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1047
|
+
function classifyExistingProfile(profile: ProfileRow): ArchiveProfileTier {
|
|
1048
|
+
const externalUserId = profile.id.startsWith("profile_user_")
|
|
1049
|
+
? profile.id.slice("profile_user_".length)
|
|
1050
|
+
: "";
|
|
1051
|
+
const fallbackHandle = externalUserId
|
|
1052
|
+
? `id${externalUserId}`
|
|
1053
|
+
: profile.id;
|
|
1054
|
+
const hasLiveSignals =
|
|
1055
|
+
profile.followersCount > 0 ||
|
|
1056
|
+
profile.followingCount > 0 ||
|
|
1057
|
+
profile.publicMetricsJson.trim() !== "{}" ||
|
|
1058
|
+
profile.avatarUrl !== null ||
|
|
1059
|
+
profile.location !== null ||
|
|
1060
|
+
profile.url !== null ||
|
|
1061
|
+
profile.verifiedType !== null ||
|
|
1062
|
+
profile.entitiesJson.trim() !== "{}" ||
|
|
1063
|
+
profile.rawJson.trim() !== "{}";
|
|
1064
|
+
|
|
1065
|
+
if (hasLiveSignals) return "live_or_hydrated";
|
|
1066
|
+
if (
|
|
1067
|
+
profile.handle === fallbackHandle &&
|
|
1068
|
+
profile.displayName === "" &&
|
|
1069
|
+
profile.bio === ""
|
|
1070
|
+
) {
|
|
1071
|
+
return "archive_follow_stub";
|
|
1072
|
+
}
|
|
1073
|
+
if (profile.bio.startsWith("Imported from archive user ")) {
|
|
1074
|
+
return profile.handle === fallbackHandle &&
|
|
1075
|
+
profile.displayName === fallbackHandle
|
|
1076
|
+
? "archive_dm_stub"
|
|
1077
|
+
: "archive_mention_inferred";
|
|
1078
|
+
}
|
|
1079
|
+
return profile.handle === fallbackHandle && profile.displayName === ""
|
|
1080
|
+
? "archive_follow_stub"
|
|
1081
|
+
: "archive_mention_inferred";
|
|
1008
1082
|
}
|
|
1009
|
-
const targetIncoming =
|
|
1010
|
-
targetId === incoming.id ? incoming : { ...incoming, id: targetId };
|
|
1011
|
-
const incomingTier = classifyExistingProfile(incoming);
|
|
1012
|
-
const current = profiles.get(targetId);
|
|
1013
|
-
const currentTier = current ? classifyExistingProfile(current) : null;
|
|
1014
|
-
const existingProfile = targetExisting
|
|
1015
|
-
? existingProfileToProfileRow(targetExisting)
|
|
1016
|
-
: null;
|
|
1017
|
-
const existingTier = existingProfile
|
|
1018
|
-
? classifyExistingProfile(existingProfile)
|
|
1019
|
-
: null;
|
|
1020
1083
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
shouldPreserveProfile(currentTier, incomingTier) &&
|
|
1025
|
-
(!existingTier || shouldPreserveProfile(currentTier, existingTier))
|
|
1084
|
+
function shouldPreserveProfile(
|
|
1085
|
+
existingTier: ArchiveProfileTier,
|
|
1086
|
+
incomingTier: ArchiveProfileTier,
|
|
1026
1087
|
) {
|
|
1027
|
-
return
|
|
1088
|
+
return (
|
|
1089
|
+
archiveProfileTierRank[existingTier] >=
|
|
1090
|
+
archiveProfileTierRank[incomingTier]
|
|
1091
|
+
);
|
|
1028
1092
|
}
|
|
1029
1093
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1094
|
+
function existingProfileToProfileRow(
|
|
1095
|
+
profile: ExistingProfileRow,
|
|
1096
|
+
): ProfileRow {
|
|
1097
|
+
return {
|
|
1098
|
+
id: profile.id,
|
|
1099
|
+
handle: profile.handle,
|
|
1100
|
+
displayName: profile.display_name,
|
|
1101
|
+
bio: profile.bio,
|
|
1102
|
+
followersCount: profile.followers_count,
|
|
1103
|
+
followingCount: profile.following_count,
|
|
1104
|
+
publicMetricsJson: profile.public_metrics_json,
|
|
1105
|
+
avatarHue: profile.avatar_hue,
|
|
1106
|
+
avatarUrl: profile.avatar_url,
|
|
1107
|
+
location: profile.location,
|
|
1108
|
+
url: profile.url,
|
|
1109
|
+
verifiedType: profile.verified_type,
|
|
1110
|
+
entitiesJson: profile.entities_json,
|
|
1111
|
+
rawJson: profile.raw_json,
|
|
1112
|
+
createdAt: profile.created_at,
|
|
1113
|
+
};
|
|
1037
1114
|
}
|
|
1038
1115
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1116
|
+
function mergeArchiveProfile(incoming: ProfileRow) {
|
|
1117
|
+
const existingById = existingProfiles.get(incoming.id);
|
|
1118
|
+
const existingByHandle = selection
|
|
1119
|
+
? existingProfilesByHandle.get(incoming.handle.toLowerCase())
|
|
1120
|
+
: undefined;
|
|
1121
|
+
const targetExisting = existingById ?? existingByHandle;
|
|
1122
|
+
const targetId = targetExisting?.id ?? incoming.id;
|
|
1123
|
+
if (targetId !== incoming.id) {
|
|
1124
|
+
profileIdAliases.set(incoming.id, targetId);
|
|
1125
|
+
}
|
|
1126
|
+
const targetIncoming =
|
|
1127
|
+
targetId === incoming.id ? incoming : { ...incoming, id: targetId };
|
|
1128
|
+
const incomingTier = classifyExistingProfile(incoming);
|
|
1129
|
+
const current = profiles.get(targetId);
|
|
1130
|
+
const currentTier = current ? classifyExistingProfile(current) : null;
|
|
1131
|
+
const existingProfile = targetExisting
|
|
1132
|
+
? existingProfileToProfileRow(targetExisting)
|
|
1133
|
+
: null;
|
|
1134
|
+
const existingTier = existingProfile
|
|
1135
|
+
? classifyExistingProfile(existingProfile)
|
|
1136
|
+
: null;
|
|
1041
1137
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1138
|
+
if (
|
|
1139
|
+
current &&
|
|
1140
|
+
currentTier &&
|
|
1141
|
+
shouldPreserveProfile(currentTier, incomingTier) &&
|
|
1142
|
+
(!existingTier || shouldPreserveProfile(currentTier, existingTier))
|
|
1143
|
+
) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1045
1146
|
|
|
1046
|
-
function isProfileHandleTakenByOtherId(handle: string, profileId: string) {
|
|
1047
|
-
const normalizedHandle = handle.toLowerCase();
|
|
1048
|
-
const existingProfile = existingProfilesByHandle.get(normalizedHandle);
|
|
1049
|
-
if (existingProfile && existingProfile.id !== profileId) return true;
|
|
1050
|
-
for (const profile of profiles.values()) {
|
|
1051
1147
|
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1148
|
+
existingProfile &&
|
|
1149
|
+
existingTier &&
|
|
1150
|
+
shouldPreserveProfile(existingTier, incomingTier)
|
|
1054
1151
|
) {
|
|
1055
|
-
|
|
1152
|
+
profiles.set(targetId, existingProfile);
|
|
1153
|
+
return;
|
|
1056
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
profiles.set(targetId, targetIncoming);
|
|
1057
1157
|
}
|
|
1058
|
-
return false;
|
|
1059
|
-
}
|
|
1060
1158
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
return baseHandle;
|
|
1159
|
+
function resolveProfileId(profileId: string) {
|
|
1160
|
+
return profileIdAliases.get(profileId) ?? profileId;
|
|
1064
1161
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1162
|
+
|
|
1163
|
+
function isProfileHandleTakenByOtherId(handle: string, profileId: string) {
|
|
1164
|
+
const normalizedHandle = handle.toLowerCase();
|
|
1165
|
+
const existingProfile = existingProfilesByHandle.get(normalizedHandle);
|
|
1166
|
+
if (existingProfile && existingProfile.id !== profileId) return true;
|
|
1167
|
+
for (const profile of profiles.values()) {
|
|
1168
|
+
if (
|
|
1169
|
+
profile.id !== profileId &&
|
|
1170
|
+
profile.handle.toLowerCase() === normalizedHandle
|
|
1171
|
+
) {
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1071
1174
|
}
|
|
1072
|
-
|
|
1175
|
+
return false;
|
|
1073
1176
|
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function addArchiveFollowProfile(profileId: string, externalUserId: string) {
|
|
1077
|
-
if (!profileId) return;
|
|
1078
|
-
const fallbackId =
|
|
1079
|
-
externalUserId || profileId.replace(/^profile_user_/, "");
|
|
1080
|
-
mergeArchiveProfile({
|
|
1081
|
-
id: profileId,
|
|
1082
|
-
handle: fallbackId ? `id${fallbackId}` : profileId,
|
|
1083
|
-
displayName: "",
|
|
1084
|
-
bio: "",
|
|
1085
|
-
followersCount: 0,
|
|
1086
|
-
followingCount: 0,
|
|
1087
|
-
...defaultProfileMetadata,
|
|
1088
|
-
avatarHue: 210,
|
|
1089
|
-
avatarUrl: null,
|
|
1090
|
-
createdAt: accountPayload.createdAt,
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
1177
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
.toLowerCase();
|
|
1108
|
-
if (
|
|
1109
|
-
!existingExternalUserId &&
|
|
1110
|
-
existingHandle !== accountPayload.username.toLowerCase()
|
|
1111
|
-
) {
|
|
1112
|
-
throw new Error(
|
|
1113
|
-
`Existing acct_primary (@${existingHandle}) does not match archive account @${accountPayload.username}`,
|
|
1114
|
-
);
|
|
1178
|
+
function uniqueArchiveProfileHandle(baseHandle: string, profileId: string) {
|
|
1179
|
+
if (!isProfileHandleTakenByOtherId(baseHandle, profileId)) {
|
|
1180
|
+
return baseHandle;
|
|
1181
|
+
}
|
|
1182
|
+
let index = 1;
|
|
1183
|
+
while (true) {
|
|
1184
|
+
const suffix = index === 1 ? "archive" : `archive_${index}`;
|
|
1185
|
+
const candidate = `${baseHandle}_${suffix}`;
|
|
1186
|
+
if (!isProfileHandleTakenByOtherId(candidate, profileId)) {
|
|
1187
|
+
return candidate;
|
|
1188
|
+
}
|
|
1189
|
+
index += 1;
|
|
1190
|
+
}
|
|
1115
1191
|
}
|
|
1116
|
-
}
|
|
1117
1192
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
(
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
...existingProfileToProfileRow(existingLocalProfile),
|
|
1131
|
-
handle: accountPayload.username,
|
|
1132
|
-
displayName: accountPayload.displayName,
|
|
1133
|
-
bio: accountPayload.bio,
|
|
1134
|
-
createdAt: accountPayload.createdAt,
|
|
1135
|
-
}
|
|
1136
|
-
: {
|
|
1137
|
-
id: "profile_me",
|
|
1138
|
-
handle: accountPayload.username,
|
|
1139
|
-
displayName: accountPayload.displayName,
|
|
1140
|
-
bio: accountPayload.bio,
|
|
1193
|
+
function addArchiveFollowProfile(
|
|
1194
|
+
profileId: string,
|
|
1195
|
+
externalUserId: string,
|
|
1196
|
+
) {
|
|
1197
|
+
if (!profileId) return;
|
|
1198
|
+
const fallbackId =
|
|
1199
|
+
externalUserId || profileId.replace(/^profile_user_/, "");
|
|
1200
|
+
mergeArchiveProfile({
|
|
1201
|
+
id: profileId,
|
|
1202
|
+
handle: fallbackId ? `id${fallbackId}` : profileId,
|
|
1203
|
+
displayName: "",
|
|
1204
|
+
bio: "",
|
|
1141
1205
|
followersCount: 0,
|
|
1142
1206
|
followingCount: 0,
|
|
1143
1207
|
...defaultProfileMetadata,
|
|
1144
|
-
avatarHue:
|
|
1208
|
+
avatarHue: 210,
|
|
1145
1209
|
avatarUrl: null,
|
|
1146
1210
|
createdAt: accountPayload.createdAt,
|
|
1147
|
-
};
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function assertSelectedAccountMatchesArchive() {
|
|
1215
|
+
if (!selection || !existingPrimaryAccount) return;
|
|
1216
|
+
const existingExternalUserId = existingPrimaryAccount.external_user_id;
|
|
1217
|
+
if (
|
|
1218
|
+
existingExternalUserId &&
|
|
1219
|
+
existingExternalUserId !== accountPayload.accountId
|
|
1220
|
+
) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`Existing acct_primary (${existingExternalUserId}) does not match archive account ${accountPayload.accountId}`,
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
const existingHandle = existingPrimaryAccount.handle
|
|
1226
|
+
.replace(/^@/, "")
|
|
1227
|
+
.toLowerCase();
|
|
1228
|
+
if (
|
|
1229
|
+
!existingExternalUserId &&
|
|
1230
|
+
existingHandle !== accountPayload.username.toLowerCase()
|
|
1231
|
+
) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`Existing acct_primary (@${existingHandle}) does not match archive account @${accountPayload.username}`,
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
assertSelectedAccountMatchesArchive();
|
|
1239
|
+
|
|
1240
|
+
const existingLocalProfile =
|
|
1241
|
+
selection &&
|
|
1242
|
+
(existingProfiles.get("profile_me") ??
|
|
1243
|
+
[...existingProfiles.values()].find(
|
|
1244
|
+
(profile) =>
|
|
1245
|
+
profile.handle.toLowerCase() ===
|
|
1246
|
+
accountPayload.username.toLowerCase(),
|
|
1247
|
+
));
|
|
1248
|
+
const archivedLocalProfile = existingLocalProfile
|
|
1249
|
+
? {
|
|
1250
|
+
...existingProfileToProfileRow(existingLocalProfile),
|
|
1251
|
+
handle: accountPayload.username,
|
|
1252
|
+
displayName: accountPayload.displayName,
|
|
1253
|
+
bio: accountPayload.bio,
|
|
1254
|
+
createdAt: accountPayload.createdAt,
|
|
1255
|
+
}
|
|
1256
|
+
: {
|
|
1257
|
+
id: "profile_me",
|
|
1258
|
+
handle: accountPayload.username,
|
|
1259
|
+
displayName: accountPayload.displayName,
|
|
1260
|
+
bio: accountPayload.bio,
|
|
1261
|
+
followersCount: 0,
|
|
1262
|
+
followingCount: 0,
|
|
1263
|
+
...defaultProfileMetadata,
|
|
1264
|
+
avatarHue: 18,
|
|
1265
|
+
avatarUrl: null,
|
|
1266
|
+
createdAt: accountPayload.createdAt,
|
|
1267
|
+
};
|
|
1268
|
+
const localProfile =
|
|
1269
|
+
existingLocalProfile && !includeProfiles
|
|
1270
|
+
? existingProfileToProfileRow(existingLocalProfile)
|
|
1271
|
+
: archivedLocalProfile;
|
|
1272
|
+
profiles.set(localProfile.id, localProfile);
|
|
1273
|
+
|
|
1274
|
+
const existingDmConversationAccounts = new Map(
|
|
1275
|
+
(
|
|
1276
|
+
getNativeDb()
|
|
1277
|
+
.prepare("select id, account_id from dm_conversations")
|
|
1278
|
+
.all() as Array<{ id: string; account_id: string }>
|
|
1279
|
+
).map((row) => [row.id, row.account_id]),
|
|
1280
|
+
);
|
|
1281
|
+
const existingOtherDmMessageIds = new Set(
|
|
1282
|
+
(
|
|
1283
|
+
getNativeDb()
|
|
1284
|
+
.prepare(
|
|
1285
|
+
`
|
|
1166
1286
|
select m.id
|
|
1167
1287
|
from dm_messages m
|
|
1168
1288
|
join dm_conversations c on c.id = m.conversation_id
|
|
1169
1289
|
where c.account_id <> 'acct_primary'
|
|
1170
1290
|
`,
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1291
|
+
)
|
|
1292
|
+
.all() as Array<{ id: string }>
|
|
1293
|
+
).map((row) => row.id),
|
|
1294
|
+
);
|
|
1295
|
+
const archiveDmConversationIdAliases = new Map<string, string>();
|
|
1296
|
+
const archiveDmMessageIdAliases = new Map<string, string>();
|
|
1297
|
+
|
|
1298
|
+
function uniquePrimaryArchiveId(
|
|
1299
|
+
baseId: string,
|
|
1300
|
+
isTakenByOtherAccount: (candidate: string) => boolean,
|
|
1301
|
+
isPending: (candidate: string) => boolean,
|
|
1302
|
+
) {
|
|
1303
|
+
let index = 1;
|
|
1304
|
+
while (true) {
|
|
1305
|
+
const suffix = index === 1 ? "" : `:${index}`;
|
|
1306
|
+
const candidate = `acct_primary:${baseId}${suffix}`;
|
|
1307
|
+
if (!isTakenByOtherAccount(candidate) && !isPending(candidate)) {
|
|
1308
|
+
return candidate;
|
|
1309
|
+
}
|
|
1310
|
+
index += 1;
|
|
1189
1311
|
}
|
|
1190
|
-
index += 1;
|
|
1191
1312
|
}
|
|
1192
|
-
}
|
|
1193
1313
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1314
|
+
function resolveArchiveDmConversationId(conversationId: string) {
|
|
1315
|
+
const existingAlias = archiveDmConversationIdAliases.get(conversationId);
|
|
1316
|
+
if (existingAlias) return existingAlias;
|
|
1317
|
+
if (!selection) {
|
|
1318
|
+
archiveDmConversationIdAliases.set(conversationId, conversationId);
|
|
1319
|
+
return conversationId;
|
|
1320
|
+
}
|
|
1201
1321
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1322
|
+
const takenByOtherAccount = (candidate: string) => {
|
|
1323
|
+
const accountId = existingDmConversationAccounts.get(candidate);
|
|
1324
|
+
return accountId !== undefined && accountId !== "acct_primary";
|
|
1325
|
+
};
|
|
1326
|
+
const resolved = takenByOtherAccount(conversationId)
|
|
1327
|
+
? uniquePrimaryArchiveId(
|
|
1328
|
+
conversationId,
|
|
1329
|
+
takenByOtherAccount,
|
|
1330
|
+
(candidate) => conversations.has(candidate),
|
|
1331
|
+
)
|
|
1332
|
+
: conversationId;
|
|
1333
|
+
archiveDmConversationIdAliases.set(conversationId, resolved);
|
|
1334
|
+
return resolved;
|
|
1335
|
+
}
|
|
1216
1336
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1337
|
+
function resolveArchiveDmMessageId(
|
|
1338
|
+
messageId: string,
|
|
1339
|
+
conversationIdChanged: boolean,
|
|
1340
|
+
) {
|
|
1341
|
+
const existingAlias = archiveDmMessageIdAliases.get(messageId);
|
|
1342
|
+
if (existingAlias) return existingAlias;
|
|
1343
|
+
const shouldRemap =
|
|
1344
|
+
selection &&
|
|
1345
|
+
(conversationIdChanged || existingOtherDmMessageIds.has(messageId));
|
|
1346
|
+
const resolved = shouldRemap
|
|
1347
|
+
? uniquePrimaryArchiveId(
|
|
1348
|
+
messageId,
|
|
1349
|
+
(candidate) => existingOtherDmMessageIds.has(candidate),
|
|
1350
|
+
(candidate) =>
|
|
1351
|
+
dmMessages.some((message) => message.id === candidate),
|
|
1352
|
+
)
|
|
1353
|
+
: messageId;
|
|
1354
|
+
archiveDmMessageIdAliases.set(messageId, resolved);
|
|
1355
|
+
return resolved;
|
|
1356
|
+
}
|
|
1236
1357
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
const
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1358
|
+
if (dmEntries.length > 0) {
|
|
1359
|
+
onProgress({
|
|
1360
|
+
kind: "slice-start",
|
|
1361
|
+
slice: "directMessages",
|
|
1362
|
+
files: dmEntries.length,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
for (const [dmFileIndex, entry] of dmEntries.entries()) {
|
|
1366
|
+
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1367
|
+
for (const wrapper of parseArchiveArray(content)) {
|
|
1368
|
+
const dmConversation = asRecord(wrapper.dmConversation);
|
|
1369
|
+
if (!dmConversation) continue;
|
|
1370
|
+
|
|
1371
|
+
const rawConversationId = String(dmConversation.conversationId ?? "");
|
|
1372
|
+
if (!rawConversationId) continue;
|
|
1373
|
+
const conversationId =
|
|
1374
|
+
resolveArchiveDmConversationId(rawConversationId);
|
|
1375
|
+
const conversationIdChanged = conversationId !== rawConversationId;
|
|
1376
|
+
|
|
1377
|
+
const conversationName = String(dmConversation.name ?? "").trim();
|
|
1378
|
+
const participantIds = new Set<string>();
|
|
1379
|
+
const rawMessages = asArray<Record<string, unknown>>(
|
|
1380
|
+
dmConversation.messages,
|
|
1381
|
+
);
|
|
1253
1382
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1383
|
+
for (const event of rawMessages) {
|
|
1384
|
+
const messageCreate = asRecord(event.messageCreate);
|
|
1385
|
+
if (messageCreate) {
|
|
1386
|
+
const senderId = String(messageCreate.senderId ?? "");
|
|
1387
|
+
const recipientId = String(messageCreate.recipientId ?? "");
|
|
1388
|
+
if (senderId) participantIds.add(senderId);
|
|
1389
|
+
if (recipientId) participantIds.add(recipientId);
|
|
1390
|
+
}
|
|
1262
1391
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1392
|
+
const joinConversation = asRecord(event.joinConversation);
|
|
1393
|
+
if (joinConversation) {
|
|
1394
|
+
for (const userId of asArray<string>(
|
|
1395
|
+
joinConversation.participantsSnapshot,
|
|
1396
|
+
)) {
|
|
1397
|
+
participantIds.add(String(userId));
|
|
1398
|
+
}
|
|
1269
1399
|
}
|
|
1270
|
-
}
|
|
1271
1400
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1401
|
+
const participantsJoin = asRecord(event.participantsJoin);
|
|
1402
|
+
if (participantsJoin) {
|
|
1403
|
+
for (const userId of asArray<string>(participantsJoin.userIds)) {
|
|
1404
|
+
participantIds.add(String(userId));
|
|
1405
|
+
}
|
|
1406
|
+
const initiatingUserId = String(
|
|
1407
|
+
participantsJoin.initiatingUserId ?? "",
|
|
1408
|
+
);
|
|
1409
|
+
if (initiatingUserId) {
|
|
1410
|
+
participantIds.add(initiatingUserId);
|
|
1411
|
+
}
|
|
1276
1412
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
)
|
|
1280
|
-
|
|
1281
|
-
|
|
1413
|
+
|
|
1414
|
+
const participantsLeave = asRecord(event.participantsLeave);
|
|
1415
|
+
if (participantsLeave) {
|
|
1416
|
+
for (const userId of asArray<string>(participantsLeave.userIds)) {
|
|
1417
|
+
participantIds.add(String(userId));
|
|
1418
|
+
}
|
|
1419
|
+
const initiatingUserId = String(
|
|
1420
|
+
participantsLeave.initiatingUserId ?? "",
|
|
1421
|
+
);
|
|
1422
|
+
if (initiatingUserId) {
|
|
1423
|
+
participantIds.add(initiatingUserId);
|
|
1424
|
+
}
|
|
1282
1425
|
}
|
|
1283
1426
|
}
|
|
1284
1427
|
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1428
|
+
const externalParticipantIds = [...participantIds].filter(
|
|
1429
|
+
(userId) => userId && userId !== accountPayload.accountId,
|
|
1430
|
+
);
|
|
1431
|
+
const isGroup =
|
|
1432
|
+
conversationName.length > 0 || externalParticipantIds.length > 1;
|
|
1433
|
+
const participantProfileId = isGroup
|
|
1434
|
+
? `profile_group_${conversationId}`
|
|
1435
|
+
: `profile_user_${externalParticipantIds[0] ?? conversationId}`;
|
|
1436
|
+
|
|
1437
|
+
if (!profiles.has(participantProfileId)) {
|
|
1438
|
+
if (isGroup) {
|
|
1439
|
+
profiles.set(participantProfileId, {
|
|
1440
|
+
id: participantProfileId,
|
|
1441
|
+
handle: `group-${conversationId}`,
|
|
1442
|
+
displayName:
|
|
1443
|
+
conversationName || `Group DM ${externalParticipantIds.length}`,
|
|
1444
|
+
bio: `Group DM with ${externalParticipantIds.length} participants`,
|
|
1445
|
+
followersCount: 0,
|
|
1446
|
+
followingCount: 0,
|
|
1447
|
+
...defaultProfileMetadata,
|
|
1448
|
+
avatarHue: 220,
|
|
1449
|
+
avatarUrl: null,
|
|
1450
|
+
createdAt: accountPayload.createdAt,
|
|
1451
|
+
});
|
|
1452
|
+
} else {
|
|
1453
|
+
const otherUserId = externalParticipantIds[0] ?? conversationId;
|
|
1454
|
+
const inferred = inferProfileFromDirectory(
|
|
1455
|
+
otherUserId,
|
|
1456
|
+
mentionDirectory,
|
|
1457
|
+
);
|
|
1458
|
+
mergeArchiveProfile({
|
|
1459
|
+
id: participantProfileId,
|
|
1460
|
+
handle: inferred.handle,
|
|
1461
|
+
displayName: inferred.displayName,
|
|
1462
|
+
bio: `Imported from archive user ${otherUserId}`,
|
|
1463
|
+
followersCount: 0,
|
|
1464
|
+
followingCount: 0,
|
|
1465
|
+
...defaultProfileMetadata,
|
|
1466
|
+
avatarHue: 210,
|
|
1467
|
+
avatarUrl: null,
|
|
1468
|
+
createdAt: accountPayload.createdAt,
|
|
1469
|
+
});
|
|
1295
1470
|
}
|
|
1296
1471
|
}
|
|
1297
|
-
}
|
|
1298
1472
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
displayName: inferred.displayName,
|
|
1333
|
-
bio: `Imported from archive user ${otherUserId}`,
|
|
1334
|
-
followersCount: 0,
|
|
1335
|
-
followingCount: 0,
|
|
1336
|
-
...defaultProfileMetadata,
|
|
1337
|
-
avatarHue: 210,
|
|
1338
|
-
avatarUrl: null,
|
|
1339
|
-
createdAt: accountPayload.createdAt,
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1473
|
+
const messageEvents = rawMessages
|
|
1474
|
+
.map((event) => asRecord(event.messageCreate))
|
|
1475
|
+
.filter((event): event is Record<string, unknown> => event !== null)
|
|
1476
|
+
.map((messageCreate) => {
|
|
1477
|
+
const senderId = String(messageCreate.senderId ?? "");
|
|
1478
|
+
const rawMessageId = String(
|
|
1479
|
+
messageCreate.id ?? `${rawConversationId}-${senderId}`,
|
|
1480
|
+
);
|
|
1481
|
+
const senderProfileId =
|
|
1482
|
+
senderId === accountPayload.accountId
|
|
1483
|
+
? localProfile.id
|
|
1484
|
+
: `profile_user_${senderId}`;
|
|
1485
|
+
|
|
1486
|
+
if (senderId && senderId !== accountPayload.accountId) {
|
|
1487
|
+
const inferred = inferProfileFromDirectory(
|
|
1488
|
+
senderId,
|
|
1489
|
+
mentionDirectory,
|
|
1490
|
+
);
|
|
1491
|
+
if (!profiles.has(senderProfileId)) {
|
|
1492
|
+
mergeArchiveProfile({
|
|
1493
|
+
id: senderProfileId,
|
|
1494
|
+
handle: inferred.handle,
|
|
1495
|
+
displayName: inferred.displayName,
|
|
1496
|
+
bio: `Imported from archive user ${senderId}`,
|
|
1497
|
+
followersCount: 0,
|
|
1498
|
+
followingCount: 0,
|
|
1499
|
+
...defaultProfileMetadata,
|
|
1500
|
+
avatarHue: 240,
|
|
1501
|
+
avatarUrl: null,
|
|
1502
|
+
createdAt: accountPayload.createdAt,
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1343
1506
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1507
|
+
return {
|
|
1508
|
+
id: resolveArchiveDmMessageId(
|
|
1509
|
+
rawMessageId,
|
|
1510
|
+
conversationIdChanged,
|
|
1511
|
+
),
|
|
1512
|
+
conversationId,
|
|
1513
|
+
senderProfileId: resolveProfileId(senderProfileId),
|
|
1514
|
+
text: String(messageCreate.text ?? ""),
|
|
1515
|
+
createdAt: parseTwitterDate(messageCreate.createdAt),
|
|
1516
|
+
direction:
|
|
1517
|
+
senderId === accountPayload.accountId ? "outbound" : "inbound",
|
|
1518
|
+
mediaCount: asArray(messageCreate.mediaUrls).length,
|
|
1519
|
+
} satisfies MessageRow;
|
|
1520
|
+
})
|
|
1521
|
+
.sort((left, right) =>
|
|
1522
|
+
compareIsoTimestamp(left.createdAt, right.createdAt),
|
|
1351
1523
|
);
|
|
1352
|
-
const senderProfileId =
|
|
1353
|
-
senderId === accountPayload.accountId
|
|
1354
|
-
? localProfile.id
|
|
1355
|
-
: `profile_user_${senderId}`;
|
|
1356
1524
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
mentionDirectory,
|
|
1361
|
-
);
|
|
1362
|
-
if (!profiles.has(senderProfileId)) {
|
|
1363
|
-
mergeArchiveProfile({
|
|
1364
|
-
id: senderProfileId,
|
|
1365
|
-
handle: inferred.handle,
|
|
1366
|
-
displayName: inferred.displayName,
|
|
1367
|
-
bio: `Imported from archive user ${senderId}`,
|
|
1368
|
-
followersCount: 0,
|
|
1369
|
-
followingCount: 0,
|
|
1370
|
-
...defaultProfileMetadata,
|
|
1371
|
-
avatarHue: 240,
|
|
1372
|
-
avatarUrl: null,
|
|
1373
|
-
createdAt: accountPayload.createdAt,
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1525
|
+
if (messageEvents.length === 0) {
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1377
1528
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1529
|
+
const lastMessage = messageEvents.at(-1);
|
|
1530
|
+
if (!lastMessage) continue;
|
|
1531
|
+
|
|
1532
|
+
dmMessages.push(...messageEvents);
|
|
1533
|
+
const resolvedParticipantProfileId =
|
|
1534
|
+
resolveProfileId(participantProfileId);
|
|
1535
|
+
conversations.set(conversationId, {
|
|
1536
|
+
id: conversationId,
|
|
1537
|
+
title:
|
|
1538
|
+
profiles.get(resolvedParticipantProfileId)?.displayName ||
|
|
1539
|
+
conversationName ||
|
|
1380
1540
|
conversationId,
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
} satisfies MessageRow;
|
|
1388
|
-
})
|
|
1389
|
-
.sort((left, right) =>
|
|
1390
|
-
compareIsoTimestamp(left.createdAt, right.createdAt),
|
|
1391
|
-
);
|
|
1392
|
-
|
|
1393
|
-
if (messageEvents.length === 0) {
|
|
1394
|
-
continue;
|
|
1541
|
+
accountId: "acct_primary",
|
|
1542
|
+
participantProfileId: resolvedParticipantProfileId,
|
|
1543
|
+
lastMessageAt: lastMessage.createdAt,
|
|
1544
|
+
unreadCount: 0,
|
|
1545
|
+
needsReply: lastMessage.direction === "inbound" ? 1 : 0,
|
|
1546
|
+
});
|
|
1395
1547
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
conversationId,
|
|
1409
|
-
accountId: "acct_primary",
|
|
1410
|
-
participantProfileId: resolvedParticipantProfileId,
|
|
1411
|
-
lastMessageAt: lastMessage.createdAt,
|
|
1412
|
-
unreadCount: 0,
|
|
1413
|
-
needsReply: lastMessage.direction === "inbound" ? 1 : 0,
|
|
1548
|
+
onProgress({
|
|
1549
|
+
kind: "slice-file",
|
|
1550
|
+
slice: "directMessages",
|
|
1551
|
+
processed: dmFileIndex + 1,
|
|
1552
|
+
files: dmEntries.length,
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
if (dmEntries.length > 0) {
|
|
1556
|
+
onProgress({
|
|
1557
|
+
kind: "slice-done",
|
|
1558
|
+
slice: "directMessages",
|
|
1559
|
+
count: dmMessages.length,
|
|
1414
1560
|
});
|
|
1415
1561
|
}
|
|
1416
|
-
}
|
|
1417
1562
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
const tweet = extractCollectionTweet(like, "like");
|
|
1424
|
-
if (!tweet) continue;
|
|
1425
|
-
collectionRows.push({
|
|
1426
|
-
tweetId: tweet.id,
|
|
1427
|
-
kind: "likes",
|
|
1428
|
-
collectedAt: tweet.createdAt,
|
|
1429
|
-
source: "archive",
|
|
1430
|
-
rawJson: JSON.stringify(like),
|
|
1563
|
+
if (likeEntries.length > 0) {
|
|
1564
|
+
onProgress({
|
|
1565
|
+
kind: "slice-start",
|
|
1566
|
+
slice: "likes",
|
|
1567
|
+
files: likeEntries.length,
|
|
1431
1568
|
});
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1569
|
+
}
|
|
1570
|
+
let likeCount = 0;
|
|
1571
|
+
for (const [likeFileIndex, entry] of likeEntries.entries()) {
|
|
1572
|
+
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1573
|
+
const likes = parseArchiveArray(content);
|
|
1574
|
+
for (const like of likes) {
|
|
1575
|
+
const tweet = extractCollectionTweet(like, "like");
|
|
1576
|
+
if (!tweet) continue;
|
|
1577
|
+
collectionRows.push({
|
|
1578
|
+
tweetId: tweet.id,
|
|
1579
|
+
kind: "likes",
|
|
1580
|
+
collectedAt: tweet.createdAt,
|
|
1581
|
+
source: "archive",
|
|
1582
|
+
rawJson: JSON.stringify(like),
|
|
1583
|
+
});
|
|
1584
|
+
addTweetRow({
|
|
1585
|
+
id: tweet.id,
|
|
1586
|
+
kind: "like",
|
|
1587
|
+
authorProfileId: "profile_unknown",
|
|
1588
|
+
text: tweet.text,
|
|
1589
|
+
createdAt: tweet.createdAt,
|
|
1590
|
+
isReplied: 0,
|
|
1591
|
+
replyToId: null,
|
|
1592
|
+
likeCount: tweet.likeCount,
|
|
1593
|
+
mediaCount: 0,
|
|
1594
|
+
bookmarked: 0,
|
|
1595
|
+
liked: 1,
|
|
1596
|
+
entitiesJson: "{}",
|
|
1597
|
+
mediaJson: "[]",
|
|
1598
|
+
quotedTweetId: null,
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
likeCount += likes.length;
|
|
1602
|
+
onProgress({
|
|
1603
|
+
kind: "slice-file",
|
|
1604
|
+
slice: "likes",
|
|
1605
|
+
processed: likeFileIndex + 1,
|
|
1606
|
+
files: likeEntries.length,
|
|
1447
1607
|
});
|
|
1448
1608
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1609
|
+
if (likeEntries.length > 0) {
|
|
1610
|
+
onProgress({ kind: "slice-done", slice: "likes", count: likeCount });
|
|
1611
|
+
}
|
|
1451
1612
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const tweet = extractCollectionTweet(bookmark, "bookmark");
|
|
1458
|
-
if (!tweet) continue;
|
|
1459
|
-
collectionRows.push({
|
|
1460
|
-
tweetId: tweet.id,
|
|
1461
|
-
kind: "bookmarks",
|
|
1462
|
-
collectedAt: tweet.createdAt,
|
|
1463
|
-
source: "archive",
|
|
1464
|
-
rawJson: JSON.stringify(bookmark),
|
|
1613
|
+
if (bookmarkEntries.length > 0) {
|
|
1614
|
+
onProgress({
|
|
1615
|
+
kind: "slice-start",
|
|
1616
|
+
slice: "bookmarks",
|
|
1617
|
+
files: bookmarkEntries.length,
|
|
1465
1618
|
});
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1619
|
+
}
|
|
1620
|
+
let bookmarkCount = 0;
|
|
1621
|
+
for (const [bookmarkFileIndex, entry] of bookmarkEntries.entries()) {
|
|
1622
|
+
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1623
|
+
const bookmarks = parseArchiveArray(content);
|
|
1624
|
+
for (const bookmark of bookmarks) {
|
|
1625
|
+
const tweet = extractCollectionTweet(bookmark, "bookmark");
|
|
1626
|
+
if (!tweet) continue;
|
|
1627
|
+
collectionRows.push({
|
|
1628
|
+
tweetId: tweet.id,
|
|
1629
|
+
kind: "bookmarks",
|
|
1630
|
+
collectedAt: tweet.createdAt,
|
|
1631
|
+
source: "archive",
|
|
1632
|
+
rawJson: JSON.stringify(bookmark),
|
|
1633
|
+
});
|
|
1634
|
+
addTweetRow({
|
|
1635
|
+
id: tweet.id,
|
|
1636
|
+
kind: "bookmark",
|
|
1637
|
+
authorProfileId: "profile_unknown",
|
|
1638
|
+
text: tweet.text,
|
|
1639
|
+
createdAt: tweet.createdAt,
|
|
1640
|
+
isReplied: 0,
|
|
1641
|
+
replyToId: null,
|
|
1642
|
+
likeCount: tweet.likeCount,
|
|
1643
|
+
mediaCount: 0,
|
|
1644
|
+
bookmarked: 1,
|
|
1645
|
+
liked: 0,
|
|
1646
|
+
entitiesJson: "{}",
|
|
1647
|
+
mediaJson: "[]",
|
|
1648
|
+
quotedTweetId: null,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
bookmarkCount += bookmarks.length;
|
|
1652
|
+
onProgress({
|
|
1653
|
+
kind: "slice-file",
|
|
1654
|
+
slice: "bookmarks",
|
|
1655
|
+
processed: bookmarkFileIndex + 1,
|
|
1656
|
+
files: bookmarkEntries.length,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
if (bookmarkEntries.length > 0) {
|
|
1660
|
+
onProgress({
|
|
1661
|
+
kind: "slice-done",
|
|
1662
|
+
slice: "bookmarks",
|
|
1663
|
+
count: bookmarkCount,
|
|
1481
1664
|
});
|
|
1482
1665
|
}
|
|
1483
|
-
bookmarkCount += bookmarks.length;
|
|
1484
|
-
}
|
|
1485
1666
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1667
|
+
onProgress({ kind: "slice-start", slice: "media", files: 0 });
|
|
1668
|
+
const mediaFileCounts = yield* extractArchiveMediaFilesEffect(
|
|
1669
|
+
archivePath,
|
|
1670
|
+
selectedArchiveMediaKinds(selection),
|
|
1671
|
+
);
|
|
1672
|
+
onProgress({
|
|
1673
|
+
kind: "slice-done",
|
|
1674
|
+
slice: "media",
|
|
1675
|
+
count: Object.values(mediaFileCounts).reduce(
|
|
1676
|
+
(total, value) => total + value,
|
|
1677
|
+
0,
|
|
1678
|
+
),
|
|
1679
|
+
});
|
|
1490
1680
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1681
|
+
if (followerEntries.length > 0) {
|
|
1682
|
+
onProgress({
|
|
1683
|
+
kind: "slice-start",
|
|
1684
|
+
slice: "followers",
|
|
1685
|
+
files: followerEntries.length,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
for (const [followerFileIndex, entry] of followerEntries.entries()) {
|
|
1689
|
+
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1690
|
+
for (const row of getArchiveFollowRows(content, "follower")) {
|
|
1691
|
+
if (followerIds.has(row.externalUserId)) continue;
|
|
1692
|
+
followerIds.add(row.externalUserId);
|
|
1693
|
+
followerRows.push(row);
|
|
1694
|
+
}
|
|
1695
|
+
onProgress({
|
|
1696
|
+
kind: "slice-file",
|
|
1697
|
+
slice: "followers",
|
|
1698
|
+
processed: followerFileIndex + 1,
|
|
1699
|
+
files: followerEntries.length,
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
if (followerEntries.length > 0) {
|
|
1703
|
+
onProgress({
|
|
1704
|
+
kind: "slice-done",
|
|
1705
|
+
slice: "followers",
|
|
1706
|
+
count: followerRows.length,
|
|
1707
|
+
});
|
|
1497
1708
|
}
|
|
1498
|
-
}
|
|
1499
1709
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1710
|
+
if (followingEntries.length > 0) {
|
|
1711
|
+
onProgress({
|
|
1712
|
+
kind: "slice-start",
|
|
1713
|
+
slice: "following",
|
|
1714
|
+
files: followingEntries.length,
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
for (const [followingFileIndex, entry] of followingEntries.entries()) {
|
|
1718
|
+
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1719
|
+
for (const row of getArchiveFollowRows(content, "following")) {
|
|
1720
|
+
if (followingIds.has(row.externalUserId)) continue;
|
|
1721
|
+
followingIds.add(row.externalUserId);
|
|
1722
|
+
followingRows.push(row);
|
|
1723
|
+
}
|
|
1724
|
+
onProgress({
|
|
1725
|
+
kind: "slice-file",
|
|
1726
|
+
slice: "following",
|
|
1727
|
+
processed: followingFileIndex + 1,
|
|
1728
|
+
files: followingEntries.length,
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
if (followingEntries.length > 0) {
|
|
1732
|
+
onProgress({
|
|
1733
|
+
kind: "slice-done",
|
|
1734
|
+
slice: "following",
|
|
1735
|
+
count: followingRows.length,
|
|
1736
|
+
});
|
|
1506
1737
|
}
|
|
1507
|
-
}
|
|
1508
1738
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1739
|
+
for (const row of [...followerRows, ...followingRows]) {
|
|
1740
|
+
addArchiveFollowProfile(row.profileId, row.externalUserId);
|
|
1741
|
+
}
|
|
1512
1742
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1743
|
+
const clearedFollowDirections = new Set<ArchiveFollowDirection>();
|
|
1744
|
+
if (includeFollowers && followerEntries.length === 0) {
|
|
1745
|
+
clearedFollowDirections.add("followers");
|
|
1746
|
+
}
|
|
1747
|
+
if (includeFollowing && followingEntries.length === 0) {
|
|
1748
|
+
clearedFollowDirections.add("following");
|
|
1749
|
+
}
|
|
1750
|
+
const retainedFollowProfiles = getNativeDb()
|
|
1751
|
+
.prepare(
|
|
1752
|
+
`
|
|
1523
1753
|
select direction, profile_id, external_user_id, source, null as snapshot_id, null as snapshot_source
|
|
1524
1754
|
from follow_edges
|
|
1525
1755
|
union
|
|
@@ -1527,57 +1757,54 @@ export async function importArchive(
|
|
|
1527
1757
|
from follow_events ev
|
|
1528
1758
|
left join follow_snapshots snap on snap.id = ev.snapshot_id
|
|
1529
1759
|
`,
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
if (tweetRows.some((tweet) => tweet.authorProfileId === "profile_unknown")) {
|
|
1551
|
-
const unknownProfile = {
|
|
1552
|
-
id: "profile_unknown",
|
|
1553
|
-
handle: selection
|
|
1554
|
-
? uniqueArchiveProfileHandle("unknown", "profile_unknown")
|
|
1555
|
-
: "unknown",
|
|
1556
|
-
displayName: "Unknown",
|
|
1557
|
-
bio: "Imported from archive collection metadata",
|
|
1558
|
-
followersCount: 0,
|
|
1559
|
-
followingCount: 0,
|
|
1560
|
-
...defaultProfileMetadata,
|
|
1561
|
-
avatarHue: 210,
|
|
1562
|
-
avatarUrl: null,
|
|
1563
|
-
createdAt: accountPayload.createdAt,
|
|
1564
|
-
};
|
|
1565
|
-
const existingUnknownProfile = existingProfiles.get("profile_unknown");
|
|
1566
|
-
profiles.set(
|
|
1567
|
-
"profile_unknown",
|
|
1568
|
-
existingUnknownProfile
|
|
1569
|
-
? existingProfileToProfileRow(existingUnknownProfile)
|
|
1570
|
-
: unknownProfile,
|
|
1571
|
-
);
|
|
1572
|
-
}
|
|
1760
|
+
)
|
|
1761
|
+
.all() as Array<{
|
|
1762
|
+
direction: ArchiveFollowDirection;
|
|
1763
|
+
profile_id: string;
|
|
1764
|
+
external_user_id: string;
|
|
1765
|
+
source: string | null;
|
|
1766
|
+
snapshot_id: string | null;
|
|
1767
|
+
snapshot_source: string | null;
|
|
1768
|
+
}>;
|
|
1769
|
+
for (const row of retainedFollowProfiles) {
|
|
1770
|
+
const isClearedArchiveRow =
|
|
1771
|
+
clearedFollowDirections.has(row.direction) &&
|
|
1772
|
+
(row.source === "archive" ||
|
|
1773
|
+
row.snapshot_source === "archive" ||
|
|
1774
|
+
row.snapshot_id ===
|
|
1775
|
+
`follow_snapshot_archive_acct_primary_${row.direction}`);
|
|
1776
|
+
if (isClearedArchiveRow) continue;
|
|
1777
|
+
addArchiveFollowProfile(row.profile_id, row.external_user_id);
|
|
1778
|
+
}
|
|
1573
1779
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1780
|
+
if (
|
|
1781
|
+
tweetRows.some((tweet) => tweet.authorProfileId === "profile_unknown")
|
|
1782
|
+
) {
|
|
1783
|
+
const unknownProfile = {
|
|
1784
|
+
id: "profile_unknown",
|
|
1785
|
+
handle: selection
|
|
1786
|
+
? uniqueArchiveProfileHandle("unknown", "profile_unknown")
|
|
1787
|
+
: "unknown",
|
|
1788
|
+
displayName: "Unknown",
|
|
1789
|
+
bio: "Imported from archive collection metadata",
|
|
1790
|
+
followersCount: 0,
|
|
1791
|
+
followingCount: 0,
|
|
1792
|
+
...defaultProfileMetadata,
|
|
1793
|
+
avatarHue: 210,
|
|
1794
|
+
avatarUrl: null,
|
|
1795
|
+
createdAt: accountPayload.createdAt,
|
|
1796
|
+
};
|
|
1797
|
+
const existingUnknownProfile = existingProfiles.get("profile_unknown");
|
|
1798
|
+
profiles.set(
|
|
1799
|
+
"profile_unknown",
|
|
1800
|
+
existingUnknownProfile
|
|
1801
|
+
? existingProfileToProfileRow(existingUnknownProfile)
|
|
1802
|
+
: unknownProfile,
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1578
1805
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1806
|
+
const db = getNativeDb();
|
|
1807
|
+
const insertAccount = db.prepare(`
|
|
1581
1808
|
insert into accounts (id, name, handle, external_user_id, transport, is_default, created_at)
|
|
1582
1809
|
values (?, ?, ?, ?, ?, 1, ?)
|
|
1583
1810
|
on conflict(id) do update set
|
|
@@ -1588,11 +1815,11 @@ export async function importArchive(
|
|
|
1588
1815
|
is_default = 1,
|
|
1589
1816
|
created_at = excluded.created_at
|
|
1590
1817
|
`);
|
|
1591
|
-
|
|
1818
|
+
const insertAccountIfMissing = db.prepare(`
|
|
1592
1819
|
insert or ignore into accounts (id, name, handle, external_user_id, transport, is_default, created_at)
|
|
1593
1820
|
values (?, ?, ?, ?, ?, 1, ?)
|
|
1594
1821
|
`);
|
|
1595
|
-
|
|
1822
|
+
const insertProfile = db.prepare(`
|
|
1596
1823
|
insert into profiles (
|
|
1597
1824
|
id, handle, display_name, bio, followers_count, following_count,
|
|
1598
1825
|
public_metrics_json, avatar_hue, avatar_url, location, url, verified_type,
|
|
@@ -1615,7 +1842,7 @@ export async function importArchive(
|
|
|
1615
1842
|
raw_json = excluded.raw_json,
|
|
1616
1843
|
created_at = excluded.created_at
|
|
1617
1844
|
`);
|
|
1618
|
-
|
|
1845
|
+
const insertProfileIfMissing = db.prepare(`
|
|
1619
1846
|
insert or ignore into profiles (
|
|
1620
1847
|
id, handle, display_name, bio, followers_count, following_count,
|
|
1621
1848
|
public_metrics_json, avatar_hue, avatar_url, location, url, verified_type,
|
|
@@ -1623,7 +1850,7 @@ export async function importArchive(
|
|
|
1623
1850
|
)
|
|
1624
1851
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1625
1852
|
`);
|
|
1626
|
-
|
|
1853
|
+
const insertTweet = db.prepare(`
|
|
1627
1854
|
insert into tweets (
|
|
1628
1855
|
id, account_id, author_profile_id, kind, text, created_at, is_replied,
|
|
1629
1856
|
reply_to_id, like_count, media_count, bookmarked, liked, entities_json, media_json, quoted_tweet_id
|
|
@@ -1667,14 +1894,16 @@ export async function importArchive(
|
|
|
1667
1894
|
media_json = case when excluded.media_json <> '[]' then excluded.media_json else tweets.media_json end,
|
|
1668
1895
|
quoted_tweet_id = coalesce(excluded.quoted_tweet_id, tweets.quoted_tweet_id)
|
|
1669
1896
|
`);
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1897
|
+
const deleteTweetFts = db.prepare(
|
|
1898
|
+
"delete from tweets_fts where tweet_id = ?",
|
|
1899
|
+
);
|
|
1900
|
+
const insertTweetFts = db.prepare(
|
|
1901
|
+
"insert into tweets_fts (tweet_id, text) values (?, ?)",
|
|
1902
|
+
);
|
|
1903
|
+
const selectTweetFtsText = db.prepare(
|
|
1904
|
+
"select text from tweets where id = ?",
|
|
1905
|
+
);
|
|
1906
|
+
const insertTimelineEdge = db.prepare(`
|
|
1678
1907
|
insert into tweet_account_edges (
|
|
1679
1908
|
account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count, source,
|
|
1680
1909
|
raw_json, updated_at
|
|
@@ -1684,7 +1913,7 @@ export async function importArchive(
|
|
|
1684
1913
|
last_seen_at = max(tweet_account_edges.last_seen_at, excluded.last_seen_at),
|
|
1685
1914
|
updated_at = max(tweet_account_edges.updated_at, excluded.updated_at)
|
|
1686
1915
|
`);
|
|
1687
|
-
|
|
1916
|
+
const insertCollection = db.prepare(`
|
|
1688
1917
|
insert into tweet_collections (
|
|
1689
1918
|
account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
|
|
1690
1919
|
) values (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1700,20 +1929,20 @@ export async function importArchive(
|
|
|
1700
1929
|
end,
|
|
1701
1930
|
updated_at = max(tweet_collections.updated_at, excluded.updated_at)
|
|
1702
1931
|
`);
|
|
1703
|
-
|
|
1932
|
+
const insertConversation = db.prepare(`
|
|
1704
1933
|
insert into dm_conversations (
|
|
1705
1934
|
id, account_id, participant_profile_id, title, last_message_at, unread_count, needs_reply
|
|
1706
1935
|
) values (?, ?, ?, ?, ?, ?, ?)
|
|
1707
1936
|
`);
|
|
1708
|
-
|
|
1937
|
+
const insertMessage = db.prepare(`
|
|
1709
1938
|
insert into dm_messages (
|
|
1710
1939
|
id, conversation_id, sender_profile_id, text, created_at, direction, is_replied, media_count
|
|
1711
1940
|
) values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1712
1941
|
`);
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1942
|
+
const insertDmFts = db.prepare(
|
|
1943
|
+
"insert into dm_fts (message_id, text) values (?, ?)",
|
|
1944
|
+
);
|
|
1945
|
+
const insertFollowSnapshot = db.prepare(`
|
|
1717
1946
|
insert into follow_snapshots (
|
|
1718
1947
|
id, account_id, direction, source, status, page_count, result_count,
|
|
1719
1948
|
started_at, completed_at, raw_meta_json
|
|
@@ -1729,21 +1958,21 @@ export async function importArchive(
|
|
|
1729
1958
|
completed_at = excluded.completed_at,
|
|
1730
1959
|
raw_meta_json = excluded.raw_meta_json
|
|
1731
1960
|
`);
|
|
1732
|
-
|
|
1961
|
+
const insertFollowSnapshotMember = db.prepare(`
|
|
1733
1962
|
insert into follow_snapshot_members (
|
|
1734
1963
|
snapshot_id, profile_id, external_user_id, position
|
|
1735
1964
|
) values (?, ?, ?, ?)
|
|
1736
1965
|
`);
|
|
1737
|
-
|
|
1966
|
+
const selectFollowSnapshotMembers = db.prepare(`
|
|
1738
1967
|
select profile_id, external_user_id
|
|
1739
1968
|
from follow_snapshot_members
|
|
1740
1969
|
where snapshot_id = ?
|
|
1741
1970
|
order by position, profile_id
|
|
1742
1971
|
`);
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1972
|
+
const deleteFollowSnapshotMembers = db.prepare(
|
|
1973
|
+
"delete from follow_snapshot_members where snapshot_id = ?",
|
|
1974
|
+
);
|
|
1975
|
+
const deleteArchiveFollowEvents = db.prepare(`
|
|
1747
1976
|
delete from follow_events
|
|
1748
1977
|
where account_id = ? and direction = ? and (
|
|
1749
1978
|
snapshot_id = ? or snapshot_id in (
|
|
@@ -1752,27 +1981,27 @@ export async function importArchive(
|
|
|
1752
1981
|
)
|
|
1753
1982
|
)
|
|
1754
1983
|
`);
|
|
1755
|
-
|
|
1984
|
+
const deleteArchiveFollowSnapshotMembers = db.prepare(`
|
|
1756
1985
|
delete from follow_snapshot_members
|
|
1757
1986
|
where snapshot_id in (
|
|
1758
1987
|
select id from follow_snapshots
|
|
1759
1988
|
where account_id = ? and direction = ? and source = 'archive'
|
|
1760
1989
|
)
|
|
1761
1990
|
`);
|
|
1762
|
-
|
|
1991
|
+
const deleteArchiveFollowSnapshots = db.prepare(`
|
|
1763
1992
|
delete from follow_snapshots
|
|
1764
1993
|
where account_id = ? and direction = ? and source = 'archive'
|
|
1765
1994
|
`);
|
|
1766
|
-
|
|
1995
|
+
const deleteArchiveFollowEdges = db.prepare(`
|
|
1767
1996
|
delete from follow_edges
|
|
1768
1997
|
where account_id = ? and direction = ? and source = 'archive'
|
|
1769
1998
|
`);
|
|
1770
|
-
|
|
1999
|
+
const selectFollowEdges = db.prepare(`
|
|
1771
2000
|
select profile_id, external_user_id, current
|
|
1772
2001
|
from follow_edges
|
|
1773
2002
|
where account_id = ? and direction = ?
|
|
1774
2003
|
`);
|
|
1775
|
-
|
|
2004
|
+
const insertFollowEdge = db.prepare(`
|
|
1776
2005
|
insert into follow_edges (
|
|
1777
2006
|
account_id, direction, profile_id, external_user_id, source, current,
|
|
1778
2007
|
first_seen_at, last_seen_at, ended_at, updated_at
|
|
@@ -1788,25 +2017,25 @@ export async function importArchive(
|
|
|
1788
2017
|
ended_at = null,
|
|
1789
2018
|
updated_at = excluded.updated_at
|
|
1790
2019
|
`);
|
|
1791
|
-
|
|
2020
|
+
const endFollowEdge = db.prepare(`
|
|
1792
2021
|
update follow_edges
|
|
1793
2022
|
set current = 0, ended_at = ?, updated_at = ?
|
|
1794
2023
|
where account_id = ? and direction = ? and profile_id = ?
|
|
1795
2024
|
`);
|
|
1796
|
-
|
|
2025
|
+
const insertFollowEvent = db.prepare(`
|
|
1797
2026
|
insert into follow_events (
|
|
1798
2027
|
id, account_id, direction, profile_id, external_user_id, kind, event_at, snapshot_id
|
|
1799
2028
|
) values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1800
2029
|
`);
|
|
1801
|
-
|
|
2030
|
+
const clearSelectedLikes = db.prepare(`
|
|
1802
2031
|
delete from tweet_collections
|
|
1803
2032
|
where account_id = ? and kind = 'likes' and source in ('archive', 'legacy')
|
|
1804
2033
|
`);
|
|
1805
|
-
|
|
2034
|
+
const clearSelectedBookmarks = db.prepare(`
|
|
1806
2035
|
delete from tweet_collections
|
|
1807
2036
|
where account_id = ? and kind = 'bookmarks' and source in ('archive', 'legacy')
|
|
1808
2037
|
`);
|
|
1809
|
-
|
|
2038
|
+
const clearTweetLikedFlag = db.prepare(`
|
|
1810
2039
|
update tweets
|
|
1811
2040
|
set liked = 0
|
|
1812
2041
|
where account_id = ?
|
|
@@ -1816,7 +2045,7 @@ export async function importArchive(
|
|
|
1816
2045
|
where account_id = ? and kind = 'likes' and source in ('archive', 'legacy')
|
|
1817
2046
|
)
|
|
1818
2047
|
`);
|
|
1819
|
-
|
|
2048
|
+
const clearTweetBookmarkedFlag = db.prepare(`
|
|
1820
2049
|
update tweets
|
|
1821
2050
|
set bookmarked = 0
|
|
1822
2051
|
where account_id = ?
|
|
@@ -1826,7 +2055,7 @@ export async function importArchive(
|
|
|
1826
2055
|
where account_id = ? and kind = 'bookmarks' and source in ('archive', 'legacy')
|
|
1827
2056
|
)
|
|
1828
2057
|
`);
|
|
1829
|
-
|
|
2058
|
+
const clearSelectedArchiveTweetEdges = db.prepare(`
|
|
1830
2059
|
delete from tweet_account_edges
|
|
1831
2060
|
where account_id = ?
|
|
1832
2061
|
and kind in ('home', 'authored')
|
|
@@ -1844,12 +2073,12 @@ export async function importArchive(
|
|
|
1844
2073
|
)
|
|
1845
2074
|
)
|
|
1846
2075
|
`);
|
|
1847
|
-
|
|
2076
|
+
const deleteOrphanTweetLinkOccurrences = db.prepare(`
|
|
1848
2077
|
delete from link_occurrences
|
|
1849
2078
|
where source_kind = 'tweet'
|
|
1850
2079
|
and source_id not in (select id from tweets)
|
|
1851
2080
|
`);
|
|
1852
|
-
|
|
2081
|
+
const deleteOrphanArchiveCollectionTweets = db.prepare(`
|
|
1853
2082
|
delete from tweets
|
|
1854
2083
|
where account_id = ?
|
|
1855
2084
|
and kind in ('like', 'bookmark')
|
|
@@ -1870,7 +2099,7 @@ export async function importArchive(
|
|
|
1870
2099
|
or referencing_tweet.quoted_tweet_id = tweets.id
|
|
1871
2100
|
)
|
|
1872
2101
|
`);
|
|
1873
|
-
|
|
2102
|
+
const demoteSelectedArchiveTweetsWithCollections = db.prepare(`
|
|
1874
2103
|
update tweets
|
|
1875
2104
|
set kind = case
|
|
1876
2105
|
when exists (
|
|
@@ -1911,7 +2140,7 @@ export async function importArchive(
|
|
|
1911
2140
|
where account_id = ?
|
|
1912
2141
|
)
|
|
1913
2142
|
`);
|
|
1914
|
-
|
|
2143
|
+
const preserveSelectedArchiveTweetsReferencedElsewhere = db.prepare(`
|
|
1915
2144
|
update tweets
|
|
1916
2145
|
set kind = 'archive_stale'
|
|
1917
2146
|
where account_id = ?
|
|
@@ -1988,7 +2217,7 @@ export async function importArchive(
|
|
|
1988
2217
|
)
|
|
1989
2218
|
)
|
|
1990
2219
|
`);
|
|
1991
|
-
|
|
2220
|
+
const deleteSelectedArchiveTweetsWithoutCollections = db.prepare(`
|
|
1992
2221
|
delete from tweets
|
|
1993
2222
|
where account_id = ?
|
|
1994
2223
|
and id in (
|
|
@@ -2061,11 +2290,11 @@ export async function importArchive(
|
|
|
2061
2290
|
)
|
|
2062
2291
|
)
|
|
2063
2292
|
`);
|
|
2064
|
-
|
|
2293
|
+
const deleteOrphanTweetFts = db.prepare(`
|
|
2065
2294
|
delete from tweets_fts
|
|
2066
2295
|
where tweet_id not in (select id from tweets)
|
|
2067
2296
|
`);
|
|
2068
|
-
|
|
2297
|
+
const clearDmFts = db.prepare(`
|
|
2069
2298
|
delete from dm_fts
|
|
2070
2299
|
where message_id in (
|
|
2071
2300
|
select m.id
|
|
@@ -2074,7 +2303,7 @@ export async function importArchive(
|
|
|
2074
2303
|
where c.account_id = ?
|
|
2075
2304
|
)
|
|
2076
2305
|
`);
|
|
2077
|
-
|
|
2306
|
+
const clearDmLinkOccurrences = db.prepare(`
|
|
2078
2307
|
delete from link_occurrences
|
|
2079
2308
|
where source_kind = 'dm'
|
|
2080
2309
|
and source_id in (
|
|
@@ -2084,362 +2313,435 @@ export async function importArchive(
|
|
|
2084
2313
|
where c.account_id = ?
|
|
2085
2314
|
)
|
|
2086
2315
|
`);
|
|
2087
|
-
|
|
2316
|
+
const clearDmMessages = db.prepare(`
|
|
2088
2317
|
delete from dm_messages
|
|
2089
2318
|
where conversation_id in (
|
|
2090
2319
|
select id from dm_conversations where account_id = ?
|
|
2091
2320
|
)
|
|
2092
2321
|
`);
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2322
|
+
const clearDmConversations = db.prepare(
|
|
2323
|
+
"delete from dm_conversations where account_id = ?",
|
|
2324
|
+
);
|
|
2096
2325
|
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2326
|
+
function importFollowRows(
|
|
2327
|
+
direction: ArchiveFollowDirection,
|
|
2328
|
+
rows: Array<{ profileId: string; externalUserId: string }>,
|
|
2329
|
+
entryCount: number,
|
|
2330
|
+
now: string,
|
|
2331
|
+
) {
|
|
2332
|
+
const snapshotId = `follow_snapshot_archive_acct_primary_${direction}`;
|
|
2333
|
+
const existingEdges = new Map(
|
|
2334
|
+
(
|
|
2335
|
+
selectFollowEdges.all("acct_primary", direction) as Array<{
|
|
2336
|
+
profile_id: string;
|
|
2337
|
+
external_user_id: string;
|
|
2338
|
+
current: number;
|
|
2339
|
+
}>
|
|
2340
|
+
).map((row) => [row.profile_id, row]),
|
|
2341
|
+
);
|
|
2342
|
+
const existingMemberKey = (
|
|
2343
|
+
selectFollowSnapshotMembers.all(snapshotId) as Array<{
|
|
2107
2344
|
profile_id: string;
|
|
2108
2345
|
external_user_id: string;
|
|
2109
|
-
current: number;
|
|
2110
2346
|
}>
|
|
2111
|
-
).map((row) => [row.profile_id, row]),
|
|
2112
|
-
);
|
|
2113
|
-
const existingMemberKey = (
|
|
2114
|
-
selectFollowSnapshotMembers.all(snapshotId) as Array<{
|
|
2115
|
-
profile_id: string;
|
|
2116
|
-
external_user_id: string;
|
|
2117
|
-
}>
|
|
2118
|
-
)
|
|
2119
|
-
.map(
|
|
2120
|
-
(row, index) =>
|
|
2121
|
-
`${String(index)}:${row.profile_id}:${row.external_user_id}`,
|
|
2122
2347
|
)
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
direction,
|
|
2137
|
-
entryCount,
|
|
2138
|
-
rows.length,
|
|
2139
|
-
now,
|
|
2140
|
-
now,
|
|
2141
|
-
JSON.stringify({ archivePath, result_count: rows.length }),
|
|
2142
|
-
);
|
|
2143
|
-
|
|
2144
|
-
if (membersChanged) {
|
|
2145
|
-
deleteFollowSnapshotMembers.run(snapshotId);
|
|
2146
|
-
}
|
|
2147
|
-
rows.forEach((row, index) => {
|
|
2148
|
-
const profileId = resolveProfileId(row.profileId);
|
|
2149
|
-
currentProfileIds.add(profileId);
|
|
2150
|
-
if (membersChanged) {
|
|
2151
|
-
insertFollowSnapshotMember.run(
|
|
2152
|
-
snapshotId,
|
|
2153
|
-
profileId,
|
|
2154
|
-
row.externalUserId,
|
|
2155
|
-
index,
|
|
2156
|
-
);
|
|
2157
|
-
}
|
|
2348
|
+
.map(
|
|
2349
|
+
(row, index) =>
|
|
2350
|
+
`${String(index)}:${row.profile_id}:${row.external_user_id}`,
|
|
2351
|
+
)
|
|
2352
|
+
.join("\n");
|
|
2353
|
+
const nextMemberKey = rows
|
|
2354
|
+
.map(
|
|
2355
|
+
(row, index) =>
|
|
2356
|
+
`${String(index)}:${row.profileId}:${row.externalUserId}`,
|
|
2357
|
+
)
|
|
2358
|
+
.join("\n");
|
|
2359
|
+
const membersChanged = existingMemberKey !== nextMemberKey;
|
|
2360
|
+
const currentProfileIds = new Set<string>();
|
|
2158
2361
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2362
|
+
insertFollowSnapshot.run(
|
|
2363
|
+
snapshotId,
|
|
2161
2364
|
"acct_primary",
|
|
2162
2365
|
direction,
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
now,
|
|
2366
|
+
entryCount,
|
|
2367
|
+
rows.length,
|
|
2166
2368
|
now,
|
|
2167
2369
|
now,
|
|
2370
|
+
JSON.stringify({ archivePath, result_count: rows.length }),
|
|
2168
2371
|
);
|
|
2169
|
-
|
|
2372
|
+
|
|
2373
|
+
if (membersChanged) {
|
|
2374
|
+
deleteFollowSnapshotMembers.run(snapshotId);
|
|
2375
|
+
}
|
|
2376
|
+
rows.forEach((row, index) => {
|
|
2377
|
+
const profileId = resolveProfileId(row.profileId);
|
|
2378
|
+
currentProfileIds.add(profileId);
|
|
2379
|
+
if (membersChanged) {
|
|
2380
|
+
insertFollowSnapshotMember.run(
|
|
2381
|
+
snapshotId,
|
|
2382
|
+
profileId,
|
|
2383
|
+
row.externalUserId,
|
|
2384
|
+
index,
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
const previous = existingEdges.get(profileId);
|
|
2389
|
+
insertFollowEdge.run(
|
|
2390
|
+
"acct_primary",
|
|
2391
|
+
direction,
|
|
2392
|
+
profileId,
|
|
2393
|
+
row.externalUserId,
|
|
2394
|
+
now,
|
|
2395
|
+
now,
|
|
2396
|
+
now,
|
|
2397
|
+
);
|
|
2398
|
+
if (!previous || previous.current === 0) {
|
|
2399
|
+
insertFollowEvent.run(
|
|
2400
|
+
`follow_event_${randomUUID()}`,
|
|
2401
|
+
"acct_primary",
|
|
2402
|
+
direction,
|
|
2403
|
+
profileId,
|
|
2404
|
+
row.externalUserId,
|
|
2405
|
+
"started",
|
|
2406
|
+
now,
|
|
2407
|
+
snapshotId,
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
for (const [profileId, previous] of existingEdges) {
|
|
2413
|
+
if (previous.current === 0 || currentProfileIds.has(profileId)) {
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
endFollowEdge.run(now, now, "acct_primary", direction, profileId);
|
|
2170
2417
|
insertFollowEvent.run(
|
|
2171
2418
|
`follow_event_${randomUUID()}`,
|
|
2172
2419
|
"acct_primary",
|
|
2173
2420
|
direction,
|
|
2174
2421
|
profileId,
|
|
2175
|
-
|
|
2176
|
-
"
|
|
2422
|
+
previous.external_user_id,
|
|
2423
|
+
"ended",
|
|
2177
2424
|
now,
|
|
2178
2425
|
snapshotId,
|
|
2179
2426
|
);
|
|
2180
2427
|
}
|
|
2181
|
-
}
|
|
2428
|
+
}
|
|
2182
2429
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
insertFollowEvent.run(
|
|
2189
|
-
`follow_event_${randomUUID()}`,
|
|
2430
|
+
function clearArchiveFollowRows(direction: ArchiveFollowDirection) {
|
|
2431
|
+
deleteArchiveFollowEvents.run(
|
|
2432
|
+
"acct_primary",
|
|
2433
|
+
direction,
|
|
2434
|
+
`follow_snapshot_archive_acct_primary_${direction}`,
|
|
2190
2435
|
"acct_primary",
|
|
2191
2436
|
direction,
|
|
2192
|
-
profileId,
|
|
2193
|
-
previous.external_user_id,
|
|
2194
|
-
"ended",
|
|
2195
|
-
now,
|
|
2196
|
-
snapshotId,
|
|
2197
2437
|
);
|
|
2438
|
+
deleteArchiveFollowSnapshotMembers.run("acct_primary", direction);
|
|
2439
|
+
deleteArchiveFollowSnapshots.run("acct_primary", direction);
|
|
2440
|
+
deleteArchiveFollowEdges.run("acct_primary", direction);
|
|
2198
2441
|
}
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
function clearArchiveFollowRows(direction: ArchiveFollowDirection) {
|
|
2202
|
-
deleteArchiveFollowEvents.run(
|
|
2203
|
-
"acct_primary",
|
|
2204
|
-
direction,
|
|
2205
|
-
`follow_snapshot_archive_acct_primary_${direction}`,
|
|
2206
|
-
"acct_primary",
|
|
2207
|
-
direction,
|
|
2208
|
-
);
|
|
2209
|
-
deleteArchiveFollowSnapshotMembers.run("acct_primary", direction);
|
|
2210
|
-
deleteArchiveFollowSnapshots.run("acct_primary", direction);
|
|
2211
|
-
deleteArchiveFollowEdges.run("acct_primary", direction);
|
|
2212
|
-
}
|
|
2213
2442
|
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
"acct_primary",
|
|
2224
|
-
localProfile.id,
|
|
2225
|
-
"acct_primary",
|
|
2226
|
-
);
|
|
2227
|
-
preserveSelectedArchiveTweetsReferencedElsewhere.run(
|
|
2228
|
-
"acct_primary",
|
|
2229
|
-
"acct_primary",
|
|
2230
|
-
"acct_primary",
|
|
2231
|
-
localProfile.id,
|
|
2232
|
-
"acct_primary",
|
|
2233
|
-
"acct_primary",
|
|
2234
|
-
"acct_primary",
|
|
2235
|
-
localProfile.id,
|
|
2236
|
-
"acct_primary",
|
|
2237
|
-
"acct_primary",
|
|
2238
|
-
localProfile.id,
|
|
2239
|
-
);
|
|
2240
|
-
deleteSelectedArchiveTweetsWithoutCollections.run(
|
|
2241
|
-
"acct_primary",
|
|
2242
|
-
"acct_primary",
|
|
2243
|
-
"acct_primary",
|
|
2244
|
-
localProfile.id,
|
|
2245
|
-
"acct_primary",
|
|
2246
|
-
"acct_primary",
|
|
2247
|
-
localProfile.id,
|
|
2248
|
-
"acct_primary",
|
|
2249
|
-
"acct_primary",
|
|
2250
|
-
localProfile.id,
|
|
2251
|
-
);
|
|
2252
|
-
deleteOrphanTweetFts.run();
|
|
2253
|
-
deleteOrphanTweetLinkOccurrences.run();
|
|
2254
|
-
clearSelectedArchiveTweetEdges.run(
|
|
2255
|
-
"acct_primary",
|
|
2256
|
-
"acct_primary",
|
|
2257
|
-
localProfile.id,
|
|
2258
|
-
);
|
|
2259
|
-
}
|
|
2260
|
-
if (includeLikes) {
|
|
2261
|
-
clearTweetLikedFlag.run("acct_primary", "acct_primary");
|
|
2262
|
-
clearSelectedLikes.run("acct_primary");
|
|
2263
|
-
}
|
|
2264
|
-
if (includeBookmarks) {
|
|
2265
|
-
clearTweetBookmarkedFlag.run("acct_primary", "acct_primary");
|
|
2266
|
-
clearSelectedBookmarks.run("acct_primary");
|
|
2267
|
-
}
|
|
2268
|
-
if (includeLikes || includeBookmarks) {
|
|
2269
|
-
deleteOrphanArchiveCollectionTweets.run("acct_primary");
|
|
2270
|
-
deleteOrphanTweetFts.run();
|
|
2271
|
-
deleteOrphanTweetLinkOccurrences.run();
|
|
2272
|
-
}
|
|
2273
|
-
if (includeDirectMessages) {
|
|
2274
|
-
clearDmLinkOccurrences.run("acct_primary");
|
|
2275
|
-
clearDmFts.run("acct_primary");
|
|
2276
|
-
clearDmMessages.run("acct_primary");
|
|
2277
|
-
clearDmConversations.run("acct_primary");
|
|
2443
|
+
onProgress({ kind: "writing" });
|
|
2444
|
+
const WRITE_PROGRESS_INTERVAL = 1000;
|
|
2445
|
+
function tickWrite(
|
|
2446
|
+
phase: ImportWritePhase,
|
|
2447
|
+
processed: number,
|
|
2448
|
+
total: number,
|
|
2449
|
+
) {
|
|
2450
|
+
if (processed === total || processed % WRITE_PROGRESS_INTERVAL === 0) {
|
|
2451
|
+
onProgress({ kind: "write-progress", phase, processed, total });
|
|
2278
2452
|
}
|
|
2279
2453
|
}
|
|
2454
|
+
db.transaction(() => {
|
|
2455
|
+
if (!selection) {
|
|
2456
|
+
clearImportedData(db);
|
|
2457
|
+
clearMentionSyncState(db);
|
|
2458
|
+
}
|
|
2280
2459
|
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2460
|
+
if (selection) {
|
|
2461
|
+
if (includeTweets) {
|
|
2462
|
+
clearAuthoredSyncCursors(db, "acct_primary");
|
|
2463
|
+
demoteSelectedArchiveTweetsWithCollections.run(
|
|
2464
|
+
"acct_primary",
|
|
2465
|
+
"acct_primary",
|
|
2466
|
+
"acct_primary",
|
|
2467
|
+
"acct_primary",
|
|
2468
|
+
"acct_primary",
|
|
2469
|
+
localProfile.id,
|
|
2470
|
+
"acct_primary",
|
|
2471
|
+
);
|
|
2472
|
+
preserveSelectedArchiveTweetsReferencedElsewhere.run(
|
|
2473
|
+
"acct_primary",
|
|
2474
|
+
"acct_primary",
|
|
2475
|
+
"acct_primary",
|
|
2476
|
+
localProfile.id,
|
|
2477
|
+
"acct_primary",
|
|
2478
|
+
"acct_primary",
|
|
2479
|
+
"acct_primary",
|
|
2480
|
+
localProfile.id,
|
|
2481
|
+
"acct_primary",
|
|
2482
|
+
"acct_primary",
|
|
2483
|
+
localProfile.id,
|
|
2484
|
+
);
|
|
2485
|
+
deleteSelectedArchiveTweetsWithoutCollections.run(
|
|
2486
|
+
"acct_primary",
|
|
2487
|
+
"acct_primary",
|
|
2488
|
+
"acct_primary",
|
|
2489
|
+
localProfile.id,
|
|
2490
|
+
"acct_primary",
|
|
2491
|
+
"acct_primary",
|
|
2492
|
+
localProfile.id,
|
|
2493
|
+
"acct_primary",
|
|
2494
|
+
"acct_primary",
|
|
2495
|
+
localProfile.id,
|
|
2496
|
+
);
|
|
2497
|
+
deleteOrphanTweetFts.run();
|
|
2498
|
+
deleteOrphanTweetLinkOccurrences.run();
|
|
2499
|
+
clearSelectedArchiveTweetEdges.run(
|
|
2500
|
+
"acct_primary",
|
|
2501
|
+
"acct_primary",
|
|
2502
|
+
localProfile.id,
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
if (includeLikes) {
|
|
2506
|
+
clearTweetLikedFlag.run("acct_primary", "acct_primary");
|
|
2507
|
+
clearSelectedLikes.run("acct_primary");
|
|
2508
|
+
}
|
|
2509
|
+
if (includeBookmarks) {
|
|
2510
|
+
clearTweetBookmarkedFlag.run("acct_primary", "acct_primary");
|
|
2511
|
+
clearSelectedBookmarks.run("acct_primary");
|
|
2512
|
+
}
|
|
2513
|
+
if (includeLikes || includeBookmarks) {
|
|
2514
|
+
deleteOrphanArchiveCollectionTweets.run("acct_primary");
|
|
2515
|
+
deleteOrphanTweetFts.run();
|
|
2516
|
+
deleteOrphanTweetLinkOccurrences.run();
|
|
2517
|
+
}
|
|
2518
|
+
if (includeDirectMessages) {
|
|
2519
|
+
clearDmLinkOccurrences.run("acct_primary");
|
|
2520
|
+
clearDmFts.run("acct_primary");
|
|
2521
|
+
clearDmMessages.run("acct_primary");
|
|
2522
|
+
clearDmConversations.run("acct_primary");
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2312
2525
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
tweet.authorProfileId === "profile_me"
|
|
2316
|
-
? localProfile.id
|
|
2317
|
-
: resolveProfileId(tweet.authorProfileId);
|
|
2318
|
-
insertTweet.run(
|
|
2319
|
-
tweet.id,
|
|
2526
|
+
const writeAccount = selection ? insertAccountIfMissing : insertAccount;
|
|
2527
|
+
writeAccount.run(
|
|
2320
2528
|
"acct_primary",
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
tweet.replyToId,
|
|
2327
|
-
tweet.likeCount,
|
|
2328
|
-
tweet.mediaCount,
|
|
2329
|
-
tweet.bookmarked,
|
|
2330
|
-
tweet.liked,
|
|
2331
|
-
tweet.entitiesJson,
|
|
2332
|
-
tweet.mediaJson,
|
|
2333
|
-
tweet.quotedTweetId,
|
|
2529
|
+
accountPayload.displayName,
|
|
2530
|
+
`@${accountPayload.username}`,
|
|
2531
|
+
accountPayload.accountId,
|
|
2532
|
+
"archive",
|
|
2533
|
+
accountPayload.createdAt,
|
|
2334
2534
|
);
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2535
|
+
|
|
2536
|
+
const writeProfile =
|
|
2537
|
+
!selection || includeProfiles ? insertProfile : insertProfileIfMissing;
|
|
2538
|
+
const profilesTotal = profiles.size;
|
|
2539
|
+
if (profilesTotal > 0) {
|
|
2540
|
+
onProgress({
|
|
2541
|
+
kind: "write-start",
|
|
2542
|
+
phase: "profiles",
|
|
2543
|
+
total: profilesTotal,
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
let profileIndex = 0;
|
|
2547
|
+
for (const profile of profiles.values()) {
|
|
2548
|
+
writeProfile.run(
|
|
2549
|
+
profile.id,
|
|
2550
|
+
profile.handle,
|
|
2551
|
+
profile.displayName,
|
|
2552
|
+
profile.bio,
|
|
2553
|
+
profile.followersCount,
|
|
2554
|
+
profile.followingCount,
|
|
2555
|
+
profile.publicMetricsJson,
|
|
2556
|
+
profile.avatarHue,
|
|
2557
|
+
profile.avatarUrl,
|
|
2558
|
+
profile.location,
|
|
2559
|
+
profile.url,
|
|
2560
|
+
profile.verifiedType,
|
|
2561
|
+
profile.entitiesJson,
|
|
2562
|
+
profile.rawJson,
|
|
2563
|
+
profile.createdAt,
|
|
2564
|
+
);
|
|
2565
|
+
profileIndex += 1;
|
|
2566
|
+
tickWrite("profiles", profileIndex, profilesTotal);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
if (tweetRows.length > 0) {
|
|
2570
|
+
onProgress({
|
|
2571
|
+
kind: "write-start",
|
|
2572
|
+
phase: "tweets",
|
|
2573
|
+
total: tweetRows.length,
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
let tweetWriteIndex = 0;
|
|
2577
|
+
for (const tweet of tweetRows) {
|
|
2578
|
+
const authorProfileId =
|
|
2579
|
+
tweet.authorProfileId === "profile_me"
|
|
2580
|
+
? localProfile.id
|
|
2581
|
+
: resolveProfileId(tweet.authorProfileId);
|
|
2582
|
+
insertTweet.run(
|
|
2339
2583
|
tweet.id,
|
|
2584
|
+
"acct_primary",
|
|
2585
|
+
authorProfileId,
|
|
2340
2586
|
tweet.kind,
|
|
2587
|
+
tweet.text,
|
|
2341
2588
|
tweet.createdAt,
|
|
2342
|
-
tweet.
|
|
2343
|
-
|
|
2589
|
+
tweet.isReplied,
|
|
2590
|
+
tweet.replyToId,
|
|
2591
|
+
tweet.likeCount,
|
|
2592
|
+
tweet.mediaCount,
|
|
2593
|
+
tweet.bookmarked,
|
|
2594
|
+
tweet.liked,
|
|
2595
|
+
tweet.entitiesJson,
|
|
2596
|
+
tweet.mediaJson,
|
|
2597
|
+
tweet.quotedTweetId,
|
|
2344
2598
|
);
|
|
2599
|
+
deleteTweetFts.run(tweet.id);
|
|
2600
|
+
if (tweet.kind === "home") {
|
|
2601
|
+
insertTimelineEdge.run(
|
|
2602
|
+
"acct_primary",
|
|
2603
|
+
tweet.id,
|
|
2604
|
+
tweet.kind,
|
|
2605
|
+
tweet.createdAt,
|
|
2606
|
+
tweet.createdAt,
|
|
2607
|
+
new Date().toISOString(),
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
if (authorProfileId === localProfile.id) {
|
|
2611
|
+
insertTimelineEdge.run(
|
|
2612
|
+
"acct_primary",
|
|
2613
|
+
tweet.id,
|
|
2614
|
+
"authored",
|
|
2615
|
+
tweet.createdAt,
|
|
2616
|
+
tweet.createdAt,
|
|
2617
|
+
new Date().toISOString(),
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
const storedTweet = selectTweetFtsText.get(tweet.id) as
|
|
2621
|
+
| { text: string }
|
|
2622
|
+
| undefined;
|
|
2623
|
+
insertTweetFts.run(tweet.id, storedTweet?.text ?? tweet.text);
|
|
2624
|
+
tweetWriteIndex += 1;
|
|
2625
|
+
tickWrite("tweets", tweetWriteIndex, tweetRows.length);
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
const importedAt = new Date().toISOString();
|
|
2629
|
+
if (collectionRows.length > 0) {
|
|
2630
|
+
onProgress({
|
|
2631
|
+
kind: "write-start",
|
|
2632
|
+
phase: "collections",
|
|
2633
|
+
total: collectionRows.length,
|
|
2634
|
+
});
|
|
2345
2635
|
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2636
|
+
let collectionIndex = 0;
|
|
2637
|
+
for (const collection of collectionRows) {
|
|
2638
|
+
insertCollection.run(
|
|
2348
2639
|
"acct_primary",
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2640
|
+
collection.tweetId,
|
|
2641
|
+
collection.kind,
|
|
2642
|
+
collection.collectedAt,
|
|
2643
|
+
collection.source,
|
|
2644
|
+
collection.rawJson,
|
|
2645
|
+
importedAt,
|
|
2354
2646
|
);
|
|
2647
|
+
collectionIndex += 1;
|
|
2648
|
+
tickWrite("collections", collectionIndex, collectionRows.length);
|
|
2355
2649
|
}
|
|
2356
|
-
const storedTweet = selectTweetFtsText.get(tweet.id) as
|
|
2357
|
-
| { text: string }
|
|
2358
|
-
| undefined;
|
|
2359
|
-
insertTweetFts.run(tweet.id, storedTweet?.text ?? tweet.text);
|
|
2360
|
-
}
|
|
2361
2650
|
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
}
|
|
2651
|
+
for (const conversation of conversations.values()) {
|
|
2652
|
+
insertConversation.run(
|
|
2653
|
+
conversation.id,
|
|
2654
|
+
conversation.accountId,
|
|
2655
|
+
conversation.participantProfileId,
|
|
2656
|
+
conversation.title,
|
|
2657
|
+
conversation.lastMessageAt,
|
|
2658
|
+
conversation.unreadCount,
|
|
2659
|
+
conversation.needsReply,
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2374
2662
|
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2663
|
+
if (dmMessages.length > 0) {
|
|
2664
|
+
onProgress({
|
|
2665
|
+
kind: "write-start",
|
|
2666
|
+
phase: "dmMessages",
|
|
2667
|
+
total: dmMessages.length,
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
let dmWriteIndex = 0;
|
|
2671
|
+
for (const message of dmMessages) {
|
|
2672
|
+
insertMessage.run(
|
|
2673
|
+
message.id,
|
|
2674
|
+
message.conversationId,
|
|
2675
|
+
message.senderProfileId,
|
|
2676
|
+
message.text,
|
|
2677
|
+
message.createdAt,
|
|
2678
|
+
message.direction,
|
|
2679
|
+
message.direction === "outbound" ? 1 : 0,
|
|
2680
|
+
message.mediaCount,
|
|
2681
|
+
);
|
|
2682
|
+
insertDmFts.run(message.id, message.text);
|
|
2683
|
+
dmWriteIndex += 1;
|
|
2684
|
+
tickWrite("dmMessages", dmWriteIndex, dmMessages.length);
|
|
2685
|
+
}
|
|
2386
2686
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
)
|
|
2398
|
-
|
|
2399
|
-
|
|
2687
|
+
if (includeFollowers && followerEntries.length > 0) {
|
|
2688
|
+
importFollowRows(
|
|
2689
|
+
"followers",
|
|
2690
|
+
followerRows,
|
|
2691
|
+
followerEntries.length,
|
|
2692
|
+
importedAt,
|
|
2693
|
+
);
|
|
2694
|
+
} else if (includeFollowers) {
|
|
2695
|
+
clearArchiveFollowRows("followers");
|
|
2696
|
+
}
|
|
2697
|
+
if (includeFollowing && followingEntries.length > 0) {
|
|
2698
|
+
importFollowRows(
|
|
2699
|
+
"following",
|
|
2700
|
+
followingRows,
|
|
2701
|
+
followingEntries.length,
|
|
2702
|
+
importedAt,
|
|
2703
|
+
);
|
|
2704
|
+
} else if (includeFollowing) {
|
|
2705
|
+
clearArchiveFollowRows("following");
|
|
2706
|
+
}
|
|
2707
|
+
})();
|
|
2708
|
+
onProgress({ kind: "done" });
|
|
2400
2709
|
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
}
|
|
2421
|
-
})
|
|
2710
|
+
return {
|
|
2711
|
+
ok: true,
|
|
2712
|
+
archivePath,
|
|
2713
|
+
account: {
|
|
2714
|
+
id: accountPayload.accountId,
|
|
2715
|
+
handle: accountPayload.username,
|
|
2716
|
+
displayName: accountPayload.displayName,
|
|
2717
|
+
},
|
|
2718
|
+
counts: {
|
|
2719
|
+
tweets: authoredTweetCount,
|
|
2720
|
+
likes: likeCount,
|
|
2721
|
+
bookmarks: bookmarkCount,
|
|
2722
|
+
dmConversations: conversations.size,
|
|
2723
|
+
dmMessages: dmMessages.length,
|
|
2724
|
+
profiles: profiles.size,
|
|
2725
|
+
mediaFiles: mediaFileCounts,
|
|
2726
|
+
followers: followerRows.length,
|
|
2727
|
+
following: followingRows.length,
|
|
2728
|
+
},
|
|
2729
|
+
};
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2422
2732
|
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
dmConversations: conversations.size,
|
|
2436
|
-
dmMessages: dmMessages.length,
|
|
2437
|
-
profiles: profiles.size,
|
|
2438
|
-
mediaFiles: mediaFileCounts,
|
|
2439
|
-
followers: followerRows.length,
|
|
2440
|
-
following: followingRows.length,
|
|
2441
|
-
},
|
|
2442
|
-
};
|
|
2733
|
+
export function importArchiveEffect(
|
|
2734
|
+
archivePath: string,
|
|
2735
|
+
options: ImportArchiveOptions = {},
|
|
2736
|
+
): Effect.Effect<ImportedArchiveSummary, unknown> {
|
|
2737
|
+
return importArchiveInternalEffect(archivePath, options);
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
export function importArchive(
|
|
2741
|
+
archivePath: string,
|
|
2742
|
+
options: ImportArchiveOptions = {},
|
|
2743
|
+
): Promise<ImportedArchiveSummary> {
|
|
2744
|
+
return runEffectPromise(importArchiveEffect(archivePath, options));
|
|
2443
2745
|
}
|
|
2444
2746
|
|
|
2445
2747
|
export const __test__ = {
|