@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
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
and,
|
|
3
|
+
asc,
|
|
4
|
+
eq,
|
|
5
|
+
exists,
|
|
6
|
+
gt,
|
|
7
|
+
inArray,
|
|
8
|
+
isNotNull,
|
|
9
|
+
isNull,
|
|
10
|
+
ne,
|
|
11
|
+
not,
|
|
12
|
+
notExists,
|
|
13
|
+
or,
|
|
14
|
+
sql,
|
|
15
|
+
} from "drizzle-orm";
|
|
16
|
+
import type { BatchItem } from "drizzle-orm/batch";
|
|
2
17
|
import {
|
|
3
18
|
activities,
|
|
4
19
|
actors,
|
|
@@ -6,6 +21,7 @@ import {
|
|
|
6
21
|
blocks,
|
|
7
22
|
bookmarks,
|
|
8
23
|
communities,
|
|
24
|
+
communityBans,
|
|
9
25
|
communityInvites,
|
|
10
26
|
communityJoinRequests,
|
|
11
27
|
communityMembers,
|
|
@@ -33,15 +49,32 @@ import type { Database } from "../../db/index.ts";
|
|
|
33
49
|
import type { Env } from "../types.ts";
|
|
34
50
|
import type { IObjectStorage } from "../runtime/types.ts";
|
|
35
51
|
import { activityApId, generateId } from "../federation-helpers.ts";
|
|
52
|
+
import { chunkForInClause } from "../lib/chunk.ts";
|
|
36
53
|
import { snapshotAndEnqueueFollowerDeliveries } from "../lib/delivery/queue-batching.ts";
|
|
37
54
|
import { logger } from "../lib/logger.ts";
|
|
55
|
+
import { deleteActivitiesCascade } from "../lib/activity-delete-cascade.ts";
|
|
38
56
|
|
|
39
57
|
const log = logger.child({ component: "actors" });
|
|
40
58
|
|
|
59
|
+
// D1 has no interactive transactions, but both the D1 and libsql drivers
|
|
60
|
+
// expose an atomic `db.batch([...])` surface. Keep follow-edge removal and its
|
|
61
|
+
// counter reconciliation in one batch so a retry can never observe counters
|
|
62
|
+
// updated while the edge is still present.
|
|
63
|
+
type BatchStatement = BatchItem<"sqlite">;
|
|
64
|
+
interface BatchableDb {
|
|
65
|
+
batch(
|
|
66
|
+
statements: readonly [BatchStatement, ...BatchStatement[]],
|
|
67
|
+
): Promise<unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
41
70
|
/**
|
|
42
|
-
* Hard-delete an actor's media uploads
|
|
43
|
-
*
|
|
44
|
-
*
|
|
71
|
+
* Hard-delete an actor's media uploads after purging the backing R2 objects.
|
|
72
|
+
*
|
|
73
|
+
* The media row is the durable retry/GC identity (`r2_key`). When a configured
|
|
74
|
+
* object store rejects a delete, keep that row and propagate the error so the
|
|
75
|
+
* account route reports failure and a later retry can attempt the same key.
|
|
76
|
+
* Without a MEDIA binding there is no external delete to fail, so metadata can
|
|
77
|
+
* still be removed.
|
|
45
78
|
* Shared by the owner teardown (routes/actors.ts POST /me/delete) and each
|
|
46
79
|
* sub-account teardown in {@link teardownActor}.
|
|
47
80
|
*/
|
|
@@ -70,11 +103,128 @@ export async function purgeActorMediaUploads(
|
|
|
70
103
|
count: keys.length,
|
|
71
104
|
error: err,
|
|
72
105
|
});
|
|
106
|
+
// Do not discard the only durable identity from which a later retry or
|
|
107
|
+
// GC pass can recover the object. The caller must return a non-success
|
|
108
|
+
// response while the metadata remains available for retry.
|
|
109
|
+
throw err;
|
|
73
110
|
}
|
|
74
111
|
}
|
|
75
112
|
await db.delete(mediaUploads).where(eq(mediaUploads.uploaderApId, apId));
|
|
76
113
|
}
|
|
77
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Tombstone and scrub an actor after every owner/sub-account teardown step has
|
|
117
|
+
* succeeded. Keeping this finalization separate lets the owner route retain a
|
|
118
|
+
* live actor/session while a sub-account still needs a retry.
|
|
119
|
+
*/
|
|
120
|
+
export async function finalizeActorDeletion(
|
|
121
|
+
db: Database,
|
|
122
|
+
apId: string,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
await actorDeletionUpdate(db, apId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function actorDeletionUpdate(db: Database, apId: string): BatchStatement {
|
|
128
|
+
return db
|
|
129
|
+
.update(actors)
|
|
130
|
+
.set({
|
|
131
|
+
preferredUsername: `deleted-${generateId()}`,
|
|
132
|
+
name: null,
|
|
133
|
+
summary: null,
|
|
134
|
+
iconUrl: null,
|
|
135
|
+
headerUrl: null,
|
|
136
|
+
takosUserId: null,
|
|
137
|
+
followerCount: 0,
|
|
138
|
+
followingCount: 0,
|
|
139
|
+
postCount: 0,
|
|
140
|
+
fieldsJson: "[]",
|
|
141
|
+
alsoKnownAsJson: "[]",
|
|
142
|
+
movedTo: null,
|
|
143
|
+
ownerActorApId: null,
|
|
144
|
+
role: "member",
|
|
145
|
+
deletedAt: nowIso(),
|
|
146
|
+
})
|
|
147
|
+
.where(eq(actors.apId, apId)) as unknown as BatchStatement;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Commit the owner's final tombstone and every owner/sub-account session
|
|
152
|
+
* deletion together. If either operation fails, D1/libsql rolls the whole
|
|
153
|
+
* batch back so the live owner session can retry the incomplete cascade.
|
|
154
|
+
*/
|
|
155
|
+
export async function finalizeActorDeletionAndSessions(
|
|
156
|
+
db: Database,
|
|
157
|
+
apId: string,
|
|
158
|
+
sessionActorIds: string[],
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const statements: BatchStatement[] = [actorDeletionUpdate(db, apId)];
|
|
161
|
+
for (const chunk of chunkForInClause(sessionActorIds)) {
|
|
162
|
+
statements.push(
|
|
163
|
+
db
|
|
164
|
+
.delete(sessions)
|
|
165
|
+
.where(inArray(sessions.memberId, chunk)) as unknown as BatchStatement,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
await (db as unknown as BatchableDb).batch(
|
|
169
|
+
statements as [BatchStatement, ...BatchStatement[]],
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Persist one stable outbound Delete intent and snapshot every current remote
|
|
175
|
+
* follower before its authority edge can be removed. Both account actors and
|
|
176
|
+
* community Group actors use the same durable-delivery contract.
|
|
177
|
+
*/
|
|
178
|
+
async function ensureDeleteFollowerSnapshot(
|
|
179
|
+
db: Database,
|
|
180
|
+
env: Env,
|
|
181
|
+
baseUrl: string,
|
|
182
|
+
subjectApId: string,
|
|
183
|
+
followersUrl: string,
|
|
184
|
+
): Promise<string> {
|
|
185
|
+
const existingDelete = await db
|
|
186
|
+
.select({ apId: activities.apId })
|
|
187
|
+
.from(activities)
|
|
188
|
+
.where(
|
|
189
|
+
and(
|
|
190
|
+
eq(activities.actorApId, subjectApId),
|
|
191
|
+
eq(activities.type, "Delete"),
|
|
192
|
+
eq(activities.direction, "outbound"),
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
.orderBy(asc(activities.createdAt))
|
|
196
|
+
.get();
|
|
197
|
+
const deleteActivityId =
|
|
198
|
+
existingDelete?.apId ?? activityApId(baseUrl, generateId());
|
|
199
|
+
const deleteActivity = {
|
|
200
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
201
|
+
id: deleteActivityId,
|
|
202
|
+
type: "Delete",
|
|
203
|
+
actor: subjectApId,
|
|
204
|
+
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
|
205
|
+
cc: [followersUrl],
|
|
206
|
+
object: subjectApId,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (!existingDelete) {
|
|
210
|
+
await db.insert(activities).values({
|
|
211
|
+
apId: deleteActivityId,
|
|
212
|
+
type: "Delete",
|
|
213
|
+
actorApId: subjectApId,
|
|
214
|
+
objectApId: subjectApId,
|
|
215
|
+
rawJson: JSON.stringify(deleteActivity),
|
|
216
|
+
direction: "outbound",
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
await snapshotAndEnqueueFollowerDeliveries(
|
|
220
|
+
db,
|
|
221
|
+
env,
|
|
222
|
+
deleteActivityId,
|
|
223
|
+
subjectApId,
|
|
224
|
+
);
|
|
225
|
+
return deleteActivityId;
|
|
226
|
+
}
|
|
227
|
+
|
|
78
228
|
/**
|
|
79
229
|
* Full per-actor teardown for account deletion: federate Delete(Actor),
|
|
80
230
|
* reconcile every counterparty's denormalized counters, delete all of the
|
|
@@ -92,6 +242,8 @@ export async function purgeActorMediaUploads(
|
|
|
92
242
|
* (POST /me/delete) — keep the two in sync.
|
|
93
243
|
*
|
|
94
244
|
* `followersUrl` is the actor's followers collection (used as the Delete's cc).
|
|
245
|
+
* `options.finalizeActor = false` leaves the actor live for a parent owner
|
|
246
|
+
* route to finalize after every sub-account has completed.
|
|
95
247
|
*/
|
|
96
248
|
export async function teardownActor(
|
|
97
249
|
db: Database,
|
|
@@ -99,89 +251,85 @@ export async function teardownActor(
|
|
|
99
251
|
baseUrl: string,
|
|
100
252
|
apId: string,
|
|
101
253
|
followersUrl: string,
|
|
254
|
+
options: { finalizeActor?: boolean } = {},
|
|
102
255
|
): Promise<void> {
|
|
103
256
|
// Federate the Delete BEFORE local teardown, snapshotting follower inboxes
|
|
104
257
|
// into delivery jobs while the follower graph + activity row still exist. The
|
|
105
258
|
// Delete activity row is preserved through teardown (excluded from the
|
|
106
259
|
// activities delete below) so the deliver_endpoint consumer can read its
|
|
107
260
|
// rawJson after the actor's other rows are gone.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
type: "Delete",
|
|
113
|
-
actor: apId,
|
|
114
|
-
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
|
115
|
-
cc: [followersUrl],
|
|
116
|
-
object: apId,
|
|
117
|
-
};
|
|
261
|
+
// Retry the same durable Delete intent when an earlier teardown attempt
|
|
262
|
+
// failed after snapshotting followers. Minting a fresh Activity here would
|
|
263
|
+
// see an already-erased follower graph, then the cleanup below could delete
|
|
264
|
+
// the original Activity and its only endpoint/resolution jobs.
|
|
118
265
|
try {
|
|
119
|
-
await db
|
|
120
|
-
apId: deleteActivityId,
|
|
121
|
-
type: "Delete",
|
|
122
|
-
actorApId: apId,
|
|
123
|
-
objectApId: apId,
|
|
124
|
-
rawJson: JSON.stringify(deleteActivity),
|
|
125
|
-
direction: "outbound",
|
|
126
|
-
});
|
|
127
|
-
await snapshotAndEnqueueFollowerDeliveries(db, env, deleteActivityId, apId);
|
|
266
|
+
await ensureDeleteFollowerSnapshot(db, env, baseUrl, apId, followersUrl);
|
|
128
267
|
} catch (err) {
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
268
|
+
// Queue publication is already best-effort inside the snapshot helper, but
|
|
269
|
+
// the durable Activity/outbox write is not. Continuing would erase the
|
|
270
|
+
// follower graph and make the missing remote Delete impossible to rebuild.
|
|
271
|
+
// Preserve the live actor/session as retry authority, matching media purge
|
|
272
|
+
// failures later in this same teardown.
|
|
273
|
+
log.error("Failed to persist account Delete follower snapshot", {
|
|
274
|
+
event: "actors.account.delete_snapshot_failed",
|
|
132
275
|
actor: apId,
|
|
133
276
|
error: err,
|
|
134
277
|
});
|
|
278
|
+
throw err;
|
|
135
279
|
}
|
|
136
280
|
|
|
137
|
-
await db.delete(sessions).where(eq(sessions.memberId, apId));
|
|
138
|
-
|
|
139
281
|
// Reconcile counterparties' follower/following counts BEFORE dropping edges.
|
|
140
282
|
// Only ACCEPTED edges ever incremented a counter; gt(...,0) guards underflow.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
283
|
+
// Keep both updates and the edge delete in one atomic batch. If a later
|
|
284
|
+
// teardown step fails, a retry sees either the original edge+counts or the
|
|
285
|
+
// fully removed edge+reconciled counts — never a half-applied decrement.
|
|
286
|
+
await (db as unknown as BatchableDb).batch([
|
|
287
|
+
db
|
|
288
|
+
.update(actors)
|
|
289
|
+
.set({ followerCount: sql`${actors.followerCount} - 1` })
|
|
290
|
+
.where(
|
|
291
|
+
and(
|
|
292
|
+
inArray(
|
|
293
|
+
actors.apId,
|
|
294
|
+
db
|
|
295
|
+
.select({ id: follows.followingApId })
|
|
296
|
+
.from(follows)
|
|
297
|
+
.where(
|
|
298
|
+
and(
|
|
299
|
+
eq(follows.followerApId, apId),
|
|
300
|
+
eq(follows.status, "accepted"),
|
|
301
|
+
),
|
|
155
302
|
),
|
|
156
|
-
|
|
303
|
+
),
|
|
304
|
+
gt(actors.followerCount, 0),
|
|
157
305
|
),
|
|
158
|
-
gt(actors.followerCount, 0),
|
|
159
306
|
),
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
307
|
+
db
|
|
308
|
+
.update(actors)
|
|
309
|
+
.set({ followingCount: sql`${actors.followingCount} - 1` })
|
|
310
|
+
.where(
|
|
311
|
+
and(
|
|
312
|
+
inArray(
|
|
313
|
+
actors.apId,
|
|
314
|
+
db
|
|
315
|
+
.select({ id: follows.followerApId })
|
|
316
|
+
.from(follows)
|
|
317
|
+
.where(
|
|
318
|
+
and(
|
|
319
|
+
eq(follows.followingApId, apId),
|
|
320
|
+
eq(follows.status, "accepted"),
|
|
321
|
+
),
|
|
175
322
|
),
|
|
176
|
-
|
|
323
|
+
),
|
|
324
|
+
gt(actors.followingCount, 0),
|
|
177
325
|
),
|
|
178
|
-
gt(actors.followingCount, 0),
|
|
179
326
|
),
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
327
|
+
db
|
|
328
|
+
.delete(follows)
|
|
329
|
+
.where(
|
|
330
|
+
or(eq(follows.followerApId, apId), eq(follows.followingApId, apId)),
|
|
331
|
+
),
|
|
332
|
+
]);
|
|
185
333
|
await db
|
|
186
334
|
.delete(blocks)
|
|
187
335
|
.where(or(eq(blocks.blockerApId, apId), eq(blocks.blockedApId, apId)));
|
|
@@ -189,57 +337,60 @@ export async function teardownActor(
|
|
|
189
337
|
.delete(mutes)
|
|
190
338
|
.where(or(eq(mutes.muterApId, apId), eq(mutes.mutedApId, apId)));
|
|
191
339
|
|
|
192
|
-
// Reconcile like/announce/share counters on OTHER actors' objects
|
|
193
|
-
// deleting this actor's edges
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
.
|
|
203
|
-
|
|
204
|
-
|
|
340
|
+
// Reconcile like/announce/share counters on OTHER actors' objects together
|
|
341
|
+
// with deleting this actor's edges. If a delete fails after a counter update,
|
|
342
|
+
// a retry must not see the surviving edge and apply the same decrement again.
|
|
343
|
+
await (db as unknown as BatchableDb).batch([
|
|
344
|
+
db
|
|
345
|
+
.update(objects)
|
|
346
|
+
.set({ likeCount: sql`${objects.likeCount} - 1` })
|
|
347
|
+
.where(
|
|
348
|
+
and(
|
|
349
|
+
inArray(
|
|
350
|
+
objects.apId,
|
|
351
|
+
db
|
|
352
|
+
.select({ id: likes.objectApId })
|
|
353
|
+
.from(likes)
|
|
354
|
+
.where(eq(likes.actorApId, apId)),
|
|
355
|
+
),
|
|
356
|
+
gt(objects.likeCount, 0),
|
|
205
357
|
),
|
|
206
|
-
gt(objects.likeCount, 0),
|
|
207
358
|
),
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
359
|
+
db
|
|
360
|
+
.update(objects)
|
|
361
|
+
.set({ announceCount: sql`${objects.announceCount} - 1` })
|
|
362
|
+
.where(
|
|
363
|
+
and(
|
|
364
|
+
inArray(
|
|
365
|
+
objects.apId,
|
|
366
|
+
db
|
|
367
|
+
.select({ id: announces.objectApId })
|
|
368
|
+
.from(announces)
|
|
369
|
+
.where(eq(announces.actorApId, apId)),
|
|
370
|
+
),
|
|
371
|
+
gt(objects.announceCount, 0),
|
|
220
372
|
),
|
|
221
|
-
gt(objects.announceCount, 0),
|
|
222
373
|
),
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
374
|
+
db
|
|
375
|
+
.update(objects)
|
|
376
|
+
.set({ shareCount: sql`${objects.shareCount} - 1` })
|
|
377
|
+
.where(
|
|
378
|
+
and(
|
|
379
|
+
inArray(
|
|
380
|
+
objects.apId,
|
|
381
|
+
db
|
|
382
|
+
.select({ id: storyShares.storyApId })
|
|
383
|
+
.from(storyShares)
|
|
384
|
+
.where(eq(storyShares.actorApId, apId)),
|
|
385
|
+
),
|
|
386
|
+
gt(objects.shareCount, 0),
|
|
235
387
|
),
|
|
236
|
-
gt(objects.shareCount, 0),
|
|
237
388
|
),
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
389
|
+
db.delete(likes).where(eq(likes.actorApId, apId)),
|
|
390
|
+
db.delete(bookmarks).where(eq(bookmarks.actorApId, apId)),
|
|
391
|
+
db.delete(announces).where(eq(announces.actorApId, apId)),
|
|
392
|
+
db.delete(storyShares).where(eq(storyShares.actorApId, apId)),
|
|
393
|
+
]);
|
|
243
394
|
|
|
244
395
|
await db.delete(inbox).where(eq(inbox.actorApId, apId));
|
|
245
396
|
await db
|
|
@@ -257,13 +408,13 @@ export async function teardownActor(
|
|
|
257
408
|
.delete(notificationPushJobs)
|
|
258
409
|
.where(eq(notificationPushJobs.actorApId, apId));
|
|
259
410
|
|
|
260
|
-
// Media:
|
|
411
|
+
// Media: purge backing R2 first, then remove the durable upload metadata.
|
|
412
|
+
// A storage failure propagates and leaves the row for the retry path.
|
|
261
413
|
await purgeActorMediaUploads(db, env.MEDIA, apId);
|
|
262
414
|
|
|
263
415
|
// Story interactions the actor performed on OTHER/remote stories.
|
|
264
416
|
await db.delete(storyVotes).where(eq(storyVotes.actorApId, apId));
|
|
265
417
|
await db.delete(storyViews).where(eq(storyViews.actorApId, apId));
|
|
266
|
-
await db.delete(storyShares).where(eq(storyShares.actorApId, apId));
|
|
267
418
|
|
|
268
419
|
// Per-actor DM status metadata (no FK cascade).
|
|
269
420
|
await db.delete(dmReadStatus).where(eq(dmReadStatus.actorApId, apId));
|
|
@@ -298,7 +449,7 @@ export async function teardownActor(
|
|
|
298
449
|
})
|
|
299
450
|
.from(communityMembers)
|
|
300
451
|
.where(eq(communityMembers.actorApId, apId));
|
|
301
|
-
const
|
|
452
|
+
const retirementCandidates: string[] = [];
|
|
302
453
|
|
|
303
454
|
// Hand off sole-owned communities to the oldest remaining member.
|
|
304
455
|
for (const m of memberships) {
|
|
@@ -336,41 +487,172 @@ export async function teardownActor(
|
|
|
336
487
|
eq(communityMembers.actorApId, heir.actorApId),
|
|
337
488
|
),
|
|
338
489
|
);
|
|
490
|
+
} else {
|
|
491
|
+
retirementCandidates.push(m.communityApId);
|
|
339
492
|
}
|
|
340
493
|
}
|
|
341
494
|
|
|
342
|
-
|
|
495
|
+
// A community with no successor cannot keep a live Group actor after its
|
|
496
|
+
// only owner disappears. Retire it first with an atomic last-member guard.
|
|
497
|
+
// Keeping the membership/follow graph at this stage gives a failed snapshot
|
|
498
|
+
// a durable retry path; once deletedAt is set, every join/write/read route is
|
|
499
|
+
// closed so no new heir can race in between retirement and cleanup.
|
|
500
|
+
const retiredCommunityApIds: string[] = [];
|
|
501
|
+
for (const communityApId of retirementCandidates) {
|
|
502
|
+
const otherMember = db
|
|
503
|
+
.select({ actorApId: communityMembers.actorApId })
|
|
504
|
+
.from(communityMembers)
|
|
505
|
+
.where(
|
|
506
|
+
and(
|
|
507
|
+
eq(communityMembers.communityApId, communityApId),
|
|
508
|
+
ne(communityMembers.actorApId, apId),
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
.limit(1);
|
|
512
|
+
const deletingOwner = db
|
|
513
|
+
.select({ actorApId: communityMembers.actorApId })
|
|
514
|
+
.from(communityMembers)
|
|
515
|
+
.where(
|
|
516
|
+
and(
|
|
517
|
+
eq(communityMembers.communityApId, communityApId),
|
|
518
|
+
eq(communityMembers.actorApId, apId),
|
|
519
|
+
eq(communityMembers.role, "owner"),
|
|
520
|
+
),
|
|
521
|
+
)
|
|
522
|
+
.limit(1);
|
|
343
523
|
await db
|
|
344
524
|
.update(communities)
|
|
345
|
-
.set({
|
|
525
|
+
.set({ deletedAt: nowIso() })
|
|
346
526
|
.where(
|
|
347
527
|
and(
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
.from(communityMembers)
|
|
353
|
-
.where(eq(communityMembers.actorApId, apId)),
|
|
354
|
-
),
|
|
355
|
-
gt(communities.memberCount, 0),
|
|
528
|
+
eq(communities.apId, communityApId),
|
|
529
|
+
isNull(communities.deletedAt),
|
|
530
|
+
notExists(otherMember),
|
|
531
|
+
exists(deletingOwner),
|
|
356
532
|
),
|
|
357
533
|
);
|
|
534
|
+
|
|
535
|
+
const retired = await db
|
|
536
|
+
.select({
|
|
537
|
+
deletedAt: communities.deletedAt,
|
|
538
|
+
followersUrl: communities.followersUrl,
|
|
539
|
+
})
|
|
540
|
+
.from(communities)
|
|
541
|
+
.where(eq(communities.apId, communityApId))
|
|
542
|
+
.get();
|
|
543
|
+
if (!retired?.deletedAt) continue;
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
await ensureDeleteFollowerSnapshot(
|
|
547
|
+
db,
|
|
548
|
+
env,
|
|
549
|
+
baseUrl,
|
|
550
|
+
communityApId,
|
|
551
|
+
retired.followersUrl,
|
|
552
|
+
);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
log.error("Failed to persist community Delete follower snapshot", {
|
|
555
|
+
event: "actors.account.community_delete_snapshot_failed",
|
|
556
|
+
actor: apId,
|
|
557
|
+
community: communityApId,
|
|
558
|
+
error: err,
|
|
559
|
+
});
|
|
560
|
+
throw err;
|
|
561
|
+
}
|
|
562
|
+
retiredCommunityApIds.push(communityApId);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (memberships.length > 0) {
|
|
566
|
+
// Keep the denormalized count and its membership authority edge atomic.
|
|
567
|
+
// Otherwise a failed DELETE leaves the edge visible to a retry, which
|
|
568
|
+
// would decrement the same community for a second time.
|
|
569
|
+
const membershipCleanup: BatchStatement[] = [
|
|
570
|
+
db
|
|
571
|
+
.update(communities)
|
|
572
|
+
.set({ memberCount: sql`${communities.memberCount} - 1` })
|
|
573
|
+
.where(
|
|
574
|
+
and(
|
|
575
|
+
inArray(
|
|
576
|
+
communities.apId,
|
|
577
|
+
db
|
|
578
|
+
.select({ id: communityMembers.communityApId })
|
|
579
|
+
.from(communityMembers)
|
|
580
|
+
.where(eq(communityMembers.actorApId, apId)),
|
|
581
|
+
),
|
|
582
|
+
gt(communities.memberCount, 0),
|
|
583
|
+
),
|
|
584
|
+
) as unknown as BatchStatement,
|
|
585
|
+
];
|
|
586
|
+
|
|
587
|
+
// A retired Group keeps its row + signing keys while the durable Delete
|
|
588
|
+
// drains, but no membership, moderation/read state, or follow authority
|
|
589
|
+
// should remain. Chunk every IN predicate for D1's 100-bind limit and keep
|
|
590
|
+
// all retirement cleanup in the same batch as memberCount + membership.
|
|
591
|
+
// The follows DELETE binds each id twice (follower OR following), so use
|
|
592
|
+
// 45 rather than the normal 90 to stay below D1's 100-bind query ceiling.
|
|
593
|
+
for (const chunk of chunkForInClause(retiredCommunityApIds, 45)) {
|
|
594
|
+
membershipCleanup.push(
|
|
595
|
+
db
|
|
596
|
+
.delete(follows)
|
|
597
|
+
.where(
|
|
598
|
+
or(
|
|
599
|
+
inArray(follows.followerApId, chunk),
|
|
600
|
+
inArray(follows.followingApId, chunk),
|
|
601
|
+
),
|
|
602
|
+
) as unknown as BatchStatement,
|
|
603
|
+
db
|
|
604
|
+
.delete(communityJoinRequests)
|
|
605
|
+
.where(
|
|
606
|
+
inArray(communityJoinRequests.communityApId, chunk),
|
|
607
|
+
) as unknown as BatchStatement,
|
|
608
|
+
db
|
|
609
|
+
.delete(communityInvites)
|
|
610
|
+
.where(
|
|
611
|
+
inArray(communityInvites.communityApId, chunk),
|
|
612
|
+
) as unknown as BatchStatement,
|
|
613
|
+
db
|
|
614
|
+
.delete(communityBans)
|
|
615
|
+
.where(
|
|
616
|
+
inArray(communityBans.communityApId, chunk),
|
|
617
|
+
) as unknown as BatchStatement,
|
|
618
|
+
db
|
|
619
|
+
.delete(dmCommunityReadStatus)
|
|
620
|
+
.where(
|
|
621
|
+
inArray(dmCommunityReadStatus.communityApId, chunk),
|
|
622
|
+
) as unknown as BatchStatement,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
membershipCleanup.push(
|
|
626
|
+
db
|
|
627
|
+
.delete(communityMembers)
|
|
628
|
+
.where(
|
|
629
|
+
eq(communityMembers.actorApId, apId),
|
|
630
|
+
) as unknown as BatchStatement,
|
|
631
|
+
);
|
|
632
|
+
await (db as unknown as BatchableDb).batch(
|
|
633
|
+
membershipCleanup as [BatchStatement, ...BatchStatement[]],
|
|
634
|
+
);
|
|
358
635
|
}
|
|
359
|
-
await db.delete(communityMembers).where(eq(communityMembers.actorApId, apId));
|
|
360
636
|
|
|
361
637
|
await db
|
|
362
638
|
.delete(objectRecipients)
|
|
363
639
|
.where(eq(objectRecipients.recipientApId, apId));
|
|
364
|
-
// Preserve
|
|
365
|
-
// rawJson)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
640
|
+
// Preserve every outbound federation Delete activity (the delivery consumer
|
|
641
|
+
// needs rawJson). Current retries reuse the oldest one, while retaining all
|
|
642
|
+
// covers rows created by older builds without cancelling their delivery
|
|
643
|
+
// projections before they drain.
|
|
644
|
+
await deleteActivitiesCascade(
|
|
645
|
+
db,
|
|
646
|
+
and(
|
|
647
|
+
eq(activities.actorApId, apId),
|
|
648
|
+
not(
|
|
649
|
+
and(
|
|
650
|
+
eq(activities.type, "Delete"),
|
|
651
|
+
eq(activities.direction, "outbound"),
|
|
652
|
+
)!,
|
|
372
653
|
),
|
|
373
|
-
)
|
|
654
|
+
)!,
|
|
655
|
+
);
|
|
374
656
|
|
|
375
657
|
// Interactions on the actor's authored objects, via subqueries.
|
|
376
658
|
const authoredObjectIds = () =>
|
|
@@ -418,26 +700,10 @@ export async function teardownActor(
|
|
|
418
700
|
|
|
419
701
|
await db.delete(objects).where(eq(objects.attributedTo, apId));
|
|
420
702
|
|
|
421
|
-
// Tombstone + scrub the actor row
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
name: null,
|
|
428
|
-
summary: null,
|
|
429
|
-
iconUrl: null,
|
|
430
|
-
headerUrl: null,
|
|
431
|
-
takosUserId: null,
|
|
432
|
-
followerCount: 0,
|
|
433
|
-
followingCount: 0,
|
|
434
|
-
postCount: 0,
|
|
435
|
-
fieldsJson: "[]",
|
|
436
|
-
alsoKnownAsJson: "[]",
|
|
437
|
-
movedTo: null,
|
|
438
|
-
ownerActorApId: null,
|
|
439
|
-
role: "member",
|
|
440
|
-
deletedAt: nowIso(),
|
|
441
|
-
})
|
|
442
|
-
.where(eq(actors.apId, apId));
|
|
703
|
+
// Tombstone + scrub the actor row only when the caller asks for immediate
|
|
704
|
+
// finalization. The owner route defers this final step until every
|
|
705
|
+
// sub-account has completed, retaining a live retry authority on failure.
|
|
706
|
+
if (options.finalizeActor !== false) {
|
|
707
|
+
await finalizeActorDeletion(db, apId);
|
|
708
|
+
}
|
|
443
709
|
}
|