@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
|
@@ -2,42 +2,40 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { and, desc, eq, gt, inArray, sql } from "drizzle-orm";
|
|
3
3
|
import type { Env, Variables } from "../types.ts";
|
|
4
4
|
import {
|
|
5
|
-
activities,
|
|
6
5
|
actorCache,
|
|
7
6
|
actors,
|
|
8
7
|
follows,
|
|
9
8
|
inbox,
|
|
9
|
+
runBatch,
|
|
10
|
+
type D1Statement,
|
|
10
11
|
} from "../../db/index.ts";
|
|
11
12
|
import {
|
|
12
|
-
activityApId,
|
|
13
13
|
formatUsername,
|
|
14
|
-
generateId,
|
|
15
14
|
isLocal,
|
|
16
15
|
isSafeRemoteUrl,
|
|
17
16
|
parseLimit,
|
|
18
17
|
parseOffset,
|
|
19
18
|
} from "../federation-helpers.ts";
|
|
20
|
-
import { enqueueDeliveryToActor } from "../lib/delivery/queue.ts";
|
|
21
19
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
filterBlockedActorApIdsStrict,
|
|
21
|
+
isActorBlockedStrict,
|
|
22
|
+
operatorActorNotBlockedSql,
|
|
23
|
+
} from "../lib/blocklist.ts";
|
|
24
|
+
import {
|
|
25
25
|
findPendingFollow,
|
|
26
26
|
handleLocalFollow,
|
|
27
27
|
handleRemoteFollow,
|
|
28
28
|
isResponse,
|
|
29
29
|
parseNonEmptyString,
|
|
30
30
|
parseStringArray,
|
|
31
|
+
prepareActivityDelivery,
|
|
32
|
+
prepareResponseIfRemote,
|
|
31
33
|
requireActorAndBody,
|
|
32
34
|
} from "./follow-helpers.ts";
|
|
33
35
|
import { logger } from "../lib/logger.ts";
|
|
34
36
|
|
|
35
37
|
const log = logger.child({ component: "follow" });
|
|
36
38
|
|
|
37
|
-
// `.batch` lives only on the concrete D1/libsql subclasses, not the Database
|
|
38
|
-
// union; reach it through a narrow structural cast (matching the other routes).
|
|
39
|
-
type Batchable = { batch: (stmts: unknown[]) => Promise<unknown> };
|
|
40
|
-
|
|
41
39
|
// Capped at 90 (not 100): the accepted ids are re-queried via
|
|
42
40
|
// `inArray(follows.followerApId, requesterApIds)` and Cloudflare D1 allows at
|
|
43
41
|
// most 100 bound parameters per query (libsql, used by the tests, allows ~32k).
|
|
@@ -62,6 +60,17 @@ follow.post("/", async (c) => {
|
|
|
62
60
|
return c.json({ error: "Cannot follow yourself" }, 400);
|
|
63
61
|
}
|
|
64
62
|
|
|
63
|
+
// An operator block is an instance-wide authority boundary, not merely an
|
|
64
|
+
// outbound delivery filter. Reject before reading/cleaning an existing edge
|
|
65
|
+
// or resolving a remote actor so the request cannot report success while the
|
|
66
|
+
// queue silently drops it, create an inert Follow ledger row, fetch the
|
|
67
|
+
// defederated host, or mutate a retained relationship that unblock restores.
|
|
68
|
+
// Match the personal-block route's not-found response to avoid presenting a
|
|
69
|
+
// blocked target through this mutation surface.
|
|
70
|
+
if (await isActorBlockedStrict(db, targetApId)) {
|
|
71
|
+
return c.json({ error: "Target actor not found" }, 404);
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
const existing = await db
|
|
66
75
|
.select()
|
|
67
76
|
.from(follows)
|
|
@@ -125,6 +134,23 @@ follow.delete("/", async (c) => {
|
|
|
125
134
|
const wasAccepted = existingFollow.status === "accepted";
|
|
126
135
|
const targetIsLocal = isLocal(targetApId, baseUrl);
|
|
127
136
|
|
|
137
|
+
const preparedUndo = targetIsLocal
|
|
138
|
+
? null
|
|
139
|
+
: await prepareActivityDelivery(
|
|
140
|
+
c.env,
|
|
141
|
+
db,
|
|
142
|
+
baseUrl,
|
|
143
|
+
"Undo",
|
|
144
|
+
actor.ap_id,
|
|
145
|
+
{
|
|
146
|
+
type: "Follow",
|
|
147
|
+
actor: actor.ap_id,
|
|
148
|
+
object: targetApId,
|
|
149
|
+
},
|
|
150
|
+
targetApId,
|
|
151
|
+
targetApId,
|
|
152
|
+
);
|
|
153
|
+
|
|
128
154
|
const deleteEdge = db
|
|
129
155
|
.delete(follows)
|
|
130
156
|
.where(
|
|
@@ -134,13 +160,14 @@ follow.delete("/", async (c) => {
|
|
|
134
160
|
),
|
|
135
161
|
);
|
|
136
162
|
|
|
163
|
+
const statements: D1Statement[] = [];
|
|
137
164
|
if (wasAccepted) {
|
|
138
165
|
// Co-commit the decrements + delete in ONE batch so a crash between them
|
|
139
166
|
// can't leave the edge gone with un-decremented counts (permanent over-
|
|
140
167
|
// count). Decrements run BEFORE the delete, guarded by EXISTS(accepted edge)
|
|
141
168
|
// + count>0 (underflow) — mirrors the federated undoFollowEdge.
|
|
142
169
|
const acceptedEdgeExists = sql`EXISTS (SELECT 1 FROM ${follows} WHERE ${follows.followerApId} = ${actor.ap_id} AND ${follows.followingApId} = ${targetApId} AND ${follows.status} = 'accepted')`;
|
|
143
|
-
|
|
170
|
+
statements.push(
|
|
144
171
|
db
|
|
145
172
|
.update(actors)
|
|
146
173
|
.set({ followingCount: sql`${actors.followingCount} - 1` })
|
|
@@ -150,10 +177,10 @@ follow.delete("/", async (c) => {
|
|
|
150
177
|
gt(actors.followingCount, 0),
|
|
151
178
|
acceptedEdgeExists,
|
|
152
179
|
),
|
|
153
|
-
),
|
|
154
|
-
|
|
180
|
+
) as D1Statement,
|
|
181
|
+
);
|
|
155
182
|
if (targetIsLocal) {
|
|
156
|
-
|
|
183
|
+
statements.push(
|
|
157
184
|
db
|
|
158
185
|
.update(actors)
|
|
159
186
|
.set({ followerCount: sql`${actors.followerCount} - 1` })
|
|
@@ -163,32 +190,14 @@ follow.delete("/", async (c) => {
|
|
|
163
190
|
gt(actors.followerCount, 0),
|
|
164
191
|
acceptedEdgeExists,
|
|
165
192
|
),
|
|
166
|
-
),
|
|
193
|
+
) as D1Statement,
|
|
167
194
|
);
|
|
168
195
|
}
|
|
169
|
-
stmts.push(deleteEdge);
|
|
170
|
-
await (db as unknown as Batchable).batch(stmts);
|
|
171
|
-
} else {
|
|
172
|
-
await deleteEdge;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!targetIsLocal) {
|
|
176
|
-
const undoObject = {
|
|
177
|
-
type: "Follow",
|
|
178
|
-
actor: actor.ap_id,
|
|
179
|
-
object: targetApId,
|
|
180
|
-
};
|
|
181
|
-
await createAndDeliverActivity(
|
|
182
|
-
c.env,
|
|
183
|
-
db,
|
|
184
|
-
baseUrl,
|
|
185
|
-
"Undo",
|
|
186
|
-
actor.ap_id,
|
|
187
|
-
undoObject,
|
|
188
|
-
targetApId,
|
|
189
|
-
targetApId,
|
|
190
|
-
);
|
|
191
196
|
}
|
|
197
|
+
statements.push(deleteEdge as D1Statement);
|
|
198
|
+
if (preparedUndo) statements.push(...preparedUndo.statements);
|
|
199
|
+
await runBatch(db, statements as [D1Statement, ...D1Statement[]]);
|
|
200
|
+
await preparedUndo?.publish();
|
|
192
201
|
|
|
193
202
|
return c.json({ success: true });
|
|
194
203
|
});
|
|
@@ -209,8 +218,10 @@ follow.post("/accept", async (c) => {
|
|
|
209
218
|
400,
|
|
210
219
|
);
|
|
211
220
|
}
|
|
221
|
+
if (await isActorBlockedStrict(db, requesterApId)) {
|
|
222
|
+
return c.json({ error: "No pending follow request" }, 404);
|
|
223
|
+
}
|
|
212
224
|
|
|
213
|
-
let pendingFollow: Awaited<ReturnType<typeof findPendingFollow>>;
|
|
214
225
|
try {
|
|
215
226
|
// Read the pending edge first (we need its activityApId for the Accept
|
|
216
227
|
// delivery below), then co-commit the flip + both increments in ONE batch.
|
|
@@ -223,21 +234,34 @@ follow.post("/accept", async (c) => {
|
|
|
223
234
|
// count nor under-count. Mirrors the federated handleAccept.
|
|
224
235
|
const pending = await findPendingFollow(db, requesterApId, actor.ap_id);
|
|
225
236
|
if (!pending) {
|
|
226
|
-
|
|
237
|
+
return c.json({ error: "No pending follow request" }, 404);
|
|
227
238
|
} else {
|
|
239
|
+
const preparedResponse = await prepareResponseIfRemote(
|
|
240
|
+
c.env,
|
|
241
|
+
db,
|
|
242
|
+
baseUrl,
|
|
243
|
+
"Accept",
|
|
244
|
+
actor.ap_id,
|
|
245
|
+
requesterApId,
|
|
246
|
+
pending.activityApId,
|
|
247
|
+
);
|
|
228
248
|
const pendingExists = sql`EXISTS (SELECT 1 FROM ${follows} WHERE ${follows.followerApId} = ${requesterApId} AND ${follows.followingApId} = ${actor.ap_id} AND ${follows.status} = 'pending')`;
|
|
229
|
-
const stmts:
|
|
249
|
+
const stmts: D1Statement[] = [
|
|
230
250
|
db
|
|
231
251
|
.update(actors)
|
|
232
252
|
.set({ followerCount: sql`${actors.followerCount} + 1` })
|
|
233
|
-
.where(
|
|
253
|
+
.where(
|
|
254
|
+
and(eq(actors.apId, actor.ap_id), pendingExists),
|
|
255
|
+
) as D1Statement,
|
|
234
256
|
];
|
|
235
257
|
if (isLocal(requesterApId, baseUrl)) {
|
|
236
258
|
stmts.push(
|
|
237
259
|
db
|
|
238
260
|
.update(actors)
|
|
239
261
|
.set({ followingCount: sql`${actors.followingCount} + 1` })
|
|
240
|
-
.where(
|
|
262
|
+
.where(
|
|
263
|
+
and(eq(actors.apId, requesterApId), pendingExists),
|
|
264
|
+
) as D1Statement,
|
|
241
265
|
);
|
|
242
266
|
}
|
|
243
267
|
stmts.push(
|
|
@@ -250,10 +274,11 @@ follow.post("/accept", async (c) => {
|
|
|
250
274
|
eq(follows.followingApId, actor.ap_id),
|
|
251
275
|
eq(follows.status, "pending"),
|
|
252
276
|
),
|
|
253
|
-
),
|
|
277
|
+
) as D1Statement,
|
|
254
278
|
);
|
|
255
|
-
|
|
256
|
-
|
|
279
|
+
if (preparedResponse) stmts.push(...preparedResponse.statements);
|
|
280
|
+
await runBatch(db, stmts as [D1Statement, ...D1Statement[]]);
|
|
281
|
+
await preparedResponse?.publish();
|
|
257
282
|
}
|
|
258
283
|
} catch (e) {
|
|
259
284
|
log.error("Error in accept", {
|
|
@@ -263,20 +288,6 @@ follow.post("/accept", async (c) => {
|
|
|
263
288
|
return c.json({ error: "Internal error" }, 500);
|
|
264
289
|
}
|
|
265
290
|
|
|
266
|
-
if (!pendingFollow) {
|
|
267
|
-
return c.json({ error: "No pending follow request" }, 404);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
await deliverResponseIfRemote(
|
|
271
|
-
c.env,
|
|
272
|
-
db,
|
|
273
|
-
baseUrl,
|
|
274
|
-
"Accept",
|
|
275
|
-
actor.ap_id,
|
|
276
|
-
requesterApId,
|
|
277
|
-
pendingFollow.activityApId,
|
|
278
|
-
);
|
|
279
|
-
|
|
280
291
|
return c.json({ success: true });
|
|
281
292
|
});
|
|
282
293
|
|
|
@@ -308,32 +319,31 @@ follow.post("/accept/batch", async (c) => {
|
|
|
308
319
|
);
|
|
309
320
|
}
|
|
310
321
|
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
const blockedRequesterApIds = await filterBlockedActorApIdsStrict(
|
|
323
|
+
db,
|
|
324
|
+
requesterApIds,
|
|
325
|
+
);
|
|
326
|
+
const visibleRequesterApIds = requesterApIds.filter(
|
|
327
|
+
(requesterApId) => !blockedRequesterApIds.has(requesterApId),
|
|
328
|
+
);
|
|
329
|
+
const pendingFollows =
|
|
330
|
+
visibleRequesterApIds.length === 0
|
|
331
|
+
? []
|
|
332
|
+
: await db
|
|
333
|
+
.select()
|
|
334
|
+
.from(follows)
|
|
335
|
+
.where(
|
|
336
|
+
and(
|
|
337
|
+
inArray(follows.followerApId, visibleRequesterApIds),
|
|
338
|
+
eq(follows.followingApId, actor.ap_id),
|
|
339
|
+
eq(follows.status, "pending"),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
321
342
|
const pendingFollowMap = new Map(
|
|
322
343
|
pendingFollows.map((f) => [f.followerApId, f]),
|
|
323
344
|
);
|
|
324
345
|
|
|
325
346
|
const results: { ap_id: string; success: boolean; error?: string }[] = [];
|
|
326
|
-
const activitiesToCreate: Array<{
|
|
327
|
-
apId: string;
|
|
328
|
-
type: string;
|
|
329
|
-
actorApId: string;
|
|
330
|
-
objectApId: string | undefined;
|
|
331
|
-
rawJson: string;
|
|
332
|
-
direction: string;
|
|
333
|
-
}> = [];
|
|
334
|
-
const remoteEnqueues: Array<{ activityId: string; recipientApId: string }> =
|
|
335
|
-
[];
|
|
336
|
-
|
|
337
347
|
for (const requesterApId of requesterApIds) {
|
|
338
348
|
const pendingFollow = pendingFollowMap.get(requesterApId);
|
|
339
349
|
if (!pendingFollow) {
|
|
@@ -352,19 +362,36 @@ follow.post("/accept/batch", async (c) => {
|
|
|
352
362
|
// accumulated increments for already-flipped rows. Increments guarded by
|
|
353
363
|
// EXISTS(pending) before the flip; the flip keeps its pending predicate.
|
|
354
364
|
const requesterIsLocal = isLocal(requesterApId, baseUrl);
|
|
365
|
+
const requesterIsSafeRemote =
|
|
366
|
+
!requesterIsLocal && isSafeRemoteUrl(requesterApId);
|
|
367
|
+
const preparedResponse = requesterIsSafeRemote
|
|
368
|
+
? await prepareResponseIfRemote(
|
|
369
|
+
c.env,
|
|
370
|
+
db,
|
|
371
|
+
baseUrl,
|
|
372
|
+
"Accept",
|
|
373
|
+
actor.ap_id,
|
|
374
|
+
requesterApId,
|
|
375
|
+
pendingFollow.activityApId,
|
|
376
|
+
)
|
|
377
|
+
: null;
|
|
355
378
|
const pendingExists = sql`EXISTS (SELECT 1 FROM ${follows} WHERE ${follows.followerApId} = ${requesterApId} AND ${follows.followingApId} = ${actor.ap_id} AND ${follows.status} = 'pending')`;
|
|
356
|
-
const stmts:
|
|
379
|
+
const stmts: D1Statement[] = [
|
|
357
380
|
db
|
|
358
381
|
.update(actors)
|
|
359
382
|
.set({ followerCount: sql`${actors.followerCount} + 1` })
|
|
360
|
-
.where(
|
|
383
|
+
.where(
|
|
384
|
+
and(eq(actors.apId, actor.ap_id), pendingExists),
|
|
385
|
+
) as D1Statement,
|
|
361
386
|
];
|
|
362
387
|
if (requesterIsLocal) {
|
|
363
388
|
stmts.push(
|
|
364
389
|
db
|
|
365
390
|
.update(actors)
|
|
366
391
|
.set({ followingCount: sql`${actors.followingCount} + 1` })
|
|
367
|
-
.where(
|
|
392
|
+
.where(
|
|
393
|
+
and(eq(actors.apId, requesterApId), pendingExists),
|
|
394
|
+
) as D1Statement,
|
|
368
395
|
);
|
|
369
396
|
}
|
|
370
397
|
stmts.push(
|
|
@@ -377,31 +404,15 @@ follow.post("/accept/batch", async (c) => {
|
|
|
377
404
|
eq(follows.followingApId, actor.ap_id),
|
|
378
405
|
eq(follows.status, "pending"),
|
|
379
406
|
),
|
|
380
|
-
),
|
|
407
|
+
) as D1Statement,
|
|
381
408
|
);
|
|
382
|
-
|
|
409
|
+
if (preparedResponse) stmts.push(...preparedResponse.statements);
|
|
410
|
+
await runBatch(db, stmts as [D1Statement, ...D1Statement[]]);
|
|
411
|
+
await preparedResponse?.publish();
|
|
383
412
|
|
|
384
413
|
if (requesterIsLocal) {
|
|
385
414
|
// local follower's followingCount already bumped in the batch above
|
|
386
|
-
} else if (
|
|
387
|
-
const id = activityApId(baseUrl, generateId());
|
|
388
|
-
const activity = buildApActivity(
|
|
389
|
-
"Accept",
|
|
390
|
-
actor.ap_id,
|
|
391
|
-
pendingFollow.activityApId,
|
|
392
|
-
id,
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
activitiesToCreate.push({
|
|
396
|
-
apId: id,
|
|
397
|
-
type: "Accept",
|
|
398
|
-
actorApId: actor.ap_id,
|
|
399
|
-
objectApId: pendingFollow.activityApId || undefined,
|
|
400
|
-
rawJson: JSON.stringify(activity),
|
|
401
|
-
direction: "outbound",
|
|
402
|
-
});
|
|
403
|
-
remoteEnqueues.push({ activityId: id, recipientApId: requesterApId });
|
|
404
|
-
} else {
|
|
415
|
+
} else if (!requesterIsSafeRemote) {
|
|
405
416
|
log.warn("Blocked unsafe remote actor", {
|
|
406
417
|
event: "follow.accept.unsafe_remote_actor",
|
|
407
418
|
actor: requesterApId,
|
|
@@ -421,18 +432,6 @@ follow.post("/accept/batch", async (c) => {
|
|
|
421
432
|
// (Counts are bumped per-request inside the loop's batch — no aggregate
|
|
422
433
|
// post-loop increment, which could be lost on a mid-loop crash.)
|
|
423
434
|
|
|
424
|
-
if (activitiesToCreate.length > 0) {
|
|
425
|
-
await db.insert(activities).values(activitiesToCreate);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (remoteEnqueues.length > 0) {
|
|
429
|
-
await Promise.allSettled(
|
|
430
|
-
remoteEnqueues.map((e) =>
|
|
431
|
-
enqueueDeliveryToActor(c.env, e.activityId, e.recipientApId),
|
|
432
|
-
),
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
435
|
return c.json({
|
|
437
436
|
results,
|
|
438
437
|
accepted_count: results.filter((r) => r.success).length,
|
|
@@ -468,28 +467,7 @@ follow.post("/reject", async (c) => {
|
|
|
468
467
|
// the INBOUND reject path (handleReject -> deleteFollowByCompoundKey) and the
|
|
469
468
|
// community join-request re-pend behaviour, letting a fresh Follow start clean.
|
|
470
469
|
// A pending edge was never counted, so no counter reconcile is needed.
|
|
471
|
-
await
|
|
472
|
-
.delete(follows)
|
|
473
|
-
.where(
|
|
474
|
-
and(
|
|
475
|
-
eq(follows.followerApId, requesterApId),
|
|
476
|
-
eq(follows.followingApId, actor.ap_id),
|
|
477
|
-
),
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
if (pendingFollow.activityApId) {
|
|
481
|
-
await db
|
|
482
|
-
.update(inbox)
|
|
483
|
-
.set({ read: 1 })
|
|
484
|
-
.where(
|
|
485
|
-
and(
|
|
486
|
-
eq(inbox.actorApId, actor.ap_id),
|
|
487
|
-
eq(inbox.activityApId, pendingFollow.activityApId),
|
|
488
|
-
),
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
await deliverResponseIfRemote(
|
|
470
|
+
const preparedResponse = await prepareResponseIfRemote(
|
|
493
471
|
c.env,
|
|
494
472
|
db,
|
|
495
473
|
baseUrl,
|
|
@@ -498,6 +476,33 @@ follow.post("/reject", async (c) => {
|
|
|
498
476
|
requesterApId,
|
|
499
477
|
pendingFollow.activityApId,
|
|
500
478
|
);
|
|
479
|
+
const statements: D1Statement[] = [
|
|
480
|
+
db
|
|
481
|
+
.delete(follows)
|
|
482
|
+
.where(
|
|
483
|
+
and(
|
|
484
|
+
eq(follows.followerApId, requesterApId),
|
|
485
|
+
eq(follows.followingApId, actor.ap_id),
|
|
486
|
+
),
|
|
487
|
+
) as D1Statement,
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
if (pendingFollow.activityApId) {
|
|
491
|
+
statements.push(
|
|
492
|
+
db
|
|
493
|
+
.update(inbox)
|
|
494
|
+
.set({ read: 1 })
|
|
495
|
+
.where(
|
|
496
|
+
and(
|
|
497
|
+
eq(inbox.actorApId, actor.ap_id),
|
|
498
|
+
eq(inbox.activityApId, pendingFollow.activityApId),
|
|
499
|
+
),
|
|
500
|
+
) as D1Statement,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
if (preparedResponse) statements.push(...preparedResponse.statements);
|
|
504
|
+
await runBatch(db, statements as [D1Statement, ...D1Statement[]]);
|
|
505
|
+
await preparedResponse?.publish();
|
|
501
506
|
|
|
502
507
|
return c.json({ success: true });
|
|
503
508
|
});
|
|
@@ -525,6 +530,7 @@ follow.get("/requests", async (c) => {
|
|
|
525
530
|
and(
|
|
526
531
|
eq(follows.followingApId, actor.ap_id),
|
|
527
532
|
eq(follows.status, "pending"),
|
|
533
|
+
operatorActorNotBlockedSql(sql`${follows.followerApId}`),
|
|
528
534
|
),
|
|
529
535
|
)
|
|
530
536
|
// followerApId is the PK discriminator (followingApId is fixed = me); add it
|
|
@@ -574,7 +580,7 @@ follow.get("/requests", async (c) => {
|
|
|
574
580
|
const actorInfo = actorInfoMap.get(f.followerApId);
|
|
575
581
|
return {
|
|
576
582
|
ap_id: f.followerApId,
|
|
577
|
-
username: formatUsername(f.followerApId),
|
|
583
|
+
username: formatUsername(f.followerApId, actorInfo?.preferredUsername),
|
|
578
584
|
preferred_username: actorInfo?.preferredUsername || null,
|
|
579
585
|
name: actorInfo?.name || null,
|
|
580
586
|
icon_url: actorInfo?.iconUrl || null,
|
|
@@ -6,13 +6,12 @@ import {
|
|
|
6
6
|
actors,
|
|
7
7
|
communities,
|
|
8
8
|
communityMembers,
|
|
9
|
-
follows,
|
|
10
9
|
mediaUploads,
|
|
11
10
|
objects,
|
|
12
11
|
} from "../../db/index.ts";
|
|
13
|
-
import { generateId
|
|
12
|
+
import { generateId } from "../federation-helpers.ts";
|
|
14
13
|
import { canViewerReadObject } from "../lib/community-visibility.ts";
|
|
15
|
-
import {
|
|
14
|
+
import { canViewerReadObjectFull } from "../lib/post-visibility.ts";
|
|
16
15
|
import { stripImageMetadata } from "../lib/strip-image-metadata.ts";
|
|
17
16
|
import { logger } from "../lib/logger.ts";
|
|
18
17
|
|
|
@@ -311,6 +310,7 @@ type ReferencingObject = {
|
|
|
311
310
|
visibility: string;
|
|
312
311
|
toJson: string;
|
|
313
312
|
ccJson: string;
|
|
313
|
+
audienceJson: string;
|
|
314
314
|
communityApId: string | null;
|
|
315
315
|
endTime: string | null;
|
|
316
316
|
};
|
|
@@ -364,6 +364,7 @@ async function findReferencingObject(
|
|
|
364
364
|
visibility: objects.visibility,
|
|
365
365
|
toJson: objects.toJson,
|
|
366
366
|
ccJson: objects.ccJson,
|
|
367
|
+
audienceJson: objects.audienceJson,
|
|
367
368
|
communityApId: objects.communityApId,
|
|
368
369
|
endTime: objects.endTime,
|
|
369
370
|
attachmentsJson: objects.attachmentsJson,
|
|
@@ -389,6 +390,7 @@ async function findReferencingObject(
|
|
|
389
390
|
visibility: row.visibility,
|
|
390
391
|
toJson: row.toJson,
|
|
391
392
|
ccJson: row.ccJson,
|
|
393
|
+
audienceJson: row.audienceJson,
|
|
392
394
|
communityApId: row.communityApId,
|
|
393
395
|
endTime: row.endTime,
|
|
394
396
|
};
|
|
@@ -518,102 +520,26 @@ async function checkMediaAuthorization(
|
|
|
518
520
|
return ALLOW_PRIVATE;
|
|
519
521
|
}
|
|
520
522
|
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
obj.endTime &&
|
|
528
|
-
obj.endTime <= new Date().toISOString()
|
|
529
|
-
) {
|
|
523
|
+
// Delegate object reach to the same gate used by post detail, replies,
|
|
524
|
+
// interactions and ActivityPub object reads. This includes community scope,
|
|
525
|
+
// Story expiry, accepted follows, visible to/cc recipients, and private
|
|
526
|
+
// bto/bcc recipient projections. Keeping one authority prevents the post body
|
|
527
|
+
// and its attachment from disagreeing about who may read them.
|
|
528
|
+
if (!(await canViewerReadObjectFull(db, obj, currentActorApId))) {
|
|
530
529
|
return currentActorApId ? DENY_NOT_AUTHORIZED : DENY_AUTH_REQUIRED;
|
|
531
530
|
}
|
|
532
531
|
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const allowed = await canViewerReadObject(
|
|
542
|
-
db,
|
|
543
|
-
{ communityApId: obj.communityApId },
|
|
544
|
-
currentActorApId,
|
|
545
|
-
);
|
|
546
|
-
if (!allowed) {
|
|
547
|
-
return currentActorApId ? DENY_NOT_AUTHORIZED : DENY_AUTH_REQUIRED;
|
|
548
|
-
}
|
|
549
|
-
// Community membership is necessary but NOT sufficient: a community post
|
|
550
|
-
// created with visibility=followers/direct must ALSO pass that per-post gate
|
|
551
|
-
// so this blob matches the post-detail / outbox / feed gates (which all
|
|
552
|
-
// follower-gate such a post). Public / unlisted / Story community posts are
|
|
553
|
-
// member-readable; followers/direct fall through to the gates below.
|
|
554
|
-
if (obj.visibility !== "followers" && obj.visibility !== "direct") {
|
|
555
|
-
return ALLOW_PRIVATE;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// A personal Story is stored visibility="public" but its REACH is the author's
|
|
560
|
-
// followers (addressed to=<actor>/followers; it only surfaces in followers'
|
|
561
|
-
// story feed). The public short-circuit below would make its media blob
|
|
562
|
-
// world-readable to anyone with the URL, so gate it on follower status like a
|
|
563
|
-
// followers-only post. (Community stories were gated by the branch above; the
|
|
564
|
-
// author by the branch above that.)
|
|
565
|
-
if (obj.type === "Story") {
|
|
566
|
-
if (!currentActorApId) return DENY_AUTH_REQUIRED;
|
|
567
|
-
const follow = await db
|
|
568
|
-
.select()
|
|
569
|
-
.from(follows)
|
|
570
|
-
.where(
|
|
571
|
-
and(
|
|
572
|
-
eq(follows.followerApId, currentActorApId),
|
|
573
|
-
eq(follows.followingApId, obj.attributedTo),
|
|
574
|
-
eq(follows.status, "accepted"),
|
|
575
|
-
),
|
|
576
|
-
)
|
|
577
|
-
.get();
|
|
578
|
-
return follow ? ALLOW_PRIVATE : DENY_NOT_AUTHORIZED;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
if (obj.visibility === "public" || obj.visibility === "unlisted") {
|
|
532
|
+
// Only ordinary world-readable objects may use shared/public caching.
|
|
533
|
+
// Community content and Stories remain private-cache responses even when the
|
|
534
|
+
// canonical gate admits the current viewer.
|
|
535
|
+
if (
|
|
536
|
+
!obj.communityApId &&
|
|
537
|
+
obj.type !== "Story" &&
|
|
538
|
+
(obj.visibility === "public" || obj.visibility === "unlisted")
|
|
539
|
+
) {
|
|
582
540
|
return ALLOW_PUBLIC;
|
|
583
541
|
}
|
|
584
|
-
|
|
585
|
-
// Non-public content requires authentication
|
|
586
|
-
if (!currentActorApId) return DENY_AUTH_REQUIRED;
|
|
587
|
-
|
|
588
|
-
if (obj.visibility === "followers") {
|
|
589
|
-
// An explicitly-addressed (to/cc) recipient — e.g. a mention — reads it even
|
|
590
|
-
// without a follow edge, matching the canonical canViewerReadObjectFull gate
|
|
591
|
-
// (this branch previously ignored to/cc and 403'd the blob for a legit
|
|
592
|
-
// recipient the post-detail view showed).
|
|
593
|
-
if (isExplicitRecipient(obj, currentActorApId)) return ALLOW_PRIVATE;
|
|
594
|
-
const follow = await db
|
|
595
|
-
.select()
|
|
596
|
-
.from(follows)
|
|
597
|
-
.where(
|
|
598
|
-
and(
|
|
599
|
-
eq(follows.followerApId, currentActorApId),
|
|
600
|
-
eq(follows.followingApId, obj.attributedTo),
|
|
601
|
-
eq(follows.status, "accepted"),
|
|
602
|
-
),
|
|
603
|
-
)
|
|
604
|
-
.get();
|
|
605
|
-
return follow ? ALLOW_PRIVATE : DENY_NOT_AUTHORIZED;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (obj.visibility === "direct") {
|
|
609
|
-
// Check both to AND cc (the canonical gate does), not toJson alone.
|
|
610
|
-
return isExplicitRecipient(obj, currentActorApId)
|
|
611
|
-
? ALLOW_PRIVATE
|
|
612
|
-
: DENY_NOT_AUTHORIZED;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Unknown visibility - deny by default
|
|
616
|
-
return DENY_NOT_AUTHORIZED;
|
|
542
|
+
return ALLOW_PRIVATE;
|
|
617
543
|
}
|
|
618
544
|
|
|
619
545
|
async function serveMediaByR2Key(c: MediaContext, r2Key: string) {
|