matrix-nodejs-homeserver 0.3.1 → 0.3.3
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 +89 -6
- package/dist/adapters/cluster.d.ts +1 -26
- package/dist/adapters/cluster.js +50 -103
- package/dist/api/account.js +22 -3
- package/dist/api/admin.js +9 -4
- package/dist/api/media.js +1 -1
- package/dist/api/preview.js +33 -70
- package/dist/api/slidingsync.js +2 -2
- package/dist/api/sync.js +402 -134
- package/dist/core/cluster.d.ts +12 -5
- package/dist/core/cluster.js +46 -29
- package/dist/core/html.js +13 -0
- package/dist/core/identity.js +1 -1
- package/dist/core/json.js +10 -1
- package/dist/core/keys.d.ts +10 -1
- package/dist/core/keys.js +60 -41
- package/dist/core/outbound.d.ts +16 -0
- package/dist/core/outbound.js +172 -0
- package/dist/core/push.js +52 -34
- package/dist/core/pushrules.js +24 -5
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/rooms.d.ts +10 -1
- package/dist/core/rooms.js +25 -0
- package/dist/core/semaphore.js +23 -0
- package/dist/core/sso.d.ts +15 -1
- package/dist/core/sso.js +38 -2
- package/dist/core/threepid.js +1 -9
- package/dist/federation/client.js +7 -0
- package/dist/federation/index.d.ts +1 -1
- package/dist/federation/index.js +13 -6
- package/dist/homeserver.d.ts +21 -3
- package/dist/homeserver.js +13 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/storage/batch.js +82 -0
- package/dist/storage/memory.js +1 -0
- package/dist/storage/schema.js +8 -3
- package/dist/storage/sql.d.ts +22 -3
- package/dist/storage/sql.js +138 -21
- package/dist/storage/types.d.ts +30 -0
- package/package.json +6 -2
package/dist/api/sync.js
CHANGED
|
@@ -2,23 +2,27 @@ 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
|
+
import { deviceListChangesQuery } from "../core/keys.js";
|
|
7
8
|
import { body } from "../http/input.js";
|
|
8
9
|
import { procedure } from "../http/procedure.js";
|
|
10
|
+
import { readBatch } from "../storage/batch.js";
|
|
9
11
|
import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
|
|
12
|
+
import { Semaphore } from "../core/semaphore.js";
|
|
10
13
|
import { fullProfile } from "../core/profile.js";
|
|
11
14
|
//#region src/api/sync.ts
|
|
12
15
|
function parseSyncToken(raw) {
|
|
13
16
|
if (!raw) return null;
|
|
14
|
-
const m = /^s(\d+)(?:_(\d+))?$/.exec(raw);
|
|
17
|
+
const m = /^s(\d+)(?:_(\d+))?(_a)?$/.exec(raw);
|
|
15
18
|
if (!m) throw errors.invalidParam("Invalid since token");
|
|
16
19
|
return {
|
|
17
20
|
pos: Number(m[1]),
|
|
18
|
-
toDevice: Number(m[2] ?? m[1])
|
|
21
|
+
toDevice: Number(m[2] ?? m[1]),
|
|
22
|
+
ack: !!m[3]
|
|
19
23
|
};
|
|
20
24
|
}
|
|
21
|
-
const formatToken = (t) => `s${t.pos}_${t.toDevice}`;
|
|
25
|
+
const formatToken = (t) => `s${t.pos}_${t.toDevice}${t.ack ? "_a" : ""}`;
|
|
22
26
|
async function loadFilter(hs, userId, raw) {
|
|
23
27
|
if (!raw) return {};
|
|
24
28
|
if (raw.trim().startsWith("{")) {
|
|
@@ -64,7 +68,8 @@ async function snapshotAt(hs, roomId, pos) {
|
|
|
64
68
|
field: "stream",
|
|
65
69
|
direction: "desc"
|
|
66
70
|
},
|
|
67
|
-
limit: 1
|
|
71
|
+
limit: 1,
|
|
72
|
+
fields: ["state_snapshot"]
|
|
68
73
|
}))[0]?.state_snapshot ?? 0;
|
|
69
74
|
}
|
|
70
75
|
/** Build the timeline + state section of a room. */
|
|
@@ -84,6 +89,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
84
89
|
operator: "gt",
|
|
85
90
|
value: since
|
|
86
91
|
});
|
|
92
|
+
const sinceIdsRead = since !== null ? snapshotAt(hs, roomId, since).then((id) => hs.rooms.snapshotIds(id)) : Promise.resolve({});
|
|
93
|
+
const scanned = [];
|
|
87
94
|
const events = [];
|
|
88
95
|
let limited = false;
|
|
89
96
|
let cursor = upTo;
|
|
@@ -102,6 +109,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
102
109
|
limit: batchSize
|
|
103
110
|
});
|
|
104
111
|
if (rows.length === 0) break;
|
|
112
|
+
scanned.push(...rows);
|
|
105
113
|
const stored = rows.map((r) => hs.rooms.rowToStored(r));
|
|
106
114
|
const visible = await hs.rooms.filterVisible(p.session.userId, roomId, stored);
|
|
107
115
|
const ok = new Map(visible.map((v) => [v.pdu.event_id, v]));
|
|
@@ -126,7 +134,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
126
134
|
}
|
|
127
135
|
if (limit === 0 && events.length === 0) limited = (await hs.db.findMany("events", {
|
|
128
136
|
where,
|
|
129
|
-
limit: 1
|
|
137
|
+
limit: 1,
|
|
138
|
+
fields: ["stream"]
|
|
130
139
|
})).length > 0;
|
|
131
140
|
if (since === null) events.sort((a, b) => (a.pdu.depth ?? 0) - (b.pdu.depth ?? 0) || a.stream - b.stream);
|
|
132
141
|
else events.reverse();
|
|
@@ -138,24 +147,50 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
138
147
|
events.splice(0, lastGap);
|
|
139
148
|
limited = true;
|
|
140
149
|
}
|
|
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
150
|
const startPos = events.length ? Math.min(...events.map((e) => e.stream)) - 1 : upTo;
|
|
150
|
-
const
|
|
151
|
+
const newestAtOrBefore = async (pos) => {
|
|
152
|
+
const row = scanned.find((r) => r.stream <= pos);
|
|
153
|
+
if (row) return row;
|
|
154
|
+
return (await hs.db.findMany("events", {
|
|
155
|
+
where: [{
|
|
156
|
+
field: "room_id",
|
|
157
|
+
value: roomId
|
|
158
|
+
}, {
|
|
159
|
+
field: "stream",
|
|
160
|
+
operator: "lte",
|
|
161
|
+
value: pos
|
|
162
|
+
}],
|
|
163
|
+
sortBy: {
|
|
164
|
+
field: "stream",
|
|
165
|
+
direction: "desc"
|
|
166
|
+
},
|
|
167
|
+
limit: 1,
|
|
168
|
+
fields: ["stream", "state_snapshot"]
|
|
169
|
+
}))[0] ?? null;
|
|
170
|
+
};
|
|
171
|
+
const [atStart, atEnd] = await Promise.all([newestAtOrBefore(startPos), startPos === upTo ? null : newestAtOrBefore(upTo)]);
|
|
172
|
+
const startSnapshot = atStart?.state_snapshot ?? 0;
|
|
173
|
+
const endSnapshot = startPos === upTo ? startSnapshot : atEnd?.state_snapshot ?? 0;
|
|
174
|
+
if (!limited && events.length) {
|
|
175
|
+
if (events[0].stream === startPos + 1) limited = atStart !== null && (since === null || atStart.stream > since);
|
|
176
|
+
else limited = (await hs.db.findMany("events", {
|
|
177
|
+
where: [...where.filter((c) => c.operator !== "lte"), {
|
|
178
|
+
field: "stream",
|
|
179
|
+
operator: "lt",
|
|
180
|
+
value: events[0].stream
|
|
181
|
+
}],
|
|
182
|
+
limit: 1,
|
|
183
|
+
fields: ["stream"]
|
|
184
|
+
})).length > 0;
|
|
185
|
+
}
|
|
186
|
+
const [startState, endState] = await Promise.all([hs.rooms.stateAtSnapshot(startSnapshot), hs.rooms.stateAtSnapshot(endSnapshot)]);
|
|
151
187
|
const timelineIds = new Set(events.map((e) => e.pdu.event_id));
|
|
152
|
-
const endState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, upTo));
|
|
153
188
|
for (const [key, ev] of endState) {
|
|
154
189
|
if (timelineIds.has(ev.event_id) || startState.get(key)?.event_id === ev.event_id) continue;
|
|
155
190
|
if (events.some((e) => e.pdu.state_key !== void 0 && stateKey(e.pdu.type, e.pdu.state_key) === key)) continue;
|
|
156
191
|
startState.set(key, ev);
|
|
157
192
|
}
|
|
158
|
-
const sinceIds =
|
|
193
|
+
const sinceIds = await sinceIdsRead;
|
|
159
194
|
const sf = p.filter.room?.state;
|
|
160
195
|
const lazy = !!sf?.lazy_load_members;
|
|
161
196
|
const senders = new Set(events.map((e) => e.pdu.sender));
|
|
@@ -175,15 +210,12 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
175
210
|
if (member && matchesFilter(sf, member)) state.push(member);
|
|
176
211
|
}
|
|
177
212
|
const stateAfter = [];
|
|
178
|
-
if (p.useStateAfter) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
|
|
185
|
-
stateAfter.push(ev);
|
|
186
|
-
}
|
|
213
|
+
if (p.useStateAfter) for (const [key, ev] of endState) {
|
|
214
|
+
if (sinceIds[key] === ev.event_id) continue;
|
|
215
|
+
const [type, sk] = splitStateKey(key);
|
|
216
|
+
if (!matchesFilter(sf, ev)) continue;
|
|
217
|
+
if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
|
|
218
|
+
stateAfter.push(ev);
|
|
187
219
|
}
|
|
188
220
|
return {
|
|
189
221
|
stateAfter,
|
|
@@ -196,7 +228,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
196
228
|
timelineEvents: events
|
|
197
229
|
};
|
|
198
230
|
}
|
|
199
|
-
async function roomEphemeral(hs, p, roomId) {
|
|
231
|
+
async function roomEphemeral(hs, p, roomId, prefetched) {
|
|
200
232
|
const since = p.since?.pos ?? null;
|
|
201
233
|
const out = [];
|
|
202
234
|
const typing = hs.typing.typingSince(roomId, since);
|
|
@@ -213,7 +245,7 @@ async function roomEphemeral(hs, p, roomId) {
|
|
|
213
245
|
operator: "gt",
|
|
214
246
|
value: since
|
|
215
247
|
});
|
|
216
|
-
const receipts = (await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
|
|
248
|
+
const receipts = (prefetched ?? await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
|
|
217
249
|
const content = {};
|
|
218
250
|
for (const r of receipts) {
|
|
219
251
|
if (r.receipt_type === M.READ.PRIVATE && r.user_id !== p.session.userId) continue;
|
|
@@ -231,7 +263,7 @@ async function roomEphemeral(hs, p, roomId) {
|
|
|
231
263
|
});
|
|
232
264
|
return out.filter((e) => matchesFilter(p.filter.room?.ephemeral, e));
|
|
233
265
|
}
|
|
234
|
-
async function accountData(hs, p, roomId) {
|
|
266
|
+
async function accountData(hs, p, roomId, prefetched) {
|
|
235
267
|
const where = [{
|
|
236
268
|
field: "user_id",
|
|
237
269
|
value: p.session.userId
|
|
@@ -244,7 +276,7 @@ async function accountData(hs, p, roomId) {
|
|
|
244
276
|
operator: "gt",
|
|
245
277
|
value: p.since.pos
|
|
246
278
|
});
|
|
247
|
-
const rows = await hs.db.findMany("account_data", { where });
|
|
279
|
+
const rows = prefetched ?? await hs.db.findMany("account_data", { where });
|
|
248
280
|
const filter = roomId ? p.filter.room?.account_data : p.filter.account_data;
|
|
249
281
|
return rows.filter((r) => r.stream <= p.to && (p.since || Object.keys(r.content ?? {}).length > 0)).map((r) => ({
|
|
250
282
|
type: r.type,
|
|
@@ -271,50 +303,223 @@ async function roomSummary(hs, roomId, userId) {
|
|
|
271
303
|
if (!getState(state, M.ROOM.NAME)?.content?.name && !getState(state, M.ROOM.CANONICAL_ALIAS)?.content?.alias) summary[M.HEROES] = heroes.sort();
|
|
272
304
|
return summary;
|
|
273
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* What changed for the user since the token, for all of their rooms at once: the rooms with new
|
|
308
|
+
* events or newly complete state, and the new receipts and account data by room.
|
|
309
|
+
*
|
|
310
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/33: an incremental sync queried every
|
|
311
|
+
* joined room (timeline, receipts, account data) and only then found that most had nothing new,
|
|
312
|
+
* so a sync cost as many queries as the user had rooms, each time a long-poll woke up. These few
|
|
313
|
+
* queries find the rooms that changed, and only those are built.
|
|
314
|
+
*/
|
|
315
|
+
/** One-time key counts by algorithm, as /sync reports them (signed_curve25519 always present). */
|
|
316
|
+
function countAlgorithms(rows) {
|
|
317
|
+
const counts = { signed_curve25519: 0 };
|
|
318
|
+
for (const r of rows) counts[r.algorithm] = (counts[r.algorithm] ?? 0) + 1;
|
|
319
|
+
return counts;
|
|
320
|
+
}
|
|
321
|
+
async function changesSince(hs, p, memberships, since, accountRows) {
|
|
322
|
+
const joined = memberships.filter((m) => m.membership === "join").map((m) => m.room_id);
|
|
323
|
+
const anyMembership = [...new Set(memberships.map((m) => m.room_id))];
|
|
324
|
+
const range = [{
|
|
325
|
+
field: "stream",
|
|
326
|
+
operator: "gt",
|
|
327
|
+
value: since
|
|
328
|
+
}, {
|
|
329
|
+
field: "stream",
|
|
330
|
+
operator: "lte",
|
|
331
|
+
value: p.to
|
|
332
|
+
}];
|
|
333
|
+
const chunks = (list) => Array.from({ length: Math.ceil(list.length / 500) }, (_, i) => list.slice(i * 500, i * 500 + 500));
|
|
334
|
+
const inRooms = (chunk) => ({
|
|
335
|
+
field: "room_id",
|
|
336
|
+
operator: "in",
|
|
337
|
+
value: chunk
|
|
338
|
+
});
|
|
339
|
+
const queries = [];
|
|
340
|
+
for (const chunk of chunks(joined)) {
|
|
341
|
+
queries.push({
|
|
342
|
+
model: "events",
|
|
343
|
+
opts: {
|
|
344
|
+
where: [inRooms(chunk), ...range],
|
|
345
|
+
fields: ["room_id"]
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
queries.push({
|
|
349
|
+
model: "receipts",
|
|
350
|
+
opts: { where: [inRooms(chunk), ...range] }
|
|
351
|
+
});
|
|
352
|
+
queries.push({
|
|
353
|
+
model: "partial_state_rooms",
|
|
354
|
+
opts: {
|
|
355
|
+
where: [inRooms(chunk), {
|
|
356
|
+
field: "completed_stream",
|
|
357
|
+
operator: "gt",
|
|
358
|
+
value: since
|
|
359
|
+
}],
|
|
360
|
+
fields: ["room_id"]
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
for (const chunk of chunks(anyMembership)) queries.push({
|
|
365
|
+
model: "memberships",
|
|
366
|
+
opts: { where: [inRooms(chunk), {
|
|
367
|
+
field: "stream",
|
|
368
|
+
operator: "gt",
|
|
369
|
+
value: since
|
|
370
|
+
}] }
|
|
371
|
+
});
|
|
372
|
+
const results = await readBatch(hs.db, queries);
|
|
373
|
+
const touched = /* @__PURE__ */ new Set();
|
|
374
|
+
const receipts = /* @__PURE__ */ new Map();
|
|
375
|
+
const joinedChunks = Math.ceil(joined.length / 500);
|
|
376
|
+
for (let c = 0; c < joinedChunks; c++) {
|
|
377
|
+
const [events, receiptRows, completed] = results.slice(c * 3, c * 3 + 3);
|
|
378
|
+
for (const r of events) touched.add(r.room_id);
|
|
379
|
+
for (const r of receiptRows) {
|
|
380
|
+
touched.add(r.room_id);
|
|
381
|
+
receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
|
|
382
|
+
}
|
|
383
|
+
for (const r of completed) touched.add(r.room_id);
|
|
384
|
+
}
|
|
385
|
+
const memberChanges = results.slice(joinedChunks * 3).flat();
|
|
386
|
+
const accountData = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const r of accountRows) {
|
|
388
|
+
if (r.room_id) touched.add(r.room_id);
|
|
389
|
+
accountData.set(r.room_id, [...accountData.get(r.room_id) ?? [], r]);
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
touched,
|
|
393
|
+
receipts,
|
|
394
|
+
accountData,
|
|
395
|
+
memberChanges
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/** Presence or device-list changes after a token read without a user filter, at most (#44). */
|
|
399
|
+
const RECENT_CHANGES = 500;
|
|
400
|
+
/** How many of a sync's joined rooms are built at once (#38). */
|
|
401
|
+
const JOINED_ROOMS_AT_ONCE = 8;
|
|
402
|
+
/** `fn` over `items` with at most `limit` running at once; results in input order. */
|
|
403
|
+
async function mapConcurrently(items, limit, fn) {
|
|
404
|
+
const out = [];
|
|
405
|
+
let next = 0;
|
|
406
|
+
const worker = async () => {
|
|
407
|
+
while (next < items.length) {
|
|
408
|
+
const i = next++;
|
|
409
|
+
out[i] = await fn(items[i]);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
274
415
|
async function buildSync(hs, p) {
|
|
275
416
|
const userId = p.session.userId;
|
|
276
417
|
const since = p.since?.pos ?? null;
|
|
277
|
-
const
|
|
418
|
+
const incremental = since !== null && !p.fullState;
|
|
419
|
+
const deviceWhere = [{
|
|
278
420
|
field: "user_id",
|
|
279
421
|
value: userId
|
|
280
|
-
}
|
|
422
|
+
}, {
|
|
423
|
+
field: "device_id",
|
|
424
|
+
value: p.session.deviceId
|
|
425
|
+
}];
|
|
426
|
+
const acknowledged = p.since?.ack ? hs.db.delete("to_device", [...deviceWhere, {
|
|
427
|
+
field: "stream",
|
|
428
|
+
operator: "lte",
|
|
429
|
+
value: p.since.toDevice
|
|
430
|
+
}]) : Promise.resolve(0);
|
|
431
|
+
const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await readBatch(hs.db, [
|
|
432
|
+
{
|
|
433
|
+
model: "memberships",
|
|
434
|
+
opts: { where: [{
|
|
435
|
+
field: "user_id",
|
|
436
|
+
value: userId
|
|
437
|
+
}] }
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
model: "account_data",
|
|
441
|
+
opts: { where: [
|
|
442
|
+
{
|
|
443
|
+
field: "user_id",
|
|
444
|
+
value: userId
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
field: "room_id",
|
|
448
|
+
value: ""
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
field: "type",
|
|
452
|
+
value: M.IGNORED_USER_LIST
|
|
453
|
+
}
|
|
454
|
+
] }
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
model: "to_device",
|
|
458
|
+
opts: {
|
|
459
|
+
where: [...deviceWhere, {
|
|
460
|
+
field: "stream",
|
|
461
|
+
operator: "gt",
|
|
462
|
+
value: p.since?.toDevice ?? 0
|
|
463
|
+
}],
|
|
464
|
+
sortBy: {
|
|
465
|
+
field: "stream",
|
|
466
|
+
direction: "asc"
|
|
467
|
+
},
|
|
468
|
+
limit: 100
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
model: "one_time_keys",
|
|
473
|
+
opts: {
|
|
474
|
+
where: [...deviceWhere, {
|
|
475
|
+
field: "fallback",
|
|
476
|
+
value: false
|
|
477
|
+
}],
|
|
478
|
+
fields: ["algorithm"]
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
model: "fallback_keys",
|
|
483
|
+
opts: {
|
|
484
|
+
where: [...deviceWhere, {
|
|
485
|
+
field: "used",
|
|
486
|
+
value: false
|
|
487
|
+
}],
|
|
488
|
+
fields: ["algorithm"]
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
...incremental ? [{
|
|
492
|
+
model: "account_data",
|
|
493
|
+
opts: { where: [{
|
|
494
|
+
field: "user_id",
|
|
495
|
+
value: userId
|
|
496
|
+
}, {
|
|
497
|
+
field: "stream",
|
|
498
|
+
operator: "gt",
|
|
499
|
+
value: since
|
|
500
|
+
}] }
|
|
501
|
+
}] : []
|
|
502
|
+
]);
|
|
503
|
+
p = {
|
|
504
|
+
...p,
|
|
505
|
+
ignored: new Set(Object.keys(ignoredRows[0]?.content?.ignored_users ?? {}))
|
|
506
|
+
};
|
|
507
|
+
const changes = incremental ? await changesSince(hs, p, memberships, since, accountRows) : null;
|
|
508
|
+
let memberChangesRead = changes ? Promise.resolve(changes.memberChanges) : null;
|
|
509
|
+
const memberChanges = () => memberChangesRead ??= hs.rooms.membershipChangesSince(userId, since ?? 0, memberships);
|
|
281
510
|
const join = {};
|
|
282
511
|
const invite = {};
|
|
283
512
|
const leave = {};
|
|
284
513
|
const knock = {};
|
|
285
514
|
let empty = true;
|
|
515
|
+
const joinedBuilds = [];
|
|
286
516
|
for (const m of memberships) {
|
|
287
517
|
if (m.stream > p.to || !roomAllowed(p.filter, m.room_id)) continue;
|
|
288
518
|
const roomId = m.room_id;
|
|
289
519
|
const changed = since === null || m.stream > since;
|
|
290
520
|
if (m.membership === "join") {
|
|
291
|
-
if (!
|
|
292
|
-
|
|
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;
|
|
521
|
+
if (changes && !changed && !changes.touched.has(roomId) && hs.typing.typingSince(roomId, since) === null) continue;
|
|
522
|
+
joinedBuilds.push(m);
|
|
318
523
|
} else if (m.membership === "invite") {
|
|
319
524
|
if (!changed && !p.fullState) continue;
|
|
320
525
|
const pdu = await hs.rooms.getPdu(m.event_id);
|
|
@@ -341,68 +546,135 @@ async function buildSync(hs, p) {
|
|
|
341
546
|
leave[roomId] = {
|
|
342
547
|
timeline,
|
|
343
548
|
state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) },
|
|
344
|
-
account_data: { events: await accountData(hs, p, roomId) }
|
|
549
|
+
account_data: { events: await accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0) }
|
|
345
550
|
};
|
|
346
551
|
empty = false;
|
|
347
552
|
}
|
|
348
553
|
}
|
|
349
|
-
const
|
|
554
|
+
const joinedRoom = async (m) => {
|
|
555
|
+
const roomId = m.room_id;
|
|
556
|
+
if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) return null;
|
|
557
|
+
const completedAt = await hs.partialState.completedAt(roomId);
|
|
558
|
+
const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
|
|
559
|
+
const [{ timeline, state, stateAfter, timelineEvents }, ephemeral, roomAccountData, summary, counts] = await Promise.all([
|
|
560
|
+
roomTimeline(hs, p, roomId, initial ? null : since, p.to),
|
|
561
|
+
roomEphemeral(hs, p, roomId, changes ? changes.receipts.get(roomId) ?? [] : void 0),
|
|
562
|
+
accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0),
|
|
563
|
+
roomSummary(hs, roomId, userId),
|
|
564
|
+
hs.push.unreadCounts(userId, roomId)
|
|
565
|
+
]);
|
|
566
|
+
if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) return null;
|
|
567
|
+
const room = {
|
|
568
|
+
timeline,
|
|
569
|
+
...p.useStateAfter ? { [p.stateAfterKey ?? "state_after"]: { events: stateAfter.map((e) => toClientEvent(e, { includeRoomId: false })) } } : { state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) } },
|
|
570
|
+
ephemeral: { events: ephemeral },
|
|
571
|
+
account_data: { events: roomAccountData },
|
|
572
|
+
summary
|
|
573
|
+
};
|
|
574
|
+
if (p.filter.room?.timeline?.unread_thread_notifications) {
|
|
575
|
+
room.unread_notifications = counts.room;
|
|
576
|
+
if (Object.keys(counts.threads).length) room.unread_thread_notifications = counts.threads;
|
|
577
|
+
} else {
|
|
578
|
+
const total = { ...counts.room };
|
|
579
|
+
for (const t of Object.values(counts.threads)) {
|
|
580
|
+
total.notification_count += t.notification_count;
|
|
581
|
+
total.highlight_count += t.highlight_count;
|
|
582
|
+
}
|
|
583
|
+
room.unread_notifications = total;
|
|
584
|
+
}
|
|
585
|
+
return room;
|
|
586
|
+
};
|
|
587
|
+
const built = await mapConcurrently(joinedBuilds, JOINED_ROOMS_AT_ONCE, joinedRoom);
|
|
588
|
+
joinedBuilds.forEach((m, i) => {
|
|
589
|
+
if (!built[i]) return;
|
|
590
|
+
join[m.room_id] = built[i];
|
|
591
|
+
empty = false;
|
|
592
|
+
});
|
|
593
|
+
const globalAccountData = await accountData(hs, p, "", changes ? changes.accountData.get("") ?? [] : void 0);
|
|
350
594
|
if (since === null && !globalAccountData.some((e) => e.type === M.PUSH_RULES)) globalAccountData.push({
|
|
351
595
|
type: M.PUSH_RULES,
|
|
352
|
-
content: { global: await
|
|
596
|
+
content: { global: await readOnlyRules(hs, userId) }
|
|
353
597
|
});
|
|
354
598
|
if (globalAccountData.length) empty = false;
|
|
355
|
-
|
|
356
|
-
|
|
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);
|
|
599
|
+
await acknowledged;
|
|
600
|
+
const toDevice = toDeviceRows.filter((r) => r.stream <= p.to);
|
|
379
601
|
const nextToDevice = toDevice.length ? toDevice[toDevice.length - 1].stream : p.since?.toDevice ?? 0;
|
|
380
602
|
if (toDevice.length) empty = false;
|
|
381
603
|
const presenceEvents = [];
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
604
|
+
const wantPresence = !p.filter.presence?.types || p.filter.presence.types.length > 0;
|
|
605
|
+
const sharing = await hs.rooms.usersSharingRooms(userId);
|
|
606
|
+
const joinedRooms = memberships.filter((m) => m.membership === "join").map((m) => m.room_id);
|
|
607
|
+
const newlyShared = /* @__PURE__ */ new Set();
|
|
608
|
+
if (since !== null && wantPresence) {
|
|
609
|
+
const myRooms = new Set(joinedRooms);
|
|
610
|
+
for (const c of await memberChanges()) {
|
|
611
|
+
if (c.membership !== "join" || !myRooms.has(c.room_id)) continue;
|
|
612
|
+
if (c.user_id === userId) for (const u of await hs.rooms.joinedMembers(c.room_id)) newlyShared.add(u);
|
|
613
|
+
else newlyShared.add(c.user_id);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const sharingList = [...sharing];
|
|
617
|
+
const recent = (model) => ({
|
|
618
|
+
model,
|
|
619
|
+
opts: {
|
|
620
|
+
where: [{
|
|
387
621
|
field: "stream",
|
|
388
622
|
operator: "gt",
|
|
389
623
|
value: since
|
|
390
|
-
}]
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
624
|
+
}],
|
|
625
|
+
sortBy: {
|
|
626
|
+
field: "stream",
|
|
627
|
+
direction: "asc"
|
|
628
|
+
},
|
|
629
|
+
limit: 501
|
|
397
630
|
}
|
|
398
|
-
|
|
631
|
+
});
|
|
632
|
+
const presenceQuery = since === null ? {
|
|
633
|
+
model: "presence",
|
|
634
|
+
opts: { where: [{
|
|
635
|
+
field: "user_id",
|
|
636
|
+
operator: "in",
|
|
637
|
+
value: wantPresence ? sharingList : []
|
|
638
|
+
}] }
|
|
639
|
+
} : wantPresence ? recent("presence") : {
|
|
640
|
+
model: "presence",
|
|
641
|
+
opts: { where: [{
|
|
399
642
|
field: "user_id",
|
|
400
643
|
operator: "in",
|
|
401
|
-
value: [
|
|
402
|
-
}]
|
|
403
|
-
|
|
644
|
+
value: []
|
|
645
|
+
}] }
|
|
646
|
+
};
|
|
647
|
+
let [presenceRows = [], newlySharedPresence = [], deviceRows] = await readBatch(hs.db, [
|
|
648
|
+
presenceQuery,
|
|
649
|
+
{
|
|
650
|
+
model: "presence",
|
|
651
|
+
opts: { where: [{
|
|
652
|
+
field: "user_id",
|
|
653
|
+
operator: "in",
|
|
654
|
+
value: [...newlyShared]
|
|
655
|
+
}] }
|
|
656
|
+
},
|
|
657
|
+
...since !== null ? [recent("device_list_changes")] : []
|
|
658
|
+
]);
|
|
659
|
+
if (since !== null) {
|
|
660
|
+
if (presenceRows.length > RECENT_CHANGES) presenceRows = await hs.db.findMany("presence", { where: [{
|
|
661
|
+
field: "user_id",
|
|
662
|
+
operator: "in",
|
|
663
|
+
value: sharingList
|
|
664
|
+
}, {
|
|
665
|
+
field: "stream",
|
|
666
|
+
operator: "gt",
|
|
667
|
+
value: since
|
|
668
|
+
}] });
|
|
669
|
+
else presenceRows = presenceRows.filter((r) => sharing.has(r.user_id));
|
|
670
|
+
if (deviceRows.length > RECENT_CHANGES) deviceRows = await hs.db.findMany("device_list_changes", deviceListChangesQuery(sharing, since));
|
|
671
|
+
}
|
|
672
|
+
if (wantPresence) {
|
|
673
|
+
const seen = /* @__PURE__ */ new Set();
|
|
674
|
+
for (const row of [...presenceRows, ...newlySharedPresence]) {
|
|
675
|
+
if (seen.has(row.user_id)) continue;
|
|
676
|
+
seen.add(row.user_id);
|
|
404
677
|
if (row.stream > p.to) continue;
|
|
405
|
-
if (since !== null && row.stream <= since && !newlyShared.has(row.user_id)) continue;
|
|
406
678
|
const content = {
|
|
407
679
|
presence: row.presence,
|
|
408
680
|
last_active_ago: Date.now() - row.last_active_ts
|
|
@@ -418,12 +690,13 @@ async function buildSync(hs, p) {
|
|
|
418
690
|
}
|
|
419
691
|
}
|
|
420
692
|
if (presenceEvents.length) empty = false;
|
|
421
|
-
const profileUpdates = await profileUpdatesFor(hs, p, memberships);
|
|
693
|
+
const profileUpdates = await profileUpdatesFor(hs, p, memberships, memberChanges);
|
|
422
694
|
if (profileUpdates) empty = false;
|
|
423
695
|
const body = {
|
|
424
696
|
next_batch: formatToken({
|
|
425
697
|
pos: p.to,
|
|
426
|
-
toDevice: nextToDevice
|
|
698
|
+
toDevice: nextToDevice,
|
|
699
|
+
ack: toDevice.length > 0
|
|
427
700
|
}),
|
|
428
701
|
rooms: Object.fromEntries(Object.entries({
|
|
429
702
|
join,
|
|
@@ -438,12 +711,16 @@ async function buildSync(hs, p) {
|
|
|
438
711
|
type: r.type,
|
|
439
712
|
content: r.content
|
|
440
713
|
})) },
|
|
441
|
-
device_one_time_keys_count:
|
|
442
|
-
device_unused_fallback_key_types:
|
|
714
|
+
device_one_time_keys_count: countAlgorithms(oneTimeKeyRows),
|
|
715
|
+
device_unused_fallback_key_types: [...new Set(fallbackRows.map((r) => r.algorithm))]
|
|
443
716
|
};
|
|
444
717
|
if (profileUpdates) body[UNSTABLE.ORG.MATRIX.MSC4429.USERS] = profileUpdates;
|
|
445
718
|
if (since !== null) {
|
|
446
|
-
const lists = await hs.keys.changesSince(userId, since, p.to
|
|
719
|
+
const lists = await hs.keys.changesSince(userId, since, p.to, {
|
|
720
|
+
memberChanges: await memberChanges(),
|
|
721
|
+
deviceRows,
|
|
722
|
+
joinedRooms
|
|
723
|
+
});
|
|
447
724
|
body.device_lists = lists;
|
|
448
725
|
if (lists.changed.length || lists.left.length) empty = false;
|
|
449
726
|
}
|
|
@@ -453,6 +730,12 @@ async function buildSync(hs, p) {
|
|
|
453
730
|
nextToDevice
|
|
454
731
|
};
|
|
455
732
|
}
|
|
733
|
+
const slots = /* @__PURE__ */ new WeakMap();
|
|
734
|
+
function syncSlots(hs) {
|
|
735
|
+
let s = slots.get(hs);
|
|
736
|
+
if (!s) slots.set(hs, s = new Semaphore(hs.config.maxConcurrentSyncs));
|
|
737
|
+
return s;
|
|
738
|
+
}
|
|
456
739
|
async function sync(ctx) {
|
|
457
740
|
const hs = ctx.hs;
|
|
458
741
|
const session = await ctx.auth();
|
|
@@ -465,33 +748,21 @@ async function sync(ctx) {
|
|
|
465
748
|
const deadline = Date.now() + timeout;
|
|
466
749
|
for (;;) {
|
|
467
750
|
const to = await hs.streams.position();
|
|
468
|
-
const
|
|
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 ?? {}));
|
|
751
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
483
752
|
const stable = ctx.query.get("use_state_after") === "true";
|
|
484
753
|
const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
|
|
485
|
-
const
|
|
754
|
+
const useStateAfter = stable || unstable;
|
|
755
|
+
const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
|
|
756
|
+
const { body, empty } = await syncSlots(hs).run(() => buildSync(hs, {
|
|
486
757
|
session,
|
|
487
758
|
ignored,
|
|
488
759
|
since,
|
|
489
760
|
to,
|
|
490
761
|
filter,
|
|
491
762
|
fullState,
|
|
492
|
-
useStateAfter
|
|
493
|
-
stateAfterKey
|
|
494
|
-
});
|
|
763
|
+
useStateAfter,
|
|
764
|
+
stateAfterKey
|
|
765
|
+
}));
|
|
495
766
|
const remaining = deadline - Date.now();
|
|
496
767
|
if (!since || !empty || remaining <= 0 || fullState) return body;
|
|
497
768
|
if (await hs.streams.position() > to) continue;
|
|
@@ -499,6 +770,7 @@ async function sync(ctx) {
|
|
|
499
770
|
if (ctx.request.signal?.aborted) return body;
|
|
500
771
|
}
|
|
501
772
|
}
|
|
773
|
+
const LAST_ACTIVE_GRANULARITY_MS = 6e4;
|
|
502
774
|
async function touchPresence(hs, userId, presence) {
|
|
503
775
|
const state = presence === "unavailable" ? "unavailable" : "online";
|
|
504
776
|
const row = await hs.db.findOne("presence", [{
|
|
@@ -506,7 +778,7 @@ async function touchPresence(hs, userId, presence) {
|
|
|
506
778
|
value: userId
|
|
507
779
|
}]);
|
|
508
780
|
if (row && row.presence === state) {
|
|
509
|
-
await hs.db.update("presence", [{
|
|
781
|
+
if (Date.now() - row.last_active_ts >= LAST_ACTIVE_GRANULARITY_MS) await hs.db.update("presence", [{
|
|
510
782
|
field: "user_id",
|
|
511
783
|
value: userId
|
|
512
784
|
}], { last_active_ts: Date.now() });
|
|
@@ -564,7 +836,7 @@ function registerSyncRoutes(r) {
|
|
|
564
836
|
* MSC4429: profile changes of users sharing a room, limited to the fields the filter asks for.
|
|
565
837
|
* The initial sync carries current values; users we no longer share any room with get `null`.
|
|
566
838
|
*/
|
|
567
|
-
async function profileUpdatesFor(hs, p, memberships) {
|
|
839
|
+
async function profileUpdatesFor(hs, p, memberships, memberChanges) {
|
|
568
840
|
const ids = (p.filter.profile_fields ?? p.filter[UNSTABLE.ORG.MATRIX.MSC4429.PROFILE_FIELDS])?.ids;
|
|
569
841
|
if (!Array.isArray(ids) || !ids.length) return null;
|
|
570
842
|
const wanted = new Set(ids.filter((x) => typeof x === "string"));
|
|
@@ -577,11 +849,7 @@ async function profileUpdatesFor(hs, p, memberships) {
|
|
|
577
849
|
const full = new Set(since === null ? sharing : []);
|
|
578
850
|
if (since !== null) {
|
|
579
851
|
const myRooms = new Set(memberships.filter((m) => m.membership === "join").map((m) => m.room_id));
|
|
580
|
-
const changes = await
|
|
581
|
-
field: "stream",
|
|
582
|
-
operator: "gt",
|
|
583
|
-
value: since
|
|
584
|
-
}] });
|
|
852
|
+
const changes = await memberChanges();
|
|
585
853
|
for (const c of changes) {
|
|
586
854
|
if (c.stream > p.to || c.user_id === p.session.userId) continue;
|
|
587
855
|
if (c.membership === "join" && myRooms.has(c.room_id)) {
|