@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.
- 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 +35 -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/public.ts +16 -0
- package/src/backend/retention.ts +115 -0
- 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 +333 -169
- 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 +130 -115
- 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
|
@@ -9,6 +9,14 @@ import { signRequest } from "./ap-signing.ts";
|
|
|
9
9
|
import type { RemoteFetchSigner } from "./activitypub-actor-cache.ts";
|
|
10
10
|
import { parseWebFinger } from "./activitypub-validators.ts";
|
|
11
11
|
import { isSafeRemoteUrl, normalizeRemoteDomain } from "./ssrf.ts";
|
|
12
|
+
import type { Env } from "../types.ts";
|
|
13
|
+
import type { Database, D1Statement } from "../../db/index.ts";
|
|
14
|
+
import { activities, actors, follows, runBatch } from "../../db/index.ts";
|
|
15
|
+
import { and, asc, eq, exists, gt, ne, notExists, sql } from "drizzle-orm";
|
|
16
|
+
import { alias } from "drizzle-orm/sqlite-core";
|
|
17
|
+
import { activityApId } from "./ap-ids.ts";
|
|
18
|
+
import { enqueueDeliveriesToActor } from "./delivery/queue.ts";
|
|
19
|
+
import { sha256Hex } from "./delivery/transformers.ts";
|
|
12
20
|
|
|
13
21
|
const ALIAS_FETCH_TIMEOUT_MS = 15000;
|
|
14
22
|
|
|
@@ -104,3 +112,335 @@ export async function destinationDeclaresAlias(
|
|
|
104
112
|
return false;
|
|
105
113
|
}
|
|
106
114
|
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Inbound Move follow-graph rewrite
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
const MOVE_REFOLLOW_PAGE_SIZE = 100;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Stable namespace for the outbound Follow activities produced by one inbound
|
|
124
|
+
* Move. The inbox stamps a fixed-size, signer-bound internal activity id before
|
|
125
|
+
* dispatch; the old/new ids are mixed in as defense-in-depth and to keep direct
|
|
126
|
+
* handler tests deterministic when no inbox id is supplied.
|
|
127
|
+
*/
|
|
128
|
+
export async function moveRefollowPrefix(args: {
|
|
129
|
+
readonly baseUrl: string;
|
|
130
|
+
readonly inboundActivityId?: string;
|
|
131
|
+
readonly oldActorApId: string;
|
|
132
|
+
readonly newActorApId: string;
|
|
133
|
+
}): Promise<string> {
|
|
134
|
+
const digest = await sha256Hex(
|
|
135
|
+
`${args.inboundActivityId ?? "synthetic-move"}\0${args.oldActorApId}\0${args.newActorApId}`,
|
|
136
|
+
);
|
|
137
|
+
return activityApId(args.baseUrl, `move-refollow-${digest}-`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Re-enqueue every durable local re-Follow created for one Move. Returns the
|
|
142
|
+
* number of persisted activities found, so the handler can resume a graph
|
|
143
|
+
* rewrite that committed before a Queue send failed without re-fetching the
|
|
144
|
+
* destination alias or trying to reconstruct already-deleted old edges.
|
|
145
|
+
*/
|
|
146
|
+
export async function enqueuePersistedMoveRefollows(args: {
|
|
147
|
+
readonly db: Database;
|
|
148
|
+
readonly env: Env;
|
|
149
|
+
readonly newActorApId: string;
|
|
150
|
+
readonly refollowPrefix: string;
|
|
151
|
+
}): Promise<number> {
|
|
152
|
+
let cursor: string | null = null;
|
|
153
|
+
let found = 0;
|
|
154
|
+
|
|
155
|
+
while (true) {
|
|
156
|
+
const page = await args.db
|
|
157
|
+
.select({ apId: activities.apId })
|
|
158
|
+
.from(activities)
|
|
159
|
+
.where(
|
|
160
|
+
and(
|
|
161
|
+
eq(activities.type, "Follow"),
|
|
162
|
+
eq(activities.objectApId, args.newActorApId),
|
|
163
|
+
eq(activities.direction, "outbound"),
|
|
164
|
+
// `LIKE prefix%` would hit D1's short LIKE-pattern complexity limit
|
|
165
|
+
// because the stable prefix contains a full URL + SHA-256 digest.
|
|
166
|
+
sql`instr(${activities.apId}, ${args.refollowPrefix}) = 1`,
|
|
167
|
+
cursor ? gt(activities.apId, cursor) : undefined,
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
.orderBy(asc(activities.apId))
|
|
171
|
+
.limit(MOVE_REFOLLOW_PAGE_SIZE);
|
|
172
|
+
|
|
173
|
+
if (page.length === 0) break;
|
|
174
|
+
await enqueueDeliveriesToActor(
|
|
175
|
+
args.env,
|
|
176
|
+
page.map((row) => row.apId),
|
|
177
|
+
args.newActorApId,
|
|
178
|
+
);
|
|
179
|
+
found += page.length;
|
|
180
|
+
cursor = page[page.length - 1]!.apId;
|
|
181
|
+
if (page.length < MOVE_REFOLLOW_PAGE_SIZE) break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Atomically rewrite every edge that names a moved remote actor.
|
|
189
|
+
*
|
|
190
|
+
* The statement count is fixed at eight regardless of graph size. Each INSERT
|
|
191
|
+
* reads the old graph with INSERT ... SELECT, so it binds only the handful of
|
|
192
|
+
* actor/prefix values rather than six parameters per edge. Both counter repairs
|
|
193
|
+
* are set-based UPDATEs. This is deliberately not implemented as insertMany()
|
|
194
|
+
* chunks: a large graph would eventually exceed the 50-statement atomic-batch
|
|
195
|
+
* ceiling, while splitting the delete/insert work across batches would expose
|
|
196
|
+
* a permanent half-migrated graph after a crash.
|
|
197
|
+
*
|
|
198
|
+
* Outbound local re-Follow activities are written in the same batch as their
|
|
199
|
+
* pending edges. If Queue delivery throws after commit, the stable prefix lets
|
|
200
|
+
* a retry call enqueuePersistedMoveRefollows() before touching the graph.
|
|
201
|
+
*/
|
|
202
|
+
export async function rewriteMovedFollowGraph(args: {
|
|
203
|
+
readonly db: Database;
|
|
204
|
+
readonly oldActorApId: string;
|
|
205
|
+
readonly newActorApId: string;
|
|
206
|
+
readonly refollowPrefix: string;
|
|
207
|
+
}): Promise<void> {
|
|
208
|
+
const oldAsFollower = alias(follows, "move_old_as_follower");
|
|
209
|
+
const oldAsFollowee = alias(follows, "move_old_as_followee");
|
|
210
|
+
const existingFollowerTarget = alias(
|
|
211
|
+
follows,
|
|
212
|
+
"move_existing_follower_target",
|
|
213
|
+
);
|
|
214
|
+
const existingFollowingSource = alias(
|
|
215
|
+
follows,
|
|
216
|
+
"move_existing_following_source",
|
|
217
|
+
);
|
|
218
|
+
const localFollowerActor = alias(actors, "move_local_follower_actor");
|
|
219
|
+
|
|
220
|
+
// These aliases belong to the counter statements. Keep them separate from
|
|
221
|
+
// the INSERT aliases so each generated statement is unambiguous to SQLite.
|
|
222
|
+
const acceptedOldFollowee = alias(follows, "move_accepted_old_followee");
|
|
223
|
+
const acceptedOldFollower = alias(follows, "move_accepted_old_follower");
|
|
224
|
+
const duplicateNewFollower = alias(follows, "move_duplicate_new_follower");
|
|
225
|
+
|
|
226
|
+
const decrementLocalFollowers = args.db
|
|
227
|
+
.update(actors)
|
|
228
|
+
.set({ followingCount: sql`${actors.followingCount} - 1` })
|
|
229
|
+
.where(
|
|
230
|
+
and(
|
|
231
|
+
gt(actors.followingCount, 0),
|
|
232
|
+
exists(
|
|
233
|
+
args.db
|
|
234
|
+
.select({ one: sql`1` })
|
|
235
|
+
.from(acceptedOldFollowee)
|
|
236
|
+
.where(
|
|
237
|
+
and(
|
|
238
|
+
eq(acceptedOldFollowee.followingApId, args.oldActorApId),
|
|
239
|
+
eq(acceptedOldFollowee.followerApId, actors.apId),
|
|
240
|
+
eq(acceptedOldFollowee.status, "accepted"),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const decrementDroppedLocalFollowees = args.db
|
|
248
|
+
.update(actors)
|
|
249
|
+
.set({ followerCount: sql`${actors.followerCount} - 1` })
|
|
250
|
+
.where(
|
|
251
|
+
and(
|
|
252
|
+
gt(actors.followerCount, 0),
|
|
253
|
+
exists(
|
|
254
|
+
args.db
|
|
255
|
+
.select({ one: sql`1` })
|
|
256
|
+
.from(acceptedOldFollower)
|
|
257
|
+
.where(
|
|
258
|
+
and(
|
|
259
|
+
eq(acceptedOldFollower.followerApId, args.oldActorApId),
|
|
260
|
+
eq(acceptedOldFollower.followingApId, actors.apId),
|
|
261
|
+
eq(acceptedOldFollower.status, "accepted"),
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
),
|
|
265
|
+
sql`(
|
|
266
|
+
${actors.apId} = ${args.newActorApId}
|
|
267
|
+
OR ${exists(
|
|
268
|
+
args.db
|
|
269
|
+
.select({ one: sql`1` })
|
|
270
|
+
.from(duplicateNewFollower)
|
|
271
|
+
.where(
|
|
272
|
+
and(
|
|
273
|
+
eq(duplicateNewFollower.followerApId, args.newActorApId),
|
|
274
|
+
eq(duplicateNewFollower.followingApId, actors.apId),
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
)}
|
|
278
|
+
)`,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const rewriteOldAsFollower = args.db
|
|
283
|
+
.insert(follows)
|
|
284
|
+
.select(
|
|
285
|
+
args.db
|
|
286
|
+
.select({
|
|
287
|
+
followerApId: sql<string>`${args.newActorApId}`.as("follower_ap_id"),
|
|
288
|
+
followingApId: oldAsFollower.followingApId,
|
|
289
|
+
status: oldAsFollower.status,
|
|
290
|
+
activityApId: oldAsFollower.activityApId,
|
|
291
|
+
createdAt: oldAsFollower.createdAt,
|
|
292
|
+
acceptedAt: oldAsFollower.acceptedAt,
|
|
293
|
+
})
|
|
294
|
+
.from(oldAsFollower)
|
|
295
|
+
.where(
|
|
296
|
+
and(
|
|
297
|
+
eq(oldAsFollower.followerApId, args.oldActorApId),
|
|
298
|
+
ne(oldAsFollower.followingApId, args.newActorApId),
|
|
299
|
+
notExists(
|
|
300
|
+
args.db
|
|
301
|
+
.select({ one: sql`1` })
|
|
302
|
+
.from(existingFollowerTarget)
|
|
303
|
+
.where(
|
|
304
|
+
and(
|
|
305
|
+
eq(existingFollowerTarget.followerApId, args.newActorApId),
|
|
306
|
+
eq(
|
|
307
|
+
existingFollowerTarget.followingApId,
|
|
308
|
+
oldAsFollower.followingApId,
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
.onConflictDoNothing();
|
|
317
|
+
|
|
318
|
+
const localFollowerExists = exists(
|
|
319
|
+
args.db
|
|
320
|
+
.select({ one: sql`1` })
|
|
321
|
+
.from(localFollowerActor)
|
|
322
|
+
.where(eq(localFollowerActor.apId, oldAsFollowee.followerApId)),
|
|
323
|
+
);
|
|
324
|
+
const targetEdgeDoesNotExist = notExists(
|
|
325
|
+
args.db
|
|
326
|
+
.select({ one: sql`1` })
|
|
327
|
+
.from(existingFollowingSource)
|
|
328
|
+
.where(
|
|
329
|
+
and(
|
|
330
|
+
eq(existingFollowingSource.followerApId, oldAsFollowee.followerApId),
|
|
331
|
+
eq(existingFollowingSource.followingApId, args.newActorApId),
|
|
332
|
+
),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const rewriteLocalFollowers = args.db
|
|
337
|
+
.insert(follows)
|
|
338
|
+
.select(
|
|
339
|
+
args.db
|
|
340
|
+
.select({
|
|
341
|
+
followerApId: oldAsFollowee.followerApId,
|
|
342
|
+
followingApId: sql<string>`${args.newActorApId}`.as(
|
|
343
|
+
"following_ap_id",
|
|
344
|
+
),
|
|
345
|
+
status: sql<string>`'pending'`.as("status"),
|
|
346
|
+
activityApId:
|
|
347
|
+
sql<string>`${args.refollowPrefix} || lower(hex(randomblob(32)))`.as(
|
|
348
|
+
"activity_ap_id",
|
|
349
|
+
),
|
|
350
|
+
createdAt: oldAsFollowee.createdAt,
|
|
351
|
+
acceptedAt: sql<null>`NULL`.as("accepted_at"),
|
|
352
|
+
})
|
|
353
|
+
.from(oldAsFollowee)
|
|
354
|
+
.where(
|
|
355
|
+
and(
|
|
356
|
+
eq(oldAsFollowee.followingApId, args.oldActorApId),
|
|
357
|
+
ne(oldAsFollowee.followerApId, args.newActorApId),
|
|
358
|
+
targetEdgeDoesNotExist,
|
|
359
|
+
localFollowerExists,
|
|
360
|
+
),
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
.onConflictDoNothing();
|
|
364
|
+
|
|
365
|
+
const rewriteRemoteFollowers = args.db
|
|
366
|
+
.insert(follows)
|
|
367
|
+
.select(
|
|
368
|
+
args.db
|
|
369
|
+
.select({
|
|
370
|
+
followerApId: oldAsFollowee.followerApId,
|
|
371
|
+
followingApId: sql<string>`${args.newActorApId}`.as(
|
|
372
|
+
"following_ap_id",
|
|
373
|
+
),
|
|
374
|
+
status: oldAsFollowee.status,
|
|
375
|
+
activityApId: oldAsFollowee.activityApId,
|
|
376
|
+
createdAt: oldAsFollowee.createdAt,
|
|
377
|
+
acceptedAt: oldAsFollowee.acceptedAt,
|
|
378
|
+
})
|
|
379
|
+
.from(oldAsFollowee)
|
|
380
|
+
.where(
|
|
381
|
+
and(
|
|
382
|
+
eq(oldAsFollowee.followingApId, args.oldActorApId),
|
|
383
|
+
ne(oldAsFollowee.followerApId, args.newActorApId),
|
|
384
|
+
targetEdgeDoesNotExist,
|
|
385
|
+
notExists(
|
|
386
|
+
args.db
|
|
387
|
+
.select({ one: sql`1` })
|
|
388
|
+
.from(localFollowerActor)
|
|
389
|
+
.where(eq(localFollowerActor.apId, oldAsFollowee.followerApId)),
|
|
390
|
+
),
|
|
391
|
+
),
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
.onConflictDoNothing();
|
|
395
|
+
|
|
396
|
+
const migratedLocalFollow = alias(follows, "move_migrated_local_follow");
|
|
397
|
+
const recordLocalRefollows = args.db
|
|
398
|
+
.insert(activities)
|
|
399
|
+
.select(
|
|
400
|
+
args.db
|
|
401
|
+
.select({
|
|
402
|
+
apId: migratedLocalFollow.activityApId,
|
|
403
|
+
type: sql<string>`'Follow'`.as("type"),
|
|
404
|
+
actorApId: migratedLocalFollow.followerApId,
|
|
405
|
+
objectApId: sql<string>`${args.newActorApId}`.as("object_ap_id"),
|
|
406
|
+
objectJson: sql<null>`NULL`.as("object_json"),
|
|
407
|
+
targetApId: sql<null>`NULL`.as("target_ap_id"),
|
|
408
|
+
rawJson: sql<string>`json_object(
|
|
409
|
+
'@context', 'https://www.w3.org/ns/activitystreams',
|
|
410
|
+
'id', ${migratedLocalFollow.activityApId},
|
|
411
|
+
'type', 'Follow',
|
|
412
|
+
'actor', ${migratedLocalFollow.followerApId},
|
|
413
|
+
'object', ${args.newActorApId}
|
|
414
|
+
)`.as("raw_json"),
|
|
415
|
+
direction: sql<string>`'outbound'`.as("direction"),
|
|
416
|
+
processed: sql<number>`0`.as("processed"),
|
|
417
|
+
createdAt: sql<string>`datetime('now')`.as("created_at"),
|
|
418
|
+
})
|
|
419
|
+
.from(migratedLocalFollow)
|
|
420
|
+
.where(
|
|
421
|
+
and(
|
|
422
|
+
eq(migratedLocalFollow.followingApId, args.newActorApId),
|
|
423
|
+
sql`instr(${migratedLocalFollow.activityApId}, ${args.refollowPrefix}) = 1`,
|
|
424
|
+
),
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
.onConflictDoNothing();
|
|
428
|
+
|
|
429
|
+
const deleteOldAsFollower = args.db
|
|
430
|
+
.delete(follows)
|
|
431
|
+
.where(eq(follows.followerApId, args.oldActorApId));
|
|
432
|
+
const deleteOldAsFollowee = args.db
|
|
433
|
+
.delete(follows)
|
|
434
|
+
.where(eq(follows.followingApId, args.oldActorApId));
|
|
435
|
+
|
|
436
|
+
await runBatch(args.db, [
|
|
437
|
+
decrementLocalFollowers,
|
|
438
|
+
decrementDroppedLocalFollowees,
|
|
439
|
+
rewriteOldAsFollower,
|
|
440
|
+
rewriteLocalFollowers,
|
|
441
|
+
rewriteRemoteFollowers,
|
|
442
|
+
recordLocalRefollows,
|
|
443
|
+
deleteOldAsFollower,
|
|
444
|
+
deleteOldAsFollowee,
|
|
445
|
+
] as unknown as [D1Statement, ...D1Statement[]]);
|
|
446
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { and, inArray, notExists, or, type SQL } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
activities,
|
|
5
|
+
deliveryFanouts,
|
|
6
|
+
deliveryQueue,
|
|
7
|
+
deliveryResolutions,
|
|
8
|
+
inboundActivityClaims,
|
|
9
|
+
inbox,
|
|
10
|
+
notificationArchived,
|
|
11
|
+
notificationPushJobs,
|
|
12
|
+
runBatch,
|
|
13
|
+
type Database,
|
|
14
|
+
type D1Statement,
|
|
15
|
+
} from "../../db/index.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A write-time fence for deleting an Activity actor's signing material.
|
|
19
|
+
*
|
|
20
|
+
* Preliminary reads are not sufficient: a fanout/resolution worker can
|
|
21
|
+
* materialize an endpoint after the reaper checked delivery_queue and then
|
|
22
|
+
* mark its own outbox terminal before the reaper checks that table. Reuse this
|
|
23
|
+
* predicate on EVERY statement in the atomic cleanup batch so newly active
|
|
24
|
+
* work stops the remaining deletes before the Activity or signer disappears.
|
|
25
|
+
*/
|
|
26
|
+
export function activityDeliveryDrainedGuard(
|
|
27
|
+
db: Database,
|
|
28
|
+
activityWhere: SQL,
|
|
29
|
+
): SQL {
|
|
30
|
+
const targetActivityIds = () =>
|
|
31
|
+
db.select({ apId: activities.apId }).from(activities).where(activityWhere);
|
|
32
|
+
|
|
33
|
+
return and(
|
|
34
|
+
notExists(
|
|
35
|
+
db
|
|
36
|
+
.select({ id: deliveryQueue.id })
|
|
37
|
+
.from(deliveryQueue)
|
|
38
|
+
.where(
|
|
39
|
+
and(
|
|
40
|
+
inArray(deliveryQueue.activityApId, targetActivityIds()),
|
|
41
|
+
inArray(deliveryQueue.status, [
|
|
42
|
+
"pending",
|
|
43
|
+
"processing",
|
|
44
|
+
"retry_wait",
|
|
45
|
+
]),
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
notExists(
|
|
50
|
+
db
|
|
51
|
+
.select({ id: deliveryResolutions.id })
|
|
52
|
+
.from(deliveryResolutions)
|
|
53
|
+
.where(
|
|
54
|
+
and(
|
|
55
|
+
inArray(deliveryResolutions.activityApId, targetActivityIds()),
|
|
56
|
+
inArray(deliveryResolutions.status, [
|
|
57
|
+
"pending",
|
|
58
|
+
"queued",
|
|
59
|
+
"processing",
|
|
60
|
+
"retry_wait",
|
|
61
|
+
]),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
notExists(
|
|
66
|
+
db
|
|
67
|
+
.select({ id: deliveryFanouts.id })
|
|
68
|
+
.from(deliveryFanouts)
|
|
69
|
+
.where(
|
|
70
|
+
and(
|
|
71
|
+
or(
|
|
72
|
+
inArray(deliveryFanouts.activityApId, targetActivityIds()),
|
|
73
|
+
inArray(
|
|
74
|
+
deliveryFanouts.announceActivityApId,
|
|
75
|
+
targetActivityIds(),
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
inArray(deliveryFanouts.status, ["pending", "published"]),
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
)!;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the atomic cleanup unit for every durable projection of matching
|
|
87
|
+
* activities while retaining the activity ledger rows themselves.
|
|
88
|
+
*
|
|
89
|
+
* The inbox FK/trigger only removes push jobs in selected non-terminal states;
|
|
90
|
+
* delivery work, archived markers, in-flight/terminal push jobs, and inbound
|
|
91
|
+
* claim leases otherwise survive. This projection-only form is also used when
|
|
92
|
+
* an object is deleted but its historical Activity must remain available.
|
|
93
|
+
*/
|
|
94
|
+
export function activityProjectionDeleteStatements(
|
|
95
|
+
db: Database,
|
|
96
|
+
activityWhere: SQL,
|
|
97
|
+
options: { readonly guard?: SQL } = {},
|
|
98
|
+
): readonly [D1Statement, ...D1Statement[]] {
|
|
99
|
+
const targetActivityIds = () =>
|
|
100
|
+
db.select({ apId: activities.apId }).from(activities).where(activityWhere);
|
|
101
|
+
const guarded = (where: SQL): SQL =>
|
|
102
|
+
options.guard ? and(where, options.guard)! : where;
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
db
|
|
106
|
+
.delete(notificationPushJobs)
|
|
107
|
+
.where(
|
|
108
|
+
guarded(
|
|
109
|
+
inArray(notificationPushJobs.activityApId, targetActivityIds()),
|
|
110
|
+
),
|
|
111
|
+
) as D1Statement,
|
|
112
|
+
db
|
|
113
|
+
.delete(notificationArchived)
|
|
114
|
+
.where(
|
|
115
|
+
guarded(
|
|
116
|
+
inArray(notificationArchived.activityApId, targetActivityIds()),
|
|
117
|
+
),
|
|
118
|
+
) as D1Statement,
|
|
119
|
+
db
|
|
120
|
+
.delete(inboundActivityClaims)
|
|
121
|
+
.where(
|
|
122
|
+
guarded(
|
|
123
|
+
inArray(inboundActivityClaims.activityApId, targetActivityIds()),
|
|
124
|
+
),
|
|
125
|
+
) as D1Statement,
|
|
126
|
+
db
|
|
127
|
+
.delete(deliveryQueue)
|
|
128
|
+
.where(
|
|
129
|
+
guarded(inArray(deliveryQueue.activityApId, targetActivityIds())),
|
|
130
|
+
) as D1Statement,
|
|
131
|
+
db
|
|
132
|
+
.delete(deliveryResolutions)
|
|
133
|
+
.where(
|
|
134
|
+
guarded(inArray(deliveryResolutions.activityApId, targetActivityIds())),
|
|
135
|
+
) as D1Statement,
|
|
136
|
+
db
|
|
137
|
+
.delete(deliveryFanouts)
|
|
138
|
+
.where(
|
|
139
|
+
guarded(inArray(deliveryFanouts.activityApId, targetActivityIds())),
|
|
140
|
+
) as D1Statement,
|
|
141
|
+
// Keep the secondary Announce reference in its own statement. Combining
|
|
142
|
+
// both predicates with OR duplicates every bound value in D1, so a normal
|
|
143
|
+
// 90-id cascade would exceed the platform's 100-parameter ceiling.
|
|
144
|
+
db
|
|
145
|
+
.delete(deliveryFanouts)
|
|
146
|
+
.where(
|
|
147
|
+
guarded(
|
|
148
|
+
inArray(deliveryFanouts.announceActivityApId, targetActivityIds()),
|
|
149
|
+
),
|
|
150
|
+
) as D1Statement,
|
|
151
|
+
db
|
|
152
|
+
.delete(inbox)
|
|
153
|
+
.where(
|
|
154
|
+
guarded(inArray(inbox.activityApId, targetActivityIds())),
|
|
155
|
+
) as D1Statement,
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build one atomic hard-delete unit for activities and every durable
|
|
161
|
+
* projection keyed by their AP IDs.
|
|
162
|
+
*
|
|
163
|
+
* Keep the activities rows until the final statement so every preceding
|
|
164
|
+
* subquery sees the same target set. Semantic edges (`likes`, `announces`,
|
|
165
|
+
* `follows`) are intentionally caller-owned because removing them also
|
|
166
|
+
* requires domain-specific counter repair.
|
|
167
|
+
*/
|
|
168
|
+
export function activityDeleteCascadeStatements(
|
|
169
|
+
db: Database,
|
|
170
|
+
activityWhere: SQL,
|
|
171
|
+
options: { readonly guard?: SQL } = {},
|
|
172
|
+
): readonly [D1Statement, ...D1Statement[]] {
|
|
173
|
+
return [
|
|
174
|
+
...activityProjectionDeleteStatements(db, activityWhere, options),
|
|
175
|
+
db
|
|
176
|
+
.delete(activities)
|
|
177
|
+
.where(
|
|
178
|
+
options.guard ? and(activityWhere, options.guard) : activityWhere,
|
|
179
|
+
) as D1Statement,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Atomically hard-delete matching activities and all of their projections.
|
|
185
|
+
* A failed final activity delete rolls the preceding cleanup back, so the
|
|
186
|
+
* caller can retry the same idempotent predicate without partial state.
|
|
187
|
+
*/
|
|
188
|
+
export async function deleteActivitiesCascade(
|
|
189
|
+
db: Database,
|
|
190
|
+
activityWhere: SQL,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
await runBatch(db, activityDeleteCascadeStatements(db, activityWhere));
|
|
193
|
+
}
|