@takosjp/yurucommu-core 3.0.0
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/LICENSE +16 -0
- package/README.md +82 -0
- package/migrations/0001_init.sql +495 -0
- package/migrations/0002_social_remote_actor_edges.sql +92 -0
- package/migrations/0003_activity_remote_object_edges.sql +68 -0
- package/migrations/0004_blocklist.sql +26 -0
- package/migrations/0005_story_community_scope.sql +13 -0
- package/migrations/0006_dm_community_read_status.sql +19 -0
- package/migrations/0007_moderation_reports.sql +22 -0
- package/migrations/0008_actor_fields_aka.sql +18 -0
- package/migrations/0009_object_tags.sql +13 -0
- package/migrations/0010_object_recipients_drop_actor_fk.sql +34 -0
- package/migrations/0011_drop_remote_actor_fks.sql +205 -0
- package/migrations/0012_objects_content_fts.sql +39 -0
- package/migrations/0013_efficiency_indexes.sql +13 -0
- package/migrations/0014_inbox_actor_created_idx.sql +15 -0
- package/migrations/0015_community_bans.sql +16 -0
- package/migrations/0016_namespace_takos_oidc_subject.sql +19 -0
- package/migrations/0017_mobile_push_registrations.sql +22 -0
- package/migrations/README.md +122 -0
- package/package.json +75 -0
- package/packages/api/LICENSE +16 -0
- package/packages/api/package.json +30 -0
- package/packages/api/src/index.ts +4 -0
- package/packages/api/src/lib/api/account.ts +20 -0
- package/packages/api/src/lib/api/actors.ts +149 -0
- package/packages/api/src/lib/api/auth.ts +46 -0
- package/packages/api/src/lib/api/communities.ts +329 -0
- package/packages/api/src/lib/api/dm.test.ts +67 -0
- package/packages/api/src/lib/api/dm.ts +236 -0
- package/packages/api/src/lib/api/fetch.ts +111 -0
- package/packages/api/src/lib/api/follow.ts +30 -0
- package/packages/api/src/lib/api/media.ts +100 -0
- package/packages/api/src/lib/api/moderation.ts +98 -0
- package/packages/api/src/lib/api/normalize.ts +71 -0
- package/packages/api/src/lib/api/notifications.test.ts +63 -0
- package/packages/api/src/lib/api/notifications.ts +61 -0
- package/packages/api/src/lib/api/posts.test.ts +110 -0
- package/packages/api/src/lib/api/posts.ts +181 -0
- package/packages/api/src/lib/api/recommendations.ts +22 -0
- package/packages/api/src/lib/api/search.ts +88 -0
- package/packages/api/src/lib/api/stories.ts +80 -0
- package/packages/api/src/lib/api.ts +15 -0
- package/packages/api/src/lib/fetch-with-timeout.ts +42 -0
- package/packages/api/src/lib/transport.ts +40 -0
- package/packages/api/src/social-server.ts +47 -0
- package/packages/api/src/types/index.ts +185 -0
- package/scripts/apply-takosumi-migrations.ts +621 -0
- package/src/backend/federation-helpers.ts +36 -0
- package/src/backend/index.ts +872 -0
- package/src/backend/lib/account-migration.ts +106 -0
- package/src/backend/lib/activitypub-actor-cache.ts +238 -0
- package/src/backend/lib/activitypub-helpers.ts +131 -0
- package/src/backend/lib/activitypub-validators.ts +323 -0
- package/src/backend/lib/ap-context.ts +16 -0
- package/src/backend/lib/ap-ids.ts +101 -0
- package/src/backend/lib/ap-response.ts +30 -0
- package/src/backend/lib/ap-signing.ts +87 -0
- package/src/backend/lib/ap-verify.ts +670 -0
- package/src/backend/lib/auth-lockout.ts +230 -0
- package/src/backend/lib/backend-paths.ts +34 -0
- package/src/backend/lib/base64.ts +30 -0
- package/src/backend/lib/blocklist-purge.ts +109 -0
- package/src/backend/lib/blocklist.ts +279 -0
- package/src/backend/lib/chunk.ts +33 -0
- package/src/backend/lib/client-ip.ts +169 -0
- package/src/backend/lib/community-visibility.ts +230 -0
- package/src/backend/lib/crypto.ts +424 -0
- package/src/backend/lib/delivery/circuit.ts +265 -0
- package/src/backend/lib/delivery/metrics.ts +30 -0
- package/src/backend/lib/delivery/planner.ts +190 -0
- package/src/backend/lib/delivery/queue-batching.ts +626 -0
- package/src/backend/lib/delivery/queue-delivery.ts +641 -0
- package/src/backend/lib/delivery/queue.ts +576 -0
- package/src/backend/lib/delivery/transformers.ts +56 -0
- package/src/backend/lib/delivery/types.ts +139 -0
- package/src/backend/lib/errors.ts +114 -0
- package/src/backend/lib/federation-fetch.ts +296 -0
- package/src/backend/lib/feed-cursor.ts +57 -0
- package/src/backend/lib/feed-exclude.ts +48 -0
- package/src/backend/lib/hex.ts +8 -0
- package/src/backend/lib/log-mask.ts +213 -0
- package/src/backend/lib/logger.ts +285 -0
- package/src/backend/lib/mobile-contract.ts +137 -0
- package/src/backend/lib/oauth-providers.ts +324 -0
- package/src/backend/lib/oauth-utils.ts +148 -0
- package/src/backend/lib/oidc-id-token.ts +151 -0
- package/src/backend/lib/parse-helpers.ts +31 -0
- package/src/backend/lib/post-visibility.ts +190 -0
- package/src/backend/lib/session-actor.ts +61 -0
- package/src/backend/lib/ssrf.ts +428 -0
- package/src/backend/lib/strip-image-metadata.ts +191 -0
- package/src/backend/middleware/bearer-auth.ts +70 -0
- package/src/backend/middleware/body-limit.ts +212 -0
- package/src/backend/middleware/cache.ts +429 -0
- package/src/backend/middleware/csrf.ts +130 -0
- package/src/backend/middleware/error-handler.ts +77 -0
- package/src/backend/middleware/rate-limit.ts +308 -0
- package/src/backend/public.ts +21 -0
- package/src/backend/routes/account-teardown.ts +430 -0
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +354 -0
- package/src/backend/routes/activitypub/handlers/inbound-timestamp.ts +29 -0
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1634 -0
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +547 -0
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +497 -0
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +262 -0
- package/src/backend/routes/activitypub/handlers/user-inbox-handlers.ts +35 -0
- package/src/backend/routes/activitypub/inbox-types.ts +74 -0
- package/src/backend/routes/activitypub/inbox.ts +1191 -0
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/activitypub/query-helpers.ts +227 -0
- package/src/backend/routes/activitypub.ts +616 -0
- package/src/backend/routes/actors-helpers.ts +487 -0
- package/src/backend/routes/actors.ts +1311 -0
- package/src/backend/routes/apps.ts +313 -0
- package/src/backend/routes/auth-helpers.ts +566 -0
- package/src/backend/routes/auth.ts +615 -0
- package/src/backend/routes/communities/membership-invites.ts +208 -0
- package/src/backend/routes/communities/membership-join.ts +335 -0
- package/src/backend/routes/communities/membership-members.ts +539 -0
- package/src/backend/routes/communities/membership-requests.ts +296 -0
- package/src/backend/routes/communities/membership-shared.ts +364 -0
- package/src/backend/routes/communities/messages.ts +479 -0
- package/src/backend/routes/communities/routes.ts +624 -0
- package/src/backend/routes/communities.ts +21 -0
- package/src/backend/routes/dm/contacts.ts +525 -0
- package/src/backend/routes/dm/conversations-helpers.ts +197 -0
- package/src/backend/routes/dm/conversations.ts +25 -0
- package/src/backend/routes/dm/messages.ts +658 -0
- package/src/backend/routes/dm/query-helpers.ts +85 -0
- package/src/backend/routes/dm/read-archive.ts +228 -0
- package/src/backend/routes/dm/requests.ts +222 -0
- package/src/backend/routes/dm/typing.ts +81 -0
- package/src/backend/routes/dm.ts +15 -0
- package/src/backend/routes/follow-helpers.ts +370 -0
- package/src/backend/routes/follow.ts +588 -0
- package/src/backend/routes/media.ts +692 -0
- package/src/backend/routes/mobile.ts +159 -0
- package/src/backend/routes/moderation.ts +373 -0
- package/src/backend/routes/notifications.ts +757 -0
- package/src/backend/routes/posts/delete-cascade.ts +330 -0
- package/src/backend/routes/posts/interactions.ts +795 -0
- package/src/backend/routes/posts/post-helpers.ts +847 -0
- package/src/backend/routes/posts/queries.ts +537 -0
- package/src/backend/routes/posts/routes.ts +865 -0
- package/src/backend/routes/posts/transformers.ts +161 -0
- package/src/backend/routes/posts.ts +17 -0
- package/src/backend/routes/recommendations.ts +88 -0
- package/src/backend/routes/search.ts +730 -0
- package/src/backend/routes/stories/interactions.ts +576 -0
- package/src/backend/routes/stories/query-helpers.ts +482 -0
- package/src/backend/routes/stories/routes.ts +906 -0
- package/src/backend/routes/stories.ts +13 -0
- package/src/backend/routes/takos-tools/dm.ts +249 -0
- package/src/backend/routes/takos-tools/follows.ts +225 -0
- package/src/backend/routes/takos-tools/posts.ts +292 -0
- package/src/backend/routes/takos-tools/search.ts +228 -0
- package/src/backend/routes/takos-tools/timeline.ts +132 -0
- package/src/backend/routes/takos-tools/types.ts +10 -0
- package/src/backend/routes/takos-tools-response.ts +178 -0
- package/src/backend/routes/takos-tools.ts +153 -0
- package/src/backend/routes/timeline.ts +755 -0
- package/src/backend/runtime/bun.ts +620 -0
- package/src/backend/runtime/cloudflare.ts +202 -0
- package/src/backend/runtime/compat-bun/types.ts +44 -0
- package/src/backend/runtime/memory-kv.ts +104 -0
- package/src/backend/runtime/shared.ts +142 -0
- package/src/backend/runtime/types.ts +205 -0
- package/src/backend/server.ts +636 -0
- package/src/backend/types.ts +143 -0
- package/src/db/index.ts +97 -0
- package/src/db/schema/actors.ts +129 -0
- package/src/db/schema/communities.ts +133 -0
- package/src/db/schema/date-utils.ts +17 -0
- package/src/db/schema/index.ts +17 -0
- package/src/db/schema/messaging.ts +241 -0
- package/src/db/schema/mobile.ts +37 -0
- package/src/db/schema/posts.ts +150 -0
- package/src/db/schema/relations.ts +266 -0
- package/src/db/schema/reports.ts +33 -0
- package/src/db/schema/social.ts +106 -0
- package/src/db/schema/stories.ts +70 -0
- package/src/db/schema.ts +15 -0
- package/src/plugin/public.ts +7 -0
- package/src/runtime/site-worker.ts +10 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { and, asc, eq, gt, inArray, isNotNull, ne, or, sql } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
activities,
|
|
4
|
+
actors,
|
|
5
|
+
announces,
|
|
6
|
+
blocks,
|
|
7
|
+
bookmarks,
|
|
8
|
+
communities,
|
|
9
|
+
communityInvites,
|
|
10
|
+
communityJoinRequests,
|
|
11
|
+
communityMembers,
|
|
12
|
+
dmArchivedConversations,
|
|
13
|
+
dmCommunityReadStatus,
|
|
14
|
+
dmReadStatus,
|
|
15
|
+
dmTyping,
|
|
16
|
+
follows,
|
|
17
|
+
inbox,
|
|
18
|
+
likes,
|
|
19
|
+
mediaUploads,
|
|
20
|
+
mutes,
|
|
21
|
+
notificationArchived,
|
|
22
|
+
nowIso,
|
|
23
|
+
objectRecipients,
|
|
24
|
+
objects,
|
|
25
|
+
sessions,
|
|
26
|
+
storyShares,
|
|
27
|
+
storyViews,
|
|
28
|
+
storyVotes,
|
|
29
|
+
} from "../../db/index.ts";
|
|
30
|
+
import type { Database } from "../../db/index.ts";
|
|
31
|
+
import type { Env } from "../types.ts";
|
|
32
|
+
import type { IObjectStorage } from "../runtime/types.ts";
|
|
33
|
+
import { activityApId, generateId } from "../federation-helpers.ts";
|
|
34
|
+
import { snapshotAndEnqueueFollowerDeliveries } from "../lib/delivery/queue-batching.ts";
|
|
35
|
+
import { logger } from "../lib/logger.ts";
|
|
36
|
+
|
|
37
|
+
const log = logger.child({ component: "actors" });
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hard-delete an actor's media uploads and best-effort purge the backing R2
|
|
41
|
+
* objects. The DB rows are removed regardless of whether the object-store
|
|
42
|
+
* delete succeeds; account teardown is never blocked on storage availability.
|
|
43
|
+
* Shared by the owner teardown (routes/actors.ts POST /me/delete) and each
|
|
44
|
+
* sub-account teardown in {@link teardownActor}.
|
|
45
|
+
*/
|
|
46
|
+
export async function purgeActorMediaUploads(
|
|
47
|
+
db: Database,
|
|
48
|
+
media: IObjectStorage | undefined,
|
|
49
|
+
apId: string,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const uploads = await db
|
|
52
|
+
.select({ r2Key: mediaUploads.r2Key })
|
|
53
|
+
.from(mediaUploads)
|
|
54
|
+
.where(eq(mediaUploads.uploaderApId, apId));
|
|
55
|
+
if (uploads.length === 0) return;
|
|
56
|
+
if (media) {
|
|
57
|
+
const keys = uploads.map((u) => u.r2Key);
|
|
58
|
+
// R2 caps a single delete() at 1000 keys; chunk the purge.
|
|
59
|
+
const R2_DELETE_BATCH = 1000;
|
|
60
|
+
try {
|
|
61
|
+
for (let i = 0; i < keys.length; i += R2_DELETE_BATCH) {
|
|
62
|
+
await media.delete(keys.slice(i, i + R2_DELETE_BATCH));
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
log.error("Failed to purge R2 objects for deleted account", {
|
|
66
|
+
event: "actors.account.delete_media_purge_failed",
|
|
67
|
+
actor: apId,
|
|
68
|
+
count: keys.length,
|
|
69
|
+
error: err,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
await db.delete(mediaUploads).where(eq(mediaUploads.uploaderApId, apId));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Full per-actor teardown for account deletion: federate Delete(Actor),
|
|
78
|
+
* reconcile every counterparty's denormalized counters, delete all of the
|
|
79
|
+
* actor's edges / interactions / memberships / media / DM state, hand off
|
|
80
|
+
* sole-owned communities to an heir, hard-delete its authored objects, and
|
|
81
|
+
* tombstone+scrub the actor row.
|
|
82
|
+
*
|
|
83
|
+
* Applied to the deleting OWNER **and to each of its sub-accounts** (profiles
|
|
84
|
+
* minted via /accounts + /switch). A sub-account is a first-class actor that can
|
|
85
|
+
* post / follow / like / join+own communities, so without this its "deleted"
|
|
86
|
+
* content stays live in feeds, its edges keep counterparty counters inflated,
|
|
87
|
+
* its memberships keep community rosters/counts wrong (and a sole-owned
|
|
88
|
+
* community becomes permanently unmanageable), and remote followers never learn
|
|
89
|
+
* it is gone. This MUST mirror the owner teardown in routes/actors.ts
|
|
90
|
+
* (POST /me/delete) — keep the two in sync.
|
|
91
|
+
*
|
|
92
|
+
* `followersUrl` is the actor's followers collection (used as the Delete's cc).
|
|
93
|
+
*/
|
|
94
|
+
export async function teardownActor(
|
|
95
|
+
db: Database,
|
|
96
|
+
env: Env,
|
|
97
|
+
baseUrl: string,
|
|
98
|
+
apId: string,
|
|
99
|
+
followersUrl: string,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
// Federate the Delete BEFORE local teardown, snapshotting follower inboxes
|
|
102
|
+
// into delivery jobs while the follower graph + activity row still exist. The
|
|
103
|
+
// Delete activity row is preserved through teardown (excluded from the
|
|
104
|
+
// activities delete below) so the deliver_endpoint consumer can read its
|
|
105
|
+
// rawJson after the actor's other rows are gone.
|
|
106
|
+
const deleteActivityId = activityApId(baseUrl, generateId());
|
|
107
|
+
const deleteActivity = {
|
|
108
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
109
|
+
id: deleteActivityId,
|
|
110
|
+
type: "Delete",
|
|
111
|
+
actor: apId,
|
|
112
|
+
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
|
113
|
+
cc: [followersUrl],
|
|
114
|
+
object: apId,
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
await db.insert(activities).values({
|
|
118
|
+
apId: deleteActivityId,
|
|
119
|
+
type: "Delete",
|
|
120
|
+
actorApId: apId,
|
|
121
|
+
objectApId: apId,
|
|
122
|
+
rawJson: JSON.stringify(deleteActivity),
|
|
123
|
+
direction: "outbound",
|
|
124
|
+
});
|
|
125
|
+
await snapshotAndEnqueueFollowerDeliveries(db, env, deleteActivityId, apId);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// Federation is best-effort; never block local teardown on it.
|
|
128
|
+
log.error("Failed to enqueue account Delete federation", {
|
|
129
|
+
event: "actors.account.delete_federation_failed",
|
|
130
|
+
actor: apId,
|
|
131
|
+
error: err,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await db.delete(sessions).where(eq(sessions.memberId, apId));
|
|
136
|
+
|
|
137
|
+
// Reconcile counterparties' follower/following counts BEFORE dropping edges.
|
|
138
|
+
// Only ACCEPTED edges ever incremented a counter; gt(...,0) guards underflow.
|
|
139
|
+
await db
|
|
140
|
+
.update(actors)
|
|
141
|
+
.set({ followerCount: sql`${actors.followerCount} - 1` })
|
|
142
|
+
.where(
|
|
143
|
+
and(
|
|
144
|
+
inArray(
|
|
145
|
+
actors.apId,
|
|
146
|
+
db
|
|
147
|
+
.select({ id: follows.followingApId })
|
|
148
|
+
.from(follows)
|
|
149
|
+
.where(
|
|
150
|
+
and(
|
|
151
|
+
eq(follows.followerApId, apId),
|
|
152
|
+
eq(follows.status, "accepted"),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
gt(actors.followerCount, 0),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
await db
|
|
160
|
+
.update(actors)
|
|
161
|
+
.set({ followingCount: sql`${actors.followingCount} - 1` })
|
|
162
|
+
.where(
|
|
163
|
+
and(
|
|
164
|
+
inArray(
|
|
165
|
+
actors.apId,
|
|
166
|
+
db
|
|
167
|
+
.select({ id: follows.followerApId })
|
|
168
|
+
.from(follows)
|
|
169
|
+
.where(
|
|
170
|
+
and(
|
|
171
|
+
eq(follows.followingApId, apId),
|
|
172
|
+
eq(follows.status, "accepted"),
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
gt(actors.followingCount, 0),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await db
|
|
181
|
+
.delete(follows)
|
|
182
|
+
.where(or(eq(follows.followerApId, apId), eq(follows.followingApId, apId)));
|
|
183
|
+
await db
|
|
184
|
+
.delete(blocks)
|
|
185
|
+
.where(or(eq(blocks.blockerApId, apId), eq(blocks.blockedApId, apId)));
|
|
186
|
+
await db
|
|
187
|
+
.delete(mutes)
|
|
188
|
+
.where(or(eq(mutes.muterApId, apId), eq(mutes.mutedApId, apId)));
|
|
189
|
+
|
|
190
|
+
// Reconcile like/announce/share counters on OTHER actors' objects BEFORE
|
|
191
|
+
// deleting this actor's edges (griefable inflated counters otherwise).
|
|
192
|
+
await db
|
|
193
|
+
.update(objects)
|
|
194
|
+
.set({ likeCount: sql`${objects.likeCount} - 1` })
|
|
195
|
+
.where(
|
|
196
|
+
and(
|
|
197
|
+
inArray(
|
|
198
|
+
objects.apId,
|
|
199
|
+
db
|
|
200
|
+
.select({ id: likes.objectApId })
|
|
201
|
+
.from(likes)
|
|
202
|
+
.where(eq(likes.actorApId, apId)),
|
|
203
|
+
),
|
|
204
|
+
gt(objects.likeCount, 0),
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
await db
|
|
208
|
+
.update(objects)
|
|
209
|
+
.set({ announceCount: sql`${objects.announceCount} - 1` })
|
|
210
|
+
.where(
|
|
211
|
+
and(
|
|
212
|
+
inArray(
|
|
213
|
+
objects.apId,
|
|
214
|
+
db
|
|
215
|
+
.select({ id: announces.objectApId })
|
|
216
|
+
.from(announces)
|
|
217
|
+
.where(eq(announces.actorApId, apId)),
|
|
218
|
+
),
|
|
219
|
+
gt(objects.announceCount, 0),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
await db
|
|
223
|
+
.update(objects)
|
|
224
|
+
.set({ shareCount: sql`${objects.shareCount} - 1` })
|
|
225
|
+
.where(
|
|
226
|
+
and(
|
|
227
|
+
inArray(
|
|
228
|
+
objects.apId,
|
|
229
|
+
db
|
|
230
|
+
.select({ id: storyShares.storyApId })
|
|
231
|
+
.from(storyShares)
|
|
232
|
+
.where(eq(storyShares.actorApId, apId)),
|
|
233
|
+
),
|
|
234
|
+
gt(objects.shareCount, 0),
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
await db.delete(likes).where(eq(likes.actorApId, apId));
|
|
239
|
+
await db.delete(bookmarks).where(eq(bookmarks.actorApId, apId));
|
|
240
|
+
await db.delete(announces).where(eq(announces.actorApId, apId));
|
|
241
|
+
|
|
242
|
+
await db.delete(inbox).where(eq(inbox.actorApId, apId));
|
|
243
|
+
await db
|
|
244
|
+
.delete(notificationArchived)
|
|
245
|
+
.where(eq(notificationArchived.actorApId, apId));
|
|
246
|
+
|
|
247
|
+
// Media: hard-delete the actor's uploads + best-effort purge backing R2.
|
|
248
|
+
await purgeActorMediaUploads(db, env.MEDIA, apId);
|
|
249
|
+
|
|
250
|
+
// Story interactions the actor performed on OTHER/remote stories.
|
|
251
|
+
await db.delete(storyVotes).where(eq(storyVotes.actorApId, apId));
|
|
252
|
+
await db.delete(storyViews).where(eq(storyViews.actorApId, apId));
|
|
253
|
+
await db.delete(storyShares).where(eq(storyShares.actorApId, apId));
|
|
254
|
+
|
|
255
|
+
// Per-actor DM status metadata (no FK cascade).
|
|
256
|
+
await db.delete(dmReadStatus).where(eq(dmReadStatus.actorApId, apId));
|
|
257
|
+
await db
|
|
258
|
+
.delete(dmCommunityReadStatus)
|
|
259
|
+
.where(eq(dmCommunityReadStatus.actorApId, apId));
|
|
260
|
+
await db
|
|
261
|
+
.delete(dmArchivedConversations)
|
|
262
|
+
.where(eq(dmArchivedConversations.actorApId, apId));
|
|
263
|
+
await db
|
|
264
|
+
.delete(dmTyping)
|
|
265
|
+
.where(or(eq(dmTyping.actorApId, apId), eq(dmTyping.recipientApId, apId)));
|
|
266
|
+
|
|
267
|
+
// Community membership lifecycle rows.
|
|
268
|
+
await db
|
|
269
|
+
.delete(communityJoinRequests)
|
|
270
|
+
.where(eq(communityJoinRequests.actorApId, apId));
|
|
271
|
+
await db
|
|
272
|
+
.delete(communityInvites)
|
|
273
|
+
.where(
|
|
274
|
+
or(
|
|
275
|
+
eq(communityInvites.invitedByApId, apId),
|
|
276
|
+
eq(communityInvites.usedByApId, apId),
|
|
277
|
+
eq(communityInvites.invitedApId, apId),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const memberships = await db
|
|
282
|
+
.select({
|
|
283
|
+
communityApId: communityMembers.communityApId,
|
|
284
|
+
role: communityMembers.role,
|
|
285
|
+
})
|
|
286
|
+
.from(communityMembers)
|
|
287
|
+
.where(eq(communityMembers.actorApId, apId));
|
|
288
|
+
const communityApIds = memberships.map((m) => m.communityApId);
|
|
289
|
+
|
|
290
|
+
// Hand off sole-owned communities to the oldest remaining member.
|
|
291
|
+
for (const m of memberships) {
|
|
292
|
+
if (m.role !== "owner") continue;
|
|
293
|
+
const otherOwner = await db
|
|
294
|
+
.select({ actorApId: communityMembers.actorApId })
|
|
295
|
+
.from(communityMembers)
|
|
296
|
+
.where(
|
|
297
|
+
and(
|
|
298
|
+
eq(communityMembers.communityApId, m.communityApId),
|
|
299
|
+
eq(communityMembers.role, "owner"),
|
|
300
|
+
ne(communityMembers.actorApId, apId),
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
.get();
|
|
304
|
+
if (otherOwner) continue;
|
|
305
|
+
const heir = await db
|
|
306
|
+
.select({ actorApId: communityMembers.actorApId })
|
|
307
|
+
.from(communityMembers)
|
|
308
|
+
.where(
|
|
309
|
+
and(
|
|
310
|
+
eq(communityMembers.communityApId, m.communityApId),
|
|
311
|
+
ne(communityMembers.actorApId, apId),
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
.orderBy(asc(communityMembers.joinedAt))
|
|
315
|
+
.get();
|
|
316
|
+
if (heir) {
|
|
317
|
+
await db
|
|
318
|
+
.update(communityMembers)
|
|
319
|
+
.set({ role: "owner" })
|
|
320
|
+
.where(
|
|
321
|
+
and(
|
|
322
|
+
eq(communityMembers.communityApId, m.communityApId),
|
|
323
|
+
eq(communityMembers.actorApId, heir.actorApId),
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (communityApIds.length > 0) {
|
|
330
|
+
await db
|
|
331
|
+
.update(communities)
|
|
332
|
+
.set({ memberCount: sql`${communities.memberCount} - 1` })
|
|
333
|
+
.where(
|
|
334
|
+
and(
|
|
335
|
+
inArray(
|
|
336
|
+
communities.apId,
|
|
337
|
+
db
|
|
338
|
+
.select({ id: communityMembers.communityApId })
|
|
339
|
+
.from(communityMembers)
|
|
340
|
+
.where(eq(communityMembers.actorApId, apId)),
|
|
341
|
+
),
|
|
342
|
+
gt(communities.memberCount, 0),
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
await db.delete(communityMembers).where(eq(communityMembers.actorApId, apId));
|
|
347
|
+
|
|
348
|
+
await db
|
|
349
|
+
.delete(objectRecipients)
|
|
350
|
+
.where(eq(objectRecipients.recipientApId, apId));
|
|
351
|
+
// Preserve the federation Delete activity (the delivery consumer needs its
|
|
352
|
+
// rawJson); all other activities by this actor go.
|
|
353
|
+
await db
|
|
354
|
+
.delete(activities)
|
|
355
|
+
.where(
|
|
356
|
+
and(
|
|
357
|
+
eq(activities.actorApId, apId),
|
|
358
|
+
ne(activities.apId, deleteActivityId),
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Interactions on the actor's authored objects, via subqueries.
|
|
363
|
+
const authoredObjectIds = () =>
|
|
364
|
+
db
|
|
365
|
+
.select({ id: objects.apId })
|
|
366
|
+
.from(objects)
|
|
367
|
+
.where(eq(objects.attributedTo, apId));
|
|
368
|
+
await db.delete(likes).where(inArray(likes.objectApId, authoredObjectIds()));
|
|
369
|
+
await db
|
|
370
|
+
.delete(announces)
|
|
371
|
+
.where(inArray(announces.objectApId, authoredObjectIds()));
|
|
372
|
+
await db
|
|
373
|
+
.delete(bookmarks)
|
|
374
|
+
.where(inArray(bookmarks.objectApId, authoredObjectIds()));
|
|
375
|
+
await db
|
|
376
|
+
.delete(storyVotes)
|
|
377
|
+
.where(inArray(storyVotes.storyApId, authoredObjectIds()));
|
|
378
|
+
await db
|
|
379
|
+
.delete(storyViews)
|
|
380
|
+
.where(inArray(storyViews.storyApId, authoredObjectIds()));
|
|
381
|
+
await db
|
|
382
|
+
.delete(storyShares)
|
|
383
|
+
.where(inArray(storyShares.storyApId, authoredObjectIds()));
|
|
384
|
+
await db
|
|
385
|
+
.delete(objectRecipients)
|
|
386
|
+
.where(inArray(objectRecipients.objectApId, authoredObjectIds()));
|
|
387
|
+
|
|
388
|
+
// Recompute affected parents' replyCount as COUNT(*) of remaining replies.
|
|
389
|
+
await db
|
|
390
|
+
.update(objects)
|
|
391
|
+
.set({
|
|
392
|
+
replyCount: sql`(SELECT COUNT(*) FROM objects AS child WHERE child.in_reply_to = ${objects.apId} AND child.attributed_to <> ${apId})`,
|
|
393
|
+
})
|
|
394
|
+
.where(
|
|
395
|
+
inArray(
|
|
396
|
+
objects.apId,
|
|
397
|
+
db
|
|
398
|
+
.select({ id: objects.inReplyTo })
|
|
399
|
+
.from(objects)
|
|
400
|
+
.where(
|
|
401
|
+
and(eq(objects.attributedTo, apId), isNotNull(objects.inReplyTo)),
|
|
402
|
+
),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
await db.delete(objects).where(eq(objects.attributedTo, apId));
|
|
407
|
+
|
|
408
|
+
// Tombstone + scrub the actor row (keep only the delivery signer's key
|
|
409
|
+
// material; free the unique handle by renaming it to a sentinel).
|
|
410
|
+
await db
|
|
411
|
+
.update(actors)
|
|
412
|
+
.set({
|
|
413
|
+
preferredUsername: `deleted-${generateId()}`,
|
|
414
|
+
name: null,
|
|
415
|
+
summary: null,
|
|
416
|
+
iconUrl: null,
|
|
417
|
+
headerUrl: null,
|
|
418
|
+
takosUserId: null,
|
|
419
|
+
followerCount: 0,
|
|
420
|
+
followingCount: 0,
|
|
421
|
+
postCount: 0,
|
|
422
|
+
fieldsJson: "[]",
|
|
423
|
+
alsoKnownAsJson: "[]",
|
|
424
|
+
movedTo: null,
|
|
425
|
+
ownerActorApId: null,
|
|
426
|
+
role: "member",
|
|
427
|
+
deletedAt: nowIso(),
|
|
428
|
+
})
|
|
429
|
+
.where(eq(actors.apId, apId));
|
|
430
|
+
}
|