matrix-nodejs-homeserver 0.0.0-stage → 0.2.0

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 (120) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +457 -2
  3. package/dist/adapters/auth.d.ts +50 -0
  4. package/dist/adapters/auth.js +18 -0
  5. package/dist/adapters/cluster.d.ts +55 -0
  6. package/dist/adapters/cluster.js +249 -0
  7. package/dist/adapters/media.d.ts +50 -0
  8. package/dist/adapters/media.js +63 -0
  9. package/dist/adapters/s3.d.ts +33 -0
  10. package/dist/adapters/s3.js +95 -0
  11. package/dist/api/account.js +560 -0
  12. package/dist/api/admin.js +251 -0
  13. package/dist/api/appservice.js +33 -0
  14. package/dist/api/backup.js +269 -0
  15. package/dist/api/federation.js +792 -0
  16. package/dist/api/filters.js +87 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +170 -0
  19. package/dist/api/media.js +255 -0
  20. package/dist/api/misc.js +86 -0
  21. package/dist/api/preview.js +200 -0
  22. package/dist/api/profile.js +165 -0
  23. package/dist/api/push.js +187 -0
  24. package/dist/api/relationships.js +264 -0
  25. package/dist/api/rooms.js +1364 -0
  26. package/dist/api/rtc.d.ts +24 -0
  27. package/dist/api/rtc.js +52 -0
  28. package/dist/api/search.js +155 -0
  29. package/dist/api/slidingsync.js +464 -0
  30. package/dist/api/spaces.js +172 -0
  31. package/dist/api/sync.js +600 -0
  32. package/dist/api/threadsubs.js +129 -0
  33. package/dist/api/threepid.js +122 -0
  34. package/dist/api/uia.js +126 -0
  35. package/dist/api/user.js +286 -0
  36. package/dist/core/admin.d.ts +85 -0
  37. package/dist/core/admin.js +307 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +482 -0
  40. package/dist/core/appservices.d.ts +73 -0
  41. package/dist/core/appservices.js +368 -0
  42. package/dist/core/audit.d.ts +51 -0
  43. package/dist/core/audit.js +86 -0
  44. package/dist/core/auth.d.ts +48 -0
  45. package/dist/core/auth.js +237 -0
  46. package/dist/core/authrules.d.ts +4 -0
  47. package/dist/core/authrules.js +347 -0
  48. package/dist/core/cluster.d.ts +66 -0
  49. package/dist/core/cluster.js +191 -0
  50. package/dist/core/crypto.d.ts +10 -0
  51. package/dist/core/crypto.js +100 -0
  52. package/dist/core/delayed.d.ts +39 -0
  53. package/dist/core/delayed.js +159 -0
  54. package/dist/core/errors.d.ts +9 -0
  55. package/dist/core/errors.js +45 -0
  56. package/dist/core/events.d.ts +32 -0
  57. package/dist/core/events.js +185 -0
  58. package/dist/core/glob.js +66 -0
  59. package/dist/core/identity.d.ts +24 -0
  60. package/dist/core/identity.js +96 -0
  61. package/dist/core/ids.js +36 -0
  62. package/dist/core/invitefilter.js +35 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +449 -0
  66. package/dist/core/notifier.d.ts +17 -0
  67. package/dist/core/notifier.js +54 -0
  68. package/dist/core/oauth.d.ts +38 -0
  69. package/dist/core/oauth.js +108 -0
  70. package/dist/core/partialstate.d.ts +32 -0
  71. package/dist/core/partialstate.js +197 -0
  72. package/dist/core/phone.js +268 -0
  73. package/dist/core/policy.d.ts +29 -0
  74. package/dist/core/policy.js +96 -0
  75. package/dist/core/profile.js +39 -0
  76. package/dist/core/push.d.ts +47 -0
  77. package/dist/core/push.js +306 -0
  78. package/dist/core/pushrules.d.ts +1 -0
  79. package/dist/core/pushrules.js +228 -0
  80. package/dist/core/ratelimit.d.ts +23 -0
  81. package/dist/core/ratelimit.js +77 -0
  82. package/dist/core/rooms.d.ts +215 -0
  83. package/dist/core/rooms.js +1729 -0
  84. package/dist/core/settings.d.ts +11 -0
  85. package/dist/core/settings.js +29 -0
  86. package/dist/core/sso.d.ts +46 -0
  87. package/dist/core/sso.js +165 -0
  88. package/dist/core/stateres.d.ts +4 -0
  89. package/dist/core/stateres.js +208 -0
  90. package/dist/core/threepid.d.ts +80 -0
  91. package/dist/core/threepid.js +275 -0
  92. package/dist/core/typing.d.ts +16 -0
  93. package/dist/core/typing.js +60 -0
  94. package/dist/federation/client.d.ts +51 -0
  95. package/dist/federation/client.js +278 -0
  96. package/dist/federation/index.d.ts +153 -0
  97. package/dist/federation/index.js +1058 -0
  98. package/dist/federation/keyring.d.ts +26 -0
  99. package/dist/federation/keyring.js +122 -0
  100. package/dist/federation/sender.d.ts +26 -0
  101. package/dist/federation/sender.js +164 -0
  102. package/dist/homeserver.d.ts +223 -0
  103. package/dist/homeserver.js +301 -0
  104. package/dist/http/adapters.d.ts +40 -0
  105. package/dist/http/adapters.js +130 -0
  106. package/dist/http/handler.d.ts +8 -0
  107. package/dist/http/handler.js +202 -0
  108. package/dist/http/input.js +24 -0
  109. package/dist/http/router.d.ts +60 -0
  110. package/dist/http/router.js +76 -0
  111. package/dist/index.d.ts +25 -0
  112. package/dist/index.js +14 -0
  113. package/dist/storage/memory.d.ts +5 -0
  114. package/dist/storage/memory.js +122 -0
  115. package/dist/storage/schema.d.ts +4 -0
  116. package/dist/storage/schema.js +664 -0
  117. package/dist/storage/sql.d.ts +72 -0
  118. package/dist/storage/sql.js +274 -0
  119. package/dist/storage/types.d.ts +55 -0
  120. package/package.json +67 -4
@@ -0,0 +1,1364 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { SUPPORTED_ROOM_VERSIONS, roomVersionFeatures, splitStateKey, toClientEvent } from "../core/events.js";
3
+ import { MatrixError, errors } from "../core/errors.js";
4
+ import { isValidAlias, isValidRoomId, isValidUserId, parseId } from "../core/ids.js";
5
+ import { getState, membershipOf, ownerOfStateKey, powerLevels, roomCreators, userLevel } from "../core/authrules.js";
6
+ import { validatePowerLevelContent } from "../core/rooms.js";
7
+ import { assertNotSuspended } from "../core/auth.js";
8
+ import { body, fail, lenient } from "../http/input.js";
9
+ import { matchesFilter, parseRoomEventFilter } from "./filters.js";
10
+ import { z } from "zod";
11
+ //#region src/api/rooms.ts
12
+ const parseToken = (token) => {
13
+ if (token === null || token === "") return null;
14
+ const m = /^[st]?(\d+)/.exec(token);
15
+ if (!m) throw errors.invalidParam("Invalid pagination token");
16
+ return Number(m[1]);
17
+ };
18
+ const viewerOf = (s) => ({
19
+ userId: s.userId,
20
+ deviceId: s.deviceId
21
+ });
22
+ async function requireRoom(hs, roomId) {
23
+ if (!isValidRoomId(roomId)) throw errors.invalidParam("Invalid room ID");
24
+ const version = await hs.rooms.roomVersion(roomId);
25
+ if (!version) throw errors.forbidden("Room not found or you are not a member");
26
+ return version;
27
+ }
28
+ /**
29
+ * State visible to the user: current state when joined (or world readable),
30
+ * otherwise the state at the moment they left.
31
+ */
32
+ async function visibleState(hs, roomId, userId) {
33
+ await requireRoom(hs, roomId);
34
+ const { joined, worldReadable, membership } = await hs.rooms.canPeek(roomId, userId);
35
+ if (joined || worldReadable) return hs.rooms.currentState(roomId);
36
+ if ((await hs.db.findOne("memberships", [{
37
+ field: "room_id",
38
+ value: roomId
39
+ }, {
40
+ field: "user_id",
41
+ value: userId
42
+ }]))?.forgotten) throw errors.forbidden("You have forgotten this room");
43
+ if (membership === "leave" || membership === "ban") {
44
+ const ev = await hs.rooms.membershipEvent(roomId, userId);
45
+ if (ev && ev.pdu.sender !== void 0) {
46
+ if (ev.pdu.unsigned?.prev_content?.membership === "join" || membership === "ban") return hs.rooms.stateAtSnapshot(ev.snapshot);
47
+ }
48
+ }
49
+ throw errors.forbidden("You are not a member of this room");
50
+ }
51
+ /** A gap event reached by backwards pagination (in the page, or anywhere older when the page ran out). */
52
+ async function findGapBefore(hs, roomId, events, end) {
53
+ if (!hs.federation.enabled) return null;
54
+ const inPage = events.find((e) => e.gap);
55
+ if (inPage) return inPage;
56
+ if (end !== null) return null;
57
+ const rows = await hs.db.findMany("events", {
58
+ where: [{
59
+ field: "room_id",
60
+ value: roomId
61
+ }, {
62
+ field: "gap",
63
+ value: true
64
+ }],
65
+ limit: 1
66
+ });
67
+ return rows[0] ? hs.rooms.rowToStored(rows[0]) : null;
68
+ }
69
+ /** Backfill the history behind a gap event, then clear its gap flag. */
70
+ async function backfillGap(hs, roomId, gapEvent) {
71
+ const fetched = await hs.federation.backfill(roomId, [gapEvent.pdu.event_id], 100);
72
+ await hs.db.update("events", [{
73
+ field: "event_id",
74
+ value: gapEvent.pdu.event_id
75
+ }], { gap: false });
76
+ hs.rooms.forgetEventCache(gapEvent.pdu.event_id);
77
+ return fetched > 0;
78
+ }
79
+ /** Backfill from other servers when our oldest event has unknown predecessors. Returns whether anything was fetched. */
80
+ async function backfillIfNeeded(hs, roomId) {
81
+ if (!hs.federation.enabled) return false;
82
+ const first = (await hs.db.findMany("events", {
83
+ where: [{
84
+ field: "room_id",
85
+ value: roomId
86
+ }],
87
+ sortBy: {
88
+ field: "stream",
89
+ direction: "asc"
90
+ },
91
+ limit: 1
92
+ }))[0];
93
+ if (!first || first.type === "m.room.create") return false;
94
+ const prevs = (first.json.prev_events ?? []).map((p) => Array.isArray(p) ? p[0] : p);
95
+ const known = await hs.rooms.getEvents(prevs);
96
+ if (prevs.every((p) => known.has(p))) return false;
97
+ return await hs.federation.backfill(roomId, [first.event_id], 50) > 0;
98
+ }
99
+ function parsePageToken(raw) {
100
+ if (raw === null || raw === "") return null;
101
+ const topo = /^t(-?\d+)_(-?\d+)$/.exec(raw);
102
+ if (topo) return {
103
+ depth: Number(topo[1]),
104
+ stream: Number(topo[2])
105
+ };
106
+ const stream = /^s?(-?\d+)/.exec(raw);
107
+ if (stream) return {
108
+ depth: null,
109
+ stream: Number(stream[1])
110
+ };
111
+ throw errors.invalidParam("Invalid pagination token");
112
+ }
113
+ const topoToken = (ev) => `t${ev.pdu.depth}_${ev.stream}`;
114
+ /** Topological comparison of (depth, stream) pairs. */
115
+ const cmp = (a, b) => a.depth - b.depth || a.stream - b.stream;
116
+ /**
117
+ * Fetch room events in topological order (as /messages requires), applying history
118
+ * visibility and filters, until `limit` events are found.
119
+ */
120
+ async function paginate(hs, userId, roomId, opts) {
121
+ const back = opts.dir === "b";
122
+ const out = [];
123
+ let cursor = opts.from && opts.from.depth !== null ? {
124
+ depth: opts.from.depth,
125
+ stream: opts.from.stream
126
+ } : null;
127
+ const streamBound = opts.from && opts.from.depth === null ? opts.from.stream : null;
128
+ const batch = Math.max(opts.limit * 2, 50);
129
+ const beyond = (pos) => {
130
+ if (!opts.to) return false;
131
+ if (opts.to.depth === null) return back ? pos.stream <= opts.to.stream : pos.stream > opts.to.stream;
132
+ const c = cmp(pos, {
133
+ depth: opts.to.depth,
134
+ stream: opts.to.stream
135
+ });
136
+ return back ? c <= 0 : c > 0;
137
+ };
138
+ for (;;) {
139
+ const where = [{
140
+ field: "room_id",
141
+ value: roomId
142
+ }];
143
+ if (cursor) where.push({
144
+ field: "depth",
145
+ operator: back ? "lte" : "gte",
146
+ value: cursor.depth
147
+ });
148
+ if (streamBound !== null) where.push({
149
+ field: "stream",
150
+ operator: back ? "lte" : "gt",
151
+ value: streamBound
152
+ });
153
+ let rows = await hs.db.findMany("events", {
154
+ where,
155
+ sortBy: {
156
+ field: "depth",
157
+ direction: back ? "desc" : "asc"
158
+ },
159
+ limit: batch
160
+ });
161
+ const full = rows.length === batch;
162
+ if (full) {
163
+ const edge = rows[rows.length - 1].depth;
164
+ rows = rows.filter((r) => r.depth !== edge);
165
+ rows.push(...await hs.db.findMany("events", { where: [...where, {
166
+ field: "depth",
167
+ value: edge
168
+ }] }));
169
+ }
170
+ const stored = rows.map((r) => hs.rooms.rowToStored(r)).map((ev) => ({
171
+ ev,
172
+ pos: {
173
+ depth: ev.pdu.depth,
174
+ stream: ev.stream
175
+ }
176
+ })).filter(({ pos }) => !cursor || (back ? cmp(pos, cursor) < 0 : cmp(pos, cursor) > 0)).sort((a, b) => back ? cmp(b.pos, a.pos) : cmp(a.pos, b.pos));
177
+ if (stored.length === 0) return {
178
+ events: out,
179
+ end: null
180
+ };
181
+ const visible = await hs.rooms.filterVisible(userId, roomId, stored.map((x) => x.ev));
182
+ const visibleById = new Map(visible.map((v) => [v.pdu.event_id, v]));
183
+ for (const { ev: raw, pos } of stored) {
184
+ if (beyond(pos)) return {
185
+ events: out,
186
+ end: null
187
+ };
188
+ cursor = pos;
189
+ const ev = visibleById.get(raw.pdu.event_id);
190
+ if (!ev || !matchesFilter(opts.filter, ev.pdu)) continue;
191
+ out.push(ev);
192
+ if (out.length >= opts.limit) return {
193
+ events: out,
194
+ end: `t${pos.depth}_${pos.stream}`
195
+ };
196
+ }
197
+ if (!full) return {
198
+ events: out,
199
+ end: null
200
+ };
201
+ }
202
+ }
203
+ /**
204
+ * Invite by email address or phone number (spec "Third-party invites"): invite the Matrix user the
205
+ * identity server knows for it, or store the invite there and emit an m.room.third_party_invite.
206
+ */
207
+ async function inviteByThreepid(ctx, s, body) {
208
+ const hs = ctx.hs;
209
+ const roomId = ctx.params.roomId;
210
+ for (const k of [
211
+ "id_server",
212
+ "id_access_token",
213
+ "medium",
214
+ "address"
215
+ ]) if (typeof body[k] !== "string") throw errors.missingParam(`Missing ${k}`);
216
+ await requireRoom(hs, roomId);
217
+ await hs.rooms.requireJoined(roomId, s.userId);
218
+ const mxid = await hs.identityServers.lookup(body.id_server, body.id_access_token, body.medium, body.address);
219
+ if (mxid) {
220
+ if (await hs.rooms.membership(roomId, mxid) === "ban") throw errors.forbidden("User is banned");
221
+ await hs.rooms.invite(roomId, s.userId, mxid);
222
+ return {};
223
+ }
224
+ const state = await hs.rooms.currentState(roomId);
225
+ const profile = await hs.profile(s.userId);
226
+ const create = getState(state, "m.room.create", "");
227
+ const invite = {
228
+ medium: body.medium,
229
+ address: body.address,
230
+ room_id: roomId,
231
+ 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,
236
+ room_type: create?.content?.type,
237
+ sender_display_name: profile.displayname,
238
+ sender_avatar_url: profile.avatar_url
239
+ };
240
+ for (const k of Object.keys(invite)) if (invite[k] === void 0 || invite[k] === null) delete invite[k];
241
+ const stored = await hs.identityServers.storeInvite(body.id_server, body.id_access_token, invite);
242
+ const [first] = stored.public_keys;
243
+ await hs.rooms.sendEvent(roomId, s.userId, {
244
+ type: "m.room.third_party_invite",
245
+ state_key: stored.token,
246
+ content: {
247
+ display_name: stored.display_name,
248
+ key_validity_url: first?.key_validity_url,
249
+ public_key: first?.public_key,
250
+ public_keys: stored.public_keys
251
+ }
252
+ });
253
+ return {};
254
+ }
255
+ async function ownEvent(hs, userId, eventId) {
256
+ return typeof eventId === "string" && (await hs.rooms.getPdu(eventId))?.sender === userId;
257
+ }
258
+ const membershipBody = body({
259
+ user_id: lenient(z.string()),
260
+ reason: lenient(z.string())
261
+ });
262
+ async function sendMembership(ctx, membership) {
263
+ const s = await ctx.auth();
264
+ const roomId = ctx.params.roomId;
265
+ if (membership !== "leave") await assertNotSuspended(ctx.hs, s, roomId);
266
+ const body = ctx.input;
267
+ const hs = ctx.hs;
268
+ const reason = body.reason;
269
+ if (membership === "leave") {
270
+ const row = await hs.db.findOne("memberships", [{
271
+ field: "room_id",
272
+ value: roomId
273
+ }, {
274
+ field: "user_id",
275
+ value: s.userId
276
+ }]);
277
+ if (!await hs.rooms.exists(roomId) && !row) throw errors.notFound("Room not found");
278
+ const current = await hs.rooms.serverIsJoined(roomId) ? await hs.rooms.membership(roomId, s.userId) : row?.membership ?? "leave";
279
+ if (current === "leave") return {};
280
+ if (current === "ban") throw errors.forbidden("You are banned from this room");
281
+ if (current === "invite" && await hs.admin.isNoticeRoom(roomId)) throw new MatrixError(403, "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM", "Server notice invites cannot be rejected");
282
+ await hs.rooms.setMembership(roomId, s.userId, s.userId, "leave", reason);
283
+ return {};
284
+ }
285
+ if (!body.user_id || !isValidUserId(body.user_id)) throw errors.invalidParam("Invalid user_id");
286
+ const target = body.user_id;
287
+ await requireRoom(hs, roomId);
288
+ if (membership === "unban") {
289
+ if (await hs.rooms.membership(roomId, target) !== "ban") throw errors.forbidden("User is not banned");
290
+ await hs.rooms.setMembership(roomId, s.userId, target, "leave", reason);
291
+ } else if (membership === "kick") {
292
+ const current = await hs.rooms.membership(roomId, target);
293
+ if (current === "leave" || current === "ban") throw errors.forbidden("User is not in the room");
294
+ await hs.rooms.setMembership(roomId, s.userId, target, "leave", reason);
295
+ } else await hs.rooms.setMembership(roomId, s.userId, target, "ban", reason);
296
+ await hs.audit.record({
297
+ action: membership === "kick" ? "member_kick" : membership === "ban" ? "member_ban" : "member_unban",
298
+ actor: s.userId,
299
+ target: body.user_id,
300
+ ip: ctx.ip,
301
+ details: {
302
+ room_id: roomId,
303
+ reason
304
+ }
305
+ });
306
+ return {};
307
+ }
308
+ async function validateStateEvent(hs, roomId, type, content, stateKeyValue = "") {
309
+ if (type === "m.room.create") throw errors.badJson("The create event cannot be sent");
310
+ 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") {
312
+ const version = await hs.rooms.roomVersion(roomId);
313
+ validatePowerLevelContent(content, roomCreators(await hs.rooms.currentState(roomId), version));
314
+ }
315
+ if (type === "m.room.canonical_alias") {
316
+ const aliases = [content.alias, ...Array.isArray(content.alt_aliases) ? content.alt_aliases : []].filter((a) => a !== void 0 && a !== null);
317
+ 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
+ for (const alias of aliases) {
319
+ if (!isValidAlias(alias)) throw new MatrixError(400, "M_INVALID_PARAM", "Invalid alias");
320
+ const row = await hs.db.findOne("room_aliases", [{
321
+ field: "alias",
322
+ value: alias
323
+ }]);
324
+ if (!row || row.room_id !== roomId) throw new MatrixError(400, "M_BAD_ALIAS", `Alias ${alias} does not point to this room`);
325
+ }
326
+ }
327
+ }
328
+ async function publicRoomChunk(hs, roomId) {
329
+ const state = await hs.rooms.currentState(roomId);
330
+ const get = (t) => getState(state, t)?.content;
331
+ const chunk = {
332
+ room_id: roomId,
333
+ 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"
337
+ };
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;
343
+ if (typeof roomType === "string") chunk.room_type = roomType;
344
+ return chunk;
345
+ }
346
+ async function publicRooms(ctx, body) {
347
+ const hs = ctx.hs;
348
+ const limit = Number(body.limit ?? ctx.query.get("limit") ?? 0) || void 0;
349
+ const since = body.since ?? ctx.query.get("since");
350
+ const offset = since ? Number.parseInt(String(since).replace(/^o/, ""), 10) || 0 : 0;
351
+ const term = isObject(body.filter) && typeof body.filter.generic_search_term === "string" ? body.filter.generic_search_term.toLowerCase() : null;
352
+ const roomTypes = isObject(body.filter) && Array.isArray(body.filter.room_types) ? body.filter.room_types : null;
353
+ const rooms = await hs.db.findMany("rooms", {
354
+ where: [{
355
+ field: "is_public",
356
+ value: true
357
+ }],
358
+ sortBy: {
359
+ field: "created_ts",
360
+ direction: "asc"
361
+ }
362
+ });
363
+ let chunks = await Promise.all(rooms.map((r) => publicRoomChunk(hs, r.room_id)));
364
+ if (term) chunks = chunks.filter((c) => [
365
+ c.name,
366
+ c.topic,
367
+ c.canonical_alias
368
+ ].some((v) => typeof v === "string" && v.toLowerCase().includes(term)));
369
+ if (roomTypes) chunks = chunks.filter((c) => roomTypes.includes(c.room_type ?? null));
370
+ chunks.sort((a, b) => b.num_joined_members - a.num_joined_members);
371
+ const out = {
372
+ chunk: limit ? chunks.slice(offset, offset + limit) : chunks.slice(offset),
373
+ total_room_count_estimate: chunks.length
374
+ };
375
+ if (limit && offset + limit < chunks.length) out.next_batch = `o${offset + limit}`;
376
+ if (offset > 0) out.prev_batch = `o${Math.max(0, offset - (limit ?? 0))}`;
377
+ return out;
378
+ }
379
+ async function canDeleteAlias(hs, s, alias) {
380
+ if (alias.creator === s.userId || s.isAdmin) return true;
381
+ const version = await hs.rooms.roomVersion(alias.room_id);
382
+ if (!version) return false;
383
+ const state = await hs.rooms.currentState(alias.room_id);
384
+ const pl = powerLevels(state, version);
385
+ const needed = pl.events["m.room.canonical_alias"] ?? pl.state_default;
386
+ return membershipOf(state, s.userId) === "join" && userLevel(pl, s.userId) >= needed;
387
+ }
388
+ async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
389
+ if (!SUPPORTED_ROOM_VERSIONS.includes(newVersion)) throw errors.unsupportedRoomVersion();
390
+ const version = await requireRoom(hs, roomId);
391
+ const state = await hs.rooms.requireJoined(roomId, s.userId);
392
+ const pl = powerLevels(state, version);
393
+ const tombstoneLevel = pl.events["m.room.tombstone"] ?? pl.state_default;
394
+ if (userLevel(pl, s.userId) < tombstoneLevel) throw errors.forbidden("You cannot upgrade this room");
395
+ const create = getState(state, "m.room.create");
396
+ const creationContent = {};
397
+ for (const k of ["m.federate", "type"]) if (create.content[k] !== void 0) creationContent[k] = create.content[k];
398
+ const toV12 = roomVersionFeatures(newVersion).v12;
399
+ if (toV12) {
400
+ creationContent.predecessor = { room_id: roomId };
401
+ if (additionalCreators !== void 0) creationContent.additional_creators = additionalCreators;
402
+ } else creationContent.predecessor = {
403
+ room_id: roomId,
404
+ event_id: (await hs.db.findMany("events", {
405
+ where: [{
406
+ field: "room_id",
407
+ value: roomId
408
+ }],
409
+ sortBy: {
410
+ field: "stream",
411
+ direction: "desc"
412
+ },
413
+ limit: 1
414
+ }))[0].event_id
415
+ };
416
+ 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"
426
+ ];
427
+ const initialState = [];
428
+ for (const t of copyTypes) {
429
+ const ev = getState(state, t);
430
+ if (ev && t !== "m.room.power_levels") initialState.push({
431
+ type: t,
432
+ state_key: "",
433
+ content: ev.content
434
+ });
435
+ }
436
+ let oldPl = structuredClone(getState(state, "m.room.power_levels")?.content ?? {});
437
+ const isPublic = (await hs.db.findOne("rooms", [{
438
+ field: "room_id",
439
+ value: roomId
440
+ }]))?.is_public;
441
+ let override;
442
+ if (toV12) {
443
+ const creators = /* @__PURE__ */ new Set([s.userId, ...Array.isArray(additionalCreators) ? additionalCreators : []]);
444
+ const users = { ...oldPl.users };
445
+ for (const u of creators) delete users[u];
446
+ oldPl = {
447
+ ...oldPl,
448
+ users
449
+ };
450
+ override = oldPl;
451
+ } else {
452
+ const level = userLevel(pl, s.userId);
453
+ override = {
454
+ ...oldPl,
455
+ users: {
456
+ ...oldPl.users,
457
+ [s.userId]: Number.isFinite(level) ? Math.max(level, 100) : 100
458
+ }
459
+ };
460
+ }
461
+ const { room_id: newRoomId } = await hs.rooms.createRoom(s.userId, {
462
+ room_version: newVersion,
463
+ creation_content: creationContent,
464
+ initial_state: initialState,
465
+ power_level_content_override: override,
466
+ preset: "private_chat",
467
+ visibility: isPublic ? "public" : "private"
468
+ });
469
+ if (getState(state, "m.room.power_levels") && !toV12) try {
470
+ await hs.rooms.sendEvent(newRoomId, s.userId, {
471
+ type: "m.room.power_levels",
472
+ state_key: "",
473
+ content: oldPl
474
+ });
475
+ } catch {}
476
+ const aliases = await hs.db.findMany("room_aliases", { where: [{
477
+ field: "room_id",
478
+ value: roomId
479
+ }] });
480
+ for (const a of aliases) await hs.db.update("room_aliases", [{
481
+ field: "alias",
482
+ value: a.alias
483
+ }], { room_id: newRoomId });
484
+ const canonical = getState(state, "m.room.canonical_alias");
485
+ if (canonical && Object.keys(canonical.content).length) {
486
+ await hs.rooms.sendEvent(roomId, s.userId, {
487
+ type: "m.room.canonical_alias",
488
+ state_key: "",
489
+ content: {}
490
+ }).catch(() => {});
491
+ await hs.rooms.sendEvent(newRoomId, s.userId, {
492
+ type: "m.room.canonical_alias",
493
+ state_key: "",
494
+ content: canonical.content
495
+ }).catch(() => {});
496
+ }
497
+ if (isPublic) await hs.db.update("rooms", [{
498
+ field: "room_id",
499
+ value: roomId
500
+ }], { is_public: false });
501
+ await hs.rooms.sendEvent(roomId, s.userId, {
502
+ type: "m.room.tombstone",
503
+ state_key: "",
504
+ content: {
505
+ body: "This room has been replaced",
506
+ replacement_room: newRoomId
507
+ }
508
+ });
509
+ const restricted = {
510
+ ...oldPl,
511
+ events_default: Math.max(50, oldPl.events_default ?? 0),
512
+ invite: Math.max(50, oldPl.invite ?? 0)
513
+ };
514
+ await hs.rooms.sendEvent(roomId, s.userId, {
515
+ type: "m.room.power_levels",
516
+ state_key: "",
517
+ content: restricted
518
+ }).catch(() => {});
519
+ return newRoomId;
520
+ }
521
+ 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
+ });
540
+ const joinBody = body({
541
+ reason: lenient(z.string()),
542
+ third_party_signed: z.unknown().optional()
543
+ });
544
+ const join = async (ctx, idOrAlias) => {
545
+ const s = await ctx.auth();
546
+ await ctx.rateLimit("join");
547
+ const body = ctx.input;
548
+ const { roomId, servers: resolvedServers } = await ctx.hs.rooms.resolveRoom(idOrAlias);
549
+ await assertNotSuspended(ctx.hs, s, roomId);
550
+ const { reason, membership, join_authorised_via_users_server, third_party_signed, ...custom } = body;
551
+ const servers = [
552
+ ...ctx.query.getAll("via"),
553
+ ...ctx.query.getAll("server_name"),
554
+ ...resolvedServers
555
+ ];
556
+ if (isObject(third_party_signed)) {
557
+ const signed = third_party_signed;
558
+ if (typeof signed.sender !== "string" || signed.mxid !== s.userId || typeof signed.token !== "string") throw errors.badJson("Invalid third_party_signed");
559
+ const sender = signed.sender;
560
+ const proof = signed;
561
+ if (await ctx.hs.rooms.membership(roomId, s.userId) !== "invite") {
562
+ const event = {
563
+ type: "m.room.member",
564
+ room_id: roomId,
565
+ sender,
566
+ state_key: s.userId,
567
+ content: {
568
+ membership: "invite",
569
+ third_party_invite: { signed: proof }
570
+ }
571
+ };
572
+ if (ctx.hs.isLocalUser(sender)) {
573
+ const pending = getState(await ctx.hs.rooms.currentState(roomId), "m.room.third_party_invite", signed.token);
574
+ if (!pending || pending.sender !== sender) throw errors.forbidden("No such third-party invite");
575
+ await ctx.hs.rooms.invite(roomId, sender, s.userId, { thirdPartyInvite: {
576
+ display_name: pending.content?.display_name ?? "",
577
+ signed: proof
578
+ } });
579
+ } else await ctx.hs.federation.client.request(sender.slice(sender.indexOf(":") + 1), "PUT", `/_matrix/federation/v1/exchange_third_party_invite/${encodeURIComponent(roomId)}`, { body: event });
580
+ }
581
+ }
582
+ await ctx.hs.rooms.join(s.userId, roomId, {
583
+ reason,
584
+ content: custom,
585
+ servers
586
+ });
587
+ return { room_id: roomId };
588
+ };
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
+ });
604
+ 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
+ }) };
672
+ });
673
+ const putState = async (ctx) => {
674
+ const s = await ctx.auth();
675
+ const content = await ctx.json();
676
+ const roomId = ctx.params.roomId;
677
+ await requireRoom(ctx.hs, roomId);
678
+ const type = ctx.params.eventType;
679
+ const key = ctx.params.stateKey ?? "";
680
+ if (!(type === "m.room.member" && key === s.userId && content?.membership === "leave")) await assertNotSuspended(ctx.hs, s, roomId);
681
+ await validateStateEvent(ctx.hs, roomId, type, content, key);
682
+ const delay = ctx.query.get("org.matrix.msc4140.delay");
683
+ if (delay !== null) return { delay_id: await ctx.hs.delayedEvents.schedule({
684
+ userId: s.userId,
685
+ deviceId: s.deviceId,
686
+ roomId,
687
+ type,
688
+ stateKey: key,
689
+ content,
690
+ delayMs: Number(delay)
691
+ }) };
692
+ const current = getState(await ctx.hs.rooms.currentState(roomId), type, key);
693
+ if (current && current.sender === s.userId && JSON.stringify(current.content) === JSON.stringify(content)) return { event_id: current.event_id };
694
+ const ts = s.appservice && ctx.query.get("ts") ? Number(ctx.query.get("ts")) : void 0;
695
+ return { event_id: await ctx.hs.rooms.sendEvent(roomId, s.userId, {
696
+ type,
697
+ state_key: key,
698
+ content
699
+ }, { ts }) };
700
+ };
701
+ r.client("PUT", "/rooms/:roomId/state/:eventType", putState);
702
+ r.client("PUT", "/rooms/:roomId/state/:eventType/:stateKey", putState);
703
+ const getStateEvent = async (ctx) => {
704
+ const s = await ctx.auth();
705
+ const state = await visibleState(ctx.hs, ctx.params.roomId, s.userId);
706
+ const ev = getState(state, ctx.params.eventType, ctx.params.stateKey ?? "");
707
+ if (!ev) throw errors.notFound("State event not found");
708
+ if (ctx.query.get("format") === "event") return ctx.hs.rooms.formatEvent({
709
+ pdu: ev,
710
+ stream: 0,
711
+ snapshot: 0,
712
+ txnId: null,
713
+ txnToken: null
714
+ });
715
+ return ev.content;
716
+ };
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", {
748
+ where: [{
749
+ field: "room_id",
750
+ value: ctx.params.roomId
751
+ }],
752
+ sortBy: {
753
+ field: "stream",
754
+ direction: "asc"
755
+ },
756
+ limit: 2
757
+ });
758
+ if (first.length) state = await hs.rooms.stateAtSnapshot(first[first.length - 1].state_snapshot);
759
+ }
760
+ }
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
+ };
788
+ }
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, {
817
+ from,
818
+ to,
819
+ dir,
820
+ limit: filter?.limit ?? limit,
821
+ filter
822
+ }));
823
+ }
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));
833
+ }
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
+ });
911
+ const RECEIPT_TYPES = [
912
+ "m.read",
913
+ "m.read.private",
914
+ "m.fully_read"
915
+ ];
916
+ const setReceipt = async (hs, s, roomId, type, eventId, threadId) => {
917
+ if (type === "m.fully_read") {
918
+ await hs.streams.write((stream) => hs.db.upsert("account_data", {
919
+ user_id: s.userId,
920
+ room_id: roomId,
921
+ type: "m.fully_read",
922
+ content: { event_id: eventId },
923
+ stream
924
+ }));
925
+ hs.notifier.notify([s.userId]);
926
+ return;
927
+ }
928
+ const ev = await hs.rooms.getEvent(eventId);
929
+ if (!ev || ev.pdu.room_id !== roomId) throw errors.notFound("Event not found");
930
+ await hs.streams.write((stream) => hs.db.upsert("receipts", {
931
+ room_id: roomId,
932
+ user_id: s.userId,
933
+ receipt_type: type,
934
+ thread_id: threadId ?? "",
935
+ event_id: eventId,
936
+ ts: Date.now(),
937
+ stream
938
+ }));
939
+ hs.notifier.notify(type === "m.read.private" ? [s.userId] : await hs.rooms.joinedMembers(roomId));
940
+ await hs.federation.onReceipt(roomId, s.userId, type, eventId, Date.now(), threadId ?? "").catch(() => {});
941
+ };
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);
958
+ }
959
+ return {};
960
+ });
961
+ r.client("GET", "/directory/room/:alias", async (ctx) => {
962
+ if (!isValidAlias(ctx.params.alias)) throw errors.invalidParam("Invalid alias");
963
+ const { roomId, servers } = await ctx.hs.rooms.resolveRoom(ctx.params.alias);
964
+ const inRoom = await ctx.hs.rooms.exists(roomId) ? [...await ctx.hs.federation.roomDestinations(roomId, await ctx.hs.rooms.currentState(roomId))] : [];
965
+ return {
966
+ room_id: roomId,
967
+ servers: [.../* @__PURE__ */ new Set([...servers, ...inRoom])]
968
+ };
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;
1014
+ }
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(() => {});
1020
+ }
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) => {
1032
+ const row = await ctx.hs.db.findOne("rooms", [{
1033
+ field: "room_id",
1034
+ value: ctx.params.roomId
1035
+ }]);
1036
+ if (!row) throw errors.notFound("Room not found");
1037
+ 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
+ });
1054
+ const remoteOrLocal = async (ctx, body) => {
1055
+ const server = ctx.query.get("server");
1056
+ if (server && server !== ctx.hs.serverName) {
1057
+ const query = { ...body };
1058
+ if (ctx.query.get("limit")) query.limit = Number(ctx.query.get("limit"));
1059
+ if (ctx.query.get("since")) query.since = ctx.query.get("since");
1060
+ return ctx.hs.federation.queryPublicRooms(server, query);
1061
+ }
1062
+ return publicRooms(ctx, body);
1063
+ };
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
+ });
1069
+ 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) };
1077
+ });
1078
+ 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()
1081
+ });
1082
+ const report = async (ctx) => {
1083
+ const s = await ctx.auth();
1084
+ const hs = ctx.hs;
1085
+ const roomId = ctx.params.roomId;
1086
+ await requireRoom(hs, roomId);
1087
+ if (ctx.params.eventId) {
1088
+ const ev = await hs.rooms.getEvent(ctx.params.eventId);
1089
+ if (!ev || ev.pdu.room_id !== roomId || !(await hs.rooms.filterVisible(s.userId, roomId, [ev])).length) throw errors.notFound("Event not found");
1090
+ } else if (await hs.rooms.membership(roomId, s.userId) !== "join") throw errors.notFound("Room not found");
1091
+ const body = ctx.input;
1092
+ const id = await hs.cluster.nextId("event_reports");
1093
+ await hs.db.create("event_reports", {
1094
+ id,
1095
+ ts: Date.now(),
1096
+ room_id: roomId,
1097
+ event_id: ctx.params.eventId ?? null,
1098
+ reporter: s.userId,
1099
+ reason: body.reason ?? null,
1100
+ score: body.score ?? null,
1101
+ resolved: false
1102
+ });
1103
+ await hs.audit.record({
1104
+ action: "event_report",
1105
+ actor: s.userId,
1106
+ target: ctx.params.eventId ?? roomId,
1107
+ details: {
1108
+ room_id: roomId,
1109
+ report_id: id
1110
+ }
1111
+ });
1112
+ 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
+ return {};
1114
+ };
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
+ });
1194
+ 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;
1222
+ }
1223
+ }
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"]);
1245
+ }
1246
+ async function relations(ctx) {
1247
+ const s = await ctx.auth();
1248
+ const hs = ctx.hs;
1249
+ const roomId = ctx.params.roomId;
1250
+ await visibleState(hs, roomId, s.userId);
1251
+ const parent = await hs.rooms.getEvent(ctx.params.eventId);
1252
+ if (!parent || parent.pdu.room_id !== roomId) throw errors.notFound("Event not found");
1253
+ const dir = ctx.query.get("dir") === "f" ? "f" : "b";
1254
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "50", 10) || 50, 1), 1e3);
1255
+ const from = parseToken(ctx.query.get("from"));
1256
+ const rows = await hs.db.findMany("events", {
1257
+ where: [{
1258
+ field: "room_id",
1259
+ value: roomId
1260
+ }, ...from !== null ? [{
1261
+ field: "stream",
1262
+ operator: dir === "b" ? "lte" : "gt",
1263
+ value: from
1264
+ }] : []],
1265
+ sortBy: {
1266
+ field: "stream",
1267
+ direction: dir === "b" ? "desc" : "asc"
1268
+ }
1269
+ });
1270
+ const matches = [];
1271
+ let next = null;
1272
+ for (const row of rows) {
1273
+ const rel = row.json.content?.["m.relates_to"];
1274
+ if (!isObject(rel) || rel.event_id !== ctx.params.eventId) continue;
1275
+ if (ctx.params.relType && rel.rel_type !== ctx.params.relType) continue;
1276
+ if (ctx.params.eventType && row.type !== ctx.params.eventType) continue;
1277
+ const stored = hs.rooms.rowToStored(row);
1278
+ if ((await hs.rooms.filterVisible(s.userId, roomId, [stored])).length === 0) continue;
1279
+ if (matches.length >= limit) {
1280
+ next = dir === "b" ? matches[matches.length - 1].stream - 1 : matches[matches.length - 1].stream;
1281
+ break;
1282
+ }
1283
+ matches.push(stored);
1284
+ }
1285
+ const out = { chunk: matches.map((e) => hs.rooms.formatEvent(e, {
1286
+ userId: s.userId,
1287
+ deviceId: s.deviceId
1288
+ })) };
1289
+ if (next !== null) out.next_batch = `s${next}`;
1290
+ return out;
1291
+ }
1292
+ /** `GET /rooms/{roomId}/threads`: thread roots, most recently active first. */
1293
+ async function threads(ctx) {
1294
+ const s = await ctx.auth();
1295
+ const hs = ctx.hs;
1296
+ const roomId = ctx.params.roomId;
1297
+ await visibleState(hs, roomId, s.userId);
1298
+ const include = ctx.query.get("include") === "participated" ? "participated" : "all";
1299
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "50", 10) || 50, 1), 1e3);
1300
+ const from = parseToken(ctx.query.get("from"));
1301
+ const rows = await hs.db.findMany("events", {
1302
+ where: [{
1303
+ field: "room_id",
1304
+ value: roomId
1305
+ }],
1306
+ sortBy: {
1307
+ field: "stream",
1308
+ direction: "desc"
1309
+ }
1310
+ });
1311
+ const latest = /* @__PURE__ */ new Map();
1312
+ 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;
1315
+ const entry = latest.get(rel.event_id) ?? {
1316
+ stream: row.stream,
1317
+ latestId: row.event_id,
1318
+ count: 0,
1319
+ participants: /* @__PURE__ */ new Set()
1320
+ };
1321
+ entry.participants.add(row.sender);
1322
+ entry.count++;
1323
+ latest.set(rel.event_id, entry);
1324
+ }
1325
+ const roots = [];
1326
+ for (const [rootId, info] of latest) {
1327
+ const root = await hs.rooms.getEvent(rootId);
1328
+ if (!root || root.pdu.room_id !== roomId) continue;
1329
+ if (include === "participated" && root.pdu.sender !== s.userId && !info.participants.has(s.userId)) continue;
1330
+ if ((await hs.rooms.filterVisible(s.userId, roomId, [root])).length === 0) continue;
1331
+ roots.push({
1332
+ ev: root,
1333
+ stream: info.stream
1334
+ });
1335
+ }
1336
+ roots.sort((a, b) => b.stream - a.stream);
1337
+ const page = roots.filter((r) => from === null || r.stream <= from);
1338
+ const chunk = page.slice(0, limit);
1339
+ const viewer = {
1340
+ userId: s.userId,
1341
+ deviceId: s.deviceId
1342
+ };
1343
+ const out = { chunk: await Promise.all(chunk.map(async (r) => {
1344
+ const ev = hs.rooms.formatEvent(r.ev, viewer);
1345
+ const info = latest.get(r.ev.pdu.event_id);
1346
+ const latestEv = await hs.rooms.getEvent(info.latestId);
1347
+ ev.unsigned = {
1348
+ ...ev.unsigned,
1349
+ "m.relations": {
1350
+ ...ev.unsigned?.["m.relations"],
1351
+ "m.thread": {
1352
+ latest_event: latestEv ? hs.rooms.formatEvent(latestEv, viewer) : void 0,
1353
+ count: info.count,
1354
+ current_user_participated: r.ev.pdu.sender === s.userId || info.participants.has(s.userId)
1355
+ }
1356
+ }
1357
+ };
1358
+ return ev;
1359
+ })) };
1360
+ if (page.length > limit) out.next_batch = `s${page[limit].stream}`;
1361
+ return out;
1362
+ }
1363
+ //#endregion
1364
+ export { paginate, parsePageToken, parseToken, registerRoomRoutes, topoToken, visibleState };