@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
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* the remote `handleDelete` inbox path so neither can orphan rows.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { and, eq, inArray, isNull,
|
|
23
|
-
import type { Database } from "../../../db/index.ts";
|
|
22
|
+
import { and, asc, eq, gt, inArray, isNull, or, sql } from "drizzle-orm";
|
|
23
|
+
import type { D1Statement, Database } from "../../../db/index.ts";
|
|
24
24
|
import type { IObjectStorage } from "../../runtime/types.ts";
|
|
25
25
|
import {
|
|
26
26
|
activities,
|
|
@@ -28,15 +28,24 @@ import {
|
|
|
28
28
|
announces,
|
|
29
29
|
bookmarks,
|
|
30
30
|
communities,
|
|
31
|
-
|
|
31
|
+
D1_MAX_BATCH_STATEMENTS,
|
|
32
32
|
likes,
|
|
33
33
|
mediaUploads,
|
|
34
34
|
objectRecipients,
|
|
35
35
|
objects,
|
|
36
|
+
runBatch,
|
|
36
37
|
storyShares,
|
|
37
38
|
storyViews,
|
|
38
39
|
storyVotes,
|
|
39
40
|
} from "../../../db/index.ts";
|
|
41
|
+
import { activityProjectionDeleteStatements } from "../../lib/activity-delete-cascade.ts";
|
|
42
|
+
import { chunkForInClause, D1_IN_CHUNK } from "../../lib/chunk.ts";
|
|
43
|
+
|
|
44
|
+
type CascadeObject = {
|
|
45
|
+
apId: string;
|
|
46
|
+
attributedTo: string;
|
|
47
|
+
attachmentsJson: string;
|
|
48
|
+
};
|
|
40
49
|
|
|
41
50
|
/**
|
|
42
51
|
* Reap the `media_uploads` rows attached to a single object.
|
|
@@ -51,8 +60,8 @@ import {
|
|
|
51
60
|
* `attachmentMatches` semantics. There is no engine-level CASCADE for this edge,
|
|
52
61
|
* so without this the upload rows orphan on every runtime.
|
|
53
62
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
63
|
+
* The caller supplies a still-present object snapshot; this returns silently
|
|
64
|
+
* when that snapshot has no attachments.
|
|
56
65
|
*
|
|
57
66
|
* When a `media` object-store binding is provided, the backing R2 blobs for the
|
|
58
67
|
* reaped uploads are best-effort deleted by `r2_key` (mirroring the
|
|
@@ -67,22 +76,19 @@ import {
|
|
|
67
76
|
* dropping to zero. The DB-row delete is unconditional (the reaped rows belong
|
|
68
77
|
* to this object's reap set regardless).
|
|
69
78
|
*/
|
|
70
|
-
async function
|
|
79
|
+
async function deleteAttachedMediaUploadsForObject(
|
|
71
80
|
db: Database,
|
|
72
|
-
|
|
81
|
+
obj: CascadeObject,
|
|
82
|
+
removedObjectApIds: ReadonlySet<string>,
|
|
73
83
|
media?: IObjectStorage,
|
|
74
|
-
): Promise<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.get();
|
|
83
|
-
|
|
84
|
-
// No object row (already deleted) or no attachment payload: nothing to reap.
|
|
85
|
-
if (!obj || !obj.attachmentsJson || obj.attachmentsJson === "[]") return [];
|
|
84
|
+
): Promise<{
|
|
85
|
+
mediaKeys: string[];
|
|
86
|
+
mediaUploadIds: string[];
|
|
87
|
+
}> {
|
|
88
|
+
// No attachment payload: nothing to reap.
|
|
89
|
+
if (!obj.attachmentsJson || obj.attachmentsJson === "[]") {
|
|
90
|
+
return { mediaKeys: [], mediaUploadIds: [] };
|
|
91
|
+
}
|
|
86
92
|
|
|
87
93
|
const attachmentsJson = obj.attachmentsJson;
|
|
88
94
|
|
|
@@ -110,42 +116,47 @@ async function deleteAttachedMediaUploads(
|
|
|
110
116
|
attachmentsJson.includes(mediaUrlForKey(m.r2Key)),
|
|
111
117
|
);
|
|
112
118
|
|
|
113
|
-
if (orphaned.length === 0)
|
|
119
|
+
if (orphaned.length === 0) {
|
|
120
|
+
return { mediaKeys: [], mediaUploadIds: [] };
|
|
121
|
+
}
|
|
114
122
|
|
|
115
|
-
// Before any R2 purge, find which
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// honoured; the lookup is scoped to the author's own objects (indexed) and
|
|
121
|
-
// excludes the object being reaped.
|
|
123
|
+
// Before any R2 purge, find which keys are still referenced by an object of
|
|
124
|
+
// the same author OUTSIDE the complete set being removed. This matters for a
|
|
125
|
+
// batch containing two posts that share one blob: checking only "another
|
|
126
|
+
// object" would make each target keep the other target's key, then leak the
|
|
127
|
+
// row and blob after both objects disappear.
|
|
122
128
|
const stillReferencedKeys = new Set<string>();
|
|
123
129
|
if (media) {
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// bounded set of indexed lookups. Uses instr() (literal substring), NOT
|
|
130
|
-
// `LIKE '%<key>%'`: a 73-char `uploads/<64-hex>.png` key in a `%...%` pattern
|
|
131
|
-
// exceeds D1's LIKE pattern-complexity limit (SQLITE_ERROR 7500, ~50 chars).
|
|
132
|
-
// Mirrors media.ts findReferencingObject.
|
|
130
|
+
// Page only matching AP-IDs and stop at the first survivor. A NOT IN list
|
|
131
|
+
// cannot safely carry an unbounded removal set through D1's 100-parameter
|
|
132
|
+
// ceiling; keyset pages keep every query constant-sized without loading a
|
|
133
|
+
// prolific author's complete object history. instr() is literal and avoids
|
|
134
|
+
// D1's long-LIKE complexity failure.
|
|
133
135
|
for (const m of orphaned) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
or(
|
|
142
|
-
sql`instr(${objects.attachmentsJson}, ${m.r2Key}) > 0`,
|
|
143
|
-
sql`instr(${objects.attachmentsJson}, ${mediaUrlForKey(m.r2Key)}) > 0`,
|
|
144
|
-
),
|
|
136
|
+
let cursor: string | undefined;
|
|
137
|
+
while (true) {
|
|
138
|
+
const referenceMatch = and(
|
|
139
|
+
eq(objects.attributedTo, obj.attributedTo),
|
|
140
|
+
or(
|
|
141
|
+
sql`instr(${objects.attachmentsJson}, ${m.r2Key}) > 0`,
|
|
142
|
+
sql`instr(${objects.attachmentsJson}, ${mediaUrlForKey(m.r2Key)}) > 0`,
|
|
145
143
|
),
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
cursor ? gt(objects.apId, cursor) : undefined,
|
|
145
|
+
);
|
|
146
|
+
const refs = await db
|
|
147
|
+
.select({ apId: objects.apId })
|
|
148
|
+
.from(objects)
|
|
149
|
+
.where(referenceMatch)
|
|
150
|
+
.orderBy(asc(objects.apId))
|
|
151
|
+
.limit(D1_IN_CHUNK);
|
|
152
|
+
if (refs.some((ref) => !removedObjectApIds.has(ref.apId))) {
|
|
153
|
+
stillReferencedKeys.add(m.r2Key);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
if (refs.length < D1_IN_CHUNK) break;
|
|
157
|
+
cursor = refs.at(-1)?.apId;
|
|
158
|
+
if (!cursor) break;
|
|
159
|
+
}
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
|
|
@@ -158,10 +169,6 @@ async function deleteAttachedMediaUploads(
|
|
|
158
169
|
const idsToDelete = media
|
|
159
170
|
? orphaned.filter((m) => !stillReferencedKeys.has(m.r2Key)).map((m) => m.id)
|
|
160
171
|
: orphaned.map((m) => m.id);
|
|
161
|
-
if (idsToDelete.length > 0) {
|
|
162
|
-
await db.delete(mediaUploads).where(inArray(mediaUploads.id, idsToDelete));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
172
|
// Return the keys whose reference count has now dropped to zero. The caller
|
|
166
173
|
// purges them via purgeMediaBlobs AFTER it deletes the objects row, so the
|
|
167
174
|
// IRREVERSIBLE R2 delete is the trailing step: if the objects-row delete fails
|
|
@@ -169,9 +176,12 @@ async function deleteAttachedMediaUploads(
|
|
|
169
176
|
// surviving with a permanently-deleted blob (a broken image with no recovery).
|
|
170
177
|
// Keys still embedded in another present object's `attachments_json` are kept
|
|
171
178
|
// (blob + media_uploads row) so shared media isn't lost.
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
return {
|
|
180
|
+
mediaKeys: media
|
|
181
|
+
? orphaned.map((m) => m.r2Key).filter((k) => !stillReferencedKeys.has(k))
|
|
182
|
+
: [],
|
|
183
|
+
mediaUploadIds: idsToDelete,
|
|
184
|
+
};
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
/**
|
|
@@ -286,45 +296,199 @@ export async function reapReplacedMediaUrl(
|
|
|
286
296
|
* a `media` binding is passed, the backing R2 blobs are best-effort deleted too
|
|
287
297
|
* so storage does not leak; pass `c.env.MEDIA` from the request context.
|
|
288
298
|
*
|
|
289
|
-
* Does not touch the `objects` row or `activities` (SET NULL, not CASCADE)
|
|
299
|
+
* Does not touch the `objects` row or `activities` (SET NULL, not CASCADE),
|
|
300
|
+
* but cancels every durable notification/delivery projection of Activities
|
|
301
|
+
* that target the object.
|
|
290
302
|
*/
|
|
291
303
|
export async function deleteObjectCascade(
|
|
292
304
|
db: Database,
|
|
293
305
|
objectApId: string,
|
|
294
306
|
media?: IObjectStorage,
|
|
295
307
|
): Promise<string[]> {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
return await deleteObjectsCascade(db, [objectApId], media);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Prepare one local object's complete child/projection cleanup without writing
|
|
313
|
+
* it. Local delete owners compose this with counters, the outbound Delete,
|
|
314
|
+
* durable fanout intent, and final object removal in one D1 batch.
|
|
315
|
+
*/
|
|
316
|
+
export async function prepareObjectDeleteCascade(
|
|
317
|
+
db: Database,
|
|
318
|
+
objectApId: string,
|
|
319
|
+
media?: IObjectStorage,
|
|
320
|
+
): Promise<{
|
|
321
|
+
mediaKeys: string[];
|
|
322
|
+
statements: readonly [D1Statement, ...D1Statement[]];
|
|
323
|
+
}> {
|
|
324
|
+
const obj = await db
|
|
325
|
+
.select({
|
|
326
|
+
apId: objects.apId,
|
|
327
|
+
attributedTo: objects.attributedTo,
|
|
328
|
+
attachmentsJson: objects.attachmentsJson,
|
|
329
|
+
})
|
|
330
|
+
.from(objects)
|
|
331
|
+
.where(eq(objects.apId, objectApId))
|
|
332
|
+
.get();
|
|
333
|
+
|
|
334
|
+
const mediaPlan = obj
|
|
335
|
+
? await deleteAttachedMediaUploadsForObject(
|
|
336
|
+
db,
|
|
337
|
+
obj,
|
|
338
|
+
new Set([objectApId]),
|
|
339
|
+
media,
|
|
340
|
+
)
|
|
341
|
+
: { mediaKeys: [], mediaUploadIds: [] };
|
|
310
342
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
343
|
+
const mediaDeleteStatements = chunkForInClause(mediaPlan.mediaUploadIds).map(
|
|
344
|
+
(ids) =>
|
|
345
|
+
db
|
|
346
|
+
.delete(mediaUploads)
|
|
347
|
+
.where(inArray(mediaUploads.id, ids)) as D1Statement,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
mediaKeys: [...new Set(mediaPlan.mediaKeys)],
|
|
352
|
+
statements: [
|
|
353
|
+
...mediaDeleteStatements,
|
|
354
|
+
db.delete(likes).where(eq(likes.objectApId, objectApId)) as D1Statement,
|
|
355
|
+
db
|
|
356
|
+
.delete(announces)
|
|
357
|
+
.where(eq(announces.objectApId, objectApId)) as D1Statement,
|
|
358
|
+
db
|
|
359
|
+
.delete(bookmarks)
|
|
360
|
+
.where(eq(bookmarks.objectApId, objectApId)) as D1Statement,
|
|
361
|
+
db
|
|
362
|
+
.delete(objectRecipients)
|
|
363
|
+
.where(eq(objectRecipients.objectApId, objectApId)) as D1Statement,
|
|
364
|
+
db
|
|
365
|
+
.delete(storyViews)
|
|
366
|
+
.where(eq(storyViews.storyApId, objectApId)) as D1Statement,
|
|
367
|
+
db
|
|
368
|
+
.delete(storyVotes)
|
|
369
|
+
.where(eq(storyVotes.storyApId, objectApId)) as D1Statement,
|
|
370
|
+
db
|
|
371
|
+
.delete(storyShares)
|
|
372
|
+
.where(eq(storyShares.storyApId, objectApId)) as D1Statement,
|
|
373
|
+
...activityProjectionDeleteStatements(
|
|
374
|
+
db,
|
|
375
|
+
eq(activities.objectApId, objectApId),
|
|
327
376
|
),
|
|
377
|
+
] as [D1Statement, ...D1Statement[]],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Reap every child row for a set of objects without deleting the object rows.
|
|
383
|
+
* This is the set-shaped counterpart to {@link deleteObjectCascade}; callers
|
|
384
|
+
* still own the final object delete and trailing {@link purgeMediaBlobs}.
|
|
385
|
+
*
|
|
386
|
+
* The old bulk callers invoked the singular helper once per object. On D1 that
|
|
387
|
+
* meant one attachment read plus eight serial delete round-trips per post: five
|
|
388
|
+
* empty posts took about ten seconds in a real workerd probe and larger domain
|
|
389
|
+
* purges could outlive the request. Here each <=90-id D1-safe chunk issues one
|
|
390
|
+
* atomic twelve-statement batch, so latency scales by chunks rather than posts.
|
|
391
|
+
*/
|
|
392
|
+
export async function deleteObjectsCascade(
|
|
393
|
+
db: Database,
|
|
394
|
+
objectApIds: string[],
|
|
395
|
+
media?: IObjectStorage,
|
|
396
|
+
): Promise<string[]> {
|
|
397
|
+
const uniqueApIds = [...new Set(objectApIds)];
|
|
398
|
+
if (uniqueApIds.length === 0) return [];
|
|
399
|
+
|
|
400
|
+
const cascadeObjects: CascadeObject[] = [];
|
|
401
|
+
for (const chunk of chunkForInClause(uniqueApIds)) {
|
|
402
|
+
cascadeObjects.push(
|
|
403
|
+
...(await db
|
|
404
|
+
.select({
|
|
405
|
+
apId: objects.apId,
|
|
406
|
+
attributedTo: objects.attributedTo,
|
|
407
|
+
attachmentsJson: objects.attachmentsJson,
|
|
408
|
+
})
|
|
409
|
+
.from(objects)
|
|
410
|
+
.where(inArray(objects.apId, chunk))),
|
|
328
411
|
);
|
|
329
|
-
|
|
412
|
+
}
|
|
413
|
+
if (cascadeObjects.length === 0) return [];
|
|
414
|
+
const existingApIds = cascadeObjects.map((obj) => obj.apId);
|
|
415
|
+
const removedObjectApIds = new Set(existingApIds);
|
|
416
|
+
|
|
417
|
+
// Remote attachments are normally ordinary remote URLs with no local
|
|
418
|
+
// media_uploads rows. Discover the small set of authors that actually own a
|
|
419
|
+
// managed upload before entering the per-object media GC, otherwise every
|
|
420
|
+
// image post would reintroduce one serial indexed read during defederation.
|
|
421
|
+
const objectsWithAttachments = cascadeObjects.filter(
|
|
422
|
+
(obj) => obj.attachmentsJson && obj.attachmentsJson !== "[]",
|
|
423
|
+
);
|
|
424
|
+
const authorsWithUploads = new Set<string>();
|
|
425
|
+
if (objectsWithAttachments.length > 0) {
|
|
426
|
+
const authors = [
|
|
427
|
+
...new Set(objectsWithAttachments.map((obj) => obj.attributedTo)),
|
|
428
|
+
];
|
|
429
|
+
for (const chunk of chunkForInClause(authors)) {
|
|
430
|
+
const rows = await db
|
|
431
|
+
.selectDistinct({ uploaderApId: mediaUploads.uploaderApId })
|
|
432
|
+
.from(mediaUploads)
|
|
433
|
+
.where(inArray(mediaUploads.uploaderApId, chunk));
|
|
434
|
+
for (const row of rows) authorsWithUploads.add(row.uploaderApId);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const mediaKeys = new Set<string>();
|
|
439
|
+
const mediaUploadIds = new Set<string>();
|
|
440
|
+
for (const obj of objectsWithAttachments) {
|
|
441
|
+
if (!authorsWithUploads.has(obj.attributedTo)) continue;
|
|
442
|
+
const plan = await deleteAttachedMediaUploadsForObject(
|
|
443
|
+
db,
|
|
444
|
+
obj,
|
|
445
|
+
removedObjectApIds,
|
|
446
|
+
media,
|
|
447
|
+
);
|
|
448
|
+
for (const key of plan.mediaKeys) mediaKeys.add(key);
|
|
449
|
+
for (const id of plan.mediaUploadIds) mediaUploadIds.add(id);
|
|
450
|
+
}
|
|
451
|
+
const mediaStatements = chunkForInClause([...mediaUploadIds]).map(
|
|
452
|
+
(ids) =>
|
|
453
|
+
db
|
|
454
|
+
.delete(mediaUploads)
|
|
455
|
+
.where(inArray(mediaUploads.id, ids)) as D1Statement,
|
|
456
|
+
);
|
|
457
|
+
for (
|
|
458
|
+
let offset = 0;
|
|
459
|
+
offset < mediaStatements.length;
|
|
460
|
+
offset += D1_MAX_BATCH_STATEMENTS
|
|
461
|
+
) {
|
|
462
|
+
const page = mediaStatements.slice(
|
|
463
|
+
offset,
|
|
464
|
+
offset + D1_MAX_BATCH_STATEMENTS,
|
|
465
|
+
);
|
|
466
|
+
await runBatch(db, page as [D1Statement, ...D1Statement[]]);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for (const chunk of chunkForInClause(existingApIds)) {
|
|
470
|
+
// Cancel every notification/delivery projection of retained Activities
|
|
471
|
+
// that target these objects. A queued Create/Update must not be delivered
|
|
472
|
+
// after its object has disappeared, and an in-flight/terminal push job or
|
|
473
|
+
// claim must not retain that deleted payload. Keep the Activity ledger rows
|
|
474
|
+
// themselves; callers may need history/Undo, and the new outbound Delete
|
|
475
|
+
// Activity is created only after this cascade so it remains unaffected.
|
|
476
|
+
await runBatch(db, [
|
|
477
|
+
db.delete(likes).where(inArray(likes.objectApId, chunk)),
|
|
478
|
+
db.delete(announces).where(inArray(announces.objectApId, chunk)),
|
|
479
|
+
db.delete(bookmarks).where(inArray(bookmarks.objectApId, chunk)),
|
|
480
|
+
db
|
|
481
|
+
.delete(objectRecipients)
|
|
482
|
+
.where(inArray(objectRecipients.objectApId, chunk)),
|
|
483
|
+
db.delete(storyViews).where(inArray(storyViews.storyApId, chunk)),
|
|
484
|
+
db.delete(storyVotes).where(inArray(storyVotes.storyApId, chunk)),
|
|
485
|
+
db.delete(storyShares).where(inArray(storyShares.storyApId, chunk)),
|
|
486
|
+
...activityProjectionDeleteStatements(
|
|
487
|
+
db,
|
|
488
|
+
inArray(activities.objectApId, chunk),
|
|
489
|
+
),
|
|
490
|
+
]);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return [...mediaKeys];
|
|
330
494
|
}
|