matrix-nodejs-homeserver 0.2.0 → 0.3.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/README.md +29 -7
- package/dist/api/account.js +385 -351
- package/dist/api/admin.js +76 -60
- package/dist/api/appservice.js +37 -25
- package/dist/api/backup.js +73 -62
- package/dist/api/federation.js +172 -138
- package/dist/api/filters.js +4 -3
- package/dist/api/legacy.js +91 -78
- package/dist/api/media.js +70 -54
- package/dist/api/misc.js +46 -37
- package/dist/api/preview.js +4 -3
- package/dist/api/profile.js +108 -97
- package/dist/api/push.js +114 -99
- package/dist/api/relationships.js +64 -58
- package/dist/api/rooms.js +703 -580
- package/dist/api/rtc.d.ts +0 -1
- package/dist/api/rtc.js +48 -34
- package/dist/api/search.js +133 -127
- package/dist/api/slidingsync.js +41 -31
- package/dist/api/spaces.js +82 -77
- package/dist/api/sync.js +59 -50
- package/dist/api/threadsubs.js +72 -61
- package/dist/api/threepid.js +99 -66
- package/dist/api/uia.js +17 -16
- package/dist/api/user.js +221 -178
- package/dist/core/admin.d.ts +0 -1
- package/dist/core/admin.js +6 -5
- package/dist/core/adminroom.js +9 -8
- package/dist/core/appservices.d.ts +2 -1
- package/dist/core/appservices.js +5 -4
- package/dist/core/audit.d.ts +0 -1
- package/dist/core/authrules.js +26 -25
- package/dist/core/delayed.js +2 -1
- package/dist/core/events.js +11 -10
- package/dist/core/invitefilter.js +2 -1
- package/dist/core/keys.js +2 -1
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/partialstate.js +2 -1
- package/dist/core/policy.js +4 -3
- package/dist/core/push.d.ts +0 -1
- package/dist/core/push.js +12 -11
- package/dist/core/pushrules.js +36 -35
- package/dist/core/rooms.js +76 -74
- package/dist/core/stateres.js +7 -6
- package/dist/federation/client.js +2 -1
- package/dist/federation/index.d.ts +1 -1
- package/dist/federation/index.js +30 -29
- package/dist/homeserver.d.ts +2 -1
- package/dist/homeserver.js +1 -0
- package/dist/http/handler.js +2 -23
- package/dist/http/input.js +2 -2
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +9 -26
- package/dist/http/router.js +10 -30
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/vendor/zod/LICENSE +21 -0
- package/dist/vendor/zod/v4/classic/errors.js +54 -0
- package/dist/vendor/zod/v4/classic/parse.js +17 -0
- package/dist/vendor/zod/v4/classic/schemas.js +865 -0
- package/dist/vendor/zod/v4/core/api.js +512 -0
- package/dist/vendor/zod/v4/core/checks.js +300 -0
- package/dist/vendor/zod/v4/core/core.js +109 -0
- package/dist/vendor/zod/v4/core/doc.js +35 -0
- package/dist/vendor/zod/v4/core/errors.js +118 -0
- package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
- package/dist/vendor/zod/v4/core/memoizer.js +255 -0
- package/dist/vendor/zod/v4/core/parse.js +206 -0
- package/dist/vendor/zod/v4/core/regexes.js +73 -0
- package/dist/vendor/zod/v4/core/registries.js +48 -0
- package/dist/vendor/zod/v4/core/schemas.js +1378 -0
- package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
- package/dist/vendor/zod/v4/core/util.js +537 -0
- package/dist/vendor/zod/v4/core/versions.js +8 -0
- package/dist/vendor/zod/v4/locales/en.js +110 -0
- package/package.json +3 -5
- package/dist/core/pushrules.d.ts +0 -1
package/dist/api/federation.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isObject } from "../core/json.js";
|
|
2
2
|
import { randomToken, signJson, verifyJsonSignature } from "../core/crypto.js";
|
|
3
|
+
import { M } from "../core/m.js";
|
|
3
4
|
import { SUPPORTED_ROOM_VERSIONS, eventIdOf, redact, roomVersionFeatures, splitStateKey, stateKey } from "../core/events.js";
|
|
4
5
|
import { MatrixError, errors } from "../core/errors.js";
|
|
5
6
|
import { isValidUserId } from "../core/ids.js";
|
|
@@ -7,26 +8,27 @@ import { authEventKeys, checkAuth, getState, membershipOf, powerLevels, userLeve
|
|
|
7
8
|
import { inviteDecision } from "../core/invitefilter.js";
|
|
8
9
|
import { parseXMatrix } from "../federation/client.js";
|
|
9
10
|
import { domainOf, serverAllowedByAcl } from "../federation/index.js";
|
|
11
|
+
import { array, looseObject, record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
|
|
10
12
|
import { body, lenient } from "../http/input.js";
|
|
13
|
+
import { procedure } from "../http/procedure.js";
|
|
11
14
|
import { roomDetails } from "./spaces.js";
|
|
12
15
|
import { fullProfile } from "../core/profile.js";
|
|
13
16
|
import { relationshipsForServer } from "./relationships.js";
|
|
14
17
|
import { canonicalJsonResponse } from "../http/handler.js";
|
|
15
|
-
import { z } from "zod";
|
|
16
18
|
//#region src/api/federation.ts
|
|
17
|
-
const objectMap = lenient(
|
|
19
|
+
const objectMap = lenient(record(string(), unknown()));
|
|
18
20
|
const keyQueryBody = body({ server_keys: objectMap });
|
|
19
21
|
const missingEventsBody = body({
|
|
20
|
-
earliest_events: lenient(
|
|
21
|
-
latest_events: lenient(
|
|
22
|
-
limit:
|
|
23
|
-
min_depth:
|
|
22
|
+
earliest_events: lenient(array(string())),
|
|
23
|
+
latest_events: lenient(array(string())),
|
|
24
|
+
limit: unknown().optional(),
|
|
25
|
+
min_depth: unknown().optional()
|
|
24
26
|
});
|
|
25
27
|
const userKeysQueryBody = body({ device_keys: objectMap });
|
|
26
28
|
const userKeysClaimBody = body({ one_time_keys: objectMap });
|
|
27
29
|
const publicRoomsBody = body({
|
|
28
|
-
limit:
|
|
29
|
-
filter: lenient(
|
|
30
|
+
limit: unknown().optional(),
|
|
31
|
+
filter: lenient(looseObject({ generic_search_term: lenient(string()) }))
|
|
30
32
|
});
|
|
31
33
|
const F = "/_matrix/federation";
|
|
32
34
|
/** Authenticate a server-server request; returns the origin server name. */
|
|
@@ -66,12 +68,12 @@ async function checkAcl(hs, roomId, origin) {
|
|
|
66
68
|
/** Whether `server` may see an event, per history visibility. */
|
|
67
69
|
async function serverCanSee(hs, server, ev) {
|
|
68
70
|
const ids = await hs.rooms.snapshotIds(ev.snapshot);
|
|
69
|
-
const hvId = ids[stateKey(
|
|
71
|
+
const hvId = ids[stateKey(M.ROOM.HISTORY_VISIBILITY, "")];
|
|
70
72
|
const hv = hvId ? (await hs.rooms.getPdu(hvId))?.content?.history_visibility ?? "shared" : "shared";
|
|
71
73
|
if (hv === "world_readable" || hv === "shared") return true;
|
|
72
74
|
for (const [key, id] of Object.entries(ids)) {
|
|
73
75
|
const [type, userId] = splitStateKey(key);
|
|
74
|
-
if (type !==
|
|
76
|
+
if (type !== M.ROOM.MEMBER || domainOf(userId) !== server) continue;
|
|
75
77
|
const m = (await hs.rooms.getPdu(id))?.content?.membership;
|
|
76
78
|
if (m === "join" || hv === "invited" && m === "invite") return true;
|
|
77
79
|
}
|
|
@@ -119,7 +121,7 @@ async function makeMembership(hs, roomId, version, userId, membership, content =
|
|
|
119
121
|
const prevs = [...(await hs.rooms.getPdus(extremities)).values()].sort((a, b) => b.depth - a.depth).slice(0, 10);
|
|
120
122
|
const f = roomVersionFeatures(version);
|
|
121
123
|
const draft = {
|
|
122
|
-
type:
|
|
124
|
+
type: M.ROOM.MEMBER,
|
|
123
125
|
sender: userId,
|
|
124
126
|
state_key: userId,
|
|
125
127
|
content: {
|
|
@@ -131,7 +133,7 @@ async function makeMembership(hs, roomId, version, userId, membership, content =
|
|
|
131
133
|
const ref = (id, pdu) => f.legacyEventIds ? [id, pdu?.hashes ?? {}] : id;
|
|
132
134
|
const authPdus = await hs.rooms.getPdus(authIds);
|
|
133
135
|
const event = {
|
|
134
|
-
type:
|
|
136
|
+
type: M.ROOM.MEMBER,
|
|
135
137
|
room_id: roomId,
|
|
136
138
|
sender: userId,
|
|
137
139
|
state_key: userId,
|
|
@@ -152,7 +154,7 @@ async function makeMembership(hs, roomId, version, userId, membership, content =
|
|
|
152
154
|
}
|
|
153
155
|
/** For restricted rooms: a local user who can authorise a remote join, or why not. */
|
|
154
156
|
async function restrictedAuthoriser(hs, state, version, userId) {
|
|
155
|
-
const joinRules = getState(state,
|
|
157
|
+
const joinRules = getState(state, M.ROOM.JOIN_RULES)?.content;
|
|
156
158
|
const rule = joinRules?.join_rule;
|
|
157
159
|
const f = roomVersionFeatures(version);
|
|
158
160
|
if (!(rule === "restricted" && f.restricted || rule === "knock_restricted" && f.knockRestricted)) return null;
|
|
@@ -161,7 +163,7 @@ async function restrictedAuthoriser(hs, state, version, userId) {
|
|
|
161
163
|
let allowed = false;
|
|
162
164
|
let unverifiable = false;
|
|
163
165
|
for (const allow of Array.isArray(joinRules?.allow) ? joinRules.allow : []) {
|
|
164
|
-
if (allow?.type !==
|
|
166
|
+
if (allow?.type !== M.ROOM_MEMBERSHIP || typeof allow.room_id !== "string") continue;
|
|
165
167
|
if (!await hs.rooms.serverIsJoined(allow.room_id)) {
|
|
166
168
|
unverifiable = true;
|
|
167
169
|
continue;
|
|
@@ -178,7 +180,7 @@ async function restrictedAuthoriser(hs, state, version, userId) {
|
|
|
178
180
|
const pl = powerLevels(state, version);
|
|
179
181
|
for (const [key, ev] of state) {
|
|
180
182
|
const [type, member] = splitStateKey(key);
|
|
181
|
-
if (type ===
|
|
183
|
+
if (type === M.ROOM.MEMBER && hs.isLocalUser(member) && ev.content?.membership === "join" && userLevel(pl, member) >= pl.invite) return member;
|
|
182
184
|
}
|
|
183
185
|
throw new MatrixError(400, "M_UNABLE_TO_GRANT_JOIN", "No local user can authorise this join");
|
|
184
186
|
}
|
|
@@ -187,7 +189,7 @@ async function restrictedAuthoriser(hs, state, version, userId) {
|
|
|
187
189
|
async function exchangeThirdPartyInvite(hs, roomId, sender, invitee, signed) {
|
|
188
190
|
const token = signed.token;
|
|
189
191
|
if (typeof token !== "string") throw errors.badJson("signed.token is required");
|
|
190
|
-
const pending = getState(await hs.rooms.currentState(roomId),
|
|
192
|
+
const pending = getState(await hs.rooms.currentState(roomId), M.ROOM.THIRD_PARTY_INVITE, token);
|
|
191
193
|
if (!pending) throw errors.forbidden("No such third-party invite");
|
|
192
194
|
if (pending.sender !== sender) throw errors.forbidden("The third-party invite was sent by someone else");
|
|
193
195
|
await hs.rooms.invite(roomId, sender, invitee, { thirdPartyInvite: {
|
|
@@ -201,7 +203,7 @@ async function sendMembership(ctx, origin, membership) {
|
|
|
201
203
|
const version = await roomOr404(hs, roomId);
|
|
202
204
|
await checkAcl(hs, roomId, origin);
|
|
203
205
|
const raw = await ctx.json();
|
|
204
|
-
if (raw.type !==
|
|
206
|
+
if (raw.type !== M.ROOM.MEMBER || raw.content?.membership !== membership || raw.state_key !== raw.sender) throw errors.badJson(`Expected a ${membership} event`);
|
|
205
207
|
if (domainOf(raw.sender) !== origin) throw errors.forbidden("Sender does not belong to the origin server");
|
|
206
208
|
let { pdu, joinAuthorisedSignatureValid } = await hs.federation.verifyPdu(raw, version, roomId);
|
|
207
209
|
if (pdu.event_id !== ctx.params.eventId) throw errors.badJson("Event ID mismatch");
|
|
@@ -235,8 +237,12 @@ async function sendMembership(ctx, origin, membership) {
|
|
|
235
237
|
};
|
|
236
238
|
}
|
|
237
239
|
function registerFederationRoutes(r) {
|
|
238
|
-
const fed = (method, path,
|
|
239
|
-
r.add(method, `${F}${path}`,
|
|
240
|
+
const fed = (method, path, def) => {
|
|
241
|
+
r.add(method, `${F}${path}`, procedure({
|
|
242
|
+
input: def.input,
|
|
243
|
+
guard: async (ctx) => ({ origin: await authenticate(ctx) }),
|
|
244
|
+
handler: (ctx) => def.handler(ctx, ctx.origin)
|
|
245
|
+
}));
|
|
240
246
|
};
|
|
241
247
|
const notary = async (ctx, servers) => {
|
|
242
248
|
const out = [];
|
|
@@ -262,16 +268,26 @@ function registerFederationRoutes(r) {
|
|
|
262
268
|
}
|
|
263
269
|
return { server_keys: out };
|
|
264
270
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
r.
|
|
269
|
-
|
|
271
|
+
const requireFederation = (ctx) => {
|
|
272
|
+
if (!ctx.hs.federation.enabled) throw errors.unrecognized("Federation is disabled");
|
|
273
|
+
};
|
|
274
|
+
r.post("/_matrix/key/v2/query", procedure({
|
|
275
|
+
input: keyQueryBody,
|
|
276
|
+
handler: async (ctx) => {
|
|
277
|
+
requireFederation(ctx);
|
|
278
|
+
return canonicalJsonResponse(await notary(ctx, Object.keys(ctx.input.server_keys ?? {})));
|
|
279
|
+
}
|
|
280
|
+
}));
|
|
281
|
+
r.get("/_matrix/key/v2/query/:serverName", procedure({ handler: async (ctx) => {
|
|
282
|
+
requireFederation(ctx);
|
|
283
|
+
return canonicalJsonResponse(await notary(ctx, [ctx.params.serverName]));
|
|
284
|
+
} }));
|
|
285
|
+
fed("PUT", "/v1/send/:txnId", { handler: async (ctx, origin) => {
|
|
270
286
|
const body = await ctx.json();
|
|
271
287
|
if (body.origin !== void 0 && body.origin !== origin) throw errors.forbidden("Transaction origin mismatch");
|
|
272
288
|
return ctx.hs.federation.receiveTransaction(origin, ctx.params.txnId, body);
|
|
273
|
-
});
|
|
274
|
-
fed("GET", "/v1/make_join/:roomId/:userId", async (ctx, origin) => {
|
|
289
|
+
} });
|
|
290
|
+
fed("GET", "/v1/make_join/:roomId/:userId", { handler: async (ctx, origin) => {
|
|
275
291
|
const hs = ctx.hs;
|
|
276
292
|
const { roomId, userId } = ctx.params;
|
|
277
293
|
if (domainOf(userId) !== origin) throw errors.forbidden("User does not belong to the origin server");
|
|
@@ -286,7 +302,7 @@ function registerFederationRoutes(r) {
|
|
|
286
302
|
event: await makeMembership(hs, roomId, version, userId, "join", via ? { join_authorised_via_users_server: via } : {}),
|
|
287
303
|
room_version: version
|
|
288
304
|
};
|
|
289
|
-
});
|
|
305
|
+
} });
|
|
290
306
|
const sendJoin = async (ctx, origin) => {
|
|
291
307
|
const hs = ctx.hs;
|
|
292
308
|
await rejectIfPartial(hs, ctx.params.roomId);
|
|
@@ -304,9 +320,9 @@ function registerFederationRoutes(r) {
|
|
|
304
320
|
servers_in_room: servers
|
|
305
321
|
};
|
|
306
322
|
};
|
|
307
|
-
fed("PUT", "/v2/send_join/:roomId/:eventId", sendJoin);
|
|
308
|
-
fed("PUT", "/v1/send_join/:roomId/:eventId", async (ctx, origin) => [200, await sendJoin(ctx, origin)]);
|
|
309
|
-
fed("GET", "/v1/make_leave/:roomId/:userId", async (ctx, origin) => {
|
|
323
|
+
fed("PUT", "/v2/send_join/:roomId/:eventId", { handler: sendJoin });
|
|
324
|
+
fed("PUT", "/v1/send_join/:roomId/:eventId", { handler: async (ctx, origin) => [200, await sendJoin(ctx, origin)] });
|
|
325
|
+
fed("GET", "/v1/make_leave/:roomId/:userId", { handler: async (ctx, origin) => {
|
|
310
326
|
const { roomId, userId } = ctx.params;
|
|
311
327
|
if (domainOf(userId) !== origin) throw errors.forbidden("User does not belong to the origin server");
|
|
312
328
|
const version = await roomOr404(ctx.hs, roomId);
|
|
@@ -315,16 +331,16 @@ function registerFederationRoutes(r) {
|
|
|
315
331
|
event: await makeMembership(ctx.hs, roomId, version, userId, "leave"),
|
|
316
332
|
room_version: version
|
|
317
333
|
};
|
|
318
|
-
});
|
|
319
|
-
fed("PUT", "/v2/send_leave/:roomId/:eventId", async (ctx, origin) => {
|
|
334
|
+
} });
|
|
335
|
+
fed("PUT", "/v2/send_leave/:roomId/:eventId", { handler: async (ctx, origin) => {
|
|
320
336
|
await sendMembership(ctx, origin, "leave");
|
|
321
337
|
return {};
|
|
322
|
-
});
|
|
323
|
-
fed("PUT", "/v1/send_leave/:roomId/:eventId", async (ctx, origin) => {
|
|
338
|
+
} });
|
|
339
|
+
fed("PUT", "/v1/send_leave/:roomId/:eventId", { handler: async (ctx, origin) => {
|
|
324
340
|
await sendMembership(ctx, origin, "leave");
|
|
325
341
|
return [200, {}];
|
|
326
|
-
});
|
|
327
|
-
fed("GET", "/v1/make_knock/:roomId/:userId", async (ctx, origin) => {
|
|
342
|
+
} });
|
|
343
|
+
fed("GET", "/v1/make_knock/:roomId/:userId", { handler: async (ctx, origin) => {
|
|
328
344
|
const { roomId, userId } = ctx.params;
|
|
329
345
|
if (domainOf(userId) !== origin) throw errors.forbidden("User does not belong to the origin server");
|
|
330
346
|
const version = await roomOr404(ctx.hs, roomId);
|
|
@@ -336,19 +352,19 @@ function registerFederationRoutes(r) {
|
|
|
336
352
|
event: await makeMembership(ctx.hs, roomId, version, userId, "knock"),
|
|
337
353
|
room_version: version
|
|
338
354
|
};
|
|
339
|
-
});
|
|
340
|
-
fed("PUT", "/v1/send_knock/:roomId/:eventId", async (ctx, origin) => {
|
|
355
|
+
} });
|
|
356
|
+
fed("PUT", "/v1/send_knock/:roomId/:eventId", { handler: async (ctx, origin) => {
|
|
341
357
|
await rejectIfPartial(ctx.hs, ctx.params.roomId);
|
|
342
358
|
await sendMembership(ctx, origin, "knock");
|
|
343
359
|
return { knock_room_state: ctx.hs.rooms.strippedState(await ctx.hs.rooms.currentState(ctx.params.roomId)) };
|
|
344
|
-
});
|
|
360
|
+
} });
|
|
345
361
|
const invite = async (ctx, origin, v2) => {
|
|
346
362
|
const hs = ctx.hs;
|
|
347
363
|
const body = await ctx.json();
|
|
348
364
|
const raw = v2 ? body.event : body;
|
|
349
365
|
const version = v2 ? String(body.room_version ?? "1") : "1";
|
|
350
366
|
if (!SUPPORTED_ROOM_VERSIONS.includes(version)) throw new MatrixError(400, "M_INCOMPATIBLE_ROOM_VERSION", "Unsupported room version", { room_version: version });
|
|
351
|
-
if (!isObject(raw) || raw.type !==
|
|
367
|
+
if (!isObject(raw) || raw.type !== M.ROOM.MEMBER || raw.content?.membership !== "invite" || typeof raw.state_key !== "string") throw errors.badJson("Expected an invite event");
|
|
352
368
|
if (raw.room_id !== ctx.params.roomId) throw errors.badJson("Room ID mismatch");
|
|
353
369
|
if (domainOf(raw.sender) !== origin) throw errors.forbidden("Sender does not belong to the origin server");
|
|
354
370
|
const target = raw.state_key;
|
|
@@ -373,7 +389,7 @@ function registerFederationRoutes(r) {
|
|
|
373
389
|
},
|
|
374
390
|
{
|
|
375
391
|
field: "type",
|
|
376
|
-
value:
|
|
392
|
+
value: M.IGNORED_USER_LIST
|
|
377
393
|
}
|
|
378
394
|
]);
|
|
379
395
|
const signedRedacted = signJson(redact({ ...raw }, version), hs.serverName, hs.signingKey);
|
|
@@ -382,7 +398,7 @@ function registerFederationRoutes(r) {
|
|
|
382
398
|
[hs.serverName]: signedRedacted.signatures[hs.serverName]
|
|
383
399
|
};
|
|
384
400
|
const inviteState = Array.isArray(body.invite_room_state) ? body.invite_room_state : Array.isArray(raw.unsigned?.invite_room_state) ? raw.unsigned.invite_room_state : [];
|
|
385
|
-
const createInState = inviteState.find((e) => e?.type ===
|
|
401
|
+
const createInState = inviteState.find((e) => e?.type === M.ROOM.CREATE);
|
|
386
402
|
if (v2 && !createInState && roomVersionFeatures(version).v12) throw errors.missingParam("invite_room_state must include the m.room.create event");
|
|
387
403
|
if (createInState && roomVersionFeatures(version).v12 && createInState.event_id === void 0) try {
|
|
388
404
|
if (hs.federation.computeEventId(createInState, version) !== `$${raw.room_id.slice(1)}`) throw new Error("mismatch");
|
|
@@ -404,9 +420,9 @@ function registerFederationRoutes(r) {
|
|
|
404
420
|
}, version);
|
|
405
421
|
return v2 ? { event: wire } : [200, { event: wire }];
|
|
406
422
|
};
|
|
407
|
-
fed("PUT", "/v2/invite/:roomId/:eventId", (ctx, origin) => invite(ctx, origin, true));
|
|
408
|
-
fed("PUT", "/v1/invite/:roomId/:eventId", (ctx, origin) => invite(ctx, origin, false));
|
|
409
|
-
fed("GET", "/v1/event/:eventId", async (ctx, origin) => {
|
|
423
|
+
fed("PUT", "/v2/invite/:roomId/:eventId", { handler: (ctx, origin) => invite(ctx, origin, true) });
|
|
424
|
+
fed("PUT", "/v1/invite/:roomId/:eventId", { handler: (ctx, origin) => invite(ctx, origin, false) });
|
|
425
|
+
fed("GET", "/v1/event/:eventId", { handler: async (ctx, origin) => {
|
|
410
426
|
const hs = ctx.hs;
|
|
411
427
|
const ev = await hs.rooms.getEvent(ctx.params.eventId);
|
|
412
428
|
const pdu = ev?.pdu ?? await hs.rooms.getPdu(ctx.params.eventId);
|
|
@@ -419,7 +435,7 @@ function registerFederationRoutes(r) {
|
|
|
419
435
|
origin_server_ts: Date.now(),
|
|
420
436
|
pdus: [hs.federation.toFederationPdu(pdu, version)]
|
|
421
437
|
};
|
|
422
|
-
});
|
|
438
|
+
} });
|
|
423
439
|
const stateHandler = (idsOnly) => async (ctx, origin) => {
|
|
424
440
|
const hs = ctx.hs;
|
|
425
441
|
const roomId = ctx.params.roomId;
|
|
@@ -440,15 +456,15 @@ function registerFederationRoutes(r) {
|
|
|
440
456
|
auth_chain: chain.map((p) => hs.federation.toFederationPdu(p, version))
|
|
441
457
|
};
|
|
442
458
|
};
|
|
443
|
-
fed("GET", "/v1/state/:roomId", stateHandler(false));
|
|
444
|
-
fed("GET", "/v1/state_ids/:roomId", stateHandler(true));
|
|
445
|
-
fed("GET", "/v1/event_auth/:roomId/:eventId", async (ctx, origin) => {
|
|
459
|
+
fed("GET", "/v1/state/:roomId", { handler: stateHandler(false) });
|
|
460
|
+
fed("GET", "/v1/state_ids/:roomId", { handler: stateHandler(true) });
|
|
461
|
+
fed("GET", "/v1/event_auth/:roomId/:eventId", { handler: async (ctx, origin) => {
|
|
446
462
|
const hs = ctx.hs;
|
|
447
463
|
const version = await roomOr404(hs, ctx.params.roomId);
|
|
448
464
|
await checkAcl(hs, ctx.params.roomId, origin);
|
|
449
465
|
return { auth_chain: (await authChainOf(hs, [ctx.params.eventId])).map((p) => hs.federation.toFederationPdu(p, version)) };
|
|
450
|
-
});
|
|
451
|
-
fed("GET", "/v1/backfill/:roomId", async (ctx, origin) => {
|
|
466
|
+
} });
|
|
467
|
+
fed("GET", "/v1/backfill/:roomId", { handler: async (ctx, origin) => {
|
|
452
468
|
const hs = ctx.hs;
|
|
453
469
|
const roomId = ctx.params.roomId;
|
|
454
470
|
const version = await roomOr404(hs, roomId);
|
|
@@ -475,40 +491,43 @@ function registerFederationRoutes(r) {
|
|
|
475
491
|
origin_server_ts: Date.now(),
|
|
476
492
|
pdus: out.map((p) => hs.federation.toFederationPdu(p, version))
|
|
477
493
|
};
|
|
478
|
-
});
|
|
479
|
-
fed("POST", "/v1/get_missing_events/:roomId",
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
while (frontier.length && out.length < limit) {
|
|
497
|
-
const next = [];
|
|
498
|
-
for (const id of frontier) {
|
|
499
|
-
if (seen.has(id) || earliest.has(id) || out.length >= limit) continue;
|
|
500
|
-
seen.add(id);
|
|
494
|
+
} });
|
|
495
|
+
fed("POST", "/v1/get_missing_events/:roomId", {
|
|
496
|
+
input: missingEventsBody,
|
|
497
|
+
handler: async (ctx, origin) => {
|
|
498
|
+
const hs = ctx.hs;
|
|
499
|
+
const roomId = ctx.params.roomId;
|
|
500
|
+
const version = await roomOr404(hs, roomId);
|
|
501
|
+
const state = await checkAcl(hs, roomId, origin);
|
|
502
|
+
if (!FederationJoinGuard.serverInRoom(state, origin)) throw errors.forbidden("Your server is not in the room");
|
|
503
|
+
const body = ctx.input;
|
|
504
|
+
const earliest = new Set(body.earliest_events ?? []);
|
|
505
|
+
const limit = Math.min(Number(body.limit ?? 10) || 10, 100);
|
|
506
|
+
const minDepth = Number(body.min_depth ?? 0) || 0;
|
|
507
|
+
const out = [];
|
|
508
|
+
const seen = new Set(body.latest_events ?? []);
|
|
509
|
+
let frontier = [];
|
|
510
|
+
for (const id of seen) {
|
|
501
511
|
const ev = await hs.rooms.getEvent(id);
|
|
502
|
-
if (
|
|
503
|
-
out.push(await serverCanSee(hs, origin, ev) ? ev.pdu : redact(ev.pdu, version));
|
|
504
|
-
next.push(...(ev.pdu.prev_events ?? []).map(eventIdOf));
|
|
512
|
+
if (ev) frontier.push(...(ev.pdu.prev_events ?? []).map(eventIdOf));
|
|
505
513
|
}
|
|
506
|
-
frontier
|
|
514
|
+
while (frontier.length && out.length < limit) {
|
|
515
|
+
const next = [];
|
|
516
|
+
for (const id of frontier) {
|
|
517
|
+
if (seen.has(id) || earliest.has(id) || out.length >= limit) continue;
|
|
518
|
+
seen.add(id);
|
|
519
|
+
const ev = await hs.rooms.getEvent(id);
|
|
520
|
+
if (!ev || ev.pdu.depth < minDepth) continue;
|
|
521
|
+
out.push(await serverCanSee(hs, origin, ev) ? ev.pdu : redact(ev.pdu, version));
|
|
522
|
+
next.push(...(ev.pdu.prev_events ?? []).map(eventIdOf));
|
|
523
|
+
}
|
|
524
|
+
frontier = next;
|
|
525
|
+
}
|
|
526
|
+
out.sort((a, b) => a.depth - b.depth);
|
|
527
|
+
return { events: out.map((p) => hs.federation.toFederationPdu(p, version)) };
|
|
507
528
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}, { input: missingEventsBody });
|
|
511
|
-
fed("GET", "/v1/timestamp_to_event/:roomId", async (ctx, origin) => {
|
|
529
|
+
});
|
|
530
|
+
fed("GET", "/v1/timestamp_to_event/:roomId", { handler: async (ctx, origin) => {
|
|
512
531
|
const hs = ctx.hs;
|
|
513
532
|
const roomId = ctx.params.roomId;
|
|
514
533
|
await roomOr404(hs, roomId);
|
|
@@ -534,8 +553,8 @@ function registerFederationRoutes(r) {
|
|
|
534
553
|
};
|
|
535
554
|
}
|
|
536
555
|
throw errors.notFound("No event found");
|
|
537
|
-
});
|
|
538
|
-
fed("GET", "/v1/query/directory", async (ctx) => {
|
|
556
|
+
} });
|
|
557
|
+
fed("GET", "/v1/query/directory", { handler: async (ctx) => {
|
|
539
558
|
const alias = ctx.query.get("room_alias");
|
|
540
559
|
if (!alias) throw errors.missingParam("Missing room_alias");
|
|
541
560
|
const row = await ctx.hs.db.findOne("room_aliases", [{
|
|
@@ -548,8 +567,8 @@ function registerFederationRoutes(r) {
|
|
|
548
567
|
room_id: row.room_id,
|
|
549
568
|
servers: [...new Set(servers)]
|
|
550
569
|
};
|
|
551
|
-
});
|
|
552
|
-
fed("GET", "/v1/query/profile", async (ctx) => {
|
|
570
|
+
} });
|
|
571
|
+
fed("GET", "/v1/query/profile", { handler: async (ctx) => {
|
|
553
572
|
const userId = ctx.query.get("user_id");
|
|
554
573
|
if (!userId) throw errors.missingParam("Missing user_id");
|
|
555
574
|
if (!isValidUserId(userId)) throw errors.invalidParam("Invalid user ID");
|
|
@@ -560,24 +579,33 @@ function registerFederationRoutes(r) {
|
|
|
560
579
|
if (!field) return profile;
|
|
561
580
|
if (profile[field] === void 0) throw errors.notFound("Profile field not set");
|
|
562
581
|
return { [field]: profile[field] };
|
|
582
|
+
} });
|
|
583
|
+
fed("POST", "/unstable/event_relationships", {
|
|
584
|
+
input: body({}),
|
|
585
|
+
handler: async (ctx, origin) => relationshipsForServer(ctx.hs, origin, ctx.input, (ids) => authChainOf(ctx.hs, ids))
|
|
563
586
|
});
|
|
564
|
-
fed("POST", "/
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
587
|
+
fed("POST", "/v1/user/keys/query", {
|
|
588
|
+
input: userKeysQueryBody,
|
|
589
|
+
handler: async (ctx) => {
|
|
590
|
+
const local = {};
|
|
591
|
+
for (const [userId, devices] of Object.entries(ctx.input.device_keys ?? {})) if (ctx.hs.isLocalUser(userId)) local[userId] = Array.isArray(devices) ? devices : [];
|
|
592
|
+
const res = await ctx.hs.keys.query("", local);
|
|
593
|
+
return {
|
|
594
|
+
device_keys: res.device_keys,
|
|
595
|
+
master_keys: res.master_keys,
|
|
596
|
+
self_signing_keys: res.self_signing_keys
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
fed("POST", "/v1/user/keys/claim", {
|
|
601
|
+
input: userKeysClaimBody,
|
|
602
|
+
handler: async (ctx) => {
|
|
603
|
+
const local = {};
|
|
604
|
+
for (const [userId, devices] of Object.entries(ctx.input.one_time_keys ?? {})) if (ctx.hs.isLocalUser(userId) && isObject(devices)) local[userId] = devices;
|
|
605
|
+
return { one_time_keys: await ctx.hs.keys.claim(local) };
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
fed("GET", "/v1/user/devices/:userId", { handler: async (ctx) => {
|
|
581
609
|
const hs = ctx.hs;
|
|
582
610
|
const userId = ctx.params.userId;
|
|
583
611
|
if (!hs.isLocalUser(userId)) throw errors.forbidden("User is not local");
|
|
@@ -601,7 +629,7 @@ function registerFederationRoutes(r) {
|
|
|
601
629
|
if (keys.master_keys[userId]) out.master_key = keys.master_keys[userId];
|
|
602
630
|
if (keys.self_signing_keys[userId]) out.self_signing_key = keys.self_signing_keys[userId];
|
|
603
631
|
return out;
|
|
604
|
-
});
|
|
632
|
+
} });
|
|
605
633
|
const publicRooms = async (ctx) => {
|
|
606
634
|
const hs = ctx.hs;
|
|
607
635
|
const body = (ctx.method === "POST" ? ctx.input : void 0) ?? {};
|
|
@@ -626,9 +654,12 @@ function registerFederationRoutes(r) {
|
|
|
626
654
|
if (limit && since + limit < chunk.length) out.next_batch = `o${since + limit}`;
|
|
627
655
|
return out;
|
|
628
656
|
};
|
|
629
|
-
fed("GET", "/v1/publicRooms", (ctx) => publicRooms(ctx));
|
|
630
|
-
fed("POST", "/v1/publicRooms",
|
|
631
|
-
|
|
657
|
+
fed("GET", "/v1/publicRooms", { handler: (ctx) => publicRooms(ctx) });
|
|
658
|
+
fed("POST", "/v1/publicRooms", {
|
|
659
|
+
input: publicRoomsBody,
|
|
660
|
+
handler: (ctx) => publicRooms(ctx)
|
|
661
|
+
});
|
|
662
|
+
fed("GET", "/v1/hierarchy/:roomId", { handler: async (ctx, origin) => {
|
|
632
663
|
const hs = ctx.hs;
|
|
633
664
|
const roomId = ctx.params.roomId;
|
|
634
665
|
await roomOr404(hs, roomId);
|
|
@@ -636,14 +667,14 @@ function registerFederationRoutes(r) {
|
|
|
636
667
|
const suggestedOnly = ctx.query.get("suggested_only") === "true";
|
|
637
668
|
const summary = async (id, s) => {
|
|
638
669
|
const details = await roomDetails(hs, id);
|
|
639
|
-
const jr = getState(s,
|
|
640
|
-
if (["restricted", "knock_restricted"].includes(jr?.join_rule)) details.allowed_room_ids = (Array.isArray(jr?.allow) ? jr.allow : []).filter((a) => a?.type ===
|
|
670
|
+
const jr = getState(s, M.ROOM.JOIN_RULES)?.content;
|
|
671
|
+
if (["restricted", "knock_restricted"].includes(jr?.join_rule)) details.allowed_room_ids = (Array.isArray(jr?.allow) ? jr.allow : []).filter((a) => a?.type === M.ROOM_MEMBERSHIP).map((a) => a.room_id);
|
|
641
672
|
return details;
|
|
642
673
|
};
|
|
643
674
|
const accessible = async (s) => {
|
|
644
|
-
const jr = getState(s,
|
|
675
|
+
const jr = getState(s, M.ROOM.JOIN_RULES)?.content;
|
|
645
676
|
const rule = jr?.join_rule ?? "invite";
|
|
646
|
-
const hv = getState(s,
|
|
677
|
+
const hv = getState(s, M.ROOM.HISTORY_VISIBILITY)?.content?.history_visibility;
|
|
647
678
|
if ([
|
|
648
679
|
"public",
|
|
649
680
|
"knock",
|
|
@@ -651,7 +682,7 @@ function registerFederationRoutes(r) {
|
|
|
651
682
|
].includes(rule) || hv === "world_readable" || FederationJoinGuard.serverInRoom(s, origin)) return true;
|
|
652
683
|
if (rule !== "restricted") return false;
|
|
653
684
|
for (const allow of Array.isArray(jr?.allow) ? jr.allow : []) {
|
|
654
|
-
if (allow?.type !==
|
|
685
|
+
if (allow?.type !== M.ROOM_MEMBERSHIP || typeof allow.room_id !== "string" || !await hs.rooms.exists(allow.room_id)) continue;
|
|
655
686
|
if (FederationJoinGuard.serverInRoom(await hs.rooms.currentState(allow.room_id), origin)) return true;
|
|
656
687
|
}
|
|
657
688
|
return false;
|
|
@@ -662,7 +693,7 @@ function registerFederationRoutes(r) {
|
|
|
662
693
|
const inaccessible = [];
|
|
663
694
|
for (const [key, ev] of state) {
|
|
664
695
|
const [type, childId] = splitStateKey(key);
|
|
665
|
-
if (type !==
|
|
696
|
+
if (type !== M.SPACE.CHILD || !Array.isArray(ev.content?.via) || !ev.content.via.length) continue;
|
|
666
697
|
if (suggestedOnly && ev.content.suggested !== true) continue;
|
|
667
698
|
childrenState.push({
|
|
668
699
|
type,
|
|
@@ -688,7 +719,7 @@ function registerFederationRoutes(r) {
|
|
|
688
719
|
children,
|
|
689
720
|
inaccessible_children: inaccessible
|
|
690
721
|
};
|
|
691
|
-
});
|
|
722
|
+
} });
|
|
692
723
|
const mediaHandler = async (ctx) => {
|
|
693
724
|
const hs = ctx.hs;
|
|
694
725
|
const meta = await hs.db.findOne("media", [{
|
|
@@ -710,40 +741,43 @@ function registerFederationRoutes(r) {
|
|
|
710
741
|
headers: { "content-type": `multipart/mixed; boundary=${boundary}` }
|
|
711
742
|
});
|
|
712
743
|
};
|
|
713
|
-
fed("GET", "/v1/media/download/:mediaId", (ctx) => mediaHandler(ctx));
|
|
714
|
-
fed("GET", "/v1/media/thumbnail/:mediaId", (ctx) => mediaHandler(ctx));
|
|
715
|
-
r.get(`${F}/v1/openid/userinfo`, async (ctx) => {
|
|
744
|
+
fed("GET", "/v1/media/download/:mediaId", { handler: (ctx) => mediaHandler(ctx) });
|
|
745
|
+
fed("GET", "/v1/media/thumbnail/:mediaId", { handler: (ctx) => mediaHandler(ctx) });
|
|
746
|
+
r.get(`${F}/v1/openid/userinfo`, procedure({ handler: async (ctx) => {
|
|
716
747
|
const token = ctx.query.get("access_token");
|
|
717
748
|
const userId = token ? await ctx.hs.cluster.get(`openid:${token}`) : null;
|
|
718
749
|
if (!userId) throw new MatrixError(401, "M_UNKNOWN_TOKEN", "Invalid OpenID token");
|
|
719
750
|
return { sub: userId };
|
|
720
|
-
});
|
|
721
|
-
r.client("POST", "/user/:userId/openid/request_token",
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
751
|
+
} }));
|
|
752
|
+
r.client("POST", "/user/:userId/openid/request_token", procedure({
|
|
753
|
+
auth: "user",
|
|
754
|
+
handler: async (ctx) => {
|
|
755
|
+
const s = ctx.session;
|
|
756
|
+
if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot request tokens for other users");
|
|
757
|
+
const token = randomToken();
|
|
758
|
+
await ctx.hs.cluster.set(`openid:${token}`, s.userId, 36e5);
|
|
759
|
+
return {
|
|
760
|
+
access_token: token,
|
|
761
|
+
token_type: "Bearer",
|
|
762
|
+
matrix_server_name: ctx.hs.serverName,
|
|
763
|
+
expires_in: 3600
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
}));
|
|
767
|
+
fed("PUT", "/v1/exchange_third_party_invite/:roomId", { handler: async (ctx, origin) => {
|
|
734
768
|
const hs = ctx.hs;
|
|
735
769
|
const roomId = ctx.params.roomId;
|
|
736
770
|
const event = await ctx.json();
|
|
737
771
|
const signed = event.content?.third_party_invite?.signed;
|
|
738
|
-
if (event.type !==
|
|
772
|
+
if (event.type !== M.ROOM.MEMBER || event.room_id !== roomId || event.content?.membership !== "invite" || !isObject(signed)) throw errors.badJson("Expected an m.room.member invite with third_party_invite");
|
|
739
773
|
if (typeof event.sender !== "string" || !hs.isLocalUser(event.sender)) throw errors.forbidden("The sender is not a user of this server");
|
|
740
774
|
if (typeof event.state_key !== "string" || domainOf(event.state_key) !== origin || signed.mxid !== event.state_key) throw errors.forbidden("The invitee does not belong to the requesting server");
|
|
741
775
|
await roomOr404(hs, roomId);
|
|
742
776
|
await checkAcl(hs, roomId, origin);
|
|
743
777
|
await exchangeThirdPartyInvite(hs, roomId, event.sender, event.state_key, signed);
|
|
744
778
|
return {};
|
|
745
|
-
});
|
|
746
|
-
r.put(`${F}/v1/3pid/onbind`, async (ctx) => {
|
|
779
|
+
} });
|
|
780
|
+
r.put(`${F}/v1/3pid/onbind`, procedure({ handler: async (ctx) => {
|
|
747
781
|
const hs = ctx.hs;
|
|
748
782
|
const body = await ctx.json();
|
|
749
783
|
const invites = Array.isArray(body.invites) ? body.invites : [];
|
|
@@ -752,7 +786,7 @@ function registerFederationRoutes(r) {
|
|
|
752
786
|
if (!isObject(signed) || typeof signed.mxid !== "string" || !hs.isLocalUser(signed.mxid)) continue;
|
|
753
787
|
if (typeof invite.room_id !== "string" || typeof invite.sender !== "string") continue;
|
|
754
788
|
const event = {
|
|
755
|
-
type:
|
|
789
|
+
type: M.ROOM.MEMBER,
|
|
756
790
|
room_id: invite.room_id,
|
|
757
791
|
sender: invite.sender,
|
|
758
792
|
state_key: signed.mxid,
|
|
@@ -769,21 +803,21 @@ function registerFederationRoutes(r) {
|
|
|
769
803
|
}
|
|
770
804
|
}
|
|
771
805
|
return {};
|
|
772
|
-
});
|
|
806
|
+
} }));
|
|
773
807
|
}
|
|
774
808
|
/** Helpers for "is a server in the room" checks. */
|
|
775
809
|
const FederationJoinGuard = {
|
|
776
810
|
serverInRoom(state, server) {
|
|
777
811
|
for (const [key, ev] of state) {
|
|
778
812
|
const [type, userId] = splitStateKey(key);
|
|
779
|
-
if (type ===
|
|
813
|
+
if (type === M.ROOM.MEMBER && domainOf(userId) === server && ev.content?.membership === "join") return true;
|
|
780
814
|
}
|
|
781
815
|
return false;
|
|
782
816
|
},
|
|
783
817
|
noLocalMembers(hs, state) {
|
|
784
818
|
for (const [key, ev] of state) {
|
|
785
819
|
const [type, userId] = splitStateKey(key);
|
|
786
|
-
if (type ===
|
|
820
|
+
if (type === M.ROOM.MEMBER && hs.isLocalUser(userId) && ev.content?.membership === "join") return false;
|
|
787
821
|
}
|
|
788
822
|
return true;
|
|
789
823
|
}
|
package/dist/api/filters.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isObject } from "../core/json.js";
|
|
2
|
+
import { M, UNSTABLE } from "../core/m.js";
|
|
2
3
|
import { errors } from "../core/errors.js";
|
|
3
4
|
import { globMatch as globMatch$1 } from "../core/glob.js";
|
|
4
5
|
//#region src/api/filters.ts
|
|
@@ -17,10 +18,10 @@ function matchesFilter(filter, ev) {
|
|
|
17
18
|
if (filter.not_rooms?.includes(ev.room_id)) return false;
|
|
18
19
|
if (filter.rooms && !filter.rooms.includes(ev.room_id)) return false;
|
|
19
20
|
}
|
|
20
|
-
const relTypes = filter.rel_types ?? filter[
|
|
21
|
-
const notRelTypes = filter.not_rel_types ?? filter[
|
|
21
|
+
const relTypes = filter.rel_types ?? filter[UNSTABLE.ORG.MATRIX.MSC3874.REL_TYPES];
|
|
22
|
+
const notRelTypes = filter.not_rel_types ?? filter[UNSTABLE.ORG.MATRIX.MSC3874.NOT_REL_TYPES];
|
|
22
23
|
if (relTypes || notRelTypes) {
|
|
23
|
-
const relType = ev.content?.[
|
|
24
|
+
const relType = ev.content?.[M.RELATES_TO]?.rel_type;
|
|
24
25
|
if (relTypes && !relTypes.includes(relType)) return false;
|
|
25
26
|
if (notRelTypes && typeof relType === "string" && notRelTypes.includes(relType)) return false;
|
|
26
27
|
}
|