@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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { sql, type SQL } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
function expandedActorIdentitiesSql(
|
|
4
|
+
rawActorIds: SQL,
|
|
5
|
+
targetActorId: SQL | undefined,
|
|
6
|
+
finalSelect: SQL,
|
|
7
|
+
): SQL {
|
|
8
|
+
const targetValue = targetActorId ?? sql`NULL`;
|
|
9
|
+
return sql`
|
|
10
|
+
WITH
|
|
11
|
+
actor_identity_candidates(actor_id) AS MATERIALIZED (
|
|
12
|
+
${rawActorIds}
|
|
13
|
+
),
|
|
14
|
+
actor_identity_target(actor_id) AS MATERIALIZED (
|
|
15
|
+
SELECT ${targetValue}
|
|
16
|
+
),
|
|
17
|
+
actor_identity_raw(source, raw_actor_id, actor_id) AS MATERIALIZED (
|
|
18
|
+
SELECT 0, actor_id, actor_id
|
|
19
|
+
FROM actor_identity_candidates
|
|
20
|
+
UNION ALL
|
|
21
|
+
SELECT 1, actor_id, actor_id
|
|
22
|
+
FROM actor_identity_target
|
|
23
|
+
WHERE actor_id IS NOT NULL
|
|
24
|
+
),
|
|
25
|
+
actor_identity_fragmentless(source, raw_actor_id, actor_id) AS MATERIALIZED (
|
|
26
|
+
SELECT
|
|
27
|
+
source,
|
|
28
|
+
raw_actor_id,
|
|
29
|
+
CASE WHEN instr(actor_id, '#') > 0
|
|
30
|
+
THEN substr(actor_id, 1, instr(actor_id, '#') - 1)
|
|
31
|
+
ELSE actor_id
|
|
32
|
+
END
|
|
33
|
+
FROM actor_identity_raw
|
|
34
|
+
),
|
|
35
|
+
actor_identity_clean(source, raw_actor_id, actor_id) AS MATERIALIZED (
|
|
36
|
+
SELECT
|
|
37
|
+
source,
|
|
38
|
+
raw_actor_id,
|
|
39
|
+
CASE WHEN substr(actor_id, -1) = '/'
|
|
40
|
+
THEN substr(actor_id, 1, length(actor_id) - 1)
|
|
41
|
+
ELSE actor_id
|
|
42
|
+
END
|
|
43
|
+
FROM actor_identity_fragmentless
|
|
44
|
+
),
|
|
45
|
+
actor_identity_split(
|
|
46
|
+
source, raw_actor_id, actor_id, separator, tail
|
|
47
|
+
) AS MATERIALIZED (
|
|
48
|
+
SELECT
|
|
49
|
+
source,
|
|
50
|
+
raw_actor_id,
|
|
51
|
+
actor_id,
|
|
52
|
+
instr(actor_id, '://'),
|
|
53
|
+
substr(actor_id, instr(actor_id, '://') + 3)
|
|
54
|
+
FROM actor_identity_clean
|
|
55
|
+
),
|
|
56
|
+
actor_identity_bounds(
|
|
57
|
+
source, raw_actor_id, actor_id, separator, tail, boundary
|
|
58
|
+
) AS MATERIALIZED (
|
|
59
|
+
SELECT
|
|
60
|
+
source,
|
|
61
|
+
raw_actor_id,
|
|
62
|
+
actor_id,
|
|
63
|
+
separator,
|
|
64
|
+
tail,
|
|
65
|
+
CASE
|
|
66
|
+
WHEN instr(tail, '/') = 0 THEN instr(tail, '?')
|
|
67
|
+
WHEN instr(tail, '?') = 0 THEN instr(tail, '/')
|
|
68
|
+
WHEN instr(tail, '/') < instr(tail, '?') THEN instr(tail, '/')
|
|
69
|
+
ELSE instr(tail, '?')
|
|
70
|
+
END
|
|
71
|
+
FROM actor_identity_split
|
|
72
|
+
),
|
|
73
|
+
actor_identity_components(
|
|
74
|
+
source, raw_actor_id, separator, scheme, authority, path_query
|
|
75
|
+
) AS MATERIALIZED (
|
|
76
|
+
SELECT
|
|
77
|
+
source,
|
|
78
|
+
raw_actor_id,
|
|
79
|
+
separator,
|
|
80
|
+
lower(substr(actor_id, 1, separator - 1)),
|
|
81
|
+
CASE WHEN boundary = 0
|
|
82
|
+
THEN tail
|
|
83
|
+
ELSE substr(tail, 1, boundary - 1)
|
|
84
|
+
END,
|
|
85
|
+
CASE
|
|
86
|
+
WHEN boundary = 0 THEN ''
|
|
87
|
+
WHEN substr(tail, boundary, 1) = '?'
|
|
88
|
+
THEN '/' || substr(tail, boundary)
|
|
89
|
+
ELSE substr(tail, boundary)
|
|
90
|
+
END
|
|
91
|
+
FROM actor_identity_bounds
|
|
92
|
+
),
|
|
93
|
+
actor_identity_normalized(
|
|
94
|
+
source, raw_actor_id, separator, scheme, authority, path_query
|
|
95
|
+
) AS MATERIALIZED (
|
|
96
|
+
SELECT
|
|
97
|
+
source,
|
|
98
|
+
raw_actor_id,
|
|
99
|
+
separator,
|
|
100
|
+
scheme,
|
|
101
|
+
CASE
|
|
102
|
+
WHEN scheme = 'https' AND substr(lower(authority), -4) = ':443'
|
|
103
|
+
THEN substr(lower(authority), 1, length(authority) - 4)
|
|
104
|
+
WHEN scheme = 'http' AND substr(lower(authority), -3) = ':80'
|
|
105
|
+
THEN substr(lower(authority), 1, length(authority) - 3)
|
|
106
|
+
ELSE lower(authority)
|
|
107
|
+
END,
|
|
108
|
+
path_query
|
|
109
|
+
FROM actor_identity_components
|
|
110
|
+
WHERE instr(authority, '@') = 0
|
|
111
|
+
),
|
|
112
|
+
actor_identity_values(source, raw_actor_id, actor_id) AS MATERIALIZED (
|
|
113
|
+
SELECT source, raw_actor_id, actor_id
|
|
114
|
+
FROM actor_identity_raw
|
|
115
|
+
UNION
|
|
116
|
+
SELECT
|
|
117
|
+
source,
|
|
118
|
+
raw_actor_id,
|
|
119
|
+
scheme || '://' || authority || path_query
|
|
120
|
+
FROM actor_identity_normalized
|
|
121
|
+
WHERE separator > 1
|
|
122
|
+
AND scheme IN ('http', 'https')
|
|
123
|
+
AND length(authority) > 0
|
|
124
|
+
)
|
|
125
|
+
${finalSelect}
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Expand an uncorrelated one-column actor-ID query into its raw spellings plus
|
|
131
|
+
* the canonical HTTP(S) spellings accepted by the signature boundary.
|
|
132
|
+
*
|
|
133
|
+
* Feed callers compare their actor column to this set with `NOT IN`. The CTE is
|
|
134
|
+
* independent of the outer feed row, so SQLite computes the small personal
|
|
135
|
+
* moderation set once instead of reparsing actor URLs for every post or
|
|
136
|
+
* notification. Raw IDs stay in the result for malformed legacy values and
|
|
137
|
+
* content retained under the exact historical spelling. The canonical branch
|
|
138
|
+
* preserves path/query bytes while folding only scheme/authority case,
|
|
139
|
+
* default ports, fragments, and one trailing slash.
|
|
140
|
+
*
|
|
141
|
+
* Keep the parsing stages MATERIALIZED. Letting SQLite flatten this expression
|
|
142
|
+
* chain causes workerd's D1 engine to exhaust memory even for a tiny relation
|
|
143
|
+
* set; explicit materialization also keeps runtime bounded as the set grows.
|
|
144
|
+
*/
|
|
145
|
+
export function activityPubActorIdentitySetSql(rawActorIds: SQL): SQL {
|
|
146
|
+
return expandedActorIdentitiesSql(
|
|
147
|
+
rawActorIds,
|
|
148
|
+
undefined,
|
|
149
|
+
sql`
|
|
150
|
+
SELECT DISTINCT actor_id
|
|
151
|
+
FROM actor_identity_values
|
|
152
|
+
WHERE source = 0
|
|
153
|
+
`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Return the retained raw spellings from `rawActorIds` that identify
|
|
159
|
+
* `targetActorId`. This is the mutation counterpart to the identity-set read
|
|
160
|
+
* predicate: callers can retain an existing primary-key spelling or delete all
|
|
161
|
+
* historical spellings without loading/scanning the whole relation set in JS.
|
|
162
|
+
*/
|
|
163
|
+
export function activityPubActorIdentityMatchesSql(
|
|
164
|
+
rawActorIds: SQL,
|
|
165
|
+
targetActorId: string | SQL,
|
|
166
|
+
): SQL {
|
|
167
|
+
return expandedActorIdentitiesSql(
|
|
168
|
+
rawActorIds,
|
|
169
|
+
typeof targetActorId === "string" ? sql`${targetActorId}` : targetActorId,
|
|
170
|
+
sql`
|
|
171
|
+
SELECT DISTINCT candidate.raw_actor_id AS actor_id
|
|
172
|
+
FROM actor_identity_values AS candidate
|
|
173
|
+
INNER JOIN actor_identity_values AS target
|
|
174
|
+
ON target.source = 1
|
|
175
|
+
AND target.actor_id = candidate.actor_id
|
|
176
|
+
WHERE candidate.source = 0
|
|
177
|
+
`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize the deliberately-small set of cosmetic actor-IRI differences that
|
|
3
|
+
* the HTTP-signature boundary accepts.
|
|
4
|
+
*
|
|
5
|
+
* URL parsing lowercases the scheme/host and removes default ports. Fragments
|
|
6
|
+
* never identify an ActivityPub actor, and a single trailing slash is treated
|
|
7
|
+
* as cosmetic. Path case, query strings, and every other component remain
|
|
8
|
+
* identity-significant so sibling actors on one origin are never conflated.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeActivityPubActorId(value: string): string | null {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(value);
|
|
13
|
+
url.hash = "";
|
|
14
|
+
let normalized = `${url.protocol}//${url.host}${url.pathname}${url.search}`;
|
|
15
|
+
if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
16
|
+
return normalized;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compare two actor IRIs using exactly the equivalence accepted when binding
|
|
24
|
+
* an Activity actor to its verified HTTP-signature key owner.
|
|
25
|
+
*/
|
|
26
|
+
export function isSameActivityPubActor(
|
|
27
|
+
left: string | null | undefined,
|
|
28
|
+
right: string | null | undefined,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (!left || !right) return false;
|
|
31
|
+
if (left === right) return true;
|
|
32
|
+
const normalizedLeft = normalizeActivityPubActorId(left);
|
|
33
|
+
const normalizedRight = normalizeActivityPubActorId(right);
|
|
34
|
+
return (
|
|
35
|
+
normalizedLeft !== null &&
|
|
36
|
+
normalizedRight !== null &&
|
|
37
|
+
normalizedLeft === normalizedRight
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
* - ActivityPub is lenient about extension fields; parsers accept unknown
|
|
12
12
|
* extra fields and copy through only the keys we care about.
|
|
13
13
|
* - Required-but-wrong-type fields fail closed.
|
|
14
|
-
* - Optional fields that are present but malformed are dropped
|
|
15
|
-
* (treated as absent) rather than rejecting the whole document.
|
|
14
|
+
* - Optional display fields that are present but malformed are dropped
|
|
15
|
+
* silently (treated as absent) rather than rejecting the whole document.
|
|
16
|
+
* - Optional authority fields whose presence changes behavior may be preserved
|
|
17
|
+
* as `unknown` so their owning handler can reject them instead of confusing a
|
|
18
|
+
* malformed value with omission.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
/** Error thrown when a remote ActivityPub document violates our contract. */
|
|
@@ -204,9 +207,13 @@ export interface ActivityObjectDocument {
|
|
|
204
207
|
id?: string;
|
|
205
208
|
type?: string | string[];
|
|
206
209
|
object?: string;
|
|
207
|
-
|
|
210
|
+
/** Preserved raw so malformed explicit reply edges cannot become roots. */
|
|
211
|
+
inReplyTo?: unknown;
|
|
208
212
|
to?: string[];
|
|
209
213
|
cc?: string[];
|
|
214
|
+
bto?: string[];
|
|
215
|
+
bcc?: string[];
|
|
216
|
+
audience?: string[];
|
|
210
217
|
conversation?: string;
|
|
211
218
|
content?: string;
|
|
212
219
|
summary?: string | null;
|
|
@@ -228,8 +235,17 @@ export interface ActivityDocument {
|
|
|
228
235
|
type?: string;
|
|
229
236
|
actor?: string;
|
|
230
237
|
object?: string | ActivityObjectDocument;
|
|
238
|
+
/** Bounded IRI projection of scalar/embedded/array-valued AS2 `object`. */
|
|
239
|
+
objectIds?: string[];
|
|
231
240
|
target?: string | ActivityObjectDocument;
|
|
241
|
+
/** Envelope-level Flag reason. */
|
|
242
|
+
content?: string;
|
|
232
243
|
room?: string;
|
|
244
|
+
to?: string[];
|
|
245
|
+
cc?: string[];
|
|
246
|
+
bto?: string[];
|
|
247
|
+
bcc?: string[];
|
|
248
|
+
audience?: string[];
|
|
233
249
|
}
|
|
234
250
|
|
|
235
251
|
function parseActivityObjectFields(
|
|
@@ -240,9 +256,15 @@ function parseActivityObjectFields(
|
|
|
240
256
|
id: getString(record, "id"),
|
|
241
257
|
type: getStringOrStringArray(record, "type"),
|
|
242
258
|
object: getString(record, "object"),
|
|
243
|
-
|
|
259
|
+
// This is an authority-bearing thread edge. Preserve a malformed explicit
|
|
260
|
+
// value for the Note ingress validator instead of silently converting it
|
|
261
|
+
// to absence (which would persist a root post or allow a partial Update).
|
|
262
|
+
inReplyTo: record["inReplyTo"],
|
|
244
263
|
to: getStringArray(record, "to"),
|
|
245
264
|
cc: getStringArray(record, "cc"),
|
|
265
|
+
bto: getStringArray(record, "bto"),
|
|
266
|
+
bcc: getStringArray(record, "bcc"),
|
|
267
|
+
audience: getStringArray(record, "audience"),
|
|
246
268
|
conversation: getString(record, "conversation"),
|
|
247
269
|
content: getString(record, "content"),
|
|
248
270
|
summary:
|
|
@@ -269,6 +291,35 @@ function parseActivityObjectOrIri(
|
|
|
269
291
|
return undefined;
|
|
270
292
|
}
|
|
271
293
|
|
|
294
|
+
// Below D1's 100-bound-parameter ceiling when a handler resolves all reported
|
|
295
|
+
// entities in one IN (...) query. The request body itself is bounded separately.
|
|
296
|
+
export const MAX_ACTIVITY_OBJECT_IDS = 64;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Normalize every usable `object` reference without widening the existing
|
|
300
|
+
* scalar/embedded `Activity.object` model. In particular, Mastodon-compatible
|
|
301
|
+
* Flag activities carry `[reportedPost, reportedActor]`; the old parser dropped
|
|
302
|
+
* that array completely, leaving an unactionable target-less report.
|
|
303
|
+
*/
|
|
304
|
+
function parseActivityObjectIds(value: unknown): string[] | undefined {
|
|
305
|
+
const values = Array.isArray(value) ? value : [value];
|
|
306
|
+
const ids: string[] = [];
|
|
307
|
+
const seen = new Set<string>();
|
|
308
|
+
for (const item of values) {
|
|
309
|
+
const id =
|
|
310
|
+
typeof item === "string"
|
|
311
|
+
? item
|
|
312
|
+
: isJsonRecord(item)
|
|
313
|
+
? getString(item, "id")
|
|
314
|
+
: undefined;
|
|
315
|
+
if (!id || seen.has(id)) continue;
|
|
316
|
+
seen.add(id);
|
|
317
|
+
ids.push(id);
|
|
318
|
+
if (ids.length >= MAX_ACTIVITY_OBJECT_IDS) break;
|
|
319
|
+
}
|
|
320
|
+
return ids.length > 0 ? ids : undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
272
323
|
/**
|
|
273
324
|
* Narrow an `unknown` (an already-`JSON.parse`d inbox body) into an
|
|
274
325
|
* {@link ActivityDocument}. Throws if the value is not a JSON object.
|
|
@@ -283,8 +334,15 @@ export function parseActivity(value: unknown, path = "$"): ActivityDocument {
|
|
|
283
334
|
type: getString(record, "type"),
|
|
284
335
|
actor: getString(record, "actor"),
|
|
285
336
|
object: parseActivityObjectOrIri(record["object"]),
|
|
337
|
+
objectIds: parseActivityObjectIds(record["object"]),
|
|
286
338
|
target: parseActivityObjectOrIri(record["target"]),
|
|
339
|
+
content: getString(record, "content"),
|
|
287
340
|
room: getString(record, "room"),
|
|
341
|
+
to: getStringArray(record, "to"),
|
|
342
|
+
cc: getStringArray(record, "cc"),
|
|
343
|
+
bto: getStringArray(record, "bto"),
|
|
344
|
+
bcc: getStringArray(record, "bcc"),
|
|
345
|
+
audience: getStringArray(record, "audience"),
|
|
288
346
|
};
|
|
289
347
|
}
|
|
290
348
|
|
|
@@ -91,11 +91,41 @@ export function isLocal(apId: string, baseUrl: string): boolean {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
function cleanPreferredUsername(value: string | null | undefined) {
|
|
95
|
+
const trimmed = value?.trim();
|
|
96
|
+
return trimmed ? trimmed : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render an actor handle without treating the actor-id path as fresher than
|
|
101
|
+
* ActivityPub's preferredUsername. Actor ids are stable across a profile rename,
|
|
102
|
+
* so `/users/old-name` may legitimately describe `preferredUsername=new-name`.
|
|
103
|
+
*/
|
|
104
|
+
export function formatUsername(
|
|
105
|
+
apId: string,
|
|
106
|
+
preferredUsername?: string | null,
|
|
107
|
+
): string {
|
|
108
|
+
const preferred = cleanPreferredUsername(preferredUsername);
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(apId);
|
|
111
|
+
if (preferred) return `${preferred}@${url.host}`;
|
|
112
|
+
const match = url.pathname.match(/\/(?:users|groups)\/([^/]+)\/?$/u);
|
|
113
|
+
if (match) return `${match[1]}@${url.host}`;
|
|
114
|
+
} catch {
|
|
115
|
+
// Retained legacy rows must remain displayable/removable even if their id
|
|
116
|
+
// predates the current absolute-URL validation boundary.
|
|
99
117
|
}
|
|
100
|
-
return apId;
|
|
118
|
+
return preferred || apId;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Always return a non-empty primary label for an actor roster row. */
|
|
122
|
+
export function formatPreferredUsername(
|
|
123
|
+
apId: string,
|
|
124
|
+
preferredUsername?: string | null,
|
|
125
|
+
): string {
|
|
126
|
+
const preferred = cleanPreferredUsername(preferredUsername);
|
|
127
|
+
if (preferred) return preferred;
|
|
128
|
+
const formatted = formatUsername(apId);
|
|
129
|
+
const separator = formatted.lastIndexOf("@");
|
|
130
|
+
return separator > 0 ? formatted.slice(0, separator) : formatted;
|
|
101
131
|
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "../federation-helpers.ts";
|
|
22
22
|
import { tryParseRemoteActor } from "./activitypub-validators.ts";
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
cacheRemoteActorDocument,
|
|
25
25
|
getInstanceFetchSignerByDb,
|
|
26
26
|
} from "./activitypub-actor-cache.ts";
|
|
27
27
|
import { logger } from "./logger.ts";
|
|
@@ -339,22 +339,12 @@ export async function fetchActorPublicKey(
|
|
|
339
339
|
isSafeRemoteUrl(actorData.id) &&
|
|
340
340
|
isSafeRemoteUrl(actorData.inbox)
|
|
341
341
|
) {
|
|
342
|
-
// Reuse the already-fetched signature actor document
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// Worker isolates: two isolates racing the same cold actor can both
|
|
349
|
-
// miss the existence check and then both INSERT, and the loser hits a
|
|
350
|
-
// primary-key violation that would null out a successfully-fetched
|
|
351
|
-
// key and spuriously reject a validly-signed activity.
|
|
352
|
-
// `onConflictDoUpdate` collapses that to one race-safe statement
|
|
353
|
-
// (same pattern as fetchAndCacheRemoteActor in queue-batching.ts).
|
|
354
|
-
await db
|
|
355
|
-
.insert(actorCache)
|
|
356
|
-
.values({ apId: actorData.id, ...cacheFields })
|
|
357
|
-
.onConflictDoUpdate({ target: actorCache.apId, set: cacheFields });
|
|
342
|
+
// Reuse the already-fetched signature actor document through the one
|
|
343
|
+
// successful persistence boundary. This atomically refreshes the
|
|
344
|
+
// canonical cache shape and clears any older fetch-failure authority.
|
|
345
|
+
await cacheRemoteActorDocument(db, actorUrl, rawActor, {
|
|
346
|
+
publicKey: "require-key",
|
|
347
|
+
});
|
|
358
348
|
}
|
|
359
349
|
|
|
360
350
|
return result.publicKeyPem;
|