matrix-nodejs-homeserver 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +457 -2
  3. package/dist/adapters/auth.d.ts +50 -0
  4. package/dist/adapters/auth.js +18 -0
  5. package/dist/adapters/cluster.d.ts +55 -0
  6. package/dist/adapters/cluster.js +249 -0
  7. package/dist/adapters/media.d.ts +50 -0
  8. package/dist/adapters/media.js +63 -0
  9. package/dist/adapters/s3.d.ts +33 -0
  10. package/dist/adapters/s3.js +95 -0
  11. package/dist/api/account.js +560 -0
  12. package/dist/api/admin.js +251 -0
  13. package/dist/api/appservice.js +33 -0
  14. package/dist/api/backup.js +269 -0
  15. package/dist/api/federation.js +792 -0
  16. package/dist/api/filters.js +87 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +170 -0
  19. package/dist/api/media.js +255 -0
  20. package/dist/api/misc.js +86 -0
  21. package/dist/api/preview.js +200 -0
  22. package/dist/api/profile.js +165 -0
  23. package/dist/api/push.js +187 -0
  24. package/dist/api/relationships.js +264 -0
  25. package/dist/api/rooms.js +1364 -0
  26. package/dist/api/rtc.d.ts +24 -0
  27. package/dist/api/rtc.js +52 -0
  28. package/dist/api/search.js +155 -0
  29. package/dist/api/slidingsync.js +464 -0
  30. package/dist/api/spaces.js +172 -0
  31. package/dist/api/sync.js +600 -0
  32. package/dist/api/threadsubs.js +129 -0
  33. package/dist/api/threepid.js +122 -0
  34. package/dist/api/uia.js +126 -0
  35. package/dist/api/user.js +286 -0
  36. package/dist/core/admin.d.ts +85 -0
  37. package/dist/core/admin.js +307 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +482 -0
  40. package/dist/core/appservices.d.ts +73 -0
  41. package/dist/core/appservices.js +368 -0
  42. package/dist/core/audit.d.ts +51 -0
  43. package/dist/core/audit.js +86 -0
  44. package/dist/core/auth.d.ts +48 -0
  45. package/dist/core/auth.js +237 -0
  46. package/dist/core/authrules.d.ts +4 -0
  47. package/dist/core/authrules.js +347 -0
  48. package/dist/core/cluster.d.ts +66 -0
  49. package/dist/core/cluster.js +191 -0
  50. package/dist/core/crypto.d.ts +10 -0
  51. package/dist/core/crypto.js +100 -0
  52. package/dist/core/delayed.d.ts +39 -0
  53. package/dist/core/delayed.js +159 -0
  54. package/dist/core/errors.d.ts +9 -0
  55. package/dist/core/errors.js +45 -0
  56. package/dist/core/events.d.ts +32 -0
  57. package/dist/core/events.js +185 -0
  58. package/dist/core/glob.js +66 -0
  59. package/dist/core/identity.d.ts +24 -0
  60. package/dist/core/identity.js +96 -0
  61. package/dist/core/ids.js +36 -0
  62. package/dist/core/invitefilter.js +35 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +449 -0
  66. package/dist/core/notifier.d.ts +17 -0
  67. package/dist/core/notifier.js +54 -0
  68. package/dist/core/oauth.d.ts +38 -0
  69. package/dist/core/oauth.js +108 -0
  70. package/dist/core/partialstate.d.ts +32 -0
  71. package/dist/core/partialstate.js +197 -0
  72. package/dist/core/phone.js +268 -0
  73. package/dist/core/policy.d.ts +29 -0
  74. package/dist/core/policy.js +96 -0
  75. package/dist/core/profile.js +39 -0
  76. package/dist/core/push.d.ts +47 -0
  77. package/dist/core/push.js +306 -0
  78. package/dist/core/pushrules.d.ts +1 -0
  79. package/dist/core/pushrules.js +228 -0
  80. package/dist/core/ratelimit.d.ts +23 -0
  81. package/dist/core/ratelimit.js +77 -0
  82. package/dist/core/rooms.d.ts +215 -0
  83. package/dist/core/rooms.js +1729 -0
  84. package/dist/core/settings.d.ts +11 -0
  85. package/dist/core/settings.js +29 -0
  86. package/dist/core/sso.d.ts +46 -0
  87. package/dist/core/sso.js +165 -0
  88. package/dist/core/stateres.d.ts +4 -0
  89. package/dist/core/stateres.js +208 -0
  90. package/dist/core/threepid.d.ts +80 -0
  91. package/dist/core/threepid.js +275 -0
  92. package/dist/core/typing.d.ts +16 -0
  93. package/dist/core/typing.js +60 -0
  94. package/dist/federation/client.d.ts +51 -0
  95. package/dist/federation/client.js +278 -0
  96. package/dist/federation/index.d.ts +153 -0
  97. package/dist/federation/index.js +1058 -0
  98. package/dist/federation/keyring.d.ts +26 -0
  99. package/dist/federation/keyring.js +122 -0
  100. package/dist/federation/sender.d.ts +26 -0
  101. package/dist/federation/sender.js +164 -0
  102. package/dist/homeserver.d.ts +223 -0
  103. package/dist/homeserver.js +301 -0
  104. package/dist/http/adapters.d.ts +40 -0
  105. package/dist/http/adapters.js +130 -0
  106. package/dist/http/handler.d.ts +8 -0
  107. package/dist/http/handler.js +202 -0
  108. package/dist/http/input.js +24 -0
  109. package/dist/http/router.d.ts +60 -0
  110. package/dist/http/router.js +76 -0
  111. package/dist/index.d.ts +25 -0
  112. package/dist/index.js +14 -0
  113. package/dist/storage/memory.d.ts +5 -0
  114. package/dist/storage/memory.js +122 -0
  115. package/dist/storage/schema.d.ts +4 -0
  116. package/dist/storage/schema.js +664 -0
  117. package/dist/storage/sql.d.ts +72 -0
  118. package/dist/storage/sql.js +274 -0
  119. package/dist/storage/types.d.ts +55 -0
  120. package/package.json +67 -4
@@ -0,0 +1,24 @@
1
+ import "../http/router.js";
2
+ //#region src/api/rtc.d.ts
3
+ export interface TurnOptions {
4
+ uris: string[];
5
+ /** coturn `static-auth-secret`: time-limited credentials are derived from it. */
6
+ sharedSecret?: string;
7
+ /** Static credentials, when not using a shared secret. */
8
+ username?: string;
9
+ password?: string;
10
+ /** Credential lifetime in seconds. Default: 86400. */
11
+ ttlSeconds?: number;
12
+ /** Whether guests may obtain credentials. Default: false. */
13
+ allowGuests?: boolean;
14
+ }
15
+ export interface RtcOptions {
16
+ /** VoIP TURN servers. */
17
+ turn?: TurnOptions;
18
+ /**
19
+ * MatrixRTC transports (MSC4143), e.g. `[{ type: "livekit", livekit_service_url: "https://lk-jwt.example.com" }]`.
20
+ * Advertised at /rtc/transports and in /.well-known/matrix/client.
21
+ */
22
+ transports?: Record<string, unknown>[];
23
+ }
24
+ //#endregion
@@ -0,0 +1,52 @@
1
+ import { errors } from "../core/errors.js";
2
+ import { body, fail } from "../http/input.js";
3
+ import { createHmac } from "node:crypto";
4
+ import { z } from "zod";
5
+ //#region src/api/rtc.ts
6
+ function registerRtcRoutes(r) {
7
+ r.client("GET", "/voip/turnServer", async (ctx) => {
8
+ const s = await ctx.auth();
9
+ const turn = ctx.hs.config.rtc.turn;
10
+ if (!turn || !turn.uris.length) return {};
11
+ if (s.isGuest && !turn.allowGuests) throw errors.forbidden("Guests may not use TURN");
12
+ const ttl = turn.ttlSeconds ?? 86400;
13
+ if (turn.sharedSecret) {
14
+ const username = `${Math.floor(Date.now() / 1e3) + ttl}:${s.userId}`;
15
+ return {
16
+ username,
17
+ password: createHmac("sha1", turn.sharedSecret).update(username).digest("base64"),
18
+ uris: turn.uris,
19
+ ttl
20
+ };
21
+ }
22
+ return {
23
+ username: turn.username ?? "",
24
+ password: turn.password ?? "",
25
+ uris: turn.uris,
26
+ ttl
27
+ };
28
+ });
29
+ const transports = async (ctx) => {
30
+ await ctx.auth();
31
+ return { rtc_transports: ctx.hs.config.rtc.transports ?? [] };
32
+ };
33
+ r.client("GET", "/rtc/transports", transports, ["v1"]);
34
+ r.get("/_matrix/client/unstable/org.matrix.msc4143/rtc/transports", transports);
35
+ const base = "/_matrix/client/unstable/org.matrix.msc4140/delayed_events";
36
+ r.get(base, async (ctx) => {
37
+ const s = await ctx.auth();
38
+ return { delayed_events: await ctx.hs.delayedEvents.list(s.userId) };
39
+ });
40
+ r.post(`${base}/:delayId`, { input: body({ action: z.string(fail("M_MISSING_PARAM", "Missing action")) }) }, async (ctx) => {
41
+ const s = await ctx.auth();
42
+ await ctx.hs.delayedEvents.update(s.userId, ctx.params.delayId, ctx.input.action);
43
+ return {};
44
+ });
45
+ r.post(`${base}/:delayId/:action`, async (ctx) => {
46
+ const s = await ctx.auth();
47
+ await ctx.hs.delayedEvents.update(s.userId, ctx.params.delayId, ctx.params.action);
48
+ return {};
49
+ });
50
+ }
51
+ //#endregion
52
+ export { registerRtcRoutes };
@@ -0,0 +1,155 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { getState } from "../core/authrules.js";
3
+ import { body, fail } from "../http/input.js";
4
+ import { matchesFilter } from "./filters.js";
5
+ import { paginate, topoToken } from "./rooms.js";
6
+ import { z } from "zod";
7
+ //#region src/api/search.ts
8
+ const DEFAULT_KEYS = [
9
+ "content.body",
10
+ "content.name",
11
+ "content.topic"
12
+ ];
13
+ function valueAt(obj, path) {
14
+ return path.split(".").reduce((o, k) => isObject(o) ? o[k] : void 0, obj);
15
+ }
16
+ function registerSearchRoutes(r) {
17
+ const missingTerm = fail("M_BAD_JSON", "Missing search_categories.room_events.search_term");
18
+ const searchBody = body({ search_categories: z.looseObject({ room_events: z.looseObject({
19
+ search_term: z.string(missingTerm),
20
+ keys: z.unknown().optional(),
21
+ filter: z.unknown().optional(),
22
+ order_by: z.unknown().optional()
23
+ }, missingTerm) }, missingTerm) });
24
+ r.client("POST", "/search", { input: searchBody }, async (ctx) => {
25
+ const s = await ctx.auth();
26
+ const hs = ctx.hs;
27
+ const criteria = ctx.input.search_categories.room_events;
28
+ const terms = criteria.search_term.toLowerCase().split(/\s+/).filter(Boolean);
29
+ const keys = Array.isArray(criteria.keys) && criteria.keys.length ? criteria.keys : DEFAULT_KEYS;
30
+ const filter = isObject(criteria.filter) ? criteria.filter : {};
31
+ const limit = Math.min(filter.limit ?? 10, 100);
32
+ const orderBy = criteria.order_by === "recent" ? "recent" : "rank";
33
+ const offset = Number.parseInt(ctx.query.get("next_batch") ?? "0", 10) || 0;
34
+ let rooms = await hs.rooms.roomsForUser(s.userId);
35
+ if (filter.rooms) rooms = rooms.filter((r) => filter.rooms.includes(r));
36
+ if (filter.not_rooms) rooms = rooms.filter((r) => !filter.not_rooms.includes(r));
37
+ const hits = [];
38
+ for (const roomId of rooms) {
39
+ const rows = await hs.db.findMany("events", {
40
+ where: [{
41
+ field: "room_id",
42
+ value: roomId
43
+ }],
44
+ sortBy: {
45
+ field: "stream",
46
+ direction: "desc"
47
+ }
48
+ });
49
+ const candidates = [];
50
+ for (const row of rows) {
51
+ if (row.state_key !== null || !matchesFilter(filter, row.json)) continue;
52
+ let rank = 0;
53
+ const words = [];
54
+ for (const key of keys) {
55
+ const v = valueAt(row.json, key);
56
+ if (typeof v === "string") words.push(...v.toLowerCase().split(/[^\p{L}\p{N}]+/u));
57
+ }
58
+ if (terms.every((t) => words.includes(t))) for (const t of terms) rank += words.filter((w) => w === t).length;
59
+ if (rank > 0) candidates.push({
60
+ ev: hs.rooms.rowToStored(row),
61
+ rank
62
+ });
63
+ }
64
+ const visible = new Set((await hs.rooms.filterVisible(s.userId, roomId, candidates.map((c) => c.ev))).map((v) => v.pdu.event_id));
65
+ hits.push(...candidates.filter((c) => visible.has(c.ev.pdu.event_id)));
66
+ }
67
+ hits.sort((a, b) => orderBy === "rank" && b.rank !== a.rank ? b.rank - a.rank : b.ev.stream - a.ev.stream);
68
+ const page = hits.slice(offset, offset + limit);
69
+ const viewer = {
70
+ userId: s.userId,
71
+ deviceId: s.deviceId
72
+ };
73
+ const ec = isObject(criteria.event_context) ? criteria.event_context : null;
74
+ const results = [];
75
+ for (const hit of page) {
76
+ const result = {
77
+ rank: hit.rank,
78
+ result: hs.rooms.formatEvent(hit.ev, viewer)
79
+ };
80
+ if (ec) {
81
+ const roomId = hit.ev.pdu.room_id;
82
+ const here = {
83
+ depth: hit.ev.pdu.depth,
84
+ stream: hit.ev.stream
85
+ };
86
+ const before = await paginate(hs, s.userId, roomId, {
87
+ from: here,
88
+ to: null,
89
+ dir: "b",
90
+ limit: ec.before_limit ?? 5
91
+ });
92
+ const after = await paginate(hs, s.userId, roomId, {
93
+ from: here,
94
+ to: null,
95
+ dir: "f",
96
+ limit: ec.after_limit ?? 5
97
+ });
98
+ const context = {
99
+ events_before: before.events.map((e) => hs.rooms.formatEvent(e, viewer)),
100
+ events_after: after.events.map((e) => hs.rooms.formatEvent(e, viewer)),
101
+ start: before.end ?? topoToken(before.events.at(-1) ?? hit.ev),
102
+ end: topoToken(after.events.at(-1) ?? hit.ev)
103
+ };
104
+ if (ec.include_profile) {
105
+ const state = await hs.rooms.currentState(roomId);
106
+ const profile_info = {};
107
+ for (const e of [
108
+ ...before.events,
109
+ hit.ev,
110
+ ...after.events
111
+ ]) {
112
+ const m = getState(state, "m.room.member", e.pdu.sender)?.content;
113
+ profile_info[e.pdu.sender] = {
114
+ displayname: m?.displayname,
115
+ avatar_url: m?.avatar_url
116
+ };
117
+ }
118
+ context.profile_info = profile_info;
119
+ }
120
+ result.context = context;
121
+ }
122
+ results.push(result);
123
+ }
124
+ const out = {
125
+ results,
126
+ count: hits.length,
127
+ highlights: terms
128
+ };
129
+ if (page.length === limit && offset + limit <= hits.length) out.next_batch = String(offset + limit);
130
+ if (criteria.include_state) {
131
+ out.state = {};
132
+ for (const roomId of new Set(page.map((h) => h.ev.pdu.room_id))) out.state[roomId] = [...(await hs.rooms.currentState(roomId)).values()].map((e) => hs.rooms.formatEvent({
133
+ pdu: e,
134
+ stream: 0,
135
+ snapshot: 0,
136
+ txnId: null,
137
+ txnToken: null
138
+ }));
139
+ }
140
+ if (criteria.groupings?.group_by?.some?.((g) => g?.key === "room_id")) {
141
+ const groups = {};
142
+ hits.forEach((h, i) => {
143
+ (groups[h.ev.pdu.room_id] ??= {
144
+ results: [],
145
+ order: i,
146
+ next_batch: null
147
+ }).results.push(h.ev.pdu.event_id);
148
+ });
149
+ out.groups = { room_id: groups };
150
+ }
151
+ return { search_categories: { room_events: out } };
152
+ });
153
+ }
154
+ //#endregion
155
+ export { registerSearchRoutes };
@@ -0,0 +1,464 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { splitStateKey, toClientEvent } from "../core/events.js";
3
+ import { MatrixError } from "../core/errors.js";
4
+ import { getState } from "../core/authrules.js";
5
+ import { body, lenient } from "../http/input.js";
6
+ import { roomEphemeral, roomTimeline, toStripped } from "./sync.js";
7
+ import { z } from "zod";
8
+ //#region src/api/slidingsync.ts
9
+ const slidingSyncBody = body({
10
+ conn_id: lenient(z.string()),
11
+ lists: lenient(z.record(z.string(), z.unknown())),
12
+ room_subscriptions: lenient(z.record(z.string(), z.unknown())),
13
+ extensions: lenient(z.record(z.string(), z.unknown()))
14
+ });
15
+ const BUMP_TYPES = /* @__PURE__ */ new Set([
16
+ "m.room.create",
17
+ "m.room.message",
18
+ "m.room.encrypted",
19
+ "m.sticker",
20
+ "m.call.invite",
21
+ "m.poll.start",
22
+ "m.beacon_info"
23
+ ]);
24
+ async function loadConnection(hs, s, connId) {
25
+ return hs.db.findOne("sliding_sync_connections", [
26
+ {
27
+ field: "user_id",
28
+ value: s.userId
29
+ },
30
+ {
31
+ field: "device_id",
32
+ value: s.deviceId
33
+ },
34
+ {
35
+ field: "conn_id",
36
+ value: connId
37
+ }
38
+ ]);
39
+ }
40
+ /** Stream position of the latest "interesting" event in a room. */
41
+ async function bumpStamp(hs, roomId, upTo) {
42
+ const rows = await hs.db.findMany("events", {
43
+ where: [{
44
+ field: "room_id",
45
+ value: roomId
46
+ }, {
47
+ field: "stream",
48
+ operator: "lte",
49
+ value: upTo
50
+ }],
51
+ sortBy: {
52
+ field: "stream",
53
+ direction: "desc"
54
+ },
55
+ limit: 50
56
+ });
57
+ for (const r of rows) if (BUMP_TYPES.has(r.type)) return r.stream;
58
+ return rows[rows.length - 1]?.stream ?? 0;
59
+ }
60
+ function matchesRequiredState(required, type, key, userId, lazyMembers) {
61
+ for (const [t, k] of required) {
62
+ if (t !== "*" && t !== type) continue;
63
+ if (k === "*" || k === key) return true;
64
+ if (k === "$ME" && key === userId) return true;
65
+ if (k === "$LAZY" && type === "m.room.member" && lazyMembers.has(key)) return true;
66
+ }
67
+ return false;
68
+ }
69
+ function registerSlidingSyncRoutes(r) {
70
+ r.post("/_matrix/client/unstable/org.matrix.simplified_msc3575/sync", { input: slidingSyncBody }, slidingSync);
71
+ r.post("/_matrix/client/v5/sync", { input: slidingSyncBody }, slidingSync);
72
+ }
73
+ async function slidingSync(ctx) {
74
+ const hs = ctx.hs;
75
+ const s = await ctx.auth();
76
+ const body = ctx.input;
77
+ const connId = body.conn_id ?? "";
78
+ const posParam = ctx.query.get("pos");
79
+ const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
80
+ let conn = {
81
+ rooms: {},
82
+ prevPos: null,
83
+ toDeviceSince: 0
84
+ };
85
+ let since = null;
86
+ if (posParam !== null) {
87
+ const row = await loadConnection(hs, s, connId);
88
+ const pos = Number(posParam);
89
+ 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");
90
+ conn = row.state;
91
+ since = pos;
92
+ if (pos === row.state.prevPos && pos !== row.pos) conn = {
93
+ ...conn,
94
+ rooms: {}
95
+ };
96
+ }
97
+ const deadline = Date.now() + timeout;
98
+ for (;;) {
99
+ const to = await hs.streams.position();
100
+ const { response, empty, next } = await buildResponse(hs, s, body, conn, since, to);
101
+ const remaining = deadline - Date.now();
102
+ if (since === null || !empty || remaining <= 0) {
103
+ await hs.db.upsert("sliding_sync_connections", {
104
+ user_id: s.userId,
105
+ device_id: s.deviceId,
106
+ conn_id: connId,
107
+ pos: to,
108
+ state: {
109
+ ...next,
110
+ prevPos: since
111
+ },
112
+ updated_ts: Date.now()
113
+ });
114
+ return {
115
+ pos: String(to),
116
+ ...response
117
+ };
118
+ }
119
+ if (await hs.streams.position() > to) continue;
120
+ await hs.notifier.wait(s.userId, remaining, ctx.request.signal);
121
+ if (ctx.request.signal?.aborted) return {
122
+ pos: String(since),
123
+ lists: {},
124
+ rooms: {},
125
+ extensions: {}
126
+ };
127
+ }
128
+ }
129
+ async function buildResponse(hs, s, body, conn, since, to) {
130
+ const userId = s.userId;
131
+ const memberships = await hs.db.findMany("memberships", { where: [{
132
+ field: "user_id",
133
+ value: userId
134
+ }] });
135
+ const direct = await hs.db.findOne("account_data", [
136
+ {
137
+ field: "user_id",
138
+ value: userId
139
+ },
140
+ {
141
+ field: "room_id",
142
+ value: ""
143
+ },
144
+ {
145
+ field: "type",
146
+ value: "m.direct"
147
+ }
148
+ ]);
149
+ const dmRooms = new Set(Object.values(direct?.content ?? {}).flat().filter((x) => typeof x === "string"));
150
+ const candidates = memberships.filter((m) => m.stream <= to && ([
151
+ "join",
152
+ "invite",
153
+ "knock"
154
+ ].includes(m.membership) || since !== null && m.stream > since && !m.forgotten));
155
+ const infos = [];
156
+ for (const m of candidates) {
157
+ const state = await hs.rooms.currentState(m.room_id);
158
+ const bump = m.membership === "join" ? await bumpStamp(hs, m.room_id, to) : m.stream;
159
+ infos.push({
160
+ roomId: m.room_id,
161
+ membership: m.membership,
162
+ bump,
163
+ state
164
+ });
165
+ const invite = getState(state, "m.room.member", userId);
166
+ if (m.membership === "invite" && invite?.content?.is_direct) dmRooms.add(m.room_id);
167
+ }
168
+ infos.sort((a, b) => b.bump - a.bump);
169
+ const wanted = /* @__PURE__ */ new Map();
170
+ const lists = {};
171
+ for (const [name, raw] of Object.entries(isObject(body.lists) ? body.lists : {})) {
172
+ const list = raw;
173
+ const f = list.filters ?? {};
174
+ let rooms = infos;
175
+ if (f.is_dm !== void 0) rooms = rooms.filter((r) => dmRooms.has(r.roomId) === f.is_dm);
176
+ if (f.is_invite !== void 0) rooms = rooms.filter((r) => r.membership === "invite" === f.is_invite);
177
+ if (f.is_encrypted !== void 0) rooms = rooms.filter((r) => !!getState(r.state, "m.room.encryption") === f.is_encrypted);
178
+ const roomType = (r) => getState(r.state, "m.room.create")?.content?.type ?? null;
179
+ if (f.room_types) rooms = rooms.filter((r) => f.room_types.includes(roomType(r)));
180
+ if (f.not_room_types) rooms = rooms.filter((r) => !f.not_room_types.includes(roomType(r)));
181
+ if (f.spaces) {
182
+ const children = /* @__PURE__ */ new Set();
183
+ for (const space of f.spaces) {
184
+ if (!await hs.rooms.exists(space)) continue;
185
+ for (const key of (await hs.rooms.currentState(space)).keys()) {
186
+ const [t, k] = splitStateKey(key);
187
+ if (t === "m.space.child") children.add(k);
188
+ }
189
+ }
190
+ rooms = rooms.filter((r) => children.has(r.roomId));
191
+ }
192
+ lists[name] = { count: rooms.length };
193
+ 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)) {
194
+ const prev = wanted.get(room.roomId);
195
+ wanted.set(room.roomId, mergeConfig(prev, list));
196
+ }
197
+ }
198
+ for (const [roomId, raw] of Object.entries(isObject(body.room_subscriptions) ? body.room_subscriptions : {})) {
199
+ if (!infos.find((i) => i.roomId === roomId)) {
200
+ if (!await hs.rooms.exists(roomId) || !(await hs.rooms.canPeek(roomId, userId)).worldReadable) continue;
201
+ infos.push({
202
+ roomId,
203
+ membership: "leave",
204
+ bump: 0,
205
+ state: await hs.rooms.currentState(roomId)
206
+ });
207
+ }
208
+ wanted.set(roomId, mergeConfig(wanted.get(roomId), raw));
209
+ }
210
+ const next = {
211
+ rooms: { ...conn.rooms },
212
+ prevPos: conn.prevPos,
213
+ toDeviceSince: conn.toDeviceSince
214
+ };
215
+ const rooms = {};
216
+ for (const [roomId, config] of wanted) {
217
+ const info = infos.find((i) => i.roomId === roomId);
218
+ const configKey = JSON.stringify(config);
219
+ const known = since !== null ? conn.rooms[roomId] : void 0;
220
+ const initial = !known || known.config !== configKey;
221
+ const room = await buildRoom(hs, s, info, config, initial ? null : since, to, dmRooms.has(roomId));
222
+ const counts = JSON.stringify([
223
+ room.notification_count,
224
+ room.highlight_count,
225
+ room.joined_count,
226
+ room.invited_count
227
+ ]);
228
+ if (!initial && room.timeline.length === 0 && room.required_state.length === 0 && known.counts === counts) continue;
229
+ if (initial) room.initial = true;
230
+ if (since !== null && !initial) room.num_live = room.timeline.filter((e) => e.__stream > since).length;
231
+ for (const e of room.timeline) delete e.__stream;
232
+ rooms[roomId] = room;
233
+ next.rooms[roomId] = {
234
+ config: configKey,
235
+ counts
236
+ };
237
+ }
238
+ const extensions = await buildExtensions(hs, s, body.extensions ?? {}, since, to, Object.keys(rooms), next);
239
+ const empty = Object.keys(rooms).length === 0 && Object.values(extensions).every((e) => isEmptyExtension(e));
240
+ return {
241
+ response: {
242
+ lists,
243
+ rooms,
244
+ extensions
245
+ },
246
+ empty,
247
+ next
248
+ };
249
+ }
250
+ function mergeConfig(a, b) {
251
+ if (!a) return {
252
+ required_state: b.required_state ?? [],
253
+ timeline_limit: b.timeline_limit ?? 0
254
+ };
255
+ return {
256
+ required_state: [...a.required_state ?? [], ...b.required_state ?? []],
257
+ timeline_limit: Math.max(a.timeline_limit ?? 0, b.timeline_limit ?? 0)
258
+ };
259
+ }
260
+ async function buildRoom(hs, s, info, config, since, to, isDm) {
261
+ const { roomId, state } = info;
262
+ const userId = s.userId;
263
+ const room = {
264
+ bump_stamp: info.bump,
265
+ timeline: [],
266
+ required_state: []
267
+ };
268
+ const name = getState(state, "m.room.name")?.content?.name;
269
+ if (typeof name === "string") room.name = name;
270
+ const avatar = getState(state, "m.room.avatar")?.content?.url;
271
+ if (typeof avatar === "string") room.avatar = avatar;
272
+ if (isDm) room.is_dm = true;
273
+ if (info.membership === "invite" || info.membership === "knock") {
274
+ const own = getState(state, "m.room.member", userId);
275
+ const ev = own?.event_id ? await hs.rooms.getEvent(own.event_id) : null;
276
+ if (ev) {
277
+ const key = info.membership === "invite" ? "invite_room_state" : "knock_room_state";
278
+ room[info.membership === "invite" ? "invite_state" : "knock_state"] = [...ev.pdu.unsigned?.[key] ?? [], ev.pdu].map(toStripped);
279
+ }
280
+ return room;
281
+ }
282
+ const p = {
283
+ session: s,
284
+ ignored: /* @__PURE__ */ new Set(),
285
+ since: since === null ? null : {
286
+ pos: since,
287
+ toDevice: 0
288
+ },
289
+ to,
290
+ filter: { room: { timeline: { limit: config.timeline_limit ?? 0 } } },
291
+ fullState: false
292
+ };
293
+ const upTo = info.membership === "join" ? to : Math.min(to, (await hs.db.findOne("memberships", [{
294
+ field: "room_id",
295
+ value: roomId
296
+ }, {
297
+ field: "user_id",
298
+ value: userId
299
+ }]))?.stream ?? to);
300
+ if ((config.timeline_limit ?? 0) > 0) {
301
+ const { timeline, timelineEvents } = await roomTimeline(hs, p, roomId, since, upTo);
302
+ room.timeline = timeline.events.map((e, i) => ({
303
+ ...e,
304
+ room_id: void 0,
305
+ __stream: timelineEvents[i].stream
306
+ }));
307
+ room.limited = timeline.limited;
308
+ room.prev_batch = timeline.prev_batch;
309
+ }
310
+ const lazy = /* @__PURE__ */ new Set([userId, ...room.timeline.map((e) => e.sender)]);
311
+ const required = config.required_state ?? [];
312
+ for (const [key, ev] of state) {
313
+ const [type, sk] = splitStateKey(key);
314
+ if (!matchesRequiredState(required, type, sk, userId, lazy)) continue;
315
+ if (since !== null) {
316
+ const stored = ev.event_id ? await hs.rooms.getEvent(ev.event_id) : null;
317
+ if (!stored || stored.stream <= since) continue;
318
+ }
319
+ room.required_state.push(toClientEvent(ev, { includeRoomId: false }));
320
+ }
321
+ let joined = 0;
322
+ let invited = 0;
323
+ const heroes = [];
324
+ for (const [key, ev] of state) {
325
+ const [type, sk] = splitStateKey(key);
326
+ if (type !== "m.room.member") continue;
327
+ const m = ev.content?.membership;
328
+ if (m === "join") joined++;
329
+ if (m === "invite") invited++;
330
+ if ((m === "join" || m === "invite") && sk !== userId && heroes.length < 5) heroes.push({
331
+ user_id: sk,
332
+ displayname: ev.content?.displayname,
333
+ avatar_url: ev.content?.avatar_url
334
+ });
335
+ }
336
+ room.joined_count = joined;
337
+ room.invited_count = invited;
338
+ if (!room.name) room.heroes = heroes;
339
+ const counts = await hs.push.unreadCounts(userId, roomId);
340
+ room.notification_count = counts.room.notification_count + Object.values(counts.threads).reduce((n, t) => n + t.notification_count, 0);
341
+ room.highlight_count = counts.room.highlight_count + Object.values(counts.threads).reduce((n, t) => n + t.highlight_count, 0);
342
+ return room;
343
+ }
344
+ async function buildExtensions(hs, s, ext, since, to, roomIds, next) {
345
+ const out = {};
346
+ const userId = s.userId;
347
+ if (ext.to_device?.enabled) {
348
+ const where = [{
349
+ field: "user_id",
350
+ value: userId
351
+ }, {
352
+ field: "device_id",
353
+ value: s.deviceId
354
+ }];
355
+ const ackUpTo = Number(ext.to_device.since ?? next.toDeviceSince) || 0;
356
+ if (ackUpTo) await hs.db.delete("to_device", [...where, {
357
+ field: "stream",
358
+ operator: "lte",
359
+ value: ackUpTo
360
+ }]);
361
+ const rows = (await hs.db.findMany("to_device", {
362
+ where: [...where, {
363
+ field: "stream",
364
+ operator: "gt",
365
+ value: ackUpTo
366
+ }],
367
+ sortBy: {
368
+ field: "stream",
369
+ direction: "asc"
370
+ },
371
+ limit: Math.min(Number(ext.to_device.limit ?? 100) || 100, 1e3)
372
+ })).filter((r) => r.stream <= to);
373
+ const nextBatch = rows.length ? rows[rows.length - 1].stream : ackUpTo;
374
+ next.toDeviceSince = nextBatch;
375
+ out.to_device = {
376
+ next_batch: String(nextBatch),
377
+ events: rows.map((r) => ({
378
+ sender: r.sender,
379
+ type: r.type,
380
+ content: r.content
381
+ }))
382
+ };
383
+ }
384
+ if (ext.e2ee?.enabled) {
385
+ const e2ee = {
386
+ device_one_time_keys_count: await hs.keys.oneTimeKeyCounts(userId, s.deviceId),
387
+ device_unused_fallback_key_types: await hs.keys.unusedFallbackKeyTypes(userId, s.deviceId)
388
+ };
389
+ if (since !== null) e2ee.device_lists = await hs.keys.changesSince(userId, since, to);
390
+ out.e2ee = e2ee;
391
+ }
392
+ if (ext.account_data?.enabled) {
393
+ const where = [{
394
+ field: "user_id",
395
+ value: userId
396
+ }];
397
+ if (since !== null) where.push({
398
+ field: "stream",
399
+ operator: "gt",
400
+ value: since
401
+ });
402
+ const rows = (await hs.db.findMany("account_data", { where })).filter((r) => r.stream <= to);
403
+ const rooms = {};
404
+ const global = [];
405
+ for (const r of rows) {
406
+ const ev = {
407
+ type: r.type,
408
+ content: r.content
409
+ };
410
+ if (r.room_id === "") global.push(ev);
411
+ else if (roomIds.includes(r.room_id) || since !== null) (rooms[r.room_id] ??= []).push(ev);
412
+ }
413
+ if (since === null && !global.some((e) => e.type === "m.push_rules")) {
414
+ const { loadRules } = await import("../core/pushrules.js");
415
+ global.push({
416
+ type: "m.push_rules",
417
+ content: { global: await loadRules(hs, userId) }
418
+ });
419
+ }
420
+ out.account_data = {
421
+ global,
422
+ rooms
423
+ };
424
+ }
425
+ if (ext["io.element.msc4308.thread_subscriptions"]?.enabled) {
426
+ const { threadSubscriptionsExtension } = await import("./threadsubs.js");
427
+ out["io.element.msc4308.thread_subscriptions"] = await threadSubscriptionsExtension(hs, userId, ext["io.element.msc4308.thread_subscriptions"], since, to);
428
+ }
429
+ if (ext.receipts?.enabled || ext.typing?.enabled) {
430
+ const p = {
431
+ session: s,
432
+ ignored: /* @__PURE__ */ new Set(),
433
+ since: since === null ? null : {
434
+ pos: since,
435
+ toDevice: 0
436
+ },
437
+ to,
438
+ filter: {},
439
+ fullState: false
440
+ };
441
+ const receipts = {};
442
+ const typing = {};
443
+ const joined = new Set(await hs.rooms.roomsForUser(userId));
444
+ for (const roomId of since === null ? roomIds : [...joined]) {
445
+ if (!joined.has(roomId)) continue;
446
+ for (const e of await roomEphemeral(hs, p, roomId)) {
447
+ if (e.type === "m.receipt" && ext.receipts?.enabled) receipts[roomId] = e;
448
+ if (e.type === "m.typing" && ext.typing?.enabled && (since !== null || e.content.user_ids.length)) typing[roomId] = e;
449
+ }
450
+ }
451
+ if (ext.receipts?.enabled) out.receipts = { rooms: receipts };
452
+ if (ext.typing?.enabled) out.typing = { rooms: typing };
453
+ }
454
+ return out;
455
+ }
456
+ function isEmptyExtension(e) {
457
+ if (Array.isArray(e.events)) return e.events.length === 0;
458
+ if (e.rooms && Object.keys(e.rooms).length) return false;
459
+ if (e.global && e.global.length) return false;
460
+ if (e.device_lists && (e.device_lists.changed.length || e.device_lists.left.length)) return false;
461
+ return true;
462
+ }
463
+ //#endregion
464
+ export { registerSlidingSyncRoutes };