@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.
Files changed (112) hide show
  1. package/migrations/0023_delivery_resolution_outbox.sql +33 -0
  2. package/migrations/0024_delivery_fanout_outbox.sql +35 -0
  3. package/migrations/0025_delivery_endpoint_terminal_retention.sql +6 -0
  4. package/migrations/0026_remote_actor_fetch_failures.sql +29 -0
  5. package/migrations/0027_remote_actor_tombstones.sql +12 -0
  6. package/migrations/0028_remote_actor_delivery_fence.sql +30 -0
  7. package/migrations/0029_delivery_endpoint_recipients.sql +47 -0
  8. package/package.json +2 -1
  9. package/packages/api/package.json +1 -1
  10. package/packages/api/src/lib/api/communities.ts +2 -0
  11. package/packages/api/src/lib/api/fetch.ts +69 -13
  12. package/packages/api/src/lib/api/normalize.ts +32 -10
  13. package/packages/api/src/lib/api/notifications.ts +4 -1
  14. package/packages/api/src/lib/rtc-client.ts +127 -29
  15. package/src/backend/federation-helpers.ts +1 -0
  16. package/src/backend/index.ts +16 -3
  17. package/src/backend/lib/account-migration.ts +340 -0
  18. package/src/backend/lib/activity-delete-cascade.ts +193 -0
  19. package/src/backend/lib/activitypub-actor-cache.ts +615 -48
  20. package/src/backend/lib/activitypub-actor-identity-sql.ts +179 -0
  21. package/src/backend/lib/activitypub-actor-identity.ts +39 -0
  22. package/src/backend/lib/activitypub-validators.ts +62 -4
  23. package/src/backend/lib/ap-ids.ts +36 -6
  24. package/src/backend/lib/ap-verify.ts +7 -17
  25. package/src/backend/lib/blocklist-purge.ts +676 -42
  26. package/src/backend/lib/blocklist.ts +278 -39
  27. package/src/backend/lib/community-visibility.ts +47 -33
  28. package/src/backend/lib/delivery/fanout-outbox.ts +336 -0
  29. package/src/backend/lib/delivery/planner.ts +16 -12
  30. package/src/backend/lib/delivery/queue-batching.ts +389 -145
  31. package/src/backend/lib/delivery/queue-delivery.ts +47 -25
  32. package/src/backend/lib/delivery/queue.ts +479 -73
  33. package/src/backend/lib/delivery/resolution-outbox.ts +456 -0
  34. package/src/backend/lib/delivery/types.ts +31 -7
  35. package/src/backend/lib/feed-exclude.ts +40 -28
  36. package/src/backend/lib/follow-edge-mutations.ts +217 -0
  37. package/src/backend/lib/notification-eligibility.ts +15 -9
  38. package/src/backend/lib/notification-push.ts +2 -2
  39. package/src/backend/lib/oidc-id-token.ts +66 -0
  40. package/src/backend/lib/personal-actor-moderation.ts +239 -0
  41. package/src/backend/lib/post-visibility.ts +79 -16
  42. package/src/backend/lib/remote-activity-id.ts +61 -0
  43. package/src/backend/lib/unread-counts.ts +3 -0
  44. package/src/backend/retention.ts +38 -1
  45. package/src/backend/routes/account-teardown.ts +422 -156
  46. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +104 -105
  47. package/src/backend/routes/activitypub/handlers/inbound-community-scope.ts +121 -0
  48. package/src/backend/routes/activitypub/handlers/inbound-object-identity.ts +54 -0
  49. package/src/backend/routes/activitypub/handlers/inbound-reply-target.ts +34 -0
  50. package/src/backend/routes/activitypub/handlers/inbound-story-projection.ts +438 -0
  51. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1121 -806
  52. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +118 -153
  53. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +185 -168
  54. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +220 -32
  55. package/src/backend/routes/activitypub/inbound-activity-identity.ts +16 -0
  56. package/src/backend/routes/activitypub/inbound-activity-reference.ts +116 -0
  57. package/src/backend/routes/activitypub/inbound-addressing.ts +87 -0
  58. package/src/backend/routes/activitypub/inbox-addressing.ts +22 -19
  59. package/src/backend/routes/activitypub/inbox-types.ts +14 -2
  60. package/src/backend/routes/activitypub/inbox.ts +81 -105
  61. package/src/backend/routes/activitypub/outbox.ts +0 -0
  62. package/src/backend/routes/activitypub.ts +6 -5
  63. package/src/backend/routes/actors-helpers.ts +34 -8
  64. package/src/backend/routes/actors.ts +328 -164
  65. package/src/backend/routes/auth-helpers.ts +57 -9
  66. package/src/backend/routes/auth.ts +10 -2
  67. package/src/backend/routes/communities/membership-invites.ts +4 -1
  68. package/src/backend/routes/communities/membership-members.ts +201 -73
  69. package/src/backend/routes/communities/membership-requests.ts +141 -76
  70. package/src/backend/routes/communities/membership-shared.ts +228 -21
  71. package/src/backend/routes/communities/messages.ts +9 -2
  72. package/src/backend/routes/communities/routes.ts +48 -13
  73. package/src/backend/routes/dm/contacts.ts +29 -41
  74. package/src/backend/routes/dm/conversations-helpers.ts +9 -1
  75. package/src/backend/routes/dm/messages.ts +36 -42
  76. package/src/backend/routes/dm/read-archive.ts +4 -2
  77. package/src/backend/routes/dm/requests.ts +62 -54
  78. package/src/backend/routes/follow-helpers.ts +200 -60
  79. package/src/backend/routes/follow.ts +146 -140
  80. package/src/backend/routes/media.ts +20 -94
  81. package/src/backend/routes/moderation.ts +72 -4
  82. package/src/backend/routes/notes.ts +3 -4
  83. package/src/backend/routes/notifications.ts +209 -108
  84. package/src/backend/routes/posts/delete-cascade.ts +253 -89
  85. package/src/backend/routes/posts/federation.ts +373 -0
  86. package/src/backend/routes/posts/interactions.ts +160 -184
  87. package/src/backend/routes/posts/like-mutation.ts +240 -0
  88. package/src/backend/routes/posts/post-helpers.ts +112 -162
  89. package/src/backend/routes/posts/queries.ts +150 -54
  90. package/src/backend/routes/posts/routes.ts +169 -329
  91. package/src/backend/routes/posts/transformers.ts +61 -6
  92. package/src/backend/routes/recommendations.ts +37 -44
  93. package/src/backend/routes/rtc/index.ts +26 -6
  94. package/src/backend/routes/search.ts +39 -55
  95. package/src/backend/routes/stories/interactions.ts +22 -6
  96. package/src/backend/routes/stories/query-helpers.ts +28 -41
  97. package/src/backend/routes/stories/routes.ts +125 -108
  98. package/src/backend/routes/takos-tools/dm.ts +17 -17
  99. package/src/backend/routes/takos-tools/posts.ts +189 -106
  100. package/src/backend/routes/takos-tools/search.ts +13 -4
  101. package/src/backend/routes/takos-tools/timeline.ts +35 -27
  102. package/src/backend/routes/takos-tools-response.ts +6 -2
  103. package/src/backend/routes/timeline.ts +5 -5
  104. package/src/backend/runtime/call-signaling-do.ts +35 -4
  105. package/src/backend/runtime/one-time-ticket.ts +116 -0
  106. package/src/backend/runtime/realtime-stream-do.ts +5 -61
  107. package/src/backend/runtime/signaling-hub.ts +36 -3
  108. package/src/backend/server.ts +95 -80
  109. package/src/db/d1-write.ts +67 -43
  110. package/src/db/schema/federation.ts +42 -1
  111. package/src/db/schema/messaging.ts +110 -2
  112. package/src/db/schema.ts +1 -1
@@ -0,0 +1,373 @@
1
+ import { eq } from "drizzle-orm";
2
+
3
+ import {
4
+ activities,
5
+ objects,
6
+ type D1Statement,
7
+ type Database,
8
+ } from "../../../db/index.ts";
9
+ import { OBJECT_CONTEXT } from "../../lib/ap-context.ts";
10
+ import {
11
+ activityApId,
12
+ formatUsername,
13
+ generateId,
14
+ isLocal,
15
+ isSafeRemoteUrl,
16
+ safeJsonParse,
17
+ } from "../../federation-helpers.ts";
18
+ import { toApAttachments } from "../../lib/activitypub-helpers.ts";
19
+ import { enqueueDeliveryToActor } from "../../lib/delivery/queue.ts";
20
+ import { logger } from "../../lib/logger.ts";
21
+ import type { Env } from "../../types.ts";
22
+ import {
23
+ persistPostMentionProjections,
24
+ resolvePostMentions,
25
+ } from "./post-helpers.ts";
26
+ import {
27
+ buildAddressing,
28
+ buildCommunityObjectAddressing,
29
+ mergeCc,
30
+ preparePersistAndFanout,
31
+ preparePersistAndFanoutToCommunity,
32
+ type CommunityAddressingTarget,
33
+ type MentionFailure,
34
+ type PostAttachment,
35
+ type PostTag,
36
+ type ProcessMentionsResult,
37
+ } from "./queries.ts";
38
+
39
+ const log = logger.child({ component: "posts.federation" });
40
+ const PUBLIC_COLLECTION = "https://www.w3.org/ns/activitystreams#Public";
41
+
42
+ export type CreatedPostFederationInput = {
43
+ db: Database;
44
+ env: Env;
45
+ actorApId: string;
46
+ objectApId: string;
47
+ content: string;
48
+ summary: string | null;
49
+ attachments?: PostAttachment[];
50
+ inReplyTo: string | null;
51
+ parentAuthor: string | null;
52
+ visibility: string;
53
+ community: CommunityAddressingTarget | null;
54
+ published: string;
55
+ };
56
+
57
+ export type PreparedCreatedPostFederation = {
58
+ readonly to: string[];
59
+ readonly cc: string[];
60
+ readonly audience: string[];
61
+ readonly tags: PostTag[];
62
+ readonly statements: readonly D1Statement[];
63
+ complete(): Promise<{ mentionFailures: MentionFailure[] }>;
64
+ };
65
+
66
+ /**
67
+ * Resolve and prepare a post's outbound Create without writing anything. The
68
+ * caller co-commits these statements with the local Note/counter mutation,
69
+ * then invokes complete() for best-effort Queue wakeups and notifications.
70
+ */
71
+ export async function prepareCreatedPostFederation(
72
+ input: CreatedPostFederationInput,
73
+ ): Promise<PreparedCreatedPostFederation> {
74
+ const {
75
+ db,
76
+ env,
77
+ actorApId,
78
+ objectApId: postApId,
79
+ content,
80
+ summary,
81
+ attachments,
82
+ inReplyTo,
83
+ parentAuthor,
84
+ visibility,
85
+ community,
86
+ published,
87
+ } = input;
88
+ const baseUrl = env.APP_URL;
89
+
90
+ const {
91
+ failures: mentionFailures,
92
+ tags: mentionTags,
93
+ mentionedActorApIds,
94
+ remoteMentionedActorApIds,
95
+ } = await resolvePostMentions(db, {
96
+ content,
97
+ actorApId,
98
+ baseUrl,
99
+ });
100
+ const resolvedMentions: ProcessMentionsResult = {
101
+ failures: mentionFailures,
102
+ tags: mentionTags,
103
+ mentionedActorApIds,
104
+ remoteMentionedActorApIds,
105
+ };
106
+
107
+ // A non-direct reply must reach its parent author's instance even when the
108
+ // body does not repeat an @mention. Direct reach remains explicit-only.
109
+ const recipients = [...mentionedActorApIds];
110
+ const remoteRecipients = [...remoteMentionedActorApIds];
111
+ const tags = [...mentionTags];
112
+ if (
113
+ inReplyTo &&
114
+ parentAuthor &&
115
+ parentAuthor !== actorApId &&
116
+ visibility !== "direct" &&
117
+ !recipients.includes(parentAuthor)
118
+ ) {
119
+ recipients.push(parentAuthor);
120
+ tags.push({
121
+ type: "Mention",
122
+ href: parentAuthor,
123
+ name: `@${formatUsername(parentAuthor)}`,
124
+ });
125
+ if (!isLocal(parentAuthor, baseUrl)) remoteRecipients.push(parentAuthor);
126
+ }
127
+
128
+ // A plain direct Note with no resolved recipient has no federation reach.
129
+ let to: string[];
130
+ let cc: string[];
131
+ let audience: string[] | undefined;
132
+ if (visibility === "direct") {
133
+ to = [];
134
+ cc = [];
135
+ } else if (community) {
136
+ const addressing = buildCommunityObjectAddressing(visibility, community);
137
+ to = addressing.to;
138
+ cc = addressing.cc;
139
+ audience = addressing.audience;
140
+ } else {
141
+ ({ to, cc } = buildAddressing(visibility, `${actorApId}/followers`));
142
+ }
143
+ cc = mergeCc(cc, recipients);
144
+
145
+ const tag = tags.length > 0 ? tags : undefined;
146
+ const createActivity = {
147
+ "@context": OBJECT_CONTEXT,
148
+ id: activityApId(baseUrl, generateId()),
149
+ type: "Create",
150
+ actor: actorApId,
151
+ published,
152
+ to,
153
+ cc,
154
+ ...(audience ? { audience } : {}),
155
+ ...(tag ? { tag } : {}),
156
+ object: {
157
+ "@context": OBJECT_CONTEXT,
158
+ id: postApId,
159
+ type: "Note",
160
+ attributedTo: actorApId,
161
+ content,
162
+ summary,
163
+ ...(summary ? { sensitive: true } : {}),
164
+ attachment: toApAttachments(attachments || [], baseUrl),
165
+ inReplyTo,
166
+ published,
167
+ to,
168
+ cc,
169
+ ...(audience ? { audience } : {}),
170
+ ...(tag ? { tag } : {}),
171
+ },
172
+ };
173
+
174
+ let statements: readonly D1Statement[] = [];
175
+ let publish = async () => {};
176
+ // A plain direct Note with no resolved recipient has no federation reach and
177
+ // therefore no outbound Activity. The local object still commits below.
178
+ if (visibility === "direct" && mentionedActorApIds.length === 0) {
179
+ statements = [];
180
+ } else if (visibility === "direct") {
181
+ statements = [
182
+ db.insert(activities).values({
183
+ apId: createActivity.id,
184
+ type: createActivity.type,
185
+ actorApId: createActivity.actor,
186
+ objectApId: postApId,
187
+ rawJson: JSON.stringify(createActivity),
188
+ direction: "outbound",
189
+ }) as D1Statement,
190
+ ];
191
+ } else if (community) {
192
+ const prepared = await preparePersistAndFanoutToCommunity(
193
+ db,
194
+ env,
195
+ createActivity,
196
+ postApId,
197
+ community.apId,
198
+ );
199
+ statements = prepared.statements;
200
+ publish = prepared.publish;
201
+ } else {
202
+ const prepared = await preparePersistAndFanout(
203
+ db,
204
+ env,
205
+ createActivity,
206
+ postApId,
207
+ );
208
+ statements = prepared.statements;
209
+ publish = prepared.publish;
210
+ }
211
+
212
+ return {
213
+ to,
214
+ cc,
215
+ audience: audience ?? [],
216
+ tags,
217
+ statements,
218
+ async complete() {
219
+ await publish();
220
+
221
+ const persistedMentionFailures = await persistPostMentionProjections(db, {
222
+ result: resolvedMentions,
223
+ postApId,
224
+ actorApId,
225
+ parentAuthor,
226
+ baseUrl,
227
+ now: published,
228
+ });
229
+
230
+ for (const recipient of new Set(remoteRecipients)) {
231
+ try {
232
+ await enqueueDeliveryToActor(env, createActivity.id, recipient);
233
+ } catch (error) {
234
+ log.error("Failed to enqueue explicit Create delivery", {
235
+ event: "posts.create.delivery_enqueue_failed",
236
+ activityId: createActivity.id,
237
+ recipient,
238
+ error,
239
+ });
240
+ }
241
+ }
242
+
243
+ return {
244
+ mentionFailures: [...mentionFailures, ...persistedMentionFailures],
245
+ };
246
+ },
247
+ };
248
+ }
249
+
250
+ export type DeletedPostFederationInput = {
251
+ db: Database;
252
+ env: Env;
253
+ actorApId: string;
254
+ post: {
255
+ apId: string;
256
+ inReplyTo: string | null;
257
+ visibility: string;
258
+ communityApId: string | null;
259
+ toJson: string;
260
+ ccJson: string;
261
+ };
262
+ };
263
+
264
+ export type PreparedDeletedPostFederation = {
265
+ readonly statements: readonly [D1Statement, ...D1Statement[]];
266
+ complete(): Promise<string>;
267
+ };
268
+
269
+ /** Prepare a Tombstone Delete so its owner can co-commit local removal. */
270
+ export async function prepareDeletedPostFederation(
271
+ input: DeletedPostFederationInput,
272
+ ): Promise<PreparedDeletedPostFederation> {
273
+ const { db, env, actorApId, post } = input;
274
+ const baseUrl = env.APP_URL;
275
+ const originalTo = safeJsonParse<string[]>(post.toJson, []);
276
+ const originalCc = safeJsonParse<string[]>(post.ccJson, []);
277
+ const explicitRecipients = new Set<string>();
278
+
279
+ for (const iri of [...originalTo, ...originalCc]) {
280
+ if (
281
+ iri &&
282
+ iri !== PUBLIC_COLLECTION &&
283
+ !iri.endsWith("/followers") &&
284
+ !isLocal(iri, baseUrl) &&
285
+ isSafeRemoteUrl(iri)
286
+ ) {
287
+ explicitRecipients.add(iri);
288
+ }
289
+ }
290
+
291
+ if (post.inReplyTo) {
292
+ const parent = await db
293
+ .select({ attributedTo: objects.attributedTo })
294
+ .from(objects)
295
+ .where(eq(objects.apId, post.inReplyTo))
296
+ .get();
297
+ if (
298
+ parent?.attributedTo &&
299
+ !isLocal(parent.attributedTo, baseUrl) &&
300
+ isSafeRemoteUrl(parent.attributedTo)
301
+ ) {
302
+ explicitRecipients.add(parent.attributedTo);
303
+ }
304
+ }
305
+
306
+ const deleteActivity = {
307
+ "@context": "https://www.w3.org/ns/activitystreams",
308
+ id: activityApId(baseUrl, generateId()),
309
+ type: "Delete",
310
+ actor: actorApId,
311
+ to: originalTo,
312
+ cc: originalCc,
313
+ object: { id: post.apId, type: "Tombstone" },
314
+ };
315
+
316
+ // Direct objects were never broadcast to followers. Persist their Delete for
317
+ // the ledger and send only to explicit recipients; follower fanout here would
318
+ // disclose the existence and id of a private object.
319
+ let statements: readonly [D1Statement, ...D1Statement[]];
320
+ let publish = async () => {};
321
+ if (post.visibility === "direct") {
322
+ statements = [
323
+ db.insert(activities).values({
324
+ apId: deleteActivity.id,
325
+ type: deleteActivity.type,
326
+ actorApId: deleteActivity.actor,
327
+ objectApId: post.apId,
328
+ rawJson: JSON.stringify(deleteActivity),
329
+ direction: "outbound",
330
+ }) as D1Statement,
331
+ ];
332
+ } else if (post.communityApId) {
333
+ const prepared = await preparePersistAndFanoutToCommunity(
334
+ db,
335
+ env,
336
+ deleteActivity,
337
+ post.apId,
338
+ post.communityApId,
339
+ );
340
+ statements = prepared.statements;
341
+ publish = prepared.publish;
342
+ } else {
343
+ const prepared = await preparePersistAndFanout(
344
+ db,
345
+ env,
346
+ deleteActivity,
347
+ post.apId,
348
+ );
349
+ statements = prepared.statements;
350
+ publish = prepared.publish;
351
+ }
352
+
353
+ return {
354
+ statements,
355
+ async complete() {
356
+ await publish();
357
+ for (const recipient of explicitRecipients) {
358
+ try {
359
+ await enqueueDeliveryToActor(env, deleteActivity.id, recipient);
360
+ } catch (error) {
361
+ log.error("Failed to enqueue explicit Delete delivery", {
362
+ event: "posts.delete.delivery_enqueue_failed",
363
+ activityId: deleteActivity.id,
364
+ recipient,
365
+ error,
366
+ });
367
+ }
368
+ }
369
+
370
+ return deleteActivity.id;
371
+ },
372
+ };
373
+ }