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.
Files changed (78) hide show
  1. package/README.md +29 -7
  2. package/dist/api/account.js +385 -351
  3. package/dist/api/admin.js +76 -60
  4. package/dist/api/appservice.js +37 -25
  5. package/dist/api/backup.js +73 -62
  6. package/dist/api/federation.js +172 -138
  7. package/dist/api/filters.js +4 -3
  8. package/dist/api/legacy.js +91 -78
  9. package/dist/api/media.js +70 -54
  10. package/dist/api/misc.js +46 -37
  11. package/dist/api/preview.js +4 -3
  12. package/dist/api/profile.js +108 -97
  13. package/dist/api/push.js +114 -99
  14. package/dist/api/relationships.js +64 -58
  15. package/dist/api/rooms.js +703 -580
  16. package/dist/api/rtc.d.ts +0 -1
  17. package/dist/api/rtc.js +48 -34
  18. package/dist/api/search.js +133 -127
  19. package/dist/api/slidingsync.js +41 -31
  20. package/dist/api/spaces.js +82 -77
  21. package/dist/api/sync.js +59 -50
  22. package/dist/api/threadsubs.js +72 -61
  23. package/dist/api/threepid.js +99 -66
  24. package/dist/api/uia.js +17 -16
  25. package/dist/api/user.js +221 -178
  26. package/dist/core/admin.d.ts +0 -1
  27. package/dist/core/admin.js +6 -5
  28. package/dist/core/adminroom.js +9 -8
  29. package/dist/core/appservices.d.ts +2 -1
  30. package/dist/core/appservices.js +5 -4
  31. package/dist/core/audit.d.ts +0 -1
  32. package/dist/core/authrules.js +26 -25
  33. package/dist/core/delayed.js +2 -1
  34. package/dist/core/events.js +11 -10
  35. package/dist/core/invitefilter.js +2 -1
  36. package/dist/core/keys.js +2 -1
  37. package/dist/core/m.d.ts +209 -0
  38. package/dist/core/m.js +182 -0
  39. package/dist/core/partialstate.js +2 -1
  40. package/dist/core/policy.js +4 -3
  41. package/dist/core/push.d.ts +0 -1
  42. package/dist/core/push.js +12 -11
  43. package/dist/core/pushrules.js +36 -35
  44. package/dist/core/rooms.js +76 -74
  45. package/dist/core/stateres.js +7 -6
  46. package/dist/federation/client.js +2 -1
  47. package/dist/federation/index.d.ts +1 -1
  48. package/dist/federation/index.js +30 -29
  49. package/dist/homeserver.d.ts +2 -1
  50. package/dist/homeserver.js +1 -0
  51. package/dist/http/handler.js +2 -23
  52. package/dist/http/input.js +2 -2
  53. package/dist/http/procedure.js +25 -0
  54. package/dist/http/router.d.ts +9 -26
  55. package/dist/http/router.js +10 -30
  56. package/dist/index.d.ts +2 -1
  57. package/dist/index.js +2 -1
  58. package/dist/vendor/zod/LICENSE +21 -0
  59. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  60. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  61. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  62. package/dist/vendor/zod/v4/core/api.js +512 -0
  63. package/dist/vendor/zod/v4/core/checks.js +300 -0
  64. package/dist/vendor/zod/v4/core/core.js +109 -0
  65. package/dist/vendor/zod/v4/core/doc.js +35 -0
  66. package/dist/vendor/zod/v4/core/errors.js +118 -0
  67. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  68. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  69. package/dist/vendor/zod/v4/core/parse.js +206 -0
  70. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  71. package/dist/vendor/zod/v4/core/registries.js +48 -0
  72. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  73. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  74. package/dist/vendor/zod/v4/core/util.js +537 -0
  75. package/dist/vendor/zod/v4/core/versions.js +8 -0
  76. package/dist/vendor/zod/v4/locales/en.js +110 -0
  77. package/package.json +3 -5
  78. 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 === "m.room.create") return false;
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, "m.room.create", "");
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, "m.room.canonical_alias", "")?.content?.alias,
233
- room_avatar_url: getState(state, "m.room.avatar", "")?.content?.url,
234
- room_join_rules: getState(state, "m.room.join_rules", "")?.content?.join_rule,
235
- room_name: getState(state, "m.room.name", "")?.content?.name,
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: "m.room.third_party_invite",
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(z.string()),
260
- reason: lenient(z.string())
261
+ user_id: lenient(string()),
262
+ reason: lenient(string())
261
263
  });
262
264
  async function sendMembership(ctx, membership) {
263
- const s = await ctx.auth();
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 === "m.room.create") throw errors.badJson("The create event cannot be sent");
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 === "m.room.power_levels") {
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 === "m.room.canonical_alias") {
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("m.room.history_visibility")?.history_visibility === "world_readable",
335
- guest_can_join: get("m.room.guest_access")?.guest_access === "can_join",
336
- join_rule: get("m.room.join_rules")?.join_rule ?? "invite"
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("m.room.name")?.name === "string") chunk.name = get("m.room.name").name;
339
- if (typeof get("m.room.topic")?.topic === "string") chunk.topic = get("m.room.topic").topic;
340
- if (typeof get("m.room.canonical_alias")?.alias === "string") chunk.canonical_alias = get("m.room.canonical_alias").alias;
341
- if (typeof get("m.room.avatar")?.url === "string") chunk.avatar_url = get("m.room.avatar").url;
342
- const roomType = get("m.room.create")?.type;
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["m.room.canonical_alias"] ?? pl.state_default;
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["m.room.tombstone"] ?? pl.state_default;
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, "m.room.create");
397
+ const create = getState(state, M.ROOM.CREATE);
396
398
  const creationContent = {};
397
- for (const k of ["m.federate", "type"]) if (create.content[k] !== void 0) creationContent[k] = create.content[k];
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
- "m.room.server_acl",
418
- "m.room.encryption",
419
- "m.room.name",
420
- "m.room.avatar",
421
- "m.room.topic",
422
- "m.room.guest_access",
423
- "m.room.history_visibility",
424
- "m.room.join_rules",
425
- "m.room.power_levels"
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 !== "m.room.power_levels") initialState.push({
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, "m.room.power_levels")?.content ?? {});
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, "m.room.power_levels") && !toV12) try {
471
+ if (getState(state, M.ROOM.POWER_LEVELS) && !toV12) try {
470
472
  await hs.rooms.sendEvent(newRoomId, s.userId, {
471
- type: "m.room.power_levels",
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, "m.room.canonical_alias");
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: "m.room.canonical_alias",
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: "m.room.canonical_alias",
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: "m.room.tombstone",
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: "m.room.power_levels",
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", { input: body({}) }, async (ctx) => {
523
- const s = await ctx.auth();
524
- if (s.isGuest) throw errors.forbidden("Guests cannot create rooms");
525
- await assertNotSuspended(ctx.hs, s);
526
- await ctx.rateLimit("createRoom");
527
- const created = await ctx.hs.rooms.createRoom(s.userId, ctx.input);
528
- await ctx.hs.audit.record({
529
- action: "room_create",
530
- actor: s.userId,
531
- target: created.room_id,
532
- ip: ctx.ip
533
- });
534
- return created;
535
- });
536
- r.client("GET", "/joined_rooms", async (ctx) => {
537
- const s = await ctx.auth();
538
- return { joined_rooms: await ctx.hs.rooms.roomsForUser(s.userId) };
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(z.string()),
542
- third_party_signed: z.unknown().optional()
550
+ reason: lenient(string()),
551
+ third_party_signed: unknown().optional()
543
552
  });
544
553
  const join = async (ctx, idOrAlias) => {
545
- const s = await ctx.auth();
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: "m.room.member",
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), "m.room.third_party_invite", signed.token);
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", { input: joinBody }, (ctx) => join(ctx, ctx.params.roomId));
590
- r.client("POST", "/join/:roomIdOrAlias", { input: joinBody }, (ctx) => join(ctx, ctx.params.roomIdOrAlias));
591
- r.client("POST", "/knock/:roomIdOrAlias", { input: body({ reason: lenient(z.string()) }) }, async (ctx) => {
592
- const s = await ctx.auth();
593
- const body = ctx.input;
594
- const { roomId, servers } = await ctx.hs.rooms.resolveRoom(ctx.params.roomIdOrAlias);
595
- await assertNotSuspended(ctx.hs, s, roomId);
596
- const via = [
597
- ...ctx.query.getAll("via"),
598
- ...ctx.query.getAll("server_name"),
599
- ...servers
600
- ];
601
- await ctx.hs.rooms.setMembership(roomId, s.userId, s.userId, "knock", body.reason, via);
602
- return { room_id: roomId };
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(z.string()),
606
- reason: lenient(z.string())
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 === "m.room.member" && key === s.userId && content?.membership === "leave")) await assertNotSuspended(ctx.hs, s, roomId);
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("org.matrix.msc4140.delay");
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", async (ctx) => {
720
- const s = await ctx.auth();
721
- return [...(await visibleState(ctx.hs, ctx.params.roomId, s.userId)).values()].map((ev) => toClientEvent(ev));
722
- });
723
- r.client("GET", "/rooms/:roomId/members", async (ctx) => {
724
- const s = await ctx.auth();
725
- const hs = ctx.hs;
726
- await hs.partialState.waitForFullState(ctx.params.roomId, ctx.request.signal);
727
- let state = await visibleState(hs, ctx.params.roomId, s.userId);
728
- const at = parseToken(ctx.query.get("at"));
729
- if (at !== null) {
730
- const rows = await hs.db.findMany("events", {
731
- where: [{
732
- field: "room_id",
733
- value: ctx.params.roomId
734
- }, {
735
- field: "stream",
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: "asc"
794
+ direction: "desc"
755
795
  },
756
- limit: 2
796
+ limit: 1
757
797
  });
758
- if (first.length) state = await hs.rooms.stateAtSnapshot(first[first.length - 1].state_snapshot);
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
- const membership = ctx.query.get("membership");
762
- const notMembership = ctx.query.get("not_membership");
763
- const chunk = [];
764
- for (const [k, ev] of state) {
765
- if (splitStateKey(k)[0] !== "m.room.member") continue;
766
- const m = ev.content?.membership;
767
- if (membership && m !== membership) continue;
768
- if (notMembership && m === notMembership) continue;
769
- chunk.push(toClientEvent(ev));
770
- }
771
- return { chunk };
772
- });
773
- r.client("GET", "/rooms/:roomId/joined_members", async (ctx) => {
774
- const s = await ctx.auth();
775
- await requireRoom(ctx.hs, ctx.params.roomId);
776
- await ctx.hs.partialState.waitForFullState(ctx.params.roomId, ctx.request.signal);
777
- const { joined: isJoined, worldReadable } = await ctx.hs.rooms.canPeek(ctx.params.roomId, s.userId);
778
- if (!isJoined && !worldReadable) throw errors.forbidden("You are not joined to this room");
779
- const state = await ctx.hs.rooms.currentState(ctx.params.roomId);
780
- const joined = {};
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
- return { joined };
790
- });
791
- r.client("GET", "/rooms/:roomId/messages", async (ctx) => {
792
- const s = await ctx.auth();
793
- const hs = ctx.hs;
794
- const roomId = ctx.params.roomId;
795
- await visibleState(hs, roomId, s.userId);
796
- const dir = ctx.query.get("dir") ?? "b";
797
- if (dir !== "b" && dir !== "f") throw errors.invalidParam("dir must be 'b' or 'f'");
798
- const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 1), 1e3);
799
- const filter = parseRoomEventFilter(ctx);
800
- const fromRaw = ctx.query.get("from");
801
- const from = parsePageToken(fromRaw) ?? (dir === "b" ? {
802
- depth: null,
803
- stream: await hs.streams.position()
804
- } : null);
805
- const to = parsePageToken(ctx.query.get("to"));
806
- let { events, end } = await paginate(hs, s.userId, roomId, {
807
- from,
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
- const out = {
825
- chunk: events.map((e) => hs.rooms.formatEvent(e, viewerOf(s))),
826
- start: fromRaw ?? (from ? from.depth === null ? `s${from.stream}` : `t${from.depth}_${from.stream}` : "t0_0")
827
- };
828
- if (end !== null) out.end = end;
829
- else if (events.length) out.end = topoToken(events[events.length - 1]);
830
- if (filter?.lazy_load_members) {
831
- const current = await hs.rooms.currentState(roomId);
832
- out.state = [...new Set(events.map((e) => e.pdu.sender))].map((u) => getState(current, "m.room.member", u)).filter(Boolean).map((e) => toClientEvent(e));
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
- return out;
835
- });
836
- r.client("GET", "/rooms/:roomId/event/:eventId", async (ctx) => {
837
- const s = await ctx.auth();
838
- const hs = ctx.hs;
839
- await requireRoom(hs, ctx.params.roomId);
840
- const ev = await hs.rooms.getEvent(ctx.params.eventId);
841
- if (!ev || ev.pdu.room_id !== ctx.params.roomId) throw errors.notFound("Event not found");
842
- if ((await hs.rooms.filterVisible(s.userId, ctx.params.roomId, [ev])).length === 0) throw errors.notFound("Event not found");
843
- return hs.rooms.formatEvent(ev, viewerOf(s));
844
- });
845
- r.client("GET", "/rooms/:roomId/context/:eventId", async (ctx) => {
846
- const s = await ctx.auth();
847
- const hs = ctx.hs;
848
- const roomId = ctx.params.roomId;
849
- await requireRoom(hs, roomId);
850
- const ev = await hs.rooms.getEvent(ctx.params.eventId);
851
- if (!ev || ev.pdu.room_id !== roomId || (await hs.rooms.filterVisible(s.userId, roomId, [ev])).length === 0) throw errors.notFound("Event not found");
852
- const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 0), 100);
853
- const filter = parseRoomEventFilter(ctx);
854
- const here = {
855
- depth: ev.pdu.depth,
856
- stream: ev.stream
857
- };
858
- const before = await paginate(hs, s.userId, roomId, {
859
- from: here,
860
- to: null,
861
- dir: "b",
862
- limit: Math.floor(limit / 2) || (limit ? 1 : 0),
863
- filter
864
- });
865
- const after = await paginate(hs, s.userId, roomId, {
866
- from: here,
867
- to: null,
868
- dir: "f",
869
- limit: Math.ceil(limit / 2),
870
- filter
871
- });
872
- const beforeEvents = limit ? before.events : [];
873
- const afterEvents = limit ? after.events : [];
874
- const state = await hs.rooms.stateAtSnapshot(ev.snapshot);
875
- return {
876
- event: hs.rooms.formatEvent(ev, viewerOf(s)),
877
- events_before: beforeEvents.map((e) => hs.rooms.formatEvent(e, viewerOf(s))),
878
- events_after: afterEvents.map((e) => hs.rooms.formatEvent(e, viewerOf(s))),
879
- start: topoToken(beforeEvents.length ? beforeEvents[beforeEvents.length - 1] : ev),
880
- end: topoToken(afterEvents.length ? afterEvents[afterEvents.length - 1] : ev),
881
- state: [...state.values()].map((e) => toClientEvent(e))
882
- };
883
- });
884
- r.client("PUT", "/rooms/:roomId/redact/:eventId/:txnId", async (ctx) => {
885
- const s = await ctx.auth();
886
- const body = await ctx.json();
887
- const roomId = ctx.params.roomId;
888
- await requireRoom(ctx.hs, roomId);
889
- if (!await ownEvent(ctx.hs, s.userId, ctx.params.eventId)) await assertNotSuspended(ctx.hs, s, roomId);
890
- await ctx.hs.rooms.checkRedactionAllowed(roomId, s.userId, ctx.params.eventId);
891
- const content = {};
892
- if (typeof body.reason === "string") content.reason = body.reason;
893
- return { event_id: await ctx.hs.rooms.sendEvent(roomId, s.userId, {
894
- type: "m.room.redaction",
895
- content,
896
- redacts: ctx.params.eventId
897
- }, {
898
- txnId: ctx.params.txnId,
899
- deviceId: s.deviceId
900
- }) };
901
- });
902
- r.client("PUT", "/rooms/:roomId/typing/:userId", async (ctx) => {
903
- const s = await ctx.auth();
904
- if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot set typing for another user");
905
- const body = await ctx.json();
906
- if (typeof body.typing !== "boolean") throw errors.badJson("typing must be a boolean");
907
- await ctx.hs.rooms.requireJoined(ctx.params.roomId, s.userId);
908
- await ctx.hs.typing.setTyping(ctx.params.roomId, s.userId, body.typing, Number(body.timeout) || 3e4);
909
- return {};
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
- "m.read",
913
- "m.read.private",
914
- "m.fully_read"
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 === "m.fully_read") {
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: "m.fully_read",
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 === "m.read.private" ? [s.userId] : await hs.rooms.joinedMembers(roomId));
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: z.string(fail("M_INVALID_PARAM", "Invalid thread_id")).min(1, "M_INVALID_PARAM: Invalid thread_id").optional() });
943
- r.client("POST", "/rooms/:roomId/receipt/:receiptType/:eventId", { input: receiptBody }, async (ctx) => {
944
- const s = await ctx.auth();
945
- if (!RECEIPT_TYPES.includes(ctx.params.receiptType)) throw errors.invalidParam("Unknown receipt type");
946
- const body = ctx.input;
947
- await ctx.hs.rooms.requireJoined(ctx.params.roomId, s.userId);
948
- await setReceipt(ctx.hs, s, ctx.params.roomId, ctx.params.receiptType, ctx.params.eventId, body.thread_id);
949
- return {};
950
- });
951
- r.client("POST", "/rooms/:roomId/read_markers", { input: body({}) }, async (ctx) => {
952
- const s = await ctx.auth();
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
- return {};
960
- });
961
- r.client("GET", "/directory/room/:alias", async (ctx) => {
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", async (ctx) => {
971
- const s = await ctx.auth();
972
- const body = await ctx.json();
973
- const alias = ctx.params.alias;
974
- if (!isValidAlias(alias) || /\s/.test(alias)) throw errors.invalidParam("Invalid alias");
975
- if (parseId(alias, "#").server !== ctx.hs.serverName) throw errors.invalidParam("Alias must be on this server");
976
- if (!isValidRoomId(body.room_id)) throw errors.invalidParam("Invalid room_id");
977
- if (!await ctx.hs.rooms.exists(body.room_id)) throw errors.notFound("Room not found");
978
- const owner = ctx.hs.appservices.exclusiveAliasOwner(alias);
979
- if (owner && s.appservice !== owner) throw errors.exclusive("This alias is reserved by an application service");
980
- if (await ctx.hs.db.findOne("room_aliases", [{
981
- field: "alias",
982
- value: alias
983
- }])) throw new MatrixError(409, "M_UNKNOWN", "Room alias already exists");
984
- await ctx.hs.db.create("room_aliases", {
985
- alias,
986
- room_id: body.room_id,
987
- creator: s.userId
988
- });
989
- return {};
990
- });
991
- r.client("DELETE", "/directory/room/:alias", async (ctx) => {
992
- const s = await ctx.auth();
993
- const row = await ctx.hs.db.findOne("room_aliases", [{
994
- field: "alias",
995
- value: ctx.params.alias
996
- }]);
997
- if (!row) throw errors.notFound("Room alias not found");
998
- if (!await canDeleteAlias(ctx.hs, s, row)) throw errors.forbidden("You may not delete this alias");
999
- await ctx.hs.db.delete("room_aliases", [{
1000
- field: "alias",
1001
- value: ctx.params.alias
1002
- }]);
1003
- const canonical = getState(await ctx.hs.rooms.currentState(row.room_id), "m.room.canonical_alias");
1004
- if (canonical) {
1005
- const content = { ...canonical.content };
1006
- let changed = false;
1007
- if (content.alias === ctx.params.alias) {
1008
- delete content.alias;
1009
- changed = true;
1010
- }
1011
- if (Array.isArray(content.alt_aliases) && content.alt_aliases.includes(ctx.params.alias)) {
1012
- content.alt_aliases = content.alt_aliases.filter((a) => a !== ctx.params.alias);
1013
- changed = true;
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
- if (changed) await ctx.hs.rooms.sendEvent(row.room_id, s.userId, {
1016
- type: "m.room.canonical_alias",
1017
- state_key: "",
1018
- content
1019
- }).catch(() => {});
1106
+ return {};
1020
1107
  }
1021
- return {};
1022
- });
1023
- r.client("GET", "/rooms/:roomId/aliases", async (ctx) => {
1024
- const s = await ctx.auth();
1025
- await visibleState(ctx.hs, ctx.params.roomId, s.userId);
1026
- return { aliases: (await ctx.hs.db.findMany("room_aliases", { where: [{
1027
- field: "room_id",
1028
- value: ctx.params.roomId
1029
- }] })).map((r) => r.alias) };
1030
- });
1031
- r.client("GET", "/directory/list/room/:roomId", async (ctx) => {
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", async (ctx) => {
1040
- const s = await ctx.auth();
1041
- const body = await ctx.json();
1042
- if (!["public", "private"].includes(body.visibility)) throw errors.invalidParam("Invalid visibility");
1043
- const version = await ctx.hs.rooms.roomVersion(ctx.params.roomId);
1044
- if (!version) throw errors.notFound("Room not found");
1045
- const state = await ctx.hs.rooms.requireJoined(ctx.params.roomId, s.userId);
1046
- const pl = powerLevels(state, version);
1047
- if (!s.isAdmin && userLevel(pl, s.userId) < (pl.events["m.room.canonical_alias"] ?? pl.state_default)) throw errors.forbidden("Insufficient power level");
1048
- await ctx.hs.db.update("rooms", [{
1049
- field: "room_id",
1050
- value: ctx.params.roomId
1051
- }], { is_public: body.visibility === "public" });
1052
- return {};
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", { input: body({}) }, async (ctx) => {
1066
- await ctx.auth();
1067
- return remoteOrLocal(ctx, ctx.input);
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: z.string(fail("M_BAD_JSON", "Missing new_version")),
1071
- additional_creators: z.unknown().optional()
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: z.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(),
1080
- reason: z.string(fail("M_BAD_JSON", "reason must be a string")).optional()
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 = await ctx.auth();
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", { input: body({ reason: z.string(fail("M_BAD_JSON", "reason must be a string")) }) }, async (ctx) => {
1116
- const s = await ctx.auth();
1117
- const hs = ctx.hs;
1118
- const body = ctx.input;
1119
- const userId = ctx.params.userId;
1120
- if (!isValidUserId(userId)) throw errors.invalidParam("Invalid user ID");
1121
- if (hs.isLocalUser(userId) && !await hs.db.findOne("users", [{
1122
- field: "user_id",
1123
- value: userId
1124
- }])) throw errors.notFound("Unknown user");
1125
- const id = await hs.cluster.nextId("user_reports");
1126
- await hs.db.create("user_reports", {
1127
- id,
1128
- ts: Date.now(),
1129
- user_id: userId,
1130
- reporter: s.userId,
1131
- reason: body.reason,
1132
- resolved: false
1133
- });
1134
- await hs.audit.record({
1135
- action: "user_report",
1136
- actor: s.userId,
1137
- target: userId,
1138
- details: { report_id: id }
1139
- });
1140
- if (hs.adminRoom) hs.adminRoom.say(`New user report #${id} from ${s.userId} about ${userId}: ${body.reason || "(no reason)"}`).catch(() => {});
1141
- return {};
1142
- });
1143
- r.client("GET", "/mutual_rooms", async (ctx) => {
1144
- const s = await ctx.auth();
1145
- const other = ctx.query.get("user_id");
1146
- if (other === null) throw errors.missingParam("Missing user_id");
1147
- if (!isValidUserId(other)) throw errors.invalidParam("Invalid user_id");
1148
- if (other === s.userId) throw errors.invalidParam("user_id must not be your own");
1149
- const theirs = new Set(await ctx.hs.rooms.roomsForUser(other));
1150
- const joined = (await ctx.hs.rooms.roomsForUser(s.userId)).filter((r) => theirs.has(r)).sort();
1151
- const from = ctx.query.get("from");
1152
- const start = from === null ? 0 : joined.findIndex((r) => r > from);
1153
- if (from !== null && !isValidRoomId(from)) throw errors.invalidParam("Invalid from token");
1154
- const page = start < 0 ? [] : joined.slice(start, start + 100);
1155
- const out = {
1156
- joined: page,
1157
- count: joined.length
1158
- };
1159
- if (start >= 0 && start + 100 < joined.length) out.next_batch = page[page.length - 1];
1160
- return out;
1161
- }, ["v1"]);
1162
- r.client("POST", "/rooms/:roomId/report/:eventId", { input: reportBody }, report);
1163
- r.client("POST", "/rooms/:roomId/report", { input: reportBody }, report);
1164
- r.client("GET", "/admin/whois/:userId", async (ctx) => {
1165
- const s = await ctx.auth();
1166
- const userId = ctx.params.userId;
1167
- if (userId !== s.userId && !s.isAdmin) throw errors.forbidden("Only server admins can look up other users");
1168
- if (!ctx.hs.isLocalUser(userId)) throw errors.notFound("Unknown user");
1169
- const devices = await ctx.hs.db.findMany("devices", { where: [{
1170
- field: "user_id",
1171
- value: userId
1172
- }] });
1173
- const out = {};
1174
- for (const d of devices) out[d.device_id] = { sessions: [{ connections: d.last_seen_ip ? [{
1175
- ip: d.last_seen_ip,
1176
- last_seen: d.last_seen_ts
1177
- }] : [] }] };
1178
- return {
1179
- user_id: userId,
1180
- devices: out
1181
- };
1182
- });
1183
- r.client("PUT", "/directory/list/appservice/:networkId/:roomId", async (ctx) => {
1184
- if (!(await ctx.auth()).appservice) throw errors.forbidden("Only application services may use this endpoint");
1185
- const body = await ctx.json();
1186
- if (!["public", "private"].includes(body.visibility)) throw errors.invalidParam("Invalid visibility");
1187
- if (!await ctx.hs.rooms.exists(ctx.params.roomId)) throw errors.notFound("Room not found");
1188
- await ctx.hs.db.update("rooms", [{
1189
- field: "room_id",
1190
- value: ctx.params.roomId
1191
- }], { is_public: body.visibility === "public" });
1192
- return {};
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", async (ctx) => {
1200
- const s = await ctx.auth();
1201
- const hs = ctx.hs;
1202
- const roomId = ctx.params.roomId;
1203
- await visibleState(hs, roomId, s.userId);
1204
- const ts = Number(ctx.query.get("ts"));
1205
- const dir = ctx.query.get("dir");
1206
- if (!Number.isFinite(ts) || dir !== "f" && dir !== "b") throw errors.invalidParam("Invalid ts or dir");
1207
- let local = null;
1208
- const rows = await hs.db.findMany("events", { where: [{
1209
- field: "room_id",
1210
- value: roomId
1211
- }] });
1212
- 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);
1213
- for (const row of rows) {
1214
- const t = row.json.origin_server_ts;
1215
- if (!(dir === "f" ? t >= ts : t <= ts)) continue;
1216
- if ((await hs.rooms.filterVisible(s.userId, roomId, [hs.rooms.rowToStored(row)])).length) {
1217
- local = {
1218
- event_id: row.event_id,
1219
- origin_server_ts: t
1220
- };
1221
- break;
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
- if (hs.federation.enabled) for (const server of hs.federation.serversInRoom(await hs.rooms.currentState(roomId))) try {
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?.["m.relates_to"];
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?.["m.relates_to"];
1314
- if (!isObject(rel) || rel.rel_type !== "m.thread" || typeof rel.event_id !== "string") continue;
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
- "m.relations": {
1350
- ...ev.unsigned?.["m.relations"],
1351
- "m.thread": {
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)