birdclaw 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- package/src/styles.css +22 -0
package/src/lib/dms-live.ts
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import type { Database } from "./sqlite";
|
|
2
3
|
import {
|
|
3
4
|
type BirdDmConversation,
|
|
4
5
|
type BirdDmEvent,
|
|
5
6
|
type BirdDmUser,
|
|
6
|
-
|
|
7
|
+
getAuthenticatedBirdAccountEffect,
|
|
8
|
+
listDirectMessagesViaBirdEffect,
|
|
7
9
|
} from "./bird";
|
|
8
10
|
import { getNativeDb } from "./db";
|
|
11
|
+
import { runEffectPromise } from "./effect-runtime";
|
|
9
12
|
import { readSyncCache, writeSyncCache } from "./sync-cache";
|
|
10
|
-
import type { XurlMentionUser } from "./types";
|
|
11
|
-
import {
|
|
13
|
+
import type { XurlDmEventsResponse, XurlMentionUser } from "./types";
|
|
14
|
+
import {
|
|
15
|
+
listDirectMessageEventsViaXurlEffect,
|
|
16
|
+
lookupAuthenticatedOAuth2UserEffect,
|
|
17
|
+
} from "./xurl";
|
|
18
|
+
import {
|
|
19
|
+
buildExternalProfileId,
|
|
20
|
+
randomAvatarHue,
|
|
21
|
+
upsertProfileFromXUser,
|
|
22
|
+
} from "./x-profile";
|
|
12
23
|
|
|
13
24
|
export const DEFAULT_DMS_CACHE_TTL_MS = 2 * 60_000;
|
|
25
|
+
const PREVIEW_MESSAGE_ID_PREFIX = "preview:";
|
|
26
|
+
const XURL_DMS_MAX_RESULTS = 100;
|
|
27
|
+
|
|
28
|
+
export type DirectMessagesSyncMode = "auto" | "bird" | "xurl";
|
|
29
|
+
|
|
30
|
+
export interface SyncDirectMessagesViaCachedBirdOptions {
|
|
31
|
+
account?: string;
|
|
32
|
+
mode?: DirectMessagesSyncMode;
|
|
33
|
+
limit?: number;
|
|
34
|
+
inbox?: "all" | "accepted" | "requests";
|
|
35
|
+
maxPages?: number;
|
|
36
|
+
allPages?: boolean;
|
|
37
|
+
pageDelayMs?: number;
|
|
38
|
+
refresh?: boolean;
|
|
39
|
+
cacheTtlMs?: number;
|
|
40
|
+
}
|
|
14
41
|
|
|
15
42
|
function parseCacheTtlMs(value?: number) {
|
|
16
43
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
@@ -25,21 +52,60 @@ function assertBirdLimit(limit: number) {
|
|
|
25
52
|
}
|
|
26
53
|
}
|
|
27
54
|
|
|
55
|
+
function assertXurlLimit(limit: number) {
|
|
56
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > XURL_DMS_MAX_RESULTS) {
|
|
57
|
+
throw new Error("xurl DM mode requires --limit between 1 and 100");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseSyncMode(mode: DirectMessagesSyncMode | undefined) {
|
|
62
|
+
if (!mode || mode === "bird" || mode === "xurl" || mode === "auto") {
|
|
63
|
+
return mode ?? "bird";
|
|
64
|
+
}
|
|
65
|
+
throw new Error("--mode must be auto, bird, or xurl");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeExternalUserId(value: string | null | undefined) {
|
|
69
|
+
const trimmed = value?.trim();
|
|
70
|
+
return trimmed ? trimmed : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function makePreviewMessageId(conversationId: string): string {
|
|
74
|
+
return `${PREVIEW_MESSAGE_ID_PREFIX}${conversationId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deleteDmFtsRows(db: Database, messageIds: string[]) {
|
|
78
|
+
const chunkSize = 500;
|
|
79
|
+
for (let index = 0; index < messageIds.length; index += chunkSize) {
|
|
80
|
+
const chunk = messageIds.slice(index, index + chunkSize);
|
|
81
|
+
if (chunk.length === 0) continue;
|
|
82
|
+
db.prepare(
|
|
83
|
+
`delete from dm_fts where message_id in (${chunk.map(() => "?").join(",")})`,
|
|
84
|
+
).run(...chunk);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
28
88
|
function resolveAccount(db: Database, accountId?: string) {
|
|
29
89
|
const row = accountId
|
|
30
90
|
? (db
|
|
31
|
-
.prepare(
|
|
32
|
-
|
|
91
|
+
.prepare(
|
|
92
|
+
"select id, handle, external_user_id from accounts where id = ?",
|
|
93
|
+
)
|
|
94
|
+
.get(accountId) as
|
|
95
|
+
| { id: string; handle: string; external_user_id: string | null }
|
|
96
|
+
| undefined)
|
|
33
97
|
: (db
|
|
34
98
|
.prepare(
|
|
35
99
|
`
|
|
36
|
-
select id, handle
|
|
100
|
+
select id, handle, external_user_id
|
|
37
101
|
from accounts
|
|
38
102
|
order by is_default desc, created_at asc
|
|
39
103
|
limit 1
|
|
40
104
|
`,
|
|
41
105
|
)
|
|
42
|
-
.get() as
|
|
106
|
+
.get() as
|
|
107
|
+
| { id: string; handle: string; external_user_id: string | null }
|
|
108
|
+
| undefined);
|
|
43
109
|
|
|
44
110
|
if (!row) {
|
|
45
111
|
throw new Error(`Unknown account: ${accountId ?? "default"}`);
|
|
@@ -48,6 +114,7 @@ function resolveAccount(db: Database, accountId?: string) {
|
|
|
48
114
|
return {
|
|
49
115
|
accountId: row.id,
|
|
50
116
|
username: row.handle.replace(/^@/, ""),
|
|
117
|
+
externalUserId: normalizeExternalUserId(row.external_user_id),
|
|
51
118
|
};
|
|
52
119
|
}
|
|
53
120
|
|
|
@@ -69,15 +136,30 @@ function toXUser(user: BirdDmUser): XurlMentionUser {
|
|
|
69
136
|
};
|
|
70
137
|
}
|
|
71
138
|
|
|
72
|
-
function collectUsers(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
139
|
+
function collectUsers(
|
|
140
|
+
payload: {
|
|
141
|
+
conversations: BirdDmConversation[];
|
|
142
|
+
events: BirdDmEvent[];
|
|
143
|
+
},
|
|
144
|
+
accountExternalUserId?: string,
|
|
145
|
+
) {
|
|
76
146
|
const users = new Map<string, BirdDmUser>();
|
|
77
147
|
const add = (user?: BirdDmUser) => {
|
|
78
148
|
if (!user?.id) return;
|
|
149
|
+
if (
|
|
150
|
+
accountExternalUserId &&
|
|
151
|
+
user.id === accountExternalUserId &&
|
|
152
|
+
!user.username &&
|
|
153
|
+
!user.name
|
|
154
|
+
) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
79
157
|
users.set(user.id, { ...users.get(user.id), ...user });
|
|
80
158
|
};
|
|
159
|
+
const addId = (id?: string) => {
|
|
160
|
+
if (!id || users.has(id) || id === accountExternalUserId) return;
|
|
161
|
+
users.set(id, { id });
|
|
162
|
+
};
|
|
81
163
|
|
|
82
164
|
for (const conversation of payload.conversations) {
|
|
83
165
|
for (const participant of conversation.participants) {
|
|
@@ -87,6 +169,8 @@ function collectUsers(payload: {
|
|
|
87
169
|
for (const event of payload.events) {
|
|
88
170
|
add(event.sender);
|
|
89
171
|
add(event.recipient);
|
|
172
|
+
addId(event.senderId);
|
|
173
|
+
addId(event.recipientId);
|
|
90
174
|
}
|
|
91
175
|
return users;
|
|
92
176
|
}
|
|
@@ -94,9 +178,14 @@ function collectUsers(payload: {
|
|
|
94
178
|
function getLocalExternalUserId(
|
|
95
179
|
users: Map<string, BirdDmUser>,
|
|
96
180
|
accountUsername: string,
|
|
181
|
+
accountExternalUserId?: string,
|
|
97
182
|
) {
|
|
183
|
+
if (accountExternalUserId) {
|
|
184
|
+
return accountExternalUserId;
|
|
185
|
+
}
|
|
186
|
+
const normalizedAccountUsername = accountUsername.toLowerCase();
|
|
98
187
|
for (const user of users.values()) {
|
|
99
|
-
if (user.username ===
|
|
188
|
+
if (user.username?.toLowerCase() === normalizedAccountUsername) {
|
|
100
189
|
return user.id;
|
|
101
190
|
}
|
|
102
191
|
}
|
|
@@ -111,22 +200,199 @@ function getLatestEvent(events: BirdDmEvent[]) {
|
|
|
111
200
|
)[0];
|
|
112
201
|
}
|
|
113
202
|
|
|
203
|
+
function assertAuthenticatedBirdAccountMatches({
|
|
204
|
+
source,
|
|
205
|
+
accountId,
|
|
206
|
+
username,
|
|
207
|
+
externalUserId,
|
|
208
|
+
liveUsername,
|
|
209
|
+
liveExternalUserId,
|
|
210
|
+
}: {
|
|
211
|
+
source: "bird" | "xurl";
|
|
212
|
+
accountId: string;
|
|
213
|
+
username: string;
|
|
214
|
+
externalUserId?: string;
|
|
215
|
+
liveUsername: string;
|
|
216
|
+
liveExternalUserId?: string;
|
|
217
|
+
}) {
|
|
218
|
+
if (
|
|
219
|
+
externalUserId &&
|
|
220
|
+
liveExternalUserId &&
|
|
221
|
+
liveExternalUserId === externalUserId
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (externalUserId && liveExternalUserId) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`${source} is authenticated as user ${liveExternalUserId}; refusing to sync into ${accountId} (${externalUserId})`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (liveUsername.toLowerCase() !== username.toLowerCase()) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`${source} is authenticated as @${liveUsername}; refusing to sync into ${accountId} (@${username})`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getAuthenticatedXurlAccount(payload: Record<string, unknown> | null): {
|
|
238
|
+
id?: string;
|
|
239
|
+
username?: string;
|
|
240
|
+
} {
|
|
241
|
+
if (!payload) return {};
|
|
242
|
+
return {
|
|
243
|
+
...(typeof payload.id === "string" ? { id: payload.id } : {}),
|
|
244
|
+
...(typeof payload.username === "string"
|
|
245
|
+
? { username: payload.username }
|
|
246
|
+
: {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function persistAccountExternalUserId(
|
|
251
|
+
db: Database,
|
|
252
|
+
accountId: string,
|
|
253
|
+
externalUserId: string,
|
|
254
|
+
) {
|
|
255
|
+
db.prepare(
|
|
256
|
+
`
|
|
257
|
+
update accounts
|
|
258
|
+
set external_user_id = ?
|
|
259
|
+
where id = ?
|
|
260
|
+
and (external_user_id is null or trim(external_user_id) = '')
|
|
261
|
+
`,
|
|
262
|
+
).run(externalUserId, accountId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function conversationIdReferencesExternalUserId(
|
|
266
|
+
conversationId: string,
|
|
267
|
+
externalUserId: string,
|
|
268
|
+
) {
|
|
269
|
+
return conversationId.split(/[^0-9]+/).includes(externalUserId);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function payloadReferencesExternalUserId(
|
|
273
|
+
payload: {
|
|
274
|
+
conversations: BirdDmConversation[];
|
|
275
|
+
events: BirdDmEvent[];
|
|
276
|
+
},
|
|
277
|
+
externalUserId: string,
|
|
278
|
+
) {
|
|
279
|
+
for (const conversation of payload.conversations) {
|
|
280
|
+
if (
|
|
281
|
+
conversationIdReferencesExternalUserId(conversation.id, externalUserId)
|
|
282
|
+
) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
if (conversation.participants.some((user) => user.id === externalUserId)) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const event of payload.events) {
|
|
290
|
+
if (
|
|
291
|
+
event.senderId === externalUserId ||
|
|
292
|
+
event.recipientId === externalUserId
|
|
293
|
+
) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
event.sender?.id === externalUserId ||
|
|
298
|
+
event.recipient?.id === externalUserId
|
|
299
|
+
) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (
|
|
303
|
+
event.conversationId &&
|
|
304
|
+
conversationIdReferencesExternalUserId(
|
|
305
|
+
event.conversationId,
|
|
306
|
+
externalUserId,
|
|
307
|
+
)
|
|
308
|
+
) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function ensureSparseLocalProfile(
|
|
316
|
+
db: Database,
|
|
317
|
+
externalUserId: string,
|
|
318
|
+
accountUsername: string,
|
|
319
|
+
) {
|
|
320
|
+
const profileId = buildExternalProfileId(externalUserId);
|
|
321
|
+
const existing = db
|
|
322
|
+
.prepare("select id from profiles where id = ? or handle = ? limit 1")
|
|
323
|
+
.get(profileId, accountUsername) as { id: string } | undefined;
|
|
324
|
+
if (existing) {
|
|
325
|
+
return existing.id;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const createdAt = new Date().toISOString();
|
|
329
|
+
db.prepare(
|
|
330
|
+
`
|
|
331
|
+
insert into profiles (
|
|
332
|
+
id, handle, display_name, bio, followers_count, following_count,
|
|
333
|
+
public_metrics_json, avatar_hue, entities_json, raw_json, created_at
|
|
334
|
+
) values (?, ?, ?, '', 0, 0, '{}', ?, '{}', '{}', ?)
|
|
335
|
+
`,
|
|
336
|
+
).run(
|
|
337
|
+
profileId,
|
|
338
|
+
accountUsername,
|
|
339
|
+
accountUsername,
|
|
340
|
+
randomAvatarHue(accountUsername),
|
|
341
|
+
createdAt,
|
|
342
|
+
);
|
|
343
|
+
return profileId;
|
|
344
|
+
}
|
|
345
|
+
|
|
114
346
|
function mergeDirectMessagesIntoLocalStore(
|
|
115
347
|
db: Database,
|
|
116
348
|
accountId: string,
|
|
117
349
|
accountUsername: string,
|
|
350
|
+
accountExternalUserId: string | undefined,
|
|
118
351
|
payload: {
|
|
119
352
|
conversations: BirdDmConversation[];
|
|
120
353
|
events: BirdDmEvent[];
|
|
121
354
|
},
|
|
122
355
|
) {
|
|
123
|
-
const users = collectUsers(payload);
|
|
124
|
-
const localExternalUserId = getLocalExternalUserId(
|
|
356
|
+
const users = collectUsers(payload, accountExternalUserId);
|
|
357
|
+
const localExternalUserId = getLocalExternalUserId(
|
|
358
|
+
users,
|
|
359
|
+
accountUsername,
|
|
360
|
+
accountExternalUserId,
|
|
361
|
+
);
|
|
362
|
+
if (
|
|
363
|
+
accountExternalUserId &&
|
|
364
|
+
(payload.conversations.length > 0 || payload.events.length > 0) &&
|
|
365
|
+
!payloadReferencesExternalUserId(payload, accountExternalUserId)
|
|
366
|
+
) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`bird DM payload does not include @${accountUsername}; refusing to sync into ${accountId}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (
|
|
372
|
+
!localExternalUserId &&
|
|
373
|
+
(payload.conversations.length > 0 || payload.events.length > 0)
|
|
374
|
+
) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`bird DM payload does not include @${accountUsername}; refusing to sync into ${accountId}`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (!localExternalUserId) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
125
382
|
const profilesByExternalId = new Map<string, string>();
|
|
126
383
|
for (const user of users.values()) {
|
|
127
384
|
const resolved = upsertProfileFromXUser(db, toXUser(user));
|
|
128
385
|
profilesByExternalId.set(user.id, resolved.profile.id);
|
|
129
386
|
}
|
|
387
|
+
if (
|
|
388
|
+
accountExternalUserId &&
|
|
389
|
+
!profilesByExternalId.has(accountExternalUserId)
|
|
390
|
+
) {
|
|
391
|
+
profilesByExternalId.set(
|
|
392
|
+
accountExternalUserId,
|
|
393
|
+
ensureSparseLocalProfile(db, accountExternalUserId, accountUsername),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
130
396
|
|
|
131
397
|
const eventsByConversation = new Map<string, BirdDmEvent[]>();
|
|
132
398
|
for (const event of payload.events) {
|
|
@@ -138,12 +404,13 @@ function mergeDirectMessagesIntoLocalStore(
|
|
|
138
404
|
|
|
139
405
|
const upsertConversation = db.prepare(`
|
|
140
406
|
insert into dm_conversations (
|
|
141
|
-
id, account_id, participant_profile_id, title, last_message_at, unread_count, needs_reply
|
|
142
|
-
) values (?, ?, ?, ?, ?, 0, ?)
|
|
407
|
+
id, account_id, participant_profile_id, title, inbox_kind, last_message_at, unread_count, needs_reply
|
|
408
|
+
) values (?, ?, ?, ?, ?, ?, 0, ?)
|
|
143
409
|
on conflict(id) do update set
|
|
144
410
|
account_id = excluded.account_id,
|
|
145
411
|
participant_profile_id = excluded.participant_profile_id,
|
|
146
412
|
title = excluded.title,
|
|
413
|
+
inbox_kind = excluded.inbox_kind,
|
|
147
414
|
last_message_at = excluded.last_message_at,
|
|
148
415
|
needs_reply = excluded.needs_reply
|
|
149
416
|
`);
|
|
@@ -159,15 +426,45 @@ function mergeDirectMessagesIntoLocalStore(
|
|
|
159
426
|
direction = excluded.direction,
|
|
160
427
|
media_count = excluded.media_count
|
|
161
428
|
`);
|
|
162
|
-
const replaceFts = db.prepare("delete from dm_fts where message_id = ?");
|
|
163
429
|
const insertFts = db.prepare(
|
|
164
430
|
"insert into dm_fts (message_id, text) values (?, ?)",
|
|
165
431
|
);
|
|
432
|
+
const deleteMessage = db.prepare("delete from dm_messages where id = ?");
|
|
433
|
+
const ftsMessageIdsToReplace = new Set<string>();
|
|
434
|
+
for (const conversation of payload.conversations) {
|
|
435
|
+
const events = eventsByConversation.get(conversation.id) ?? [];
|
|
436
|
+
if (events.length === 0 && !conversation.lastMessagePreview) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const participant =
|
|
440
|
+
conversation.participants.find(
|
|
441
|
+
(user) =>
|
|
442
|
+
user.id !== localExternalUserId &&
|
|
443
|
+
user.username?.toLowerCase() !== accountUsername.toLowerCase(),
|
|
444
|
+
) ?? conversation.participants[0];
|
|
445
|
+
if (!participant || !profilesByExternalId.has(participant.id)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (events.length === 0) {
|
|
449
|
+
ftsMessageIdsToReplace.add(makePreviewMessageId(conversation.id));
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
ftsMessageIdsToReplace.add(makePreviewMessageId(conversation.id));
|
|
453
|
+
for (const event of events) {
|
|
454
|
+
const senderId = event.senderId ?? event.sender?.id;
|
|
455
|
+
if (senderId && profilesByExternalId.has(senderId)) {
|
|
456
|
+
ftsMessageIdsToReplace.add(event.id);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
166
460
|
|
|
167
461
|
db.transaction(() => {
|
|
462
|
+
deleteDmFtsRows(db, [...ftsMessageIdsToReplace]);
|
|
463
|
+
const ftsTextByMessageId = new Map<string, string>();
|
|
464
|
+
|
|
168
465
|
for (const conversation of payload.conversations) {
|
|
169
466
|
const events = eventsByConversation.get(conversation.id) ?? [];
|
|
170
|
-
if (events.length === 0) {
|
|
467
|
+
if (events.length === 0 && !conversation.lastMessagePreview) {
|
|
171
468
|
continue;
|
|
172
469
|
}
|
|
173
470
|
|
|
@@ -175,7 +472,7 @@ function mergeDirectMessagesIntoLocalStore(
|
|
|
175
472
|
conversation.participants.find(
|
|
176
473
|
(user) =>
|
|
177
474
|
user.id !== localExternalUserId &&
|
|
178
|
-
user.username !== accountUsername,
|
|
475
|
+
user.username?.toLowerCase() !== accountUsername.toLowerCase(),
|
|
179
476
|
) ?? conversation.participants[0];
|
|
180
477
|
if (!participant) {
|
|
181
478
|
continue;
|
|
@@ -189,18 +486,47 @@ function mergeDirectMessagesIntoLocalStore(
|
|
|
189
486
|
const lastMessageAt = toIsoTimestamp(
|
|
190
487
|
latest?.createdAt ?? conversation.lastMessageAt,
|
|
191
488
|
);
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
489
|
+
const inboxKind =
|
|
490
|
+
conversation.inboxKind ??
|
|
491
|
+
(conversation.isMessageRequest ? "request" : "accepted");
|
|
492
|
+
const latestInbound = latest
|
|
493
|
+
? latest.senderId !== localExternalUserId &&
|
|
494
|
+
latest.sender?.username?.toLowerCase() !==
|
|
495
|
+
accountUsername.toLowerCase()
|
|
496
|
+
: inboxKind === "request";
|
|
195
497
|
upsertConversation.run(
|
|
196
498
|
conversation.id,
|
|
197
499
|
accountId,
|
|
198
500
|
participantProfileId,
|
|
199
501
|
participant.username ?? participant.name ?? participant.id,
|
|
502
|
+
inboxKind,
|
|
200
503
|
lastMessageAt,
|
|
201
504
|
latestInbound ? 1 : 0,
|
|
202
505
|
);
|
|
203
506
|
|
|
507
|
+
const previewMessageId = makePreviewMessageId(conversation.id);
|
|
508
|
+
if (events.length === 0 && conversation.lastMessagePreview) {
|
|
509
|
+
const previewSenderProfileId = latestInbound
|
|
510
|
+
? participantProfileId
|
|
511
|
+
: (profilesByExternalId.get(localExternalUserId) ??
|
|
512
|
+
participantProfileId);
|
|
513
|
+
upsertMessage.run(
|
|
514
|
+
previewMessageId,
|
|
515
|
+
conversation.id,
|
|
516
|
+
previewSenderProfileId,
|
|
517
|
+
conversation.lastMessagePreview,
|
|
518
|
+
lastMessageAt,
|
|
519
|
+
latestInbound ? "inbound" : "outbound",
|
|
520
|
+
);
|
|
521
|
+
ftsTextByMessageId.set(
|
|
522
|
+
previewMessageId,
|
|
523
|
+
conversation.lastMessagePreview,
|
|
524
|
+
);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
deleteMessage.run(previewMessageId);
|
|
529
|
+
|
|
204
530
|
for (const event of events) {
|
|
205
531
|
const senderId = event.senderId ?? event.sender?.id;
|
|
206
532
|
if (!senderId) {
|
|
@@ -212,7 +538,8 @@ function mergeDirectMessagesIntoLocalStore(
|
|
|
212
538
|
}
|
|
213
539
|
const direction =
|
|
214
540
|
senderId === localExternalUserId ||
|
|
215
|
-
event.sender?.username ===
|
|
541
|
+
event.sender?.username?.toLowerCase() ===
|
|
542
|
+
accountUsername.toLowerCase()
|
|
216
543
|
? "outbound"
|
|
217
544
|
: "inbound";
|
|
218
545
|
upsertMessage.run(
|
|
@@ -223,57 +550,384 @@ function mergeDirectMessagesIntoLocalStore(
|
|
|
223
550
|
toIsoTimestamp(event.createdAt),
|
|
224
551
|
direction,
|
|
225
552
|
);
|
|
226
|
-
|
|
227
|
-
insertFts.run(event.id, event.text);
|
|
553
|
+
ftsTextByMessageId.set(event.id, event.text);
|
|
228
554
|
}
|
|
229
555
|
}
|
|
556
|
+
|
|
557
|
+
for (const [messageId, text] of ftsTextByMessageId) {
|
|
558
|
+
insertFts.run(messageId, text);
|
|
559
|
+
}
|
|
230
560
|
})();
|
|
231
561
|
}
|
|
232
562
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
563
|
+
function getMetaNextToken(meta?: Record<string, unknown>) {
|
|
564
|
+
const token = meta?.next_token;
|
|
565
|
+
return typeof token === "string" && token.length > 0 ? token : undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function xurlUserToBirdDmUser(user: XurlMentionUser): BirdDmUser {
|
|
569
|
+
return {
|
|
570
|
+
id: String(user.id),
|
|
571
|
+
username: user.username,
|
|
572
|
+
name: user.name,
|
|
573
|
+
profileImageUrl: user.profile_image_url,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function uniqueDefined(values: Array<string | undefined>) {
|
|
578
|
+
return [
|
|
579
|
+
...new Set(values.filter((value): value is string => Boolean(value))),
|
|
580
|
+
];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function conversationIdForXurlEvent(
|
|
584
|
+
event: XurlDmEventsResponse["data"][number],
|
|
585
|
+
localExternalUserId: string,
|
|
586
|
+
) {
|
|
587
|
+
if (event.dm_conversation_id) {
|
|
588
|
+
return event.dm_conversation_id;
|
|
589
|
+
}
|
|
590
|
+
const ids = uniqueDefined([
|
|
591
|
+
localExternalUserId,
|
|
592
|
+
event.sender_id,
|
|
593
|
+
...(event.participant_ids ?? []),
|
|
594
|
+
]).sort();
|
|
595
|
+
return ids.length > 1 ? ids.join("-") : `dm-${event.id}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function adaptXurlDmEventsToBirdPayload({
|
|
599
|
+
payload,
|
|
600
|
+
localExternalUserId,
|
|
601
|
+
accountUsername,
|
|
238
602
|
}: {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
conversations: BirdDmConversation[];
|
|
251
|
-
events: BirdDmEvent[];
|
|
252
|
-
}>(cacheKey, db);
|
|
253
|
-
const cacheAgeMs = cached
|
|
254
|
-
? Date.now() - new Date(cached.updatedAt).getTime()
|
|
255
|
-
: Number.POSITIVE_INFINITY;
|
|
256
|
-
|
|
257
|
-
const payload =
|
|
258
|
-
!refresh && cached && cacheAgeMs <= ttlMs
|
|
259
|
-
? cached.value
|
|
260
|
-
: await listDirectMessagesViaBird({ maxResults: limit });
|
|
261
|
-
|
|
262
|
-
mergeDirectMessagesIntoLocalStore(
|
|
263
|
-
db,
|
|
264
|
-
resolvedAccount.accountId,
|
|
265
|
-
resolvedAccount.username,
|
|
266
|
-
payload,
|
|
267
|
-
);
|
|
268
|
-
if (!cached || refresh || cacheAgeMs > ttlMs) {
|
|
269
|
-
writeSyncCache(cacheKey, payload, db);
|
|
603
|
+
payload: XurlDmEventsResponse;
|
|
604
|
+
localExternalUserId: string;
|
|
605
|
+
accountUsername: string;
|
|
606
|
+
}): { conversations: BirdDmConversation[]; events: BirdDmEvent[] } {
|
|
607
|
+
const users = new Map<string, BirdDmUser>();
|
|
608
|
+
const addUser = (user?: BirdDmUser) => {
|
|
609
|
+
if (!user?.id) return;
|
|
610
|
+
users.set(user.id, { ...users.get(user.id), ...user });
|
|
611
|
+
};
|
|
612
|
+
for (const user of payload.includes?.users ?? []) {
|
|
613
|
+
addUser(xurlUserToBirdDmUser(user));
|
|
270
614
|
}
|
|
615
|
+
addUser({
|
|
616
|
+
id: localExternalUserId,
|
|
617
|
+
username: accountUsername,
|
|
618
|
+
name: accountUsername,
|
|
619
|
+
});
|
|
271
620
|
|
|
621
|
+
const eventsByConversation = new Map<string, BirdDmEvent[]>();
|
|
622
|
+
const events: BirdDmEvent[] = [];
|
|
623
|
+
for (const event of payload.data) {
|
|
624
|
+
if (event.event_type && event.event_type !== "MessageCreate") continue;
|
|
625
|
+
if (!event.id || !event.sender_id || event.text === undefined) continue;
|
|
626
|
+
const conversationId = conversationIdForXurlEvent(
|
|
627
|
+
event,
|
|
628
|
+
localExternalUserId,
|
|
629
|
+
);
|
|
630
|
+
const participantIds = uniqueDefined([
|
|
631
|
+
localExternalUserId,
|
|
632
|
+
event.sender_id,
|
|
633
|
+
...(event.participant_ids ?? []),
|
|
634
|
+
]);
|
|
635
|
+
for (const id of participantIds) {
|
|
636
|
+
addUser({ id });
|
|
637
|
+
}
|
|
638
|
+
const recipientId =
|
|
639
|
+
event.sender_id === localExternalUserId
|
|
640
|
+
? participantIds.find((id) => id !== localExternalUserId)
|
|
641
|
+
: localExternalUserId;
|
|
642
|
+
const dmEvent: BirdDmEvent = {
|
|
643
|
+
id: event.id,
|
|
644
|
+
conversationId,
|
|
645
|
+
text: event.text,
|
|
646
|
+
createdAt: event.created_at,
|
|
647
|
+
senderId: event.sender_id,
|
|
648
|
+
...(recipientId ? { recipientId } : {}),
|
|
649
|
+
sender: users.get(event.sender_id) ?? { id: event.sender_id },
|
|
650
|
+
...(recipientId
|
|
651
|
+
? { recipient: users.get(recipientId) ?? { id: recipientId } }
|
|
652
|
+
: {}),
|
|
653
|
+
inboxKind: "accepted",
|
|
654
|
+
isMessageRequest: false,
|
|
655
|
+
};
|
|
656
|
+
events.push(dmEvent);
|
|
657
|
+
const conversationEvents = eventsByConversation.get(conversationId) ?? [];
|
|
658
|
+
conversationEvents.push(dmEvent);
|
|
659
|
+
eventsByConversation.set(conversationId, conversationEvents);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const conversations = [...eventsByConversation].map(
|
|
663
|
+
([conversationId, conversationEvents]) => {
|
|
664
|
+
const latest = getLatestEvent(conversationEvents);
|
|
665
|
+
const participantIds = uniqueDefined([
|
|
666
|
+
localExternalUserId,
|
|
667
|
+
...conversationEvents.flatMap((event) => [
|
|
668
|
+
event.senderId,
|
|
669
|
+
event.recipientId,
|
|
670
|
+
]),
|
|
671
|
+
]);
|
|
672
|
+
const participants = participantIds.map((id) => users.get(id) ?? { id });
|
|
673
|
+
return {
|
|
674
|
+
id: conversationId,
|
|
675
|
+
participants,
|
|
676
|
+
messages: conversationEvents,
|
|
677
|
+
lastMessageAt: latest?.createdAt,
|
|
678
|
+
lastMessagePreview: latest?.text,
|
|
679
|
+
inboxKind: "accepted" as const,
|
|
680
|
+
isMessageRequest: false,
|
|
681
|
+
};
|
|
682
|
+
},
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
return { conversations, events };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function mergeXurlDmPages(pages: XurlDmEventsResponse[]): XurlDmEventsResponse {
|
|
689
|
+
const usersById = new Map<string, XurlMentionUser>();
|
|
690
|
+
const eventsById = new Map<string, XurlDmEventsResponse["data"][number]>();
|
|
691
|
+
let meta: Record<string, unknown> | undefined;
|
|
692
|
+
for (const page of pages) {
|
|
693
|
+
for (const user of page.includes?.users ?? []) {
|
|
694
|
+
usersById.set(user.id, { ...usersById.get(user.id), ...user });
|
|
695
|
+
}
|
|
696
|
+
for (const event of page.data) {
|
|
697
|
+
eventsById.set(event.id, event);
|
|
698
|
+
}
|
|
699
|
+
meta = page.meta ?? meta;
|
|
700
|
+
}
|
|
272
701
|
return {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
702
|
+
data: [...eventsById.values()],
|
|
703
|
+
...(usersById.size > 0
|
|
704
|
+
? { includes: { users: [...usersById.values()] } }
|
|
705
|
+
: {}),
|
|
706
|
+
...(meta ? { meta } : {}),
|
|
278
707
|
};
|
|
279
708
|
}
|
|
709
|
+
|
|
710
|
+
function sleepEffect(ms: number | undefined) {
|
|
711
|
+
return typeof ms === "number" && ms > 0 ? Effect.sleep(ms) : Effect.void;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function fetchDirectMessagesViaXurlEffect({
|
|
715
|
+
limit,
|
|
716
|
+
username,
|
|
717
|
+
maxPages,
|
|
718
|
+
allPages,
|
|
719
|
+
pageDelayMs,
|
|
720
|
+
}: {
|
|
721
|
+
limit: number;
|
|
722
|
+
username: string;
|
|
723
|
+
maxPages?: number;
|
|
724
|
+
allPages: boolean;
|
|
725
|
+
pageDelayMs?: number;
|
|
726
|
+
}) {
|
|
727
|
+
return Effect.gen(function* () {
|
|
728
|
+
const pages: XurlDmEventsResponse[] = [];
|
|
729
|
+
let paginationToken: string | undefined;
|
|
730
|
+
let pageIndex = 0;
|
|
731
|
+
const pageLimit = allPages
|
|
732
|
+
? Number.POSITIVE_INFINITY
|
|
733
|
+
: Math.max(1, (maxPages ?? 0) + 1);
|
|
734
|
+
while (pageIndex < pageLimit) {
|
|
735
|
+
const page = yield* listDirectMessageEventsViaXurlEffect({
|
|
736
|
+
maxResults: limit,
|
|
737
|
+
username,
|
|
738
|
+
...(paginationToken ? { paginationToken } : {}),
|
|
739
|
+
});
|
|
740
|
+
pages.push(page);
|
|
741
|
+
pageIndex += 1;
|
|
742
|
+
paginationToken = getMetaNextToken(page.meta);
|
|
743
|
+
if (!paginationToken) break;
|
|
744
|
+
yield* sleepEffect(pageDelayMs);
|
|
745
|
+
}
|
|
746
|
+
return mergeXurlDmPages(pages);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export function syncDirectMessagesViaCachedBirdEffect({
|
|
751
|
+
account,
|
|
752
|
+
mode,
|
|
753
|
+
limit = 20,
|
|
754
|
+
inbox = "all",
|
|
755
|
+
maxPages,
|
|
756
|
+
allPages = false,
|
|
757
|
+
pageDelayMs,
|
|
758
|
+
refresh = false,
|
|
759
|
+
cacheTtlMs,
|
|
760
|
+
}: SyncDirectMessagesViaCachedBirdOptions = {}): Effect.Effect<
|
|
761
|
+
{
|
|
762
|
+
ok: true;
|
|
763
|
+
source: "bird" | "cache" | "xurl";
|
|
764
|
+
accountId: string;
|
|
765
|
+
conversations: number;
|
|
766
|
+
messages: number;
|
|
767
|
+
},
|
|
768
|
+
unknown
|
|
769
|
+
> {
|
|
770
|
+
return Effect.gen(function* () {
|
|
771
|
+
const parsedMode = parseSyncMode(mode);
|
|
772
|
+
if (parsedMode === "xurl") {
|
|
773
|
+
assertXurlLimit(limit);
|
|
774
|
+
} else {
|
|
775
|
+
assertBirdLimit(limit);
|
|
776
|
+
}
|
|
777
|
+
if (inbox === "requests" && parsedMode === "xurl") {
|
|
778
|
+
throw new Error(
|
|
779
|
+
"xurl DM mode cannot read the message-request inbox or accept/reject state; use --mode bird",
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
const db = getNativeDb();
|
|
783
|
+
const resolvedAccount = resolveAccount(db, account);
|
|
784
|
+
const pageKey = allPages
|
|
785
|
+
? "all-pages"
|
|
786
|
+
: `max-pages:${String(maxPages ?? 0)}`;
|
|
787
|
+
const cacheMode = parsedMode === "auto" ? "auto" : parsedMode;
|
|
788
|
+
const cacheKey = `dms:${cacheMode}:${resolvedAccount.accountId}:${String(limit)}:${inbox}:${pageKey}`;
|
|
789
|
+
const ttlMs = parseCacheTtlMs(cacheTtlMs);
|
|
790
|
+
const cached = readSyncCache<{
|
|
791
|
+
conversations: BirdDmConversation[];
|
|
792
|
+
events: BirdDmEvent[];
|
|
793
|
+
}>(cacheKey, db);
|
|
794
|
+
const cacheAgeMs = cached
|
|
795
|
+
? Date.now() - new Date(cached.updatedAt).getTime()
|
|
796
|
+
: Number.POSITIVE_INFINITY;
|
|
797
|
+
|
|
798
|
+
const cacheHit = !refresh && cached && cacheAgeMs <= ttlMs;
|
|
799
|
+
let accountExternalUserId = resolvedAccount.externalUserId;
|
|
800
|
+
let payload:
|
|
801
|
+
| {
|
|
802
|
+
conversations: BirdDmConversation[];
|
|
803
|
+
events: BirdDmEvent[];
|
|
804
|
+
}
|
|
805
|
+
| undefined;
|
|
806
|
+
let source: "bird" | "xurl" | undefined;
|
|
807
|
+
if (cacheHit) {
|
|
808
|
+
payload = cached.value;
|
|
809
|
+
} else {
|
|
810
|
+
const tryXurl =
|
|
811
|
+
(parsedMode === "xurl" || parsedMode === "auto") &&
|
|
812
|
+
inbox !== "requests";
|
|
813
|
+
if (tryXurl) {
|
|
814
|
+
const xurlPayload = yield* Effect.gen(function* () {
|
|
815
|
+
const authenticated = getAuthenticatedXurlAccount(
|
|
816
|
+
yield* lookupAuthenticatedOAuth2UserEffect(
|
|
817
|
+
resolvedAccount.username,
|
|
818
|
+
),
|
|
819
|
+
);
|
|
820
|
+
if (!authenticated.username && !authenticated.id) {
|
|
821
|
+
return yield* Effect.fail(
|
|
822
|
+
new Error("xurl authenticated user unavailable"),
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
assertAuthenticatedBirdAccountMatches({
|
|
826
|
+
source: "xurl",
|
|
827
|
+
accountId: resolvedAccount.accountId,
|
|
828
|
+
username: resolvedAccount.username,
|
|
829
|
+
externalUserId: resolvedAccount.externalUserId,
|
|
830
|
+
liveUsername: authenticated.username ?? resolvedAccount.username,
|
|
831
|
+
liveExternalUserId: authenticated.id,
|
|
832
|
+
});
|
|
833
|
+
accountExternalUserId ??= authenticated.id;
|
|
834
|
+
if (!resolvedAccount.externalUserId && accountExternalUserId) {
|
|
835
|
+
persistAccountExternalUserId(
|
|
836
|
+
db,
|
|
837
|
+
resolvedAccount.accountId,
|
|
838
|
+
accountExternalUserId,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
if (!accountExternalUserId) {
|
|
842
|
+
return yield* Effect.fail(
|
|
843
|
+
new Error(
|
|
844
|
+
"xurl authenticated user id unavailable; refusing to sync DMs",
|
|
845
|
+
),
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
return yield* fetchDirectMessagesViaXurlEffect({
|
|
849
|
+
limit,
|
|
850
|
+
username: resolvedAccount.username,
|
|
851
|
+
...(typeof maxPages === "number" ? { maxPages } : {}),
|
|
852
|
+
allPages,
|
|
853
|
+
...(typeof pageDelayMs === "number" ? { pageDelayMs } : {}),
|
|
854
|
+
});
|
|
855
|
+
}).pipe(
|
|
856
|
+
Effect.catchAll((error) => {
|
|
857
|
+
if (parsedMode === "xurl") return Effect.fail(error);
|
|
858
|
+
return Effect.succeed(undefined);
|
|
859
|
+
}),
|
|
860
|
+
);
|
|
861
|
+
if (xurlPayload) {
|
|
862
|
+
const localExternalUserId = accountExternalUserId;
|
|
863
|
+
if (!localExternalUserId) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
"xurl authenticated user id unavailable; refusing to sync DMs",
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
payload = adaptXurlDmEventsToBirdPayload({
|
|
869
|
+
payload: xurlPayload,
|
|
870
|
+
localExternalUserId,
|
|
871
|
+
accountUsername: resolvedAccount.username,
|
|
872
|
+
});
|
|
873
|
+
source = "xurl";
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (!payload) {
|
|
877
|
+
const authenticated = yield* getAuthenticatedBirdAccountEffect();
|
|
878
|
+
assertAuthenticatedBirdAccountMatches({
|
|
879
|
+
source: "bird",
|
|
880
|
+
accountId: resolvedAccount.accountId,
|
|
881
|
+
username: resolvedAccount.username,
|
|
882
|
+
externalUserId: resolvedAccount.externalUserId,
|
|
883
|
+
liveUsername: authenticated.username,
|
|
884
|
+
liveExternalUserId: authenticated.id,
|
|
885
|
+
});
|
|
886
|
+
accountExternalUserId ??= authenticated.id;
|
|
887
|
+
if (!resolvedAccount.externalUserId && accountExternalUserId) {
|
|
888
|
+
persistAccountExternalUserId(
|
|
889
|
+
db,
|
|
890
|
+
resolvedAccount.accountId,
|
|
891
|
+
accountExternalUserId,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
payload = yield* listDirectMessagesViaBirdEffect({
|
|
895
|
+
maxResults: limit,
|
|
896
|
+
...(inbox !== "all" ? { inbox } : {}),
|
|
897
|
+
...(typeof maxPages === "number" ? { maxPages } : {}),
|
|
898
|
+
...(allPages ? { allPages } : {}),
|
|
899
|
+
...(typeof pageDelayMs === "number" ? { pageDelayMs } : {}),
|
|
900
|
+
});
|
|
901
|
+
source = "bird";
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (!payload) {
|
|
905
|
+
throw new Error("DM sync produced no payload");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
mergeDirectMessagesIntoLocalStore(
|
|
909
|
+
db,
|
|
910
|
+
resolvedAccount.accountId,
|
|
911
|
+
resolvedAccount.username,
|
|
912
|
+
accountExternalUserId,
|
|
913
|
+
payload,
|
|
914
|
+
);
|
|
915
|
+
if (!cached || refresh || cacheAgeMs > ttlMs) {
|
|
916
|
+
writeSyncCache(cacheKey, payload, db);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
ok: true,
|
|
921
|
+
source: cacheHit ? "cache" : source!,
|
|
922
|
+
accountId: resolvedAccount.accountId,
|
|
923
|
+
conversations: payload.conversations.length,
|
|
924
|
+
messages: payload.events.length,
|
|
925
|
+
} as const;
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function syncDirectMessagesViaCachedBird(
|
|
930
|
+
options: SyncDirectMessagesViaCachedBirdOptions = {},
|
|
931
|
+
) {
|
|
932
|
+
return runEffectPromise(syncDirectMessagesViaCachedBirdEffect(options));
|
|
933
|
+
}
|