birdclaw 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +117 -10
- package/bin/birdclaw.mjs +3 -1
- package/package.json +4 -3
- package/src/cli.ts +237 -4
- package/src/components/AppNav.tsx +2 -0
- package/src/components/SavedTimelineView.tsx +115 -0
- package/src/components/TimelineCard.tsx +26 -17
- package/src/lib/archive-import.ts +193 -16
- package/src/lib/backup.ts +1617 -0
- package/src/lib/bird.ts +163 -24
- package/src/lib/blocks-write.ts +2 -2
- package/src/lib/blocks.ts +2 -7
- package/src/lib/bookmark-sync-job.ts +446 -0
- package/src/lib/config.ts +6 -0
- package/src/lib/db.ts +82 -4
- package/src/lib/mentions-live.ts +2 -5
- package/src/lib/moderation-target.ts +4 -4
- package/src/lib/mutes-write.ts +2 -2
- package/src/lib/queries.ts +70 -1
- package/src/lib/seed.ts +4 -2
- package/src/lib/theme-transition.ts +6 -10
- package/src/lib/theme.tsx +13 -5
- package/src/lib/timeline-collections-live.ts +406 -0
- package/src/lib/types.ts +9 -1
- package/src/lib/xurl.ts +90 -4
- package/src/routeTree.gen.ts +42 -0
- package/src/routes/__root.tsx +6 -1
- package/src/routes/api/action.tsx +14 -4
- package/src/routes/api/inbox.tsx +3 -1
- package/src/routes/api/query.tsx +20 -2
- package/src/routes/api/status.tsx +6 -3
- package/src/routes/bookmarks.tsx +18 -0
- package/src/routes/likes.tsx +18 -0
- package/tsconfig.json +0 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { TimelineCard } from "#/components/TimelineCard";
|
|
3
|
+
import type { QueryEnvelope, QueryResponse, TimelineItem } from "#/lib/types";
|
|
4
|
+
import {
|
|
5
|
+
cx,
|
|
6
|
+
eyebrowClass,
|
|
7
|
+
feedPageClass,
|
|
8
|
+
heroControlsClass,
|
|
9
|
+
heroCopyClass,
|
|
10
|
+
heroShellClass,
|
|
11
|
+
heroTitleClass,
|
|
12
|
+
pageWrapClass,
|
|
13
|
+
textFieldClass,
|
|
14
|
+
textFieldWideClass,
|
|
15
|
+
timelineLaneClass,
|
|
16
|
+
} from "#/lib/ui";
|
|
17
|
+
|
|
18
|
+
interface SavedTimelineViewProps {
|
|
19
|
+
filter: "liked" | "bookmarked";
|
|
20
|
+
eyebrow: string;
|
|
21
|
+
title: string;
|
|
22
|
+
loadingLabel: string;
|
|
23
|
+
searchPlaceholder: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SavedTimelineView({
|
|
27
|
+
filter,
|
|
28
|
+
eyebrow,
|
|
29
|
+
title,
|
|
30
|
+
loadingLabel,
|
|
31
|
+
searchPlaceholder,
|
|
32
|
+
}: SavedTimelineViewProps) {
|
|
33
|
+
const [meta, setMeta] = useState<QueryEnvelope | null>(null);
|
|
34
|
+
const [items, setItems] = useState<TimelineItem[]>([]);
|
|
35
|
+
const [search, setSearch] = useState("");
|
|
36
|
+
const [refreshTick, setRefreshTick] = useState(0);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
fetch("/api/status")
|
|
40
|
+
.then((response) => response.json())
|
|
41
|
+
.then((data: QueryEnvelope) => setMeta(data));
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const url = new URL("/api/query", window.location.origin);
|
|
46
|
+
url.searchParams.set("resource", "home");
|
|
47
|
+
url.searchParams.set(filter, "true");
|
|
48
|
+
url.searchParams.set("refresh", String(refreshTick));
|
|
49
|
+
if (search.trim()) {
|
|
50
|
+
url.searchParams.set("search", search.trim());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fetch(url)
|
|
54
|
+
.then((response) => response.json())
|
|
55
|
+
.then((data: QueryResponse) => setItems(data.items as TimelineItem[]));
|
|
56
|
+
}, [filter, refreshTick, search]);
|
|
57
|
+
|
|
58
|
+
const subtitle = useMemo(() => {
|
|
59
|
+
if (!meta) {
|
|
60
|
+
return items.length > 0 ? `${items.length} visible` : loadingLabel;
|
|
61
|
+
}
|
|
62
|
+
return `${items.length} visible · ${meta.transport.statusText}`;
|
|
63
|
+
}, [items.length, loadingLabel, meta]);
|
|
64
|
+
|
|
65
|
+
async function replyToTweet(tweetId: string) {
|
|
66
|
+
const text = window.prompt("Reply text");
|
|
67
|
+
if (!text?.trim()) return;
|
|
68
|
+
|
|
69
|
+
await fetch("/api/action", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "content-type": "application/json" },
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
kind: "replyTweet",
|
|
74
|
+
accountId: "acct_primary",
|
|
75
|
+
tweetId,
|
|
76
|
+
text,
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
setRefreshTick((value) => value + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<main className={pageWrapClass}>
|
|
85
|
+
<div className={feedPageClass}>
|
|
86
|
+
<section className={heroShellClass}>
|
|
87
|
+
<div>
|
|
88
|
+
<p className={eyebrowClass}>{eyebrow}</p>
|
|
89
|
+
<h2 className={heroTitleClass}>{title}</h2>
|
|
90
|
+
<p className={heroCopyClass}>{subtitle}</p>
|
|
91
|
+
</div>
|
|
92
|
+
<div className={heroControlsClass}>
|
|
93
|
+
<input
|
|
94
|
+
className={cx(textFieldClass, textFieldWideClass)}
|
|
95
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
96
|
+
placeholder={searchPlaceholder}
|
|
97
|
+
value={search}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<section className={timelineLaneClass}>
|
|
103
|
+
{items.map((item) => (
|
|
104
|
+
<TimelineCard
|
|
105
|
+
key={item.id}
|
|
106
|
+
item={item}
|
|
107
|
+
onReply={replyToTweet}
|
|
108
|
+
showReplyControls={false}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</section>
|
|
112
|
+
</div>
|
|
113
|
+
</main>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -34,10 +34,15 @@ function getVisibleUrlCards(item: TimelineItem) {
|
|
|
34
34
|
export function TimelineCard({
|
|
35
35
|
item,
|
|
36
36
|
onReply,
|
|
37
|
+
showReplyControls = true,
|
|
37
38
|
}: {
|
|
38
39
|
item: TimelineItem;
|
|
39
40
|
onReply: (tweetId: string) => void;
|
|
41
|
+
showReplyControls?: boolean;
|
|
40
42
|
}) {
|
|
43
|
+
const canReply =
|
|
44
|
+
showReplyControls && item.kind !== "like" && item.kind !== "bookmark";
|
|
45
|
+
|
|
41
46
|
return (
|
|
42
47
|
<article className={contentCardClass}>
|
|
43
48
|
<header className={cardHeaderClass}>
|
|
@@ -62,14 +67,16 @@ export function TimelineCard({
|
|
|
62
67
|
</div>
|
|
63
68
|
</div>
|
|
64
69
|
<div className={metaStackClass}>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
{canReply ? (
|
|
71
|
+
<span
|
|
72
|
+
className={cx(
|
|
73
|
+
pillClass,
|
|
74
|
+
item.isReplied ? pillSoftClass : pillAlertClass,
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{item.isReplied ? "replied" : "needs reply"}
|
|
78
|
+
</span>
|
|
79
|
+
) : null}
|
|
73
80
|
<span className={timestampClass}>
|
|
74
81
|
{formatShortTimestamp(item.createdAt)}
|
|
75
82
|
</span>
|
|
@@ -83,9 +90,9 @@ export function TimelineCard({
|
|
|
83
90
|
{item.quotedTweet ? (
|
|
84
91
|
<EmbeddedTweetCard item={item.quotedTweet} label="Quoted tweet" />
|
|
85
92
|
) : null}
|
|
86
|
-
{getVisibleUrlCards(item).map((entry) => (
|
|
93
|
+
{getVisibleUrlCards(item).map((entry, index) => (
|
|
87
94
|
<a
|
|
88
|
-
key={entry.expandedUrl}
|
|
95
|
+
key={`${entry.expandedUrl}-${String(index)}`}
|
|
89
96
|
className={linkPreviewCardClass}
|
|
90
97
|
href={entry.expandedUrl}
|
|
91
98
|
rel="noreferrer"
|
|
@@ -105,13 +112,15 @@ export function TimelineCard({
|
|
|
105
112
|
<span>{item.bookmarked ? "bookmarked" : "not bookmarked"}</span>
|
|
106
113
|
<span>{item.accountHandle}</span>
|
|
107
114
|
</div>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
{canReply ? (
|
|
116
|
+
<button
|
|
117
|
+
className={actionButtonClass}
|
|
118
|
+
onClick={() => onReply(item.id)}
|
|
119
|
+
type="button"
|
|
120
|
+
>
|
|
121
|
+
Reply
|
|
122
|
+
</button>
|
|
123
|
+
) : null}
|
|
115
124
|
</footer>
|
|
116
125
|
</article>
|
|
117
126
|
);
|
|
@@ -25,6 +25,7 @@ interface ImportedArchiveSummary {
|
|
|
25
25
|
counts: {
|
|
26
26
|
tweets: number;
|
|
27
27
|
likes: number;
|
|
28
|
+
bookmarks: number;
|
|
28
29
|
dmConversations: number;
|
|
29
30
|
dmMessages: number;
|
|
30
31
|
profiles: number;
|
|
@@ -99,6 +100,10 @@ function parseTwitterDate(value: unknown) {
|
|
|
99
100
|
: parsed.toISOString();
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
function compareIsoTimestamp(left: string, right: string) {
|
|
104
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
102
107
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
103
108
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
104
109
|
? (value as Record<string, unknown>)
|
|
@@ -211,6 +216,39 @@ function extractTweetMedia(tweet: Record<string, unknown>) {
|
|
|
211
216
|
});
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
function extractCollectionTweet(
|
|
220
|
+
wrapper: ArchiveRecord,
|
|
221
|
+
key: "like" | "bookmark",
|
|
222
|
+
) {
|
|
223
|
+
const entry = asRecord(wrapper[key]) ?? asRecord(wrapper.tweet);
|
|
224
|
+
if (!entry) return null;
|
|
225
|
+
|
|
226
|
+
const id = String(
|
|
227
|
+
entry.tweetId ?? entry.tweet_id ?? entry.id_str ?? entry.id ?? "",
|
|
228
|
+
);
|
|
229
|
+
if (!id) return null;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
id,
|
|
233
|
+
text: String(
|
|
234
|
+
entry.fullText ??
|
|
235
|
+
entry.full_text ??
|
|
236
|
+
entry.text ??
|
|
237
|
+
entry.expandedUrl ??
|
|
238
|
+
entry.expanded_url ??
|
|
239
|
+
"",
|
|
240
|
+
),
|
|
241
|
+
createdAt: parseTwitterDate(
|
|
242
|
+
entry.likedAt ??
|
|
243
|
+
entry.bookmarkedAt ??
|
|
244
|
+
entry.createdAt ??
|
|
245
|
+
entry.created_at ??
|
|
246
|
+
new Date(0).toISOString(),
|
|
247
|
+
),
|
|
248
|
+
likeCount: toInt(entry.favorite_count ?? entry.like_count),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
214
252
|
function buildAccountPayload(
|
|
215
253
|
accountRecord: Record<string, unknown> | null,
|
|
216
254
|
profileRecord: Record<string, unknown> | null,
|
|
@@ -248,6 +286,7 @@ function clearImportedData() {
|
|
|
248
286
|
db.exec(`
|
|
249
287
|
delete from ai_scores;
|
|
250
288
|
delete from tweet_actions;
|
|
289
|
+
delete from tweet_collections;
|
|
251
290
|
delete from dm_fts;
|
|
252
291
|
delete from tweets_fts;
|
|
253
292
|
delete from dm_messages;
|
|
@@ -276,6 +315,10 @@ export async function importArchive(
|
|
|
276
315
|
entries,
|
|
277
316
|
/(?:^|\/)data\/(?:like|likes)(?:-part\d+)?\.js$/i,
|
|
278
317
|
);
|
|
318
|
+
const bookmarkEntries = getMatchingEntries(
|
|
319
|
+
entries,
|
|
320
|
+
/(?:^|\/)data\/(?:bookmark|bookmarks)(?:-part\d+)?\.js$/i,
|
|
321
|
+
);
|
|
279
322
|
const dmEntries = getMatchingEntries(
|
|
280
323
|
entries,
|
|
281
324
|
/(?:^|\/)data\/direct-messages(?:-group)?(?:-part\d+)?\.js$/i,
|
|
@@ -303,16 +346,40 @@ export async function importArchive(
|
|
|
303
346
|
>();
|
|
304
347
|
const tweetRows: Array<{
|
|
305
348
|
id: string;
|
|
349
|
+
kind: "home" | "like" | "bookmark";
|
|
350
|
+
authorProfileId: string;
|
|
306
351
|
text: string;
|
|
307
352
|
createdAt: string;
|
|
308
353
|
isReplied: number;
|
|
309
354
|
replyToId: string | null;
|
|
310
355
|
likeCount: number;
|
|
311
356
|
mediaCount: number;
|
|
357
|
+
bookmarked: number;
|
|
358
|
+
liked: number;
|
|
312
359
|
entitiesJson: string;
|
|
313
360
|
mediaJson: string;
|
|
314
361
|
quotedTweetId: string | null;
|
|
315
362
|
}> = [];
|
|
363
|
+
const collectionRows: Array<{
|
|
364
|
+
tweetId: string;
|
|
365
|
+
kind: "likes" | "bookmarks";
|
|
366
|
+
collectedAt: string | null;
|
|
367
|
+
source: string;
|
|
368
|
+
rawJson: string;
|
|
369
|
+
}> = [];
|
|
370
|
+
const tweetRowsById = new Map<string, (typeof tweetRows)[number]>();
|
|
371
|
+
|
|
372
|
+
function addTweetRow(row: (typeof tweetRows)[number]) {
|
|
373
|
+
const existing = tweetRowsById.get(row.id);
|
|
374
|
+
if (existing) {
|
|
375
|
+
existing.bookmarked = Math.max(existing.bookmarked, row.bookmarked);
|
|
376
|
+
existing.liked = Math.max(existing.liked, row.liked);
|
|
377
|
+
if (!existing.text && row.text) existing.text = row.text;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
tweetRows.push(row);
|
|
381
|
+
tweetRowsById.set(row.id, row);
|
|
382
|
+
}
|
|
316
383
|
|
|
317
384
|
for (const entry of tweetEntries) {
|
|
318
385
|
const content = await readArchiveEntry(archivePath, entry);
|
|
@@ -342,8 +409,10 @@ export async function importArchive(
|
|
|
342
409
|
});
|
|
343
410
|
}
|
|
344
411
|
|
|
345
|
-
|
|
412
|
+
addTweetRow({
|
|
346
413
|
id: String(tweet.id_str ?? tweet.id),
|
|
414
|
+
kind: "home",
|
|
415
|
+
authorProfileId: "profile_me",
|
|
347
416
|
text: String(tweet.full_text ?? tweet.text ?? ""),
|
|
348
417
|
createdAt: parseTwitterDate(tweet.created_at),
|
|
349
418
|
isReplied: tweet.in_reply_to_status_id_str ? 1 : 0,
|
|
@@ -352,6 +421,8 @@ export async function importArchive(
|
|
|
352
421
|
: null,
|
|
353
422
|
likeCount: toInt(tweet.favorite_count),
|
|
354
423
|
mediaCount: getTweetMediaCount(tweet),
|
|
424
|
+
bookmarked: 0,
|
|
425
|
+
liked: 0,
|
|
355
426
|
entitiesJson: JSON.stringify(extractTweetEntities(tweet)),
|
|
356
427
|
mediaJson: JSON.stringify(extractTweetMedia(tweet)),
|
|
357
428
|
quotedTweetId: tweet.quoted_status_id_str
|
|
@@ -367,20 +438,25 @@ export async function importArchive(
|
|
|
367
438
|
const noteTweet = asRecord(wrapper.noteTweet);
|
|
368
439
|
if (!noteTweet) continue;
|
|
369
440
|
const core = asRecord(noteTweet.core);
|
|
370
|
-
|
|
441
|
+
addTweetRow({
|
|
371
442
|
id: String(noteTweet.noteTweetId ?? noteTweet.id ?? randomUUID()),
|
|
443
|
+
kind: "home",
|
|
444
|
+
authorProfileId: "profile_me",
|
|
372
445
|
text: String(core?.text ?? ""),
|
|
373
446
|
createdAt: parseTwitterDate(noteTweet.createdAt),
|
|
374
447
|
isReplied: 0,
|
|
375
448
|
replyToId: null,
|
|
376
449
|
likeCount: 0,
|
|
377
450
|
mediaCount: 0,
|
|
451
|
+
bookmarked: 0,
|
|
452
|
+
liked: 0,
|
|
378
453
|
entitiesJson: "{}",
|
|
379
454
|
mediaJson: "[]",
|
|
380
455
|
quotedTweetId: null,
|
|
381
456
|
});
|
|
382
457
|
}
|
|
383
458
|
}
|
|
459
|
+
const authoredTweetCount = tweetRows.length;
|
|
384
460
|
|
|
385
461
|
type MessageRow = {
|
|
386
462
|
id: string;
|
|
@@ -567,10 +643,8 @@ export async function importArchive(
|
|
|
567
643
|
mediaCount: asArray(messageCreate.mediaUrls).length,
|
|
568
644
|
} satisfies MessageRow;
|
|
569
645
|
})
|
|
570
|
-
.sort(
|
|
571
|
-
(left, right)
|
|
572
|
-
new Date(left.createdAt).getTime() -
|
|
573
|
-
new Date(right.createdAt).getTime(),
|
|
646
|
+
.sort((left, right) =>
|
|
647
|
+
compareIsoTimestamp(left.createdAt, right.createdAt),
|
|
574
648
|
);
|
|
575
649
|
|
|
576
650
|
if (messageEvents.length === 0) {
|
|
@@ -596,18 +670,92 @@ export async function importArchive(
|
|
|
596
670
|
}
|
|
597
671
|
}
|
|
598
672
|
|
|
599
|
-
|
|
600
|
-
|
|
673
|
+
let likeCount = 0;
|
|
674
|
+
for (const entry of likeEntries) {
|
|
675
|
+
const content = await readArchiveEntry(archivePath, entry);
|
|
676
|
+
const likes = parseArchiveArray(content);
|
|
677
|
+
for (const like of likes) {
|
|
678
|
+
const tweet = extractCollectionTweet(like, "like");
|
|
679
|
+
if (!tweet) continue;
|
|
680
|
+
collectionRows.push({
|
|
681
|
+
tweetId: tweet.id,
|
|
682
|
+
kind: "likes",
|
|
683
|
+
collectedAt: tweet.createdAt,
|
|
684
|
+
source: "archive",
|
|
685
|
+
rawJson: JSON.stringify(like),
|
|
686
|
+
});
|
|
687
|
+
addTweetRow({
|
|
688
|
+
id: tweet.id,
|
|
689
|
+
kind: "like",
|
|
690
|
+
authorProfileId: "profile_unknown",
|
|
691
|
+
text: tweet.text,
|
|
692
|
+
createdAt: tweet.createdAt,
|
|
693
|
+
isReplied: 0,
|
|
694
|
+
replyToId: null,
|
|
695
|
+
likeCount: tweet.likeCount,
|
|
696
|
+
mediaCount: 0,
|
|
697
|
+
bookmarked: 0,
|
|
698
|
+
liked: 1,
|
|
699
|
+
entitiesJson: "{}",
|
|
700
|
+
mediaJson: "[]",
|
|
701
|
+
quotedTweetId: null,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
likeCount += likes.length;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let bookmarkCount = 0;
|
|
708
|
+
for (const entry of bookmarkEntries) {
|
|
601
709
|
const content = await readArchiveEntry(archivePath, entry);
|
|
602
|
-
|
|
603
|
-
|
|
710
|
+
const bookmarks = parseArchiveArray(content);
|
|
711
|
+
for (const bookmark of bookmarks) {
|
|
712
|
+
const tweet = extractCollectionTweet(bookmark, "bookmark");
|
|
713
|
+
if (!tweet) continue;
|
|
714
|
+
collectionRows.push({
|
|
715
|
+
tweetId: tweet.id,
|
|
716
|
+
kind: "bookmarks",
|
|
717
|
+
collectedAt: tweet.createdAt,
|
|
718
|
+
source: "archive",
|
|
719
|
+
rawJson: JSON.stringify(bookmark),
|
|
720
|
+
});
|
|
721
|
+
addTweetRow({
|
|
722
|
+
id: tweet.id,
|
|
723
|
+
kind: "bookmark",
|
|
724
|
+
authorProfileId: "profile_unknown",
|
|
725
|
+
text: tweet.text,
|
|
726
|
+
createdAt: tweet.createdAt,
|
|
727
|
+
isReplied: 0,
|
|
728
|
+
replyToId: null,
|
|
729
|
+
likeCount: tweet.likeCount,
|
|
730
|
+
mediaCount: 0,
|
|
731
|
+
bookmarked: 1,
|
|
732
|
+
liked: 0,
|
|
733
|
+
entitiesJson: "{}",
|
|
734
|
+
mediaJson: "[]",
|
|
735
|
+
quotedTweetId: null,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
bookmarkCount += bookmarks.length;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (tweetRows.some((tweet) => tweet.authorProfileId === "profile_unknown")) {
|
|
742
|
+
profiles.set("profile_unknown", {
|
|
743
|
+
id: "profile_unknown",
|
|
744
|
+
handle: "unknown",
|
|
745
|
+
displayName: "Unknown",
|
|
746
|
+
bio: "Imported from archive collection metadata",
|
|
747
|
+
followersCount: 0,
|
|
748
|
+
avatarHue: 210,
|
|
749
|
+
createdAt: accountPayload.createdAt,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
604
752
|
|
|
605
753
|
clearImportedData();
|
|
606
754
|
|
|
607
755
|
const db = getNativeDb();
|
|
608
756
|
const insertAccount = db.prepare(`
|
|
609
|
-
insert into accounts (id, name, handle, transport, is_default, created_at)
|
|
610
|
-
values (?, ?, ?, ?, 1, ?)
|
|
757
|
+
insert into accounts (id, name, handle, external_user_id, transport, is_default, created_at)
|
|
758
|
+
values (?, ?, ?, ?, ?, 1, ?)
|
|
611
759
|
`);
|
|
612
760
|
const insertProfile = db.prepare(`
|
|
613
761
|
insert into profiles (id, handle, display_name, bio, followers_count, avatar_hue, avatar_url, created_at)
|
|
@@ -617,11 +765,21 @@ export async function importArchive(
|
|
|
617
765
|
insert into tweets (
|
|
618
766
|
id, account_id, author_profile_id, kind, text, created_at, is_replied,
|
|
619
767
|
reply_to_id, like_count, media_count, bookmarked, liked, entities_json, media_json, quoted_tweet_id
|
|
620
|
-
) values (?, ?, ?,
|
|
768
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
621
769
|
`);
|
|
622
770
|
const insertTweetFts = db.prepare(
|
|
623
771
|
"insert into tweets_fts (tweet_id, text) values (?, ?)",
|
|
624
772
|
);
|
|
773
|
+
const insertCollection = db.prepare(`
|
|
774
|
+
insert into tweet_collections (
|
|
775
|
+
account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
|
|
776
|
+
) values (?, ?, ?, ?, ?, ?, ?)
|
|
777
|
+
on conflict(account_id, tweet_id, kind) do update set
|
|
778
|
+
collected_at = coalesce(excluded.collected_at, tweet_collections.collected_at),
|
|
779
|
+
source = excluded.source,
|
|
780
|
+
raw_json = excluded.raw_json,
|
|
781
|
+
updated_at = excluded.updated_at
|
|
782
|
+
`);
|
|
625
783
|
const insertConversation = db.prepare(`
|
|
626
784
|
insert into dm_conversations (
|
|
627
785
|
id, account_id, participant_profile_id, title, last_message_at, unread_count, needs_reply
|
|
@@ -641,6 +799,7 @@ export async function importArchive(
|
|
|
641
799
|
"acct_primary",
|
|
642
800
|
accountPayload.displayName,
|
|
643
801
|
`@${accountPayload.username}`,
|
|
802
|
+
accountPayload.accountId,
|
|
644
803
|
"archive",
|
|
645
804
|
accountPayload.createdAt,
|
|
646
805
|
);
|
|
@@ -662,13 +821,16 @@ export async function importArchive(
|
|
|
662
821
|
insertTweet.run(
|
|
663
822
|
tweet.id,
|
|
664
823
|
"acct_primary",
|
|
665
|
-
|
|
824
|
+
tweet.authorProfileId,
|
|
825
|
+
tweet.kind,
|
|
666
826
|
tweet.text,
|
|
667
827
|
tweet.createdAt,
|
|
668
828
|
tweet.isReplied,
|
|
669
829
|
tweet.replyToId,
|
|
670
830
|
tweet.likeCount,
|
|
671
831
|
tweet.mediaCount,
|
|
832
|
+
tweet.bookmarked,
|
|
833
|
+
tweet.liked,
|
|
672
834
|
tweet.entitiesJson,
|
|
673
835
|
tweet.mediaJson,
|
|
674
836
|
tweet.quotedTweetId,
|
|
@@ -676,6 +838,19 @@ export async function importArchive(
|
|
|
676
838
|
insertTweetFts.run(tweet.id, tweet.text);
|
|
677
839
|
}
|
|
678
840
|
|
|
841
|
+
const importedAt = new Date().toISOString();
|
|
842
|
+
for (const collection of collectionRows) {
|
|
843
|
+
insertCollection.run(
|
|
844
|
+
"acct_primary",
|
|
845
|
+
collection.tweetId,
|
|
846
|
+
collection.kind,
|
|
847
|
+
collection.collectedAt,
|
|
848
|
+
collection.source,
|
|
849
|
+
collection.rawJson,
|
|
850
|
+
importedAt,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
679
854
|
for (const conversation of conversations.values()) {
|
|
680
855
|
insertConversation.run(
|
|
681
856
|
conversation.id,
|
|
@@ -712,8 +887,9 @@ export async function importArchive(
|
|
|
712
887
|
displayName: accountPayload.displayName,
|
|
713
888
|
},
|
|
714
889
|
counts: {
|
|
715
|
-
tweets:
|
|
716
|
-
likes:
|
|
890
|
+
tweets: authoredTweetCount,
|
|
891
|
+
likes: likeCount,
|
|
892
|
+
bookmarks: bookmarkCount,
|
|
717
893
|
dmConversations: conversations.size,
|
|
718
894
|
dmMessages: dmMessages.length,
|
|
719
895
|
profiles: profiles.size,
|
|
@@ -728,6 +904,7 @@ export const __test__ = {
|
|
|
728
904
|
asRecord,
|
|
729
905
|
asArray,
|
|
730
906
|
toInt,
|
|
907
|
+
compareIsoTimestamp,
|
|
731
908
|
getTweetMediaCount,
|
|
732
909
|
extractTweetEntities,
|
|
733
910
|
extractTweetMedia,
|