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.
- package/README.md +89 -11
- package/dist/adapters/cluster.d.ts +1 -26
- package/dist/adapters/cluster.js +50 -103
- package/dist/api/account.js +407 -354
- package/dist/api/admin.js +82 -61
- package/dist/api/appservice.js +37 -25
- package/dist/api/backup.js +73 -62
- package/dist/api/federation.js +172 -138
- package/dist/api/filters.js +4 -3
- package/dist/api/legacy.js +91 -78
- package/dist/api/media.js +71 -55
- package/dist/api/misc.js +46 -37
- package/dist/api/preview.js +37 -73
- package/dist/api/profile.js +108 -97
- package/dist/api/push.js +114 -99
- package/dist/api/relationships.js +64 -58
- package/dist/api/rooms.js +703 -580
- package/dist/api/rtc.d.ts +0 -1
- package/dist/api/rtc.js +48 -34
- package/dist/api/search.js +133 -127
- package/dist/api/slidingsync.js +41 -31
- package/dist/api/spaces.js +82 -77
- package/dist/api/sync.js +325 -135
- package/dist/api/threadsubs.js +72 -61
- package/dist/api/threepid.js +99 -66
- package/dist/api/uia.js +17 -16
- package/dist/api/user.js +221 -178
- package/dist/core/admin.d.ts +0 -1
- package/dist/core/admin.js +6 -5
- package/dist/core/adminroom.js +9 -8
- package/dist/core/appservices.d.ts +2 -1
- package/dist/core/appservices.js +5 -4
- package/dist/core/audit.d.ts +0 -1
- package/dist/core/authrules.js +26 -25
- package/dist/core/cluster.d.ts +12 -5
- package/dist/core/cluster.js +46 -29
- package/dist/core/delayed.js +2 -1
- package/dist/core/events.js +11 -10
- package/dist/core/html.js +13 -0
- package/dist/core/identity.js +1 -1
- package/dist/core/invitefilter.js +2 -1
- package/dist/core/json.js +10 -1
- package/dist/core/keys.d.ts +10 -1
- package/dist/core/keys.js +62 -42
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/outbound.d.ts +16 -0
- package/dist/core/outbound.js +172 -0
- package/dist/core/partialstate.js +2 -1
- package/dist/core/policy.js +4 -3
- package/dist/core/push.d.ts +0 -1
- package/dist/core/push.js +41 -28
- package/dist/core/pushrules.js +36 -35
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/rooms.d.ts +10 -1
- package/dist/core/rooms.js +101 -74
- 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/stateres.js +7 -6
- package/dist/core/threepid.js +1 -9
- package/dist/federation/client.js +9 -1
- package/dist/federation/index.d.ts +2 -2
- package/dist/federation/index.js +43 -35
- package/dist/homeserver.d.ts +23 -4
- package/dist/homeserver.js +12 -3
- package/dist/http/handler.js +2 -23
- package/dist/http/input.js +2 -2
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +9 -26
- package/dist/http/router.js +10 -30
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/storage/batch.js +11 -0
- package/dist/storage/memory.js +1 -0
- package/dist/storage/schema.js +1 -1
- package/dist/storage/sql.js +20 -1
- package/dist/storage/types.d.ts +16 -0
- package/dist/vendor/zod/LICENSE +21 -0
- package/dist/vendor/zod/v4/classic/errors.js +54 -0
- package/dist/vendor/zod/v4/classic/parse.js +17 -0
- package/dist/vendor/zod/v4/classic/schemas.js +865 -0
- package/dist/vendor/zod/v4/core/api.js +512 -0
- package/dist/vendor/zod/v4/core/checks.js +300 -0
- package/dist/vendor/zod/v4/core/core.js +109 -0
- package/dist/vendor/zod/v4/core/doc.js +35 -0
- package/dist/vendor/zod/v4/core/errors.js +118 -0
- package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
- package/dist/vendor/zod/v4/core/memoizer.js +255 -0
- package/dist/vendor/zod/v4/core/parse.js +206 -0
- package/dist/vendor/zod/v4/core/regexes.js +73 -0
- package/dist/vendor/zod/v4/core/registries.js +48 -0
- package/dist/vendor/zod/v4/core/schemas.js +1378 -0
- package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
- package/dist/vendor/zod/v4/core/util.js +537 -0
- package/dist/vendor/zod/v4/core/versions.js +8 -0
- package/dist/vendor/zod/v4/locales/en.js +110 -0
- package/package.json +8 -6
- 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 ===
|
|
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(
|
|
171
|
-
if (startState.has(key) || state.some((s) => s.type ===
|
|
172
|
-
const member = [...(await hs.rooms.getPdus((e.pdu.auth_events ?? []).map(eventIdOf))).values()].find((a) => a.type ===
|
|
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 ===
|
|
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:
|
|
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 ===
|
|
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:
|
|
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 !==
|
|
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
|
-
|
|
267
|
-
|
|
274
|
+
[M.JOINED_MEMBER_COUNT]: joined,
|
|
275
|
+
[M.INVITED_MEMBER_COUNT]: invited
|
|
268
276
|
};
|
|
269
|
-
if (!getState(state,
|
|
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
|
|
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 ===
|
|
349
|
-
type:
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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:
|
|
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:
|
|
440
|
-
device_unused_fallback_key_types:
|
|
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[
|
|
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
|
|
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(
|
|
483
|
-
const
|
|
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
|
|
491
|
-
stateAfterKey
|
|
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",
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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[
|
|
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
|
|
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)) {
|