@takosjp/yurucommu-core 3.4.3 → 3.4.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/migrations/0023_delivery_resolution_outbox.sql +33 -0
- package/migrations/0024_delivery_fanout_outbox.sql +35 -0
- package/migrations/0025_delivery_endpoint_terminal_retention.sql +6 -0
- package/migrations/0026_remote_actor_fetch_failures.sql +29 -0
- package/migrations/0027_remote_actor_tombstones.sql +12 -0
- package/migrations/0028_remote_actor_delivery_fence.sql +30 -0
- package/migrations/0029_delivery_endpoint_recipients.sql +47 -0
- package/package.json +2 -1
- package/packages/api/package.json +1 -1
- package/packages/api/src/lib/api/communities.ts +2 -0
- package/packages/api/src/lib/api/fetch.ts +69 -13
- package/packages/api/src/lib/api/normalize.ts +32 -10
- package/packages/api/src/lib/api/notifications.ts +4 -1
- package/packages/api/src/lib/rtc-client.ts +127 -29
- package/src/backend/federation-helpers.ts +1 -0
- package/src/backend/index.ts +16 -3
- package/src/backend/lib/account-migration.ts +340 -0
- package/src/backend/lib/activity-delete-cascade.ts +193 -0
- package/src/backend/lib/activitypub-actor-cache.ts +615 -48
- package/src/backend/lib/activitypub-actor-identity-sql.ts +179 -0
- package/src/backend/lib/activitypub-actor-identity.ts +39 -0
- package/src/backend/lib/activitypub-validators.ts +62 -4
- package/src/backend/lib/ap-ids.ts +36 -6
- package/src/backend/lib/ap-verify.ts +7 -17
- package/src/backend/lib/blocklist-purge.ts +676 -42
- package/src/backend/lib/blocklist.ts +278 -39
- package/src/backend/lib/community-visibility.ts +47 -33
- package/src/backend/lib/delivery/fanout-outbox.ts +336 -0
- package/src/backend/lib/delivery/planner.ts +16 -12
- package/src/backend/lib/delivery/queue-batching.ts +389 -145
- package/src/backend/lib/delivery/queue-delivery.ts +47 -25
- package/src/backend/lib/delivery/queue.ts +479 -73
- package/src/backend/lib/delivery/resolution-outbox.ts +456 -0
- package/src/backend/lib/delivery/types.ts +31 -7
- package/src/backend/lib/feed-exclude.ts +40 -28
- package/src/backend/lib/follow-edge-mutations.ts +217 -0
- package/src/backend/lib/notification-eligibility.ts +15 -9
- package/src/backend/lib/notification-push.ts +2 -2
- package/src/backend/lib/oidc-id-token.ts +66 -0
- package/src/backend/lib/personal-actor-moderation.ts +239 -0
- package/src/backend/lib/post-visibility.ts +79 -16
- package/src/backend/lib/remote-activity-id.ts +61 -0
- package/src/backend/lib/unread-counts.ts +3 -0
- package/src/backend/retention.ts +38 -1
- package/src/backend/routes/account-teardown.ts +422 -156
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +104 -105
- package/src/backend/routes/activitypub/handlers/inbound-community-scope.ts +121 -0
- package/src/backend/routes/activitypub/handlers/inbound-object-identity.ts +54 -0
- package/src/backend/routes/activitypub/handlers/inbound-reply-target.ts +34 -0
- package/src/backend/routes/activitypub/handlers/inbound-story-projection.ts +438 -0
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1121 -806
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +118 -153
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +185 -168
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +220 -32
- package/src/backend/routes/activitypub/inbound-activity-identity.ts +16 -0
- package/src/backend/routes/activitypub/inbound-activity-reference.ts +116 -0
- package/src/backend/routes/activitypub/inbound-addressing.ts +87 -0
- package/src/backend/routes/activitypub/inbox-addressing.ts +22 -19
- package/src/backend/routes/activitypub/inbox-types.ts +14 -2
- package/src/backend/routes/activitypub/inbox.ts +81 -105
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/activitypub.ts +6 -5
- package/src/backend/routes/actors-helpers.ts +34 -8
- package/src/backend/routes/actors.ts +328 -164
- package/src/backend/routes/auth-helpers.ts +57 -9
- package/src/backend/routes/auth.ts +10 -2
- package/src/backend/routes/communities/membership-invites.ts +4 -1
- package/src/backend/routes/communities/membership-members.ts +201 -73
- package/src/backend/routes/communities/membership-requests.ts +141 -76
- package/src/backend/routes/communities/membership-shared.ts +228 -21
- package/src/backend/routes/communities/messages.ts +9 -2
- package/src/backend/routes/communities/routes.ts +48 -13
- package/src/backend/routes/dm/contacts.ts +29 -41
- package/src/backend/routes/dm/conversations-helpers.ts +9 -1
- package/src/backend/routes/dm/messages.ts +36 -42
- package/src/backend/routes/dm/read-archive.ts +4 -2
- package/src/backend/routes/dm/requests.ts +62 -54
- package/src/backend/routes/follow-helpers.ts +200 -60
- package/src/backend/routes/follow.ts +146 -140
- package/src/backend/routes/media.ts +20 -94
- package/src/backend/routes/moderation.ts +72 -4
- package/src/backend/routes/notes.ts +3 -4
- package/src/backend/routes/notifications.ts +209 -108
- package/src/backend/routes/posts/delete-cascade.ts +253 -89
- package/src/backend/routes/posts/federation.ts +373 -0
- package/src/backend/routes/posts/interactions.ts +160 -184
- package/src/backend/routes/posts/like-mutation.ts +240 -0
- package/src/backend/routes/posts/post-helpers.ts +112 -162
- package/src/backend/routes/posts/queries.ts +150 -54
- package/src/backend/routes/posts/routes.ts +169 -329
- package/src/backend/routes/posts/transformers.ts +61 -6
- package/src/backend/routes/recommendations.ts +37 -44
- package/src/backend/routes/rtc/index.ts +26 -6
- package/src/backend/routes/search.ts +39 -55
- package/src/backend/routes/stories/interactions.ts +22 -6
- package/src/backend/routes/stories/query-helpers.ts +28 -41
- package/src/backend/routes/stories/routes.ts +125 -108
- package/src/backend/routes/takos-tools/dm.ts +17 -17
- package/src/backend/routes/takos-tools/posts.ts +189 -106
- package/src/backend/routes/takos-tools/search.ts +13 -4
- package/src/backend/routes/takos-tools/timeline.ts +35 -27
- package/src/backend/routes/takos-tools-response.ts +6 -2
- package/src/backend/routes/timeline.ts +5 -5
- package/src/backend/runtime/call-signaling-do.ts +35 -4
- package/src/backend/runtime/one-time-ticket.ts +116 -0
- package/src/backend/runtime/realtime-stream-do.ts +5 -61
- package/src/backend/runtime/signaling-hub.ts +36 -3
- package/src/backend/server.ts +95 -80
- package/src/db/d1-write.ts +67 -43
- package/src/db/schema/federation.ts +42 -1
- package/src/db/schema/messaging.ts +110 -2
- package/src/db/schema.ts +1 -1
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
MAX_POST_SUMMARY_LENGTH,
|
|
39
39
|
} from "./transformers.ts";
|
|
40
40
|
import {
|
|
41
|
-
buildCommunityObjectAddressing,
|
|
42
41
|
type CreatePostBody,
|
|
43
42
|
isRecord,
|
|
44
43
|
type MentionFailure,
|
|
@@ -277,20 +276,12 @@ export async function checkCommunityPostPermission(
|
|
|
277
276
|
// Reply handling
|
|
278
277
|
// ---------------------------------------------------------------------------
|
|
279
278
|
|
|
280
|
-
export const REPLY_TARGET_NOT_FOUND = "REPLY_TARGET_NOT_FOUND";
|
|
281
|
-
|
|
282
|
-
// `.batch` lives only on the concrete D1/libsql subclasses, not the Database
|
|
283
|
-
// union; reach it through a narrow structural cast (matching the other routes).
|
|
284
|
-
type Batchable = { batch: (stmts: unknown[]) => Promise<unknown> };
|
|
285
|
-
|
|
286
279
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* Throws Error(REPLY_TARGET_NOT_FOUND) if in_reply_to references a missing post.
|
|
291
|
-
* Returns the parentAuthor apId (or null if not a reply).
|
|
280
|
+
* Prepare every local statement owned by post creation without executing it.
|
|
281
|
+
* The route composes these statements with the outbound Create Activity and
|
|
282
|
+
* durable fanout intent in one D1 batch.
|
|
292
283
|
*/
|
|
293
|
-
export
|
|
284
|
+
export function preparePostInsertStatements(
|
|
294
285
|
db: Database,
|
|
295
286
|
params: {
|
|
296
287
|
apId: string;
|
|
@@ -301,34 +292,15 @@ export async function insertPostAndHandleReply(
|
|
|
301
292
|
inReplyTo: string | null;
|
|
302
293
|
visibility: string;
|
|
303
294
|
communityId: string | null;
|
|
304
|
-
|
|
295
|
+
to: string[];
|
|
296
|
+
cc: string[];
|
|
297
|
+
audience: string[];
|
|
298
|
+
tags: PostTag[];
|
|
299
|
+
parentAuthor: string | null;
|
|
305
300
|
baseUrl: string;
|
|
306
301
|
now: string;
|
|
307
302
|
},
|
|
308
|
-
):
|
|
309
|
-
let parentAuthor: string | null = null;
|
|
310
|
-
|
|
311
|
-
// Community-scoped posts are ADDRESSED to the community Group actor: the
|
|
312
|
-
// community (and its followers collection) goes into to/audience. A non-"[]"
|
|
313
|
-
// audienceJson is exactly what excludes the post from the public/home feed,
|
|
314
|
-
// so reach is the community — not the open public timeline.
|
|
315
|
-
const addressing = buildCommunityObjectAddressing(
|
|
316
|
-
params.visibility,
|
|
317
|
-
params.community,
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
// Look up + validate the reply parent BEFORE the write batch — we need its
|
|
321
|
-
// author both as the replyCount target and for the reply notification.
|
|
322
|
-
if (params.inReplyTo) {
|
|
323
|
-
const parentPost = await db
|
|
324
|
-
.select({ attributedTo: objects.attributedTo })
|
|
325
|
-
.from(objects)
|
|
326
|
-
.where(eq(objects.apId, params.inReplyTo))
|
|
327
|
-
.get();
|
|
328
|
-
if (!parentPost) throw new Error(REPLY_TARGET_NOT_FOUND);
|
|
329
|
-
parentAuthor = parentPost.attributedTo;
|
|
330
|
-
}
|
|
331
|
-
|
|
303
|
+
): [D1Statement, ...D1Statement[]] {
|
|
332
304
|
// Co-commit the object insert + author postCount++ + parent replyCount recompute
|
|
333
305
|
// in ONE batch (mirrors the federated handleCreate): a crash between separate
|
|
334
306
|
// autocommits would otherwise leave the object inserted with an un-bumped
|
|
@@ -346,16 +318,17 @@ export async function insertPostAndHandleReply(
|
|
|
346
318
|
inReplyTo: params.inReplyTo,
|
|
347
319
|
visibility: params.visibility,
|
|
348
320
|
communityApId: params.communityId,
|
|
349
|
-
toJson: JSON.stringify(
|
|
350
|
-
ccJson: JSON.stringify(
|
|
351
|
-
audienceJson: JSON.stringify(
|
|
321
|
+
toJson: JSON.stringify(params.to),
|
|
322
|
+
ccJson: JSON.stringify(params.cc),
|
|
323
|
+
audienceJson: JSON.stringify(params.audience),
|
|
324
|
+
tagsJson: JSON.stringify(params.tags),
|
|
352
325
|
published: params.now,
|
|
353
326
|
isLocal: 1,
|
|
354
|
-
});
|
|
327
|
+
}) as D1Statement;
|
|
355
328
|
const bumpPostCount = db
|
|
356
329
|
.update(actors)
|
|
357
330
|
.set({ postCount: sql`${actors.postCount} + 1` })
|
|
358
|
-
.where(and(eq(actors.apId, params.actorApId), objectAbsent));
|
|
331
|
+
.where(and(eq(actors.apId, params.actorApId), objectAbsent)) as D1Statement;
|
|
359
332
|
|
|
360
333
|
// Direct (DM) posts do NOT count toward postCount: the dedicated DM send path
|
|
361
334
|
// (createDmNote) never bumps it, and the generic DELETE skips the decrement for
|
|
@@ -364,56 +337,51 @@ export async function insertPostAndHandleReply(
|
|
|
364
337
|
// postCount over-counts permanently.
|
|
365
338
|
const countStmts = params.visibility === "direct" ? [] : [bumpPostCount];
|
|
366
339
|
|
|
340
|
+
const statements: D1Statement[] = [...countStmts, insertObject];
|
|
367
341
|
if (params.inReplyTo) {
|
|
368
342
|
const parentId = params.inReplyTo;
|
|
369
|
-
|
|
370
|
-
...countStmts,
|
|
371
|
-
insertObject,
|
|
343
|
+
statements.push(
|
|
372
344
|
db
|
|
373
345
|
.update(objects)
|
|
374
346
|
.set({
|
|
375
347
|
replyCount: sql`(SELECT COUNT(*) FROM ${objects} WHERE ${objects.inReplyTo} = ${parentId})`,
|
|
376
348
|
})
|
|
377
|
-
.where(eq(objects.apId, parentId)),
|
|
378
|
-
|
|
379
|
-
} else {
|
|
380
|
-
await (db as unknown as Batchable).batch([
|
|
381
|
-
...countStmts,
|
|
382
|
-
insertObject,
|
|
383
|
-
] as Parameters<Batchable["batch"]>[0]);
|
|
349
|
+
.where(eq(objects.apId, parentId)) as D1Statement,
|
|
350
|
+
);
|
|
384
351
|
}
|
|
385
352
|
|
|
386
|
-
if (params.inReplyTo && parentAuthor) {
|
|
353
|
+
if (params.inReplyTo && params.parentAuthor) {
|
|
387
354
|
if (
|
|
388
|
-
parentAuthor !== params.actorApId &&
|
|
389
|
-
isLocal(parentAuthor, params.baseUrl)
|
|
355
|
+
params.parentAuthor !== params.actorApId &&
|
|
356
|
+
isLocal(params.parentAuthor, params.baseUrl)
|
|
390
357
|
) {
|
|
391
358
|
const replyActivityId = activityApId(params.baseUrl, generateId());
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
actorApId: params.actorApId,
|
|
396
|
-
objectApId: params.apId,
|
|
397
|
-
rawJson: JSON.stringify({
|
|
398
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
399
|
-
id: replyActivityId,
|
|
359
|
+
statements.push(
|
|
360
|
+
db.insert(activities).values({
|
|
361
|
+
apId: replyActivityId,
|
|
400
362
|
type: "Create",
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
363
|
+
actorApId: params.actorApId,
|
|
364
|
+
objectApId: params.apId,
|
|
365
|
+
rawJson: JSON.stringify({
|
|
366
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
367
|
+
id: replyActivityId,
|
|
368
|
+
type: "Create",
|
|
369
|
+
actor: params.actorApId,
|
|
370
|
+
object: params.apId,
|
|
371
|
+
}),
|
|
372
|
+
createdAt: params.now,
|
|
373
|
+
}) as D1Statement,
|
|
374
|
+
db.insert(inboxTable).values({
|
|
375
|
+
actorApId: params.parentAuthor,
|
|
376
|
+
activityApId: replyActivityId,
|
|
377
|
+
read: 0,
|
|
378
|
+
createdAt: params.now,
|
|
379
|
+
}) as D1Statement,
|
|
380
|
+
);
|
|
413
381
|
}
|
|
414
382
|
}
|
|
415
383
|
|
|
416
|
-
return
|
|
384
|
+
return statements as [D1Statement, ...D1Statement[]];
|
|
417
385
|
}
|
|
418
386
|
|
|
419
387
|
// ---------------------------------------------------------------------------
|
|
@@ -497,15 +465,12 @@ async function resolveMentionActorRows(
|
|
|
497
465
|
return { localActors, cachedActors };
|
|
498
466
|
}
|
|
499
467
|
|
|
500
|
-
export async function
|
|
468
|
+
export async function resolvePostMentions(
|
|
501
469
|
db: Database,
|
|
502
470
|
params: {
|
|
503
471
|
content: string;
|
|
504
|
-
postApId: string;
|
|
505
472
|
actorApId: string;
|
|
506
|
-
parentAuthor: string | null;
|
|
507
473
|
baseUrl: string;
|
|
508
|
-
now: string;
|
|
509
474
|
},
|
|
510
475
|
): Promise<ProcessMentionsResult> {
|
|
511
476
|
const mentions = extractMentions(params.content);
|
|
@@ -527,38 +492,14 @@ export async function processMentions(
|
|
|
527
492
|
});
|
|
528
493
|
}
|
|
529
494
|
|
|
530
|
-
|
|
531
|
-
// `GET /ap/objects/:id` emits the same `tag` the Create carries. The object
|
|
532
|
-
// was already inserted by `insertPostAndHandleReply`, so this is an UPDATE.
|
|
533
|
-
// Only write when there is at least one tag (the column defaults to "[]").
|
|
534
|
-
const persistTags = async () => {
|
|
535
|
-
if (tags.length === 0) return;
|
|
536
|
-
try {
|
|
537
|
-
await db
|
|
538
|
-
.update(objects)
|
|
539
|
-
.set({ tagsJson: JSON.stringify(tags) })
|
|
540
|
-
.where(eq(objects.apId, params.postApId));
|
|
541
|
-
} catch (e) {
|
|
542
|
-
log.error("Failed to persist object tags", {
|
|
543
|
-
event: "posts.mention.tags_persist_failed",
|
|
544
|
-
postApId: params.postApId,
|
|
545
|
-
error: e,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
const emptyResult: ProcessMentionsResult = {
|
|
495
|
+
const result: ProcessMentionsResult = {
|
|
551
496
|
failures: mentionFailures,
|
|
552
497
|
tags,
|
|
553
498
|
mentionedActorApIds,
|
|
554
499
|
remoteMentionedActorApIds,
|
|
555
500
|
};
|
|
556
501
|
|
|
557
|
-
|
|
558
|
-
if (mentions.length === 0) {
|
|
559
|
-
await persistTags();
|
|
560
|
-
return emptyResult;
|
|
561
|
-
}
|
|
502
|
+
if (mentions.length === 0) return result;
|
|
562
503
|
|
|
563
504
|
const localMentions = mentions.filter((m) => !m.includes("@"));
|
|
564
505
|
const remoteMentions = mentions.filter((m) => m.includes("@"));
|
|
@@ -584,21 +525,6 @@ export async function processMentions(
|
|
|
584
525
|
}
|
|
585
526
|
}
|
|
586
527
|
|
|
587
|
-
const activitiesToCreate: Array<{
|
|
588
|
-
apId: string;
|
|
589
|
-
type: string;
|
|
590
|
-
actorApId: string;
|
|
591
|
-
objectApId: string;
|
|
592
|
-
rawJson: string;
|
|
593
|
-
createdAt: string;
|
|
594
|
-
}> = [];
|
|
595
|
-
const inboxEntriesToCreate: Array<{
|
|
596
|
-
actorApId: string;
|
|
597
|
-
activityApId: string;
|
|
598
|
-
read: number;
|
|
599
|
-
createdAt: string;
|
|
600
|
-
}> = [];
|
|
601
|
-
|
|
602
528
|
for (const mention of mentions) {
|
|
603
529
|
try {
|
|
604
530
|
const mentionedActorApId = mention.includes("@")
|
|
@@ -625,15 +551,51 @@ export async function processMentions(
|
|
|
625
551
|
mentionedActorApIds.push(mentionedActorApId);
|
|
626
552
|
if (remote) remoteMentionedActorApIds.push(mentionedActorApId);
|
|
627
553
|
}
|
|
554
|
+
} catch (e) {
|
|
555
|
+
log.error("Failed to process mention", {
|
|
556
|
+
event: "posts.mention.processing_failed",
|
|
557
|
+
mention,
|
|
558
|
+
error: e,
|
|
559
|
+
});
|
|
560
|
+
mentionFailures.push({
|
|
561
|
+
mention,
|
|
562
|
+
stage: "resolve",
|
|
563
|
+
reason: "mention_processing_failed",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
628
567
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// (no local inbox row), so skip both here.
|
|
632
|
-
if (params.parentAuthor === mentionedActorApId) continue;
|
|
633
|
-
if (remote) continue;
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
634
570
|
|
|
635
|
-
|
|
636
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Persist the local-notification projections for an already resolved post.
|
|
573
|
+
* The canonical Note, tags, Activity, and fanout intent are committed by the
|
|
574
|
+
* caller first; notification failures are reported without invalidating that
|
|
575
|
+
* durable federation mutation.
|
|
576
|
+
*/
|
|
577
|
+
export async function persistPostMentionProjections(
|
|
578
|
+
db: Database,
|
|
579
|
+
params: {
|
|
580
|
+
result: ProcessMentionsResult;
|
|
581
|
+
postApId: string;
|
|
582
|
+
actorApId: string;
|
|
583
|
+
parentAuthor: string | null;
|
|
584
|
+
baseUrl: string;
|
|
585
|
+
now: string;
|
|
586
|
+
},
|
|
587
|
+
): Promise<MentionFailure[]> {
|
|
588
|
+
const failures: MentionFailure[] = [];
|
|
589
|
+
const remoteActors = new Set(params.result.remoteMentionedActorApIds);
|
|
590
|
+
const localNotificationTargets = params.result.mentionedActorApIds.filter(
|
|
591
|
+
(actorApId) =>
|
|
592
|
+
actorApId !== params.parentAuthor && !remoteActors.has(actorApId),
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const activitiesToCreate = localNotificationTargets.map((actorApId) => {
|
|
596
|
+
const mentionActivityId = activityApId(params.baseUrl, generateId());
|
|
597
|
+
return {
|
|
598
|
+
activity: {
|
|
637
599
|
apId: mentionActivityId,
|
|
638
600
|
type: "Create",
|
|
639
601
|
actorApId: params.actorApId,
|
|
@@ -646,27 +608,15 @@ export async function processMentions(
|
|
|
646
608
|
object: params.postApId,
|
|
647
609
|
}),
|
|
648
610
|
createdAt: params.now,
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
actorApId: mentionedActorApId,
|
|
611
|
+
},
|
|
612
|
+
inbox: {
|
|
613
|
+
actorApId,
|
|
653
614
|
activityApId: mentionActivityId,
|
|
654
615
|
read: 0,
|
|
655
616
|
createdAt: params.now,
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
event: "posts.mention.processing_failed",
|
|
660
|
-
mention,
|
|
661
|
-
error: e,
|
|
662
|
-
});
|
|
663
|
-
mentionFailures.push({
|
|
664
|
-
mention,
|
|
665
|
-
stage: "resolve",
|
|
666
|
-
reason: "mention_processing_failed",
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
}
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
});
|
|
670
620
|
|
|
671
621
|
if (activitiesToCreate.length > 0) {
|
|
672
622
|
// A mention notification is one invariant: its activity and inbox edge
|
|
@@ -681,17 +631,21 @@ export async function processMentions(
|
|
|
681
631
|
offset < activitiesToCreate.length;
|
|
682
632
|
offset += MENTION_NOTIFICATION_PAGE_SIZE
|
|
683
633
|
) {
|
|
684
|
-
const
|
|
685
|
-
offset,
|
|
686
|
-
offset + MENTION_NOTIFICATION_PAGE_SIZE,
|
|
687
|
-
);
|
|
688
|
-
const inboxPage = inboxEntriesToCreate.slice(
|
|
634
|
+
const page = activitiesToCreate.slice(
|
|
689
635
|
offset,
|
|
690
636
|
offset + MENTION_NOTIFICATION_PAGE_SIZE,
|
|
691
637
|
);
|
|
692
638
|
const statements = [
|
|
693
|
-
...insertMany(
|
|
694
|
-
|
|
639
|
+
...insertMany(
|
|
640
|
+
db,
|
|
641
|
+
activities,
|
|
642
|
+
page.map((entry) => entry.activity),
|
|
643
|
+
),
|
|
644
|
+
...insertMany(
|
|
645
|
+
db,
|
|
646
|
+
inboxTable,
|
|
647
|
+
page.map((entry) => entry.inbox),
|
|
648
|
+
),
|
|
695
649
|
];
|
|
696
650
|
await runBatch(db, statements as [D1Statement, ...D1Statement[]]);
|
|
697
651
|
}
|
|
@@ -700,7 +654,7 @@ export async function processMentions(
|
|
|
700
654
|
event: "posts.mention.notification_persist_failed",
|
|
701
655
|
error: e,
|
|
702
656
|
});
|
|
703
|
-
|
|
657
|
+
failures.push(
|
|
704
658
|
{
|
|
705
659
|
mention: "__batch__",
|
|
706
660
|
stage: "persist_activity",
|
|
@@ -715,19 +669,15 @@ export async function processMentions(
|
|
|
715
669
|
}
|
|
716
670
|
}
|
|
717
671
|
|
|
718
|
-
|
|
719
|
-
await persistTags();
|
|
720
|
-
|
|
721
|
-
return emptyResult;
|
|
672
|
+
return failures;
|
|
722
673
|
}
|
|
723
674
|
|
|
724
675
|
/**
|
|
725
676
|
* Derive the AS2 `tag` array (Hashtag + Mention) for a post's content WITHOUT
|
|
726
677
|
* any notification / activity side effects. The EDIT path uses this so an
|
|
727
678
|
* edited post's served object + Update(Note) carry the SAME tags a fresh post
|
|
728
|
-
* would — re-running
|
|
729
|
-
* every mention. Mirrors
|
|
730
|
-
* side-effect-free function so the create/federation path stays untouched).
|
|
679
|
+
* would — re-running notification projection on every edit would re-notify
|
|
680
|
+
* every mention. Mirrors resolvePostMentions' tag-building.
|
|
731
681
|
*/
|
|
732
682
|
export async function deriveContentTags(
|
|
733
683
|
db: Database,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Addressing logic for ActivityPub delivery
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { Database } from "../../../db/index.ts";
|
|
10
|
+
import type { D1Statement, Database } from "../../../db/index.ts";
|
|
11
11
|
import {
|
|
12
12
|
activities,
|
|
13
13
|
actorCache,
|
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
communities,
|
|
16
16
|
likes,
|
|
17
17
|
objects,
|
|
18
|
+
runBatch,
|
|
18
19
|
} from "../../../db/index.ts";
|
|
19
|
-
import { and, eq, inArray, or } from "drizzle-orm";
|
|
20
|
+
import { and, eq, inArray, isNull, or } from "drizzle-orm";
|
|
20
21
|
import type { Env } from "../../types.ts";
|
|
21
22
|
import {
|
|
22
23
|
activityApId,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
} from "../../lib/delivery/queue.ts";
|
|
32
33
|
import { isRecord, parseJsonObject } from "../../lib/parse-helpers.ts";
|
|
33
34
|
import { logger } from "../../lib/logger.ts";
|
|
35
|
+
import { prepareDeliveryFanoutJob } from "../../lib/delivery/fanout-outbox.ts";
|
|
34
36
|
|
|
35
37
|
const log = logger.child({ component: "posts.queries" });
|
|
36
38
|
|
|
@@ -398,32 +400,71 @@ export async function persistActivity(
|
|
|
398
400
|
}
|
|
399
401
|
|
|
400
402
|
/** Persist an outbound ActivityPub activity and enqueue federation fanout. */
|
|
401
|
-
export
|
|
403
|
+
export type PreparedActivityFanout = {
|
|
404
|
+
readonly statements: readonly [D1Statement, ...D1Statement[]];
|
|
405
|
+
/** Best-effort Queue wakeup; the durable intent is already in statements. */
|
|
406
|
+
publish(): Promise<void>;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Prepare an outbound Activity + durable follower-fanout intent without
|
|
411
|
+
* executing either statement. Callers that also mutate local state can place
|
|
412
|
+
* these statements in the SAME D1 batch and avoid a local-only partial commit.
|
|
413
|
+
*/
|
|
414
|
+
export async function preparePersistAndFanout(
|
|
402
415
|
db: Database,
|
|
403
416
|
env: Env,
|
|
404
417
|
activity: { id: string; type: string; actor: string; [key: string]: unknown },
|
|
405
418
|
objectApIdValue: string,
|
|
406
|
-
): Promise<
|
|
407
|
-
await db
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
objectApId: objectApIdValue,
|
|
412
|
-
rawJson: JSON.stringify(activity),
|
|
413
|
-
direction: "outbound",
|
|
419
|
+
): Promise<PreparedActivityFanout> {
|
|
420
|
+
const preparedFanout = await prepareDeliveryFanoutJob(db, {
|
|
421
|
+
kind: "followers",
|
|
422
|
+
activityId: activity.id,
|
|
423
|
+
targetApId: activity.actor,
|
|
414
424
|
});
|
|
425
|
+
return {
|
|
426
|
+
statements: [
|
|
427
|
+
db.insert(activities).values({
|
|
428
|
+
apId: activity.id,
|
|
429
|
+
type: activity.type,
|
|
430
|
+
actorApId: activity.actor,
|
|
431
|
+
objectApId: objectApIdValue,
|
|
432
|
+
rawJson: JSON.stringify(activity),
|
|
433
|
+
direction: "outbound",
|
|
434
|
+
}) as D1Statement,
|
|
435
|
+
preparedFanout.statement,
|
|
436
|
+
],
|
|
437
|
+
async publish() {
|
|
438
|
+
try {
|
|
439
|
+
await enqueueFanoutToFollowers(env, activity.id, activity.actor);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
log.error("Failed to enqueue federation fanout", {
|
|
442
|
+
event: "posts.fanout.enqueue_failed",
|
|
443
|
+
activityType: activity.type,
|
|
444
|
+
activityId: activity.id,
|
|
445
|
+
actor: activity.actor,
|
|
446
|
+
error: err,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
415
452
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
453
|
+
/** Persist an outbound ActivityPub activity and enqueue federation fanout. */
|
|
454
|
+
export async function persistAndFanout(
|
|
455
|
+
db: Database,
|
|
456
|
+
env: Env,
|
|
457
|
+
activity: { id: string; type: string; actor: string; [key: string]: unknown },
|
|
458
|
+
objectApIdValue: string,
|
|
459
|
+
): Promise<void> {
|
|
460
|
+
const prepared = await preparePersistAndFanout(
|
|
461
|
+
db,
|
|
462
|
+
env,
|
|
463
|
+
activity,
|
|
464
|
+
objectApIdValue,
|
|
465
|
+
);
|
|
466
|
+
await runBatch(db, prepared.statements);
|
|
467
|
+
await prepared.publish();
|
|
427
468
|
}
|
|
428
469
|
|
|
429
470
|
/**
|
|
@@ -431,22 +472,13 @@ export async function persistAndFanout(
|
|
|
431
472
|
* (members + community followers) instead of the author's personal followers.
|
|
432
473
|
* Used for community-scoped posts so reach == community.
|
|
433
474
|
*/
|
|
434
|
-
export async function
|
|
475
|
+
export async function preparePersistAndFanoutToCommunity(
|
|
435
476
|
db: Database,
|
|
436
477
|
env: Env,
|
|
437
478
|
activity: { id: string; type: string; actor: string; [key: string]: unknown },
|
|
438
479
|
objectApIdValue: string,
|
|
439
480
|
communityApId: string,
|
|
440
|
-
): Promise<
|
|
441
|
-
await db.insert(activities).values({
|
|
442
|
-
apId: activity.id,
|
|
443
|
-
type: activity.type,
|
|
444
|
-
actorApId: activity.actor,
|
|
445
|
-
objectApId: objectApIdValue,
|
|
446
|
-
rawJson: JSON.stringify(activity),
|
|
447
|
-
direction: "outbound",
|
|
448
|
-
});
|
|
449
|
-
|
|
481
|
+
): Promise<PreparedActivityFanout> {
|
|
450
482
|
// Announce-relay: for a new post (Create), the GROUP re-broadcasts it to its
|
|
451
483
|
// followers as its OWN Announce (Lemmy/Mobilizon convention) so the post is
|
|
452
484
|
// attributed to the community and reaches remote followers whose server
|
|
@@ -467,12 +499,35 @@ export async function persistAndFanoutToCommunity(
|
|
|
467
499
|
const row = await db
|
|
468
500
|
.select({ visibility: communities.visibility })
|
|
469
501
|
.from(communities)
|
|
470
|
-
.where(
|
|
502
|
+
.where(
|
|
503
|
+
and(eq(communities.apId, communityApId), isNull(communities.deletedAt)),
|
|
504
|
+
)
|
|
471
505
|
.get();
|
|
472
|
-
|
|
506
|
+
if (!row) {
|
|
507
|
+
log.warn("Skipped fanout for a retired community", {
|
|
508
|
+
event: "posts.fanout.community_retired",
|
|
509
|
+
activityId: activity.id,
|
|
510
|
+
communityApId,
|
|
511
|
+
});
|
|
512
|
+
return {
|
|
513
|
+
statements: [
|
|
514
|
+
db.insert(activities).values({
|
|
515
|
+
apId: activity.id,
|
|
516
|
+
type: activity.type,
|
|
517
|
+
actorApId: activity.actor,
|
|
518
|
+
objectApId: objectApIdValue,
|
|
519
|
+
rawJson: JSON.stringify(activity),
|
|
520
|
+
direction: "outbound",
|
|
521
|
+
}) as D1Statement,
|
|
522
|
+
],
|
|
523
|
+
async publish() {},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
isPublicCommunity = row.visibility === "public";
|
|
473
527
|
}
|
|
474
528
|
|
|
475
529
|
let announceActivityId: string | undefined;
|
|
530
|
+
let announceStatement: D1Statement | undefined;
|
|
476
531
|
if (activity.type === "Create" && isPublicCommunity) {
|
|
477
532
|
const baseUrl = env.APP_URL;
|
|
478
533
|
announceActivityId = activityApId(baseUrl, generateId());
|
|
@@ -486,33 +541,74 @@ export async function persistAndFanoutToCommunity(
|
|
|
486
541
|
cc: [PUBLIC_URL],
|
|
487
542
|
published: new Date().toISOString(),
|
|
488
543
|
};
|
|
489
|
-
|
|
544
|
+
announceStatement = db.insert(activities).values({
|
|
490
545
|
apId: announceActivityId,
|
|
491
546
|
type: "Announce",
|
|
492
547
|
actorApId: communityApId,
|
|
493
548
|
objectApId: objectApIdValue,
|
|
494
549
|
rawJson: JSON.stringify(announce),
|
|
495
550
|
direction: "outbound",
|
|
496
|
-
});
|
|
551
|
+
}) as D1Statement;
|
|
497
552
|
}
|
|
498
553
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
554
|
+
const preparedFanout = await prepareDeliveryFanoutJob(db, {
|
|
555
|
+
kind: "community",
|
|
556
|
+
activityId: activity.id,
|
|
557
|
+
targetApId: communityApId,
|
|
558
|
+
...(announceActivityId ? { announceActivityId } : {}),
|
|
559
|
+
});
|
|
560
|
+
const statements: D1Statement[] = [
|
|
561
|
+
db.insert(activities).values({
|
|
562
|
+
apId: activity.id,
|
|
563
|
+
type: activity.type,
|
|
564
|
+
actorApId: activity.actor,
|
|
565
|
+
objectApId: objectApIdValue,
|
|
566
|
+
rawJson: JSON.stringify(activity),
|
|
567
|
+
direction: "outbound",
|
|
568
|
+
}) as D1Statement,
|
|
569
|
+
];
|
|
570
|
+
if (announceStatement) statements.push(announceStatement);
|
|
571
|
+
statements.push(preparedFanout.statement);
|
|
572
|
+
return {
|
|
573
|
+
statements: statements as [D1Statement, ...D1Statement[]],
|
|
574
|
+
async publish() {
|
|
575
|
+
try {
|
|
576
|
+
await enqueueFanoutToCommunity(
|
|
577
|
+
env,
|
|
578
|
+
activity.id,
|
|
579
|
+
communityApId,
|
|
580
|
+
announceActivityId,
|
|
581
|
+
);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
log.error("Failed to enqueue community federation fanout", {
|
|
584
|
+
event: "posts.fanout.community_enqueue_failed",
|
|
585
|
+
activityType: activity.type,
|
|
586
|
+
activityId: activity.id,
|
|
587
|
+
actor: activity.actor,
|
|
588
|
+
communityApId,
|
|
589
|
+
error: err,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export async function persistAndFanoutToCommunity(
|
|
597
|
+
db: Database,
|
|
598
|
+
env: Env,
|
|
599
|
+
activity: { id: string; type: string; actor: string; [key: string]: unknown },
|
|
600
|
+
objectApIdValue: string,
|
|
601
|
+
communityApId: string,
|
|
602
|
+
): Promise<void> {
|
|
603
|
+
const prepared = await preparePersistAndFanoutToCommunity(
|
|
604
|
+
db,
|
|
605
|
+
env,
|
|
606
|
+
activity,
|
|
607
|
+
objectApIdValue,
|
|
608
|
+
communityApId,
|
|
609
|
+
);
|
|
610
|
+
await runBatch(db, prepared.statements);
|
|
611
|
+
await prepared.publish();
|
|
516
612
|
}
|
|
517
613
|
|
|
518
614
|
/** Batch-load cached authors for posts without a local author join. */
|