@takosjp/yurucommu-core 3.4.1 → 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 (113) 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 +35 -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/public.ts +16 -0
  45. package/src/backend/retention.ts +115 -0
  46. package/src/backend/routes/account-teardown.ts +422 -156
  47. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +104 -105
  48. package/src/backend/routes/activitypub/handlers/inbound-community-scope.ts +121 -0
  49. package/src/backend/routes/activitypub/handlers/inbound-object-identity.ts +54 -0
  50. package/src/backend/routes/activitypub/handlers/inbound-reply-target.ts +34 -0
  51. package/src/backend/routes/activitypub/handlers/inbound-story-projection.ts +438 -0
  52. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1121 -806
  53. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +118 -153
  54. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +185 -168
  55. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +220 -32
  56. package/src/backend/routes/activitypub/inbound-activity-identity.ts +16 -0
  57. package/src/backend/routes/activitypub/inbound-activity-reference.ts +116 -0
  58. package/src/backend/routes/activitypub/inbound-addressing.ts +87 -0
  59. package/src/backend/routes/activitypub/inbox-addressing.ts +22 -19
  60. package/src/backend/routes/activitypub/inbox-types.ts +14 -2
  61. package/src/backend/routes/activitypub/inbox.ts +81 -105
  62. package/src/backend/routes/activitypub/outbox.ts +0 -0
  63. package/src/backend/routes/activitypub.ts +6 -5
  64. package/src/backend/routes/actors-helpers.ts +34 -8
  65. package/src/backend/routes/actors.ts +333 -169
  66. package/src/backend/routes/auth-helpers.ts +57 -9
  67. package/src/backend/routes/auth.ts +10 -2
  68. package/src/backend/routes/communities/membership-invites.ts +4 -1
  69. package/src/backend/routes/communities/membership-members.ts +201 -73
  70. package/src/backend/routes/communities/membership-requests.ts +141 -76
  71. package/src/backend/routes/communities/membership-shared.ts +228 -21
  72. package/src/backend/routes/communities/messages.ts +9 -2
  73. package/src/backend/routes/communities/routes.ts +48 -13
  74. package/src/backend/routes/dm/contacts.ts +29 -41
  75. package/src/backend/routes/dm/conversations-helpers.ts +9 -1
  76. package/src/backend/routes/dm/messages.ts +36 -42
  77. package/src/backend/routes/dm/read-archive.ts +4 -2
  78. package/src/backend/routes/dm/requests.ts +62 -54
  79. package/src/backend/routes/follow-helpers.ts +200 -60
  80. package/src/backend/routes/follow.ts +146 -140
  81. package/src/backend/routes/media.ts +20 -94
  82. package/src/backend/routes/moderation.ts +72 -4
  83. package/src/backend/routes/notes.ts +3 -4
  84. package/src/backend/routes/notifications.ts +209 -108
  85. package/src/backend/routes/posts/delete-cascade.ts +253 -89
  86. package/src/backend/routes/posts/federation.ts +373 -0
  87. package/src/backend/routes/posts/interactions.ts +160 -184
  88. package/src/backend/routes/posts/like-mutation.ts +240 -0
  89. package/src/backend/routes/posts/post-helpers.ts +112 -162
  90. package/src/backend/routes/posts/queries.ts +150 -54
  91. package/src/backend/routes/posts/routes.ts +169 -329
  92. package/src/backend/routes/posts/transformers.ts +61 -6
  93. package/src/backend/routes/recommendations.ts +37 -44
  94. package/src/backend/routes/rtc/index.ts +26 -6
  95. package/src/backend/routes/search.ts +39 -55
  96. package/src/backend/routes/stories/interactions.ts +22 -6
  97. package/src/backend/routes/stories/query-helpers.ts +28 -41
  98. package/src/backend/routes/stories/routes.ts +130 -115
  99. package/src/backend/routes/takos-tools/dm.ts +17 -17
  100. package/src/backend/routes/takos-tools/posts.ts +189 -106
  101. package/src/backend/routes/takos-tools/search.ts +13 -4
  102. package/src/backend/routes/takos-tools/timeline.ts +35 -27
  103. package/src/backend/routes/takos-tools-response.ts +6 -2
  104. package/src/backend/routes/timeline.ts +5 -5
  105. package/src/backend/runtime/call-signaling-do.ts +35 -4
  106. package/src/backend/runtime/one-time-ticket.ts +116 -0
  107. package/src/backend/runtime/realtime-stream-do.ts +5 -61
  108. package/src/backend/runtime/signaling-hub.ts +36 -3
  109. package/src/backend/server.ts +95 -80
  110. package/src/db/d1-write.ts +67 -43
  111. package/src/db/schema/federation.ts +42 -1
  112. package/src/db/schema/messaging.ts +110 -2
  113. 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
+ }