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