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,609 @@
1
+ import { M, UNSTABLE } from "../core/m.js";
2
+ import { eventIdOf, splitStateKey, stateKey, toClientEvent } from "../core/events.js";
3
+ import { errors } from "../core/errors.js";
4
+ import { getState } from "../core/authrules.js";
5
+ import { loadRules } from "../core/pushrules.js";
6
+ import { inviteDecision } from "../core/invitefilter.js";
7
+ import { body } from "../http/input.js";
8
+ import { procedure } from "../http/procedure.js";
9
+ import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
10
+ import { fullProfile } from "../core/profile.js";
11
+ //#region src/api/sync.ts
12
+ function parseSyncToken(raw) {
13
+ if (!raw) return null;
14
+ const m = /^s(\d+)(?:_(\d+))?$/.exec(raw);
15
+ if (!m) throw errors.invalidParam("Invalid since token");
16
+ return {
17
+ pos: Number(m[1]),
18
+ toDevice: Number(m[2] ?? m[1])
19
+ };
20
+ }
21
+ const formatToken = (t) => `s${t.pos}_${t.toDevice}`;
22
+ async function loadFilter(hs, userId, raw) {
23
+ if (!raw) return {};
24
+ if (raw.trim().startsWith("{")) {
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ } catch {
29
+ throw errors.badJson("Invalid filter JSON");
30
+ }
31
+ return validateSyncFilter(parsed);
32
+ }
33
+ const row = await hs.db.findOne("filters", [{
34
+ field: "user_id",
35
+ value: userId
36
+ }, {
37
+ field: "filter_id",
38
+ value: raw
39
+ }]);
40
+ if (!row) throw errors.notFound("Unknown filter");
41
+ return row.filter;
42
+ }
43
+ /** A stripped state event as clients expect it. */
44
+ function toStripped(ev) {
45
+ return {
46
+ type: ev.type,
47
+ state_key: ev.state_key,
48
+ sender: ev.sender,
49
+ content: ev.content
50
+ };
51
+ }
52
+ /** State snapshot ID in effect at stream position `pos` in a room (0 = empty). */
53
+ async function snapshotAt(hs, roomId, pos) {
54
+ return (await hs.db.findMany("events", {
55
+ where: [{
56
+ field: "room_id",
57
+ value: roomId
58
+ }, {
59
+ field: "stream",
60
+ operator: "lte",
61
+ value: pos
62
+ }],
63
+ sortBy: {
64
+ field: "stream",
65
+ direction: "desc"
66
+ },
67
+ limit: 1
68
+ }))[0]?.state_snapshot ?? 0;
69
+ }
70
+ /** Build the timeline + state section of a room. */
71
+ async function roomTimeline(hs, p, roomId, since, upTo) {
72
+ const tf = p.filter.room?.timeline;
73
+ const limit = tf?.limit ?? 10;
74
+ const where = [{
75
+ field: "room_id",
76
+ value: roomId
77
+ }, {
78
+ field: "stream",
79
+ operator: "lte",
80
+ value: upTo
81
+ }];
82
+ if (since !== null) where.push({
83
+ field: "stream",
84
+ operator: "gt",
85
+ value: since
86
+ });
87
+ const events = [];
88
+ let limited = false;
89
+ let cursor = upTo;
90
+ const batchSize = Math.max(limit * 2, 20);
91
+ for (;;) {
92
+ const w = where.map((c) => c.field === "stream" && c.operator === "lte" ? {
93
+ ...c,
94
+ value: cursor
95
+ } : c);
96
+ const rows = await hs.db.findMany("events", {
97
+ where: w,
98
+ sortBy: {
99
+ field: "stream",
100
+ direction: "desc"
101
+ },
102
+ limit: batchSize
103
+ });
104
+ if (rows.length === 0) break;
105
+ const stored = rows.map((r) => hs.rooms.rowToStored(r));
106
+ const visible = await hs.rooms.filterVisible(p.session.userId, roomId, stored);
107
+ const ok = new Map(visible.map((v) => [v.pdu.event_id, v]));
108
+ let full = false;
109
+ for (const raw of stored) {
110
+ cursor = raw.stream - 1;
111
+ const ev = ok.get(raw.pdu.event_id);
112
+ if (!ev || !matchesFilter(tf, ev.pdu) || p.ignored.has(ev.pdu.sender)) continue;
113
+ if (events.length >= (since === null ? limit * 2 : limit)) {
114
+ limited = true;
115
+ full = true;
116
+ break;
117
+ }
118
+ events.push(ev);
119
+ }
120
+ if (full || rows.length < batchSize) break;
121
+ }
122
+ if (since === null && events.length > limit) {
123
+ events.sort((a, b) => (b.pdu.depth ?? 0) - (a.pdu.depth ?? 0) || b.stream - a.stream);
124
+ events.length = limit;
125
+ limited = true;
126
+ }
127
+ if (limit === 0 && events.length === 0) limited = (await hs.db.findMany("events", {
128
+ where,
129
+ limit: 1
130
+ })).length > 0;
131
+ if (since === null) events.sort((a, b) => (a.pdu.depth ?? 0) - (b.pdu.depth ?? 0) || a.stream - b.stream);
132
+ else events.reverse();
133
+ let lastGap = -1;
134
+ events.forEach((e, i) => {
135
+ if (e.gap) lastGap = i;
136
+ });
137
+ if (lastGap >= 0) {
138
+ events.splice(0, lastGap);
139
+ limited = true;
140
+ }
141
+ if (!limited && events.length) limited = (await hs.db.findMany("events", {
142
+ where: [...where.filter((c) => c.operator !== "lte"), {
143
+ field: "stream",
144
+ operator: "lt",
145
+ value: events[0].stream
146
+ }],
147
+ limit: 1
148
+ })).length > 0;
149
+ const startPos = events.length ? Math.min(...events.map((e) => e.stream)) - 1 : upTo;
150
+ const startState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, startPos));
151
+ const timelineIds = new Set(events.map((e) => e.pdu.event_id));
152
+ const endState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, upTo));
153
+ for (const [key, ev] of endState) {
154
+ if (timelineIds.has(ev.event_id) || startState.get(key)?.event_id === ev.event_id) continue;
155
+ if (events.some((e) => e.pdu.state_key !== void 0 && stateKey(e.pdu.type, e.pdu.state_key) === key)) continue;
156
+ startState.set(key, ev);
157
+ }
158
+ const sinceIds = since !== null ? await hs.rooms.snapshotIds(await snapshotAt(hs, roomId, since)) : {};
159
+ const sf = p.filter.room?.state;
160
+ const lazy = !!sf?.lazy_load_members;
161
+ const senders = new Set(events.map((e) => e.pdu.sender));
162
+ senders.add(p.session.userId);
163
+ const state = [];
164
+ for (const [key, ev] of startState) {
165
+ if (sinceIds[key] === ev.event_id) continue;
166
+ const [type, sk] = splitStateKey(key);
167
+ if (!matchesFilter(sf, ev)) continue;
168
+ if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
169
+ state.push(ev);
170
+ }
171
+ if (lazy) for (const e of events) {
172
+ const key = stateKey(M.ROOM.MEMBER, e.pdu.sender);
173
+ if (startState.has(key) || state.some((s) => s.type === M.ROOM.MEMBER && s.state_key === e.pdu.sender)) continue;
174
+ const member = [...(await hs.rooms.getPdus((e.pdu.auth_events ?? []).map(eventIdOf))).values()].find((a) => a.type === M.ROOM.MEMBER && a.state_key === e.pdu.sender);
175
+ if (member && matchesFilter(sf, member)) state.push(member);
176
+ }
177
+ const stateAfter = [];
178
+ if (p.useStateAfter) {
179
+ const endState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, upTo));
180
+ for (const [key, ev] of endState) {
181
+ if (sinceIds[key] === ev.event_id) continue;
182
+ const [type, sk] = splitStateKey(key);
183
+ if (!matchesFilter(sf, ev)) continue;
184
+ if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
185
+ stateAfter.push(ev);
186
+ }
187
+ }
188
+ return {
189
+ stateAfter,
190
+ timeline: {
191
+ events: events.map((e) => hs.rooms.formatEvent(e, p.session, { includeRoomId: false })),
192
+ limited,
193
+ prev_batch: `s${startPos}`
194
+ },
195
+ state,
196
+ timelineEvents: events
197
+ };
198
+ }
199
+ async function roomEphemeral(hs, p, roomId) {
200
+ const since = p.since?.pos ?? null;
201
+ const out = [];
202
+ const typing = hs.typing.typingSince(roomId, since);
203
+ if (typing !== null && (since !== null || typing.length > 0)) out.push({
204
+ type: M.TYPING,
205
+ content: { user_ids: typing }
206
+ });
207
+ const where = [{
208
+ field: "room_id",
209
+ value: roomId
210
+ }];
211
+ if (since !== null) where.push({
212
+ field: "stream",
213
+ operator: "gt",
214
+ value: since
215
+ });
216
+ const receipts = (await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
217
+ const content = {};
218
+ for (const r of receipts) {
219
+ if (r.receipt_type === M.READ.PRIVATE && r.user_id !== p.session.userId) continue;
220
+ content[r.event_id] ??= {};
221
+ content[r.event_id][r.receipt_type] ??= {};
222
+ if (r.thread_id && content[r.event_id][r.receipt_type][r.user_id] && !content[r.event_id][r.receipt_type][r.user_id].thread_id) continue;
223
+ content[r.event_id][r.receipt_type][r.user_id] = {
224
+ ts: r.ts,
225
+ ...r.thread_id ? { thread_id: r.thread_id } : {}
226
+ };
227
+ }
228
+ if (Object.keys(content).length) out.push({
229
+ type: M.RECEIPT,
230
+ content
231
+ });
232
+ return out.filter((e) => matchesFilter(p.filter.room?.ephemeral, e));
233
+ }
234
+ async function accountData(hs, p, roomId) {
235
+ const where = [{
236
+ field: "user_id",
237
+ value: p.session.userId
238
+ }, {
239
+ field: "room_id",
240
+ value: roomId
241
+ }];
242
+ if (p.since && !p.fullState) where.push({
243
+ field: "stream",
244
+ operator: "gt",
245
+ value: p.since.pos
246
+ });
247
+ const rows = await hs.db.findMany("account_data", { where });
248
+ const filter = roomId ? p.filter.room?.account_data : p.filter.account_data;
249
+ return rows.filter((r) => r.stream <= p.to && (p.since || Object.keys(r.content ?? {}).length > 0)).map((r) => ({
250
+ type: r.type,
251
+ content: r.content
252
+ })).filter((e) => matchesFilter(filter, e));
253
+ }
254
+ async function roomSummary(hs, roomId, userId) {
255
+ const state = await hs.rooms.currentState(roomId);
256
+ let joined = 0;
257
+ let invited = 0;
258
+ const heroes = [];
259
+ for (const [key, ev] of state) {
260
+ const [type, sk] = splitStateKey(key);
261
+ if (type !== M.ROOM.MEMBER) continue;
262
+ const m = ev.content?.membership;
263
+ if (m === "join") joined++;
264
+ if (m === "invite") invited++;
265
+ if ((m === "join" || m === "invite") && sk !== userId && heroes.length < 5) heroes.push(sk);
266
+ }
267
+ const summary = {
268
+ [M.JOINED_MEMBER_COUNT]: joined,
269
+ [M.INVITED_MEMBER_COUNT]: invited
270
+ };
271
+ if (!getState(state, M.ROOM.NAME)?.content?.name && !getState(state, M.ROOM.CANONICAL_ALIAS)?.content?.alias) summary[M.HEROES] = heroes.sort();
272
+ return summary;
273
+ }
274
+ async function buildSync(hs, p) {
275
+ const userId = p.session.userId;
276
+ const since = p.since?.pos ?? null;
277
+ const memberships = await hs.db.findMany("memberships", { where: [{
278
+ field: "user_id",
279
+ value: userId
280
+ }] });
281
+ const join = {};
282
+ const invite = {};
283
+ const leave = {};
284
+ const knock = {};
285
+ let empty = true;
286
+ for (const m of memberships) {
287
+ if (m.stream > p.to || !roomAllowed(p.filter, m.room_id)) continue;
288
+ const roomId = m.room_id;
289
+ const changed = since === null || m.stream > since;
290
+ if (m.membership === "join") {
291
+ if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) continue;
292
+ const completedAt = await hs.partialState.completedAt(roomId);
293
+ const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
294
+ const { timeline, state, stateAfter, timelineEvents } = await roomTimeline(hs, p, roomId, initial ? null : since, p.to);
295
+ const ephemeral = await roomEphemeral(hs, p, roomId);
296
+ const roomAccountData = await accountData(hs, p, roomId);
297
+ if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) continue;
298
+ join[roomId] = {
299
+ timeline,
300
+ ...p.useStateAfter ? { [p.stateAfterKey ?? "state_after"]: { events: stateAfter.map((e) => toClientEvent(e, { includeRoomId: false })) } } : { state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) } },
301
+ ephemeral: { events: ephemeral },
302
+ account_data: { events: roomAccountData },
303
+ summary: await roomSummary(hs, roomId, userId)
304
+ };
305
+ const counts = await hs.push.unreadCounts(userId, roomId);
306
+ if (p.filter.room?.timeline?.unread_thread_notifications) {
307
+ join[roomId].unread_notifications = counts.room;
308
+ if (Object.keys(counts.threads).length) join[roomId].unread_thread_notifications = counts.threads;
309
+ } else {
310
+ const total = { ...counts.room };
311
+ for (const t of Object.values(counts.threads)) {
312
+ total.notification_count += t.notification_count;
313
+ total.highlight_count += t.highlight_count;
314
+ }
315
+ join[roomId].unread_notifications = total;
316
+ }
317
+ empty = false;
318
+ } else if (m.membership === "invite") {
319
+ if (!changed && !p.fullState) continue;
320
+ const pdu = await hs.rooms.getPdu(m.event_id);
321
+ if (!pdu || p.ignored.has(pdu.sender)) continue;
322
+ if (await inviteDecision(hs, userId, pdu.sender) !== "allow") continue;
323
+ invite[roomId] = { invite_state: { events: [...pdu.unsigned?.invite_room_state ?? [], pdu].map(toStripped) } };
324
+ empty = false;
325
+ } else if (m.membership === "knock") {
326
+ if (!changed && !p.fullState) continue;
327
+ const knockPdu = await hs.rooms.getPdu(m.event_id);
328
+ const ev = knockPdu ? { pdu: knockPdu } : null;
329
+ if (!ev) continue;
330
+ knock[roomId] = { knock_state: { events: [...ev.pdu.unsigned?.knock_room_state ?? [], ev.pdu].map(toStripped) } };
331
+ empty = false;
332
+ } else {
333
+ if (m.forgotten && since === null) continue;
334
+ if (since === null ? !p.filter.room?.include_leave : m.stream <= since) continue;
335
+ const { timeline, state } = await roomTimeline(hs, p, roomId, since, m.stream);
336
+ const tf = p.filter.room?.timeline;
337
+ if (!await hs.rooms.exists(roomId) && (tf?.limit ?? 10) > 0 && !timeline.events.some((e) => e.event_id === m.event_id)) {
338
+ const leavePdu = await hs.rooms.getPdu(m.event_id);
339
+ if (leavePdu && matchesFilter(tf, leavePdu)) timeline.events.push(toClientEvent(leavePdu, { includeRoomId: false }));
340
+ }
341
+ leave[roomId] = {
342
+ timeline,
343
+ state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) },
344
+ account_data: { events: await accountData(hs, p, roomId) }
345
+ };
346
+ empty = false;
347
+ }
348
+ }
349
+ const globalAccountData = await accountData(hs, p, "");
350
+ if (since === null && !globalAccountData.some((e) => e.type === M.PUSH_RULES)) globalAccountData.push({
351
+ type: M.PUSH_RULES,
352
+ content: { global: await loadRules(hs, userId) }
353
+ });
354
+ if (globalAccountData.length) empty = false;
355
+ const deviceWhere = [{
356
+ field: "user_id",
357
+ value: userId
358
+ }, {
359
+ field: "device_id",
360
+ value: p.session.deviceId
361
+ }];
362
+ if (p.since) await hs.db.delete("to_device", [...deviceWhere, {
363
+ field: "stream",
364
+ operator: "lte",
365
+ value: p.since.toDevice
366
+ }]);
367
+ const toDevice = (await hs.db.findMany("to_device", {
368
+ where: [...deviceWhere, {
369
+ field: "stream",
370
+ operator: "gt",
371
+ value: p.since?.toDevice ?? 0
372
+ }],
373
+ sortBy: {
374
+ field: "stream",
375
+ direction: "asc"
376
+ },
377
+ limit: 100
378
+ })).filter((r) => r.stream <= p.to);
379
+ const nextToDevice = toDevice.length ? toDevice[toDevice.length - 1].stream : p.since?.toDevice ?? 0;
380
+ if (toDevice.length) empty = false;
381
+ const presenceEvents = [];
382
+ if (!p.filter.presence?.types || p.filter.presence.types.length) {
383
+ const sharing = await hs.rooms.usersSharingRooms(userId);
384
+ const newlyShared = /* @__PURE__ */ new Set();
385
+ if (since !== null) {
386
+ const changes = await hs.db.findMany("memberships", { where: [{
387
+ field: "stream",
388
+ operator: "gt",
389
+ value: since
390
+ }] });
391
+ const myRooms = new Set(memberships.filter((m) => m.membership === "join").map((m) => m.room_id));
392
+ for (const c of changes) {
393
+ if (c.membership !== "join" || !myRooms.has(c.room_id)) continue;
394
+ if (c.user_id === userId) for (const u of await hs.rooms.joinedMembers(c.room_id)) newlyShared.add(u);
395
+ else newlyShared.add(c.user_id);
396
+ }
397
+ }
398
+ const where = [{
399
+ field: "user_id",
400
+ operator: "in",
401
+ value: [...sharing]
402
+ }];
403
+ for (const row of await hs.db.findMany("presence", { where })) {
404
+ if (row.stream > p.to) continue;
405
+ if (since !== null && row.stream <= since && !newlyShared.has(row.user_id)) continue;
406
+ const content = {
407
+ presence: row.presence,
408
+ last_active_ago: Date.now() - row.last_active_ts
409
+ };
410
+ if (row.status_msg) content.status_msg = row.status_msg;
411
+ if (row.presence === "online") content.currently_active = true;
412
+ const ev = {
413
+ type: M.PRESENCE,
414
+ sender: row.user_id,
415
+ content
416
+ };
417
+ if (matchesFilter(p.filter.presence, ev)) presenceEvents.push(ev);
418
+ }
419
+ }
420
+ if (presenceEvents.length) empty = false;
421
+ const profileUpdates = await profileUpdatesFor(hs, p, memberships);
422
+ if (profileUpdates) empty = false;
423
+ const body = {
424
+ next_batch: formatToken({
425
+ pos: p.to,
426
+ toDevice: nextToDevice
427
+ }),
428
+ rooms: Object.fromEntries(Object.entries({
429
+ join,
430
+ invite,
431
+ leave,
432
+ knock
433
+ }).filter(([, v]) => Object.keys(v).length)),
434
+ presence: { events: presenceEvents },
435
+ account_data: { events: globalAccountData },
436
+ to_device: { events: toDevice.map((r) => ({
437
+ sender: r.sender,
438
+ type: r.type,
439
+ content: r.content
440
+ })) },
441
+ device_one_time_keys_count: await hs.keys.oneTimeKeyCounts(userId, p.session.deviceId),
442
+ device_unused_fallback_key_types: await hs.keys.unusedFallbackKeyTypes(userId, p.session.deviceId)
443
+ };
444
+ if (profileUpdates) body[UNSTABLE.ORG.MATRIX.MSC4429.USERS] = profileUpdates;
445
+ if (since !== null) {
446
+ const lists = await hs.keys.changesSince(userId, since, p.to);
447
+ body.device_lists = lists;
448
+ if (lists.changed.length || lists.left.length) empty = false;
449
+ }
450
+ return {
451
+ body,
452
+ empty,
453
+ nextToDevice
454
+ };
455
+ }
456
+ async function sync(ctx) {
457
+ const hs = ctx.hs;
458
+ const session = await ctx.auth();
459
+ const filter = await loadFilter(hs, session.userId, ctx.query.get("filter"));
460
+ const since = parseSyncToken(ctx.query.get("since"));
461
+ const fullState = ctx.query.get("full_state") === "true";
462
+ const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
463
+ const setPresence = ctx.query.get("set_presence") ?? "online";
464
+ if (setPresence !== "offline") await touchPresence(hs, session.userId, setPresence);
465
+ const deadline = Date.now() + timeout;
466
+ for (;;) {
467
+ const to = await hs.streams.position();
468
+ const ignoredRow = await hs.db.findOne("account_data", [
469
+ {
470
+ field: "user_id",
471
+ value: session.userId
472
+ },
473
+ {
474
+ field: "room_id",
475
+ value: ""
476
+ },
477
+ {
478
+ field: "type",
479
+ value: M.IGNORED_USER_LIST
480
+ }
481
+ ]);
482
+ const ignored = new Set(Object.keys(ignoredRow?.content?.ignored_users ?? {}));
483
+ const stable = ctx.query.get("use_state_after") === "true";
484
+ const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
485
+ const { body, empty } = await buildSync(hs, {
486
+ session,
487
+ ignored,
488
+ since,
489
+ to,
490
+ filter,
491
+ fullState,
492
+ useStateAfter: stable || unstable,
493
+ stateAfterKey: stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER
494
+ });
495
+ const remaining = deadline - Date.now();
496
+ if (!since || !empty || remaining <= 0 || fullState) return body;
497
+ if (await hs.streams.position() > to) continue;
498
+ await hs.notifier.wait(session.userId, remaining, ctx.request.signal);
499
+ if (ctx.request.signal?.aborted) return body;
500
+ }
501
+ }
502
+ async function touchPresence(hs, userId, presence) {
503
+ const state = presence === "unavailable" ? "unavailable" : "online";
504
+ const row = await hs.db.findOne("presence", [{
505
+ field: "user_id",
506
+ value: userId
507
+ }]);
508
+ if (row && row.presence === state) {
509
+ await hs.db.update("presence", [{
510
+ field: "user_id",
511
+ value: userId
512
+ }], { last_active_ts: Date.now() });
513
+ return;
514
+ }
515
+ await hs.streams.write((stream) => hs.db.upsert("presence", {
516
+ user_id: userId,
517
+ presence: state,
518
+ status_msg: row?.status_msg ?? null,
519
+ last_active_ts: Date.now(),
520
+ stream
521
+ }));
522
+ await hs.federation.onPresence(userId, state, row?.status_msg ?? null).catch(() => {});
523
+ }
524
+ function registerSyncRoutes(r) {
525
+ r.client("GET", "/sync", procedure({ handler: sync }));
526
+ r.client("POST", "/user/:userId/filter", procedure({
527
+ auth: "user",
528
+ input: body({}),
529
+ handler: async (ctx) => {
530
+ const s = ctx.session;
531
+ if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot create filters for other users");
532
+ const filter = validateSyncFilter(ctx.input);
533
+ const existing = await ctx.hs.db.findMany("filters", { where: [{
534
+ field: "user_id",
535
+ value: s.userId
536
+ }] });
537
+ const filterId = String(existing.reduce((max, f) => Math.max(max, Number(f.filter_id) || 0), 0) + 1);
538
+ await ctx.hs.db.create("filters", {
539
+ user_id: s.userId,
540
+ filter_id: filterId,
541
+ filter
542
+ });
543
+ return { filter_id: filterId };
544
+ }
545
+ }));
546
+ r.client("GET", "/user/:userId/filter/:filterId", procedure({
547
+ auth: "user",
548
+ handler: async (ctx) => {
549
+ const s = ctx.session;
550
+ if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot read filters of other users");
551
+ const row = await ctx.hs.db.findOne("filters", [{
552
+ field: "user_id",
553
+ value: s.userId
554
+ }, {
555
+ field: "filter_id",
556
+ value: ctx.params.filterId
557
+ }]);
558
+ if (!row) throw errors.notFound("Unknown filter");
559
+ return row.filter;
560
+ }
561
+ }));
562
+ }
563
+ /**
564
+ * MSC4429: profile changes of users sharing a room, limited to the fields the filter asks for.
565
+ * The initial sync carries current values; users we no longer share any room with get `null`.
566
+ */
567
+ async function profileUpdatesFor(hs, p, memberships) {
568
+ const ids = (p.filter.profile_fields ?? p.filter[UNSTABLE.ORG.MATRIX.MSC4429.PROFILE_FIELDS])?.ids;
569
+ if (!Array.isArray(ids) || !ids.length) return null;
570
+ const wanted = new Set(ids.filter((x) => typeof x === "string"));
571
+ const since = p.since?.pos ?? null;
572
+ const sharing = await hs.rooms.usersSharingRooms(p.session.userId);
573
+ const out = {};
574
+ const add = (userId, field, value) => {
575
+ (out[userId] ??= { profile_updates: {} }).profile_updates[field] = value ?? null;
576
+ };
577
+ const full = new Set(since === null ? sharing : []);
578
+ if (since !== null) {
579
+ const myRooms = new Set(memberships.filter((m) => m.membership === "join").map((m) => m.room_id));
580
+ const changes = await hs.db.findMany("memberships", { where: [{
581
+ field: "stream",
582
+ operator: "gt",
583
+ value: since
584
+ }] });
585
+ for (const c of changes) {
586
+ if (c.stream > p.to || c.user_id === p.session.userId) continue;
587
+ if (c.membership === "join" && myRooms.has(c.room_id)) {
588
+ if ((await hs.rooms.getEvent(c.event_id))?.pdu.unsigned?.prev_content?.membership !== "join") full.add(c.user_id);
589
+ } else if (!sharing.has(c.user_id) && (myRooms.has(c.room_id) || memberships.some((m) => m.room_id === c.room_id))) out[c.user_id] = { profile_updates: null };
590
+ }
591
+ const rows = await hs.db.findMany("profile_fields", { where: [{
592
+ field: "stream",
593
+ operator: "gt",
594
+ value: since
595
+ }] });
596
+ for (const row of rows) {
597
+ if (row.stream > p.to || !wanted.has(row.field) || !sharing.has(row.user_id) || full.has(row.user_id)) continue;
598
+ add(row.user_id, row.field, row.value);
599
+ }
600
+ }
601
+ for (const userId of full) {
602
+ if (!sharing.has(userId) || !hs.isLocalUser(userId)) continue;
603
+ const profile = await fullProfile(hs, userId) ?? {};
604
+ for (const field of wanted) if (profile[field] !== void 0) add(userId, field, profile[field]);
605
+ }
606
+ return Object.keys(out).length ? out : null;
607
+ }
608
+ //#endregion
609
+ export { registerSyncRoutes, roomEphemeral, roomSummary, roomTimeline, toStripped };