matrix-nodejs-homeserver 0.0.0-stage → 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 (141) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +479 -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 +594 -0
  12. package/dist/api/admin.js +267 -0
  13. package/dist/api/appservice.js +45 -0
  14. package/dist/api/backup.js +280 -0
  15. package/dist/api/federation.js +826 -0
  16. package/dist/api/filters.js +88 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +183 -0
  19. package/dist/api/media.js +271 -0
  20. package/dist/api/misc.js +95 -0
  21. package/dist/api/preview.js +201 -0
  22. package/dist/api/profile.js +176 -0
  23. package/dist/api/push.js +202 -0
  24. package/dist/api/relationships.js +270 -0
  25. package/dist/api/rooms.js +1487 -0
  26. package/dist/api/rtc.d.ts +23 -0
  27. package/dist/api/rtc.js +66 -0
  28. package/dist/api/search.js +161 -0
  29. package/dist/api/slidingsync.js +474 -0
  30. package/dist/api/spaces.js +177 -0
  31. package/dist/api/sync.js +609 -0
  32. package/dist/api/threadsubs.js +140 -0
  33. package/dist/api/threepid.js +155 -0
  34. package/dist/api/uia.js +127 -0
  35. package/dist/api/user.js +329 -0
  36. package/dist/core/admin.d.ts +84 -0
  37. package/dist/core/admin.js +308 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +483 -0
  40. package/dist/core/appservices.d.ts +74 -0
  41. package/dist/core/appservices.js +369 -0
  42. package/dist/core/audit.d.ts +50 -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 +348 -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 +160 -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 +186 -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 +36 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +450 -0
  66. package/dist/core/m.d.ts +209 -0
  67. package/dist/core/m.js +182 -0
  68. package/dist/core/notifier.d.ts +17 -0
  69. package/dist/core/notifier.js +54 -0
  70. package/dist/core/oauth.d.ts +38 -0
  71. package/dist/core/oauth.js +108 -0
  72. package/dist/core/partialstate.d.ts +32 -0
  73. package/dist/core/partialstate.js +198 -0
  74. package/dist/core/phone.js +268 -0
  75. package/dist/core/policy.d.ts +29 -0
  76. package/dist/core/policy.js +97 -0
  77. package/dist/core/profile.js +39 -0
  78. package/dist/core/push.d.ts +46 -0
  79. package/dist/core/push.js +307 -0
  80. package/dist/core/pushrules.js +229 -0
  81. package/dist/core/ratelimit.d.ts +23 -0
  82. package/dist/core/ratelimit.js +77 -0
  83. package/dist/core/rooms.d.ts +215 -0
  84. package/dist/core/rooms.js +1731 -0
  85. package/dist/core/settings.d.ts +11 -0
  86. package/dist/core/settings.js +29 -0
  87. package/dist/core/sso.d.ts +46 -0
  88. package/dist/core/sso.js +165 -0
  89. package/dist/core/stateres.d.ts +4 -0
  90. package/dist/core/stateres.js +209 -0
  91. package/dist/core/threepid.d.ts +80 -0
  92. package/dist/core/threepid.js +275 -0
  93. package/dist/core/typing.d.ts +16 -0
  94. package/dist/core/typing.js +60 -0
  95. package/dist/federation/client.d.ts +51 -0
  96. package/dist/federation/client.js +279 -0
  97. package/dist/federation/index.d.ts +153 -0
  98. package/dist/federation/index.js +1059 -0
  99. package/dist/federation/keyring.d.ts +26 -0
  100. package/dist/federation/keyring.js +122 -0
  101. package/dist/federation/sender.d.ts +26 -0
  102. package/dist/federation/sender.js +164 -0
  103. package/dist/homeserver.d.ts +224 -0
  104. package/dist/homeserver.js +302 -0
  105. package/dist/http/adapters.d.ts +40 -0
  106. package/dist/http/adapters.js +130 -0
  107. package/dist/http/handler.d.ts +8 -0
  108. package/dist/http/handler.js +181 -0
  109. package/dist/http/input.js +24 -0
  110. package/dist/http/procedure.js +25 -0
  111. package/dist/http/router.d.ts +43 -0
  112. package/dist/http/router.js +56 -0
  113. package/dist/index.d.ts +26 -0
  114. package/dist/index.js +15 -0
  115. package/dist/storage/memory.d.ts +5 -0
  116. package/dist/storage/memory.js +122 -0
  117. package/dist/storage/schema.d.ts +4 -0
  118. package/dist/storage/schema.js +664 -0
  119. package/dist/storage/sql.d.ts +72 -0
  120. package/dist/storage/sql.js +274 -0
  121. package/dist/storage/types.d.ts +55 -0
  122. package/dist/vendor/zod/LICENSE +21 -0
  123. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  124. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  125. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  126. package/dist/vendor/zod/v4/core/api.js +512 -0
  127. package/dist/vendor/zod/v4/core/checks.js +300 -0
  128. package/dist/vendor/zod/v4/core/core.js +109 -0
  129. package/dist/vendor/zod/v4/core/doc.js +35 -0
  130. package/dist/vendor/zod/v4/core/errors.js +118 -0
  131. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  132. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  133. package/dist/vendor/zod/v4/core/parse.js +206 -0
  134. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  135. package/dist/vendor/zod/v4/core/registries.js +48 -0
  136. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  137. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  138. package/dist/vendor/zod/v4/core/util.js +537 -0
  139. package/dist/vendor/zod/v4/core/versions.js +8 -0
  140. package/dist/vendor/zod/v4/locales/en.js +110 -0
  141. package/package.json +65 -4
@@ -0,0 +1,1487 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { M, UNSTABLE } from "../core/m.js";
3
+ import { SUPPORTED_ROOM_VERSIONS, roomVersionFeatures, splitStateKey, toClientEvent } from "../core/events.js";
4
+ import { MatrixError, errors } from "../core/errors.js";
5
+ import { isValidAlias, isValidRoomId, isValidUserId, parseId } from "../core/ids.js";
6
+ import { getState, membershipOf, ownerOfStateKey, powerLevels, roomCreators, userLevel } from "../core/authrules.js";
7
+ import { validatePowerLevelContent } from "../core/rooms.js";
8
+ import { assertNotSuspended } from "../core/auth.js";
9
+ import { number, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
10
+ import { body, fail, lenient } from "../http/input.js";
11
+ import { procedure } from "../http/procedure.js";
12
+ import { matchesFilter, parseRoomEventFilter } from "./filters.js";
13
+ //#region src/api/rooms.ts
14
+ const parseToken = (token) => {
15
+ if (token === null || token === "") return null;
16
+ const m = /^[st]?(\d+)/.exec(token);
17
+ if (!m) throw errors.invalidParam("Invalid pagination token");
18
+ return Number(m[1]);
19
+ };
20
+ const viewerOf = (s) => ({
21
+ userId: s.userId,
22
+ deviceId: s.deviceId
23
+ });
24
+ async function requireRoom(hs, roomId) {
25
+ if (!isValidRoomId(roomId)) throw errors.invalidParam("Invalid room ID");
26
+ const version = await hs.rooms.roomVersion(roomId);
27
+ if (!version) throw errors.forbidden("Room not found or you are not a member");
28
+ return version;
29
+ }
30
+ /**
31
+ * State visible to the user: current state when joined (or world readable),
32
+ * otherwise the state at the moment they left.
33
+ */
34
+ async function visibleState(hs, roomId, userId) {
35
+ await requireRoom(hs, roomId);
36
+ const { joined, worldReadable, membership } = await hs.rooms.canPeek(roomId, userId);
37
+ if (joined || worldReadable) return hs.rooms.currentState(roomId);
38
+ if ((await hs.db.findOne("memberships", [{
39
+ field: "room_id",
40
+ value: roomId
41
+ }, {
42
+ field: "user_id",
43
+ value: userId
44
+ }]))?.forgotten) throw errors.forbidden("You have forgotten this room");
45
+ if (membership === "leave" || membership === "ban") {
46
+ const ev = await hs.rooms.membershipEvent(roomId, userId);
47
+ if (ev && ev.pdu.sender !== void 0) {
48
+ if (ev.pdu.unsigned?.prev_content?.membership === "join" || membership === "ban") return hs.rooms.stateAtSnapshot(ev.snapshot);
49
+ }
50
+ }
51
+ throw errors.forbidden("You are not a member of this room");
52
+ }
53
+ /** A gap event reached by backwards pagination (in the page, or anywhere older when the page ran out). */
54
+ async function findGapBefore(hs, roomId, events, end) {
55
+ if (!hs.federation.enabled) return null;
56
+ const inPage = events.find((e) => e.gap);
57
+ if (inPage) return inPage;
58
+ if (end !== null) return null;
59
+ const rows = await hs.db.findMany("events", {
60
+ where: [{
61
+ field: "room_id",
62
+ value: roomId
63
+ }, {
64
+ field: "gap",
65
+ value: true
66
+ }],
67
+ limit: 1
68
+ });
69
+ return rows[0] ? hs.rooms.rowToStored(rows[0]) : null;
70
+ }
71
+ /** Backfill the history behind a gap event, then clear its gap flag. */
72
+ async function backfillGap(hs, roomId, gapEvent) {
73
+ const fetched = await hs.federation.backfill(roomId, [gapEvent.pdu.event_id], 100);
74
+ await hs.db.update("events", [{
75
+ field: "event_id",
76
+ value: gapEvent.pdu.event_id
77
+ }], { gap: false });
78
+ hs.rooms.forgetEventCache(gapEvent.pdu.event_id);
79
+ return fetched > 0;
80
+ }
81
+ /** Backfill from other servers when our oldest event has unknown predecessors. Returns whether anything was fetched. */
82
+ async function backfillIfNeeded(hs, roomId) {
83
+ if (!hs.federation.enabled) return false;
84
+ const first = (await hs.db.findMany("events", {
85
+ where: [{
86
+ field: "room_id",
87
+ value: roomId
88
+ }],
89
+ sortBy: {
90
+ field: "stream",
91
+ direction: "asc"
92
+ },
93
+ limit: 1
94
+ }))[0];
95
+ if (!first || first.type === M.ROOM.CREATE) return false;
96
+ const prevs = (first.json.prev_events ?? []).map((p) => Array.isArray(p) ? p[0] : p);
97
+ const known = await hs.rooms.getEvents(prevs);
98
+ if (prevs.every((p) => known.has(p))) return false;
99
+ return await hs.federation.backfill(roomId, [first.event_id], 50) > 0;
100
+ }
101
+ function parsePageToken(raw) {
102
+ if (raw === null || raw === "") return null;
103
+ const topo = /^t(-?\d+)_(-?\d+)$/.exec(raw);
104
+ if (topo) return {
105
+ depth: Number(topo[1]),
106
+ stream: Number(topo[2])
107
+ };
108
+ const stream = /^s?(-?\d+)/.exec(raw);
109
+ if (stream) return {
110
+ depth: null,
111
+ stream: Number(stream[1])
112
+ };
113
+ throw errors.invalidParam("Invalid pagination token");
114
+ }
115
+ const topoToken = (ev) => `t${ev.pdu.depth}_${ev.stream}`;
116
+ /** Topological comparison of (depth, stream) pairs. */
117
+ const cmp = (a, b) => a.depth - b.depth || a.stream - b.stream;
118
+ /**
119
+ * Fetch room events in topological order (as /messages requires), applying history
120
+ * visibility and filters, until `limit` events are found.
121
+ */
122
+ async function paginate(hs, userId, roomId, opts) {
123
+ const back = opts.dir === "b";
124
+ const out = [];
125
+ let cursor = opts.from && opts.from.depth !== null ? {
126
+ depth: opts.from.depth,
127
+ stream: opts.from.stream
128
+ } : null;
129
+ const streamBound = opts.from && opts.from.depth === null ? opts.from.stream : null;
130
+ const batch = Math.max(opts.limit * 2, 50);
131
+ const beyond = (pos) => {
132
+ if (!opts.to) return false;
133
+ if (opts.to.depth === null) return back ? pos.stream <= opts.to.stream : pos.stream > opts.to.stream;
134
+ const c = cmp(pos, {
135
+ depth: opts.to.depth,
136
+ stream: opts.to.stream
137
+ });
138
+ return back ? c <= 0 : c > 0;
139
+ };
140
+ for (;;) {
141
+ const where = [{
142
+ field: "room_id",
143
+ value: roomId
144
+ }];
145
+ if (cursor) where.push({
146
+ field: "depth",
147
+ operator: back ? "lte" : "gte",
148
+ value: cursor.depth
149
+ });
150
+ if (streamBound !== null) where.push({
151
+ field: "stream",
152
+ operator: back ? "lte" : "gt",
153
+ value: streamBound
154
+ });
155
+ let rows = await hs.db.findMany("events", {
156
+ where,
157
+ sortBy: {
158
+ field: "depth",
159
+ direction: back ? "desc" : "asc"
160
+ },
161
+ limit: batch
162
+ });
163
+ const full = rows.length === batch;
164
+ if (full) {
165
+ const edge = rows[rows.length - 1].depth;
166
+ rows = rows.filter((r) => r.depth !== edge);
167
+ rows.push(...await hs.db.findMany("events", { where: [...where, {
168
+ field: "depth",
169
+ value: edge
170
+ }] }));
171
+ }
172
+ const stored = rows.map((r) => hs.rooms.rowToStored(r)).map((ev) => ({
173
+ ev,
174
+ pos: {
175
+ depth: ev.pdu.depth,
176
+ stream: ev.stream
177
+ }
178
+ })).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));
179
+ if (stored.length === 0) return {
180
+ events: out,
181
+ end: null
182
+ };
183
+ const visible = await hs.rooms.filterVisible(userId, roomId, stored.map((x) => x.ev));
184
+ const visibleById = new Map(visible.map((v) => [v.pdu.event_id, v]));
185
+ for (const { ev: raw, pos } of stored) {
186
+ if (beyond(pos)) return {
187
+ events: out,
188
+ end: null
189
+ };
190
+ cursor = pos;
191
+ const ev = visibleById.get(raw.pdu.event_id);
192
+ if (!ev || !matchesFilter(opts.filter, ev.pdu)) continue;
193
+ out.push(ev);
194
+ if (out.length >= opts.limit) return {
195
+ events: out,
196
+ end: `t${pos.depth}_${pos.stream}`
197
+ };
198
+ }
199
+ if (!full) return {
200
+ events: out,
201
+ end: null
202
+ };
203
+ }
204
+ }
205
+ /**
206
+ * Invite by email address or phone number (spec "Third-party invites"): invite the Matrix user the
207
+ * identity server knows for it, or store the invite there and emit an m.room.third_party_invite.
208
+ */
209
+ async function inviteByThreepid(ctx, s, body) {
210
+ const hs = ctx.hs;
211
+ const roomId = ctx.params.roomId;
212
+ for (const k of [
213
+ "id_server",
214
+ "id_access_token",
215
+ "medium",
216
+ "address"
217
+ ]) if (typeof body[k] !== "string") throw errors.missingParam(`Missing ${k}`);
218
+ await requireRoom(hs, roomId);
219
+ await hs.rooms.requireJoined(roomId, s.userId);
220
+ const mxid = await hs.identityServers.lookup(body.id_server, body.id_access_token, body.medium, body.address);
221
+ if (mxid) {
222
+ if (await hs.rooms.membership(roomId, mxid) === "ban") throw errors.forbidden("User is banned");
223
+ await hs.rooms.invite(roomId, s.userId, mxid);
224
+ return {};
225
+ }
226
+ const state = await hs.rooms.currentState(roomId);
227
+ const profile = await hs.profile(s.userId);
228
+ const create = getState(state, M.ROOM.CREATE, "");
229
+ const invite = {
230
+ medium: body.medium,
231
+ address: body.address,
232
+ room_id: roomId,
233
+ sender: s.userId,
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,
238
+ room_type: create?.content?.type,
239
+ sender_display_name: profile.displayname,
240
+ sender_avatar_url: profile.avatar_url
241
+ };
242
+ for (const k of Object.keys(invite)) if (invite[k] === void 0 || invite[k] === null) delete invite[k];
243
+ const stored = await hs.identityServers.storeInvite(body.id_server, body.id_access_token, invite);
244
+ const [first] = stored.public_keys;
245
+ await hs.rooms.sendEvent(roomId, s.userId, {
246
+ type: M.ROOM.THIRD_PARTY_INVITE,
247
+ state_key: stored.token,
248
+ content: {
249
+ display_name: stored.display_name,
250
+ key_validity_url: first?.key_validity_url,
251
+ public_key: first?.public_key,
252
+ public_keys: stored.public_keys
253
+ }
254
+ });
255
+ return {};
256
+ }
257
+ async function ownEvent(hs, userId, eventId) {
258
+ return typeof eventId === "string" && (await hs.rooms.getPdu(eventId))?.sender === userId;
259
+ }
260
+ const membershipBody = body({
261
+ user_id: lenient(string()),
262
+ reason: lenient(string())
263
+ });
264
+ async function sendMembership(ctx, membership) {
265
+ const s = ctx.session;
266
+ const roomId = ctx.params.roomId;
267
+ if (membership !== "leave") await assertNotSuspended(ctx.hs, s, roomId);
268
+ const body = ctx.input;
269
+ const hs = ctx.hs;
270
+ const reason = body.reason;
271
+ if (membership === "leave") {
272
+ const row = await hs.db.findOne("memberships", [{
273
+ field: "room_id",
274
+ value: roomId
275
+ }, {
276
+ field: "user_id",
277
+ value: s.userId
278
+ }]);
279
+ if (!await hs.rooms.exists(roomId) && !row) throw errors.notFound("Room not found");
280
+ const current = await hs.rooms.serverIsJoined(roomId) ? await hs.rooms.membership(roomId, s.userId) : row?.membership ?? "leave";
281
+ if (current === "leave") return {};
282
+ if (current === "ban") throw errors.forbidden("You are banned from this room");
283
+ if (current === "invite" && await hs.admin.isNoticeRoom(roomId)) throw new MatrixError(403, "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM", "Server notice invites cannot be rejected");
284
+ await hs.rooms.setMembership(roomId, s.userId, s.userId, "leave", reason);
285
+ return {};
286
+ }
287
+ if (!body.user_id || !isValidUserId(body.user_id)) throw errors.invalidParam("Invalid user_id");
288
+ const target = body.user_id;
289
+ await requireRoom(hs, roomId);
290
+ if (membership === "unban") {
291
+ if (await hs.rooms.membership(roomId, target) !== "ban") throw errors.forbidden("User is not banned");
292
+ await hs.rooms.setMembership(roomId, s.userId, target, "leave", reason);
293
+ } else if (membership === "kick") {
294
+ const current = await hs.rooms.membership(roomId, target);
295
+ if (current === "leave" || current === "ban") throw errors.forbidden("User is not in the room");
296
+ await hs.rooms.setMembership(roomId, s.userId, target, "leave", reason);
297
+ } else await hs.rooms.setMembership(roomId, s.userId, target, "ban", reason);
298
+ await hs.audit.record({
299
+ action: membership === "kick" ? "member_kick" : membership === "ban" ? "member_ban" : "member_unban",
300
+ actor: s.userId,
301
+ target: body.user_id,
302
+ ip: ctx.ip,
303
+ details: {
304
+ room_id: roomId,
305
+ reason
306
+ }
307
+ });
308
+ return {};
309
+ }
310
+ async function validateStateEvent(hs, roomId, type, content, stateKeyValue = "") {
311
+ if (type === M.ROOM.CREATE) throw errors.badJson("The create event cannot be sent");
312
+ if (stateKeyValue.startsWith("@") && roomVersionFeatures(await hs.rooms.roomVersion(roomId)).ownedState && !ownerOfStateKey(stateKeyValue)) throw errors.badJson("Malformed user-owned state key");
313
+ if (type === M.ROOM.POWER_LEVELS) {
314
+ const version = await hs.rooms.roomVersion(roomId);
315
+ validatePowerLevelContent(content, roomCreators(await hs.rooms.currentState(roomId), version));
316
+ }
317
+ if (type === M.ROOM.CANONICAL_ALIAS) {
318
+ const aliases = [content.alias, ...Array.isArray(content.alt_aliases) ? content.alt_aliases : []].filter((a) => a !== void 0 && a !== null);
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");
320
+ for (const alias of aliases) {
321
+ if (!isValidAlias(alias)) throw new MatrixError(400, "M_INVALID_PARAM", "Invalid alias");
322
+ const row = await hs.db.findOne("room_aliases", [{
323
+ field: "alias",
324
+ value: alias
325
+ }]);
326
+ if (!row || row.room_id !== roomId) throw new MatrixError(400, "M_BAD_ALIAS", `Alias ${alias} does not point to this room`);
327
+ }
328
+ }
329
+ }
330
+ async function publicRoomChunk(hs, roomId) {
331
+ const state = await hs.rooms.currentState(roomId);
332
+ const get = (t) => getState(state, t)?.content;
333
+ const chunk = {
334
+ room_id: roomId,
335
+ num_joined_members: (await hs.rooms.joinedMembers(roomId)).length,
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"
339
+ };
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;
345
+ if (typeof roomType === "string") chunk.room_type = roomType;
346
+ return chunk;
347
+ }
348
+ async function publicRooms(ctx, body) {
349
+ const hs = ctx.hs;
350
+ const limit = Number(body.limit ?? ctx.query.get("limit") ?? 0) || void 0;
351
+ const since = body.since ?? ctx.query.get("since");
352
+ const offset = since ? Number.parseInt(String(since).replace(/^o/, ""), 10) || 0 : 0;
353
+ const term = isObject(body.filter) && typeof body.filter.generic_search_term === "string" ? body.filter.generic_search_term.toLowerCase() : null;
354
+ const roomTypes = isObject(body.filter) && Array.isArray(body.filter.room_types) ? body.filter.room_types : null;
355
+ const rooms = await hs.db.findMany("rooms", {
356
+ where: [{
357
+ field: "is_public",
358
+ value: true
359
+ }],
360
+ sortBy: {
361
+ field: "created_ts",
362
+ direction: "asc"
363
+ }
364
+ });
365
+ let chunks = await Promise.all(rooms.map((r) => publicRoomChunk(hs, r.room_id)));
366
+ if (term) chunks = chunks.filter((c) => [
367
+ c.name,
368
+ c.topic,
369
+ c.canonical_alias
370
+ ].some((v) => typeof v === "string" && v.toLowerCase().includes(term)));
371
+ if (roomTypes) chunks = chunks.filter((c) => roomTypes.includes(c.room_type ?? null));
372
+ chunks.sort((a, b) => b.num_joined_members - a.num_joined_members);
373
+ const out = {
374
+ chunk: limit ? chunks.slice(offset, offset + limit) : chunks.slice(offset),
375
+ total_room_count_estimate: chunks.length
376
+ };
377
+ if (limit && offset + limit < chunks.length) out.next_batch = `o${offset + limit}`;
378
+ if (offset > 0) out.prev_batch = `o${Math.max(0, offset - (limit ?? 0))}`;
379
+ return out;
380
+ }
381
+ async function canDeleteAlias(hs, s, alias) {
382
+ if (alias.creator === s.userId || s.isAdmin) return true;
383
+ const version = await hs.rooms.roomVersion(alias.room_id);
384
+ if (!version) return false;
385
+ const state = await hs.rooms.currentState(alias.room_id);
386
+ const pl = powerLevels(state, version);
387
+ const needed = pl.events[M.ROOM.CANONICAL_ALIAS] ?? pl.state_default;
388
+ return membershipOf(state, s.userId) === "join" && userLevel(pl, s.userId) >= needed;
389
+ }
390
+ async function upgradeRoom(hs, s, roomId, newVersion, additionalCreators) {
391
+ if (!SUPPORTED_ROOM_VERSIONS.includes(newVersion)) throw errors.unsupportedRoomVersion();
392
+ const version = await requireRoom(hs, roomId);
393
+ const state = await hs.rooms.requireJoined(roomId, s.userId);
394
+ const pl = powerLevels(state, version);
395
+ const tombstoneLevel = pl.events[M.ROOM.TOMBSTONE] ?? pl.state_default;
396
+ if (userLevel(pl, s.userId) < tombstoneLevel) throw errors.forbidden("You cannot upgrade this room");
397
+ const create = getState(state, M.ROOM.CREATE);
398
+ const creationContent = {};
399
+ for (const k of [M.FEDERATE, "type"]) if (create.content[k] !== void 0) creationContent[k] = create.content[k];
400
+ const toV12 = roomVersionFeatures(newVersion).v12;
401
+ if (toV12) {
402
+ creationContent.predecessor = { room_id: roomId };
403
+ if (additionalCreators !== void 0) creationContent.additional_creators = additionalCreators;
404
+ } else creationContent.predecessor = {
405
+ room_id: roomId,
406
+ event_id: (await hs.db.findMany("events", {
407
+ where: [{
408
+ field: "room_id",
409
+ value: roomId
410
+ }],
411
+ sortBy: {
412
+ field: "stream",
413
+ direction: "desc"
414
+ },
415
+ limit: 1
416
+ }))[0].event_id
417
+ };
418
+ const copyTypes = [
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
428
+ ];
429
+ const initialState = [];
430
+ for (const t of copyTypes) {
431
+ const ev = getState(state, t);
432
+ if (ev && t !== M.ROOM.POWER_LEVELS) initialState.push({
433
+ type: t,
434
+ state_key: "",
435
+ content: ev.content
436
+ });
437
+ }
438
+ let oldPl = structuredClone(getState(state, M.ROOM.POWER_LEVELS)?.content ?? {});
439
+ const isPublic = (await hs.db.findOne("rooms", [{
440
+ field: "room_id",
441
+ value: roomId
442
+ }]))?.is_public;
443
+ let override;
444
+ if (toV12) {
445
+ const creators = /* @__PURE__ */ new Set([s.userId, ...Array.isArray(additionalCreators) ? additionalCreators : []]);
446
+ const users = { ...oldPl.users };
447
+ for (const u of creators) delete users[u];
448
+ oldPl = {
449
+ ...oldPl,
450
+ users
451
+ };
452
+ override = oldPl;
453
+ } else {
454
+ const level = userLevel(pl, s.userId);
455
+ override = {
456
+ ...oldPl,
457
+ users: {
458
+ ...oldPl.users,
459
+ [s.userId]: Number.isFinite(level) ? Math.max(level, 100) : 100
460
+ }
461
+ };
462
+ }
463
+ const { room_id: newRoomId } = await hs.rooms.createRoom(s.userId, {
464
+ room_version: newVersion,
465
+ creation_content: creationContent,
466
+ initial_state: initialState,
467
+ power_level_content_override: override,
468
+ preset: "private_chat",
469
+ visibility: isPublic ? "public" : "private"
470
+ });
471
+ if (getState(state, M.ROOM.POWER_LEVELS) && !toV12) try {
472
+ await hs.rooms.sendEvent(newRoomId, s.userId, {
473
+ type: M.ROOM.POWER_LEVELS,
474
+ state_key: "",
475
+ content: oldPl
476
+ });
477
+ } catch {}
478
+ const aliases = await hs.db.findMany("room_aliases", { where: [{
479
+ field: "room_id",
480
+ value: roomId
481
+ }] });
482
+ for (const a of aliases) await hs.db.update("room_aliases", [{
483
+ field: "alias",
484
+ value: a.alias
485
+ }], { room_id: newRoomId });
486
+ const canonical = getState(state, M.ROOM.CANONICAL_ALIAS);
487
+ if (canonical && Object.keys(canonical.content).length) {
488
+ await hs.rooms.sendEvent(roomId, s.userId, {
489
+ type: M.ROOM.CANONICAL_ALIAS,
490
+ state_key: "",
491
+ content: {}
492
+ }).catch(() => {});
493
+ await hs.rooms.sendEvent(newRoomId, s.userId, {
494
+ type: M.ROOM.CANONICAL_ALIAS,
495
+ state_key: "",
496
+ content: canonical.content
497
+ }).catch(() => {});
498
+ }
499
+ if (isPublic) await hs.db.update("rooms", [{
500
+ field: "room_id",
501
+ value: roomId
502
+ }], { is_public: false });
503
+ await hs.rooms.sendEvent(roomId, s.userId, {
504
+ type: M.ROOM.TOMBSTONE,
505
+ state_key: "",
506
+ content: {
507
+ body: "This room has been replaced",
508
+ replacement_room: newRoomId
509
+ }
510
+ });
511
+ const restricted = {
512
+ ...oldPl,
513
+ events_default: Math.max(50, oldPl.events_default ?? 0),
514
+ invite: Math.max(50, oldPl.invite ?? 0)
515
+ };
516
+ await hs.rooms.sendEvent(roomId, s.userId, {
517
+ type: M.ROOM.POWER_LEVELS,
518
+ state_key: "",
519
+ content: restricted
520
+ }).catch(() => {});
521
+ return newRoomId;
522
+ }
523
+ function registerRoomRoutes(r) {
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
+ }));
549
+ const joinBody = body({
550
+ reason: lenient(string()),
551
+ third_party_signed: unknown().optional()
552
+ });
553
+ const join = async (ctx, idOrAlias) => {
554
+ const s = ctx.session;
555
+ const body = ctx.input;
556
+ const { roomId, servers: resolvedServers } = await ctx.hs.rooms.resolveRoom(idOrAlias);
557
+ await assertNotSuspended(ctx.hs, s, roomId);
558
+ const { reason, membership, join_authorised_via_users_server, third_party_signed, ...custom } = body;
559
+ const servers = [
560
+ ...ctx.query.getAll("via"),
561
+ ...ctx.query.getAll("server_name"),
562
+ ...resolvedServers
563
+ ];
564
+ if (isObject(third_party_signed)) {
565
+ const signed = third_party_signed;
566
+ if (typeof signed.sender !== "string" || signed.mxid !== s.userId || typeof signed.token !== "string") throw errors.badJson("Invalid third_party_signed");
567
+ const sender = signed.sender;
568
+ const proof = signed;
569
+ if (await ctx.hs.rooms.membership(roomId, s.userId) !== "invite") {
570
+ const event = {
571
+ type: M.ROOM.MEMBER,
572
+ room_id: roomId,
573
+ sender,
574
+ state_key: s.userId,
575
+ content: {
576
+ membership: "invite",
577
+ third_party_invite: { signed: proof }
578
+ }
579
+ };
580
+ if (ctx.hs.isLocalUser(sender)) {
581
+ const pending = getState(await ctx.hs.rooms.currentState(roomId), M.ROOM.THIRD_PARTY_INVITE, signed.token);
582
+ if (!pending || pending.sender !== sender) throw errors.forbidden("No such third-party invite");
583
+ await ctx.hs.rooms.invite(roomId, sender, s.userId, { thirdPartyInvite: {
584
+ display_name: pending.content?.display_name ?? "",
585
+ signed: proof
586
+ } });
587
+ } else await ctx.hs.federation.client.request(sender.slice(sender.indexOf(":") + 1), "PUT", `/_matrix/federation/v1/exchange_third_party_invite/${encodeURIComponent(roomId)}`, { body: event });
588
+ }
589
+ }
590
+ await ctx.hs.rooms.join(s.userId, roomId, {
591
+ reason,
592
+ content: custom,
593
+ servers
594
+ });
595
+ return { room_id: roomId };
596
+ };
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
+ }));
626
+ const inviteBody = body({
627
+ user_id: lenient(string()),
628
+ reason: lenient(string())
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
+ }));
721
+ const putState = async (ctx) => {
722
+ const s = await ctx.auth();
723
+ const content = await ctx.json();
724
+ const roomId = ctx.params.roomId;
725
+ await requireRoom(ctx.hs, roomId);
726
+ const type = ctx.params.eventType;
727
+ const key = ctx.params.stateKey ?? "";
728
+ if (!(type === M.ROOM.MEMBER && key === s.userId && content?.membership === "leave")) await assertNotSuspended(ctx.hs, s, roomId);
729
+ await validateStateEvent(ctx.hs, roomId, type, content, key);
730
+ const delay = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4140.DELAY);
731
+ if (delay !== null) return { delay_id: await ctx.hs.delayedEvents.schedule({
732
+ userId: s.userId,
733
+ deviceId: s.deviceId,
734
+ roomId,
735
+ type,
736
+ stateKey: key,
737
+ content,
738
+ delayMs: Number(delay)
739
+ }) };
740
+ const current = getState(await ctx.hs.rooms.currentState(roomId), type, key);
741
+ if (current && current.sender === s.userId && JSON.stringify(current.content) === JSON.stringify(content)) return { event_id: current.event_id };
742
+ const ts = s.appservice && ctx.query.get("ts") ? Number(ctx.query.get("ts")) : void 0;
743
+ return { event_id: await ctx.hs.rooms.sendEvent(roomId, s.userId, {
744
+ type,
745
+ state_key: key,
746
+ content
747
+ }, { ts }) };
748
+ };
749
+ r.client("PUT", "/rooms/:roomId/state/:eventType", procedure({ handler: putState }));
750
+ r.client("PUT", "/rooms/:roomId/state/:eventType/:stateKey", procedure({ handler: putState }));
751
+ const getStateEvent = async (ctx) => {
752
+ const s = await ctx.auth();
753
+ const state = await visibleState(ctx.hs, ctx.params.roomId, s.userId);
754
+ const ev = getState(state, ctx.params.eventType, ctx.params.stateKey ?? "");
755
+ if (!ev) throw errors.notFound("State event not found");
756
+ if (ctx.query.get("format") === "event") return ctx.hs.rooms.formatEvent({
757
+ pdu: ev,
758
+ stream: 0,
759
+ snapshot: 0,
760
+ txnId: null,
761
+ txnToken: null
762
+ });
763
+ return ev.content;
764
+ };
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", {
784
+ where: [{
785
+ field: "room_id",
786
+ value: ctx.params.roomId
787
+ }, {
788
+ field: "stream",
789
+ operator: "lte",
790
+ value: at
791
+ }],
792
+ sortBy: {
793
+ field: "stream",
794
+ direction: "desc"
795
+ },
796
+ limit: 1
797
+ });
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
+ }
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 };
825
+ }
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 };
846
+ }
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, {
866
+ from,
867
+ to,
868
+ dir,
869
+ limit: filter?.limit ?? limit,
870
+ filter
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;
894
+ }
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));
906
+ }
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
+ }));
983
+ const RECEIPT_TYPES = [
984
+ M.READ._,
985
+ M.READ.PRIVATE,
986
+ M.FULLY_READ
987
+ ];
988
+ const setReceipt = async (hs, s, roomId, type, eventId, threadId) => {
989
+ if (type === M.FULLY_READ) {
990
+ await hs.streams.write((stream) => hs.db.upsert("account_data", {
991
+ user_id: s.userId,
992
+ room_id: roomId,
993
+ type: M.FULLY_READ,
994
+ content: { event_id: eventId },
995
+ stream
996
+ }));
997
+ hs.notifier.notify([s.userId]);
998
+ return;
999
+ }
1000
+ const ev = await hs.rooms.getEvent(eventId);
1001
+ if (!ev || ev.pdu.room_id !== roomId) throw errors.notFound("Event not found");
1002
+ await hs.streams.write((stream) => hs.db.upsert("receipts", {
1003
+ room_id: roomId,
1004
+ user_id: s.userId,
1005
+ receipt_type: type,
1006
+ thread_id: threadId ?? "",
1007
+ event_id: eventId,
1008
+ ts: Date.now(),
1009
+ stream
1010
+ }));
1011
+ hs.notifier.notify(type === M.READ.PRIVATE ? [s.userId] : await hs.rooms.joinedMembers(roomId));
1012
+ await hs.federation.onReceipt(roomId, s.userId, type, eventId, Date.now(), threadId ?? "").catch(() => {});
1013
+ };
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 {};
1025
+ }
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) => {
1042
+ if (!isValidAlias(ctx.params.alias)) throw errors.invalidParam("Invalid alias");
1043
+ const { roomId, servers } = await ctx.hs.rooms.resolveRoom(ctx.params.alias);
1044
+ const inRoom = await ctx.hs.rooms.exists(roomId) ? [...await ctx.hs.federation.roomDestinations(roomId, await ctx.hs.rooms.currentState(roomId))] : [];
1045
+ return {
1046
+ room_id: roomId,
1047
+ servers: [.../* @__PURE__ */ new Set([...servers, ...inRoom])]
1048
+ };
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(() => {});
1105
+ }
1106
+ return {};
1107
+ }
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) => {
1121
+ const row = await ctx.hs.db.findOne("rooms", [{
1122
+ field: "room_id",
1123
+ value: ctx.params.roomId
1124
+ }]);
1125
+ if (!row) throw errors.notFound("Room not found");
1126
+ return { visibility: row.is_public ? "public" : "private" };
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
+ }));
1146
+ const remoteOrLocal = async (ctx, body) => {
1147
+ const server = ctx.query.get("server");
1148
+ if (server && server !== ctx.hs.serverName) {
1149
+ const query = { ...body };
1150
+ if (ctx.query.get("limit")) query.limit = Number(ctx.query.get("limit"));
1151
+ if (ctx.query.get("since")) query.since = ctx.query.get("since");
1152
+ return ctx.hs.federation.queryPublicRooms(server, query);
1153
+ }
1154
+ return publicRooms(ctx, body);
1155
+ };
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
+ }));
1164
+ const upgradeBody = body({
1165
+ new_version: string(fail("M_BAD_JSON", "Missing new_version")),
1166
+ additional_creators: unknown().optional()
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
+ }));
1177
+ const reportBody = body({
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()
1180
+ });
1181
+ const report = async (ctx) => {
1182
+ const s = ctx.session;
1183
+ const hs = ctx.hs;
1184
+ const roomId = ctx.params.roomId;
1185
+ await requireRoom(hs, roomId);
1186
+ if (ctx.params.eventId) {
1187
+ const ev = await hs.rooms.getEvent(ctx.params.eventId);
1188
+ if (!ev || ev.pdu.room_id !== roomId || !(await hs.rooms.filterVisible(s.userId, roomId, [ev])).length) throw errors.notFound("Event not found");
1189
+ } else if (await hs.rooms.membership(roomId, s.userId) !== "join") throw errors.notFound("Room not found");
1190
+ const body = ctx.input;
1191
+ const id = await hs.cluster.nextId("event_reports");
1192
+ await hs.db.create("event_reports", {
1193
+ id,
1194
+ ts: Date.now(),
1195
+ room_id: roomId,
1196
+ event_id: ctx.params.eventId ?? null,
1197
+ reporter: s.userId,
1198
+ reason: body.reason ?? null,
1199
+ score: body.score ?? null,
1200
+ resolved: false
1201
+ });
1202
+ await hs.audit.record({
1203
+ action: "event_report",
1204
+ actor: s.userId,
1205
+ target: ctx.params.eventId ?? roomId,
1206
+ details: {
1207
+ room_id: roomId,
1208
+ report_id: id
1209
+ }
1210
+ });
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(() => {});
1212
+ return {};
1213
+ };
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
+ }));
1314
+ const relVersions = ["v1", "v3"];
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
+ }
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;
1366
+ }
1367
+ }), ["v1", "v3"]);
1368
+ }
1369
+ async function relations(ctx) {
1370
+ const s = await ctx.auth();
1371
+ const hs = ctx.hs;
1372
+ const roomId = ctx.params.roomId;
1373
+ await visibleState(hs, roomId, s.userId);
1374
+ const parent = await hs.rooms.getEvent(ctx.params.eventId);
1375
+ if (!parent || parent.pdu.room_id !== roomId) throw errors.notFound("Event not found");
1376
+ const dir = ctx.query.get("dir") === "f" ? "f" : "b";
1377
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "50", 10) || 50, 1), 1e3);
1378
+ const from = parseToken(ctx.query.get("from"));
1379
+ const rows = await hs.db.findMany("events", {
1380
+ where: [{
1381
+ field: "room_id",
1382
+ value: roomId
1383
+ }, ...from !== null ? [{
1384
+ field: "stream",
1385
+ operator: dir === "b" ? "lte" : "gt",
1386
+ value: from
1387
+ }] : []],
1388
+ sortBy: {
1389
+ field: "stream",
1390
+ direction: dir === "b" ? "desc" : "asc"
1391
+ }
1392
+ });
1393
+ const matches = [];
1394
+ let next = null;
1395
+ for (const row of rows) {
1396
+ const rel = row.json.content?.[M.RELATES_TO];
1397
+ if (!isObject(rel) || rel.event_id !== ctx.params.eventId) continue;
1398
+ if (ctx.params.relType && rel.rel_type !== ctx.params.relType) continue;
1399
+ if (ctx.params.eventType && row.type !== ctx.params.eventType) continue;
1400
+ const stored = hs.rooms.rowToStored(row);
1401
+ if ((await hs.rooms.filterVisible(s.userId, roomId, [stored])).length === 0) continue;
1402
+ if (matches.length >= limit) {
1403
+ next = dir === "b" ? matches[matches.length - 1].stream - 1 : matches[matches.length - 1].stream;
1404
+ break;
1405
+ }
1406
+ matches.push(stored);
1407
+ }
1408
+ const out = { chunk: matches.map((e) => hs.rooms.formatEvent(e, {
1409
+ userId: s.userId,
1410
+ deviceId: s.deviceId
1411
+ })) };
1412
+ if (next !== null) out.next_batch = `s${next}`;
1413
+ return out;
1414
+ }
1415
+ /** `GET /rooms/{roomId}/threads`: thread roots, most recently active first. */
1416
+ async function threads(ctx) {
1417
+ const s = await ctx.auth();
1418
+ const hs = ctx.hs;
1419
+ const roomId = ctx.params.roomId;
1420
+ await visibleState(hs, roomId, s.userId);
1421
+ const include = ctx.query.get("include") === "participated" ? "participated" : "all";
1422
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "50", 10) || 50, 1), 1e3);
1423
+ const from = parseToken(ctx.query.get("from"));
1424
+ const rows = await hs.db.findMany("events", {
1425
+ where: [{
1426
+ field: "room_id",
1427
+ value: roomId
1428
+ }],
1429
+ sortBy: {
1430
+ field: "stream",
1431
+ direction: "desc"
1432
+ }
1433
+ });
1434
+ const latest = /* @__PURE__ */ new Map();
1435
+ for (const row of rows) {
1436
+ const rel = row.json.content?.[M.RELATES_TO];
1437
+ if (!isObject(rel) || rel.rel_type !== M.THREAD || typeof rel.event_id !== "string") continue;
1438
+ const entry = latest.get(rel.event_id) ?? {
1439
+ stream: row.stream,
1440
+ latestId: row.event_id,
1441
+ count: 0,
1442
+ participants: /* @__PURE__ */ new Set()
1443
+ };
1444
+ entry.participants.add(row.sender);
1445
+ entry.count++;
1446
+ latest.set(rel.event_id, entry);
1447
+ }
1448
+ const roots = [];
1449
+ for (const [rootId, info] of latest) {
1450
+ const root = await hs.rooms.getEvent(rootId);
1451
+ if (!root || root.pdu.room_id !== roomId) continue;
1452
+ if (include === "participated" && root.pdu.sender !== s.userId && !info.participants.has(s.userId)) continue;
1453
+ if ((await hs.rooms.filterVisible(s.userId, roomId, [root])).length === 0) continue;
1454
+ roots.push({
1455
+ ev: root,
1456
+ stream: info.stream
1457
+ });
1458
+ }
1459
+ roots.sort((a, b) => b.stream - a.stream);
1460
+ const page = roots.filter((r) => from === null || r.stream <= from);
1461
+ const chunk = page.slice(0, limit);
1462
+ const viewer = {
1463
+ userId: s.userId,
1464
+ deviceId: s.deviceId
1465
+ };
1466
+ const out = { chunk: await Promise.all(chunk.map(async (r) => {
1467
+ const ev = hs.rooms.formatEvent(r.ev, viewer);
1468
+ const info = latest.get(r.ev.pdu.event_id);
1469
+ const latestEv = await hs.rooms.getEvent(info.latestId);
1470
+ ev.unsigned = {
1471
+ ...ev.unsigned,
1472
+ [M.RELATIONS]: {
1473
+ ...ev.unsigned?.[M.RELATIONS],
1474
+ [M.THREAD]: {
1475
+ latest_event: latestEv ? hs.rooms.formatEvent(latestEv, viewer) : void 0,
1476
+ count: info.count,
1477
+ current_user_participated: r.ev.pdu.sender === s.userId || info.participants.has(s.userId)
1478
+ }
1479
+ }
1480
+ };
1481
+ return ev;
1482
+ })) };
1483
+ if (page.length > limit) out.next_batch = `s${page[limit].stream}`;
1484
+ return out;
1485
+ }
1486
+ //#endregion
1487
+ export { paginate, parsePageToken, parseToken, registerRoomRoutes, topoToken, visibleState };