birdclaw 0.8.2 → 0.8.4
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 +33 -0
- package/README.md +10 -8
- package/bin/birdclaw.mjs +4 -66
- package/dist/cli/birdclaw.js +28165 -0
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
- package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
- package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
- package/dist/client/assets/chunk-C0phanGV.js +1 -0
- package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
- package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
- package/dist/client/assets/discuss-DTjaOugw.js +1 -0
- package/dist/client/assets/dms-CGd6Eogq.js +1 -0
- package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
- package/dist/client/assets/index-CEIG7S6s.js +31 -0
- package/dist/client/assets/index-bpFrQ5yG.css +1 -0
- package/dist/client/assets/likes-cLrcAQem.js +1 -0
- package/dist/client/assets/links-CM9QO1LC.js +1 -0
- package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
- package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
- package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
- package/dist/client/assets/network-map-B5a3whqj.js +2 -0
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
- package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
- package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
- package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
- package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
- package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
- package/dist/client/assets/react-nBylq1U5.js +1 -0
- package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
- package/dist/client/assets/routes-q7ZaW4En.js +1 -0
- package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
- package/dist/client/assets/search-Dq5kYsxA.js +1 -0
- package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
- package/dist/client/assets/styles-B8s3tt3r.css +2 -0
- package/dist/client/assets/today-CeG1O7Py.js +1 -0
- package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
- package/dist/client/assets/users-CHbKlqUr.js +1 -0
- package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
- package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
- package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
- package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
- package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
- package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
- package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
- package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
- package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
- package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
- package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
- package/dist/server/assets/blocks-DSgQghqP.js +17 -0
- package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
- package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
- package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
- package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
- package/dist/server/assets/discuss-QTo08NCN.js +338 -0
- package/dist/server/assets/dms-BuLj4bp8.js +368 -0
- package/dist/server/assets/dms-DAyQrVM2.js +240 -0
- package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
- package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
- package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
- package/dist/server/assets/likes-DBOAtsJU.js +14 -0
- package/dist/server/assets/links-DAhSjynu.js +240 -0
- package/dist/server/assets/links-DmH5lxHO.js +468 -0
- package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
- package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
- package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
- package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
- package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
- package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
- package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
- package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
- package/dist/server/assets/query-client-BdauHBgd.js +923 -0
- package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
- package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
- package/dist/server/assets/router-DqkwSPiM.js +17026 -0
- package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
- package/dist/server/assets/start-FaDOTmto.js +4 -0
- package/dist/server/assets/today-B5hVM57Z.js +17 -0
- package/dist/server/assets/today-fvGiZ7ud.js +303 -0
- package/dist/server/assets/ui-qbmcMSSX.js +137 -0
- package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
- package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
- package/dist/server/server.js +5520 -0
- package/package.json +27 -34
- package/playwright.config.ts +0 -35
- package/scripts/browser-perf.mjs +0 -427
- package/scripts/build-docs-site.mjs +0 -967
- package/scripts/docs-site-assets.mjs +0 -311
- package/scripts/run-vitest.mjs +0 -21
- package/scripts/sanitize-node-options.mjs +0 -23
- package/scripts/start-test-server.mjs +0 -42
- package/src/cli-moderation.ts +0 -210
- package/src/cli.ts +0 -2336
- package/src/components/AccountSwitcher.tsx +0 -186
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- package/src/components/BrandMark.tsx +0 -67
- package/src/components/ConversationThread.tsx +0 -131
- package/src/components/DmWorkspace.tsx +0 -286
- package/src/components/EmbeddedTweetCard.tsx +0 -55
- package/src/components/FeedState.tsx +0 -147
- package/src/components/FloatingPreview.tsx +0 -382
- package/src/components/InboxCard.tsx +0 -152
- package/src/components/LinkPreviewCard.tsx +0 -292
- package/src/components/MarkdownViewer.tsx +0 -777
- package/src/components/ProfileAnalysisStream.tsx +0 -428
- package/src/components/ProfilePreview.tsx +0 -129
- package/src/components/SavedTimelineView.tsx +0 -155
- package/src/components/SmartTimestamp.tsx +0 -72
- package/src/components/SyncNowButton.tsx +0 -140
- package/src/components/ThemeSlider.tsx +0 -80
- package/src/components/TimelineCard.tsx +0 -533
- package/src/components/TimelineRouteFrame.tsx +0 -186
- package/src/components/TweetArticleCard.tsx +0 -66
- package/src/components/TweetMediaGrid.tsx +0 -185
- package/src/components/TweetRichText.tsx +0 -159
- package/src/components/account-selection.ts +0 -64
- package/src/components/useTimelineRouteData.ts +0 -323
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/api-client.ts +0 -419
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import.ts +0 -2764
- package/src/lib/authored-live.ts +0 -1072
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2741
- package/src/lib/bird-actions.ts +0 -294
- package/src/lib/bird-command.ts +0 -130
- package/src/lib/bird.ts +0 -1140
- package/src/lib/blocklist.ts +0 -136
- package/src/lib/blocks-write.ts +0 -167
- package/src/lib/blocks.ts +0 -389
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/client-cache.ts +0 -109
- package/src/lib/config.ts +0 -224
- package/src/lib/conversation-surface.ts +0 -205
- package/src/lib/data-sources.ts +0 -219
- package/src/lib/db.ts +0 -1135
- package/src/lib/dms-live.ts +0 -933
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1097
- package/src/lib/geocoding.ts +0 -296
- package/src/lib/http-effect.ts +0 -222
- package/src/lib/identity-search-index.ts +0 -369
- package/src/lib/inbox.ts +0 -241
- package/src/lib/link-index.ts +0 -641
- package/src/lib/link-insights.ts +0 -860
- package/src/lib/link-preview-metadata.ts +0 -754
- package/src/lib/location.ts +0 -137
- package/src/lib/media-fetch.ts +0 -860
- package/src/lib/media-includes.ts +0 -165
- package/src/lib/mention-threads-live.ts +0 -917
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1238
- package/src/lib/moderation-target.ts +0 -216
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -167
- package/src/lib/mutes.ts +0 -84
- package/src/lib/ndjson-stream.ts +0 -106
- package/src/lib/network-map.ts +0 -382
- package/src/lib/openai.ts +0 -117
- package/src/lib/period-digest.ts +0 -1552
- package/src/lib/present.ts +0 -118
- package/src/lib/profile-affiliation-hydration.ts +0 -228
- package/src/lib/profile-affiliations.ts +0 -262
- package/src/lib/profile-analysis.ts +0 -1272
- package/src/lib/profile-bio-entities.ts +0 -318
- package/src/lib/profile-history.ts +0 -164
- package/src/lib/profile-hydration.ts +0 -180
- package/src/lib/profile-replies.ts +0 -77
- package/src/lib/profile-resolver.ts +0 -650
- package/src/lib/queries.ts +0 -2381
- package/src/lib/research.ts +0 -652
- package/src/lib/search-discussion.ts +0 -1016
- package/src/lib/seed.ts +0 -613
- package/src/lib/sqlite.ts +0 -147
- package/src/lib/sync-cache.ts +0 -65
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -581
- package/src/lib/timeline-live.ts +0 -452
- package/src/lib/tweet-account-edges.ts +0 -48
- package/src/lib/tweet-lookup.ts +0 -46
- package/src/lib/tweet-render.ts +0 -430
- package/src/lib/tweet-search-live.ts +0 -566
- package/src/lib/types.ts +0 -723
- package/src/lib/ui.ts +0 -529
- package/src/lib/url-expansion-store.ts +0 -110
- package/src/lib/url-expansion.ts +0 -343
- package/src/lib/url-safety.ts +0 -220
- package/src/lib/web-sync.ts +0 -516
- package/src/lib/whois.ts +0 -1079
- package/src/lib/x-profile.ts +0 -365
- package/src/lib/x-web.ts +0 -199
- package/src/lib/xurl-rate-limits.ts +0 -267
- package/src/lib/xurl.ts +0 -1677
- package/src/routeTree.gen.ts +0 -723
- package/src/router.tsx +0 -20
- package/src/routes/__root.tsx +0 -79
- package/src/routes/api/action.tsx +0 -147
- package/src/routes/api/avatar.tsx +0 -50
- package/src/routes/api/blocks.tsx +0 -35
- package/src/routes/api/conversation.tsx +0 -45
- package/src/routes/api/data-sources.tsx +0 -24
- package/src/routes/api/inbox.tsx +0 -43
- package/src/routes/api/link-insights.tsx +0 -85
- package/src/routes/api/link-preview.tsx +0 -47
- package/src/routes/api/network-map.tsx +0 -55
- package/src/routes/api/period-digest.tsx +0 -101
- package/src/routes/api/profile-analysis.tsx +0 -97
- package/src/routes/api/profile-hydrate.tsx +0 -54
- package/src/routes/api/query.tsx +0 -115
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -28
- package/src/routes/api/sync.tsx +0 -105
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -377
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -257
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -432
- package/src/routes/inbox.tsx +0 -226
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -994
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -1035
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -309
- package/src/routes/today.tsx +0 -511
- package/src/styles.css +0 -257
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -29
- /package/{public → dist/client}/birdclaw-mark.png +0 -0
- /package/{public → dist/client}/favicon.ico +0 -0
- /package/{public → dist/client}/logo192.png +0 -0
- /package/{public → dist/client}/logo512.png +0 -0
- /package/{public → dist/client}/manifest.json +0 -0
- /package/{public → dist/client}/robots.txt +0 -0
|
@@ -1,2764 +0,0 @@
|
|
|
1
|
-
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
createWriteStream,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
renameSync,
|
|
8
|
-
statSync,
|
|
9
|
-
unlinkSync,
|
|
10
|
-
} from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { pipeline } from "node:stream/promises";
|
|
13
|
-
import { promisify } from "node:util";
|
|
14
|
-
import { Effect } from "effect";
|
|
15
|
-
import { getBirdclawPaths } from "./config";
|
|
16
|
-
import { getNativeDb } from "./db";
|
|
17
|
-
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
18
|
-
import { safeHttpUrl } from "./url-safety";
|
|
19
|
-
|
|
20
|
-
const execFileAsync = promisify(execFile);
|
|
21
|
-
const ARCHIVE_JSON_PAYLOAD = /=\s*(\[[\s\S]*\]|\{[\s\S]*\})/s;
|
|
22
|
-
|
|
23
|
-
interface ArchiveAccountPayload {
|
|
24
|
-
accountId: string;
|
|
25
|
-
username: string;
|
|
26
|
-
displayName: string;
|
|
27
|
-
createdAt: string;
|
|
28
|
-
bio: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface ImportedArchiveSummary {
|
|
32
|
-
ok: true;
|
|
33
|
-
archivePath: string;
|
|
34
|
-
account: {
|
|
35
|
-
id: string;
|
|
36
|
-
handle: string;
|
|
37
|
-
displayName: string;
|
|
38
|
-
};
|
|
39
|
-
counts: {
|
|
40
|
-
tweets: number;
|
|
41
|
-
likes: number;
|
|
42
|
-
bookmarks: number;
|
|
43
|
-
dmConversations: number;
|
|
44
|
-
dmMessages: number;
|
|
45
|
-
profiles: number;
|
|
46
|
-
mediaFiles: ArchiveMediaFileCounts;
|
|
47
|
-
followers: number;
|
|
48
|
-
following: number;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const ARCHIVE_IMPORT_SLICES = [
|
|
53
|
-
"tweets",
|
|
54
|
-
"likes",
|
|
55
|
-
"bookmarks",
|
|
56
|
-
"directMessages",
|
|
57
|
-
"profiles",
|
|
58
|
-
"followers",
|
|
59
|
-
"following",
|
|
60
|
-
] as const;
|
|
61
|
-
|
|
62
|
-
export type ArchiveImportSlice = (typeof ARCHIVE_IMPORT_SLICES)[number];
|
|
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
|
-
|
|
100
|
-
export interface ImportArchiveOptions {
|
|
101
|
-
select?: ArchiveImportSlice[];
|
|
102
|
-
onProgress?: (event: ImportProgressEvent) => void;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
type ArchiveRecord = Record<string, unknown>;
|
|
106
|
-
type ArchiveMediaKind =
|
|
107
|
-
| "tweets"
|
|
108
|
-
| "dms"
|
|
109
|
-
| "community"
|
|
110
|
-
| "profile"
|
|
111
|
-
| "deleted"
|
|
112
|
-
| "moments"
|
|
113
|
-
| "dmGroup";
|
|
114
|
-
type ArchiveMediaFileCounts = Record<ArchiveMediaKind, number>;
|
|
115
|
-
|
|
116
|
-
const ARCHIVE_MEDIA_DIRECTORIES: Array<{
|
|
117
|
-
directory: string;
|
|
118
|
-
kind: ArchiveMediaKind;
|
|
119
|
-
}> = [
|
|
120
|
-
{ directory: "tweets_media", kind: "tweets" },
|
|
121
|
-
{ directory: "direct_messages_media", kind: "dms" },
|
|
122
|
-
{ directory: "community_tweet_media", kind: "community" },
|
|
123
|
-
{ directory: "deleted_tweets_media", kind: "deleted" },
|
|
124
|
-
{ directory: "profile_media", kind: "profile" },
|
|
125
|
-
{ directory: "moments_tweets_media", kind: "moments" },
|
|
126
|
-
{ directory: "direct_messages_group_media", kind: "dmGroup" },
|
|
127
|
-
];
|
|
128
|
-
type ArchiveFollowDirection = "followers" | "following";
|
|
129
|
-
type ArchiveFollowKey = "follower" | "following";
|
|
130
|
-
|
|
131
|
-
function normalizeArchivePath(value: string) {
|
|
132
|
-
return value.replaceAll("\\", "/");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function extractArchiveJson(content: string): unknown {
|
|
136
|
-
const match = ARCHIVE_JSON_PAYLOAD.exec(content);
|
|
137
|
-
if (!match) {
|
|
138
|
-
return [];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return JSON.parse(match[1]);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function parseArchiveArray(content: string): ArchiveRecord[] {
|
|
145
|
-
const parsed = extractArchiveJson(content);
|
|
146
|
-
return Array.isArray(parsed)
|
|
147
|
-
? parsed.filter((item): item is ArchiveRecord => Boolean(item))
|
|
148
|
-
: [];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function runUnzipEffect(
|
|
152
|
-
_archivePath: string,
|
|
153
|
-
args: string[],
|
|
154
|
-
maxBuffer = 1024 * 1024 * 256,
|
|
155
|
-
) {
|
|
156
|
-
return tryPromise(() =>
|
|
157
|
-
execFileAsync("unzip", args, {
|
|
158
|
-
maxBuffer,
|
|
159
|
-
}),
|
|
160
|
-
).pipe(Effect.map(({ stdout }) => stdout));
|
|
161
|
-
}
|
|
162
|
-
|
|
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
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
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
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function readArchiveEntryEffect(
|
|
201
|
-
archivePath: string,
|
|
202
|
-
entryPath: string,
|
|
203
|
-
): Effect.Effect<string, unknown> {
|
|
204
|
-
return runUnzipEffect(archivePath, ["-p", archivePath, entryPath]);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function getFirstEntry(entries: string[], pattern: RegExp) {
|
|
208
|
-
return entries.find((entry) => pattern.test(normalizeArchivePath(entry)));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function getMatchingEntries(entries: string[], pattern: RegExp) {
|
|
212
|
-
return entries.filter((entry) => pattern.test(normalizeArchivePath(entry)));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function selectedSlices(options: ImportArchiveOptions) {
|
|
216
|
-
return options.select && options.select.length > 0
|
|
217
|
-
? new Set<ArchiveImportSlice>(options.select)
|
|
218
|
-
: null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function includesSlice(
|
|
222
|
-
selection: Set<ArchiveImportSlice> | null,
|
|
223
|
-
slice: ArchiveImportSlice,
|
|
224
|
-
) {
|
|
225
|
-
return selection === null || selection.has(slice);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function parseTwitterDate(value: unknown) {
|
|
229
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
230
|
-
return new Date(0).toISOString();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const parsed = new Date(value);
|
|
234
|
-
return Number.isNaN(parsed.getTime())
|
|
235
|
-
? new Date(0).toISOString()
|
|
236
|
-
: parsed.toISOString();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function compareIsoTimestamp(left: string, right: string) {
|
|
240
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
244
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
245
|
-
? (value as Record<string, unknown>)
|
|
246
|
-
: null;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function asArray<T>(value: unknown): T[] {
|
|
250
|
-
return Array.isArray(value) ? (value as T[]) : [];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function toInt(value: unknown) {
|
|
254
|
-
const parsed = Number(value);
|
|
255
|
-
return Number.isFinite(parsed) ? Math.trunc(parsed) : 0;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function getTweetMediaCount(tweet: Record<string, unknown>) {
|
|
259
|
-
const entities = asRecord(tweet.entities);
|
|
260
|
-
const extendedEntities = asRecord(tweet.extended_entities);
|
|
261
|
-
const entitiesMedia = asArray(entities?.media);
|
|
262
|
-
const extendedMedia = asArray(extendedEntities?.media);
|
|
263
|
-
return Math.max(entitiesMedia.length, extendedMedia.length);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function toFiniteNumber(value: unknown) {
|
|
267
|
-
const number = Number(value);
|
|
268
|
-
return Number.isFinite(number) ? number : undefined;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function archiveHttpUrl(value: unknown) {
|
|
272
|
-
return safeHttpUrl(typeof value === "string" ? value : String(value ?? ""));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function extractTweetEntities(tweet: Record<string, unknown>) {
|
|
276
|
-
const entities = asRecord(tweet.entities);
|
|
277
|
-
const urlEntries = [
|
|
278
|
-
...asArray<Record<string, unknown>>(entities?.urls),
|
|
279
|
-
...asArray<Record<string, unknown>>(entities?.media),
|
|
280
|
-
];
|
|
281
|
-
const seenUrls = new Set<string>();
|
|
282
|
-
const urls = urlEntries
|
|
283
|
-
.map((entry) => ({
|
|
284
|
-
url: archiveHttpUrl(entry.url) ?? "",
|
|
285
|
-
expandedUrl:
|
|
286
|
-
archiveHttpUrl(entry.expanded_url ?? entry.expandedUrl ?? entry.url) ??
|
|
287
|
-
"",
|
|
288
|
-
displayUrl: String(
|
|
289
|
-
entry.display_url ??
|
|
290
|
-
entry.displayUrl ??
|
|
291
|
-
entry.expanded_url ??
|
|
292
|
-
entry.url ??
|
|
293
|
-
"",
|
|
294
|
-
),
|
|
295
|
-
start: Number(asArray<number>(entry.indices)[0] ?? 0),
|
|
296
|
-
end: Number(asArray<number>(entry.indices)[1] ?? 0),
|
|
297
|
-
title: typeof entry.title === "string" ? entry.title : undefined,
|
|
298
|
-
description:
|
|
299
|
-
typeof entry.description === "string" ? entry.description : null,
|
|
300
|
-
imageUrl:
|
|
301
|
-
archiveHttpUrl(
|
|
302
|
-
entry.image_url ??
|
|
303
|
-
entry.imageUrl ??
|
|
304
|
-
entry.thumbnail_url ??
|
|
305
|
-
entry.media_url_https ??
|
|
306
|
-
entry.media_url,
|
|
307
|
-
) ?? undefined,
|
|
308
|
-
siteName:
|
|
309
|
-
typeof entry.site_name === "string"
|
|
310
|
-
? entry.site_name
|
|
311
|
-
: typeof entry.siteName === "string"
|
|
312
|
-
? entry.siteName
|
|
313
|
-
: undefined,
|
|
314
|
-
}))
|
|
315
|
-
.filter((entry) => entry.url.length > 0 || entry.expandedUrl.length > 0)
|
|
316
|
-
.filter((entry) => {
|
|
317
|
-
const key = `${entry.start}:${entry.end}:${entry.url}:${entry.expandedUrl}`;
|
|
318
|
-
if (seenUrls.has(key)) return false;
|
|
319
|
-
seenUrls.add(key);
|
|
320
|
-
return true;
|
|
321
|
-
});
|
|
322
|
-
const mentions = asArray<Record<string, unknown>>(entities?.user_mentions)
|
|
323
|
-
.map((entry) => ({
|
|
324
|
-
username: String(entry.screen_name ?? ""),
|
|
325
|
-
id: String(entry.id_str ?? entry.id ?? ""),
|
|
326
|
-
start: Number(asArray<number>(entry.indices)[0] ?? 0),
|
|
327
|
-
end: Number(asArray<number>(entry.indices)[1] ?? 0),
|
|
328
|
-
}))
|
|
329
|
-
.filter((entry) => entry.username.length > 0);
|
|
330
|
-
const hashtags = asArray<Record<string, unknown>>(entities?.hashtags)
|
|
331
|
-
.map((entry) => ({
|
|
332
|
-
tag: String(entry.text ?? ""),
|
|
333
|
-
start: Number(asArray<number>(entry.indices)[0] ?? 0),
|
|
334
|
-
end: Number(asArray<number>(entry.indices)[1] ?? 0),
|
|
335
|
-
}))
|
|
336
|
-
.filter((entry) => entry.tag.length > 0);
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
...(urls.length > 0 ? { urls } : {}),
|
|
340
|
-
...(mentions.length > 0 ? { mentions } : {}),
|
|
341
|
-
...(hashtags.length > 0 ? { hashtags } : {}),
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function archiveMediaType(value: unknown) {
|
|
346
|
-
const type = String(value ?? "image");
|
|
347
|
-
return type === "photo"
|
|
348
|
-
? "image"
|
|
349
|
-
: type === "video" || type === "animated_gif"
|
|
350
|
-
? type === "animated_gif"
|
|
351
|
-
? "gif"
|
|
352
|
-
: "video"
|
|
353
|
-
: "unknown";
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function archiveMediaSize(entry: Record<string, unknown>) {
|
|
357
|
-
const sizes = asRecord(entry.sizes);
|
|
358
|
-
const large = asRecord(sizes?.large);
|
|
359
|
-
const largeWidth = toFiniteNumber(large?.w ?? large?.width);
|
|
360
|
-
const largeHeight = toFiniteNumber(large?.h ?? large?.height);
|
|
361
|
-
if (largeWidth !== undefined && largeHeight !== undefined) {
|
|
362
|
-
return { width: largeWidth, height: largeHeight };
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return Object.values(sizes ?? {})
|
|
366
|
-
.map((size) => asRecord(size))
|
|
367
|
-
.map((size) => ({
|
|
368
|
-
width: toFiniteNumber(size?.w ?? size?.width),
|
|
369
|
-
height: toFiniteNumber(size?.h ?? size?.height),
|
|
370
|
-
}))
|
|
371
|
-
.filter(
|
|
372
|
-
(size): size is { width: number; height: number } =>
|
|
373
|
-
size.width !== undefined && size.height !== undefined,
|
|
374
|
-
)
|
|
375
|
-
.sort(
|
|
376
|
-
(left, right) => right.width * right.height - left.width * left.height,
|
|
377
|
-
)[0];
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function archiveMp4Variants(entry: Record<string, unknown>) {
|
|
381
|
-
const videoInfo = asRecord(entry.video_info);
|
|
382
|
-
return asArray<Record<string, unknown>>(videoInfo?.variants)
|
|
383
|
-
.filter(
|
|
384
|
-
(variant) =>
|
|
385
|
-
variant.content_type === "video/mp4" && typeof variant.url === "string",
|
|
386
|
-
)
|
|
387
|
-
.map((variant) => {
|
|
388
|
-
const bitRate = toFiniteNumber(variant.bitrate ?? variant.bit_rate);
|
|
389
|
-
return {
|
|
390
|
-
url: String(variant.url),
|
|
391
|
-
contentType: String(variant.content_type),
|
|
392
|
-
...(bitRate !== undefined ? { bitRate } : {}),
|
|
393
|
-
};
|
|
394
|
-
})
|
|
395
|
-
.sort(
|
|
396
|
-
(left, right) => Number(right.bitRate ?? 0) - Number(left.bitRate ?? 0),
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function extractTweetMedia(tweet: Record<string, unknown>) {
|
|
401
|
-
const extendedEntities = asRecord(tweet.extended_entities);
|
|
402
|
-
const entities = asRecord(tweet.entities);
|
|
403
|
-
const sourceMedia = [
|
|
404
|
-
...asArray<Record<string, unknown>>(extendedEntities?.media),
|
|
405
|
-
...asArray<Record<string, unknown>>(entities?.media),
|
|
406
|
-
];
|
|
407
|
-
const seen = new Set<string>();
|
|
408
|
-
|
|
409
|
-
return sourceMedia
|
|
410
|
-
.map((entry) => {
|
|
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;
|
|
416
|
-
const videoInfo = asRecord(entry.video_info);
|
|
417
|
-
const durationMs = toFiniteNumber(videoInfo?.duration_millis);
|
|
418
|
-
const variants = archiveMp4Variants(entry);
|
|
419
|
-
return {
|
|
420
|
-
url,
|
|
421
|
-
type: archiveMediaType(entry.type),
|
|
422
|
-
altText:
|
|
423
|
-
typeof entry.ext_alt_text === "string"
|
|
424
|
-
? entry.ext_alt_text
|
|
425
|
-
: undefined,
|
|
426
|
-
thumbnailUrl,
|
|
427
|
-
...archiveMediaSize(entry),
|
|
428
|
-
...(durationMs !== undefined ? { durationMs } : {}),
|
|
429
|
-
...(variants.length > 0 ? { variants } : {}),
|
|
430
|
-
};
|
|
431
|
-
})
|
|
432
|
-
.filter((entry) => {
|
|
433
|
-
if (!entry.url || seen.has(entry.url)) {
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
seen.add(entry.url);
|
|
437
|
-
return true;
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function extractCollectionTweet(
|
|
442
|
-
wrapper: ArchiveRecord,
|
|
443
|
-
key: "like" | "bookmark",
|
|
444
|
-
) {
|
|
445
|
-
const entry = asRecord(wrapper[key]) ?? asRecord(wrapper.tweet);
|
|
446
|
-
if (!entry) return null;
|
|
447
|
-
|
|
448
|
-
const id = String(
|
|
449
|
-
entry.tweetId ?? entry.tweet_id ?? entry.id_str ?? entry.id ?? "",
|
|
450
|
-
);
|
|
451
|
-
if (!id) return null;
|
|
452
|
-
|
|
453
|
-
return {
|
|
454
|
-
id,
|
|
455
|
-
text: String(
|
|
456
|
-
entry.fullText ??
|
|
457
|
-
entry.full_text ??
|
|
458
|
-
entry.text ??
|
|
459
|
-
entry.expandedUrl ??
|
|
460
|
-
entry.expanded_url ??
|
|
461
|
-
"",
|
|
462
|
-
),
|
|
463
|
-
createdAt: parseTwitterDate(
|
|
464
|
-
entry.likedAt ??
|
|
465
|
-
entry.bookmarkedAt ??
|
|
466
|
-
entry.createdAt ??
|
|
467
|
-
entry.created_at ??
|
|
468
|
-
new Date(0).toISOString(),
|
|
469
|
-
),
|
|
470
|
-
likeCount: toInt(entry.favorite_count ?? entry.like_count),
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function buildAccountPayload(
|
|
475
|
-
accountRecord: Record<string, unknown> | null,
|
|
476
|
-
profileRecord: Record<string, unknown> | null,
|
|
477
|
-
): ArchiveAccountPayload {
|
|
478
|
-
const account = asRecord(accountRecord?.account);
|
|
479
|
-
const profile = asRecord(profileRecord?.profile);
|
|
480
|
-
const description = asRecord(profile?.description);
|
|
481
|
-
|
|
482
|
-
return {
|
|
483
|
-
accountId: String(account?.accountId ?? "unknown"),
|
|
484
|
-
username: String(account?.username ?? "unknown"),
|
|
485
|
-
displayName: String(
|
|
486
|
-
account?.accountDisplayName ??
|
|
487
|
-
account?.name ??
|
|
488
|
-
account?.username ??
|
|
489
|
-
"Unknown",
|
|
490
|
-
),
|
|
491
|
-
createdAt: parseTwitterDate(account?.createdAt),
|
|
492
|
-
bio: String(description?.bio ?? ""),
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function inferProfileFromDirectory(
|
|
497
|
-
userId: string,
|
|
498
|
-
directory: Map<string, { handle?: string; displayName?: string }>,
|
|
499
|
-
) {
|
|
500
|
-
const match = directory.get(userId);
|
|
501
|
-
const handle = match?.handle?.replace(/^@/, "") || `id${userId}`;
|
|
502
|
-
const displayName = match?.displayName || handle;
|
|
503
|
-
return { handle, displayName };
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function createArchiveMediaFileCounts(): ArchiveMediaFileCounts {
|
|
507
|
-
return {
|
|
508
|
-
tweets: 0,
|
|
509
|
-
dms: 0,
|
|
510
|
-
community: 0,
|
|
511
|
-
profile: 0,
|
|
512
|
-
deleted: 0,
|
|
513
|
-
moments: 0,
|
|
514
|
-
dmGroup: 0,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function selectedArchiveMediaKinds(selection: Set<ArchiveImportSlice> | null) {
|
|
519
|
-
if (!selection) return null;
|
|
520
|
-
const kinds = new Set<ArchiveMediaKind>();
|
|
521
|
-
if (selection.has("tweets")) {
|
|
522
|
-
for (const kind of ["tweets", "community", "deleted", "moments"] as const) {
|
|
523
|
-
kinds.add(kind);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (selection.has("directMessages")) {
|
|
527
|
-
for (const kind of ["dms", "dmGroup"] as const) {
|
|
528
|
-
kinds.add(kind);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
if (selection.has("profiles")) {
|
|
532
|
-
kinds.add("profile");
|
|
533
|
-
}
|
|
534
|
-
return kinds;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function getArchiveMediaKind(entryPath: string) {
|
|
538
|
-
const normalized = normalizeArchivePath(entryPath);
|
|
539
|
-
if (normalized.endsWith("/")) return undefined;
|
|
540
|
-
return ARCHIVE_MEDIA_DIRECTORIES.find(({ directory }) =>
|
|
541
|
-
new RegExp(`(?:^|/)data/${directory}/[^/]+$`).test(normalized),
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function getArchiveMediaOwnerId(entryPath: string) {
|
|
546
|
-
const fileName = path.posix.basename(normalizeArchivePath(entryPath));
|
|
547
|
-
const separator = fileName.indexOf("-");
|
|
548
|
-
return separator > 0 ? fileName.slice(0, separator) : "unknown";
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function getArchiveMediaDestination(entryPath: string, kind: ArchiveMediaKind) {
|
|
552
|
-
const { mediaOriginalsDir } = getBirdclawPaths();
|
|
553
|
-
const normalized = normalizeArchivePath(entryPath);
|
|
554
|
-
const fileName = path.posix.basename(normalized);
|
|
555
|
-
return path.join(
|
|
556
|
-
mediaOriginalsDir,
|
|
557
|
-
"archive",
|
|
558
|
-
kind,
|
|
559
|
-
getArchiveMediaOwnerId(normalized),
|
|
560
|
-
fileName,
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function needsArchiveMediaCopy(destinationPath: string, size: number) {
|
|
565
|
-
if (!existsSync(destinationPath)) return true;
|
|
566
|
-
return statSync(destinationPath).size !== size;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function copyArchiveEntryToFileEffect(
|
|
570
|
-
archivePath: string,
|
|
571
|
-
entryPath: string,
|
|
572
|
-
destinationPath: string,
|
|
573
|
-
) {
|
|
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
|
-
});
|
|
589
|
-
|
|
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
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function extractArchiveMediaFilesEffect(
|
|
613
|
-
archivePath: string,
|
|
614
|
-
selectedKinds: Set<ArchiveMediaKind> | null,
|
|
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
|
-
}
|
|
639
|
-
return counts;
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function getArchiveFollowRows(content: string, key: ArchiveFollowKey) {
|
|
644
|
-
const rows: Array<{ profileId: string; externalUserId: string }> = [];
|
|
645
|
-
|
|
646
|
-
for (const wrapper of parseArchiveArray(content)) {
|
|
647
|
-
const item = asRecord(wrapper[key]);
|
|
648
|
-
const externalUserId = String(item?.accountId ?? "");
|
|
649
|
-
if (!externalUserId) continue;
|
|
650
|
-
|
|
651
|
-
rows.push({
|
|
652
|
-
profileId: `profile_user_${externalUserId}`,
|
|
653
|
-
externalUserId,
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return rows;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function clearImportedData(db = getNativeDb()) {
|
|
661
|
-
db.exec(`
|
|
662
|
-
delete from ai_scores;
|
|
663
|
-
delete from tweet_actions;
|
|
664
|
-
delete from tweet_account_edges;
|
|
665
|
-
delete from tweet_collections;
|
|
666
|
-
delete from link_occurrences;
|
|
667
|
-
delete from url_expansions;
|
|
668
|
-
delete from dm_fts;
|
|
669
|
-
delete from tweets_fts;
|
|
670
|
-
delete from dm_messages;
|
|
671
|
-
delete from dm_conversations;
|
|
672
|
-
delete from tweets;
|
|
673
|
-
delete from profiles;
|
|
674
|
-
delete from accounts;
|
|
675
|
-
`);
|
|
676
|
-
clearAuthoredSyncCursors(db);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function clearAuthoredSyncCursors(db = getNativeDb(), accountId?: string) {
|
|
680
|
-
if (accountId) {
|
|
681
|
-
db.prepare("delete from sync_cache where cache_key = ?").run(
|
|
682
|
-
`authored:xurl:${accountId}:cursor`,
|
|
683
|
-
);
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
db.prepare(
|
|
687
|
-
"delete from sync_cache where cache_key like 'authored:xurl:%:cursor'",
|
|
688
|
-
).run();
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function clearMentionSyncState(db = getNativeDb()) {
|
|
692
|
-
db.prepare(
|
|
693
|
-
"delete from sync_cache where cache_key like 'mentions:sync:%'",
|
|
694
|
-
).run();
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function importArchiveInternalEffect(
|
|
698
|
-
archivePath: string,
|
|
699
|
-
options: ImportArchiveOptions = {},
|
|
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
|
-
: [];
|
|
754
|
-
|
|
755
|
-
if (!accountEntry) {
|
|
756
|
-
return yield* Effect.fail(new Error("Archive missing data/account.js"));
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const [accountContent, profileContent] = yield* Effect.all([
|
|
760
|
-
readArchiveEntryEffect(archivePath, accountEntry),
|
|
761
|
-
profileEntry
|
|
762
|
-
? readArchiveEntryEffect(archivePath, profileEntry)
|
|
763
|
-
: Effect.succeed("[]"),
|
|
764
|
-
]);
|
|
765
|
-
|
|
766
|
-
const accountPayload = buildAccountPayload(
|
|
767
|
-
parseArchiveArray(accountContent)[0] ?? null,
|
|
768
|
-
parseArchiveArray(profileContent)[0] ?? null,
|
|
769
|
-
);
|
|
770
|
-
|
|
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);
|
|
810
|
-
}
|
|
811
|
-
|
|
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,
|
|
868
|
-
});
|
|
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
|
-
}
|
|
884
|
-
|
|
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,
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
onProgress({
|
|
917
|
-
kind: "slice-file",
|
|
918
|
-
slice: "noteTweets",
|
|
919
|
-
processed: noteFileIndex + 1,
|
|
920
|
-
files: noteTweetEntries.length,
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
if (noteTweetEntries.length > 0) {
|
|
924
|
-
onProgress({
|
|
925
|
-
kind: "slice-done",
|
|
926
|
-
slice: "noteTweets",
|
|
927
|
-
count: tweetRows.length - tweetRowsBeforeNotes,
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
const authoredTweetCount = tweetRows.length;
|
|
931
|
-
|
|
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
|
-
};
|
|
941
|
-
|
|
942
|
-
const profiles = new Map<
|
|
943
|
-
string,
|
|
944
|
-
{
|
|
945
|
-
id: string;
|
|
946
|
-
handle: string;
|
|
947
|
-
displayName: string;
|
|
948
|
-
bio: string;
|
|
949
|
-
followersCount: number;
|
|
950
|
-
followingCount: number;
|
|
951
|
-
publicMetricsJson: string;
|
|
952
|
-
avatarHue: number;
|
|
953
|
-
avatarUrl: string | null;
|
|
954
|
-
location: string | null;
|
|
955
|
-
url: string | null;
|
|
956
|
-
verifiedType: string | null;
|
|
957
|
-
entitiesJson: string;
|
|
958
|
-
rawJson: string;
|
|
959
|
-
createdAt: string;
|
|
960
|
-
}
|
|
961
|
-
>();
|
|
962
|
-
type ProfileRow =
|
|
963
|
-
typeof profiles extends Map<string, infer Value> ? Value : never;
|
|
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 = {
|
|
992
|
-
id: string;
|
|
993
|
-
handle: string;
|
|
994
|
-
display_name: string;
|
|
995
|
-
bio: string;
|
|
996
|
-
followers_count: number;
|
|
997
|
-
following_count: number;
|
|
998
|
-
public_metrics_json: string;
|
|
999
|
-
avatar_hue: number;
|
|
1000
|
-
avatar_url: string | null;
|
|
1001
|
-
location: string | null;
|
|
1002
|
-
url: string | null;
|
|
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
|
-
`
|
|
1013
|
-
select id, handle, display_name, bio, followers_count, following_count,
|
|
1014
|
-
public_metrics_json, avatar_hue, avatar_url, location, url,
|
|
1015
|
-
verified_type, entities_json, raw_json, created_at
|
|
1016
|
-
from profiles
|
|
1017
|
-
`,
|
|
1018
|
-
)
|
|
1019
|
-
.all() as ExistingProfileRow[]
|
|
1020
|
-
).map((profile) => [profile.id, profile]),
|
|
1021
|
-
);
|
|
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,
|
|
1045
|
-
};
|
|
1046
|
-
|
|
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";
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function shouldPreserveProfile(
|
|
1085
|
-
existingTier: ArchiveProfileTier,
|
|
1086
|
-
incomingTier: ArchiveProfileTier,
|
|
1087
|
-
) {
|
|
1088
|
-
return (
|
|
1089
|
-
archiveProfileTierRank[existingTier] >=
|
|
1090
|
-
archiveProfileTierRank[incomingTier]
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
1093
|
-
|
|
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
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
|
|
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;
|
|
1137
|
-
|
|
1138
|
-
if (
|
|
1139
|
-
current &&
|
|
1140
|
-
currentTier &&
|
|
1141
|
-
shouldPreserveProfile(currentTier, incomingTier) &&
|
|
1142
|
-
(!existingTier || shouldPreserveProfile(currentTier, existingTier))
|
|
1143
|
-
) {
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
if (
|
|
1148
|
-
existingProfile &&
|
|
1149
|
-
existingTier &&
|
|
1150
|
-
shouldPreserveProfile(existingTier, incomingTier)
|
|
1151
|
-
) {
|
|
1152
|
-
profiles.set(targetId, existingProfile);
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
profiles.set(targetId, targetIncoming);
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
function resolveProfileId(profileId: string) {
|
|
1160
|
-
return profileIdAliases.get(profileId) ?? profileId;
|
|
1161
|
-
}
|
|
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
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
return false;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
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
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
|
|
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: "",
|
|
1205
|
-
followersCount: 0,
|
|
1206
|
-
followingCount: 0,
|
|
1207
|
-
...defaultProfileMetadata,
|
|
1208
|
-
avatarHue: 210,
|
|
1209
|
-
avatarUrl: null,
|
|
1210
|
-
createdAt: accountPayload.createdAt,
|
|
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
|
-
`
|
|
1286
|
-
select m.id
|
|
1287
|
-
from dm_messages m
|
|
1288
|
-
join dm_conversations c on c.id = m.conversation_id
|
|
1289
|
-
where c.account_id <> 'acct_primary'
|
|
1290
|
-
`,
|
|
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;
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
|
|
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
|
-
}
|
|
1321
|
-
|
|
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
|
-
}
|
|
1336
|
-
|
|
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
|
-
}
|
|
1357
|
-
|
|
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
|
-
);
|
|
1382
|
-
|
|
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
|
-
}
|
|
1391
|
-
|
|
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
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
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
|
-
}
|
|
1412
|
-
}
|
|
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
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
|
|
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
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
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
|
-
}
|
|
1506
|
-
|
|
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),
|
|
1523
|
-
);
|
|
1524
|
-
|
|
1525
|
-
if (messageEvents.length === 0) {
|
|
1526
|
-
continue;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
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 ||
|
|
1540
|
-
conversationId,
|
|
1541
|
-
accountId: "acct_primary",
|
|
1542
|
-
participantProfileId: resolvedParticipantProfileId,
|
|
1543
|
-
lastMessageAt: lastMessage.createdAt,
|
|
1544
|
-
unreadCount: 0,
|
|
1545
|
-
needsReply: lastMessage.direction === "inbound" ? 1 : 0,
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
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,
|
|
1560
|
-
});
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
if (likeEntries.length > 0) {
|
|
1564
|
-
onProgress({
|
|
1565
|
-
kind: "slice-start",
|
|
1566
|
-
slice: "likes",
|
|
1567
|
-
files: likeEntries.length,
|
|
1568
|
-
});
|
|
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,
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
if (likeEntries.length > 0) {
|
|
1610
|
-
onProgress({ kind: "slice-done", slice: "likes", count: likeCount });
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
if (bookmarkEntries.length > 0) {
|
|
1614
|
-
onProgress({
|
|
1615
|
-
kind: "slice-start",
|
|
1616
|
-
slice: "bookmarks",
|
|
1617
|
-
files: bookmarkEntries.length,
|
|
1618
|
-
});
|
|
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,
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
|
|
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
|
-
});
|
|
1680
|
-
|
|
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
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
|
|
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
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
for (const row of [...followerRows, ...followingRows]) {
|
|
1740
|
-
addArchiveFollowProfile(row.profileId, row.externalUserId);
|
|
1741
|
-
}
|
|
1742
|
-
|
|
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
|
-
`
|
|
1753
|
-
select direction, profile_id, external_user_id, source, null as snapshot_id, null as snapshot_source
|
|
1754
|
-
from follow_edges
|
|
1755
|
-
union
|
|
1756
|
-
select ev.direction, ev.profile_id, ev.external_user_id, null as source, ev.snapshot_id, snap.source as snapshot_source
|
|
1757
|
-
from follow_events ev
|
|
1758
|
-
left join follow_snapshots snap on snap.id = ev.snapshot_id
|
|
1759
|
-
`,
|
|
1760
|
-
)
|
|
1761
|
-
.all() as Array<{
|
|
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
|
-
}
|
|
1779
|
-
|
|
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
|
-
}
|
|
1805
|
-
|
|
1806
|
-
const db = getNativeDb();
|
|
1807
|
-
const insertAccount = db.prepare(`
|
|
1808
|
-
insert into accounts (id, name, handle, external_user_id, transport, is_default, created_at)
|
|
1809
|
-
values (?, ?, ?, ?, ?, 1, ?)
|
|
1810
|
-
on conflict(id) do update set
|
|
1811
|
-
name = excluded.name,
|
|
1812
|
-
handle = excluded.handle,
|
|
1813
|
-
external_user_id = excluded.external_user_id,
|
|
1814
|
-
transport = excluded.transport,
|
|
1815
|
-
is_default = 1,
|
|
1816
|
-
created_at = excluded.created_at
|
|
1817
|
-
`);
|
|
1818
|
-
const insertAccountIfMissing = db.prepare(`
|
|
1819
|
-
insert or ignore into accounts (id, name, handle, external_user_id, transport, is_default, created_at)
|
|
1820
|
-
values (?, ?, ?, ?, ?, 1, ?)
|
|
1821
|
-
`);
|
|
1822
|
-
const insertProfile = db.prepare(`
|
|
1823
|
-
insert into profiles (
|
|
1824
|
-
id, handle, display_name, bio, followers_count, following_count,
|
|
1825
|
-
public_metrics_json, avatar_hue, avatar_url, location, url, verified_type,
|
|
1826
|
-
entities_json, raw_json, created_at
|
|
1827
|
-
)
|
|
1828
|
-
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1829
|
-
on conflict(id) do update set
|
|
1830
|
-
handle = excluded.handle,
|
|
1831
|
-
display_name = excluded.display_name,
|
|
1832
|
-
bio = excluded.bio,
|
|
1833
|
-
followers_count = excluded.followers_count,
|
|
1834
|
-
following_count = excluded.following_count,
|
|
1835
|
-
public_metrics_json = excluded.public_metrics_json,
|
|
1836
|
-
avatar_hue = excluded.avatar_hue,
|
|
1837
|
-
avatar_url = excluded.avatar_url,
|
|
1838
|
-
location = excluded.location,
|
|
1839
|
-
url = excluded.url,
|
|
1840
|
-
verified_type = excluded.verified_type,
|
|
1841
|
-
entities_json = excluded.entities_json,
|
|
1842
|
-
raw_json = excluded.raw_json,
|
|
1843
|
-
created_at = excluded.created_at
|
|
1844
|
-
`);
|
|
1845
|
-
const insertProfileIfMissing = db.prepare(`
|
|
1846
|
-
insert or ignore into profiles (
|
|
1847
|
-
id, handle, display_name, bio, followers_count, following_count,
|
|
1848
|
-
public_metrics_json, avatar_hue, avatar_url, location, url, verified_type,
|
|
1849
|
-
entities_json, raw_json, created_at
|
|
1850
|
-
)
|
|
1851
|
-
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1852
|
-
`);
|
|
1853
|
-
const insertTweet = db.prepare(`
|
|
1854
|
-
insert into tweets (
|
|
1855
|
-
id, account_id, author_profile_id, kind, text, created_at, is_replied,
|
|
1856
|
-
reply_to_id, like_count, media_count, bookmarked, liked, entities_json, media_json, quoted_tweet_id
|
|
1857
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1858
|
-
on conflict(id) do update set
|
|
1859
|
-
account_id = tweets.account_id,
|
|
1860
|
-
author_profile_id = case
|
|
1861
|
-
when tweets.author_profile_id = 'profile_unknown' then excluded.author_profile_id
|
|
1862
|
-
else tweets.author_profile_id
|
|
1863
|
-
end,
|
|
1864
|
-
kind = case
|
|
1865
|
-
when tweets.kind in ('home', 'mention', 'authored') and excluded.kind in ('like', 'bookmark')
|
|
1866
|
-
then tweets.kind
|
|
1867
|
-
else excluded.kind
|
|
1868
|
-
end,
|
|
1869
|
-
text = case
|
|
1870
|
-
when excluded.kind in ('like', 'bookmark')
|
|
1871
|
-
and tweets.text <> ''
|
|
1872
|
-
then tweets.text
|
|
1873
|
-
when excluded.text <> '' then excluded.text
|
|
1874
|
-
else tweets.text
|
|
1875
|
-
end,
|
|
1876
|
-
created_at = case
|
|
1877
|
-
when excluded.kind in ('like', 'bookmark')
|
|
1878
|
-
then tweets.created_at
|
|
1879
|
-
else excluded.created_at
|
|
1880
|
-
end,
|
|
1881
|
-
is_replied = max(tweets.is_replied, excluded.is_replied),
|
|
1882
|
-
reply_to_id = coalesce(excluded.reply_to_id, tweets.reply_to_id),
|
|
1883
|
-
like_count = max(tweets.like_count, excluded.like_count),
|
|
1884
|
-
media_count = max(tweets.media_count, excluded.media_count),
|
|
1885
|
-
bookmarked = case
|
|
1886
|
-
when tweets.account_id = excluded.account_id then max(tweets.bookmarked, excluded.bookmarked)
|
|
1887
|
-
else tweets.bookmarked
|
|
1888
|
-
end,
|
|
1889
|
-
liked = case
|
|
1890
|
-
when tweets.account_id = excluded.account_id then max(tweets.liked, excluded.liked)
|
|
1891
|
-
else tweets.liked
|
|
1892
|
-
end,
|
|
1893
|
-
entities_json = case when excluded.entities_json <> '{}' then excluded.entities_json else tweets.entities_json end,
|
|
1894
|
-
media_json = case when excluded.media_json <> '[]' then excluded.media_json else tweets.media_json end,
|
|
1895
|
-
quoted_tweet_id = coalesce(excluded.quoted_tweet_id, tweets.quoted_tweet_id)
|
|
1896
|
-
`);
|
|
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(`
|
|
1907
|
-
insert into tweet_account_edges (
|
|
1908
|
-
account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count, source,
|
|
1909
|
-
raw_json, updated_at
|
|
1910
|
-
) values (?, ?, ?, ?, ?, 1, 'archive', '{}', ?)
|
|
1911
|
-
on conflict(account_id, tweet_id, kind) do update set
|
|
1912
|
-
first_seen_at = min(tweet_account_edges.first_seen_at, excluded.first_seen_at),
|
|
1913
|
-
last_seen_at = max(tweet_account_edges.last_seen_at, excluded.last_seen_at),
|
|
1914
|
-
updated_at = max(tweet_account_edges.updated_at, excluded.updated_at)
|
|
1915
|
-
`);
|
|
1916
|
-
const insertCollection = db.prepare(`
|
|
1917
|
-
insert into tweet_collections (
|
|
1918
|
-
account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
|
|
1919
|
-
) values (?, ?, ?, ?, ?, ?, ?)
|
|
1920
|
-
on conflict(account_id, tweet_id, kind) do update set
|
|
1921
|
-
collected_at = coalesce(excluded.collected_at, tweet_collections.collected_at),
|
|
1922
|
-
source = case
|
|
1923
|
-
when tweet_collections.source = 'archive' then excluded.source
|
|
1924
|
-
else tweet_collections.source
|
|
1925
|
-
end,
|
|
1926
|
-
raw_json = case
|
|
1927
|
-
when tweet_collections.source = 'archive' then excluded.raw_json
|
|
1928
|
-
else tweet_collections.raw_json
|
|
1929
|
-
end,
|
|
1930
|
-
updated_at = max(tweet_collections.updated_at, excluded.updated_at)
|
|
1931
|
-
`);
|
|
1932
|
-
const insertConversation = db.prepare(`
|
|
1933
|
-
insert into dm_conversations (
|
|
1934
|
-
id, account_id, participant_profile_id, title, last_message_at, unread_count, needs_reply
|
|
1935
|
-
) values (?, ?, ?, ?, ?, ?, ?)
|
|
1936
|
-
`);
|
|
1937
|
-
const insertMessage = db.prepare(`
|
|
1938
|
-
insert into dm_messages (
|
|
1939
|
-
id, conversation_id, sender_profile_id, text, created_at, direction, is_replied, media_count
|
|
1940
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1941
|
-
`);
|
|
1942
|
-
const insertDmFts = db.prepare(
|
|
1943
|
-
"insert into dm_fts (message_id, text) values (?, ?)",
|
|
1944
|
-
);
|
|
1945
|
-
const insertFollowSnapshot = db.prepare(`
|
|
1946
|
-
insert into follow_snapshots (
|
|
1947
|
-
id, account_id, direction, source, status, page_count, result_count,
|
|
1948
|
-
started_at, completed_at, raw_meta_json
|
|
1949
|
-
) values (?, ?, ?, 'archive', 'complete', ?, ?, ?, ?, ?)
|
|
1950
|
-
on conflict(id) do update set
|
|
1951
|
-
account_id = excluded.account_id,
|
|
1952
|
-
direction = excluded.direction,
|
|
1953
|
-
source = excluded.source,
|
|
1954
|
-
status = excluded.status,
|
|
1955
|
-
page_count = excluded.page_count,
|
|
1956
|
-
result_count = excluded.result_count,
|
|
1957
|
-
started_at = excluded.started_at,
|
|
1958
|
-
completed_at = excluded.completed_at,
|
|
1959
|
-
raw_meta_json = excluded.raw_meta_json
|
|
1960
|
-
`);
|
|
1961
|
-
const insertFollowSnapshotMember = db.prepare(`
|
|
1962
|
-
insert into follow_snapshot_members (
|
|
1963
|
-
snapshot_id, profile_id, external_user_id, position
|
|
1964
|
-
) values (?, ?, ?, ?)
|
|
1965
|
-
`);
|
|
1966
|
-
const selectFollowSnapshotMembers = db.prepare(`
|
|
1967
|
-
select profile_id, external_user_id
|
|
1968
|
-
from follow_snapshot_members
|
|
1969
|
-
where snapshot_id = ?
|
|
1970
|
-
order by position, profile_id
|
|
1971
|
-
`);
|
|
1972
|
-
const deleteFollowSnapshotMembers = db.prepare(
|
|
1973
|
-
"delete from follow_snapshot_members where snapshot_id = ?",
|
|
1974
|
-
);
|
|
1975
|
-
const deleteArchiveFollowEvents = db.prepare(`
|
|
1976
|
-
delete from follow_events
|
|
1977
|
-
where account_id = ? and direction = ? and (
|
|
1978
|
-
snapshot_id = ? or snapshot_id in (
|
|
1979
|
-
select id from follow_snapshots
|
|
1980
|
-
where account_id = ? and direction = ? and source = 'archive'
|
|
1981
|
-
)
|
|
1982
|
-
)
|
|
1983
|
-
`);
|
|
1984
|
-
const deleteArchiveFollowSnapshotMembers = db.prepare(`
|
|
1985
|
-
delete from follow_snapshot_members
|
|
1986
|
-
where snapshot_id in (
|
|
1987
|
-
select id from follow_snapshots
|
|
1988
|
-
where account_id = ? and direction = ? and source = 'archive'
|
|
1989
|
-
)
|
|
1990
|
-
`);
|
|
1991
|
-
const deleteArchiveFollowSnapshots = db.prepare(`
|
|
1992
|
-
delete from follow_snapshots
|
|
1993
|
-
where account_id = ? and direction = ? and source = 'archive'
|
|
1994
|
-
`);
|
|
1995
|
-
const deleteArchiveFollowEdges = db.prepare(`
|
|
1996
|
-
delete from follow_edges
|
|
1997
|
-
where account_id = ? and direction = ? and source = 'archive'
|
|
1998
|
-
`);
|
|
1999
|
-
const selectFollowEdges = db.prepare(`
|
|
2000
|
-
select profile_id, external_user_id, current
|
|
2001
|
-
from follow_edges
|
|
2002
|
-
where account_id = ? and direction = ?
|
|
2003
|
-
`);
|
|
2004
|
-
const insertFollowEdge = db.prepare(`
|
|
2005
|
-
insert into follow_edges (
|
|
2006
|
-
account_id, direction, profile_id, external_user_id, source, current,
|
|
2007
|
-
first_seen_at, last_seen_at, ended_at, updated_at
|
|
2008
|
-
) values (?, ?, ?, ?, 'archive', 1, ?, ?, null, ?)
|
|
2009
|
-
on conflict(account_id, direction, profile_id) do update set
|
|
2010
|
-
external_user_id = excluded.external_user_id,
|
|
2011
|
-
source = case
|
|
2012
|
-
when follow_edges.source = 'archive' then excluded.source
|
|
2013
|
-
else follow_edges.source
|
|
2014
|
-
end,
|
|
2015
|
-
current = 1,
|
|
2016
|
-
last_seen_at = excluded.last_seen_at,
|
|
2017
|
-
ended_at = null,
|
|
2018
|
-
updated_at = excluded.updated_at
|
|
2019
|
-
`);
|
|
2020
|
-
const endFollowEdge = db.prepare(`
|
|
2021
|
-
update follow_edges
|
|
2022
|
-
set current = 0, ended_at = ?, updated_at = ?
|
|
2023
|
-
where account_id = ? and direction = ? and profile_id = ?
|
|
2024
|
-
`);
|
|
2025
|
-
const insertFollowEvent = db.prepare(`
|
|
2026
|
-
insert into follow_events (
|
|
2027
|
-
id, account_id, direction, profile_id, external_user_id, kind, event_at, snapshot_id
|
|
2028
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2029
|
-
`);
|
|
2030
|
-
const clearSelectedLikes = db.prepare(`
|
|
2031
|
-
delete from tweet_collections
|
|
2032
|
-
where account_id = ? and kind = 'likes' and source in ('archive', 'legacy')
|
|
2033
|
-
`);
|
|
2034
|
-
const clearSelectedBookmarks = db.prepare(`
|
|
2035
|
-
delete from tweet_collections
|
|
2036
|
-
where account_id = ? and kind = 'bookmarks' and source in ('archive', 'legacy')
|
|
2037
|
-
`);
|
|
2038
|
-
const clearTweetLikedFlag = db.prepare(`
|
|
2039
|
-
update tweets
|
|
2040
|
-
set liked = 0
|
|
2041
|
-
where account_id = ?
|
|
2042
|
-
and id in (
|
|
2043
|
-
select tweet_id
|
|
2044
|
-
from tweet_collections
|
|
2045
|
-
where account_id = ? and kind = 'likes' and source in ('archive', 'legacy')
|
|
2046
|
-
)
|
|
2047
|
-
`);
|
|
2048
|
-
const clearTweetBookmarkedFlag = db.prepare(`
|
|
2049
|
-
update tweets
|
|
2050
|
-
set bookmarked = 0
|
|
2051
|
-
where account_id = ?
|
|
2052
|
-
and id in (
|
|
2053
|
-
select tweet_id
|
|
2054
|
-
from tweet_collections
|
|
2055
|
-
where account_id = ? and kind = 'bookmarks' and source in ('archive', 'legacy')
|
|
2056
|
-
)
|
|
2057
|
-
`);
|
|
2058
|
-
const clearSelectedArchiveTweetEdges = db.prepare(`
|
|
2059
|
-
delete from tweet_account_edges
|
|
2060
|
-
where account_id = ?
|
|
2061
|
-
and kind in ('home', 'authored')
|
|
2062
|
-
and (
|
|
2063
|
-
source = 'archive'
|
|
2064
|
-
or (
|
|
2065
|
-
source = 'legacy'
|
|
2066
|
-
and exists (
|
|
2067
|
-
select 1
|
|
2068
|
-
from tweets
|
|
2069
|
-
where tweets.id = tweet_account_edges.tweet_id
|
|
2070
|
-
and tweets.account_id = ?
|
|
2071
|
-
and tweets.author_profile_id = ?
|
|
2072
|
-
)
|
|
2073
|
-
)
|
|
2074
|
-
)
|
|
2075
|
-
`);
|
|
2076
|
-
const deleteOrphanTweetLinkOccurrences = db.prepare(`
|
|
2077
|
-
delete from link_occurrences
|
|
2078
|
-
where source_kind = 'tweet'
|
|
2079
|
-
and source_id not in (select id from tweets)
|
|
2080
|
-
`);
|
|
2081
|
-
const deleteOrphanArchiveCollectionTweets = db.prepare(`
|
|
2082
|
-
delete from tweets
|
|
2083
|
-
where account_id = ?
|
|
2084
|
-
and kind in ('like', 'bookmark')
|
|
2085
|
-
and not exists (
|
|
2086
|
-
select 1
|
|
2087
|
-
from tweet_collections collection
|
|
2088
|
-
where collection.tweet_id = tweets.id
|
|
2089
|
-
)
|
|
2090
|
-
and not exists (
|
|
2091
|
-
select 1
|
|
2092
|
-
from tweet_account_edges edge
|
|
2093
|
-
where edge.tweet_id = tweets.id
|
|
2094
|
-
)
|
|
2095
|
-
and not exists (
|
|
2096
|
-
select 1
|
|
2097
|
-
from tweets referencing_tweet
|
|
2098
|
-
where referencing_tweet.reply_to_id = tweets.id
|
|
2099
|
-
or referencing_tweet.quoted_tweet_id = tweets.id
|
|
2100
|
-
)
|
|
2101
|
-
`);
|
|
2102
|
-
const demoteSelectedArchiveTweetsWithCollections = db.prepare(`
|
|
2103
|
-
update tweets
|
|
2104
|
-
set kind = case
|
|
2105
|
-
when exists (
|
|
2106
|
-
select 1
|
|
2107
|
-
from tweet_collections collection
|
|
2108
|
-
where collection.account_id = ?
|
|
2109
|
-
and collection.tweet_id = tweets.id
|
|
2110
|
-
and collection.kind = 'likes'
|
|
2111
|
-
) then 'like'
|
|
2112
|
-
when exists (
|
|
2113
|
-
select 1
|
|
2114
|
-
from tweet_collections collection
|
|
2115
|
-
where collection.account_id = ?
|
|
2116
|
-
and collection.tweet_id = tweets.id
|
|
2117
|
-
and collection.kind = 'bookmarks'
|
|
2118
|
-
) then 'bookmark'
|
|
2119
|
-
else kind
|
|
2120
|
-
end
|
|
2121
|
-
where account_id = ?
|
|
2122
|
-
and id in (
|
|
2123
|
-
select tweet_id
|
|
2124
|
-
from tweet_account_edges edge
|
|
2125
|
-
join tweets edge_tweet on edge_tweet.id = edge.tweet_id
|
|
2126
|
-
where edge.account_id = ?
|
|
2127
|
-
and edge.kind in ('home', 'authored')
|
|
2128
|
-
and (
|
|
2129
|
-
edge.source = 'archive'
|
|
2130
|
-
or (
|
|
2131
|
-
edge.source = 'legacy'
|
|
2132
|
-
and edge_tweet.account_id = ?
|
|
2133
|
-
and edge_tweet.author_profile_id = ?
|
|
2134
|
-
)
|
|
2135
|
-
)
|
|
2136
|
-
)
|
|
2137
|
-
and id in (
|
|
2138
|
-
select tweet_id
|
|
2139
|
-
from tweet_collections
|
|
2140
|
-
where account_id = ?
|
|
2141
|
-
)
|
|
2142
|
-
`);
|
|
2143
|
-
const preserveSelectedArchiveTweetsReferencedElsewhere = db.prepare(`
|
|
2144
|
-
update tweets
|
|
2145
|
-
set kind = 'archive_stale'
|
|
2146
|
-
where account_id = ?
|
|
2147
|
-
and id in (
|
|
2148
|
-
select tweet_id
|
|
2149
|
-
from tweet_account_edges edge
|
|
2150
|
-
join tweets edge_tweet on edge_tweet.id = edge.tweet_id
|
|
2151
|
-
where edge.account_id = ?
|
|
2152
|
-
and edge.kind in ('home', 'authored')
|
|
2153
|
-
and (
|
|
2154
|
-
edge.source = 'archive'
|
|
2155
|
-
or (
|
|
2156
|
-
edge.source = 'legacy'
|
|
2157
|
-
and edge_tweet.account_id = ?
|
|
2158
|
-
and edge_tweet.author_profile_id = ?
|
|
2159
|
-
)
|
|
2160
|
-
)
|
|
2161
|
-
)
|
|
2162
|
-
and id not in (
|
|
2163
|
-
select tweet_id
|
|
2164
|
-
from tweet_collections
|
|
2165
|
-
where account_id = ?
|
|
2166
|
-
)
|
|
2167
|
-
and exists (
|
|
2168
|
-
select 1
|
|
2169
|
-
from tweet_account_edges edge
|
|
2170
|
-
where edge.tweet_id = tweets.id
|
|
2171
|
-
and not (
|
|
2172
|
-
edge.account_id = ?
|
|
2173
|
-
and edge.kind in ('home', 'authored')
|
|
2174
|
-
and (
|
|
2175
|
-
edge.source = 'archive'
|
|
2176
|
-
or (
|
|
2177
|
-
edge.source = 'legacy'
|
|
2178
|
-
and tweets.account_id = ?
|
|
2179
|
-
and tweets.author_profile_id = ?
|
|
2180
|
-
)
|
|
2181
|
-
)
|
|
2182
|
-
)
|
|
2183
|
-
union all
|
|
2184
|
-
select 1
|
|
2185
|
-
from tweet_collections collection
|
|
2186
|
-
where collection.tweet_id = tweets.id
|
|
2187
|
-
union all
|
|
2188
|
-
select 1
|
|
2189
|
-
from tweets referencing_tweet
|
|
2190
|
-
where (
|
|
2191
|
-
referencing_tweet.reply_to_id = tweets.id
|
|
2192
|
-
or referencing_tweet.quoted_tweet_id = tweets.id
|
|
2193
|
-
)
|
|
2194
|
-
and (
|
|
2195
|
-
exists (
|
|
2196
|
-
select 1
|
|
2197
|
-
from tweet_collections collection
|
|
2198
|
-
where collection.tweet_id = referencing_tweet.id
|
|
2199
|
-
)
|
|
2200
|
-
or exists (
|
|
2201
|
-
select 1
|
|
2202
|
-
from tweet_account_edges edge
|
|
2203
|
-
where edge.tweet_id = referencing_tweet.id
|
|
2204
|
-
and not (
|
|
2205
|
-
edge.account_id = ?
|
|
2206
|
-
and edge.kind in ('home', 'authored')
|
|
2207
|
-
and (
|
|
2208
|
-
edge.source = 'archive'
|
|
2209
|
-
or (
|
|
2210
|
-
edge.source = 'legacy'
|
|
2211
|
-
and referencing_tweet.account_id = ?
|
|
2212
|
-
and referencing_tweet.author_profile_id = ?
|
|
2213
|
-
)
|
|
2214
|
-
)
|
|
2215
|
-
)
|
|
2216
|
-
)
|
|
2217
|
-
)
|
|
2218
|
-
)
|
|
2219
|
-
`);
|
|
2220
|
-
const deleteSelectedArchiveTweetsWithoutCollections = db.prepare(`
|
|
2221
|
-
delete from tweets
|
|
2222
|
-
where account_id = ?
|
|
2223
|
-
and id in (
|
|
2224
|
-
select tweet_id
|
|
2225
|
-
from tweet_account_edges edge
|
|
2226
|
-
join tweets edge_tweet on edge_tweet.id = edge.tweet_id
|
|
2227
|
-
where edge.account_id = ?
|
|
2228
|
-
and edge.kind in ('home', 'authored')
|
|
2229
|
-
and (
|
|
2230
|
-
edge.source = 'archive'
|
|
2231
|
-
or (
|
|
2232
|
-
edge.source = 'legacy'
|
|
2233
|
-
and edge_tweet.account_id = ?
|
|
2234
|
-
and edge_tweet.author_profile_id = ?
|
|
2235
|
-
)
|
|
2236
|
-
)
|
|
2237
|
-
)
|
|
2238
|
-
and not exists (
|
|
2239
|
-
select 1
|
|
2240
|
-
from tweet_collections collection
|
|
2241
|
-
where collection.tweet_id = tweets.id
|
|
2242
|
-
)
|
|
2243
|
-
and not exists (
|
|
2244
|
-
select 1
|
|
2245
|
-
from tweet_account_edges edge
|
|
2246
|
-
where edge.tweet_id = tweets.id
|
|
2247
|
-
and not (
|
|
2248
|
-
edge.account_id = ?
|
|
2249
|
-
and edge.kind in ('home', 'authored')
|
|
2250
|
-
and (
|
|
2251
|
-
edge.source = 'archive'
|
|
2252
|
-
or (
|
|
2253
|
-
edge.source = 'legacy'
|
|
2254
|
-
and tweets.account_id = ?
|
|
2255
|
-
and tweets.author_profile_id = ?
|
|
2256
|
-
)
|
|
2257
|
-
)
|
|
2258
|
-
)
|
|
2259
|
-
)
|
|
2260
|
-
and not exists (
|
|
2261
|
-
select 1
|
|
2262
|
-
from tweets referencing_tweet
|
|
2263
|
-
where (
|
|
2264
|
-
referencing_tweet.reply_to_id = tweets.id
|
|
2265
|
-
or referencing_tweet.quoted_tweet_id = tweets.id
|
|
2266
|
-
)
|
|
2267
|
-
and (
|
|
2268
|
-
exists (
|
|
2269
|
-
select 1
|
|
2270
|
-
from tweet_collections collection
|
|
2271
|
-
where collection.tweet_id = referencing_tweet.id
|
|
2272
|
-
)
|
|
2273
|
-
or exists (
|
|
2274
|
-
select 1
|
|
2275
|
-
from tweet_account_edges edge
|
|
2276
|
-
where edge.tweet_id = referencing_tweet.id
|
|
2277
|
-
and not (
|
|
2278
|
-
edge.account_id = ?
|
|
2279
|
-
and edge.kind in ('home', 'authored')
|
|
2280
|
-
and (
|
|
2281
|
-
edge.source = 'archive'
|
|
2282
|
-
or (
|
|
2283
|
-
edge.source = 'legacy'
|
|
2284
|
-
and referencing_tweet.account_id = ?
|
|
2285
|
-
and referencing_tweet.author_profile_id = ?
|
|
2286
|
-
)
|
|
2287
|
-
)
|
|
2288
|
-
)
|
|
2289
|
-
)
|
|
2290
|
-
)
|
|
2291
|
-
)
|
|
2292
|
-
`);
|
|
2293
|
-
const deleteOrphanTweetFts = db.prepare(`
|
|
2294
|
-
delete from tweets_fts
|
|
2295
|
-
where tweet_id not in (select id from tweets)
|
|
2296
|
-
`);
|
|
2297
|
-
const clearDmFts = db.prepare(`
|
|
2298
|
-
delete from dm_fts
|
|
2299
|
-
where message_id in (
|
|
2300
|
-
select m.id
|
|
2301
|
-
from dm_messages m
|
|
2302
|
-
join dm_conversations c on c.id = m.conversation_id
|
|
2303
|
-
where c.account_id = ?
|
|
2304
|
-
)
|
|
2305
|
-
`);
|
|
2306
|
-
const clearDmLinkOccurrences = db.prepare(`
|
|
2307
|
-
delete from link_occurrences
|
|
2308
|
-
where source_kind = 'dm'
|
|
2309
|
-
and source_id in (
|
|
2310
|
-
select m.id
|
|
2311
|
-
from dm_messages m
|
|
2312
|
-
join dm_conversations c on c.id = m.conversation_id
|
|
2313
|
-
where c.account_id = ?
|
|
2314
|
-
)
|
|
2315
|
-
`);
|
|
2316
|
-
const clearDmMessages = db.prepare(`
|
|
2317
|
-
delete from dm_messages
|
|
2318
|
-
where conversation_id in (
|
|
2319
|
-
select id from dm_conversations where account_id = ?
|
|
2320
|
-
)
|
|
2321
|
-
`);
|
|
2322
|
-
const clearDmConversations = db.prepare(
|
|
2323
|
-
"delete from dm_conversations where account_id = ?",
|
|
2324
|
-
);
|
|
2325
|
-
|
|
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<{
|
|
2344
|
-
profile_id: string;
|
|
2345
|
-
external_user_id: string;
|
|
2346
|
-
}>
|
|
2347
|
-
)
|
|
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>();
|
|
2361
|
-
|
|
2362
|
-
insertFollowSnapshot.run(
|
|
2363
|
-
snapshotId,
|
|
2364
|
-
"acct_primary",
|
|
2365
|
-
direction,
|
|
2366
|
-
entryCount,
|
|
2367
|
-
rows.length,
|
|
2368
|
-
now,
|
|
2369
|
-
now,
|
|
2370
|
-
JSON.stringify({ archivePath, result_count: rows.length }),
|
|
2371
|
-
);
|
|
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);
|
|
2417
|
-
insertFollowEvent.run(
|
|
2418
|
-
`follow_event_${randomUUID()}`,
|
|
2419
|
-
"acct_primary",
|
|
2420
|
-
direction,
|
|
2421
|
-
profileId,
|
|
2422
|
-
previous.external_user_id,
|
|
2423
|
-
"ended",
|
|
2424
|
-
now,
|
|
2425
|
-
snapshotId,
|
|
2426
|
-
);
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
function clearArchiveFollowRows(direction: ArchiveFollowDirection) {
|
|
2431
|
-
deleteArchiveFollowEvents.run(
|
|
2432
|
-
"acct_primary",
|
|
2433
|
-
direction,
|
|
2434
|
-
`follow_snapshot_archive_acct_primary_${direction}`,
|
|
2435
|
-
"acct_primary",
|
|
2436
|
-
direction,
|
|
2437
|
-
);
|
|
2438
|
-
deleteArchiveFollowSnapshotMembers.run("acct_primary", direction);
|
|
2439
|
-
deleteArchiveFollowSnapshots.run("acct_primary", direction);
|
|
2440
|
-
deleteArchiveFollowEdges.run("acct_primary", direction);
|
|
2441
|
-
}
|
|
2442
|
-
|
|
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 });
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
db.transaction(() => {
|
|
2455
|
-
if (!selection) {
|
|
2456
|
-
clearImportedData(db);
|
|
2457
|
-
clearMentionSyncState(db);
|
|
2458
|
-
}
|
|
2459
|
-
|
|
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
|
-
}
|
|
2525
|
-
|
|
2526
|
-
const writeAccount = selection ? insertAccountIfMissing : insertAccount;
|
|
2527
|
-
writeAccount.run(
|
|
2528
|
-
"acct_primary",
|
|
2529
|
-
accountPayload.displayName,
|
|
2530
|
-
`@${accountPayload.username}`,
|
|
2531
|
-
accountPayload.accountId,
|
|
2532
|
-
"archive",
|
|
2533
|
-
accountPayload.createdAt,
|
|
2534
|
-
);
|
|
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(
|
|
2583
|
-
tweet.id,
|
|
2584
|
-
"acct_primary",
|
|
2585
|
-
authorProfileId,
|
|
2586
|
-
tweet.kind,
|
|
2587
|
-
tweet.text,
|
|
2588
|
-
tweet.createdAt,
|
|
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,
|
|
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
|
-
});
|
|
2635
|
-
}
|
|
2636
|
-
let collectionIndex = 0;
|
|
2637
|
-
for (const collection of collectionRows) {
|
|
2638
|
-
insertCollection.run(
|
|
2639
|
-
"acct_primary",
|
|
2640
|
-
collection.tweetId,
|
|
2641
|
-
collection.kind,
|
|
2642
|
-
collection.collectedAt,
|
|
2643
|
-
collection.source,
|
|
2644
|
-
collection.rawJson,
|
|
2645
|
-
importedAt,
|
|
2646
|
-
);
|
|
2647
|
-
collectionIndex += 1;
|
|
2648
|
-
tickWrite("collections", collectionIndex, collectionRows.length);
|
|
2649
|
-
}
|
|
2650
|
-
|
|
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
|
-
}
|
|
2662
|
-
|
|
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
|
-
}
|
|
2686
|
-
|
|
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" });
|
|
2709
|
-
|
|
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
|
-
}
|
|
2732
|
-
|
|
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));
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
export const __test__ = {
|
|
2748
|
-
normalizeArchivePath,
|
|
2749
|
-
extractArchiveJson,
|
|
2750
|
-
parseArchiveArray,
|
|
2751
|
-
getFirstEntry,
|
|
2752
|
-
getMatchingEntries,
|
|
2753
|
-
parseTwitterDate,
|
|
2754
|
-
asRecord,
|
|
2755
|
-
asArray,
|
|
2756
|
-
toInt,
|
|
2757
|
-
compareIsoTimestamp,
|
|
2758
|
-
getTweetMediaCount,
|
|
2759
|
-
extractTweetEntities,
|
|
2760
|
-
extractTweetMedia,
|
|
2761
|
-
extractCollectionTweet,
|
|
2762
|
-
buildAccountPayload,
|
|
2763
|
-
inferProfileFromDirectory,
|
|
2764
|
-
};
|