matrix-nodejs-homeserver 0.3.2 → 0.3.4
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.
- package/README.md +58 -20
- package/dist/api/longpoll.js +61 -0
- package/dist/api/slidingsync.js +30 -32
- package/dist/api/sync.js +331 -183
- package/dist/core/admin.js +1 -0
- package/dist/core/changes.d.ts +79 -0
- package/dist/core/changes.js +332 -0
- package/dist/core/cluster.d.ts +19 -3
- package/dist/core/cluster.js +44 -4
- package/dist/core/keys.js +4 -2
- package/dist/core/notifier.d.ts +49 -3
- package/dist/core/notifier.js +123 -26
- package/dist/core/push.d.ts +12 -11
- package/dist/core/push.js +32 -24
- package/dist/core/pushrules.js +24 -5
- package/dist/core/rooms.js +7 -1
- package/dist/core/semaphore.js +1 -1
- package/dist/homeserver.d.ts +7 -1
- package/dist/homeserver.js +17 -7
- package/dist/storage/batch.js +72 -1
- package/dist/storage/schema.js +7 -2
- package/dist/storage/sql.d.ts +22 -3
- package/dist/storage/sql.js +126 -28
- package/dist/storage/types.d.ts +15 -1
- package/package.json +1 -1
package/dist/api/sync.js
CHANGED
|
@@ -2,26 +2,29 @@ import { M, UNSTABLE } from "../core/m.js";
|
|
|
2
2
|
import { eventIdOf, splitStateKey, stateKey, toClientEvent } from "../core/events.js";
|
|
3
3
|
import { errors } from "../core/errors.js";
|
|
4
4
|
import { getState } from "../core/authrules.js";
|
|
5
|
-
import {
|
|
5
|
+
import { readOnlyRules } from "../core/pushrules.js";
|
|
6
6
|
import { inviteDecision } from "../core/invitefilter.js";
|
|
7
7
|
import { deviceListChangesQuery } from "../core/keys.js";
|
|
8
8
|
import { body } from "../http/input.js";
|
|
9
9
|
import { procedure } from "../http/procedure.js";
|
|
10
|
-
import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
|
|
11
10
|
import { readBatch } from "../storage/batch.js";
|
|
11
|
+
import { trackedReads } from "../core/changes.js";
|
|
12
|
+
import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
|
|
13
|
+
import { longPoll } from "./longpoll.js";
|
|
12
14
|
import { Semaphore } from "../core/semaphore.js";
|
|
13
15
|
import { fullProfile } from "../core/profile.js";
|
|
14
16
|
//#region src/api/sync.ts
|
|
15
17
|
function parseSyncToken(raw) {
|
|
16
18
|
if (!raw) return null;
|
|
17
|
-
const m = /^s(\d+)(?:_(\d+))?$/.exec(raw);
|
|
19
|
+
const m = /^s(\d+)(?:_(\d+))?(_a)?$/.exec(raw);
|
|
18
20
|
if (!m) throw errors.invalidParam("Invalid since token");
|
|
19
21
|
return {
|
|
20
22
|
pos: Number(m[1]),
|
|
21
|
-
toDevice: Number(m[2] ?? m[1])
|
|
23
|
+
toDevice: Number(m[2] ?? m[1]),
|
|
24
|
+
ack: !!m[3]
|
|
22
25
|
};
|
|
23
26
|
}
|
|
24
|
-
const formatToken = (t) => `s${t.pos}_${t.toDevice}`;
|
|
27
|
+
const formatToken = (t) => `s${t.pos}_${t.toDevice}${t.ack ? "_a" : ""}`;
|
|
25
28
|
async function loadFilter(hs, userId, raw) {
|
|
26
29
|
if (!raw) return {};
|
|
27
30
|
if (raw.trim().startsWith("{")) {
|
|
@@ -88,6 +91,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
88
91
|
operator: "gt",
|
|
89
92
|
value: since
|
|
90
93
|
});
|
|
94
|
+
const sinceIdsRead = since !== null ? snapshotAt(hs, roomId, since).then((id) => hs.rooms.snapshotIds(id)) : Promise.resolve({});
|
|
95
|
+
const scanned = [];
|
|
91
96
|
const events = [];
|
|
92
97
|
let limited = false;
|
|
93
98
|
let cursor = upTo;
|
|
@@ -106,6 +111,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
106
111
|
limit: batchSize
|
|
107
112
|
});
|
|
108
113
|
if (rows.length === 0) break;
|
|
114
|
+
scanned.push(...rows);
|
|
109
115
|
const stored = rows.map((r) => hs.rooms.rowToStored(r));
|
|
110
116
|
const visible = await hs.rooms.filterVisible(p.session.userId, roomId, stored);
|
|
111
117
|
const ok = new Map(visible.map((v) => [v.pdu.event_id, v]));
|
|
@@ -143,25 +149,50 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
143
149
|
events.splice(0, lastGap);
|
|
144
150
|
limited = true;
|
|
145
151
|
}
|
|
146
|
-
if (!limited && events.length) limited = (await hs.db.findMany("events", {
|
|
147
|
-
where: [...where.filter((c) => c.operator !== "lte"), {
|
|
148
|
-
field: "stream",
|
|
149
|
-
operator: "lt",
|
|
150
|
-
value: events[0].stream
|
|
151
|
-
}],
|
|
152
|
-
limit: 1,
|
|
153
|
-
fields: ["stream"]
|
|
154
|
-
})).length > 0;
|
|
155
152
|
const startPos = events.length ? Math.min(...events.map((e) => e.stream)) - 1 : upTo;
|
|
156
|
-
const
|
|
153
|
+
const newestAtOrBefore = async (pos) => {
|
|
154
|
+
const row = scanned.find((r) => r.stream <= pos);
|
|
155
|
+
if (row) return row;
|
|
156
|
+
return (await hs.db.findMany("events", {
|
|
157
|
+
where: [{
|
|
158
|
+
field: "room_id",
|
|
159
|
+
value: roomId
|
|
160
|
+
}, {
|
|
161
|
+
field: "stream",
|
|
162
|
+
operator: "lte",
|
|
163
|
+
value: pos
|
|
164
|
+
}],
|
|
165
|
+
sortBy: {
|
|
166
|
+
field: "stream",
|
|
167
|
+
direction: "desc"
|
|
168
|
+
},
|
|
169
|
+
limit: 1,
|
|
170
|
+
fields: ["stream", "state_snapshot"]
|
|
171
|
+
}))[0] ?? null;
|
|
172
|
+
};
|
|
173
|
+
const [atStart, atEnd] = await Promise.all([newestAtOrBefore(startPos), startPos === upTo ? null : newestAtOrBefore(upTo)]);
|
|
174
|
+
const startSnapshot = atStart?.state_snapshot ?? 0;
|
|
175
|
+
const endSnapshot = startPos === upTo ? startSnapshot : atEnd?.state_snapshot ?? 0;
|
|
176
|
+
if (!limited && events.length) {
|
|
177
|
+
if (events[0].stream === startPos + 1) limited = atStart !== null && (since === null || atStart.stream > since);
|
|
178
|
+
else limited = (await hs.db.findMany("events", {
|
|
179
|
+
where: [...where.filter((c) => c.operator !== "lte"), {
|
|
180
|
+
field: "stream",
|
|
181
|
+
operator: "lt",
|
|
182
|
+
value: events[0].stream
|
|
183
|
+
}],
|
|
184
|
+
limit: 1,
|
|
185
|
+
fields: ["stream"]
|
|
186
|
+
})).length > 0;
|
|
187
|
+
}
|
|
188
|
+
const [startState, endState] = await Promise.all([hs.rooms.stateAtSnapshot(startSnapshot), hs.rooms.stateAtSnapshot(endSnapshot)]);
|
|
157
189
|
const timelineIds = new Set(events.map((e) => e.pdu.event_id));
|
|
158
|
-
const endState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, upTo));
|
|
159
190
|
for (const [key, ev] of endState) {
|
|
160
191
|
if (timelineIds.has(ev.event_id) || startState.get(key)?.event_id === ev.event_id) continue;
|
|
161
192
|
if (events.some((e) => e.pdu.state_key !== void 0 && stateKey(e.pdu.type, e.pdu.state_key) === key)) continue;
|
|
162
193
|
startState.set(key, ev);
|
|
163
194
|
}
|
|
164
|
-
const sinceIds =
|
|
195
|
+
const sinceIds = await sinceIdsRead;
|
|
165
196
|
const sf = p.filter.room?.state;
|
|
166
197
|
const lazy = !!sf?.lazy_load_members;
|
|
167
198
|
const senders = new Set(events.map((e) => e.pdu.sender));
|
|
@@ -181,15 +212,12 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
181
212
|
if (member && matchesFilter(sf, member)) state.push(member);
|
|
182
213
|
}
|
|
183
214
|
const stateAfter = [];
|
|
184
|
-
if (p.useStateAfter) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
|
|
191
|
-
stateAfter.push(ev);
|
|
192
|
-
}
|
|
215
|
+
if (p.useStateAfter) for (const [key, ev] of endState) {
|
|
216
|
+
if (sinceIds[key] === ev.event_id) continue;
|
|
217
|
+
const [type, sk] = splitStateKey(key);
|
|
218
|
+
if (!matchesFilter(sf, ev)) continue;
|
|
219
|
+
if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
|
|
220
|
+
stateAfter.push(ev);
|
|
193
221
|
}
|
|
194
222
|
return {
|
|
195
223
|
stateAfter,
|
|
@@ -304,6 +332,8 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
304
332
|
operator: "lte",
|
|
305
333
|
value: p.to
|
|
306
334
|
}];
|
|
335
|
+
const logs = hs.changes?.logs;
|
|
336
|
+
const mayHaveChanged = (log, rooms) => logs ? logs[log].filterChanged(rooms, since) : rooms;
|
|
307
337
|
const chunks = (list) => Array.from({ length: Math.ceil(list.length / 500) }, (_, i) => list.slice(i * 500, i * 500 + 500));
|
|
308
338
|
const inRooms = (chunk) => ({
|
|
309
339
|
field: "room_id",
|
|
@@ -311,31 +341,34 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
311
341
|
value: chunk
|
|
312
342
|
});
|
|
313
343
|
const queries = [];
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
344
|
+
const kinds = [];
|
|
345
|
+
const add = (kind, query) => {
|
|
346
|
+
kinds.push(kind);
|
|
347
|
+
queries.push(query);
|
|
348
|
+
};
|
|
349
|
+
for (const chunk of chunks(mayHaveChanged("roomEvents", joined))) add("events", {
|
|
350
|
+
model: "events",
|
|
351
|
+
opts: {
|
|
352
|
+
where: [inRooms(chunk), ...range],
|
|
353
|
+
fields: ["room_id"]
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
for (const chunk of chunks(mayHaveChanged("roomReceipts", joined))) add("receipts", {
|
|
357
|
+
model: "receipts",
|
|
358
|
+
opts: { where: [inRooms(chunk), ...range] }
|
|
359
|
+
});
|
|
360
|
+
for (const chunk of chunks(mayHaveChanged("roomPartialState", joined))) add("completed", {
|
|
361
|
+
model: "partial_state_rooms",
|
|
362
|
+
opts: {
|
|
363
|
+
where: [inRooms(chunk), {
|
|
364
|
+
field: "completed_stream",
|
|
365
|
+
operator: "gt",
|
|
366
|
+
value: since
|
|
367
|
+
}],
|
|
368
|
+
fields: ["room_id"]
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
for (const chunk of chunks(mayHaveChanged("roomMembers", anyMembership))) add("members", {
|
|
339
372
|
model: "memberships",
|
|
340
373
|
opts: { where: [inRooms(chunk), {
|
|
341
374
|
field: "stream",
|
|
@@ -343,20 +376,18 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
343
376
|
value: since
|
|
344
377
|
}] }
|
|
345
378
|
});
|
|
346
|
-
const results = await readBatch(hs.db, queries);
|
|
379
|
+
const results = queries.length ? await readBatch(hs.db, queries) : [];
|
|
347
380
|
const touched = /* @__PURE__ */ new Set();
|
|
348
381
|
const receipts = /* @__PURE__ */ new Map();
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
for (const r of
|
|
353
|
-
|
|
354
|
-
touched.add(r.room_id);
|
|
355
|
-
receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
|
|
382
|
+
const memberChanges = [];
|
|
383
|
+
results.forEach((rows, i) => {
|
|
384
|
+
const kind = kinds[i];
|
|
385
|
+
for (const r of rows) {
|
|
386
|
+
if (kind === "members") memberChanges.push(r);
|
|
387
|
+
else touched.add(r.room_id);
|
|
388
|
+
if (kind === "receipts") receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
|
|
356
389
|
}
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
const memberChanges = results.slice(joinedChunks * 3).flat();
|
|
390
|
+
});
|
|
360
391
|
const accountData = /* @__PURE__ */ new Map();
|
|
361
392
|
for (const r of accountRows) {
|
|
362
393
|
if (r.room_id) touched.add(r.room_id);
|
|
@@ -369,6 +400,23 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
369
400
|
memberChanges
|
|
370
401
|
};
|
|
371
402
|
}
|
|
403
|
+
/** Presence or device-list changes after a token read without a user filter, at most (#44). */
|
|
404
|
+
const RECENT_CHANGES = 500;
|
|
405
|
+
/** How many of a sync's joined rooms are built at once (#38). */
|
|
406
|
+
const JOINED_ROOMS_AT_ONCE = 8;
|
|
407
|
+
/** `fn` over `items` with at most `limit` running at once; results in input order. */
|
|
408
|
+
async function mapConcurrently(items, limit, fn) {
|
|
409
|
+
const out = [];
|
|
410
|
+
let next = 0;
|
|
411
|
+
const worker = async () => {
|
|
412
|
+
while (next < items.length) {
|
|
413
|
+
const i = next++;
|
|
414
|
+
out[i] = await fn(items[i]);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
372
420
|
async function buildSync(hs, p) {
|
|
373
421
|
const userId = p.session.userId;
|
|
374
422
|
const since = p.since?.pos ?? null;
|
|
@@ -380,81 +428,120 @@ async function buildSync(hs, p) {
|
|
|
380
428
|
field: "device_id",
|
|
381
429
|
value: p.session.deviceId
|
|
382
430
|
}];
|
|
383
|
-
const acknowledged = p.since ? hs.db.delete("to_device", [...deviceWhere, {
|
|
431
|
+
const acknowledged = p.since?.ack ? hs.db.delete("to_device", [...deviceWhere, {
|
|
384
432
|
field: "stream",
|
|
385
433
|
operator: "lte",
|
|
386
434
|
value: p.since.toDevice
|
|
387
435
|
}]) : Promise.resolve(0);
|
|
388
|
-
const
|
|
436
|
+
const device = `${userId}|${p.session.deviceId}`;
|
|
437
|
+
const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await trackedReads(hs.db, hs.changes, [
|
|
389
438
|
{
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
value: userId
|
|
394
|
-
}] }
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
model: "account_data",
|
|
398
|
-
opts: { where: [
|
|
399
|
-
{
|
|
439
|
+
query: {
|
|
440
|
+
model: "memberships",
|
|
441
|
+
opts: { where: [{
|
|
400
442
|
field: "user_id",
|
|
401
443
|
value: userId
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
field: "type",
|
|
409
|
-
value: M.IGNORED_USER_LIST
|
|
410
|
-
}
|
|
411
|
-
] }
|
|
444
|
+
}] }
|
|
445
|
+
},
|
|
446
|
+
cached: {
|
|
447
|
+
name: "userMemberships",
|
|
448
|
+
key: userId
|
|
449
|
+
}
|
|
412
450
|
},
|
|
413
451
|
{
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
where: [
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
452
|
+
query: {
|
|
453
|
+
model: "account_data",
|
|
454
|
+
opts: { where: [
|
|
455
|
+
{
|
|
456
|
+
field: "user_id",
|
|
457
|
+
value: userId
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
field: "room_id",
|
|
461
|
+
value: ""
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
field: "type",
|
|
465
|
+
value: M.IGNORED_USER_LIST
|
|
466
|
+
}
|
|
467
|
+
] }
|
|
468
|
+
},
|
|
469
|
+
cached: {
|
|
470
|
+
name: "ignoredUsers",
|
|
471
|
+
key: userId
|
|
426
472
|
}
|
|
427
473
|
},
|
|
428
474
|
{
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
475
|
+
query: {
|
|
476
|
+
model: "to_device",
|
|
477
|
+
opts: {
|
|
478
|
+
where: [...deviceWhere, {
|
|
479
|
+
field: "stream",
|
|
480
|
+
operator: "gt",
|
|
481
|
+
value: p.since?.toDevice ?? 0
|
|
482
|
+
}],
|
|
483
|
+
sortBy: {
|
|
484
|
+
field: "stream",
|
|
485
|
+
direction: "asc"
|
|
486
|
+
},
|
|
487
|
+
limit: 100
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
unlessChanged: {
|
|
491
|
+
log: "toDevice",
|
|
492
|
+
key: device,
|
|
493
|
+
after: p.since?.toDevice ?? 0
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
query: {
|
|
498
|
+
model: "one_time_keys",
|
|
499
|
+
opts: {
|
|
500
|
+
where: [...deviceWhere, {
|
|
501
|
+
field: "fallback",
|
|
502
|
+
value: false
|
|
503
|
+
}],
|
|
504
|
+
fields: ["algorithm"]
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
cached: {
|
|
508
|
+
name: "oneTimeKeys",
|
|
509
|
+
key: device
|
|
436
510
|
}
|
|
437
511
|
},
|
|
438
512
|
{
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
513
|
+
query: {
|
|
514
|
+
model: "fallback_keys",
|
|
515
|
+
opts: {
|
|
516
|
+
where: [...deviceWhere, {
|
|
517
|
+
field: "used",
|
|
518
|
+
value: false
|
|
519
|
+
}],
|
|
520
|
+
fields: ["algorithm"]
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
cached: {
|
|
524
|
+
name: "fallbackKeys",
|
|
525
|
+
key: device
|
|
446
526
|
}
|
|
447
527
|
},
|
|
448
528
|
...incremental ? [{
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
529
|
+
query: {
|
|
530
|
+
model: "account_data",
|
|
531
|
+
opts: { where: [{
|
|
532
|
+
field: "user_id",
|
|
533
|
+
value: userId
|
|
534
|
+
}, {
|
|
535
|
+
field: "stream",
|
|
536
|
+
operator: "gt",
|
|
537
|
+
value: since
|
|
538
|
+
}] }
|
|
539
|
+
},
|
|
540
|
+
unlessChanged: {
|
|
541
|
+
log: "accountData",
|
|
542
|
+
key: userId,
|
|
543
|
+
after: since
|
|
544
|
+
}
|
|
458
545
|
}] : []
|
|
459
546
|
]);
|
|
460
547
|
p = {
|
|
@@ -469,39 +556,14 @@ async function buildSync(hs, p) {
|
|
|
469
556
|
const leave = {};
|
|
470
557
|
const knock = {};
|
|
471
558
|
let empty = true;
|
|
559
|
+
const joinedBuilds = [];
|
|
472
560
|
for (const m of memberships) {
|
|
473
561
|
if (m.stream > p.to || !roomAllowed(p.filter, m.room_id)) continue;
|
|
474
562
|
const roomId = m.room_id;
|
|
475
563
|
const changed = since === null || m.stream > since;
|
|
476
564
|
if (m.membership === "join") {
|
|
477
565
|
if (changes && !changed && !changes.touched.has(roomId) && hs.typing.typingSince(roomId, since) === null) continue;
|
|
478
|
-
|
|
479
|
-
const completedAt = await hs.partialState.completedAt(roomId);
|
|
480
|
-
const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
|
|
481
|
-
const { timeline, state, stateAfter, timelineEvents } = await roomTimeline(hs, p, roomId, initial ? null : since, p.to);
|
|
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);
|
|
484
|
-
if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) continue;
|
|
485
|
-
join[roomId] = {
|
|
486
|
-
timeline,
|
|
487
|
-
...p.useStateAfter ? { [p.stateAfterKey ?? "state_after"]: { events: stateAfter.map((e) => toClientEvent(e, { includeRoomId: false })) } } : { state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) } },
|
|
488
|
-
ephemeral: { events: ephemeral },
|
|
489
|
-
account_data: { events: roomAccountData },
|
|
490
|
-
summary: await roomSummary(hs, roomId, userId)
|
|
491
|
-
};
|
|
492
|
-
const counts = await hs.push.unreadCounts(userId, roomId);
|
|
493
|
-
if (p.filter.room?.timeline?.unread_thread_notifications) {
|
|
494
|
-
join[roomId].unread_notifications = counts.room;
|
|
495
|
-
if (Object.keys(counts.threads).length) join[roomId].unread_thread_notifications = counts.threads;
|
|
496
|
-
} else {
|
|
497
|
-
const total = { ...counts.room };
|
|
498
|
-
for (const t of Object.values(counts.threads)) {
|
|
499
|
-
total.notification_count += t.notification_count;
|
|
500
|
-
total.highlight_count += t.highlight_count;
|
|
501
|
-
}
|
|
502
|
-
join[roomId].unread_notifications = total;
|
|
503
|
-
}
|
|
504
|
-
empty = false;
|
|
566
|
+
joinedBuilds.push(m);
|
|
505
567
|
} else if (m.membership === "invite") {
|
|
506
568
|
if (!changed && !p.fullState) continue;
|
|
507
569
|
const pdu = await hs.rooms.getPdu(m.event_id);
|
|
@@ -533,10 +595,49 @@ async function buildSync(hs, p) {
|
|
|
533
595
|
empty = false;
|
|
534
596
|
}
|
|
535
597
|
}
|
|
598
|
+
const joinedRoom = async (m) => {
|
|
599
|
+
const roomId = m.room_id;
|
|
600
|
+
if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) return null;
|
|
601
|
+
const completedAt = await hs.partialState.completedAt(roomId);
|
|
602
|
+
const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
|
|
603
|
+
const [{ timeline, state, stateAfter, timelineEvents }, ephemeral, roomAccountData, summary, counts] = await Promise.all([
|
|
604
|
+
roomTimeline(hs, p, roomId, initial ? null : since, p.to),
|
|
605
|
+
roomEphemeral(hs, p, roomId, changes ? changes.receipts.get(roomId) ?? [] : void 0),
|
|
606
|
+
accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0),
|
|
607
|
+
roomSummary(hs, roomId, userId),
|
|
608
|
+
hs.push.unreadCounts(userId, roomId)
|
|
609
|
+
]);
|
|
610
|
+
if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) return null;
|
|
611
|
+
const room = {
|
|
612
|
+
timeline,
|
|
613
|
+
...p.useStateAfter ? { [p.stateAfterKey ?? "state_after"]: { events: stateAfter.map((e) => toClientEvent(e, { includeRoomId: false })) } } : { state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) } },
|
|
614
|
+
ephemeral: { events: ephemeral },
|
|
615
|
+
account_data: { events: roomAccountData },
|
|
616
|
+
summary
|
|
617
|
+
};
|
|
618
|
+
if (p.filter.room?.timeline?.unread_thread_notifications) {
|
|
619
|
+
room.unread_notifications = counts.room;
|
|
620
|
+
if (Object.keys(counts.threads).length) room.unread_thread_notifications = counts.threads;
|
|
621
|
+
} else {
|
|
622
|
+
const total = { ...counts.room };
|
|
623
|
+
for (const t of Object.values(counts.threads)) {
|
|
624
|
+
total.notification_count += t.notification_count;
|
|
625
|
+
total.highlight_count += t.highlight_count;
|
|
626
|
+
}
|
|
627
|
+
room.unread_notifications = total;
|
|
628
|
+
}
|
|
629
|
+
return room;
|
|
630
|
+
};
|
|
631
|
+
const built = await mapConcurrently(joinedBuilds, JOINED_ROOMS_AT_ONCE, joinedRoom);
|
|
632
|
+
joinedBuilds.forEach((m, i) => {
|
|
633
|
+
if (!built[i]) return;
|
|
634
|
+
join[m.room_id] = built[i];
|
|
635
|
+
empty = false;
|
|
636
|
+
});
|
|
536
637
|
const globalAccountData = await accountData(hs, p, "", changes ? changes.accountData.get("") ?? [] : void 0);
|
|
537
638
|
if (since === null && !globalAccountData.some((e) => e.type === M.PUSH_RULES)) globalAccountData.push({
|
|
538
639
|
type: M.PUSH_RULES,
|
|
539
|
-
content: { global: await
|
|
640
|
+
content: { global: await readOnlyRules(hs, userId) }
|
|
540
641
|
});
|
|
541
642
|
if (globalAccountData.length) empty = false;
|
|
542
643
|
await acknowledged;
|
|
@@ -557,32 +658,75 @@ async function buildSync(hs, p) {
|
|
|
557
658
|
}
|
|
558
659
|
}
|
|
559
660
|
const sharingList = [...sharing];
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
field: "user_id",
|
|
565
|
-
operator: "in",
|
|
566
|
-
value: wantPresence ? sharingList : []
|
|
567
|
-
}, ...since !== null ? [{
|
|
661
|
+
const recent = (model) => ({
|
|
662
|
+
model,
|
|
663
|
+
opts: {
|
|
664
|
+
where: [{
|
|
568
665
|
field: "stream",
|
|
569
666
|
operator: "gt",
|
|
570
667
|
value: since
|
|
571
|
-
}]
|
|
668
|
+
}],
|
|
669
|
+
sortBy: {
|
|
670
|
+
field: "stream",
|
|
671
|
+
direction: "asc"
|
|
672
|
+
},
|
|
673
|
+
limit: 501
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
const presenceQuery = since === null ? {
|
|
677
|
+
model: "presence",
|
|
678
|
+
opts: { where: [{
|
|
679
|
+
field: "user_id",
|
|
680
|
+
operator: "in",
|
|
681
|
+
value: wantPresence ? sharingList : []
|
|
682
|
+
}] }
|
|
683
|
+
} : wantPresence ? recent("presence") : {
|
|
684
|
+
model: "presence",
|
|
685
|
+
opts: { where: [{
|
|
686
|
+
field: "user_id",
|
|
687
|
+
operator: "in",
|
|
688
|
+
value: []
|
|
689
|
+
}] }
|
|
690
|
+
};
|
|
691
|
+
let [presenceRows = [], newlySharedPresence = [], deviceRows] = await trackedReads(hs.db, hs.changes, [
|
|
692
|
+
since === null ? { query: presenceQuery } : {
|
|
693
|
+
query: presenceQuery,
|
|
694
|
+
unlessChanged: {
|
|
695
|
+
log: "presence",
|
|
696
|
+
key: "*",
|
|
697
|
+
after: since
|
|
698
|
+
}
|
|
572
699
|
},
|
|
573
|
-
{
|
|
700
|
+
{ query: {
|
|
574
701
|
model: "presence",
|
|
575
702
|
opts: { where: [{
|
|
576
703
|
field: "user_id",
|
|
577
704
|
operator: "in",
|
|
578
705
|
value: [...newlyShared]
|
|
579
706
|
}] }
|
|
580
|
-
},
|
|
707
|
+
} },
|
|
581
708
|
...since !== null ? [{
|
|
582
|
-
|
|
583
|
-
|
|
709
|
+
query: recent("device_list_changes"),
|
|
710
|
+
unlessChanged: {
|
|
711
|
+
log: "deviceLists",
|
|
712
|
+
key: "*",
|
|
713
|
+
after: since
|
|
714
|
+
}
|
|
584
715
|
}] : []
|
|
585
716
|
]);
|
|
717
|
+
if (since !== null) {
|
|
718
|
+
if (presenceRows.length > RECENT_CHANGES) presenceRows = await hs.db.findMany("presence", { where: [{
|
|
719
|
+
field: "user_id",
|
|
720
|
+
operator: "in",
|
|
721
|
+
value: sharingList
|
|
722
|
+
}, {
|
|
723
|
+
field: "stream",
|
|
724
|
+
operator: "gt",
|
|
725
|
+
value: since
|
|
726
|
+
}] });
|
|
727
|
+
else presenceRows = presenceRows.filter((r) => sharing.has(r.user_id));
|
|
728
|
+
if (deviceRows.length > RECENT_CHANGES) deviceRows = await hs.db.findMany("device_list_changes", deviceListChangesQuery(sharing, since));
|
|
729
|
+
}
|
|
586
730
|
if (wantPresence) {
|
|
587
731
|
const seen = /* @__PURE__ */ new Set();
|
|
588
732
|
for (const row of [...presenceRows, ...newlySharedPresence]) {
|
|
@@ -609,7 +753,8 @@ async function buildSync(hs, p) {
|
|
|
609
753
|
const body = {
|
|
610
754
|
next_batch: formatToken({
|
|
611
755
|
pos: p.to,
|
|
612
|
-
toDevice: nextToDevice
|
|
756
|
+
toDevice: nextToDevice,
|
|
757
|
+
ack: toDevice.length > 0
|
|
613
758
|
}),
|
|
614
759
|
rooms: Object.fromEntries(Object.entries({
|
|
615
760
|
join,
|
|
@@ -658,14 +803,12 @@ async function sync(ctx) {
|
|
|
658
803
|
const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
|
|
659
804
|
const setPresence = ctx.query.get("set_presence") ?? "online";
|
|
660
805
|
if (setPresence !== "offline") await touchPresence(hs, session.userId, setPresence);
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const useStateAfter = stable || unstable;
|
|
668
|
-
const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
|
|
806
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
807
|
+
const stable = ctx.query.get("use_state_after") === "true";
|
|
808
|
+
const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
|
|
809
|
+
const useStateAfter = stable || unstable;
|
|
810
|
+
const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
|
|
811
|
+
const { value } = await longPoll(hs, session.userId, timeout, ctx.request.signal, async (to) => {
|
|
669
812
|
const { body, empty } = await syncSlots(hs).run(() => buildSync(hs, {
|
|
670
813
|
session,
|
|
671
814
|
ignored,
|
|
@@ -676,20 +819,21 @@ async function sync(ctx) {
|
|
|
676
819
|
useStateAfter,
|
|
677
820
|
stateAfterKey
|
|
678
821
|
}));
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
822
|
+
return {
|
|
823
|
+
value: body,
|
|
824
|
+
done: !since || !empty || fullState
|
|
825
|
+
};
|
|
826
|
+
});
|
|
827
|
+
return value;
|
|
685
828
|
}
|
|
686
829
|
const LAST_ACTIVE_GRANULARITY_MS = 6e4;
|
|
687
830
|
async function touchPresence(hs, userId, presence) {
|
|
688
831
|
const state = presence === "unavailable" ? "unavailable" : "online";
|
|
689
|
-
const
|
|
832
|
+
const read = () => hs.db.findOne("presence", [{
|
|
690
833
|
field: "user_id",
|
|
691
834
|
value: userId
|
|
692
835
|
}]);
|
|
836
|
+
const row = hs.changes ? await hs.changes.values.presenceRow.get(userId, read) : await read();
|
|
693
837
|
if (row && row.presence === state) {
|
|
694
838
|
if (Date.now() - row.last_active_ts >= LAST_ACTIVE_GRANULARITY_MS) await hs.db.update("presence", [{
|
|
695
839
|
field: "user_id",
|
|
@@ -697,13 +841,17 @@ async function touchPresence(hs, userId, presence) {
|
|
|
697
841
|
}], { last_active_ts: Date.now() });
|
|
698
842
|
return;
|
|
699
843
|
}
|
|
700
|
-
await hs.streams.write((stream) =>
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
844
|
+
const stream = await hs.streams.write(async (stream) => {
|
|
845
|
+
await hs.db.upsert("presence", {
|
|
846
|
+
user_id: userId,
|
|
847
|
+
presence: state,
|
|
848
|
+
status_msg: row?.status_msg ?? null,
|
|
849
|
+
last_active_ts: Date.now(),
|
|
850
|
+
stream
|
|
851
|
+
});
|
|
852
|
+
return stream;
|
|
853
|
+
});
|
|
854
|
+
hs.notifier.notify([userId, ...await hs.rooms.usersSharingRooms(userId)], stream);
|
|
707
855
|
await hs.federation.onPresence(userId, state, row?.status_msg ?? null).catch(() => {});
|
|
708
856
|
}
|
|
709
857
|
function registerSyncRoutes(r) {
|