matrix-nodejs-homeserver 0.2.0 → 0.3.2

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 (99) hide show
  1. package/README.md +89 -11
  2. package/dist/adapters/cluster.d.ts +1 -26
  3. package/dist/adapters/cluster.js +50 -103
  4. package/dist/api/account.js +407 -354
  5. package/dist/api/admin.js +82 -61
  6. package/dist/api/appservice.js +37 -25
  7. package/dist/api/backup.js +73 -62
  8. package/dist/api/federation.js +172 -138
  9. package/dist/api/filters.js +4 -3
  10. package/dist/api/legacy.js +91 -78
  11. package/dist/api/media.js +71 -55
  12. package/dist/api/misc.js +46 -37
  13. package/dist/api/preview.js +37 -73
  14. package/dist/api/profile.js +108 -97
  15. package/dist/api/push.js +114 -99
  16. package/dist/api/relationships.js +64 -58
  17. package/dist/api/rooms.js +703 -580
  18. package/dist/api/rtc.d.ts +0 -1
  19. package/dist/api/rtc.js +48 -34
  20. package/dist/api/search.js +133 -127
  21. package/dist/api/slidingsync.js +41 -31
  22. package/dist/api/spaces.js +82 -77
  23. package/dist/api/sync.js +325 -135
  24. package/dist/api/threadsubs.js +72 -61
  25. package/dist/api/threepid.js +99 -66
  26. package/dist/api/uia.js +17 -16
  27. package/dist/api/user.js +221 -178
  28. package/dist/core/admin.d.ts +0 -1
  29. package/dist/core/admin.js +6 -5
  30. package/dist/core/adminroom.js +9 -8
  31. package/dist/core/appservices.d.ts +2 -1
  32. package/dist/core/appservices.js +5 -4
  33. package/dist/core/audit.d.ts +0 -1
  34. package/dist/core/authrules.js +26 -25
  35. package/dist/core/cluster.d.ts +12 -5
  36. package/dist/core/cluster.js +46 -29
  37. package/dist/core/delayed.js +2 -1
  38. package/dist/core/events.js +11 -10
  39. package/dist/core/html.js +13 -0
  40. package/dist/core/identity.js +1 -1
  41. package/dist/core/invitefilter.js +2 -1
  42. package/dist/core/json.js +10 -1
  43. package/dist/core/keys.d.ts +10 -1
  44. package/dist/core/keys.js +62 -42
  45. package/dist/core/m.d.ts +209 -0
  46. package/dist/core/m.js +182 -0
  47. package/dist/core/outbound.d.ts +16 -0
  48. package/dist/core/outbound.js +172 -0
  49. package/dist/core/partialstate.js +2 -1
  50. package/dist/core/policy.js +4 -3
  51. package/dist/core/push.d.ts +0 -1
  52. package/dist/core/push.js +41 -28
  53. package/dist/core/pushrules.js +36 -35
  54. package/dist/core/ratelimit.d.ts +1 -1
  55. package/dist/core/rooms.d.ts +10 -1
  56. package/dist/core/rooms.js +101 -74
  57. package/dist/core/semaphore.js +23 -0
  58. package/dist/core/sso.d.ts +15 -1
  59. package/dist/core/sso.js +38 -2
  60. package/dist/core/stateres.js +7 -6
  61. package/dist/core/threepid.js +1 -9
  62. package/dist/federation/client.js +9 -1
  63. package/dist/federation/index.d.ts +2 -2
  64. package/dist/federation/index.js +43 -35
  65. package/dist/homeserver.d.ts +23 -4
  66. package/dist/homeserver.js +12 -3
  67. package/dist/http/handler.js +2 -23
  68. package/dist/http/input.js +2 -2
  69. package/dist/http/procedure.js +25 -0
  70. package/dist/http/router.d.ts +9 -26
  71. package/dist/http/router.js +10 -30
  72. package/dist/index.d.ts +4 -3
  73. package/dist/index.js +3 -2
  74. package/dist/storage/batch.js +11 -0
  75. package/dist/storage/memory.js +1 -0
  76. package/dist/storage/schema.js +1 -1
  77. package/dist/storage/sql.js +20 -1
  78. package/dist/storage/types.d.ts +16 -0
  79. package/dist/vendor/zod/LICENSE +21 -0
  80. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  81. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  82. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  83. package/dist/vendor/zod/v4/core/api.js +512 -0
  84. package/dist/vendor/zod/v4/core/checks.js +300 -0
  85. package/dist/vendor/zod/v4/core/core.js +109 -0
  86. package/dist/vendor/zod/v4/core/doc.js +35 -0
  87. package/dist/vendor/zod/v4/core/errors.js +118 -0
  88. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  89. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  90. package/dist/vendor/zod/v4/core/parse.js +206 -0
  91. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  92. package/dist/vendor/zod/v4/core/registries.js +48 -0
  93. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  94. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  95. package/dist/vendor/zod/v4/core/util.js +537 -0
  96. package/dist/vendor/zod/v4/core/versions.js +8 -0
  97. package/dist/vendor/zod/v4/locales/en.js +110 -0
  98. package/package.json +8 -6
  99. package/dist/core/pushrules.d.ts +0 -1
package/dist/api/sync.js CHANGED
@@ -1,10 +1,15 @@
1
+ import { M, UNSTABLE } from "../core/m.js";
1
2
  import { eventIdOf, splitStateKey, stateKey, toClientEvent } from "../core/events.js";
2
3
  import { errors } from "../core/errors.js";
3
4
  import { getState } from "../core/authrules.js";
4
5
  import { loadRules } from "../core/pushrules.js";
5
6
  import { inviteDecision } from "../core/invitefilter.js";
7
+ import { deviceListChangesQuery } from "../core/keys.js";
6
8
  import { body } from "../http/input.js";
9
+ import { procedure } from "../http/procedure.js";
7
10
  import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
11
+ import { readBatch } from "../storage/batch.js";
12
+ import { Semaphore } from "../core/semaphore.js";
8
13
  import { fullProfile } from "../core/profile.js";
9
14
  //#region src/api/sync.ts
10
15
  function parseSyncToken(raw) {
@@ -62,7 +67,8 @@ async function snapshotAt(hs, roomId, pos) {
62
67
  field: "stream",
63
68
  direction: "desc"
64
69
  },
65
- limit: 1
70
+ limit: 1,
71
+ fields: ["state_snapshot"]
66
72
  }))[0]?.state_snapshot ?? 0;
67
73
  }
68
74
  /** Build the timeline + state section of a room. */
@@ -124,7 +130,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
124
130
  }
125
131
  if (limit === 0 && events.length === 0) limited = (await hs.db.findMany("events", {
126
132
  where,
127
- limit: 1
133
+ limit: 1,
134
+ fields: ["stream"]
128
135
  })).length > 0;
129
136
  if (since === null) events.sort((a, b) => (a.pdu.depth ?? 0) - (b.pdu.depth ?? 0) || a.stream - b.stream);
130
137
  else events.reverse();
@@ -142,7 +149,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
142
149
  operator: "lt",
143
150
  value: events[0].stream
144
151
  }],
145
- limit: 1
152
+ limit: 1,
153
+ fields: ["stream"]
146
154
  })).length > 0;
147
155
  const startPos = events.length ? Math.min(...events.map((e) => e.stream)) - 1 : upTo;
148
156
  const startState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, startPos));
@@ -163,13 +171,13 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
163
171
  if (sinceIds[key] === ev.event_id) continue;
164
172
  const [type, sk] = splitStateKey(key);
165
173
  if (!matchesFilter(sf, ev)) continue;
166
- if (lazy && type === "m.room.member" && !senders.has(sk)) continue;
174
+ if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
167
175
  state.push(ev);
168
176
  }
169
177
  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);
178
+ const key = stateKey(M.ROOM.MEMBER, e.pdu.sender);
179
+ if (startState.has(key) || state.some((s) => s.type === M.ROOM.MEMBER && s.state_key === e.pdu.sender)) continue;
180
+ 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
181
  if (member && matchesFilter(sf, member)) state.push(member);
174
182
  }
175
183
  const stateAfter = [];
@@ -179,7 +187,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
179
187
  if (sinceIds[key] === ev.event_id) continue;
180
188
  const [type, sk] = splitStateKey(key);
181
189
  if (!matchesFilter(sf, ev)) continue;
182
- if (lazy && type === "m.room.member" && !senders.has(sk)) continue;
190
+ if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
183
191
  stateAfter.push(ev);
184
192
  }
185
193
  }
@@ -194,12 +202,12 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
194
202
  timelineEvents: events
195
203
  };
196
204
  }
197
- async function roomEphemeral(hs, p, roomId) {
205
+ async function roomEphemeral(hs, p, roomId, prefetched) {
198
206
  const since = p.since?.pos ?? null;
199
207
  const out = [];
200
208
  const typing = hs.typing.typingSince(roomId, since);
201
209
  if (typing !== null && (since !== null || typing.length > 0)) out.push({
202
- type: "m.typing",
210
+ type: M.TYPING,
203
211
  content: { user_ids: typing }
204
212
  });
205
213
  const where = [{
@@ -211,10 +219,10 @@ async function roomEphemeral(hs, p, roomId) {
211
219
  operator: "gt",
212
220
  value: since
213
221
  });
214
- const receipts = (await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
222
+ const receipts = (prefetched ?? await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
215
223
  const content = {};
216
224
  for (const r of receipts) {
217
- if (r.receipt_type === "m.read.private" && r.user_id !== p.session.userId) continue;
225
+ if (r.receipt_type === M.READ.PRIVATE && r.user_id !== p.session.userId) continue;
218
226
  content[r.event_id] ??= {};
219
227
  content[r.event_id][r.receipt_type] ??= {};
220
228
  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;
@@ -224,12 +232,12 @@ async function roomEphemeral(hs, p, roomId) {
224
232
  };
225
233
  }
226
234
  if (Object.keys(content).length) out.push({
227
- type: "m.receipt",
235
+ type: M.RECEIPT,
228
236
  content
229
237
  });
230
238
  return out.filter((e) => matchesFilter(p.filter.room?.ephemeral, e));
231
239
  }
232
- async function accountData(hs, p, roomId) {
240
+ async function accountData(hs, p, roomId, prefetched) {
233
241
  const where = [{
234
242
  field: "user_id",
235
243
  value: p.session.userId
@@ -242,7 +250,7 @@ async function accountData(hs, p, roomId) {
242
250
  operator: "gt",
243
251
  value: p.since.pos
244
252
  });
245
- const rows = await hs.db.findMany("account_data", { where });
253
+ const rows = prefetched ?? await hs.db.findMany("account_data", { where });
246
254
  const filter = roomId ? p.filter.room?.account_data : p.filter.account_data;
247
255
  return rows.filter((r) => r.stream <= p.to && (p.since || Object.keys(r.content ?? {}).length > 0)).map((r) => ({
248
256
  type: r.type,
@@ -256,26 +264,206 @@ async function roomSummary(hs, roomId, userId) {
256
264
  const heroes = [];
257
265
  for (const [key, ev] of state) {
258
266
  const [type, sk] = splitStateKey(key);
259
- if (type !== "m.room.member") continue;
267
+ if (type !== M.ROOM.MEMBER) continue;
260
268
  const m = ev.content?.membership;
261
269
  if (m === "join") joined++;
262
270
  if (m === "invite") invited++;
263
271
  if ((m === "join" || m === "invite") && sk !== userId && heroes.length < 5) heroes.push(sk);
264
272
  }
265
273
  const summary = {
266
- "m.joined_member_count": joined,
267
- "m.invited_member_count": invited
274
+ [M.JOINED_MEMBER_COUNT]: joined,
275
+ [M.INVITED_MEMBER_COUNT]: invited
268
276
  };
269
- if (!getState(state, "m.room.name")?.content?.name && !getState(state, "m.room.canonical_alias")?.content?.alias) summary["m.heroes"] = heroes.sort();
277
+ if (!getState(state, M.ROOM.NAME)?.content?.name && !getState(state, M.ROOM.CANONICAL_ALIAS)?.content?.alias) summary[M.HEROES] = heroes.sort();
270
278
  return summary;
271
279
  }
280
+ /**
281
+ * What changed for the user since the token, for all of their rooms at once: the rooms with new
282
+ * events or newly complete state, and the new receipts and account data by room.
283
+ *
284
+ * https://github.com/k18k/matrix-nodejs-homeserver/issues/33: an incremental sync queried every
285
+ * joined room (timeline, receipts, account data) and only then found that most had nothing new,
286
+ * so a sync cost as many queries as the user had rooms, each time a long-poll woke up. These few
287
+ * queries find the rooms that changed, and only those are built.
288
+ */
289
+ /** One-time key counts by algorithm, as /sync reports them (signed_curve25519 always present). */
290
+ function countAlgorithms(rows) {
291
+ const counts = { signed_curve25519: 0 };
292
+ for (const r of rows) counts[r.algorithm] = (counts[r.algorithm] ?? 0) + 1;
293
+ return counts;
294
+ }
295
+ async function changesSince(hs, p, memberships, since, accountRows) {
296
+ const joined = memberships.filter((m) => m.membership === "join").map((m) => m.room_id);
297
+ const anyMembership = [...new Set(memberships.map((m) => m.room_id))];
298
+ const range = [{
299
+ field: "stream",
300
+ operator: "gt",
301
+ value: since
302
+ }, {
303
+ field: "stream",
304
+ operator: "lte",
305
+ value: p.to
306
+ }];
307
+ const chunks = (list) => Array.from({ length: Math.ceil(list.length / 500) }, (_, i) => list.slice(i * 500, i * 500 + 500));
308
+ const inRooms = (chunk) => ({
309
+ field: "room_id",
310
+ operator: "in",
311
+ value: chunk
312
+ });
313
+ const queries = [];
314
+ for (const chunk of chunks(joined)) {
315
+ queries.push({
316
+ model: "events",
317
+ opts: {
318
+ where: [inRooms(chunk), ...range],
319
+ fields: ["room_id"]
320
+ }
321
+ });
322
+ queries.push({
323
+ model: "receipts",
324
+ opts: { where: [inRooms(chunk), ...range] }
325
+ });
326
+ queries.push({
327
+ model: "partial_state_rooms",
328
+ opts: {
329
+ where: [inRooms(chunk), {
330
+ field: "completed_stream",
331
+ operator: "gt",
332
+ value: since
333
+ }],
334
+ fields: ["room_id"]
335
+ }
336
+ });
337
+ }
338
+ for (const chunk of chunks(anyMembership)) queries.push({
339
+ model: "memberships",
340
+ opts: { where: [inRooms(chunk), {
341
+ field: "stream",
342
+ operator: "gt",
343
+ value: since
344
+ }] }
345
+ });
346
+ const results = await readBatch(hs.db, queries);
347
+ const touched = /* @__PURE__ */ new Set();
348
+ const receipts = /* @__PURE__ */ new Map();
349
+ const joinedChunks = Math.ceil(joined.length / 500);
350
+ for (let c = 0; c < joinedChunks; c++) {
351
+ const [events, receiptRows, completed] = results.slice(c * 3, c * 3 + 3);
352
+ for (const r of events) touched.add(r.room_id);
353
+ for (const r of receiptRows) {
354
+ touched.add(r.room_id);
355
+ receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
356
+ }
357
+ for (const r of completed) touched.add(r.room_id);
358
+ }
359
+ const memberChanges = results.slice(joinedChunks * 3).flat();
360
+ const accountData = /* @__PURE__ */ new Map();
361
+ for (const r of accountRows) {
362
+ if (r.room_id) touched.add(r.room_id);
363
+ accountData.set(r.room_id, [...accountData.get(r.room_id) ?? [], r]);
364
+ }
365
+ return {
366
+ touched,
367
+ receipts,
368
+ accountData,
369
+ memberChanges
370
+ };
371
+ }
272
372
  async function buildSync(hs, p) {
273
373
  const userId = p.session.userId;
274
374
  const since = p.since?.pos ?? null;
275
- const memberships = await hs.db.findMany("memberships", { where: [{
375
+ const incremental = since !== null && !p.fullState;
376
+ const deviceWhere = [{
276
377
  field: "user_id",
277
378
  value: userId
278
- }] });
379
+ }, {
380
+ field: "device_id",
381
+ value: p.session.deviceId
382
+ }];
383
+ const acknowledged = p.since ? hs.db.delete("to_device", [...deviceWhere, {
384
+ field: "stream",
385
+ operator: "lte",
386
+ value: p.since.toDevice
387
+ }]) : Promise.resolve(0);
388
+ const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await readBatch(hs.db, [
389
+ {
390
+ model: "memberships",
391
+ opts: { where: [{
392
+ field: "user_id",
393
+ value: userId
394
+ }] }
395
+ },
396
+ {
397
+ model: "account_data",
398
+ opts: { where: [
399
+ {
400
+ field: "user_id",
401
+ value: userId
402
+ },
403
+ {
404
+ field: "room_id",
405
+ value: ""
406
+ },
407
+ {
408
+ field: "type",
409
+ value: M.IGNORED_USER_LIST
410
+ }
411
+ ] }
412
+ },
413
+ {
414
+ model: "to_device",
415
+ opts: {
416
+ where: [...deviceWhere, {
417
+ field: "stream",
418
+ operator: "gt",
419
+ value: p.since?.toDevice ?? 0
420
+ }],
421
+ sortBy: {
422
+ field: "stream",
423
+ direction: "asc"
424
+ },
425
+ limit: 100
426
+ }
427
+ },
428
+ {
429
+ model: "one_time_keys",
430
+ opts: {
431
+ where: [...deviceWhere, {
432
+ field: "fallback",
433
+ value: false
434
+ }],
435
+ fields: ["algorithm"]
436
+ }
437
+ },
438
+ {
439
+ model: "fallback_keys",
440
+ opts: {
441
+ where: [...deviceWhere, {
442
+ field: "used",
443
+ value: false
444
+ }],
445
+ fields: ["algorithm"]
446
+ }
447
+ },
448
+ ...incremental ? [{
449
+ model: "account_data",
450
+ opts: { where: [{
451
+ field: "user_id",
452
+ value: userId
453
+ }, {
454
+ field: "stream",
455
+ operator: "gt",
456
+ value: since
457
+ }] }
458
+ }] : []
459
+ ]);
460
+ p = {
461
+ ...p,
462
+ ignored: new Set(Object.keys(ignoredRows[0]?.content?.ignored_users ?? {}))
463
+ };
464
+ const changes = incremental ? await changesSince(hs, p, memberships, since, accountRows) : null;
465
+ let memberChangesRead = changes ? Promise.resolve(changes.memberChanges) : null;
466
+ const memberChanges = () => memberChangesRead ??= hs.rooms.membershipChangesSince(userId, since ?? 0, memberships);
279
467
  const join = {};
280
468
  const invite = {};
281
469
  const leave = {};
@@ -286,12 +474,13 @@ async function buildSync(hs, p) {
286
474
  const roomId = m.room_id;
287
475
  const changed = since === null || m.stream > since;
288
476
  if (m.membership === "join") {
477
+ if (changes && !changed && !changes.touched.has(roomId) && hs.typing.typingSince(roomId, since) === null) continue;
289
478
  if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) continue;
290
479
  const completedAt = await hs.partialState.completedAt(roomId);
291
480
  const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
292
481
  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);
482
+ const ephemeral = await roomEphemeral(hs, p, roomId, changes ? changes.receipts.get(roomId) ?? [] : void 0);
483
+ const roomAccountData = await accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0);
295
484
  if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) continue;
296
485
  join[roomId] = {
297
486
  timeline,
@@ -339,68 +528,67 @@ async function buildSync(hs, p) {
339
528
  leave[roomId] = {
340
529
  timeline,
341
530
  state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) },
342
- account_data: { events: await accountData(hs, p, roomId) }
531
+ account_data: { events: await accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0) }
343
532
  };
344
533
  empty = false;
345
534
  }
346
535
  }
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",
536
+ const globalAccountData = await accountData(hs, p, "", changes ? changes.accountData.get("") ?? [] : void 0);
537
+ if (since === null && !globalAccountData.some((e) => e.type === M.PUSH_RULES)) globalAccountData.push({
538
+ type: M.PUSH_RULES,
350
539
  content: { global: await loadRules(hs, userId) }
351
540
  });
352
541
  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);
542
+ await acknowledged;
543
+ const toDevice = toDeviceRows.filter((r) => r.stream <= p.to);
377
544
  const nextToDevice = toDevice.length ? toDevice[toDevice.length - 1].stream : p.since?.toDevice ?? 0;
378
545
  if (toDevice.length) empty = false;
379
546
  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: [{
547
+ const wantPresence = !p.filter.presence?.types || p.filter.presence.types.length > 0;
548
+ const sharing = await hs.rooms.usersSharingRooms(userId);
549
+ const joinedRooms = memberships.filter((m) => m.membership === "join").map((m) => m.room_id);
550
+ const newlyShared = /* @__PURE__ */ new Set();
551
+ if (since !== null && wantPresence) {
552
+ const myRooms = new Set(joinedRooms);
553
+ for (const c of await memberChanges()) {
554
+ if (c.membership !== "join" || !myRooms.has(c.room_id)) continue;
555
+ if (c.user_id === userId) for (const u of await hs.rooms.joinedMembers(c.room_id)) newlyShared.add(u);
556
+ else newlyShared.add(c.user_id);
557
+ }
558
+ }
559
+ const sharingList = [...sharing];
560
+ const [presenceRows = [], newlySharedPresence = [], deviceRows] = await readBatch(hs.db, [
561
+ {
562
+ model: "presence",
563
+ opts: { where: [{
564
+ field: "user_id",
565
+ operator: "in",
566
+ value: wantPresence ? sharingList : []
567
+ }, ...since !== null ? [{
385
568
  field: "stream",
386
569
  operator: "gt",
387
570
  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 })) {
571
+ }] : []] }
572
+ },
573
+ {
574
+ model: "presence",
575
+ opts: { where: [{
576
+ field: "user_id",
577
+ operator: "in",
578
+ value: [...newlyShared]
579
+ }] }
580
+ },
581
+ ...since !== null ? [{
582
+ model: "device_list_changes",
583
+ opts: deviceListChangesQuery(sharing, since)
584
+ }] : []
585
+ ]);
586
+ if (wantPresence) {
587
+ const seen = /* @__PURE__ */ new Set();
588
+ for (const row of [...presenceRows, ...newlySharedPresence]) {
589
+ if (seen.has(row.user_id)) continue;
590
+ seen.add(row.user_id);
402
591
  if (row.stream > p.to) continue;
403
- if (since !== null && row.stream <= since && !newlyShared.has(row.user_id)) continue;
404
592
  const content = {
405
593
  presence: row.presence,
406
594
  last_active_ago: Date.now() - row.last_active_ts
@@ -408,7 +596,7 @@ async function buildSync(hs, p) {
408
596
  if (row.status_msg) content.status_msg = row.status_msg;
409
597
  if (row.presence === "online") content.currently_active = true;
410
598
  const ev = {
411
- type: "m.presence",
599
+ type: M.PRESENCE,
412
600
  sender: row.user_id,
413
601
  content
414
602
  };
@@ -416,7 +604,7 @@ async function buildSync(hs, p) {
416
604
  }
417
605
  }
418
606
  if (presenceEvents.length) empty = false;
419
- const profileUpdates = await profileUpdatesFor(hs, p, memberships);
607
+ const profileUpdates = await profileUpdatesFor(hs, p, memberships, memberChanges);
420
608
  if (profileUpdates) empty = false;
421
609
  const body = {
422
610
  next_batch: formatToken({
@@ -436,12 +624,16 @@ async function buildSync(hs, p) {
436
624
  type: r.type,
437
625
  content: r.content
438
626
  })) },
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)
627
+ device_one_time_keys_count: countAlgorithms(oneTimeKeyRows),
628
+ device_unused_fallback_key_types: [...new Set(fallbackRows.map((r) => r.algorithm))]
441
629
  };
442
- if (profileUpdates) body["org.matrix.msc4429.users"] = profileUpdates;
630
+ if (profileUpdates) body[UNSTABLE.ORG.MATRIX.MSC4429.USERS] = profileUpdates;
443
631
  if (since !== null) {
444
- const lists = await hs.keys.changesSince(userId, since, p.to);
632
+ const lists = await hs.keys.changesSince(userId, since, p.to, {
633
+ memberChanges: await memberChanges(),
634
+ deviceRows,
635
+ joinedRooms
636
+ });
445
637
  body.device_lists = lists;
446
638
  if (lists.changed.length || lists.left.length) empty = false;
447
639
  }
@@ -451,6 +643,12 @@ async function buildSync(hs, p) {
451
643
  nextToDevice
452
644
  };
453
645
  }
646
+ const slots = /* @__PURE__ */ new WeakMap();
647
+ function syncSlots(hs) {
648
+ let s = slots.get(hs);
649
+ if (!s) slots.set(hs, s = new Semaphore(hs.config.maxConcurrentSyncs));
650
+ return s;
651
+ }
454
652
  async function sync(ctx) {
455
653
  const hs = ctx.hs;
456
654
  const session = await ctx.auth();
@@ -463,33 +661,21 @@ async function sync(ctx) {
463
661
  const deadline = Date.now() + timeout;
464
662
  for (;;) {
465
663
  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 ?? {}));
664
+ const ignored = /* @__PURE__ */ new Set();
481
665
  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, {
666
+ const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
667
+ const useStateAfter = stable || unstable;
668
+ const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
669
+ const { body, empty } = await syncSlots(hs).run(() => buildSync(hs, {
484
670
  session,
485
671
  ignored,
486
672
  since,
487
673
  to,
488
674
  filter,
489
675
  fullState,
490
- useStateAfter: stable || unstable,
491
- stateAfterKey: stable ? "state_after" : "org.matrix.msc4222.state_after"
492
- });
676
+ useStateAfter,
677
+ stateAfterKey
678
+ }));
493
679
  const remaining = deadline - Date.now();
494
680
  if (!since || !empty || remaining <= 0 || fullState) return body;
495
681
  if (await hs.streams.position() > to) continue;
@@ -497,6 +683,7 @@ async function sync(ctx) {
497
683
  if (ctx.request.signal?.aborted) return body;
498
684
  }
499
685
  }
686
+ const LAST_ACTIVE_GRANULARITY_MS = 6e4;
500
687
  async function touchPresence(hs, userId, presence) {
501
688
  const state = presence === "unavailable" ? "unavailable" : "online";
502
689
  const row = await hs.db.findOne("presence", [{
@@ -504,7 +691,7 @@ async function touchPresence(hs, userId, presence) {
504
691
  value: userId
505
692
  }]);
506
693
  if (row && row.presence === state) {
507
- await hs.db.update("presence", [{
694
+ if (Date.now() - row.last_active_ts >= LAST_ACTIVE_GRANULARITY_MS) await hs.db.update("presence", [{
508
695
  field: "user_id",
509
696
  value: userId
510
697
  }], { last_active_ts: Date.now() });
@@ -520,43 +707,50 @@ async function touchPresence(hs, userId, presence) {
520
707
  await hs.federation.onPresence(userId, state, row?.status_msg ?? null).catch(() => {});
521
708
  }
522
709
  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
- });
710
+ r.client("GET", "/sync", procedure({ handler: sync }));
711
+ r.client("POST", "/user/:userId/filter", procedure({
712
+ auth: "user",
713
+ input: body({}),
714
+ handler: async (ctx) => {
715
+ const s = ctx.session;
716
+ if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot create filters for other users");
717
+ const filter = validateSyncFilter(ctx.input);
718
+ const existing = await ctx.hs.db.findMany("filters", { where: [{
719
+ field: "user_id",
720
+ value: s.userId
721
+ }] });
722
+ const filterId = String(existing.reduce((max, f) => Math.max(max, Number(f.filter_id) || 0), 0) + 1);
723
+ await ctx.hs.db.create("filters", {
724
+ user_id: s.userId,
725
+ filter_id: filterId,
726
+ filter
727
+ });
728
+ return { filter_id: filterId };
729
+ }
730
+ }));
731
+ r.client("GET", "/user/:userId/filter/:filterId", procedure({
732
+ auth: "user",
733
+ handler: async (ctx) => {
734
+ const s = ctx.session;
735
+ if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot read filters of other users");
736
+ const row = await ctx.hs.db.findOne("filters", [{
737
+ field: "user_id",
738
+ value: s.userId
739
+ }, {
740
+ field: "filter_id",
741
+ value: ctx.params.filterId
742
+ }]);
743
+ if (!row) throw errors.notFound("Unknown filter");
744
+ return row.filter;
745
+ }
746
+ }));
553
747
  }
554
748
  /**
555
749
  * MSC4429: profile changes of users sharing a room, limited to the fields the filter asks for.
556
750
  * The initial sync carries current values; users we no longer share any room with get `null`.
557
751
  */
558
- async function profileUpdatesFor(hs, p, memberships) {
559
- const ids = (p.filter.profile_fields ?? p.filter["org.matrix.msc4429.profile_fields"])?.ids;
752
+ async function profileUpdatesFor(hs, p, memberships, memberChanges) {
753
+ const ids = (p.filter.profile_fields ?? p.filter[UNSTABLE.ORG.MATRIX.MSC4429.PROFILE_FIELDS])?.ids;
560
754
  if (!Array.isArray(ids) || !ids.length) return null;
561
755
  const wanted = new Set(ids.filter((x) => typeof x === "string"));
562
756
  const since = p.since?.pos ?? null;
@@ -568,11 +762,7 @@ async function profileUpdatesFor(hs, p, memberships) {
568
762
  const full = new Set(since === null ? sharing : []);
569
763
  if (since !== null) {
570
764
  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
- }] });
765
+ const changes = await memberChanges();
576
766
  for (const c of changes) {
577
767
  if (c.stream > p.to || c.user_id === p.session.userId) continue;
578
768
  if (c.membership === "join" && myRooms.has(c.room_id)) {