@takosjp/yurucommu-core 3.3.0 → 3.4.1
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/0022_inbound_dispatch_claims.sql +17 -0
- package/package.json +3 -2
- package/packages/api/package.json +1 -1
- package/packages/api/src/index.ts +1 -0
- package/packages/api/src/lib/api/notifications.ts +1 -0
- package/packages/api/src/lib/api/posts.ts +1 -0
- package/packages/api/src/lib/rtc-client.ts +1 -3
- package/packages/api/src/types/call.ts +2 -10
- package/packages/api/src/types/index.ts +3 -0
- package/packages/api/src/types/realtime.ts +139 -0
- package/src/backend/index.ts +54 -12
- package/src/backend/lib/delivery/queue-batching.ts +53 -36
- package/src/backend/lib/delivery/queue-delivery.ts +3 -3
- package/src/backend/lib/delivery/queue.ts +17 -9
- package/src/backend/lib/delivery/types.ts +12 -0
- package/src/backend/lib/notification-push.ts +2 -2
- package/src/backend/lib/oauth-providers.ts +9 -0
- package/src/backend/lib/strip-image-metadata.ts +50 -30
- package/src/backend/lib/unread-counts.ts +63 -2
- package/src/backend/middleware/bearer-auth.ts +24 -9
- package/src/backend/public.ts +25 -1
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +4 -3
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +204 -5
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +78 -53
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +66 -2
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +35 -12
- package/src/backend/routes/activitypub/inbox-addressing.ts +236 -0
- package/src/backend/routes/activitypub/inbox-types.ts +8 -0
- package/src/backend/routes/activitypub/inbox.ts +410 -205
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/auth.ts +2 -1
- package/src/backend/routes/communities/messages.ts +53 -17
- package/src/backend/routes/dm/messages.ts +47 -7
- package/src/backend/routes/dm/read-archive.ts +27 -0
- package/src/backend/routes/dm/typing.ts +16 -0
- package/src/backend/routes/notifications.ts +25 -0
- package/src/backend/routes/posts/post-helpers.ts +42 -23
- package/src/backend/routes/realtime/index.ts +67 -0
- package/src/backend/routes/rtc/index.ts +5 -1
- package/src/backend/runtime/call-hub-core.ts +13 -3
- package/src/backend/runtime/cloudflare.ts +63 -2
- package/src/backend/runtime/managed-relational.ts +197 -0
- package/src/backend/runtime/managed-runtime.ts +631 -0
- package/src/backend/runtime/queue.ts +40 -0
- package/src/backend/runtime/realtime-hub.ts +257 -0
- package/src/backend/runtime/realtime-stream-do.ts +323 -0
- package/src/backend/server.ts +15 -18
- package/src/backend/types.ts +13 -2
- package/src/db/d1-write.ts +270 -0
- package/src/db/index.ts +17 -0
- package/src/db/schema/federation.ts +19 -0
- package/src/db/schema/index.ts +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared-inbox addressing: which LOCAL actors is an inbound activity for?
|
|
3
|
+
*
|
|
4
|
+
* The old model was an exception list. Six types were "recipient independent"
|
|
5
|
+
* and dispatched once; everything else fell through to "fan out to every local
|
|
6
|
+
* actor that follows the SENDER". That fallback is a guess, and it is wrong in
|
|
7
|
+
* both directions:
|
|
8
|
+
*
|
|
9
|
+
* - A DM or a Like from someone the recipient does not follow resolved to
|
|
10
|
+
* ZERO recipients. The route then committed `processed = 1`, so the
|
|
11
|
+
* activity was permanently un-redeliverable — a silent drop with a 202.
|
|
12
|
+
* Our own delivery planner always prefers `endpoints.sharedInbox`, and so
|
|
13
|
+
* does Mastodon, so this is the NORMAL inbound path, not an edge case.
|
|
14
|
+
* - `Like` / `Announce` were fanned out once PER LOCAL FOLLOWER even though
|
|
15
|
+
* their handlers take `_recipient` and resolve the target from the object's
|
|
16
|
+
* `attributedTo`. Every extra follower was one more redundant whole-row
|
|
17
|
+
* counter recompute.
|
|
18
|
+
*
|
|
19
|
+
* The replacement is a total function over the handled activity types. Every
|
|
20
|
+
* type declares its addressing class, `satisfies` makes the map exhaustive, and
|
|
21
|
+
* a new handler therefore cannot be added without declaring where it is
|
|
22
|
+
* addressed — the enforcement is `bunx tsc --noEmit`, not a lint script.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
26
|
+
import type { Database } from "../../../db/index.ts";
|
|
27
|
+
import { actors, follows, inChunks } from "../../../db/index.ts";
|
|
28
|
+
import { isLocal } from "../../federation-helpers.ts";
|
|
29
|
+
import type { Activity } from "./inbox-types.ts";
|
|
30
|
+
import {
|
|
31
|
+
addressesPublic,
|
|
32
|
+
MAX_ADDRESS_ENTRIES,
|
|
33
|
+
} from "./handlers/inbox-content-handlers.ts";
|
|
34
|
+
|
|
35
|
+
export type ActorRow = typeof actors.$inferSelect;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How an inbound activity names its recipients.
|
|
39
|
+
*
|
|
40
|
+
* `audience` is never DECLARED — it is only ever the RESULT of resolving an
|
|
41
|
+
* `addressed` activity whose addressing actually contains a Public or
|
|
42
|
+
* followers-collection marker. That distinction is the whole point: the old
|
|
43
|
+
* code performed a follower fan-out because it had found no other recipient,
|
|
44
|
+
* rather than because the activity asked for one.
|
|
45
|
+
*/
|
|
46
|
+
export type AddressingClass =
|
|
47
|
+
"instance" | "object-actor" | "addressed" | "audience";
|
|
48
|
+
|
|
49
|
+
export type DeclaredAddressingClass = Exclude<AddressingClass, "audience">;
|
|
50
|
+
|
|
51
|
+
/** Activity types `dispatchUserActivity` handles. */
|
|
52
|
+
export const HANDLED_ACTIVITY_TYPES = [
|
|
53
|
+
"Follow",
|
|
54
|
+
"Accept",
|
|
55
|
+
"Undo",
|
|
56
|
+
"Like",
|
|
57
|
+
"Create",
|
|
58
|
+
"Delete",
|
|
59
|
+
"Announce",
|
|
60
|
+
"Update",
|
|
61
|
+
"Reject",
|
|
62
|
+
"Add",
|
|
63
|
+
"Remove",
|
|
64
|
+
"Block",
|
|
65
|
+
"Flag",
|
|
66
|
+
"Move",
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
export type HandledActivityType = (typeof HANDLED_ACTIVITY_TYPES)[number];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The addressing declaration. `satisfies` keeps it total: adding a handler to
|
|
73
|
+
* {@link HANDLED_ACTIVITY_TYPES} without a class here is a type error.
|
|
74
|
+
*
|
|
75
|
+
* `Like` and `Announce` are `instance`: their handlers already ignore the
|
|
76
|
+
* recipient argument and resolve the affected object (and its owner) from
|
|
77
|
+
* `activity.object`, so dispatching them once is not a behaviour change — it
|
|
78
|
+
* only removes the per-follower duplication.
|
|
79
|
+
*
|
|
80
|
+
* `Undo` is declared `object-actor` because Undo(Follow|Block) must reach the
|
|
81
|
+
* followed/blocked actor. Undo(Like|Announce) carries no local actor target and
|
|
82
|
+
* is actor-keyed + idempotent, so the resolver downgrades it to `instance`.
|
|
83
|
+
*/
|
|
84
|
+
export const ACTIVITY_ADDRESSING = {
|
|
85
|
+
Accept: "instance",
|
|
86
|
+
Delete: "instance",
|
|
87
|
+
Update: "instance",
|
|
88
|
+
Reject: "instance",
|
|
89
|
+
Flag: "instance",
|
|
90
|
+
Move: "instance",
|
|
91
|
+
Like: "instance",
|
|
92
|
+
Announce: "instance",
|
|
93
|
+
|
|
94
|
+
Follow: "object-actor",
|
|
95
|
+
Block: "object-actor",
|
|
96
|
+
Undo: "object-actor",
|
|
97
|
+
|
|
98
|
+
Create: "addressed",
|
|
99
|
+
Add: "addressed",
|
|
100
|
+
Remove: "addressed",
|
|
101
|
+
} satisfies Record<HandledActivityType, DeclaredAddressingClass>;
|
|
102
|
+
|
|
103
|
+
export function isHandledActivityType(
|
|
104
|
+
type: string,
|
|
105
|
+
): type is HandledActivityType {
|
|
106
|
+
return type in ACTIVITY_ADDRESSING;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Every IRI the activity uses to name an audience, from the ENVELOPE and from
|
|
111
|
+
* an embedded object. The envelope fields used to be discarded by
|
|
112
|
+
* `parseActivity`, which is why a DM's addressing never reached routing.
|
|
113
|
+
*
|
|
114
|
+
* `bto`/`bcc` are included: a peer that strips them before delivery simply
|
|
115
|
+
* contributes nothing, and one that does not must still be honoured.
|
|
116
|
+
*/
|
|
117
|
+
export function collectAddresses(activity: Activity): string[] {
|
|
118
|
+
const object = typeof activity.object === "string" ? null : activity.object;
|
|
119
|
+
const all = [
|
|
120
|
+
...(activity.to ?? []),
|
|
121
|
+
...(activity.cc ?? []),
|
|
122
|
+
...(activity.bto ?? []),
|
|
123
|
+
...(activity.bcc ?? []),
|
|
124
|
+
...(activity.audience ?? []),
|
|
125
|
+
...(object?.to ?? []),
|
|
126
|
+
...(object?.cc ?? []),
|
|
127
|
+
];
|
|
128
|
+
// Same bound the persisted addressing arrays use (policy defined once, in
|
|
129
|
+
// inbox-content-handlers): a remote must not be able to make one delivery
|
|
130
|
+
// cost an unbounded number of actor lookups.
|
|
131
|
+
return [...new Set(all)].slice(0, MAX_ADDRESS_ENTRIES);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A followers-collection IRI. Matched by suffix, exactly like the visibility
|
|
136
|
+
* classifier in inbox-content-handlers — resolving the sender's real
|
|
137
|
+
* `followers` URL would need a network fetch for a remote actor.
|
|
138
|
+
*/
|
|
139
|
+
function addressesFollowersCollection(addresses: readonly string[]): boolean {
|
|
140
|
+
return addresses.some((a) => a.endsWith("/followers"));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type AddressingResolution = {
|
|
144
|
+
/** The class actually applied (may be `audience`, which is never declared). */
|
|
145
|
+
readonly cls: AddressingClass;
|
|
146
|
+
readonly recipients: readonly ActorRow[];
|
|
147
|
+
/** Addressing IRIs read off the activity, for logging. */
|
|
148
|
+
readonly addresses: readonly string[];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Local actors explicitly named in the addressing.
|
|
153
|
+
*/
|
|
154
|
+
async function resolveAddressedLocalActors(
|
|
155
|
+
db: Database,
|
|
156
|
+
addresses: readonly string[],
|
|
157
|
+
baseUrl: string,
|
|
158
|
+
): Promise<ActorRow[]> {
|
|
159
|
+
const localIris = addresses.filter(
|
|
160
|
+
(a) => isLocal(a, baseUrl) && !addressesPublic([a]),
|
|
161
|
+
);
|
|
162
|
+
if (localIris.length === 0) return [];
|
|
163
|
+
// Bounded by MAX_ADDRESS_ENTRIES, but routed through inChunks anyway so the
|
|
164
|
+
// D1 parameter rule holds if that bound is ever raised.
|
|
165
|
+
return await inChunks(localIris, (chunk) =>
|
|
166
|
+
db.query.actors.findMany({ where: inArray(actors.apId, [...chunk]) }),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Local actors with an accepted follow of `senderApId`.
|
|
172
|
+
*/
|
|
173
|
+
async function resolveLocalFollowers(
|
|
174
|
+
db: Database,
|
|
175
|
+
senderApId: string,
|
|
176
|
+
baseUrl: string,
|
|
177
|
+
limit: number,
|
|
178
|
+
): Promise<ActorRow[]> {
|
|
179
|
+
const rows = await db
|
|
180
|
+
.select({ followerApId: follows.followerApId })
|
|
181
|
+
.from(follows)
|
|
182
|
+
.where(
|
|
183
|
+
and(
|
|
184
|
+
eq(follows.followingApId, senderApId),
|
|
185
|
+
eq(follows.status, "accepted"),
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
.limit(limit);
|
|
189
|
+
|
|
190
|
+
const localFollowerApIds = rows
|
|
191
|
+
.map((row) => row.followerApId)
|
|
192
|
+
.filter((apId) => isLocal(apId, baseUrl));
|
|
193
|
+
if (localFollowerApIds.length === 0) return [];
|
|
194
|
+
|
|
195
|
+
return await inChunks(localFollowerApIds, (chunk) =>
|
|
196
|
+
db.query.actors.findMany({ where: inArray(actors.apId, [...chunk]) }),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolve an `addressed` activity's local recipients.
|
|
202
|
+
*
|
|
203
|
+
* A follower fan-out happens ONLY when the activity actually addresses Public
|
|
204
|
+
* or a followers collection (`audience`). An activity that names specific
|
|
205
|
+
* actors resolves to exactly those (`addressed`). An activity that names
|
|
206
|
+
* neither resolves to nothing, and the caller must treat that as UNDELIVERABLE
|
|
207
|
+
* rather than as a completed no-op — the old code's `processed = 1` on that
|
|
208
|
+
* branch is what made non-follower DMs unrecoverable.
|
|
209
|
+
*/
|
|
210
|
+
export async function resolveAddressedRecipients(
|
|
211
|
+
db: Database,
|
|
212
|
+
activity: Activity,
|
|
213
|
+
senderApId: string,
|
|
214
|
+
baseUrl: string,
|
|
215
|
+
followerFanoutLimit: number,
|
|
216
|
+
): Promise<AddressingResolution> {
|
|
217
|
+
const addresses = collectAddresses(activity);
|
|
218
|
+
const addressed = await resolveAddressedLocalActors(db, addresses, baseUrl);
|
|
219
|
+
|
|
220
|
+
const wantsAudience =
|
|
221
|
+
addressesPublic([...addresses]) || addressesFollowersCollection(addresses);
|
|
222
|
+
|
|
223
|
+
if (!wantsAudience) {
|
|
224
|
+
return { cls: "addressed", recipients: addressed, addresses };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const followers = await resolveLocalFollowers(
|
|
228
|
+
db,
|
|
229
|
+
senderApId,
|
|
230
|
+
baseUrl,
|
|
231
|
+
followerFanoutLimit,
|
|
232
|
+
);
|
|
233
|
+
const byApId = new Map<string, ActorRow>();
|
|
234
|
+
for (const row of [...addressed, ...followers]) byApId.set(row.apId, row);
|
|
235
|
+
return { cls: "audience", recipients: [...byApId.values()], addresses };
|
|
236
|
+
}
|
|
@@ -29,6 +29,14 @@ export type Activity = {
|
|
|
29
29
|
object?: string | ActivityObject;
|
|
30
30
|
target?: string | ActivityObject;
|
|
31
31
|
room?: string;
|
|
32
|
+
// Envelope addressing, preserved by parseActivity. Shared-inbox routing is
|
|
33
|
+
// derived from these (inbox-addressing.ts); before they were parsed the
|
|
34
|
+
// route had to guess the recipients from the sender's follower graph.
|
|
35
|
+
to?: string[];
|
|
36
|
+
cc?: string[];
|
|
37
|
+
bto?: string[];
|
|
38
|
+
bcc?: string[];
|
|
39
|
+
audience?: string[];
|
|
32
40
|
};
|
|
33
41
|
|
|
34
42
|
export type RemoteActor = {
|