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,474 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { M, UNSTABLE } from "../core/m.js";
3
+ import { splitStateKey, toClientEvent } from "../core/events.js";
4
+ import { MatrixError } from "../core/errors.js";
5
+ import { getState } from "../core/authrules.js";
6
+ import { record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
7
+ import { body, lenient } from "../http/input.js";
8
+ import { procedure } from "../http/procedure.js";
9
+ import { roomEphemeral, roomTimeline, toStripped } from "./sync.js";
10
+ //#region src/api/slidingsync.ts
11
+ const slidingSyncBody = body({
12
+ conn_id: lenient(string()),
13
+ lists: lenient(record(string(), unknown())),
14
+ room_subscriptions: lenient(record(string(), unknown())),
15
+ extensions: lenient(record(string(), unknown()))
16
+ });
17
+ const BUMP_TYPES = /* @__PURE__ */ new Set([
18
+ M.ROOM.CREATE,
19
+ M.ROOM.MESSAGE,
20
+ M.ROOM.ENCRYPTED,
21
+ M.STICKER,
22
+ M.CALL.INVITE,
23
+ M.POLL.START,
24
+ M.BEACON_INFO
25
+ ]);
26
+ async function loadConnection(hs, s, connId) {
27
+ return hs.db.findOne("sliding_sync_connections", [
28
+ {
29
+ field: "user_id",
30
+ value: s.userId
31
+ },
32
+ {
33
+ field: "device_id",
34
+ value: s.deviceId
35
+ },
36
+ {
37
+ field: "conn_id",
38
+ value: connId
39
+ }
40
+ ]);
41
+ }
42
+ /** Stream position of the latest "interesting" event in a room. */
43
+ async function bumpStamp(hs, roomId, upTo) {
44
+ const rows = await hs.db.findMany("events", {
45
+ where: [{
46
+ field: "room_id",
47
+ value: roomId
48
+ }, {
49
+ field: "stream",
50
+ operator: "lte",
51
+ value: upTo
52
+ }],
53
+ sortBy: {
54
+ field: "stream",
55
+ direction: "desc"
56
+ },
57
+ limit: 50
58
+ });
59
+ for (const r of rows) if (BUMP_TYPES.has(r.type)) return r.stream;
60
+ return rows[rows.length - 1]?.stream ?? 0;
61
+ }
62
+ function matchesRequiredState(required, type, key, userId, lazyMembers) {
63
+ for (const [t, k] of required) {
64
+ if (t !== "*" && t !== type) continue;
65
+ if (k === "*" || k === key) return true;
66
+ if (k === "$ME" && key === userId) return true;
67
+ if (k === "$LAZY" && type === M.ROOM.MEMBER && lazyMembers.has(key)) return true;
68
+ }
69
+ return false;
70
+ }
71
+ function registerSlidingSyncRoutes(r) {
72
+ r.post("/_matrix/client/unstable/org.matrix.simplified_msc3575/sync", procedure({
73
+ auth: "user",
74
+ input: slidingSyncBody,
75
+ handler: slidingSync
76
+ }));
77
+ r.post("/_matrix/client/v5/sync", procedure({
78
+ auth: "user",
79
+ input: slidingSyncBody,
80
+ handler: slidingSync
81
+ }));
82
+ }
83
+ async function slidingSync(ctx) {
84
+ const hs = ctx.hs;
85
+ const s = ctx.session;
86
+ const body = ctx.input;
87
+ const connId = body.conn_id ?? "";
88
+ const posParam = ctx.query.get("pos");
89
+ const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
90
+ let conn = {
91
+ rooms: {},
92
+ prevPos: null,
93
+ toDeviceSince: 0
94
+ };
95
+ let since = null;
96
+ if (posParam !== null) {
97
+ const row = await loadConnection(hs, s, connId);
98
+ const pos = Number(posParam);
99
+ if (!row || !Number.isInteger(pos) || pos !== row.pos && pos !== row.state.prevPos) throw new MatrixError(400, "M_UNKNOWN_POS", "Unknown position; start a new connection");
100
+ conn = row.state;
101
+ since = pos;
102
+ if (pos === row.state.prevPos && pos !== row.pos) conn = {
103
+ ...conn,
104
+ rooms: {}
105
+ };
106
+ }
107
+ const deadline = Date.now() + timeout;
108
+ for (;;) {
109
+ const to = await hs.streams.position();
110
+ const { response, empty, next } = await buildResponse(hs, s, body, conn, since, to);
111
+ const remaining = deadline - Date.now();
112
+ if (since === null || !empty || remaining <= 0) {
113
+ await hs.db.upsert("sliding_sync_connections", {
114
+ user_id: s.userId,
115
+ device_id: s.deviceId,
116
+ conn_id: connId,
117
+ pos: to,
118
+ state: {
119
+ ...next,
120
+ prevPos: since
121
+ },
122
+ updated_ts: Date.now()
123
+ });
124
+ return {
125
+ pos: String(to),
126
+ ...response
127
+ };
128
+ }
129
+ if (await hs.streams.position() > to) continue;
130
+ await hs.notifier.wait(s.userId, remaining, ctx.request.signal);
131
+ if (ctx.request.signal?.aborted) return {
132
+ pos: String(since),
133
+ lists: {},
134
+ rooms: {},
135
+ extensions: {}
136
+ };
137
+ }
138
+ }
139
+ async function buildResponse(hs, s, body, conn, since, to) {
140
+ const userId = s.userId;
141
+ const memberships = await hs.db.findMany("memberships", { where: [{
142
+ field: "user_id",
143
+ value: userId
144
+ }] });
145
+ const direct = await hs.db.findOne("account_data", [
146
+ {
147
+ field: "user_id",
148
+ value: userId
149
+ },
150
+ {
151
+ field: "room_id",
152
+ value: ""
153
+ },
154
+ {
155
+ field: "type",
156
+ value: M.DIRECT
157
+ }
158
+ ]);
159
+ const dmRooms = new Set(Object.values(direct?.content ?? {}).flat().filter((x) => typeof x === "string"));
160
+ const candidates = memberships.filter((m) => m.stream <= to && ([
161
+ "join",
162
+ "invite",
163
+ "knock"
164
+ ].includes(m.membership) || since !== null && m.stream > since && !m.forgotten));
165
+ const infos = [];
166
+ for (const m of candidates) {
167
+ const state = await hs.rooms.currentState(m.room_id);
168
+ const bump = m.membership === "join" ? await bumpStamp(hs, m.room_id, to) : m.stream;
169
+ infos.push({
170
+ roomId: m.room_id,
171
+ membership: m.membership,
172
+ bump,
173
+ state
174
+ });
175
+ const invite = getState(state, M.ROOM.MEMBER, userId);
176
+ if (m.membership === "invite" && invite?.content?.is_direct) dmRooms.add(m.room_id);
177
+ }
178
+ infos.sort((a, b) => b.bump - a.bump);
179
+ const wanted = /* @__PURE__ */ new Map();
180
+ const lists = {};
181
+ for (const [name, raw] of Object.entries(isObject(body.lists) ? body.lists : {})) {
182
+ const list = raw;
183
+ const f = list.filters ?? {};
184
+ let rooms = infos;
185
+ if (f.is_dm !== void 0) rooms = rooms.filter((r) => dmRooms.has(r.roomId) === f.is_dm);
186
+ if (f.is_invite !== void 0) rooms = rooms.filter((r) => r.membership === "invite" === f.is_invite);
187
+ if (f.is_encrypted !== void 0) rooms = rooms.filter((r) => !!getState(r.state, M.ROOM.ENCRYPTION) === f.is_encrypted);
188
+ const roomType = (r) => getState(r.state, M.ROOM.CREATE)?.content?.type ?? null;
189
+ if (f.room_types) rooms = rooms.filter((r) => f.room_types.includes(roomType(r)));
190
+ if (f.not_room_types) rooms = rooms.filter((r) => !f.not_room_types.includes(roomType(r)));
191
+ if (f.spaces) {
192
+ const children = /* @__PURE__ */ new Set();
193
+ for (const space of f.spaces) {
194
+ if (!await hs.rooms.exists(space)) continue;
195
+ for (const key of (await hs.rooms.currentState(space)).keys()) {
196
+ const [t, k] = splitStateKey(key);
197
+ if (t === M.SPACE.CHILD) children.add(k);
198
+ }
199
+ }
200
+ rooms = rooms.filter((r) => children.has(r.roomId));
201
+ }
202
+ lists[name] = { count: rooms.length };
203
+ for (const [start, end] of list.ranges ?? [[0, rooms.length - 1]]) for (const room of rooms.slice(Math.max(0, start), Math.max(0, end) + 1)) {
204
+ const prev = wanted.get(room.roomId);
205
+ wanted.set(room.roomId, mergeConfig(prev, list));
206
+ }
207
+ }
208
+ for (const [roomId, raw] of Object.entries(isObject(body.room_subscriptions) ? body.room_subscriptions : {})) {
209
+ if (!infos.find((i) => i.roomId === roomId)) {
210
+ if (!await hs.rooms.exists(roomId) || !(await hs.rooms.canPeek(roomId, userId)).worldReadable) continue;
211
+ infos.push({
212
+ roomId,
213
+ membership: "leave",
214
+ bump: 0,
215
+ state: await hs.rooms.currentState(roomId)
216
+ });
217
+ }
218
+ wanted.set(roomId, mergeConfig(wanted.get(roomId), raw));
219
+ }
220
+ const next = {
221
+ rooms: { ...conn.rooms },
222
+ prevPos: conn.prevPos,
223
+ toDeviceSince: conn.toDeviceSince
224
+ };
225
+ const rooms = {};
226
+ for (const [roomId, config] of wanted) {
227
+ const info = infos.find((i) => i.roomId === roomId);
228
+ const configKey = JSON.stringify(config);
229
+ const known = since !== null ? conn.rooms[roomId] : void 0;
230
+ const initial = !known || known.config !== configKey;
231
+ const room = await buildRoom(hs, s, info, config, initial ? null : since, to, dmRooms.has(roomId));
232
+ const counts = JSON.stringify([
233
+ room.notification_count,
234
+ room.highlight_count,
235
+ room.joined_count,
236
+ room.invited_count
237
+ ]);
238
+ if (!initial && room.timeline.length === 0 && room.required_state.length === 0 && known.counts === counts) continue;
239
+ if (initial) room.initial = true;
240
+ if (since !== null && !initial) room.num_live = room.timeline.filter((e) => e.__stream > since).length;
241
+ for (const e of room.timeline) delete e.__stream;
242
+ rooms[roomId] = room;
243
+ next.rooms[roomId] = {
244
+ config: configKey,
245
+ counts
246
+ };
247
+ }
248
+ const extensions = await buildExtensions(hs, s, body.extensions ?? {}, since, to, Object.keys(rooms), next);
249
+ const empty = Object.keys(rooms).length === 0 && Object.values(extensions).every((e) => isEmptyExtension(e));
250
+ return {
251
+ response: {
252
+ lists,
253
+ rooms,
254
+ extensions
255
+ },
256
+ empty,
257
+ next
258
+ };
259
+ }
260
+ function mergeConfig(a, b) {
261
+ if (!a) return {
262
+ required_state: b.required_state ?? [],
263
+ timeline_limit: b.timeline_limit ?? 0
264
+ };
265
+ return {
266
+ required_state: [...a.required_state ?? [], ...b.required_state ?? []],
267
+ timeline_limit: Math.max(a.timeline_limit ?? 0, b.timeline_limit ?? 0)
268
+ };
269
+ }
270
+ async function buildRoom(hs, s, info, config, since, to, isDm) {
271
+ const { roomId, state } = info;
272
+ const userId = s.userId;
273
+ const room = {
274
+ bump_stamp: info.bump,
275
+ timeline: [],
276
+ required_state: []
277
+ };
278
+ const name = getState(state, M.ROOM.NAME)?.content?.name;
279
+ if (typeof name === "string") room.name = name;
280
+ const avatar = getState(state, M.ROOM.AVATAR)?.content?.url;
281
+ if (typeof avatar === "string") room.avatar = avatar;
282
+ if (isDm) room.is_dm = true;
283
+ if (info.membership === "invite" || info.membership === "knock") {
284
+ const own = getState(state, M.ROOM.MEMBER, userId);
285
+ const ev = own?.event_id ? await hs.rooms.getEvent(own.event_id) : null;
286
+ if (ev) {
287
+ const key = info.membership === "invite" ? "invite_room_state" : "knock_room_state";
288
+ room[info.membership === "invite" ? "invite_state" : "knock_state"] = [...ev.pdu.unsigned?.[key] ?? [], ev.pdu].map(toStripped);
289
+ }
290
+ return room;
291
+ }
292
+ const p = {
293
+ session: s,
294
+ ignored: /* @__PURE__ */ new Set(),
295
+ since: since === null ? null : {
296
+ pos: since,
297
+ toDevice: 0
298
+ },
299
+ to,
300
+ filter: { room: { timeline: { limit: config.timeline_limit ?? 0 } } },
301
+ fullState: false
302
+ };
303
+ const upTo = info.membership === "join" ? to : Math.min(to, (await hs.db.findOne("memberships", [{
304
+ field: "room_id",
305
+ value: roomId
306
+ }, {
307
+ field: "user_id",
308
+ value: userId
309
+ }]))?.stream ?? to);
310
+ if ((config.timeline_limit ?? 0) > 0) {
311
+ const { timeline, timelineEvents } = await roomTimeline(hs, p, roomId, since, upTo);
312
+ room.timeline = timeline.events.map((e, i) => ({
313
+ ...e,
314
+ room_id: void 0,
315
+ __stream: timelineEvents[i].stream
316
+ }));
317
+ room.limited = timeline.limited;
318
+ room.prev_batch = timeline.prev_batch;
319
+ }
320
+ const lazy = /* @__PURE__ */ new Set([userId, ...room.timeline.map((e) => e.sender)]);
321
+ const required = config.required_state ?? [];
322
+ for (const [key, ev] of state) {
323
+ const [type, sk] = splitStateKey(key);
324
+ if (!matchesRequiredState(required, type, sk, userId, lazy)) continue;
325
+ if (since !== null) {
326
+ const stored = ev.event_id ? await hs.rooms.getEvent(ev.event_id) : null;
327
+ if (!stored || stored.stream <= since) continue;
328
+ }
329
+ room.required_state.push(toClientEvent(ev, { includeRoomId: false }));
330
+ }
331
+ let joined = 0;
332
+ let invited = 0;
333
+ const heroes = [];
334
+ for (const [key, ev] of state) {
335
+ const [type, sk] = splitStateKey(key);
336
+ if (type !== M.ROOM.MEMBER) continue;
337
+ const m = ev.content?.membership;
338
+ if (m === "join") joined++;
339
+ if (m === "invite") invited++;
340
+ if ((m === "join" || m === "invite") && sk !== userId && heroes.length < 5) heroes.push({
341
+ user_id: sk,
342
+ displayname: ev.content?.displayname,
343
+ avatar_url: ev.content?.avatar_url
344
+ });
345
+ }
346
+ room.joined_count = joined;
347
+ room.invited_count = invited;
348
+ if (!room.name) room.heroes = heroes;
349
+ const counts = await hs.push.unreadCounts(userId, roomId);
350
+ room.notification_count = counts.room.notification_count + Object.values(counts.threads).reduce((n, t) => n + t.notification_count, 0);
351
+ room.highlight_count = counts.room.highlight_count + Object.values(counts.threads).reduce((n, t) => n + t.highlight_count, 0);
352
+ return room;
353
+ }
354
+ async function buildExtensions(hs, s, ext, since, to, roomIds, next) {
355
+ const out = {};
356
+ const userId = s.userId;
357
+ if (ext.to_device?.enabled) {
358
+ const where = [{
359
+ field: "user_id",
360
+ value: userId
361
+ }, {
362
+ field: "device_id",
363
+ value: s.deviceId
364
+ }];
365
+ const ackUpTo = Number(ext.to_device.since ?? next.toDeviceSince) || 0;
366
+ if (ackUpTo) await hs.db.delete("to_device", [...where, {
367
+ field: "stream",
368
+ operator: "lte",
369
+ value: ackUpTo
370
+ }]);
371
+ const rows = (await hs.db.findMany("to_device", {
372
+ where: [...where, {
373
+ field: "stream",
374
+ operator: "gt",
375
+ value: ackUpTo
376
+ }],
377
+ sortBy: {
378
+ field: "stream",
379
+ direction: "asc"
380
+ },
381
+ limit: Math.min(Number(ext.to_device.limit ?? 100) || 100, 1e3)
382
+ })).filter((r) => r.stream <= to);
383
+ const nextBatch = rows.length ? rows[rows.length - 1].stream : ackUpTo;
384
+ next.toDeviceSince = nextBatch;
385
+ out.to_device = {
386
+ next_batch: String(nextBatch),
387
+ events: rows.map((r) => ({
388
+ sender: r.sender,
389
+ type: r.type,
390
+ content: r.content
391
+ }))
392
+ };
393
+ }
394
+ if (ext.e2ee?.enabled) {
395
+ const e2ee = {
396
+ device_one_time_keys_count: await hs.keys.oneTimeKeyCounts(userId, s.deviceId),
397
+ device_unused_fallback_key_types: await hs.keys.unusedFallbackKeyTypes(userId, s.deviceId)
398
+ };
399
+ if (since !== null) e2ee.device_lists = await hs.keys.changesSince(userId, since, to);
400
+ out.e2ee = e2ee;
401
+ }
402
+ if (ext.account_data?.enabled) {
403
+ const where = [{
404
+ field: "user_id",
405
+ value: userId
406
+ }];
407
+ if (since !== null) where.push({
408
+ field: "stream",
409
+ operator: "gt",
410
+ value: since
411
+ });
412
+ const rows = (await hs.db.findMany("account_data", { where })).filter((r) => r.stream <= to);
413
+ const rooms = {};
414
+ const global = [];
415
+ for (const r of rows) {
416
+ const ev = {
417
+ type: r.type,
418
+ content: r.content
419
+ };
420
+ if (r.room_id === "") global.push(ev);
421
+ else if (roomIds.includes(r.room_id) || since !== null) (rooms[r.room_id] ??= []).push(ev);
422
+ }
423
+ if (since === null && !global.some((e) => e.type === M.PUSH_RULES)) {
424
+ const { loadRules } = await import("../core/pushrules.js");
425
+ global.push({
426
+ type: M.PUSH_RULES,
427
+ content: { global: await loadRules(hs, userId) }
428
+ });
429
+ }
430
+ out.account_data = {
431
+ global,
432
+ rooms
433
+ };
434
+ }
435
+ if (ext[UNSTABLE.IO.ELEMENT.MSC4308.THREAD_SUBSCRIPTIONS]?.enabled) {
436
+ const { threadSubscriptionsExtension } = await import("./threadsubs.js");
437
+ out[UNSTABLE.IO.ELEMENT.MSC4308.THREAD_SUBSCRIPTIONS] = await threadSubscriptionsExtension(hs, userId, ext[UNSTABLE.IO.ELEMENT.MSC4308.THREAD_SUBSCRIPTIONS], since, to);
438
+ }
439
+ if (ext.receipts?.enabled || ext.typing?.enabled) {
440
+ const p = {
441
+ session: s,
442
+ ignored: /* @__PURE__ */ new Set(),
443
+ since: since === null ? null : {
444
+ pos: since,
445
+ toDevice: 0
446
+ },
447
+ to,
448
+ filter: {},
449
+ fullState: false
450
+ };
451
+ const receipts = {};
452
+ const typing = {};
453
+ const joined = new Set(await hs.rooms.roomsForUser(userId));
454
+ for (const roomId of since === null ? roomIds : [...joined]) {
455
+ if (!joined.has(roomId)) continue;
456
+ for (const e of await roomEphemeral(hs, p, roomId)) {
457
+ if (e.type === M.RECEIPT && ext.receipts?.enabled) receipts[roomId] = e;
458
+ if (e.type === M.TYPING && ext.typing?.enabled && (since !== null || e.content.user_ids.length)) typing[roomId] = e;
459
+ }
460
+ }
461
+ if (ext.receipts?.enabled) out.receipts = { rooms: receipts };
462
+ if (ext.typing?.enabled) out.typing = { rooms: typing };
463
+ }
464
+ return out;
465
+ }
466
+ function isEmptyExtension(e) {
467
+ if (Array.isArray(e.events)) return e.events.length === 0;
468
+ if (e.rooms && Object.keys(e.rooms).length) return false;
469
+ if (e.global && e.global.length) return false;
470
+ if (e.device_lists && (e.device_lists.changed.length || e.device_lists.left.length)) return false;
471
+ return true;
472
+ }
473
+ //#endregion
474
+ export { registerSlidingSyncRoutes };
@@ -0,0 +1,177 @@
1
+ import { M } from "../core/m.js";
2
+ import { splitStateKey, stripState } from "../core/events.js";
3
+ import { errors } from "../core/errors.js";
4
+ import { getState, membershipOf } from "../core/authrules.js";
5
+ import { procedure } from "../http/procedure.js";
6
+ //#region src/api/spaces.ts
7
+ /** Room-level details shared by /hierarchy, /room_summary and /publicRooms. */
8
+ async function roomDetails(hs, roomId) {
9
+ const state = await hs.rooms.currentState(roomId);
10
+ const get = (t) => getState(state, t)?.content;
11
+ const out = {
12
+ room_id: roomId,
13
+ num_joined_members: (await hs.rooms.joinedMembers(roomId)).length,
14
+ world_readable: get(M.ROOM.HISTORY_VISIBILITY)?.history_visibility === "world_readable",
15
+ guest_can_join: get(M.ROOM.GUEST_ACCESS)?.guest_access === "can_join",
16
+ join_rule: get(M.ROOM.JOIN_RULES)?.join_rule ?? "invite"
17
+ };
18
+ const str = (v) => typeof v === "string" ? v : void 0;
19
+ if (str(get(M.ROOM.NAME)?.name)) out.name = get(M.ROOM.NAME).name;
20
+ if (str(get(M.ROOM.TOPIC)?.topic)) out.topic = get(M.ROOM.TOPIC).topic;
21
+ if (str(get(M.ROOM.CANONICAL_ALIAS)?.alias)) out.canonical_alias = get(M.ROOM.CANONICAL_ALIAS).alias;
22
+ if (str(get(M.ROOM.AVATAR)?.url)) out.avatar_url = get(M.ROOM.AVATAR).url;
23
+ if (str(get(M.ROOM.CREATE)?.type)) out.room_type = get(M.ROOM.CREATE).type;
24
+ return out;
25
+ }
26
+ /** Whether a user may see a room's summary (spec: "accessible" rooms). */
27
+ async function canAccess(hs, roomId, userId) {
28
+ const state = await hs.rooms.currentState(roomId);
29
+ if (userId && [
30
+ "join",
31
+ "invite",
32
+ "knock"
33
+ ].includes(membershipOf(state, userId))) return true;
34
+ if (getState(state, M.ROOM.HISTORY_VISIBILITY)?.content?.history_visibility === "world_readable") return true;
35
+ const joinRules = getState(state, M.ROOM.JOIN_RULES)?.content;
36
+ const rule = joinRules?.join_rule ?? "invite";
37
+ if (rule === "public" || rule === "knock") return true;
38
+ if ((rule === "restricted" || rule === "knock_restricted") && userId) {
39
+ if (rule === "knock_restricted") return true;
40
+ for (const allow of Array.isArray(joinRules?.allow) ? joinRules.allow : []) if (allow?.type === M.ROOM_MEMBERSHIP && typeof allow.room_id === "string" && await hs.rooms.exists(allow.room_id)) {
41
+ if (await hs.rooms.membership(allow.room_id, userId) === "join") return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+ function allowedRoomIds(joinRules) {
47
+ if (!joinRules || !["restricted", "knock_restricted"].includes(joinRules.join_rule)) return void 0;
48
+ return (Array.isArray(joinRules.allow) ? joinRules.allow : []).filter((a) => a?.type === M.ROOM_MEMBERSHIP && typeof a.room_id === "string").map((a) => a.room_id);
49
+ }
50
+ /** Spec ordering of space children: valid `order` strings first, then child event timestamp, then room ID. */
51
+ function childOrder(a, b) {
52
+ const valid = (o) => typeof o === "string" && o.length <= 50 && /^[\x20-\x7E]*$/.test(o) ? o : null;
53
+ const oa = valid(a.content?.order);
54
+ const ob = valid(b.content?.order);
55
+ if (oa !== null && ob === null) return -1;
56
+ if (oa === null && ob !== null) return 1;
57
+ if (oa !== null && ob !== null && oa !== ob) return oa < ob ? -1 : 1;
58
+ return (a.origin_server_ts ?? 0) - (b.origin_server_ts ?? 0) || (a.state_key < b.state_key ? -1 : a.state_key > b.state_key ? 1 : 0);
59
+ }
60
+ /** Fetch a room's summary from another server via federation /hierarchy, if the user may see it. */
61
+ async function remoteSummary(hs, roomId, via, suggestedOnly, userId) {
62
+ if (!hs.federation.enabled) return null;
63
+ for (const server of via.filter((v) => v !== hs.serverName)) try {
64
+ const room = (await hs.federation.queryHierarchy(server, roomId, suggestedOnly)).room;
65
+ if (!room || room.room_id !== roomId) continue;
66
+ const rule = room.join_rule ?? "invite";
67
+ let accessible = room.world_readable === true || [
68
+ "public",
69
+ "knock",
70
+ "knock_restricted"
71
+ ].includes(rule);
72
+ if (!accessible && rule === "restricted" && Array.isArray(room.allowed_room_ids)) {
73
+ for (const allowed of room.allowed_room_ids) if (await hs.rooms.exists(allowed) && await hs.rooms.membership(allowed, userId) === "join") {
74
+ accessible = true;
75
+ break;
76
+ }
77
+ }
78
+ return accessible ? room : null;
79
+ } catch {}
80
+ return null;
81
+ }
82
+ function registerSpaceRoutes(r) {
83
+ r.client("GET", "/rooms/:roomId/hierarchy", procedure({
84
+ auth: "user",
85
+ handler: async (ctx) => {
86
+ const s = ctx.session;
87
+ const hs = ctx.hs;
88
+ const rootId = ctx.params.roomId;
89
+ if (!await hs.rooms.exists(rootId) || !await canAccess(hs, rootId, s.userId)) throw errors.forbidden("You cannot view this room");
90
+ const suggestedOnly = ctx.query.get("suggested_only") === "true";
91
+ const maxDepth = ctx.query.has("max_depth") ? Number.parseInt(ctx.query.get("max_depth"), 10) : Infinity;
92
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "50", 10) || 50, 1), 100);
93
+ const from = ctx.query.get("from");
94
+ let offset = 0;
95
+ if (from) {
96
+ const m = /^(\d+)_(.+)$/.exec(from);
97
+ if (!m || m[2] !== `${rootId}|${suggestedOnly}|${ctx.query.get("max_depth") ?? ""}`) throw errors.invalidParam("Invalid pagination token");
98
+ offset = Number(m[1]);
99
+ }
100
+ const rooms = [];
101
+ const seen = /* @__PURE__ */ new Set();
102
+ const queue = [{
103
+ roomId: rootId,
104
+ depth: 0,
105
+ via: []
106
+ }];
107
+ while (queue.length) {
108
+ const { roomId, depth, via } = queue.pop();
109
+ if (seen.has(roomId)) continue;
110
+ seen.add(roomId);
111
+ if (!await hs.rooms.serverIsJoined(roomId)) {
112
+ const remote = await remoteSummary(hs, roomId, via, suggestedOnly, s.userId);
113
+ if (!remote) continue;
114
+ rooms.push(remote);
115
+ const remoteChildren = (remote.room_type === M.SPACE._ ? remote.children_state ?? [] : []).filter((c) => Array.isArray(c.content?.via) && c.content.via.length).sort(childOrder);
116
+ if (depth < maxDepth) for (const child of [...remoteChildren].reverse()) queue.push({
117
+ roomId: child.state_key,
118
+ depth: depth + 1,
119
+ via: child.content.via
120
+ });
121
+ continue;
122
+ }
123
+ if (roomId !== rootId && !await canAccess(hs, roomId, s.userId)) continue;
124
+ const state = await hs.rooms.currentState(roomId);
125
+ const children = [];
126
+ const isSpace = getState(state, M.ROOM.CREATE)?.content?.type === M.SPACE._;
127
+ for (const [key, ev] of isSpace ? state : []) {
128
+ const [type] = splitStateKey(key);
129
+ if (type !== M.SPACE.CHILD || !Array.isArray(ev.content?.via) || ev.content.via.length === 0) continue;
130
+ if (suggestedOnly && ev.content.suggested !== true) continue;
131
+ children.push({
132
+ ...stripState(ev),
133
+ origin_server_ts: ev.origin_server_ts
134
+ });
135
+ }
136
+ children.sort(childOrder);
137
+ if (depth < maxDepth) for (const child of [...children].reverse()) queue.push({
138
+ roomId: child.state_key,
139
+ depth: depth + 1,
140
+ via: child.content.via
141
+ });
142
+ const details = await roomDetails(hs, roomId);
143
+ const allowed = allowedRoomIds(getState(state, M.ROOM.JOIN_RULES)?.content);
144
+ if (allowed) details.allowed_room_ids = allowed;
145
+ details.children_state = children;
146
+ rooms.push(details);
147
+ }
148
+ const out = { rooms: rooms.slice(offset, offset + limit) };
149
+ if (offset + limit < rooms.length) out.next_batch = `${offset + limit}_${rootId}|${suggestedOnly}|${ctx.query.get("max_depth") ?? ""}`;
150
+ return out;
151
+ }
152
+ }), ["v1"]);
153
+ const summary = async (ctx) => {
154
+ const s = await ctx.optionalAuth();
155
+ const hs = ctx.hs;
156
+ let roomId;
157
+ try {
158
+ roomId = await hs.rooms.resolveRoomIdOrAlias(ctx.params.roomIdOrAlias);
159
+ } catch {
160
+ throw errors.notFound("Room not found");
161
+ }
162
+ if (!await hs.rooms.exists(roomId) || !await canAccess(hs, roomId, s?.userId ?? null)) throw errors.notFound("Room not found");
163
+ const state = await hs.rooms.currentState(roomId);
164
+ const out = await roomDetails(hs, roomId);
165
+ out.room_version = await hs.rooms.roomVersion(roomId);
166
+ const enc = getState(state, M.ROOM.ENCRYPTION)?.content?.algorithm;
167
+ if (typeof enc === "string") out.encryption = enc;
168
+ if (s) out.membership = membershipOf(state, s.userId);
169
+ const allowed = allowedRoomIds(getState(state, M.ROOM.JOIN_RULES)?.content);
170
+ if (allowed) out.allowed_room_ids = allowed;
171
+ return out;
172
+ };
173
+ r.client("GET", "/room_summary/:roomIdOrAlias", procedure({ handler: summary }), ["v1"]);
174
+ r.get("/_matrix/client/unstable/im.nheko.summary/summary/:roomIdOrAlias", procedure({ handler: summary }));
175
+ }
176
+ //#endregion
177
+ export { registerSpaceRoutes, roomDetails };