@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
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
} from "../../federation-helpers.ts";
|
|
19
19
|
import { sha256Hex } from "../../lib/delivery/transformers.ts";
|
|
20
20
|
import { getInstanceActor, loadFederatedCommunity } from "./query-helpers.ts";
|
|
21
|
-
import { communityApId
|
|
21
|
+
import { communityApId } from "../../lib/ap-ids.ts";
|
|
22
|
+
import { isTrustedRemoteActivityId } from "../../lib/remote-activity-id.ts";
|
|
23
|
+
import { isSameActivityPubActor } from "../../lib/activitypub-actor-identity.ts";
|
|
22
24
|
import type { Activity } from "./inbox-types.ts";
|
|
23
25
|
import {
|
|
24
26
|
getActivityObject,
|
|
@@ -39,10 +41,11 @@ import {
|
|
|
39
41
|
import {
|
|
40
42
|
fetchAndUpsertActorCache,
|
|
41
43
|
getInstanceFetchSignerByDb,
|
|
44
|
+
isRemoteActorTombstoned,
|
|
42
45
|
} from "../../lib/activitypub-actor-cache.ts";
|
|
43
46
|
import { logger } from "../../lib/logger.ts";
|
|
44
47
|
import { verifyHttpSignature } from "../../lib/ap-verify.ts";
|
|
45
|
-
import {
|
|
48
|
+
import { isActorBlockedStrict } from "../../lib/blocklist.ts";
|
|
46
49
|
import {
|
|
47
50
|
consumeRateLimitProgrammatic,
|
|
48
51
|
RateLimitConfigs,
|
|
@@ -68,13 +71,13 @@ import {
|
|
|
68
71
|
handleUndo,
|
|
69
72
|
handleUpdate,
|
|
70
73
|
} from "./handlers/user-inbox-handlers.ts";
|
|
74
|
+
import { internalInboundActivityId } from "./inbound-activity-identity.ts";
|
|
71
75
|
|
|
72
76
|
const log = logger.child({ component: "activitypub.inbox" });
|
|
73
77
|
|
|
74
78
|
type HonoContext = Context<{ Bindings: Env; Variables: Variables }>;
|
|
75
79
|
|
|
76
80
|
const MAX_PAYLOAD_BYTES = 512 * 1024;
|
|
77
|
-
const MAX_REMOTE_ACTIVITY_ID_LENGTH = 2_048;
|
|
78
81
|
const INBOUND_DISPATCH_LEASE_MS = 2 * 60 * 1000;
|
|
79
82
|
const TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
80
83
|
|
|
@@ -132,26 +135,6 @@ export function signingActorFromKeyId(
|
|
|
132
135
|
return keyId.includes("#") ? keyId.split("#")[0] : keyId;
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
/**
|
|
136
|
-
* Normalize an actor URL for identity comparison: lowercase the host (host names
|
|
137
|
-
* are case-insensitive) and drop a single trailing slash + any fragment, leaving
|
|
138
|
-
* the (case-sensitive) path intact. Used to compare the signing-key owner with
|
|
139
|
-
* the activity actor without rejecting cosmetically-different-but-identical IRIs
|
|
140
|
-
* (trailing slash / host case) that conformant peers occasionally emit. Returns
|
|
141
|
-
* null for an unparseable URL.
|
|
142
|
-
*/
|
|
143
|
-
function normalizeActorUrl(url: string): string | null {
|
|
144
|
-
try {
|
|
145
|
-
const u = new URL(url);
|
|
146
|
-
u.hash = "";
|
|
147
|
-
let normalized = `${u.protocol}//${u.host}${u.pathname}${u.search}`;
|
|
148
|
-
if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
149
|
-
return normalized;
|
|
150
|
-
} catch {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
138
|
/**
|
|
156
139
|
* Returns true when the HTTP-signature signing key does NOT belong to exactly
|
|
157
140
|
* the activity actor (after URL normalization).
|
|
@@ -181,18 +164,7 @@ export function isActorMismatch(
|
|
|
181
164
|
actor: string,
|
|
182
165
|
): boolean {
|
|
183
166
|
if (!signingActorUrl) return true;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const normalizedSigner = normalizeActorUrl(signingActorUrl);
|
|
187
|
-
const normalizedActor = normalizeActorUrl(actor);
|
|
188
|
-
if (
|
|
189
|
-
normalizedSigner !== null &&
|
|
190
|
-
normalizedActor !== null &&
|
|
191
|
-
normalizedSigner === normalizedActor
|
|
192
|
-
) {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
return true;
|
|
167
|
+
return !isSameActivityPubActor(signingActorUrl, actor);
|
|
196
168
|
}
|
|
197
169
|
|
|
198
170
|
type ParsedActivity = {
|
|
@@ -208,26 +180,6 @@ type ParsedActivity = {
|
|
|
208
180
|
activityObjectId: string | null;
|
|
209
181
|
};
|
|
210
182
|
|
|
211
|
-
function isBoundedProtocolActivityId(value: string): boolean {
|
|
212
|
-
return (
|
|
213
|
-
value.length > 0 &&
|
|
214
|
-
value.length <= MAX_REMOTE_ACTIVITY_ID_LENGTH &&
|
|
215
|
-
!/[\u0000-\u001f\u007f]/u.test(value)
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function internalInboundActivityId(
|
|
220
|
-
baseUrl: string,
|
|
221
|
-
actor: string,
|
|
222
|
-
source: string,
|
|
223
|
-
): Promise<string> {
|
|
224
|
-
const actorIdentity = normalizeActorUrl(actor) ?? actor;
|
|
225
|
-
return activityApId(
|
|
226
|
-
baseUrl,
|
|
227
|
-
`inbound-${await sha256Hex(`${actorIdentity}\0${source}`)}`,
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
183
|
/**
|
|
232
184
|
* Shared pipeline for both inbox endpoints: size check, signature verification,
|
|
233
185
|
* JSON parse, field extraction, and actor-mismatch check. Returns either a
|
|
@@ -285,42 +237,60 @@ async function verifyAndParseInbox(
|
|
|
285
237
|
return c.json({ error: "Invalid JSON" }, 400);
|
|
286
238
|
}
|
|
287
239
|
|
|
288
|
-
const
|
|
240
|
+
const claimedActor =
|
|
241
|
+
typeof activity.actor === "string" ? activity.actor : null;
|
|
289
242
|
const activityType = typeof activity.type === "string" ? activity.type : null;
|
|
290
243
|
|
|
291
|
-
if (!
|
|
244
|
+
if (!claimedActor || !activityType) {
|
|
292
245
|
return c.json({ error: "Invalid activity" }, 400);
|
|
293
246
|
}
|
|
294
247
|
|
|
248
|
+
const signingActor = signingActorFromKeyId(signatureResult.keyId);
|
|
249
|
+
if (isActorMismatch(signingActor, claimedActor)) {
|
|
250
|
+
log.warn("Actor mismatch between activity and signing key", {
|
|
251
|
+
event: "ap.signature.actor_mismatch",
|
|
252
|
+
actor: claimedActor,
|
|
253
|
+
signingActor,
|
|
254
|
+
keyId: signatureResult.keyId,
|
|
255
|
+
});
|
|
256
|
+
return c.json({ error: "Actor mismatch" }, 401);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// From this point onward the key owner is the authoritative actor spelling.
|
|
260
|
+
// The actor document fetched during signature verification asserts this exact
|
|
261
|
+
// URL as its id, whereas the envelope may carry an accepted cosmetic variant.
|
|
262
|
+
// Keeping the original body in rawActivityJson preserves protocol evidence;
|
|
263
|
+
// normalizing the dispatched Activity prevents one verified identity from
|
|
264
|
+
// creating separate relationship keys merely by toggling host case or `/`.
|
|
265
|
+
const actor = signingActor!;
|
|
266
|
+
activity.actor = actor;
|
|
267
|
+
|
|
295
268
|
// A peer controls Activity.id. Never use that unbounded string as our primary
|
|
296
269
|
// key, queue key, or structured-log identifier. First validate the protocol
|
|
297
270
|
// id against the signature-bound actor origin, then derive a fixed-size local
|
|
298
271
|
// id from actor + source. The original envelope remains in rawJson (bounded by
|
|
299
272
|
// MAX_PAYLOAD_BYTES) for protocol evidence.
|
|
300
273
|
const rawActivityId = typeof activity.id === "string" ? activity.id : null;
|
|
301
|
-
|
|
302
|
-
if (
|
|
274
|
+
const trustedActivityId =
|
|
303
275
|
rawActivityId !== null &&
|
|
304
|
-
|
|
305
|
-
!isLocal(rawActivityId, baseUrl)
|
|
306
|
-
) {
|
|
307
|
-
try {
|
|
308
|
-
activityIdTrusted = getDomain(rawActivityId) === getDomain(actor);
|
|
309
|
-
} catch {
|
|
310
|
-
activityIdTrusted = false;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
const sourceActivityId =
|
|
314
|
-
rawActivityId !== null && activityIdTrusted
|
|
276
|
+
isTrustedRemoteActivityId(rawActivityId, actor, baseUrl)
|
|
315
277
|
? rawActivityId
|
|
278
|
+
: null;
|
|
279
|
+
const identitySource =
|
|
280
|
+
trustedActivityId !== null
|
|
281
|
+
? trustedActivityId
|
|
316
282
|
: `synthetic:${await sha256Hex(
|
|
317
283
|
`${actor}\0${activityType}\0${getActivityObjectId(activity) ?? ""}\0${body}`,
|
|
318
284
|
)}`;
|
|
319
285
|
const activityId = await internalInboundActivityId(
|
|
320
286
|
baseUrl,
|
|
321
287
|
actor,
|
|
322
|
-
|
|
288
|
+
identitySource,
|
|
323
289
|
);
|
|
290
|
+
// Only a safe, actor-owned wire id may cross back out in an Accept/Reject.
|
|
291
|
+
// When the peer omitted or supplied an unsafe id, use our stable retained
|
|
292
|
+
// ledger IRI instead of leaking an invalid `synthetic:<hash>` pseudo-IRI.
|
|
293
|
+
const sourceActivityId = trustedActivityId ?? activityId;
|
|
324
294
|
|
|
325
295
|
// Handlers persist activity ids on interaction/follow edges. Stamp the
|
|
326
296
|
// canonical internal id before dispatch so no remote string escapes into
|
|
@@ -328,28 +298,33 @@ async function verifyAndParseInbox(
|
|
|
328
298
|
// source-id lookup in the Undo helpers and therefore still resolve.
|
|
329
299
|
activity.id = activityId;
|
|
330
300
|
|
|
331
|
-
const signingActor = signingActorFromKeyId(signatureResult.keyId);
|
|
332
|
-
if (isActorMismatch(signingActor, actor)) {
|
|
333
|
-
log.warn("Actor mismatch between activity and signing key", {
|
|
334
|
-
event: "ap.signature.actor_mismatch",
|
|
335
|
-
actor,
|
|
336
|
-
signingActor,
|
|
337
|
-
keyId: signatureResult.keyId,
|
|
338
|
-
});
|
|
339
|
-
return c.json({ error: "Actor mismatch" }, 401);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
301
|
// Central federation blocklist gate. Applied once here so every activity
|
|
343
302
|
// type (Follow / Like / Announce / Undo / content / group inbox / ...) is
|
|
344
303
|
// covered regardless of which handler dispatches it. Blocked traffic is
|
|
345
304
|
// silently discarded with a 202 ACK (never 4xx) — a 4xx would make the
|
|
346
305
|
// sending instance retry on a backoff and keep redelivering blocked
|
|
347
|
-
// traffic.
|
|
348
|
-
//
|
|
306
|
+
// traffic. A blocklist read failure propagates as 5xx so the peer retries;
|
|
307
|
+
// acknowledging while moderation authority is unavailable could admit a
|
|
308
|
+
// sender the operator has blocked.
|
|
349
309
|
if (await isActivityBlocked(c, actor, activityType)) {
|
|
350
310
|
return c.body(null, 202);
|
|
351
311
|
}
|
|
352
312
|
|
|
313
|
+
// A verified self-Delete is durable identity authority. Signature
|
|
314
|
+
// verification still runs first (so an attacker cannot use an unverified
|
|
315
|
+
// actor spelling as an oracle), then every inbox shape silently discards
|
|
316
|
+
// later traffic before dedup allocation or handler effects. As with an
|
|
317
|
+
// operator block, 202 prevents a peer from retrying traffic that can never
|
|
318
|
+
// become admissible under this actor id.
|
|
319
|
+
if (await isRemoteActorTombstoned(c.get("db"), actor)) {
|
|
320
|
+
log.info("Discarding activity from tombstoned actor", {
|
|
321
|
+
event: "ap.actor_tombstone.discard",
|
|
322
|
+
actor,
|
|
323
|
+
activityType,
|
|
324
|
+
});
|
|
325
|
+
return c.body(null, 202);
|
|
326
|
+
}
|
|
327
|
+
|
|
353
328
|
return {
|
|
354
329
|
activity,
|
|
355
330
|
activityId,
|
|
@@ -374,32 +349,14 @@ async function isActivityBlocked(
|
|
|
374
349
|
): Promise<boolean> {
|
|
375
350
|
const db = c.get("db");
|
|
376
351
|
|
|
377
|
-
if (await
|
|
378
|
-
log.info("Discarding activity from blocked actor", {
|
|
352
|
+
if (await isActorBlockedStrict(db, actor)) {
|
|
353
|
+
log.info("Discarding activity from blocked actor or domain", {
|
|
379
354
|
event: "ap.blocklist.actor_discard",
|
|
380
355
|
actor,
|
|
381
356
|
activityType,
|
|
382
357
|
});
|
|
383
358
|
return true;
|
|
384
359
|
}
|
|
385
|
-
|
|
386
|
-
let hostname: string | null = null;
|
|
387
|
-
try {
|
|
388
|
-
hostname = new URL(actor).hostname;
|
|
389
|
-
} catch {
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (await isDomainBlocked(db, hostname)) {
|
|
394
|
-
log.info("Discarding activity from blocked domain", {
|
|
395
|
-
event: "ap.blocklist.domain_discard",
|
|
396
|
-
actor,
|
|
397
|
-
domain: hostname,
|
|
398
|
-
activityType,
|
|
399
|
-
});
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
360
|
return false;
|
|
404
361
|
}
|
|
405
362
|
|
|
@@ -1185,7 +1142,15 @@ async function resolveObjectActorTarget(
|
|
|
1185
1142
|
}
|
|
1186
1143
|
const innerId = getActivityObjectId(activity);
|
|
1187
1144
|
if (innerId) {
|
|
1188
|
-
const
|
|
1145
|
+
const sourceActor =
|
|
1146
|
+
typeof activity.actor === "string" ? activity.actor : null;
|
|
1147
|
+
const follow = await findFollowByActivityId(
|
|
1148
|
+
c.get("db"),
|
|
1149
|
+
innerId,
|
|
1150
|
+
sourceActor
|
|
1151
|
+
? { actorApId: sourceActor, localBaseUrl: baseUrl }
|
|
1152
|
+
: undefined,
|
|
1153
|
+
);
|
|
1189
1154
|
if (follow && isLocal(follow.followingApId, baseUrl)) {
|
|
1190
1155
|
return {
|
|
1191
1156
|
scoped: true,
|
|
@@ -1318,6 +1283,17 @@ ap.post("/ap/inbox", async (c) => {
|
|
|
1318
1283
|
MAX_SHARED_INBOX_FANOUT,
|
|
1319
1284
|
);
|
|
1320
1285
|
|
|
1286
|
+
if (resolution.invalidReason) {
|
|
1287
|
+
log.warn("Shared-inbox activity has invalid addressing", {
|
|
1288
|
+
event: "ap.shared_inbox.addressing_invalid",
|
|
1289
|
+
activityType,
|
|
1290
|
+
actor,
|
|
1291
|
+
reason: resolution.invalidReason,
|
|
1292
|
+
});
|
|
1293
|
+
await commitActivityDispatch(c, claim, PROCESSED_UNDELIVERABLE);
|
|
1294
|
+
return c.json({ error: "Invalid or oversized addressing" }, 422);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1321
1297
|
if (resolution.recipients.length === 0) {
|
|
1322
1298
|
if (resolution.cls === "audience") {
|
|
1323
1299
|
// The activity addressed a COLLECTION (Public / followers) and nobody
|
|
Binary file
|
|
@@ -440,13 +440,14 @@ ap.get(
|
|
|
440
440
|
following: showCollections ? actor.followingUrl : undefined,
|
|
441
441
|
// Advertise sharedInbox so remote servers can deduplicate fan-out
|
|
442
442
|
// delivery (Mastodon convention). The endpoint accepts signed
|
|
443
|
-
// activities just like the per-actor inbox. `rtcSignal`
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
// back to deriving it from the inbox origin).
|
|
443
|
+
// activities just like the per-actor inbox. `rtcSignal` is a Yurucommu
|
|
444
|
+
// extension and must be advertised only when this exact deployment has a
|
|
445
|
+
// signaling namespace; otherwise peers are sent to a permanent 503.
|
|
447
446
|
endpoints: {
|
|
448
447
|
sharedInbox: `${baseUrl}/ap/inbox`,
|
|
449
|
-
|
|
448
|
+
...(c.env.CALL_SIGNALING
|
|
449
|
+
? { rtcSignal: `${baseUrl}/ap/rtc/signal` }
|
|
450
|
+
: {}),
|
|
450
451
|
},
|
|
451
452
|
publicKey: buildPublicKey(actor.apId, actor.publicKeyPem),
|
|
452
453
|
discoverable: !actor.isPrivate,
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
parseLimit,
|
|
21
21
|
parseOffset,
|
|
22
22
|
} from "../federation-helpers.ts";
|
|
23
|
+
import { canonicalPersonalModerationActorId } from "../lib/personal-actor-moderation.ts";
|
|
24
|
+
import { operatorActorNotBlockedSql } from "../lib/blocklist.ts";
|
|
23
25
|
|
|
24
26
|
// Hono context with our app's bindings and variables
|
|
25
27
|
export type AppContext = Context<
|
|
@@ -153,7 +155,7 @@ export function formatActorSummary(
|
|
|
153
155
|
} {
|
|
154
156
|
return {
|
|
155
157
|
ap_id: apId,
|
|
156
|
-
username: formatUsername(apId),
|
|
158
|
+
username: formatUsername(apId, info?.preferredUsername),
|
|
157
159
|
preferred_username: info?.preferredUsername || null,
|
|
158
160
|
name: info?.name || null,
|
|
159
161
|
icon_url: info?.iconUrl || null,
|
|
@@ -353,6 +355,11 @@ export async function createRelation(
|
|
|
353
355
|
targetApId: string,
|
|
354
356
|
) => Promise<unknown>,
|
|
355
357
|
countExisting: (db: Database, actorApIdVal: string) => Promise<number>,
|
|
358
|
+
relationExists: (
|
|
359
|
+
db: Database,
|
|
360
|
+
actorApIdVal: string,
|
|
361
|
+
targetApId: string,
|
|
362
|
+
) => Promise<boolean>,
|
|
356
363
|
): Promise<Response> {
|
|
357
364
|
const result = requireActor(c);
|
|
358
365
|
if (result instanceof Response) return result;
|
|
@@ -360,17 +367,27 @@ export async function createRelation(
|
|
|
360
367
|
|
|
361
368
|
const body = await c.req.json<{ ap_id: string }>();
|
|
362
369
|
if (!body.ap_id) return c.json({ error: "ap_id required" }, 400);
|
|
363
|
-
|
|
370
|
+
const canonicalTargetApId = canonicalPersonalModerationActorId(body.ap_id);
|
|
371
|
+
if (canonicalTargetApId === canonicalPersonalModerationActorId(actor.ap_id)) {
|
|
364
372
|
return c.json({ error: `Cannot ${verb} yourself` }, 400);
|
|
365
373
|
}
|
|
366
374
|
|
|
367
375
|
const db = c.get("db");
|
|
368
376
|
// Bound the per-actor relation set (soft cap; a small concurrent overshoot is
|
|
369
|
-
// harmless).
|
|
370
|
-
//
|
|
371
|
-
|
|
377
|
+
// harmless). Existing identities remain idempotent at the cap, including a
|
|
378
|
+
// legacy raw spelling that is cosmetically equivalent to the presented ID.
|
|
379
|
+
// Check identity only on the full-cap path so ordinary creates retain the
|
|
380
|
+
// existing one-count + one-upsert query shape.
|
|
381
|
+
if (
|
|
382
|
+
(await countExisting(db, actor.ap_id)) >= MAX_RELATIONS_PER_ACTOR &&
|
|
383
|
+
!(await relationExists(db, actor.ap_id, body.ap_id))
|
|
384
|
+
) {
|
|
372
385
|
return c.json({ error: `${verb} limit reached` }, 429);
|
|
373
386
|
}
|
|
387
|
+
// Retain the exact actor spelling the user acted on. Feed queries expand
|
|
388
|
+
// this one row to raw + canonical identity, so historical content under the
|
|
389
|
+
// presented spelling and future canonical content are both suppressed
|
|
390
|
+
// without storing duplicate moderation rows or consuming the relation quota.
|
|
374
391
|
await upsert(db, actor.ap_id, body.ap_id);
|
|
375
392
|
|
|
376
393
|
return c.json({ success: true });
|
|
@@ -394,9 +411,10 @@ export async function deleteRelation(
|
|
|
394
411
|
|
|
395
412
|
const body = await c.req.json<{ ap_id: string }>();
|
|
396
413
|
if (!body.ap_id) return c.json({ error: "ap_id required" }, 400);
|
|
414
|
+
const targetApId = canonicalPersonalModerationActorId(body.ap_id);
|
|
397
415
|
|
|
398
416
|
const db = c.get("db");
|
|
399
|
-
await remove(db, actor.ap_id,
|
|
417
|
+
await remove(db, actor.ap_id, targetApId);
|
|
400
418
|
|
|
401
419
|
return c.json({ success: true });
|
|
402
420
|
}
|
|
@@ -419,8 +437,16 @@ export async function listFollowRelation(
|
|
|
419
437
|
|
|
420
438
|
const isFollowers = direction === "followers";
|
|
421
439
|
const whereCondition = isFollowers
|
|
422
|
-
? and(
|
|
423
|
-
|
|
440
|
+
? and(
|
|
441
|
+
eq(follows.followingApId, apId),
|
|
442
|
+
eq(follows.status, "accepted"),
|
|
443
|
+
operatorActorNotBlockedSql(sql`${follows.followerApId}`),
|
|
444
|
+
)
|
|
445
|
+
: and(
|
|
446
|
+
eq(follows.followerApId, apId),
|
|
447
|
+
eq(follows.status, "accepted"),
|
|
448
|
+
operatorActorNotBlockedSql(sql`${follows.followingApId}`),
|
|
449
|
+
);
|
|
424
450
|
|
|
425
451
|
// A private (locked) account's follower/following LIST is owner-only — mirror
|
|
426
452
|
// the ActivityPub collection gate (canViewPrivateActorCollections), which this
|