@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
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { and, asc, eq, inArray, lte, sql } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
affectedRowCount,
|
|
5
|
+
deliveryFanouts,
|
|
6
|
+
type Database,
|
|
7
|
+
type D1Statement,
|
|
8
|
+
} from "../../../db/index.ts";
|
|
9
|
+
import type { IQueueProducer } from "../../runtime/queue.ts";
|
|
10
|
+
import type { Env } from "../../types.ts";
|
|
11
|
+
import type {
|
|
12
|
+
DeliveryFanoutCommunityMessageV1,
|
|
13
|
+
DeliveryFanoutFollowersMessageV1,
|
|
14
|
+
DeliveryQueueMessageV1,
|
|
15
|
+
} from "./types.ts";
|
|
16
|
+
import { sha256Hex } from "./transformers.ts";
|
|
17
|
+
|
|
18
|
+
const OUTBOX_SCAN_LIMIT = 50;
|
|
19
|
+
const TERMINAL_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
20
|
+
const TERMINAL_PURGE_LIMIT = 50;
|
|
21
|
+
const TERMINAL_STATUSES = ["completed", "failed", "discarded"] as const;
|
|
22
|
+
|
|
23
|
+
export type DeliveryFanoutIntent =
|
|
24
|
+
| {
|
|
25
|
+
readonly kind: "followers";
|
|
26
|
+
readonly activityId: string;
|
|
27
|
+
readonly targetApId: string;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
readonly kind: "community";
|
|
31
|
+
readonly activityId: string;
|
|
32
|
+
readonly targetApId: string;
|
|
33
|
+
readonly announceActivityId?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type DeliveryFanoutState = "missing" | "active" | "terminal";
|
|
37
|
+
|
|
38
|
+
export function deliveryFanoutIntentFromMessage(
|
|
39
|
+
message: DeliveryFanoutFollowersMessageV1 | DeliveryFanoutCommunityMessageV1,
|
|
40
|
+
): DeliveryFanoutIntent {
|
|
41
|
+
if (message.type === "fanout_followers") {
|
|
42
|
+
return {
|
|
43
|
+
kind: "followers",
|
|
44
|
+
activityId: message.activityId,
|
|
45
|
+
targetApId: message.followeeApId,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: "community",
|
|
50
|
+
activityId: message.activityId,
|
|
51
|
+
targetApId: message.communityApId,
|
|
52
|
+
...(message.announceActivityId
|
|
53
|
+
? { announceActivityId: message.announceActivityId }
|
|
54
|
+
: {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function computeDeliveryFanoutId(
|
|
59
|
+
intent: DeliveryFanoutIntent,
|
|
60
|
+
): Promise<string> {
|
|
61
|
+
return await sha256Hex(
|
|
62
|
+
[
|
|
63
|
+
"fanout",
|
|
64
|
+
intent.kind,
|
|
65
|
+
intent.activityId,
|
|
66
|
+
intent.targetApId,
|
|
67
|
+
intent.kind === "community" ? (intent.announceActivityId ?? "") : "",
|
|
68
|
+
].join("|"),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildFanoutMessage(row: {
|
|
73
|
+
readonly activityApId: string;
|
|
74
|
+
readonly kind: string;
|
|
75
|
+
readonly targetApId: string;
|
|
76
|
+
readonly announceActivityApId: string | null;
|
|
77
|
+
}): DeliveryFanoutFollowersMessageV1 | DeliveryFanoutCommunityMessageV1 {
|
|
78
|
+
const scheduledAt = new Date().toISOString();
|
|
79
|
+
if (row.kind === "followers") {
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
type: "fanout_followers",
|
|
83
|
+
activityId: row.activityApId,
|
|
84
|
+
followeeApId: row.targetApId,
|
|
85
|
+
scheduledAt,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (row.kind === "community") {
|
|
89
|
+
return {
|
|
90
|
+
version: 1,
|
|
91
|
+
type: "fanout_community",
|
|
92
|
+
activityId: row.activityApId,
|
|
93
|
+
communityApId: row.targetApId,
|
|
94
|
+
...(row.announceActivityApId
|
|
95
|
+
? { announceActivityId: row.announceActivityApId }
|
|
96
|
+
: {}),
|
|
97
|
+
scheduledAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Unsupported delivery fanout kind: ${row.kind}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the deterministic fanout-ledger insert without executing it. Writers
|
|
105
|
+
* that commit the Activity and its local state can compose this statement into
|
|
106
|
+
* the same D1 batch, closing the crash window before the durable intent exists.
|
|
107
|
+
*/
|
|
108
|
+
export async function prepareDeliveryFanoutJob(
|
|
109
|
+
db: Database,
|
|
110
|
+
intent: DeliveryFanoutIntent,
|
|
111
|
+
): Promise<{ readonly id: string; readonly statement: D1Statement }> {
|
|
112
|
+
const id = await computeDeliveryFanoutId(intent);
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
const statement = db
|
|
115
|
+
.insert(deliveryFanouts)
|
|
116
|
+
.values({
|
|
117
|
+
id,
|
|
118
|
+
activityApId: intent.activityId,
|
|
119
|
+
kind: intent.kind,
|
|
120
|
+
targetApId: intent.targetApId,
|
|
121
|
+
announceActivityApId:
|
|
122
|
+
intent.kind === "community"
|
|
123
|
+
? (intent.announceActivityId ?? null)
|
|
124
|
+
: null,
|
|
125
|
+
status: "pending",
|
|
126
|
+
publications: 0,
|
|
127
|
+
createdAt: now,
|
|
128
|
+
updatedAt: now,
|
|
129
|
+
})
|
|
130
|
+
.onConflictDoNothing() as unknown as D1Statement;
|
|
131
|
+
return { id, statement };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Persist the semantic fanout intent before attempting any Queue RPC. */
|
|
135
|
+
export async function persistDeliveryFanoutJob(
|
|
136
|
+
db: Database,
|
|
137
|
+
intent: DeliveryFanoutIntent,
|
|
138
|
+
): Promise<string> {
|
|
139
|
+
const prepared = await prepareDeliveryFanoutJob(db, intent);
|
|
140
|
+
await prepared.statement;
|
|
141
|
+
return prepared.id;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function publishRows(
|
|
145
|
+
db: Database,
|
|
146
|
+
queue: IQueueProducer<DeliveryQueueMessageV1>,
|
|
147
|
+
rows: readonly {
|
|
148
|
+
readonly id: string;
|
|
149
|
+
readonly activityApId: string;
|
|
150
|
+
readonly kind: string;
|
|
151
|
+
readonly targetApId: string;
|
|
152
|
+
readonly announceActivityApId: string | null;
|
|
153
|
+
}[],
|
|
154
|
+
): Promise<number> {
|
|
155
|
+
if (rows.length === 0) return 0;
|
|
156
|
+
await queue.sendBatch(rows.map((row) => ({ body: buildFanoutMessage(row) })));
|
|
157
|
+
await markRowsPublished(
|
|
158
|
+
db,
|
|
159
|
+
rows.map((row) => row.id),
|
|
160
|
+
);
|
|
161
|
+
return rows.length;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function markRowsPublished(
|
|
165
|
+
db: Database,
|
|
166
|
+
ids: readonly string[],
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
if (ids.length === 0) return;
|
|
169
|
+
const now = new Date().toISOString();
|
|
170
|
+
await db
|
|
171
|
+
.update(deliveryFanouts)
|
|
172
|
+
.set({
|
|
173
|
+
status: "published",
|
|
174
|
+
publications: sql`${deliveryFanouts.publications} + 1`,
|
|
175
|
+
lastError: null,
|
|
176
|
+
updatedAt: now,
|
|
177
|
+
})
|
|
178
|
+
.where(
|
|
179
|
+
and(
|
|
180
|
+
inArray(deliveryFanouts.id, [...ids]),
|
|
181
|
+
inArray(deliveryFanouts.status, ["pending", "published"]),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Persist one initial fanout intent and publish its wakeup when configured. */
|
|
187
|
+
export async function enqueueDeliveryFanoutJob(
|
|
188
|
+
db: Database,
|
|
189
|
+
queue: IQueueProducer<DeliveryQueueMessageV1> | undefined,
|
|
190
|
+
intent: DeliveryFanoutIntent,
|
|
191
|
+
): Promise<number> {
|
|
192
|
+
const id = await persistDeliveryFanoutJob(db, intent);
|
|
193
|
+
if (!queue) return 0;
|
|
194
|
+
const row = await db
|
|
195
|
+
.select({
|
|
196
|
+
id: deliveryFanouts.id,
|
|
197
|
+
activityApId: deliveryFanouts.activityApId,
|
|
198
|
+
kind: deliveryFanouts.kind,
|
|
199
|
+
targetApId: deliveryFanouts.targetApId,
|
|
200
|
+
announceActivityApId: deliveryFanouts.announceActivityApId,
|
|
201
|
+
})
|
|
202
|
+
.from(deliveryFanouts)
|
|
203
|
+
.where(
|
|
204
|
+
and(eq(deliveryFanouts.id, id), eq(deliveryFanouts.status, "pending")),
|
|
205
|
+
)
|
|
206
|
+
.get();
|
|
207
|
+
if (!row) return 0;
|
|
208
|
+
// Preserve the original public producer behavior for the request path: one
|
|
209
|
+
// semantic fanout is one Queue send. Recovery uses bounded sendBatch below.
|
|
210
|
+
await queue.send(buildFanoutMessage(row));
|
|
211
|
+
await markRowsPublished(db, [row.id]);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Recover pending fanout wakeups; Bun startup may also replay published rows. */
|
|
216
|
+
export async function enqueuePendingDeliveryFanoutJobs(
|
|
217
|
+
env: Env,
|
|
218
|
+
options: { readonly includePublished?: boolean } = {},
|
|
219
|
+
): Promise<number> {
|
|
220
|
+
await purgeTerminalDeliveryFanoutJobs(env.DB_INSTANCE);
|
|
221
|
+
if (!env.DELIVERY_QUEUE || !env.DELIVERY_DLQ) return 0;
|
|
222
|
+
const statuses = options.includePublished
|
|
223
|
+
? ["pending", "published"]
|
|
224
|
+
: ["pending"];
|
|
225
|
+
const rows = await env.DB_INSTANCE.select({
|
|
226
|
+
id: deliveryFanouts.id,
|
|
227
|
+
activityApId: deliveryFanouts.activityApId,
|
|
228
|
+
kind: deliveryFanouts.kind,
|
|
229
|
+
targetApId: deliveryFanouts.targetApId,
|
|
230
|
+
announceActivityApId: deliveryFanouts.announceActivityApId,
|
|
231
|
+
})
|
|
232
|
+
.from(deliveryFanouts)
|
|
233
|
+
.where(inArray(deliveryFanouts.status, statuses))
|
|
234
|
+
.orderBy(asc(deliveryFanouts.createdAt))
|
|
235
|
+
.limit(OUTBOX_SCAN_LIMIT);
|
|
236
|
+
return await publishRows(env.DB_INSTANCE, env.DELIVERY_QUEUE, rows);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function getDeliveryFanoutState(
|
|
240
|
+
db: Database,
|
|
241
|
+
intent: DeliveryFanoutIntent,
|
|
242
|
+
): Promise<DeliveryFanoutState> {
|
|
243
|
+
const id = await computeDeliveryFanoutId(intent);
|
|
244
|
+
const row = await db
|
|
245
|
+
.select({ status: deliveryFanouts.status })
|
|
246
|
+
.from(deliveryFanouts)
|
|
247
|
+
.where(eq(deliveryFanouts.id, id))
|
|
248
|
+
.get();
|
|
249
|
+
if (!row) return "missing";
|
|
250
|
+
return (TERMINAL_STATUSES as readonly string[]).includes(row.status)
|
|
251
|
+
? "terminal"
|
|
252
|
+
: "active";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function setDeliveryFanoutStatus(
|
|
256
|
+
db: Database,
|
|
257
|
+
intent: DeliveryFanoutIntent,
|
|
258
|
+
status: "pending" | "completed" | "failed" | "discarded",
|
|
259
|
+
reason?: string,
|
|
260
|
+
): Promise<boolean> {
|
|
261
|
+
const id = await computeDeliveryFanoutId(intent);
|
|
262
|
+
const terminal = status !== "pending";
|
|
263
|
+
const now = new Date().toISOString();
|
|
264
|
+
const result = await db
|
|
265
|
+
.update(deliveryFanouts)
|
|
266
|
+
.set({
|
|
267
|
+
status,
|
|
268
|
+
lastError: reason ?? null,
|
|
269
|
+
updatedAt: now,
|
|
270
|
+
completedAt: terminal ? now : null,
|
|
271
|
+
})
|
|
272
|
+
.where(
|
|
273
|
+
and(
|
|
274
|
+
eq(deliveryFanouts.id, id),
|
|
275
|
+
inArray(deliveryFanouts.status, ["pending", "published"]),
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
return affectedRowCount(result) > 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function resetDeliveryFanoutJob(
|
|
282
|
+
db: Database,
|
|
283
|
+
intent: DeliveryFanoutIntent,
|
|
284
|
+
reason?: string,
|
|
285
|
+
): Promise<boolean> {
|
|
286
|
+
return await setDeliveryFanoutStatus(db, intent, "pending", reason);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function completeDeliveryFanoutJob(
|
|
290
|
+
db: Database,
|
|
291
|
+
intent: DeliveryFanoutIntent,
|
|
292
|
+
status: "completed" | "discarded" = "completed",
|
|
293
|
+
reason?: string,
|
|
294
|
+
): Promise<boolean> {
|
|
295
|
+
return await setDeliveryFanoutStatus(db, intent, status, reason);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function failDeliveryFanoutJob(
|
|
299
|
+
db: Database,
|
|
300
|
+
intent: DeliveryFanoutIntent,
|
|
301
|
+
reason: string,
|
|
302
|
+
): Promise<boolean> {
|
|
303
|
+
return await setDeliveryFanoutStatus(db, intent, "failed", reason);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function purgeTerminalDeliveryFanoutJobs(
|
|
307
|
+
db: Database,
|
|
308
|
+
nowDate = new Date(),
|
|
309
|
+
): Promise<number> {
|
|
310
|
+
const cutoff = new Date(
|
|
311
|
+
nowDate.getTime() - TERMINAL_RETENTION_MS,
|
|
312
|
+
).toISOString();
|
|
313
|
+
const rows = await db
|
|
314
|
+
.select({ id: deliveryFanouts.id })
|
|
315
|
+
.from(deliveryFanouts)
|
|
316
|
+
.where(
|
|
317
|
+
and(
|
|
318
|
+
inArray(deliveryFanouts.status, [...TERMINAL_STATUSES]),
|
|
319
|
+
lte(deliveryFanouts.updatedAt, cutoff),
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
.orderBy(asc(deliveryFanouts.updatedAt))
|
|
323
|
+
.limit(TERMINAL_PURGE_LIMIT);
|
|
324
|
+
if (rows.length === 0) return 0;
|
|
325
|
+
const result = await db.delete(deliveryFanouts).where(
|
|
326
|
+
and(
|
|
327
|
+
inArray(
|
|
328
|
+
deliveryFanouts.id,
|
|
329
|
+
rows.map((row) => row.id),
|
|
330
|
+
),
|
|
331
|
+
inArray(deliveryFanouts.status, [...TERMINAL_STATUSES]),
|
|
332
|
+
lte(deliveryFanouts.updatedAt, cutoff),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
return affectedRowCount(result);
|
|
336
|
+
}
|
|
@@ -7,11 +7,12 @@ import {
|
|
|
7
7
|
safeParseIsoTimeMs,
|
|
8
8
|
} from "./transformers.ts";
|
|
9
9
|
import { isSafeRemoteUrl } from "../../federation-helpers.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { filterBlockedActorApIdsStrict } from "../blocklist.ts";
|
|
11
11
|
|
|
12
12
|
export type PlannedEndpointGroup = {
|
|
13
13
|
endpoint: string;
|
|
14
14
|
recipientCount: number;
|
|
15
|
+
recipientActorApIds: string[];
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export type PlanEndpointsResult = {
|
|
@@ -82,13 +83,16 @@ export async function planEndpointsFromActorCache(
|
|
|
82
83
|
// Enforce the operator blocklist on the OUTBOUND side too: drop any
|
|
83
84
|
// recipient whose actor AP-ID or hostname is defederated BEFORE grouping
|
|
84
85
|
// endpoints, so a blocked domain/actor never receives our posts or DMs.
|
|
85
|
-
// Uses the same
|
|
86
|
-
//
|
|
86
|
+
// Uses the same actor + hostname authority as the single-actor path. A read
|
|
87
|
+
// failure propagates so fanout retries instead of bypassing defederation.
|
|
87
88
|
const allowedRecipients: string[] = [];
|
|
88
89
|
const blockedRecipients: string[] = [];
|
|
89
90
|
// Batched blocklist filter (2 queries) instead of a serial isActorBlocked
|
|
90
91
|
// per recipient (2 queries each → up to ~400 round-trips per fan-out page).
|
|
91
|
-
const blockedSet = await
|
|
92
|
+
const blockedSet = await filterBlockedActorApIdsStrict(
|
|
93
|
+
db,
|
|
94
|
+
recipientActorApIds,
|
|
95
|
+
);
|
|
92
96
|
for (const apId of recipientActorApIds) {
|
|
93
97
|
if (blockedSet.has(apId)) {
|
|
94
98
|
blockedRecipients.push(apId);
|
|
@@ -141,7 +145,7 @@ export async function planEndpointsFromActorCache(
|
|
|
141
145
|
|
|
142
146
|
const byApId = new Map(rows.map((r) => [r.apId, r as ActorCacheRow]));
|
|
143
147
|
const unknownRecipients: string[] = [];
|
|
144
|
-
const
|
|
148
|
+
const endpointRecipients = new Map<string, string[]>();
|
|
145
149
|
let sharedInboxRecipients = 0;
|
|
146
150
|
|
|
147
151
|
for (const apId of allowedRecipients) {
|
|
@@ -155,17 +159,17 @@ export async function planEndpointsFromActorCache(
|
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
if (chosen.usedSharedInbox) sharedInboxRecipients++;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
162
|
+
const groupedRecipients = endpointRecipients.get(chosen.endpoint);
|
|
163
|
+
if (groupedRecipients) groupedRecipients.push(apId);
|
|
164
|
+
else endpointRecipients.set(chosen.endpoint, [apId]);
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
const groups: PlannedEndpointGroup[] = Array.from(
|
|
165
|
-
|
|
166
|
-
).map(([endpoint,
|
|
168
|
+
endpointRecipients.entries(),
|
|
169
|
+
).map(([endpoint, recipientActorApIds]) => ({
|
|
167
170
|
endpoint,
|
|
168
|
-
recipientCount,
|
|
171
|
+
recipientCount: recipientActorApIds.length,
|
|
172
|
+
recipientActorApIds,
|
|
169
173
|
}));
|
|
170
174
|
|
|
171
175
|
// Observability: sharedInbox aggregation ratio.
|