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/rooms.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { isObject } from "../core/json.js";
|
|
2
|
+
import { M, UNSTABLE } from "../core/m.js";
|
|
2
3
|
import { SUPPORTED_ROOM_VERSIONS, roomVersionFeatures, splitStateKey, toClientEvent } from "../core/events.js";
|
|
3
4
|
import { MatrixError, errors } from "../core/errors.js";
|
|
4
5
|
import { isValidAlias, isValidRoomId, isValidUserId, parseId } from "../core/ids.js";
|
|
5
6
|
import { getState, membershipOf, ownerOfStateKey, powerLevels, roomCreators, userLevel } from "../core/authrules.js";
|
|
6
7
|
import { validatePowerLevelContent } from "../core/rooms.js";
|
|
7
8
|
import { assertNotSuspended } from "../core/auth.js";
|
|
9
|
+
import { number, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
|
|
8
10
|
import { body, fail, lenient } from "../http/input.js";
|
|
11
|
+
import { procedure } from "../http/procedure.js";
|
|
9
12
|
import { matchesFilter, parseRoomEventFilter } from "./filters.js";
|
|
10
|
-
import { z } from "zod";
|
|
11
13
|
//#region src/api/rooms.ts
|
|
12
14
|
const parseToken = (token) => {
|
|
13
15
|
if (token === null || token === "") return null;
|
|
@@ -90,7 +92,7 @@ async function backfillIfNeeded(hs, roomId) {
|
|
|
90
92
|
},
|
|
91
93
|
limit: 1
|
|
92
94
|
}))[0];
|
|
93
|
-
if (!first || first.type ===
|
|
95
|
+
if (!first || first.type === M.ROOM.CREATE) return false;
|
|
94
96
|
const prevs = (first.json.prev_events ?? []).map((p) => Array.isArray(p) ? p[0] : p);
|
|
95
97
|
const known = await hs.rooms.getEvents(prevs);
|
|
96
98
|
if (prevs.every((p) => known.has(p))) return false;
|
|
@@ -223,16 +225,16 @@ async function inviteByThreepid(ctx, s, body) {
|
|
|
223
225
|
}
|
|
224
226
|
const state = await hs.rooms.currentState(roomId);
|
|
225
227
|
const profile = await hs.profile(s.userId);
|
|
226
|
-
const create = getState(state,
|
|
228
|
+
const create = getState(state, M.ROOM.CREATE, "");
|
|
227
229
|
const invite = {
|
|
228
230
|
medium: body.medium,
|
|
229
231
|
address: body.address,
|
|
230
232
|
room_id: roomId,
|
|
231
233
|
sender: s.userId,
|
|
232
|
-
room_alias: getState(state,
|
|
233
|
-
room_avatar_url: getState(state,
|
|
234
|
-
room_join_rules: getState(state,
|
|
235
|
-
room_name: getState(state,
|
|
234
|
+
room_alias: getState(state, M.ROOM.CANONICAL_ALIAS, "")?.content?.alias,
|
|
235
|
+
room_avatar_url: getState(state, M.ROOM.AVATAR, "")?.content?.url,
|
|
236
|
+
room_join_rules: getState(state, M.ROOM.JOIN_RULES, "")?.content?.join_rule,
|
|
237
|
+
room_name: getState(state, M.ROOM.NAME, "")?.content?.name,
|
|
236
238
|
room_type: create?.content?.type,
|
|
237
239
|
sender_display_name: profile.displayname,
|
|
238
240
|
sender_avatar_url: profile.avatar_url
|
|
@@ -241,7 +243,7 @@ async function inviteByThreepid(ctx, s, body) {
|
|
|
241
243
|
const stored = await hs.identityServers.storeInvite(body.id_server, body.id_access_token, invite);
|
|
242
244
|
const [first] = stored.public_keys;
|
|
243
245
|
await hs.rooms.sendEvent(roomId, s.userId, {
|
|
244
|
-
type:
|
|
246
|
+
type: M.ROOM.THIRD_PARTY_INVITE,
|
|
245
247
|
state_key: stored.token,
|
|
246
248
|
content: {
|
|
247
249
|
display_name: stored.display_name,
|
|
@@ -256,11 +258,11 @@ async function ownEvent(hs, userId, eventId) {
|
|
|
256
258
|
return typeof eventId === "string" && (await hs.rooms.getPdu(eventId))?.sender === userId;
|
|
257
259
|
}
|
|
258
260
|
const membershipBody = body({
|
|
259
|
-
user_id: lenient(
|
|
260
|
-
reason: lenient(
|
|
261
|
+
user_id: lenient(string()),
|
|
262
|
+
reason: lenient(string())
|
|
261
263
|
});
|
|
262
264
|
async function sendMembership(ctx, membership) {
|
|
263
|
-
const s =
|
|
265
|
+
const s = ctx.session;
|
|
264
266
|
const roomId = ctx.params.roomId;
|
|
265
267
|
if (membership !== "leave") await assertNotSuspended(ctx.hs, s, roomId);
|
|
266
268
|
const body = ctx.input;
|
|
@@ -306,13 +308,13 @@ async function sendMembership(ctx, membership) {
|
|
|
306
308
|
return {};
|
|
307
309
|
}
|
|
308
310
|
async function validateStateEvent(hs, roomId, type, content, stateKeyValue = "") {
|
|
309
|
-
if (type ===
|
|
311
|
+
if (type === M.ROOM.CREATE) throw errors.badJson("The create event cannot be sent");
|
|
310
312
|
if (stateKeyValue.startsWith("@") && roomVersionFeatures(await hs.rooms.roomVersion(roomId)).ownedState && !ownerOfStateKey(stateKeyValue)) throw errors.badJson("Malformed user-owned state key");
|
|
311
|
-
if (type ===
|
|
313
|
+
if (type === M.ROOM.POWER_LEVELS) {
|
|
312
314
|
const version = await hs.rooms.roomVersion(roomId);
|
|
313
315
|
validatePowerLevelContent(content, roomCreators(await hs.rooms.currentState(roomId), version));
|
|
314
316
|
}
|
|
315
|
-
if (type ===
|
|
317
|
+
if (type === M.ROOM.CANONICAL_ALIAS) {
|
|
316
318
|
const aliases = [content.alias, ...Array.isArray(content.alt_aliases) ? content.alt_aliases : []].filter((a) => a !== void 0 && a !== null);
|
|
317
319
|
if (content.alt_aliases !== void 0 && !Array.isArray(content.alt_aliases)) throw new MatrixError(400, "M_INVALID_PARAM", "alt_aliases must be a list");
|
|
318
320
|
for (const alias of aliases) {
|
|
@@ -331,15 +333,15 @@ async function publicRoomChunk(hs, roomId) {
|
|
|
331
333
|
const chunk = {
|
|
332
334
|
room_id: roomId,
|
|
333
335
|
num_joined_members: (await hs.rooms.joinedMembers(roomId)).length,
|
|
334
|
-
world_readable: get(
|
|
335
|
-
guest_can_join: get(
|
|
336
|
-
join_rule: get(
|
|
336
|
+
world_readable: get(M.ROOM.HISTORY_VISIBILITY)?.history_visibility === "world_readable",
|
|
337
|
+
guest_can_join: get(M.ROOM.GUEST_ACCESS)?.guest_access === "can_join",
|
|
338
|
+
join_rule: get(M.ROOM.JOIN_RULES)?.join_rule ?? "invite"
|
|
337
339
|
};
|
|
338
|
-
if (typeof get(
|
|
339
|
-
if (typeof get(
|
|
340
|
-
if (typeof get(
|
|
341
|
-
if (typeof get(
|
|
342
|
-
const roomType = get(
|
|
340
|
+
if (typeof get(M.ROOM.NAME)?.name === "string") chunk.name = get(M.ROOM.NAME).name;
|
|
341
|
+
if (typeof get(M.ROOM.TOPIC)?.topic === "string") chunk.topic = get(M.ROOM.TOPIC).topic;
|
|
342
|
+
if (typeof get(M.ROOM.CANONICAL_ALIAS)?.alias === "string") chunk.canonical_alias = get(M.ROOM.CANONICAL_ALIAS).alias;
|
|
343
|
+
if (typeof get(M.ROOM.AVATAR)?.url === "string") chunk.avatar_url = get(M.ROOM.AVATAR).url;
|
|
344
|
+
const roomType = get(M.ROOM.CREATE)?.type;
|
|
343
345
|
if (typeof roomType === "string") chunk.room_type = roomType;
|
|
344
346
|
return chunk;
|
|
345
347
|
}
|
|
@@ -382,7 +384,7 @@ async function canDeleteAlias(hs, s, alias) {
|
|
|
382
384
|
if (!version) return false;
|
|
383
385
|
const state = await hs.rooms.currentState(alias.room_id);
|
|
384
386
|
const pl = powerLevels(state, version);
|
|
385
|
-
const needed = pl.events[
|
|
387
|
+
const needed = pl.events[M.ROOM.CANONICAL_ALIAS] ?? pl.state_default;
|
|
386
388
|
return membershipOf(state, s.userId) === "join" && userLevel(pl, s.userId) >= needed;
|
|
387
389
|
}
|
|
388
390
|
async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
|
|
@@ -390,11 +392,11 @@ async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
|
|
|
390
392
|
const version = await requireRoom(hs, roomId);
|
|
391
393
|
const state = await hs.rooms.requireJoined(roomId, s.userId);
|
|
392
394
|
const pl = powerLevels(state, version);
|
|
393
|
-
const tombstoneLevel = pl.events[
|
|
395
|
+
const tombstoneLevel = pl.events[M.ROOM.TOMBSTONE] ?? pl.state_default;
|
|
394
396
|
if (userLevel(pl, s.userId) < tombstoneLevel) throw errors.forbidden("You cannot upgrade this room");
|
|
395
|
-
const create = getState(state,
|
|
397
|
+
const create = getState(state, M.ROOM.CREATE);
|
|
396
398
|
const creationContent = {};
|
|
397
|
-
for (const k of [
|
|
399
|
+
for (const k of [M.FEDERATE, "type"]) if (create.content[k] !== void 0) creationContent[k] = create.content[k];
|
|
398
400
|
const toV12 = roomVersionFeatures(newVersion).v12;
|
|
399
401
|
if (toV12) {
|
|
400
402
|
creationContent.predecessor = { room_id: roomId };
|
|
@@ -414,26 +416,26 @@ async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
|
|
|
414
416
|
}))[0].event_id
|
|
415
417
|
};
|
|
416
418
|
const copyTypes = [
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
419
|
+
M.ROOM.SERVER_ACL,
|
|
420
|
+
M.ROOM.ENCRYPTION,
|
|
421
|
+
M.ROOM.NAME,
|
|
422
|
+
M.ROOM.AVATAR,
|
|
423
|
+
M.ROOM.TOPIC,
|
|
424
|
+
M.ROOM.GUEST_ACCESS,
|
|
425
|
+
M.ROOM.HISTORY_VISIBILITY,
|
|
426
|
+
M.ROOM.JOIN_RULES,
|
|
427
|
+
M.ROOM.POWER_LEVELS
|
|
426
428
|
];
|
|
427
429
|
const initialState = [];
|
|
428
430
|
for (const t of copyTypes) {
|
|
429
431
|
const ev = getState(state, t);
|
|
430
|
-
if (ev && t !==
|
|
432
|
+
if (ev && t !== M.ROOM.POWER_LEVELS) initialState.push({
|
|
431
433
|
type: t,
|
|
432
434
|
state_key: "",
|
|
433
435
|
content: ev.content
|
|
434
436
|
});
|
|
435
437
|
}
|
|
436
|
-
let oldPl = structuredClone(getState(state,
|
|
438
|
+
let oldPl = structuredClone(getState(state, M.ROOM.POWER_LEVELS)?.content ?? {});
|
|
437
439
|
const isPublic = (await hs.db.findOne("rooms", [{
|
|
438
440
|
field: "room_id",
|
|
439
441
|
value: roomId
|
|
@@ -466,9 +468,9 @@ async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
|
|
|
466
468
|
preset: "private_chat",
|
|
467
469
|
visibility: isPublic ? "public" : "private"
|
|
468
470
|
});
|
|
469
|
-
if (getState(state,
|
|
471
|
+
if (getState(state, M.ROOM.POWER_LEVELS) && !toV12) try {
|
|
470
472
|
await hs.rooms.sendEvent(newRoomId, s.userId, {
|
|
471
|
-
type:
|
|
473
|
+
type: M.ROOM.POWER_LEVELS,
|
|
472
474
|
state_key: "",
|
|
473
475
|
content: oldPl
|
|
474
476
|
});
|
|
@@ -481,15 +483,15 @@ async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
|
|
|
481
483
|
field: "alias",
|
|
482
484
|
value: a.alias
|
|
483
485
|
}], { room_id: newRoomId });
|
|
484
|
-
const canonical = getState(state,
|
|
486
|
+
const canonical = getState(state, M.ROOM.CANONICAL_ALIAS);
|
|
485
487
|
if (canonical && Object.keys(canonical.content).length) {
|
|
486
488
|
await hs.rooms.sendEvent(roomId, s.userId, {
|
|
487
|
-
type:
|
|
489
|
+
type: M.ROOM.CANONICAL_ALIAS,
|
|
488
490
|
state_key: "",
|
|
489
491
|
content: {}
|
|
490
492
|
}).catch(() => {});
|
|
491
493
|
await hs.rooms.sendEvent(newRoomId, s.userId, {
|
|
492
|
-
type:
|
|
494
|
+
type: M.ROOM.CANONICAL_ALIAS,
|
|
493
495
|
state_key: "",
|
|
494
496
|
content: canonical.content
|
|
495
497
|
}).catch(() => {});
|
|
@@ -499,7 +501,7 @@ async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
|
|
|
499
501
|
value: roomId
|
|
500
502
|
}], { is_public: false });
|
|
501
503
|
await hs.rooms.sendEvent(roomId, s.userId, {
|
|
502
|
-
type:
|
|
504
|
+
type: M.ROOM.TOMBSTONE,
|
|
503
505
|
state_key: "",
|
|
504
506
|
content: {
|
|
505
507
|
body: "This room has been replaced",
|
|
@@ -512,38 +514,44 @@ async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
|
|
|
512
514
|
invite: Math.max(50, oldPl.invite ?? 0)
|
|
513
515
|
};
|
|
514
516
|
await hs.rooms.sendEvent(roomId, s.userId, {
|
|
515
|
-
type:
|
|
517
|
+
type: M.ROOM.POWER_LEVELS,
|
|
516
518
|
state_key: "",
|
|
517
519
|
content: restricted
|
|
518
520
|
}).catch(() => {});
|
|
519
521
|
return newRoomId;
|
|
520
522
|
}
|
|
521
523
|
function registerRoomRoutes(r) {
|
|
522
|
-
r.client("POST", "/createRoom",
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
});
|
|
524
|
+
r.client("POST", "/createRoom", procedure({
|
|
525
|
+
auth: "user",
|
|
526
|
+
input: body({}),
|
|
527
|
+
handler: async (ctx) => {
|
|
528
|
+
const s = ctx.session;
|
|
529
|
+
if (s.isGuest) throw errors.forbidden("Guests cannot create rooms");
|
|
530
|
+
await assertNotSuspended(ctx.hs, s);
|
|
531
|
+
await ctx.rateLimit("createRoom");
|
|
532
|
+
const created = await ctx.hs.rooms.createRoom(s.userId, ctx.input);
|
|
533
|
+
await ctx.hs.audit.record({
|
|
534
|
+
action: "room_create",
|
|
535
|
+
actor: s.userId,
|
|
536
|
+
target: created.room_id,
|
|
537
|
+
ip: ctx.ip
|
|
538
|
+
});
|
|
539
|
+
return created;
|
|
540
|
+
}
|
|
541
|
+
}));
|
|
542
|
+
r.client("GET", "/joined_rooms", procedure({
|
|
543
|
+
auth: "user",
|
|
544
|
+
handler: async (ctx) => {
|
|
545
|
+
const s = ctx.session;
|
|
546
|
+
return { joined_rooms: await ctx.hs.rooms.roomsForUser(s.userId) };
|
|
547
|
+
}
|
|
548
|
+
}));
|
|
540
549
|
const joinBody = body({
|
|
541
|
-
reason: lenient(
|
|
542
|
-
third_party_signed:
|
|
550
|
+
reason: lenient(string()),
|
|
551
|
+
third_party_signed: unknown().optional()
|
|
543
552
|
});
|
|
544
553
|
const join = async (ctx, idOrAlias) => {
|
|
545
|
-
const s =
|
|
546
|
-
await ctx.rateLimit("join");
|
|
554
|
+
const s = ctx.session;
|
|
547
555
|
const body = ctx.input;
|
|
548
556
|
const { roomId, servers: resolvedServers } = await ctx.hs.rooms.resolveRoom(idOrAlias);
|
|
549
557
|
await assertNotSuspended(ctx.hs, s, roomId);
|
|
@@ -560,7 +568,7 @@ function registerRoomRoutes(r) {
|
|
|
560
568
|
const proof = signed;
|
|
561
569
|
if (await ctx.hs.rooms.membership(roomId, s.userId) !== "invite") {
|
|
562
570
|
const event = {
|
|
563
|
-
type:
|
|
571
|
+
type: M.ROOM.MEMBER,
|
|
564
572
|
room_id: roomId,
|
|
565
573
|
sender,
|
|
566
574
|
state_key: s.userId,
|
|
@@ -570,7 +578,7 @@ function registerRoomRoutes(r) {
|
|
|
570
578
|
}
|
|
571
579
|
};
|
|
572
580
|
if (ctx.hs.isLocalUser(sender)) {
|
|
573
|
-
const pending = getState(await ctx.hs.rooms.currentState(roomId),
|
|
581
|
+
const pending = getState(await ctx.hs.rooms.currentState(roomId), M.ROOM.THIRD_PARTY_INVITE, signed.token);
|
|
574
582
|
if (!pending || pending.sender !== sender) throw errors.forbidden("No such third-party invite");
|
|
575
583
|
await ctx.hs.rooms.invite(roomId, sender, s.userId, { thirdPartyInvite: {
|
|
576
584
|
display_name: pending.content?.display_name ?? "",
|
|
@@ -586,90 +594,130 @@ function registerRoomRoutes(r) {
|
|
|
586
594
|
});
|
|
587
595
|
return { room_id: roomId };
|
|
588
596
|
};
|
|
589
|
-
r.client("POST", "/rooms/:roomId/join", {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
597
|
+
r.client("POST", "/rooms/:roomId/join", procedure({
|
|
598
|
+
auth: "user",
|
|
599
|
+
rateLimit: "join",
|
|
600
|
+
input: joinBody,
|
|
601
|
+
handler: (ctx) => join(ctx, ctx.params.roomId)
|
|
602
|
+
}));
|
|
603
|
+
r.client("POST", "/join/:roomIdOrAlias", procedure({
|
|
604
|
+
auth: "user",
|
|
605
|
+
rateLimit: "join",
|
|
606
|
+
input: joinBody,
|
|
607
|
+
handler: (ctx) => join(ctx, ctx.params.roomIdOrAlias)
|
|
608
|
+
}));
|
|
609
|
+
r.client("POST", "/knock/:roomIdOrAlias", procedure({
|
|
610
|
+
auth: "user",
|
|
611
|
+
input: body({ reason: lenient(string()) }),
|
|
612
|
+
handler: async (ctx) => {
|
|
613
|
+
const s = ctx.session;
|
|
614
|
+
const body = ctx.input;
|
|
615
|
+
const { roomId, servers } = await ctx.hs.rooms.resolveRoom(ctx.params.roomIdOrAlias);
|
|
616
|
+
await assertNotSuspended(ctx.hs, s, roomId);
|
|
617
|
+
const via = [
|
|
618
|
+
...ctx.query.getAll("via"),
|
|
619
|
+
...ctx.query.getAll("server_name"),
|
|
620
|
+
...servers
|
|
621
|
+
];
|
|
622
|
+
await ctx.hs.rooms.setMembership(roomId, s.userId, s.userId, "knock", body.reason, via);
|
|
623
|
+
return { room_id: roomId };
|
|
624
|
+
}
|
|
625
|
+
}));
|
|
604
626
|
const inviteBody = body({
|
|
605
|
-
user_id: lenient(
|
|
606
|
-
reason: lenient(
|
|
607
|
-
});
|
|
608
|
-
r.client("POST", "/rooms/:roomId/invite", { input: inviteBody }, async (ctx) => {
|
|
609
|
-
const s = await ctx.auth();
|
|
610
|
-
const body = ctx.input;
|
|
611
|
-
await assertNotSuspended(ctx.hs, s, ctx.params.roomId);
|
|
612
|
-
await ctx.rateLimit("invite");
|
|
613
|
-
if (body.id_server !== void 0 || body.medium !== void 0) return inviteByThreepid(ctx, s, body);
|
|
614
|
-
if (!body.user_id || !isValidUserId(body.user_id)) throw errors.invalidParam("Invalid user_id");
|
|
615
|
-
await requireRoom(ctx.hs, ctx.params.roomId);
|
|
616
|
-
const current = await ctx.hs.rooms.membership(ctx.params.roomId, body.user_id);
|
|
617
|
-
if (current === "ban") throw errors.forbidden("User is banned");
|
|
618
|
-
if (current === "join") throw errors.forbidden("User is already joined");
|
|
619
|
-
await ctx.hs.rooms.invite(ctx.params.roomId, s.userId, body.user_id, { reason: body.reason });
|
|
620
|
-
return {};
|
|
621
|
-
});
|
|
622
|
-
r.client("POST", "/rooms/:roomId/leave", { input: membershipBody }, (ctx) => sendMembership(ctx, "leave"));
|
|
623
|
-
r.client("POST", "/rooms/:roomId/kick", { input: membershipBody }, (ctx) => sendMembership(ctx, "kick"));
|
|
624
|
-
r.client("POST", "/rooms/:roomId/ban", { input: membershipBody }, (ctx) => sendMembership(ctx, "ban"));
|
|
625
|
-
r.client("POST", "/rooms/:roomId/unban", { input: membershipBody }, (ctx) => sendMembership(ctx, "unban"));
|
|
626
|
-
r.client("POST", "/rooms/:roomId/forget", async (ctx) => {
|
|
627
|
-
const s = await ctx.auth();
|
|
628
|
-
const hs = ctx.hs;
|
|
629
|
-
const row = await hs.db.findOne("memberships", [{
|
|
630
|
-
field: "room_id",
|
|
631
|
-
value: ctx.params.roomId
|
|
632
|
-
}, {
|
|
633
|
-
field: "user_id",
|
|
634
|
-
value: s.userId
|
|
635
|
-
}]);
|
|
636
|
-
if (!row) throw errors.notFound("Not a member of this room");
|
|
637
|
-
if (row.membership === "join" || row.membership === "invite" || row.membership === "knock") throw errors.unknown("You must leave the room before forgetting it");
|
|
638
|
-
await hs.db.update("memberships", [{
|
|
639
|
-
field: "room_id",
|
|
640
|
-
value: ctx.params.roomId
|
|
641
|
-
}, {
|
|
642
|
-
field: "user_id",
|
|
643
|
-
value: s.userId
|
|
644
|
-
}], { forgotten: true });
|
|
645
|
-
return {};
|
|
646
|
-
});
|
|
647
|
-
r.client("PUT", "/rooms/:roomId/send/:eventType/:txnId", async (ctx) => {
|
|
648
|
-
const s = await ctx.auth();
|
|
649
|
-
await ctx.rateLimit("message");
|
|
650
|
-
const content = await ctx.json();
|
|
651
|
-
await requireRoom(ctx.hs, ctx.params.roomId);
|
|
652
|
-
if (!(s.suspended && ctx.params.eventType === "m.room.redaction" && await ownEvent(ctx.hs, s.userId, content?.redacts))) await assertNotSuspended(ctx.hs, s, ctx.params.roomId);
|
|
653
|
-
const delay = ctx.query.get("org.matrix.msc4140.delay");
|
|
654
|
-
if (delay !== null) return { delay_id: await ctx.hs.delayedEvents.schedule({
|
|
655
|
-
userId: s.userId,
|
|
656
|
-
deviceId: s.deviceId,
|
|
657
|
-
roomId: ctx.params.roomId,
|
|
658
|
-
type: ctx.params.eventType,
|
|
659
|
-
content,
|
|
660
|
-
delayMs: Number(delay),
|
|
661
|
-
txnId: ctx.params.txnId
|
|
662
|
-
}) };
|
|
663
|
-
const ts = s.appservice && ctx.query.get("ts") ? Number(ctx.query.get("ts")) : void 0;
|
|
664
|
-
return { event_id: await ctx.hs.rooms.sendEvent(ctx.params.roomId, s.userId, {
|
|
665
|
-
type: ctx.params.eventType,
|
|
666
|
-
content
|
|
667
|
-
}, {
|
|
668
|
-
txnId: ctx.params.txnId,
|
|
669
|
-
deviceId: s.deviceId || void 0,
|
|
670
|
-
ts
|
|
671
|
-
}) };
|
|
627
|
+
user_id: lenient(string()),
|
|
628
|
+
reason: lenient(string())
|
|
672
629
|
});
|
|
630
|
+
r.client("POST", "/rooms/:roomId/invite", procedure({
|
|
631
|
+
auth: "user",
|
|
632
|
+
input: inviteBody,
|
|
633
|
+
handler: async (ctx) => {
|
|
634
|
+
const s = ctx.session;
|
|
635
|
+
const body = ctx.input;
|
|
636
|
+
await assertNotSuspended(ctx.hs, s, ctx.params.roomId);
|
|
637
|
+
await ctx.rateLimit("invite");
|
|
638
|
+
if (body.id_server !== void 0 || body.medium !== void 0) return inviteByThreepid(ctx, s, body);
|
|
639
|
+
if (!body.user_id || !isValidUserId(body.user_id)) throw errors.invalidParam("Invalid user_id");
|
|
640
|
+
await requireRoom(ctx.hs, ctx.params.roomId);
|
|
641
|
+
const current = await ctx.hs.rooms.membership(ctx.params.roomId, body.user_id);
|
|
642
|
+
if (current === "ban") throw errors.forbidden("User is banned");
|
|
643
|
+
if (current === "join") throw errors.forbidden("User is already joined");
|
|
644
|
+
await ctx.hs.rooms.invite(ctx.params.roomId, s.userId, body.user_id, { reason: body.reason });
|
|
645
|
+
return {};
|
|
646
|
+
}
|
|
647
|
+
}));
|
|
648
|
+
r.client("POST", "/rooms/:roomId/leave", procedure({
|
|
649
|
+
auth: "user",
|
|
650
|
+
input: membershipBody,
|
|
651
|
+
handler: (ctx) => sendMembership(ctx, "leave")
|
|
652
|
+
}));
|
|
653
|
+
r.client("POST", "/rooms/:roomId/kick", procedure({
|
|
654
|
+
auth: "user",
|
|
655
|
+
input: membershipBody,
|
|
656
|
+
handler: (ctx) => sendMembership(ctx, "kick")
|
|
657
|
+
}));
|
|
658
|
+
r.client("POST", "/rooms/:roomId/ban", procedure({
|
|
659
|
+
auth: "user",
|
|
660
|
+
input: membershipBody,
|
|
661
|
+
handler: (ctx) => sendMembership(ctx, "ban")
|
|
662
|
+
}));
|
|
663
|
+
r.client("POST", "/rooms/:roomId/unban", procedure({
|
|
664
|
+
auth: "user",
|
|
665
|
+
input: membershipBody,
|
|
666
|
+
handler: (ctx) => sendMembership(ctx, "unban")
|
|
667
|
+
}));
|
|
668
|
+
r.client("POST", "/rooms/:roomId/forget", procedure({
|
|
669
|
+
auth: "user",
|
|
670
|
+
handler: async (ctx) => {
|
|
671
|
+
const s = ctx.session;
|
|
672
|
+
const hs = ctx.hs;
|
|
673
|
+
const row = await hs.db.findOne("memberships", [{
|
|
674
|
+
field: "room_id",
|
|
675
|
+
value: ctx.params.roomId
|
|
676
|
+
}, {
|
|
677
|
+
field: "user_id",
|
|
678
|
+
value: s.userId
|
|
679
|
+
}]);
|
|
680
|
+
if (!row) throw errors.notFound("Not a member of this room");
|
|
681
|
+
if (row.membership === "join" || row.membership === "invite" || row.membership === "knock") throw errors.unknown("You must leave the room before forgetting it");
|
|
682
|
+
await hs.db.update("memberships", [{
|
|
683
|
+
field: "room_id",
|
|
684
|
+
value: ctx.params.roomId
|
|
685
|
+
}, {
|
|
686
|
+
field: "user_id",
|
|
687
|
+
value: s.userId
|
|
688
|
+
}], { forgotten: true });
|
|
689
|
+
return {};
|
|
690
|
+
}
|
|
691
|
+
}));
|
|
692
|
+
r.client("PUT", "/rooms/:roomId/send/:eventType/:txnId", procedure({
|
|
693
|
+
auth: "user",
|
|
694
|
+
rateLimit: "message",
|
|
695
|
+
handler: async (ctx) => {
|
|
696
|
+
const s = ctx.session;
|
|
697
|
+
const content = await ctx.json();
|
|
698
|
+
await requireRoom(ctx.hs, ctx.params.roomId);
|
|
699
|
+
if (!(s.suspended && ctx.params.eventType === M.ROOM.REDACTION && await ownEvent(ctx.hs, s.userId, content?.redacts))) await assertNotSuspended(ctx.hs, s, ctx.params.roomId);
|
|
700
|
+
const delay = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4140.DELAY);
|
|
701
|
+
if (delay !== null) return { delay_id: await ctx.hs.delayedEvents.schedule({
|
|
702
|
+
userId: s.userId,
|
|
703
|
+
deviceId: s.deviceId,
|
|
704
|
+
roomId: ctx.params.roomId,
|
|
705
|
+
type: ctx.params.eventType,
|
|
706
|
+
content,
|
|
707
|
+
delayMs: Number(delay),
|
|
708
|
+
txnId: ctx.params.txnId
|
|
709
|
+
}) };
|
|
710
|
+
const ts = s.appservice && ctx.query.get("ts") ? Number(ctx.query.get("ts")) : void 0;
|
|
711
|
+
return { event_id: await ctx.hs.rooms.sendEvent(ctx.params.roomId, s.userId, {
|
|
712
|
+
type: ctx.params.eventType,
|
|
713
|
+
content
|
|
714
|
+
}, {
|
|
715
|
+
txnId: ctx.params.txnId,
|
|
716
|
+
deviceId: s.deviceId || void 0,
|
|
717
|
+
ts
|
|
718
|
+
}) };
|
|
719
|
+
}
|
|
720
|
+
}));
|
|
673
721
|
const putState = async (ctx) => {
|
|
674
722
|
const s = await ctx.auth();
|
|
675
723
|
const content = await ctx.json();
|
|
@@ -677,9 +725,9 @@ function registerRoomRoutes(r) {
|
|
|
677
725
|
await requireRoom(ctx.hs, roomId);
|
|
678
726
|
const type = ctx.params.eventType;
|
|
679
727
|
const key = ctx.params.stateKey ?? "";
|
|
680
|
-
if (!(type ===
|
|
728
|
+
if (!(type === M.ROOM.MEMBER && key === s.userId && content?.membership === "leave")) await assertNotSuspended(ctx.hs, s, roomId);
|
|
681
729
|
await validateStateEvent(ctx.hs, roomId, type, content, key);
|
|
682
|
-
const delay = ctx.query.get(
|
|
730
|
+
const delay = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4140.DELAY);
|
|
683
731
|
if (delay !== null) return { delay_id: await ctx.hs.delayedEvents.schedule({
|
|
684
732
|
userId: s.userId,
|
|
685
733
|
deviceId: s.deviceId,
|
|
@@ -698,8 +746,8 @@ function registerRoomRoutes(r) {
|
|
|
698
746
|
content
|
|
699
747
|
}, { ts }) };
|
|
700
748
|
};
|
|
701
|
-
r.client("PUT", "/rooms/:roomId/state/:eventType", putState);
|
|
702
|
-
r.client("PUT", "/rooms/:roomId/state/:eventType/:stateKey", putState);
|
|
749
|
+
r.client("PUT", "/rooms/:roomId/state/:eventType", procedure({ handler: putState }));
|
|
750
|
+
r.client("PUT", "/rooms/:roomId/state/:eventType/:stateKey", procedure({ handler: putState }));
|
|
703
751
|
const getStateEvent = async (ctx) => {
|
|
704
752
|
const s = await ctx.auth();
|
|
705
753
|
const state = await visibleState(ctx.hs, ctx.params.roomId, s.userId);
|
|
@@ -714,211 +762,235 @@ function registerRoomRoutes(r) {
|
|
|
714
762
|
});
|
|
715
763
|
return ev.content;
|
|
716
764
|
};
|
|
717
|
-
r.client("GET", "/rooms/:roomId/state/:eventType", getStateEvent);
|
|
718
|
-
r.client("GET", "/rooms/:roomId/state/:eventType/:stateKey", getStateEvent);
|
|
719
|
-
r.client("GET", "/rooms/:roomId/state",
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
operator: "lte",
|
|
737
|
-
value: at
|
|
738
|
-
}],
|
|
739
|
-
sortBy: {
|
|
740
|
-
field: "stream",
|
|
741
|
-
direction: "desc"
|
|
742
|
-
},
|
|
743
|
-
limit: 1
|
|
744
|
-
});
|
|
745
|
-
if (rows[0]) state = await hs.rooms.stateAtSnapshot(rows[0].state_snapshot);
|
|
746
|
-
else {
|
|
747
|
-
const first = await hs.db.findMany("events", {
|
|
765
|
+
r.client("GET", "/rooms/:roomId/state/:eventType", procedure({ handler: getStateEvent }));
|
|
766
|
+
r.client("GET", "/rooms/:roomId/state/:eventType/:stateKey", procedure({ handler: getStateEvent }));
|
|
767
|
+
r.client("GET", "/rooms/:roomId/state", procedure({
|
|
768
|
+
auth: "user",
|
|
769
|
+
handler: async (ctx) => {
|
|
770
|
+
const s = ctx.session;
|
|
771
|
+
return [...(await visibleState(ctx.hs, ctx.params.roomId, s.userId)).values()].map((ev) => toClientEvent(ev));
|
|
772
|
+
}
|
|
773
|
+
}));
|
|
774
|
+
r.client("GET", "/rooms/:roomId/members", procedure({
|
|
775
|
+
auth: "user",
|
|
776
|
+
handler: async (ctx) => {
|
|
777
|
+
const s = ctx.session;
|
|
778
|
+
const hs = ctx.hs;
|
|
779
|
+
await hs.partialState.waitForFullState(ctx.params.roomId, ctx.request.signal);
|
|
780
|
+
let state = await visibleState(hs, ctx.params.roomId, s.userId);
|
|
781
|
+
const at = parseToken(ctx.query.get("at"));
|
|
782
|
+
if (at !== null) {
|
|
783
|
+
const rows = await hs.db.findMany("events", {
|
|
748
784
|
where: [{
|
|
749
785
|
field: "room_id",
|
|
750
786
|
value: ctx.params.roomId
|
|
787
|
+
}, {
|
|
788
|
+
field: "stream",
|
|
789
|
+
operator: "lte",
|
|
790
|
+
value: at
|
|
751
791
|
}],
|
|
752
792
|
sortBy: {
|
|
753
793
|
field: "stream",
|
|
754
|
-
direction: "
|
|
794
|
+
direction: "desc"
|
|
755
795
|
},
|
|
756
|
-
limit:
|
|
796
|
+
limit: 1
|
|
757
797
|
});
|
|
758
|
-
if (
|
|
798
|
+
if (rows[0]) state = await hs.rooms.stateAtSnapshot(rows[0].state_snapshot);
|
|
799
|
+
else {
|
|
800
|
+
const first = await hs.db.findMany("events", {
|
|
801
|
+
where: [{
|
|
802
|
+
field: "room_id",
|
|
803
|
+
value: ctx.params.roomId
|
|
804
|
+
}],
|
|
805
|
+
sortBy: {
|
|
806
|
+
field: "stream",
|
|
807
|
+
direction: "asc"
|
|
808
|
+
},
|
|
809
|
+
limit: 2
|
|
810
|
+
});
|
|
811
|
+
if (first.length) state = await hs.rooms.stateAtSnapshot(first[first.length - 1].state_snapshot);
|
|
812
|
+
}
|
|
759
813
|
}
|
|
814
|
+
const membership = ctx.query.get("membership");
|
|
815
|
+
const notMembership = ctx.query.get("not_membership");
|
|
816
|
+
const chunk = [];
|
|
817
|
+
for (const [k, ev] of state) {
|
|
818
|
+
if (splitStateKey(k)[0] !== M.ROOM.MEMBER) continue;
|
|
819
|
+
const m = ev.content?.membership;
|
|
820
|
+
if (membership && m !== membership) continue;
|
|
821
|
+
if (notMembership && m === notMembership) continue;
|
|
822
|
+
chunk.push(toClientEvent(ev));
|
|
823
|
+
}
|
|
824
|
+
return { chunk };
|
|
760
825
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
for (const [k, ev] of state) {
|
|
782
|
-
const [type, key] = splitStateKey(k);
|
|
783
|
-
if (type !== "m.room.member" || ev.content?.membership !== "join") continue;
|
|
784
|
-
joined[key] = {
|
|
785
|
-
display_name: ev.content.displayname ?? null,
|
|
786
|
-
avatar_url: ev.content.avatar_url ?? null
|
|
787
|
-
};
|
|
826
|
+
}));
|
|
827
|
+
r.client("GET", "/rooms/:roomId/joined_members", procedure({
|
|
828
|
+
auth: "user",
|
|
829
|
+
handler: async (ctx) => {
|
|
830
|
+
const s = ctx.session;
|
|
831
|
+
await requireRoom(ctx.hs, ctx.params.roomId);
|
|
832
|
+
await ctx.hs.partialState.waitForFullState(ctx.params.roomId, ctx.request.signal);
|
|
833
|
+
const { joined: isJoined, worldReadable } = await ctx.hs.rooms.canPeek(ctx.params.roomId, s.userId);
|
|
834
|
+
if (!isJoined && !worldReadable) throw errors.forbidden("You are not joined to this room");
|
|
835
|
+
const state = await ctx.hs.rooms.currentState(ctx.params.roomId);
|
|
836
|
+
const joined = {};
|
|
837
|
+
for (const [k, ev] of state) {
|
|
838
|
+
const [type, key] = splitStateKey(k);
|
|
839
|
+
if (type !== M.ROOM.MEMBER || ev.content?.membership !== "join") continue;
|
|
840
|
+
joined[key] = {
|
|
841
|
+
display_name: ev.content.displayname ?? null,
|
|
842
|
+
avatar_url: ev.content.avatar_url ?? null
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
return { joined };
|
|
788
846
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
to,
|
|
809
|
-
dir,
|
|
810
|
-
limit: filter?.limit ?? limit,
|
|
811
|
-
filter
|
|
812
|
-
});
|
|
813
|
-
for (let attempt = 0; attempt < 3 && dir === "b"; attempt++) {
|
|
814
|
-
const gapEvent = await findGapBefore(hs, roomId, events, end);
|
|
815
|
-
if (!(gapEvent ? await backfillGap(hs, roomId, gapEvent) : end === null && events.length < (filter?.limit ?? limit) && await backfillIfNeeded(hs, roomId))) break;
|
|
816
|
-
({events, end} = await paginate(hs, s.userId, roomId, {
|
|
847
|
+
}));
|
|
848
|
+
r.client("GET", "/rooms/:roomId/messages", procedure({
|
|
849
|
+
auth: "user",
|
|
850
|
+
handler: async (ctx) => {
|
|
851
|
+
const s = ctx.session;
|
|
852
|
+
const hs = ctx.hs;
|
|
853
|
+
const roomId = ctx.params.roomId;
|
|
854
|
+
await visibleState(hs, roomId, s.userId);
|
|
855
|
+
const dir = ctx.query.get("dir") ?? "b";
|
|
856
|
+
if (dir !== "b" && dir !== "f") throw errors.invalidParam("dir must be 'b' or 'f'");
|
|
857
|
+
const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 1), 1e3);
|
|
858
|
+
const filter = parseRoomEventFilter(ctx);
|
|
859
|
+
const fromRaw = ctx.query.get("from");
|
|
860
|
+
const from = parsePageToken(fromRaw) ?? (dir === "b" ? {
|
|
861
|
+
depth: null,
|
|
862
|
+
stream: await hs.streams.position()
|
|
863
|
+
} : null);
|
|
864
|
+
const to = parsePageToken(ctx.query.get("to"));
|
|
865
|
+
let { events, end } = await paginate(hs, s.userId, roomId, {
|
|
817
866
|
from,
|
|
818
867
|
to,
|
|
819
868
|
dir,
|
|
820
869
|
limit: filter?.limit ?? limit,
|
|
821
870
|
filter
|
|
822
|
-
})
|
|
871
|
+
});
|
|
872
|
+
for (let attempt = 0; attempt < 3 && dir === "b"; attempt++) {
|
|
873
|
+
const gapEvent = await findGapBefore(hs, roomId, events, end);
|
|
874
|
+
if (!(gapEvent ? await backfillGap(hs, roomId, gapEvent) : end === null && events.length < (filter?.limit ?? limit) && await backfillIfNeeded(hs, roomId))) break;
|
|
875
|
+
({events, end} = await paginate(hs, s.userId, roomId, {
|
|
876
|
+
from,
|
|
877
|
+
to,
|
|
878
|
+
dir,
|
|
879
|
+
limit: filter?.limit ?? limit,
|
|
880
|
+
filter
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
const out = {
|
|
884
|
+
chunk: events.map((e) => hs.rooms.formatEvent(e, viewerOf(s))),
|
|
885
|
+
start: fromRaw ?? (from ? from.depth === null ? `s${from.stream}` : `t${from.depth}_${from.stream}` : "t0_0")
|
|
886
|
+
};
|
|
887
|
+
if (end !== null) out.end = end;
|
|
888
|
+
else if (events.length) out.end = topoToken(events[events.length - 1]);
|
|
889
|
+
if (filter?.lazy_load_members) {
|
|
890
|
+
const current = await hs.rooms.currentState(roomId);
|
|
891
|
+
out.state = [...new Set(events.map((e) => e.pdu.sender))].map((u) => getState(current, M.ROOM.MEMBER, u)).filter(Boolean).map((e) => toClientEvent(e));
|
|
892
|
+
}
|
|
893
|
+
return out;
|
|
823
894
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
895
|
+
}));
|
|
896
|
+
r.client("GET", "/rooms/:roomId/event/:eventId", procedure({
|
|
897
|
+
auth: "user",
|
|
898
|
+
handler: async (ctx) => {
|
|
899
|
+
const s = ctx.session;
|
|
900
|
+
const hs = ctx.hs;
|
|
901
|
+
await requireRoom(hs, ctx.params.roomId);
|
|
902
|
+
const ev = await hs.rooms.getEvent(ctx.params.eventId);
|
|
903
|
+
if (!ev || ev.pdu.room_id !== ctx.params.roomId) throw errors.notFound("Event not found");
|
|
904
|
+
if ((await hs.rooms.filterVisible(s.userId, ctx.params.roomId, [ev])).length === 0) throw errors.notFound("Event not found");
|
|
905
|
+
return hs.rooms.formatEvent(ev, viewerOf(s));
|
|
833
906
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
});
|
|
907
|
+
}));
|
|
908
|
+
r.client("GET", "/rooms/:roomId/context/:eventId", procedure({
|
|
909
|
+
auth: "user",
|
|
910
|
+
handler: async (ctx) => {
|
|
911
|
+
const s = ctx.session;
|
|
912
|
+
const hs = ctx.hs;
|
|
913
|
+
const roomId = ctx.params.roomId;
|
|
914
|
+
await requireRoom(hs, roomId);
|
|
915
|
+
const ev = await hs.rooms.getEvent(ctx.params.eventId);
|
|
916
|
+
if (!ev || ev.pdu.room_id !== roomId || (await hs.rooms.filterVisible(s.userId, roomId, [ev])).length === 0) throw errors.notFound("Event not found");
|
|
917
|
+
const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 0), 100);
|
|
918
|
+
const filter = parseRoomEventFilter(ctx);
|
|
919
|
+
const here = {
|
|
920
|
+
depth: ev.pdu.depth,
|
|
921
|
+
stream: ev.stream
|
|
922
|
+
};
|
|
923
|
+
const before = await paginate(hs, s.userId, roomId, {
|
|
924
|
+
from: here,
|
|
925
|
+
to: null,
|
|
926
|
+
dir: "b",
|
|
927
|
+
limit: Math.floor(limit / 2) || (limit ? 1 : 0),
|
|
928
|
+
filter
|
|
929
|
+
});
|
|
930
|
+
const after = await paginate(hs, s.userId, roomId, {
|
|
931
|
+
from: here,
|
|
932
|
+
to: null,
|
|
933
|
+
dir: "f",
|
|
934
|
+
limit: Math.ceil(limit / 2),
|
|
935
|
+
filter
|
|
936
|
+
});
|
|
937
|
+
const beforeEvents = limit ? before.events : [];
|
|
938
|
+
const afterEvents = limit ? after.events : [];
|
|
939
|
+
const state = await hs.rooms.stateAtSnapshot(ev.snapshot);
|
|
940
|
+
return {
|
|
941
|
+
event: hs.rooms.formatEvent(ev, viewerOf(s)),
|
|
942
|
+
events_before: beforeEvents.map((e) => hs.rooms.formatEvent(e, viewerOf(s))),
|
|
943
|
+
events_after: afterEvents.map((e) => hs.rooms.formatEvent(e, viewerOf(s))),
|
|
944
|
+
start: topoToken(beforeEvents.length ? beforeEvents[beforeEvents.length - 1] : ev),
|
|
945
|
+
end: topoToken(afterEvents.length ? afterEvents[afterEvents.length - 1] : ev),
|
|
946
|
+
state: [...state.values()].map((e) => toClientEvent(e))
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}));
|
|
950
|
+
r.client("PUT", "/rooms/:roomId/redact/:eventId/:txnId", procedure({
|
|
951
|
+
auth: "user",
|
|
952
|
+
handler: async (ctx) => {
|
|
953
|
+
const s = ctx.session;
|
|
954
|
+
const body = await ctx.json();
|
|
955
|
+
const roomId = ctx.params.roomId;
|
|
956
|
+
await requireRoom(ctx.hs, roomId);
|
|
957
|
+
if (!await ownEvent(ctx.hs, s.userId, ctx.params.eventId)) await assertNotSuspended(ctx.hs, s, roomId);
|
|
958
|
+
await ctx.hs.rooms.checkRedactionAllowed(roomId, s.userId, ctx.params.eventId);
|
|
959
|
+
const content = {};
|
|
960
|
+
if (typeof body.reason === "string") content.reason = body.reason;
|
|
961
|
+
return { event_id: await ctx.hs.rooms.sendEvent(roomId, s.userId, {
|
|
962
|
+
type: M.ROOM.REDACTION,
|
|
963
|
+
content,
|
|
964
|
+
redacts: ctx.params.eventId
|
|
965
|
+
}, {
|
|
966
|
+
txnId: ctx.params.txnId,
|
|
967
|
+
deviceId: s.deviceId
|
|
968
|
+
}) };
|
|
969
|
+
}
|
|
970
|
+
}));
|
|
971
|
+
r.client("PUT", "/rooms/:roomId/typing/:userId", procedure({
|
|
972
|
+
auth: "user",
|
|
973
|
+
handler: async (ctx) => {
|
|
974
|
+
const s = ctx.session;
|
|
975
|
+
if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot set typing for another user");
|
|
976
|
+
const body = await ctx.json();
|
|
977
|
+
if (typeof body.typing !== "boolean") throw errors.badJson("typing must be a boolean");
|
|
978
|
+
await ctx.hs.rooms.requireJoined(ctx.params.roomId, s.userId);
|
|
979
|
+
await ctx.hs.typing.setTyping(ctx.params.roomId, s.userId, body.typing, Number(body.timeout) || 3e4);
|
|
980
|
+
return {};
|
|
981
|
+
}
|
|
982
|
+
}));
|
|
911
983
|
const RECEIPT_TYPES = [
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
984
|
+
M.READ._,
|
|
985
|
+
M.READ.PRIVATE,
|
|
986
|
+
M.FULLY_READ
|
|
915
987
|
];
|
|
916
988
|
const setReceipt = async (hs, s, roomId, type, eventId, threadId) => {
|
|
917
|
-
if (type ===
|
|
989
|
+
if (type === M.FULLY_READ) {
|
|
918
990
|
await hs.streams.write((stream) => hs.db.upsert("account_data", {
|
|
919
991
|
user_id: s.userId,
|
|
920
992
|
room_id: roomId,
|
|
921
|
-
type:
|
|
993
|
+
type: M.FULLY_READ,
|
|
922
994
|
content: { event_id: eventId },
|
|
923
995
|
stream
|
|
924
996
|
}));
|
|
@@ -936,29 +1008,37 @@ function registerRoomRoutes(r) {
|
|
|
936
1008
|
ts: Date.now(),
|
|
937
1009
|
stream
|
|
938
1010
|
}));
|
|
939
|
-
hs.notifier.notify(type ===
|
|
1011
|
+
hs.notifier.notify(type === M.READ.PRIVATE ? [s.userId] : await hs.rooms.joinedMembers(roomId));
|
|
940
1012
|
await hs.federation.onReceipt(roomId, s.userId, type, eventId, Date.now(), threadId ?? "").catch(() => {});
|
|
941
1013
|
};
|
|
942
|
-
const receiptBody = body({ thread_id:
|
|
943
|
-
r.client("POST", "/rooms/:roomId/receipt/:receiptType/:eventId",
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const body = ctx.input;
|
|
954
|
-
await ctx.hs.rooms.requireJoined(ctx.params.roomId, s.userId);
|
|
955
|
-
for (const type of RECEIPT_TYPES) {
|
|
956
|
-
const eventId = body[type];
|
|
957
|
-
if (typeof eventId === "string") await setReceipt(ctx.hs, s, ctx.params.roomId, type, eventId);
|
|
1014
|
+
const receiptBody = body({ thread_id: string(fail("M_INVALID_PARAM", "Invalid thread_id")).min(1, "M_INVALID_PARAM: Invalid thread_id").optional() });
|
|
1015
|
+
r.client("POST", "/rooms/:roomId/receipt/:receiptType/:eventId", procedure({
|
|
1016
|
+
auth: "user",
|
|
1017
|
+
input: receiptBody,
|
|
1018
|
+
handler: async (ctx) => {
|
|
1019
|
+
const s = ctx.session;
|
|
1020
|
+
if (!RECEIPT_TYPES.includes(ctx.params.receiptType)) throw errors.invalidParam("Unknown receipt type");
|
|
1021
|
+
const body = ctx.input;
|
|
1022
|
+
await ctx.hs.rooms.requireJoined(ctx.params.roomId, s.userId);
|
|
1023
|
+
await setReceipt(ctx.hs, s, ctx.params.roomId, ctx.params.receiptType, ctx.params.eventId, body.thread_id);
|
|
1024
|
+
return {};
|
|
958
1025
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1026
|
+
}));
|
|
1027
|
+
r.client("POST", "/rooms/:roomId/read_markers", procedure({
|
|
1028
|
+
auth: "user",
|
|
1029
|
+
input: body({}),
|
|
1030
|
+
handler: async (ctx) => {
|
|
1031
|
+
const s = ctx.session;
|
|
1032
|
+
const body = ctx.input;
|
|
1033
|
+
await ctx.hs.rooms.requireJoined(ctx.params.roomId, s.userId);
|
|
1034
|
+
for (const type of RECEIPT_TYPES) {
|
|
1035
|
+
const eventId = body[type];
|
|
1036
|
+
if (typeof eventId === "string") await setReceipt(ctx.hs, s, ctx.params.roomId, type, eventId);
|
|
1037
|
+
}
|
|
1038
|
+
return {};
|
|
1039
|
+
}
|
|
1040
|
+
}));
|
|
1041
|
+
r.client("GET", "/directory/room/:alias", procedure({ handler: async (ctx) => {
|
|
962
1042
|
if (!isValidAlias(ctx.params.alias)) throw errors.invalidParam("Invalid alias");
|
|
963
1043
|
const { roomId, servers } = await ctx.hs.rooms.resolveRoom(ctx.params.alias);
|
|
964
1044
|
const inRoom = await ctx.hs.rooms.exists(roomId) ? [...await ctx.hs.federation.roomDestinations(roomId, await ctx.hs.rooms.currentState(roomId))] : [];
|
|
@@ -966,91 +1046,103 @@ function registerRoomRoutes(r) {
|
|
|
966
1046
|
room_id: roomId,
|
|
967
1047
|
servers: [.../* @__PURE__ */ new Set([...servers, ...inRoom])]
|
|
968
1048
|
};
|
|
969
|
-
});
|
|
970
|
-
r.client("PUT", "/directory/room/:alias",
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
alias
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1049
|
+
} }));
|
|
1050
|
+
r.client("PUT", "/directory/room/:alias", procedure({
|
|
1051
|
+
auth: "user",
|
|
1052
|
+
handler: async (ctx) => {
|
|
1053
|
+
const s = ctx.session;
|
|
1054
|
+
const body = await ctx.json();
|
|
1055
|
+
const alias = ctx.params.alias;
|
|
1056
|
+
if (!isValidAlias(alias) || /\s/.test(alias)) throw errors.invalidParam("Invalid alias");
|
|
1057
|
+
if (parseId(alias, "#").server !== ctx.hs.serverName) throw errors.invalidParam("Alias must be on this server");
|
|
1058
|
+
if (!isValidRoomId(body.room_id)) throw errors.invalidParam("Invalid room_id");
|
|
1059
|
+
if (!await ctx.hs.rooms.exists(body.room_id)) throw errors.notFound("Room not found");
|
|
1060
|
+
const owner = ctx.hs.appservices.exclusiveAliasOwner(alias);
|
|
1061
|
+
if (owner && s.appservice !== owner) throw errors.exclusive("This alias is reserved by an application service");
|
|
1062
|
+
if (await ctx.hs.db.findOne("room_aliases", [{
|
|
1063
|
+
field: "alias",
|
|
1064
|
+
value: alias
|
|
1065
|
+
}])) throw new MatrixError(409, "M_UNKNOWN", "Room alias already exists");
|
|
1066
|
+
await ctx.hs.db.create("room_aliases", {
|
|
1067
|
+
alias,
|
|
1068
|
+
room_id: body.room_id,
|
|
1069
|
+
creator: s.userId
|
|
1070
|
+
});
|
|
1071
|
+
return {};
|
|
1072
|
+
}
|
|
1073
|
+
}));
|
|
1074
|
+
r.client("DELETE", "/directory/room/:alias", procedure({
|
|
1075
|
+
auth: "user",
|
|
1076
|
+
handler: async (ctx) => {
|
|
1077
|
+
const s = ctx.session;
|
|
1078
|
+
const row = await ctx.hs.db.findOne("room_aliases", [{
|
|
1079
|
+
field: "alias",
|
|
1080
|
+
value: ctx.params.alias
|
|
1081
|
+
}]);
|
|
1082
|
+
if (!row) throw errors.notFound("Room alias not found");
|
|
1083
|
+
if (!await canDeleteAlias(ctx.hs, s, row)) throw errors.forbidden("You may not delete this alias");
|
|
1084
|
+
await ctx.hs.db.delete("room_aliases", [{
|
|
1085
|
+
field: "alias",
|
|
1086
|
+
value: ctx.params.alias
|
|
1087
|
+
}]);
|
|
1088
|
+
const canonical = getState(await ctx.hs.rooms.currentState(row.room_id), M.ROOM.CANONICAL_ALIAS);
|
|
1089
|
+
if (canonical) {
|
|
1090
|
+
const content = { ...canonical.content };
|
|
1091
|
+
let changed = false;
|
|
1092
|
+
if (content.alias === ctx.params.alias) {
|
|
1093
|
+
delete content.alias;
|
|
1094
|
+
changed = true;
|
|
1095
|
+
}
|
|
1096
|
+
if (Array.isArray(content.alt_aliases) && content.alt_aliases.includes(ctx.params.alias)) {
|
|
1097
|
+
content.alt_aliases = content.alt_aliases.filter((a) => a !== ctx.params.alias);
|
|
1098
|
+
changed = true;
|
|
1099
|
+
}
|
|
1100
|
+
if (changed) await ctx.hs.rooms.sendEvent(row.room_id, s.userId, {
|
|
1101
|
+
type: M.ROOM.CANONICAL_ALIAS,
|
|
1102
|
+
state_key: "",
|
|
1103
|
+
content
|
|
1104
|
+
}).catch(() => {});
|
|
1014
1105
|
}
|
|
1015
|
-
|
|
1016
|
-
type: "m.room.canonical_alias",
|
|
1017
|
-
state_key: "",
|
|
1018
|
-
content
|
|
1019
|
-
}).catch(() => {});
|
|
1106
|
+
return {};
|
|
1020
1107
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1108
|
+
}));
|
|
1109
|
+
r.client("GET", "/rooms/:roomId/aliases", procedure({
|
|
1110
|
+
auth: "user",
|
|
1111
|
+
handler: async (ctx) => {
|
|
1112
|
+
const s = ctx.session;
|
|
1113
|
+
await visibleState(ctx.hs, ctx.params.roomId, s.userId);
|
|
1114
|
+
return { aliases: (await ctx.hs.db.findMany("room_aliases", { where: [{
|
|
1115
|
+
field: "room_id",
|
|
1116
|
+
value: ctx.params.roomId
|
|
1117
|
+
}] })).map((r) => r.alias) };
|
|
1118
|
+
}
|
|
1119
|
+
}));
|
|
1120
|
+
r.client("GET", "/directory/list/room/:roomId", procedure({ handler: async (ctx) => {
|
|
1032
1121
|
const row = await ctx.hs.db.findOne("rooms", [{
|
|
1033
1122
|
field: "room_id",
|
|
1034
1123
|
value: ctx.params.roomId
|
|
1035
1124
|
}]);
|
|
1036
1125
|
if (!row) throw errors.notFound("Room not found");
|
|
1037
1126
|
return { visibility: row.is_public ? "public" : "private" };
|
|
1038
|
-
});
|
|
1039
|
-
r.client("PUT", "/directory/list/room/:roomId",
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1127
|
+
} }));
|
|
1128
|
+
r.client("PUT", "/directory/list/room/:roomId", procedure({
|
|
1129
|
+
auth: "user",
|
|
1130
|
+
handler: async (ctx) => {
|
|
1131
|
+
const s = ctx.session;
|
|
1132
|
+
const body = await ctx.json();
|
|
1133
|
+
if (!["public", "private"].includes(body.visibility)) throw errors.invalidParam("Invalid visibility");
|
|
1134
|
+
const version = await ctx.hs.rooms.roomVersion(ctx.params.roomId);
|
|
1135
|
+
if (!version) throw errors.notFound("Room not found");
|
|
1136
|
+
const state = await ctx.hs.rooms.requireJoined(ctx.params.roomId, s.userId);
|
|
1137
|
+
const pl = powerLevels(state, version);
|
|
1138
|
+
if (!s.isAdmin && userLevel(pl, s.userId) < (pl.events[M.ROOM.CANONICAL_ALIAS] ?? pl.state_default)) throw errors.forbidden("Insufficient power level");
|
|
1139
|
+
await ctx.hs.db.update("rooms", [{
|
|
1140
|
+
field: "room_id",
|
|
1141
|
+
value: ctx.params.roomId
|
|
1142
|
+
}], { is_public: body.visibility === "public" });
|
|
1143
|
+
return {};
|
|
1144
|
+
}
|
|
1145
|
+
}));
|
|
1054
1146
|
const remoteOrLocal = async (ctx, body) => {
|
|
1055
1147
|
const server = ctx.query.get("server");
|
|
1056
1148
|
if (server && server !== ctx.hs.serverName) {
|
|
@@ -1061,26 +1153,33 @@ function registerRoomRoutes(r) {
|
|
|
1061
1153
|
}
|
|
1062
1154
|
return publicRooms(ctx, body);
|
|
1063
1155
|
};
|
|
1064
|
-
r.client("GET", "/publicRooms", (ctx) => remoteOrLocal(ctx, {}));
|
|
1065
|
-
r.client("POST", "/publicRooms",
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1156
|
+
r.client("GET", "/publicRooms", procedure({ handler: (ctx) => remoteOrLocal(ctx, {}) }));
|
|
1157
|
+
r.client("POST", "/publicRooms", procedure({
|
|
1158
|
+
auth: "user",
|
|
1159
|
+
input: body({}),
|
|
1160
|
+
handler: async (ctx) => {
|
|
1161
|
+
return remoteOrLocal(ctx, ctx.input);
|
|
1162
|
+
}
|
|
1163
|
+
}));
|
|
1069
1164
|
const upgradeBody = body({
|
|
1070
|
-
new_version:
|
|
1071
|
-
additional_creators:
|
|
1072
|
-
});
|
|
1073
|
-
r.client("POST", "/rooms/:roomId/upgrade", { input: upgradeBody }, async (ctx) => {
|
|
1074
|
-
const s = await ctx.auth();
|
|
1075
|
-
const body = ctx.input;
|
|
1076
|
-
return { replacement_room: await upgradeRoom(ctx.hs, s, ctx.params.roomId, body.new_version, body.additional_creators) };
|
|
1165
|
+
new_version: string(fail("M_BAD_JSON", "Missing new_version")),
|
|
1166
|
+
additional_creators: unknown().optional()
|
|
1077
1167
|
});
|
|
1168
|
+
r.client("POST", "/rooms/:roomId/upgrade", procedure({
|
|
1169
|
+
auth: "user",
|
|
1170
|
+
input: upgradeBody,
|
|
1171
|
+
handler: async (ctx) => {
|
|
1172
|
+
const s = ctx.session;
|
|
1173
|
+
const body = ctx.input;
|
|
1174
|
+
return { replacement_room: await upgradeRoom(ctx.hs, s, ctx.params.roomId, body.new_version, body.additional_creators) };
|
|
1175
|
+
}
|
|
1176
|
+
}));
|
|
1078
1177
|
const reportBody = body({
|
|
1079
|
-
score:
|
|
1080
|
-
reason:
|
|
1178
|
+
score: number(fail("M_INVALID_PARAM", "score must be between -100 and 0")).int("M_INVALID_PARAM: score must be between -100 and 0").min(-100, "M_INVALID_PARAM: score must be between -100 and 0").max(0, "M_INVALID_PARAM: score must be between -100 and 0").optional(),
|
|
1179
|
+
reason: string(fail("M_BAD_JSON", "reason must be a string")).optional()
|
|
1081
1180
|
});
|
|
1082
1181
|
const report = async (ctx) => {
|
|
1083
|
-
const s =
|
|
1182
|
+
const s = ctx.session;
|
|
1084
1183
|
const hs = ctx.hs;
|
|
1085
1184
|
const roomId = ctx.params.roomId;
|
|
1086
1185
|
await requireRoom(hs, roomId);
|
|
@@ -1112,136 +1211,160 @@ function registerRoomRoutes(r) {
|
|
|
1112
1211
|
if (hs.adminRoom) hs.adminRoom.say(`New report #${id} from ${s.userId} about ${ctx.params.eventId ?? "room"} in ${roomId}: ${body.reason ?? "(no reason)"}`).catch(() => {});
|
|
1113
1212
|
return {};
|
|
1114
1213
|
};
|
|
1115
|
-
r.client("POST", "/users/:userId/report",
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
joined
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1214
|
+
r.client("POST", "/users/:userId/report", procedure({
|
|
1215
|
+
auth: "user",
|
|
1216
|
+
input: body({ reason: string(fail("M_BAD_JSON", "reason must be a string")) }),
|
|
1217
|
+
handler: async (ctx) => {
|
|
1218
|
+
const s = ctx.session;
|
|
1219
|
+
const hs = ctx.hs;
|
|
1220
|
+
const body = ctx.input;
|
|
1221
|
+
const userId = ctx.params.userId;
|
|
1222
|
+
if (!isValidUserId(userId)) throw errors.invalidParam("Invalid user ID");
|
|
1223
|
+
if (hs.isLocalUser(userId) && !await hs.db.findOne("users", [{
|
|
1224
|
+
field: "user_id",
|
|
1225
|
+
value: userId
|
|
1226
|
+
}])) throw errors.notFound("Unknown user");
|
|
1227
|
+
const id = await hs.cluster.nextId("user_reports");
|
|
1228
|
+
await hs.db.create("user_reports", {
|
|
1229
|
+
id,
|
|
1230
|
+
ts: Date.now(),
|
|
1231
|
+
user_id: userId,
|
|
1232
|
+
reporter: s.userId,
|
|
1233
|
+
reason: body.reason,
|
|
1234
|
+
resolved: false
|
|
1235
|
+
});
|
|
1236
|
+
await hs.audit.record({
|
|
1237
|
+
action: "user_report",
|
|
1238
|
+
actor: s.userId,
|
|
1239
|
+
target: userId,
|
|
1240
|
+
details: { report_id: id }
|
|
1241
|
+
});
|
|
1242
|
+
if (hs.adminRoom) hs.adminRoom.say(`New user report #${id} from ${s.userId} about ${userId}: ${body.reason || "(no reason)"}`).catch(() => {});
|
|
1243
|
+
return {};
|
|
1244
|
+
}
|
|
1245
|
+
}));
|
|
1246
|
+
r.client("GET", "/mutual_rooms", procedure({
|
|
1247
|
+
auth: "user",
|
|
1248
|
+
handler: async (ctx) => {
|
|
1249
|
+
const s = ctx.session;
|
|
1250
|
+
const other = ctx.query.get("user_id");
|
|
1251
|
+
if (other === null) throw errors.missingParam("Missing user_id");
|
|
1252
|
+
if (!isValidUserId(other)) throw errors.invalidParam("Invalid user_id");
|
|
1253
|
+
if (other === s.userId) throw errors.invalidParam("user_id must not be your own");
|
|
1254
|
+
const theirs = new Set(await ctx.hs.rooms.roomsForUser(other));
|
|
1255
|
+
const joined = (await ctx.hs.rooms.roomsForUser(s.userId)).filter((r) => theirs.has(r)).sort();
|
|
1256
|
+
const from = ctx.query.get("from");
|
|
1257
|
+
const start = from === null ? 0 : joined.findIndex((r) => r > from);
|
|
1258
|
+
if (from !== null && !isValidRoomId(from)) throw errors.invalidParam("Invalid from token");
|
|
1259
|
+
const page = start < 0 ? [] : joined.slice(start, start + 100);
|
|
1260
|
+
const out = {
|
|
1261
|
+
joined: page,
|
|
1262
|
+
count: joined.length
|
|
1263
|
+
};
|
|
1264
|
+
if (start >= 0 && start + 100 < joined.length) out.next_batch = page[page.length - 1];
|
|
1265
|
+
return out;
|
|
1266
|
+
}
|
|
1267
|
+
}), ["v1"]);
|
|
1268
|
+
r.client("POST", "/rooms/:roomId/report/:eventId", procedure({
|
|
1269
|
+
auth: "user",
|
|
1270
|
+
input: reportBody,
|
|
1271
|
+
handler: report
|
|
1272
|
+
}));
|
|
1273
|
+
r.client("POST", "/rooms/:roomId/report", procedure({
|
|
1274
|
+
auth: "user",
|
|
1275
|
+
input: reportBody,
|
|
1276
|
+
handler: report
|
|
1277
|
+
}));
|
|
1278
|
+
r.client("GET", "/admin/whois/:userId", procedure({
|
|
1279
|
+
auth: "user",
|
|
1280
|
+
handler: async (ctx) => {
|
|
1281
|
+
const s = ctx.session;
|
|
1282
|
+
const userId = ctx.params.userId;
|
|
1283
|
+
if (userId !== s.userId && !s.isAdmin) throw errors.forbidden("Only server admins can look up other users");
|
|
1284
|
+
if (!ctx.hs.isLocalUser(userId)) throw errors.notFound("Unknown user");
|
|
1285
|
+
const devices = await ctx.hs.db.findMany("devices", { where: [{
|
|
1286
|
+
field: "user_id",
|
|
1287
|
+
value: userId
|
|
1288
|
+
}] });
|
|
1289
|
+
const out = {};
|
|
1290
|
+
for (const d of devices) out[d.device_id] = { sessions: [{ connections: d.last_seen_ip ? [{
|
|
1291
|
+
ip: d.last_seen_ip,
|
|
1292
|
+
last_seen: d.last_seen_ts
|
|
1293
|
+
}] : [] }] };
|
|
1294
|
+
return {
|
|
1295
|
+
user_id: userId,
|
|
1296
|
+
devices: out
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
}));
|
|
1300
|
+
r.client("PUT", "/directory/list/appservice/:networkId/:roomId", procedure({
|
|
1301
|
+
auth: "user",
|
|
1302
|
+
handler: async (ctx) => {
|
|
1303
|
+
if (!ctx.session.appservice) throw errors.forbidden("Only application services may use this endpoint");
|
|
1304
|
+
const body = await ctx.json();
|
|
1305
|
+
if (!["public", "private"].includes(body.visibility)) throw errors.invalidParam("Invalid visibility");
|
|
1306
|
+
if (!await ctx.hs.rooms.exists(ctx.params.roomId)) throw errors.notFound("Room not found");
|
|
1307
|
+
await ctx.hs.db.update("rooms", [{
|
|
1308
|
+
field: "room_id",
|
|
1309
|
+
value: ctx.params.roomId
|
|
1310
|
+
}], { is_public: body.visibility === "public" });
|
|
1311
|
+
return {};
|
|
1312
|
+
}
|
|
1313
|
+
}));
|
|
1194
1314
|
const relVersions = ["v1", "v3"];
|
|
1195
|
-
r.client("GET", "/rooms/:roomId/relations/:eventId", relations, relVersions);
|
|
1196
|
-
r.client("GET", "/rooms/:roomId/relations/:eventId/:relType", relations, relVersions);
|
|
1197
|
-
r.client("GET", "/rooms/:roomId/relations/:eventId/:relType/:eventType", relations, relVersions);
|
|
1198
|
-
r.client("GET", "/rooms/:roomId/threads", threads, ["v1"]);
|
|
1199
|
-
r.client("GET", "/rooms/:roomId/timestamp_to_event",
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1315
|
+
r.client("GET", "/rooms/:roomId/relations/:eventId", procedure({ handler: relations }), relVersions);
|
|
1316
|
+
r.client("GET", "/rooms/:roomId/relations/:eventId/:relType", procedure({ handler: relations }), relVersions);
|
|
1317
|
+
r.client("GET", "/rooms/:roomId/relations/:eventId/:relType/:eventType", procedure({ handler: relations }), relVersions);
|
|
1318
|
+
r.client("GET", "/rooms/:roomId/threads", procedure({ handler: threads }), ["v1"]);
|
|
1319
|
+
r.client("GET", "/rooms/:roomId/timestamp_to_event", procedure({
|
|
1320
|
+
auth: "user",
|
|
1321
|
+
handler: async (ctx) => {
|
|
1322
|
+
const s = ctx.session;
|
|
1323
|
+
const hs = ctx.hs;
|
|
1324
|
+
const roomId = ctx.params.roomId;
|
|
1325
|
+
await visibleState(hs, roomId, s.userId);
|
|
1326
|
+
const ts = Number(ctx.query.get("ts"));
|
|
1327
|
+
const dir = ctx.query.get("dir");
|
|
1328
|
+
if (!Number.isFinite(ts) || dir !== "f" && dir !== "b") throw errors.invalidParam("Invalid ts or dir");
|
|
1329
|
+
let local = null;
|
|
1330
|
+
const rows = await hs.db.findMany("events", { where: [{
|
|
1331
|
+
field: "room_id",
|
|
1332
|
+
value: roomId
|
|
1333
|
+
}] });
|
|
1334
|
+
rows.sort((a, b) => dir === "f" ? a.json.origin_server_ts - b.json.origin_server_ts || a.depth - b.depth || a.stream - b.stream : b.json.origin_server_ts - a.json.origin_server_ts || b.depth - a.depth || b.stream - a.stream);
|
|
1335
|
+
for (const row of rows) {
|
|
1336
|
+
const t = row.json.origin_server_ts;
|
|
1337
|
+
if (!(dir === "f" ? t >= ts : t <= ts)) continue;
|
|
1338
|
+
if ((await hs.rooms.filterVisible(s.userId, roomId, [hs.rooms.rowToStored(row)])).length) {
|
|
1339
|
+
local = {
|
|
1340
|
+
event_id: row.event_id,
|
|
1341
|
+
origin_server_ts: t
|
|
1342
|
+
};
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1222
1345
|
}
|
|
1346
|
+
if (hs.federation.enabled) for (const server of hs.federation.serversInRoom(await hs.rooms.currentState(roomId))) try {
|
|
1347
|
+
const remote = await hs.federation.client.request(server, "GET", `/_matrix/federation/v1/timestamp_to_event/${encodeURIComponent(roomId)}`, {
|
|
1348
|
+
query: {
|
|
1349
|
+
ts: String(ts),
|
|
1350
|
+
dir
|
|
1351
|
+
},
|
|
1352
|
+
timeoutMs: 1e4
|
|
1353
|
+
});
|
|
1354
|
+
if (typeof remote.event_id !== "string" || typeof remote.origin_server_ts !== "number") continue;
|
|
1355
|
+
if (!local || (dir === "f" ? remote.origin_server_ts < local.origin_server_ts : remote.origin_server_ts > local.origin_server_ts)) {
|
|
1356
|
+
if (!await hs.rooms.getEvent(remote.event_id)) await hs.federation.backfill(roomId, [remote.event_id], 1).catch(() => 0);
|
|
1357
|
+
local = {
|
|
1358
|
+
event_id: remote.event_id,
|
|
1359
|
+
origin_server_ts: remote.origin_server_ts
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
break;
|
|
1363
|
+
} catch {}
|
|
1364
|
+
if (!local) throw errors.notFound("No event found");
|
|
1365
|
+
return local;
|
|
1223
1366
|
}
|
|
1224
|
-
|
|
1225
|
-
const remote = await hs.federation.client.request(server, "GET", `/_matrix/federation/v1/timestamp_to_event/${encodeURIComponent(roomId)}`, {
|
|
1226
|
-
query: {
|
|
1227
|
-
ts: String(ts),
|
|
1228
|
-
dir
|
|
1229
|
-
},
|
|
1230
|
-
timeoutMs: 1e4
|
|
1231
|
-
});
|
|
1232
|
-
if (typeof remote.event_id !== "string" || typeof remote.origin_server_ts !== "number") continue;
|
|
1233
|
-
if (!local || (dir === "f" ? remote.origin_server_ts < local.origin_server_ts : remote.origin_server_ts > local.origin_server_ts)) {
|
|
1234
|
-
if (!await hs.rooms.getEvent(remote.event_id)) await hs.federation.backfill(roomId, [remote.event_id], 1).catch(() => 0);
|
|
1235
|
-
local = {
|
|
1236
|
-
event_id: remote.event_id,
|
|
1237
|
-
origin_server_ts: remote.origin_server_ts
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
break;
|
|
1241
|
-
} catch {}
|
|
1242
|
-
if (!local) throw errors.notFound("No event found");
|
|
1243
|
-
return local;
|
|
1244
|
-
}, ["v1", "v3"]);
|
|
1367
|
+
}), ["v1", "v3"]);
|
|
1245
1368
|
}
|
|
1246
1369
|
async function relations(ctx) {
|
|
1247
1370
|
const s = await ctx.auth();
|
|
@@ -1270,7 +1393,7 @@ async function relations(ctx) {
|
|
|
1270
1393
|
const matches = [];
|
|
1271
1394
|
let next = null;
|
|
1272
1395
|
for (const row of rows) {
|
|
1273
|
-
const rel = row.json.content?.[
|
|
1396
|
+
const rel = row.json.content?.[M.RELATES_TO];
|
|
1274
1397
|
if (!isObject(rel) || rel.event_id !== ctx.params.eventId) continue;
|
|
1275
1398
|
if (ctx.params.relType && rel.rel_type !== ctx.params.relType) continue;
|
|
1276
1399
|
if (ctx.params.eventType && row.type !== ctx.params.eventType) continue;
|
|
@@ -1310,8 +1433,8 @@ async function threads(ctx) {
|
|
|
1310
1433
|
});
|
|
1311
1434
|
const latest = /* @__PURE__ */ new Map();
|
|
1312
1435
|
for (const row of rows) {
|
|
1313
|
-
const rel = row.json.content?.[
|
|
1314
|
-
if (!isObject(rel) || rel.rel_type !==
|
|
1436
|
+
const rel = row.json.content?.[M.RELATES_TO];
|
|
1437
|
+
if (!isObject(rel) || rel.rel_type !== M.THREAD || typeof rel.event_id !== "string") continue;
|
|
1315
1438
|
const entry = latest.get(rel.event_id) ?? {
|
|
1316
1439
|
stream: row.stream,
|
|
1317
1440
|
latestId: row.event_id,
|
|
@@ -1346,9 +1469,9 @@ async function threads(ctx) {
|
|
|
1346
1469
|
const latestEv = await hs.rooms.getEvent(info.latestId);
|
|
1347
1470
|
ev.unsigned = {
|
|
1348
1471
|
...ev.unsigned,
|
|
1349
|
-
|
|
1350
|
-
...ev.unsigned?.[
|
|
1351
|
-
|
|
1472
|
+
[M.RELATIONS]: {
|
|
1473
|
+
...ev.unsigned?.[M.RELATIONS],
|
|
1474
|
+
[M.THREAD]: {
|
|
1352
1475
|
latest_event: latestEv ? hs.rooms.formatEvent(latestEv, viewer) : void 0,
|
|
1353
1476
|
count: info.count,
|
|
1354
1477
|
current_user_participated: r.ev.pdu.sender === s.userId || info.participants.has(s.userId)
|