matrix-nodejs-homeserver 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +457 -2
  3. package/dist/adapters/auth.d.ts +50 -0
  4. package/dist/adapters/auth.js +18 -0
  5. package/dist/adapters/cluster.d.ts +55 -0
  6. package/dist/adapters/cluster.js +249 -0
  7. package/dist/adapters/media.d.ts +50 -0
  8. package/dist/adapters/media.js +63 -0
  9. package/dist/adapters/s3.d.ts +33 -0
  10. package/dist/adapters/s3.js +95 -0
  11. package/dist/api/account.js +560 -0
  12. package/dist/api/admin.js +251 -0
  13. package/dist/api/appservice.js +33 -0
  14. package/dist/api/backup.js +269 -0
  15. package/dist/api/federation.js +792 -0
  16. package/dist/api/filters.js +87 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +170 -0
  19. package/dist/api/media.js +255 -0
  20. package/dist/api/misc.js +86 -0
  21. package/dist/api/preview.js +200 -0
  22. package/dist/api/profile.js +165 -0
  23. package/dist/api/push.js +187 -0
  24. package/dist/api/relationships.js +264 -0
  25. package/dist/api/rooms.js +1364 -0
  26. package/dist/api/rtc.d.ts +24 -0
  27. package/dist/api/rtc.js +52 -0
  28. package/dist/api/search.js +155 -0
  29. package/dist/api/slidingsync.js +464 -0
  30. package/dist/api/spaces.js +172 -0
  31. package/dist/api/sync.js +600 -0
  32. package/dist/api/threadsubs.js +129 -0
  33. package/dist/api/threepid.js +122 -0
  34. package/dist/api/uia.js +126 -0
  35. package/dist/api/user.js +286 -0
  36. package/dist/core/admin.d.ts +85 -0
  37. package/dist/core/admin.js +307 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +482 -0
  40. package/dist/core/appservices.d.ts +73 -0
  41. package/dist/core/appservices.js +368 -0
  42. package/dist/core/audit.d.ts +51 -0
  43. package/dist/core/audit.js +86 -0
  44. package/dist/core/auth.d.ts +48 -0
  45. package/dist/core/auth.js +237 -0
  46. package/dist/core/authrules.d.ts +4 -0
  47. package/dist/core/authrules.js +347 -0
  48. package/dist/core/cluster.d.ts +66 -0
  49. package/dist/core/cluster.js +191 -0
  50. package/dist/core/crypto.d.ts +10 -0
  51. package/dist/core/crypto.js +100 -0
  52. package/dist/core/delayed.d.ts +39 -0
  53. package/dist/core/delayed.js +159 -0
  54. package/dist/core/errors.d.ts +9 -0
  55. package/dist/core/errors.js +45 -0
  56. package/dist/core/events.d.ts +32 -0
  57. package/dist/core/events.js +185 -0
  58. package/dist/core/glob.js +66 -0
  59. package/dist/core/identity.d.ts +24 -0
  60. package/dist/core/identity.js +96 -0
  61. package/dist/core/ids.js +36 -0
  62. package/dist/core/invitefilter.js +35 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +449 -0
  66. package/dist/core/notifier.d.ts +17 -0
  67. package/dist/core/notifier.js +54 -0
  68. package/dist/core/oauth.d.ts +38 -0
  69. package/dist/core/oauth.js +108 -0
  70. package/dist/core/partialstate.d.ts +32 -0
  71. package/dist/core/partialstate.js +197 -0
  72. package/dist/core/phone.js +268 -0
  73. package/dist/core/policy.d.ts +29 -0
  74. package/dist/core/policy.js +96 -0
  75. package/dist/core/profile.js +39 -0
  76. package/dist/core/push.d.ts +47 -0
  77. package/dist/core/push.js +306 -0
  78. package/dist/core/pushrules.d.ts +1 -0
  79. package/dist/core/pushrules.js +228 -0
  80. package/dist/core/ratelimit.d.ts +23 -0
  81. package/dist/core/ratelimit.js +77 -0
  82. package/dist/core/rooms.d.ts +215 -0
  83. package/dist/core/rooms.js +1729 -0
  84. package/dist/core/settings.d.ts +11 -0
  85. package/dist/core/settings.js +29 -0
  86. package/dist/core/sso.d.ts +46 -0
  87. package/dist/core/sso.js +165 -0
  88. package/dist/core/stateres.d.ts +4 -0
  89. package/dist/core/stateres.js +208 -0
  90. package/dist/core/threepid.d.ts +80 -0
  91. package/dist/core/threepid.js +275 -0
  92. package/dist/core/typing.d.ts +16 -0
  93. package/dist/core/typing.js +60 -0
  94. package/dist/federation/client.d.ts +51 -0
  95. package/dist/federation/client.js +278 -0
  96. package/dist/federation/index.d.ts +153 -0
  97. package/dist/federation/index.js +1058 -0
  98. package/dist/federation/keyring.d.ts +26 -0
  99. package/dist/federation/keyring.js +122 -0
  100. package/dist/federation/sender.d.ts +26 -0
  101. package/dist/federation/sender.js +164 -0
  102. package/dist/homeserver.d.ts +223 -0
  103. package/dist/homeserver.js +301 -0
  104. package/dist/http/adapters.d.ts +40 -0
  105. package/dist/http/adapters.js +130 -0
  106. package/dist/http/handler.d.ts +8 -0
  107. package/dist/http/handler.js +202 -0
  108. package/dist/http/input.js +24 -0
  109. package/dist/http/router.d.ts +60 -0
  110. package/dist/http/router.js +76 -0
  111. package/dist/index.d.ts +25 -0
  112. package/dist/index.js +14 -0
  113. package/dist/storage/memory.d.ts +5 -0
  114. package/dist/storage/memory.js +122 -0
  115. package/dist/storage/schema.d.ts +4 -0
  116. package/dist/storage/schema.js +664 -0
  117. package/dist/storage/sql.d.ts +72 -0
  118. package/dist/storage/sql.js +274 -0
  119. package/dist/storage/types.d.ts +55 -0
  120. package/package.json +67 -4
@@ -0,0 +1,1729 @@
1
+ import { isObject } from "./json.js";
2
+ import { SUPPORTED_ROOM_VERSIONS, eventIdOf, finalisePdu, redact, roomVersionFeatures, splitStateKey, stateKey, toClientEvent } from "./events.js";
3
+ import { MatrixError, errors } from "./errors.js";
4
+ import { generateRoomId, isValidUserId, parseId } from "./ids.js";
5
+ import { authEventKeys, authStateFromAuthEvents, checkAuth, getState, membershipOf, powerLevels, userLevel } from "./authrules.js";
6
+ import { resolveState } from "./stateres.js";
7
+ import { copyRoomPushRules } from "./pushrules.js";
8
+ import { inviteDecision } from "./invitefilter.js";
9
+ //#region src/core/rooms.ts
10
+ const PARTIAL_REJECTION = "Partial state: ";
11
+ /** Thrown when the state before an event cannot be computed because predecessors are unknown. */
12
+ var MissingStateError = class extends Error {
13
+ eventIds;
14
+ constructor(eventIds) {
15
+ super(`Missing state for ${eventIds.join(", ")}`);
16
+ this.eventIds = eventIds;
17
+ }
18
+ };
19
+ var LruCache = class {
20
+ map = /* @__PURE__ */ new Map();
21
+ max;
22
+ constructor(max) {
23
+ this.max = max;
24
+ }
25
+ get(k) {
26
+ const v = this.map.get(k);
27
+ if (v !== void 0) {
28
+ this.map.delete(k);
29
+ this.map.set(k, v);
30
+ }
31
+ return v;
32
+ }
33
+ set(k, v) {
34
+ this.map.delete(k);
35
+ this.map.set(k, v);
36
+ if (this.map.size > this.max) this.map.delete(this.map.keys().next().value);
37
+ }
38
+ delete(k) {
39
+ this.map.delete(k);
40
+ }
41
+ };
42
+ const MAX_EVENT_SIZE = 65536;
43
+ /** A full state snapshot is written after this many deltas. */
44
+ const SNAPSHOT_INTERVAL = 64;
45
+ /** Reject malformed power level content with 400 before it reaches the auth rules. */
46
+ function validatePowerLevelContent(content, creators) {
47
+ const check = (v) => {
48
+ if (typeof v === "number" && Math.abs(v) > Number.MAX_SAFE_INTEGER) throw errors.badJson("Power level out of range");
49
+ };
50
+ for (const v of Object.values(content)) if (isObject(v)) Object.values(v).forEach(check);
51
+ else check(v);
52
+ if (isObject(content.users)) {
53
+ for (const u of Object.keys(content.users)) if (creators.has(u)) throw errors.badJson("Room creators cannot be given a power level");
54
+ }
55
+ }
56
+ const txnTokenOf = (userId, deviceId) => `${userId}|${deviceId}`;
57
+ var RoomService = class {
58
+ hs;
59
+ eventCache = new LruCache(2e4);
60
+ snapshotCache = new LruCache(2e3);
61
+ stateCache = /* @__PURE__ */ new Map();
62
+ extremityCache = /* @__PURE__ */ new Map();
63
+ outlierCache = new LruCache(5e3);
64
+ versions = /* @__PURE__ */ new Map();
65
+ snapshotDepths = new LruCache(2e4);
66
+ sharingCache = /* @__PURE__ */ new Map();
67
+ localClusterMarker;
68
+ constructor(hs, singleInstance) {
69
+ this.hs = hs;
70
+ this.localClusterMarker = singleInstance ? hs.cluster : null;
71
+ hs.cluster.subscribe("invalidate", (msg) => {
72
+ if (msg?.from !== hs.instanceId) this.invalidateLocal(msg);
73
+ });
74
+ }
75
+ /** Serialise writes per room (cluster-wide) so forward extremities stay consistent. */
76
+ async withLock(roomId, fn) {
77
+ return this.hs.cluster.lock(`room:${roomId}`, async () => {
78
+ if (this.hs.cluster !== this.localClusterMarker) this.invalidateLocal({ rooms: [roomId] });
79
+ return fn();
80
+ });
81
+ }
82
+ /** Drop cached data (for this instance only). */
83
+ invalidateLocal(msg) {
84
+ for (const roomId of msg.rooms ?? []) {
85
+ this.stateCache.delete(roomId);
86
+ this.extremityCache.delete(roomId);
87
+ }
88
+ for (const id of msg.events ?? []) {
89
+ this.eventCache.delete(id);
90
+ this.outlierCache.delete(id);
91
+ }
92
+ if (msg.sharing) this.sharingCache.clear();
93
+ }
94
+ /** Tell other instances to drop cached data. */
95
+ invalidate(msg) {
96
+ this.hs.cluster.publish("invalidate", {
97
+ ...msg,
98
+ from: this.hs.instanceId
99
+ }).catch(() => {});
100
+ }
101
+ forgetEventCache(eventId) {
102
+ this.eventCache.delete(eventId);
103
+ }
104
+ /** Drop all cached data for a room (after it was purged). */
105
+ forgetRoom(roomId) {
106
+ this.versions.delete(roomId);
107
+ this.extremityCache.delete(roomId);
108
+ this.stateCache.delete(roomId);
109
+ this.sharingCache.clear();
110
+ }
111
+ async roomVersion(roomId) {
112
+ const cached = this.versions.get(roomId);
113
+ if (cached) return cached;
114
+ const room = await this.hs.db.findOne("rooms", [{
115
+ field: "room_id",
116
+ value: roomId
117
+ }]);
118
+ if (!room) return null;
119
+ this.versions.set(roomId, room.room_version);
120
+ return room.room_version;
121
+ }
122
+ async exists(roomId) {
123
+ return await this.roomVersion(roomId) !== null;
124
+ }
125
+ rowToStored(row) {
126
+ return {
127
+ pdu: row.json.room_id ? row.json : {
128
+ ...row.json,
129
+ room_id: row.room_id
130
+ },
131
+ stream: row.stream,
132
+ snapshot: row.state_snapshot,
133
+ txnId: row.txn_id,
134
+ txnToken: row.txn_token,
135
+ gap: !!row.gap
136
+ };
137
+ }
138
+ async getEvent(eventId) {
139
+ const cached = this.eventCache.get(eventId);
140
+ if (cached) return cached;
141
+ const row = await this.hs.db.findOne("events", [{
142
+ field: "event_id",
143
+ value: eventId
144
+ }]);
145
+ if (!row) return null;
146
+ const stored = this.rowToStored(row);
147
+ this.eventCache.set(eventId, stored);
148
+ return stored;
149
+ }
150
+ async getEvents(eventIds) {
151
+ const out = /* @__PURE__ */ new Map();
152
+ const missing = [];
153
+ for (const id of eventIds) {
154
+ const c = this.eventCache.get(id);
155
+ if (c) out.set(id, c);
156
+ else missing.push(id);
157
+ }
158
+ for (let i = 0; i < missing.length; i += 500) {
159
+ const rows = await this.hs.db.findMany("events", { where: [{
160
+ field: "event_id",
161
+ operator: "in",
162
+ value: missing.slice(i, i + 500)
163
+ }] });
164
+ for (const row of rows) {
165
+ const stored = this.rowToStored(row);
166
+ this.eventCache.set(row.event_id, stored);
167
+ out.set(row.event_id, stored);
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+ async snapshotIds(snapshotId) {
173
+ if (snapshotId === 0) return {};
174
+ const cached = this.snapshotCache.get(snapshotId);
175
+ if (cached) return cached;
176
+ const row = await this.hs.db.findOne("state_snapshots", [{
177
+ field: "id",
178
+ value: snapshotId
179
+ }]);
180
+ const delta = row?.state ?? {};
181
+ const state = row?.prev ? {
182
+ ...await this.snapshotIds(row.prev),
183
+ ...delta
184
+ } : delta;
185
+ this.snapshotCache.set(snapshotId, state);
186
+ this.snapshotDepths.set(snapshotId, row?.depth ?? 0);
187
+ return state;
188
+ }
189
+ async stateAtSnapshot(snapshotId) {
190
+ return this.stateIdsToMap(await this.snapshotIds(snapshotId));
191
+ }
192
+ async currentState(roomId) {
193
+ const cached = this.stateCache.get(roomId);
194
+ if (cached) return cached;
195
+ const rows = await this.hs.db.findMany("current_state", { where: [{
196
+ field: "room_id",
197
+ value: roomId
198
+ }] });
199
+ const events = await this.getPdus(rows.map((r) => r.event_id));
200
+ const state = /* @__PURE__ */ new Map();
201
+ for (const r of rows) {
202
+ const ev = events.get(r.event_id);
203
+ if (ev) state.set(stateKey(r.type, r.state_key), ev);
204
+ }
205
+ this.stateCache.set(roomId, state);
206
+ return state;
207
+ }
208
+ async currentStateIds(roomId) {
209
+ const ids = {};
210
+ for (const [k, ev] of await this.currentState(roomId)) ids[k] = ev.event_id;
211
+ return ids;
212
+ }
213
+ /** Forward extremities: the latest events in the room DAG, used as prev_events of new events. */
214
+ async extremities(roomId) {
215
+ const cached = this.extremityCache.get(roomId);
216
+ if (cached) return cached;
217
+ const ids = (await this.hs.db.findMany("forward_extremities", { where: [{
218
+ field: "room_id",
219
+ value: roomId
220
+ }] })).map((r) => r.event_id);
221
+ this.extremityCache.set(roomId, ids);
222
+ return ids;
223
+ }
224
+ async getOutlier(eventId) {
225
+ const cached = this.outlierCache.get(eventId);
226
+ if (cached) return cached;
227
+ const row = await this.hs.db.findOne("outliers", [{
228
+ field: "event_id",
229
+ value: eventId
230
+ }]);
231
+ if (!row) return null;
232
+ const outlier = {
233
+ pdu: row.json.room_id ? row.json : {
234
+ ...row.json,
235
+ room_id: row.room_id
236
+ },
237
+ snapshot: row.state_snapshot,
238
+ rejected: row.rejected,
239
+ softFailed: !!row.soft_failed
240
+ };
241
+ this.outlierCache.set(eventId, outlier);
242
+ return outlier;
243
+ }
244
+ /** Any known PDU (timeline event or non-rejected outlier). */
245
+ async getPdu(eventId) {
246
+ const ev = await this.getEvent(eventId);
247
+ if (ev) return ev.pdu;
248
+ const o = await this.getOutlier(eventId);
249
+ return o && !o.rejected ? o.pdu : null;
250
+ }
251
+ async getPdus(eventIds) {
252
+ const out = /* @__PURE__ */ new Map();
253
+ const timeline = await this.getEvents(eventIds);
254
+ for (const [id, ev] of timeline) out.set(id, ev.pdu);
255
+ for (const id of eventIds) {
256
+ if (out.has(id)) continue;
257
+ const o = await this.getOutlier(id);
258
+ if (o && !o.rejected) out.set(id, o.pdu);
259
+ }
260
+ return out;
261
+ }
262
+ /** Whether we have seen this event at all (including rejected ones). */
263
+ async knowsEvent(eventId) {
264
+ return !!await this.getEvent(eventId) || !!await this.getOutlier(eventId);
265
+ }
266
+ /** Snapshot ID of the state after an event, or null when the state is unknown. */
267
+ async snapshotOf(eventId) {
268
+ const ev = await this.getEvent(eventId);
269
+ if (ev) return ev.snapshot;
270
+ const o = await this.getOutlier(eventId);
271
+ return o && !o.rejected && o.snapshot !== null ? o.snapshot : null;
272
+ }
273
+ async stateIdsToMap(ids) {
274
+ const events = await this.getPdus(Object.values(ids));
275
+ const state = /* @__PURE__ */ new Map();
276
+ for (const [k, id] of Object.entries(ids)) {
277
+ const ev = events.get(id);
278
+ if (ev) state.set(k, ev);
279
+ }
280
+ return state;
281
+ }
282
+ async createEventOf(roomId) {
283
+ return getState(await this.currentState(roomId), "m.room.create");
284
+ }
285
+ /**
286
+ * State before an event with the given prev_events: the snapshot of a single
287
+ * predecessor, or the resolution of several. Throws MissingStateError when a
288
+ * predecessor's state is unknown.
289
+ */
290
+ async stateBefore(roomId, version, prevIds, createEvent) {
291
+ if (prevIds.length === 0) return {
292
+ ids: {},
293
+ snapshot: 0
294
+ };
295
+ const snapshots = [];
296
+ const missing = [];
297
+ const queue = [...prevIds];
298
+ const visited = /* @__PURE__ */ new Set();
299
+ while (queue.length) {
300
+ const id = queue.shift();
301
+ if (visited.has(id) || visited.size > 200) continue;
302
+ visited.add(id);
303
+ const snap = await this.snapshotOf(id);
304
+ if (snap !== null) {
305
+ snapshots.push(snap);
306
+ continue;
307
+ }
308
+ const o = await this.getOutlier(id);
309
+ if (o?.rejected) {
310
+ queue.push(...(o.pdu.prev_events ?? []).map(eventIdOf));
311
+ continue;
312
+ }
313
+ missing.push(id);
314
+ }
315
+ if (missing.length) throw new MissingStateError(missing);
316
+ if (snapshots.length === 0) throw new MissingStateError(prevIds);
317
+ const unique = [...new Set(snapshots)];
318
+ if (unique.length === 1) return {
319
+ ids: await this.snapshotIds(unique[0]),
320
+ snapshot: unique[0]
321
+ };
322
+ const sets = await Promise.all(unique.map((s) => this.snapshotIds(s)));
323
+ return {
324
+ ids: await resolveState(sets, version, (eids) => this.getPdus(eids), createEvent ?? (version && roomVersionFeatures(version).v12 ? await this.createEventOf(roomId) : void 0)),
325
+ snapshot: null
326
+ };
327
+ }
328
+ /** Validate, authorise, sign and persist a new local event. Returns the event ID. */
329
+ async sendEvent(roomId, sender, draft, opts = {}) {
330
+ return this.withLock(roomId, async () => {
331
+ if (opts.txnId && opts.deviceId) {
332
+ const token = txnTokenOf(sender, opts.deviceId);
333
+ const existing = await this.hs.db.findOne("events", [{
334
+ field: "txn_id",
335
+ value: opts.txnId
336
+ }, {
337
+ field: "txn_token",
338
+ value: token
339
+ }]);
340
+ if (existing && existing.room_id === roomId) return existing.event_id;
341
+ }
342
+ const version = await this.roomVersion(roomId);
343
+ if (!version) throw errors.notFound("Unknown room");
344
+ const pdu = await this.buildLocalEvent(roomId, version, sender, draft, opts.skipAuth, opts.ts);
345
+ return (await this.persistEvent(roomId, version, pdu, {
346
+ local: true,
347
+ txnId: opts.txnId,
348
+ deviceId: opts.deviceId
349
+ })).eventId;
350
+ });
351
+ }
352
+ /** Build and sign (but do not persist) a local event on top of the current forward extremities. */
353
+ async buildLocalEvent(roomId, version, sender, draft, skipAuth = false, ts) {
354
+ const features = roomVersionFeatures(version);
355
+ const extremities = await this.extremities(roomId);
356
+ const prevs = [...(await this.getPdus(extremities)).values()].sort((a, b) => b.depth - a.depth).slice(0, 10);
357
+ const prevIds = prevs.map((p) => p.event_id);
358
+ const before = prevIds.length === extremities.length ? await this.currentState(roomId) : await this.stateIdsToMap((await this.stateBefore(roomId, version, prevIds)).ids);
359
+ const authEvents = authEventKeys({
360
+ ...draft,
361
+ sender
362
+ }, version).map((k) => before.get(k)?.event_id).filter((x) => !!x);
363
+ const depth = Math.min(prevs.reduce((m, p) => Math.max(m, p.depth), 0) + 1, Number.MAX_SAFE_INTEGER);
364
+ const pdu = {
365
+ room_id: roomId,
366
+ sender,
367
+ type: draft.type,
368
+ content: draft.content,
369
+ origin_server_ts: ts ?? Date.now(),
370
+ depth,
371
+ prev_events: prevIds,
372
+ auth_events: authEvents
373
+ };
374
+ if (draft.state_key !== void 0) pdu.state_key = draft.state_key;
375
+ if (draft.redacts !== void 0) {
376
+ if (features.v11) pdu.content = {
377
+ ...pdu.content,
378
+ redacts: draft.redacts
379
+ };
380
+ else pdu.redacts = draft.redacts;
381
+ }
382
+ if (features.legacyEventIds) {
383
+ pdu.prev_events = pdu.prev_events.map((id) => [id, {}]);
384
+ pdu.auth_events = pdu.auth_events.map((id) => [id, {}]);
385
+ }
386
+ if (!skipAuth) {
387
+ if (features.v12) before.set(stateKey("m.room.create", ""), await this.createEventOf(roomId));
388
+ checkAuth(pdu, before, version);
389
+ }
390
+ if (Buffer.byteLength(JSON.stringify(pdu)) > MAX_EVENT_SIZE) throw errors.tooLarge("Event too large");
391
+ return finalisePdu(pdu, version, this.hs.serverName, this.hs.signingKey);
392
+ }
393
+ /**
394
+ * Persist an event (local or remote). Remote events are checked against their auth
395
+ * events, the state before them and the current state (soft failure).
396
+ */
397
+ async persistEvent(roomId, version, pdu, opts = {}) {
398
+ const features = roomVersionFeatures(version);
399
+ const eventId = pdu.event_id;
400
+ if (await this.getEvent(eventId)) return {
401
+ eventId,
402
+ status: "persisted"
403
+ };
404
+ const createEvent = features.v12 ? pdu.type === "m.room.create" ? void 0 : opts.createEvent ?? await this.createEventOf(roomId) ?? await this.findCreateEvent(roomId) : void 0;
405
+ const prevIds = (pdu.prev_events ?? []).map(eventIdOf);
406
+ if (!opts.local) {
407
+ const reject = async (reason) => {
408
+ await this.hs.db.upsert("outliers", {
409
+ event_id: eventId,
410
+ room_id: roomId,
411
+ json: pdu,
412
+ state_snapshot: null,
413
+ rejected: reason,
414
+ soft_failed: false
415
+ });
416
+ this.outlierCache.delete(eventId);
417
+ return {
418
+ eventId,
419
+ status: "rejected",
420
+ reason
421
+ };
422
+ };
423
+ try {
424
+ const authPdus = await this.getPdus((pdu.auth_events ?? []).map(eventIdOf));
425
+ if (authPdus.size !== (pdu.auth_events ?? []).length) return reject("Missing auth events");
426
+ const authState = authStateFromAuthEvents(pdu, [...authPdus.values()], version, createEvent);
427
+ checkAuth(pdu, authState, version, { joinAuthorisedSignatureValid: opts.joinAuthorisedSignatureValid });
428
+ } catch (err) {
429
+ return reject(`Auth events: ${err.message}`);
430
+ }
431
+ }
432
+ let before;
433
+ if (opts.stateBefore) before = {
434
+ ids: opts.stateBefore,
435
+ snapshot: null
436
+ };
437
+ else before = await this.stateBefore(roomId, version, prevIds, createEvent);
438
+ const beforeMap = await this.stateIdsToMap(before.ids);
439
+ if (createEvent) beforeMap.set(stateKey("m.room.create", ""), createEvent);
440
+ const partial = !opts.local && !opts.backfill && await this.hs.partialState.isPartial(roomId);
441
+ if (partial) {
442
+ const authPdus = await this.getPdus((pdu.auth_events ?? []).map(eventIdOf));
443
+ const claimed = {};
444
+ for (const a of authPdus.values()) if (a.state_key !== void 0) claimed[stateKey(a.type, a.state_key)] = a.event_id;
445
+ const resolved = await resolveState([before.ids, claimed], version, (ids) => this.getPdus(ids), createEvent);
446
+ for (const [key, ev] of await this.stateIdsToMap(resolved)) beforeMap.set(key, ev);
447
+ }
448
+ if (!opts.local) {
449
+ const reject = async (reason) => {
450
+ await this.hs.db.upsert("outliers", {
451
+ event_id: eventId,
452
+ room_id: roomId,
453
+ json: pdu,
454
+ state_snapshot: null,
455
+ rejected: reason,
456
+ soft_failed: false
457
+ });
458
+ this.outlierCache.delete(eventId);
459
+ return {
460
+ eventId,
461
+ status: "rejected",
462
+ reason
463
+ };
464
+ };
465
+ try {
466
+ checkAuth(pdu, beforeMap, version, { joinAuthorisedSignatureValid: opts.joinAuthorisedSignatureValid });
467
+ } catch (err) {
468
+ return reject(`${partial ? PARTIAL_REJECTION : ""}State before: ${err.message}`);
469
+ }
470
+ if (!opts.backfill && !opts.stateBefore && !partial) {
471
+ const current = new Map(await this.currentState(roomId));
472
+ if (createEvent) current.set(stateKey("m.room.create", ""), createEvent);
473
+ const softFail = async () => {
474
+ const ids = pdu.state_key !== void 0 ? {
475
+ ...before.ids,
476
+ [stateKey(pdu.type, pdu.state_key)]: eventId
477
+ } : before.ids;
478
+ await this.hs.streams.write((snap) => this.hs.transaction(async (db) => {
479
+ await db.create("state_snapshots", {
480
+ id: snap,
481
+ room_id: roomId,
482
+ prev: null,
483
+ depth: 0,
484
+ state: ids
485
+ });
486
+ await db.upsert("outliers", {
487
+ event_id: eventId,
488
+ room_id: roomId,
489
+ json: pdu,
490
+ state_snapshot: snap,
491
+ rejected: null,
492
+ soft_failed: true
493
+ });
494
+ }));
495
+ this.outlierCache.delete(eventId);
496
+ return {
497
+ eventId,
498
+ status: "soft_failed"
499
+ };
500
+ };
501
+ try {
502
+ checkAuth(pdu, current, version, { joinAuthorisedSignatureValid: opts.joinAuthorisedSignatureValid });
503
+ } catch {
504
+ return softFail();
505
+ }
506
+ if (!await this.hs.policyServers.recommended(roomId, version, pdu)) return softFail();
507
+ }
508
+ }
509
+ if (opts.local) await this.hs.policyServers.signLocal(roomId, version, pdu);
510
+ return this.store(roomId, version, pdu, before, beforeMap, features, opts);
511
+ }
512
+ /**
513
+ * Finish a partial-state join (MSC3902): `fullBefore` is the complete state before our join
514
+ * event. Recompute the state of the join and of every event received since, re-checking each
515
+ * remote event against its now complete state (events wrongly accepted become rejected), then
516
+ * rebuild the room's current state. Caller holds the room lock.
517
+ */
518
+ async completePartialState(roomId, version, joinEventId, fullBefore, notified) {
519
+ const features = roomVersionFeatures(version);
520
+ const join = await this.getEvent(joinEventId);
521
+ if (!join) return [];
522
+ const createEvent = features.v12 ? await this.findCreateEvent(roomId) : void 0;
523
+ const writeSnapshot = (ids) => this.hs.streams.write(async (id) => {
524
+ await this.hs.db.create("state_snapshots", {
525
+ id,
526
+ room_id: roomId,
527
+ prev: null,
528
+ depth: 0,
529
+ state: ids
530
+ });
531
+ this.snapshotCache.set(id, ids);
532
+ this.snapshotDepths.set(id, 0);
533
+ return id;
534
+ });
535
+ const setSnapshot = async (eventId, snapshot) => {
536
+ await this.hs.db.update("events", [{
537
+ field: "event_id",
538
+ value: eventId
539
+ }], { state_snapshot: snapshot });
540
+ this.eventCache.delete(eventId);
541
+ };
542
+ const joinKey = stateKey("m.room.member", join.pdu.state_key);
543
+ await setSnapshot(joinEventId, await writeSnapshot({
544
+ ...fullBefore,
545
+ [joinKey]: joinEventId
546
+ }));
547
+ const later = (await this.hs.db.findMany("events", {
548
+ where: [{
549
+ field: "room_id",
550
+ value: roomId
551
+ }, {
552
+ field: "stream",
553
+ operator: "gt",
554
+ value: join.stream
555
+ }],
556
+ sortBy: {
557
+ field: "stream",
558
+ direction: "asc"
559
+ }
560
+ })).map((row) => ({
561
+ pdu: this.rowToStored(row).pdu,
562
+ stream: row.stream,
563
+ rejected: false
564
+ }));
565
+ for (const row of await this.hs.db.findMany("outliers", { where: [{
566
+ field: "room_id",
567
+ value: roomId
568
+ }] })) if (typeof row.rejected === "string" && row.rejected.startsWith(PARTIAL_REJECTION)) later.push({
569
+ pdu: row.json.room_id ? row.json : {
570
+ ...row.json,
571
+ room_id: roomId
572
+ },
573
+ stream: Number.MAX_SAFE_INTEGER,
574
+ rejected: true
575
+ });
576
+ later.sort((a, b) => (a.pdu.depth ?? 0) - (b.pdu.depth ?? 0) || a.stream - b.stream);
577
+ const touched = [joinEventId];
578
+ const reinstated = [];
579
+ const joinedServers = new Set(this.hs.federation.serversInRoom(await this.stateIdsToMap(fullBefore), false));
580
+ for (const item of later) {
581
+ const pdu = item.pdu;
582
+ let before;
583
+ try {
584
+ before = await this.stateBefore(roomId, version, (pdu.prev_events ?? []).map(eventIdOf), createEvent);
585
+ } catch {
586
+ continue;
587
+ }
588
+ const beforeMap = await this.stateIdsToMap(before.ids);
589
+ if (createEvent) beforeMap.set(stateKey("m.room.create", ""), createEvent);
590
+ let allowed = true;
591
+ let reason = "";
592
+ if (!this.hs.isLocalUser(pdu.sender)) try {
593
+ checkAuth(pdu, beforeMap, version, {});
594
+ } catch (err) {
595
+ allowed = false;
596
+ reason = err.message;
597
+ }
598
+ if (item.rejected) {
599
+ if (!allowed) continue;
600
+ this.outlierCache.delete(pdu.event_id);
601
+ await this.store(roomId, version, pdu, before, beforeMap, features, { noFederate: true });
602
+ touched.push(pdu.event_id);
603
+ reinstated.push(pdu.event_id);
604
+ continue;
605
+ }
606
+ if (!allowed) {
607
+ await this.hs.transaction(async (db) => {
608
+ await db.delete("events", [{
609
+ field: "event_id",
610
+ value: pdu.event_id
611
+ }]);
612
+ await db.delete("relations", [{
613
+ field: "event_id",
614
+ value: pdu.event_id
615
+ }]);
616
+ await db.upsert("outliers", {
617
+ event_id: pdu.event_id,
618
+ room_id: roomId,
619
+ json: pdu,
620
+ state_snapshot: null,
621
+ rejected: `State before: ${reason}`,
622
+ soft_failed: false
623
+ });
624
+ });
625
+ this.eventCache.delete(pdu.event_id);
626
+ this.outlierCache.delete(pdu.event_id);
627
+ touched.push(pdu.event_id);
628
+ continue;
629
+ }
630
+ if (pdu.type === "m.room.member" && pdu.content?.membership === "join") joinedServers.add(pdu.state_key.slice(pdu.state_key.indexOf(":") + 1));
631
+ const isState = pdu.state_key !== void 0;
632
+ const snapshot = !isState && before.snapshot !== null && before.snapshot !== 0 ? before.snapshot : await writeSnapshot(isState ? {
633
+ ...before.ids,
634
+ [stateKey(pdu.type, pdu.state_key)]: pdu.event_id
635
+ } : before.ids);
636
+ await setSnapshot(pdu.event_id, snapshot);
637
+ touched.push(pdu.event_id);
638
+ }
639
+ if (reinstated.length) {
640
+ const referenced = new Set(later.filter((i) => !i.rejected).flatMap((i) => (i.pdu.prev_events ?? []).map(eventIdOf)));
641
+ const stale = reinstated.filter((id) => referenced.has(id));
642
+ if (stale.length) {
643
+ for (const id of stale) await this.hs.db.delete("forward_extremities", [{
644
+ field: "room_id",
645
+ value: roomId
646
+ }, {
647
+ field: "event_id",
648
+ value: id
649
+ }]);
650
+ this.extremityCache.delete(roomId);
651
+ }
652
+ }
653
+ const rejected = /* @__PURE__ */ new Set();
654
+ for (const id of touched) if ((await this.getOutlier(id))?.rejected) rejected.add(id);
655
+ let extremities = await this.extremities(roomId);
656
+ if (extremities.some((id) => rejected.has(id))) {
657
+ const replacement = new Set(extremities.filter((id) => !rejected.has(id)));
658
+ for (const id of extremities.filter((x) => rejected.has(x))) for (const prev of ((await this.getOutlier(id))?.pdu.prev_events ?? []).map(eventIdOf)) if (await this.getEvent(prev)) replacement.add(prev);
659
+ extremities = [...replacement];
660
+ await this.hs.transaction(async (db) => {
661
+ await db.delete("forward_extremities", [{
662
+ field: "room_id",
663
+ value: roomId
664
+ }]);
665
+ for (const id of extremities) await db.upsert("forward_extremities", {
666
+ room_id: roomId,
667
+ event_id: id
668
+ });
669
+ });
670
+ this.extremityCache.set(roomId, extremities);
671
+ }
672
+ const sets = [];
673
+ for (const ext of extremities) {
674
+ const snap = await this.snapshotOf(ext);
675
+ if (snap !== null) sets.push(await this.snapshotIds(snap));
676
+ }
677
+ if (!sets.length) return [];
678
+ const newIds = sets.length === 1 ? sets[0] : await resolveState(sets, version, (ids) => this.getPdus(ids), createEvent);
679
+ const oldIds = await this.currentStateIds(roomId);
680
+ const pdus = await this.getPdus(Object.values(newIds));
681
+ await this.hs.streams.write((stream) => this.hs.transaction(async (db) => {
682
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(oldIds), ...Object.keys(newIds)])) {
683
+ if (oldIds[k] === newIds[k]) continue;
684
+ const [type, sk] = splitStateKey(k);
685
+ const where = [
686
+ {
687
+ field: "room_id",
688
+ value: roomId
689
+ },
690
+ {
691
+ field: "type",
692
+ value: type
693
+ },
694
+ {
695
+ field: "state_key",
696
+ value: sk
697
+ }
698
+ ];
699
+ if (!newIds[k]) {
700
+ await db.delete("current_state", where);
701
+ if (type === "m.room.member") await db.delete("memberships", [{
702
+ field: "room_id",
703
+ value: roomId
704
+ }, {
705
+ field: "user_id",
706
+ value: sk
707
+ }]);
708
+ continue;
709
+ }
710
+ await db.upsert("current_state", {
711
+ room_id: roomId,
712
+ type,
713
+ state_key: sk,
714
+ event_id: newIds[k]
715
+ });
716
+ if (type === "m.room.member") await db.upsert("memberships", {
717
+ room_id: roomId,
718
+ user_id: sk,
719
+ membership: pdus.get(newIds[k])?.content?.membership ?? "leave",
720
+ event_id: newIds[k],
721
+ stream,
722
+ forgotten: false
723
+ });
724
+ }
725
+ }));
726
+ this.stateCache.delete(roomId);
727
+ this.sharingCache.clear();
728
+ this.invalidate({
729
+ rooms: [roomId],
730
+ events: touched,
731
+ sharing: true
732
+ });
733
+ for (const k of Object.keys(oldIds)) {
734
+ const [type, userId] = splitStateKey(k);
735
+ if (type !== "m.room.member" || this.hs.isLocalUser(userId) || oldIds[k] === newIds[k]) continue;
736
+ if (!await this.hs.keys.isTracked(userId)) await this.hs.keys.dropRemoteCache(userId);
737
+ }
738
+ for (const s of this.hs.federation.serversInRoom(await this.currentState(roomId), false)) joinedServers.add(s);
739
+ return [...joinedServers].filter((s) => s !== this.hs.serverName && !notified.has(s));
740
+ }
741
+ /** The create event, also when the room's current state is not known yet (v12 joins). */
742
+ async findCreateEvent(roomId) {
743
+ const id = `$${roomId.slice(1)}`;
744
+ return await this.getPdu(id) ?? void 0;
745
+ }
746
+ async store(roomId, version, pdu, before, beforeMap, features, opts) {
747
+ const eventId = pdu.event_id;
748
+ const sender = pdu.sender;
749
+ const backfill = !!opts.backfill;
750
+ const lease = opts.stream !== void 0 || backfill ? null : await this.hs.streams.allocate();
751
+ const stream = opts.stream ?? (backfill ? await this.hs.streams.backfillId() : lease.id);
752
+ let wake = null;
753
+ try {
754
+ const isState = pdu.state_key !== void 0;
755
+ const key = isState ? stateKey(pdu.type, pdu.state_key) : null;
756
+ const prevState = key ? beforeMap.get(key) : void 0;
757
+ const afterIds = key ? {
758
+ ...before.ids,
759
+ [key]: eventId
760
+ } : before.ids;
761
+ let snapshot;
762
+ let snapshotRow = null;
763
+ if (!isState && before.snapshot !== null && before.snapshot !== 0) snapshot = before.snapshot;
764
+ else {
765
+ snapshot = stream;
766
+ const depth = before.snapshot ? await this.snapshotDepth(before.snapshot) + 1 : 0;
767
+ snapshotRow = before.snapshot && key && depth < SNAPSHOT_INTERVAL ? {
768
+ id: snapshot,
769
+ room_id: roomId,
770
+ prev: before.snapshot,
771
+ depth,
772
+ state: { [key]: eventId }
773
+ } : {
774
+ id: snapshot,
775
+ room_id: roomId,
776
+ prev: null,
777
+ depth: 0,
778
+ state: afterIds
779
+ };
780
+ }
781
+ const unsigned = { ...pdu.unsigned };
782
+ if (prevState) {
783
+ unsigned.prev_content = prevState.content;
784
+ unsigned.prev_sender = prevState.sender;
785
+ unsigned.replaces_state = prevState.event_id;
786
+ }
787
+ if (pdu.type === "m.room.member" && ["invite", "knock"].includes(pdu.content.membership) && opts.local) unsigned[pdu.content.membership === "invite" ? "invite_room_state" : "knock_room_state"] = this.strippedState(beforeMap);
788
+ const stored = Object.keys(unsigned).length ? {
789
+ ...pdu,
790
+ unsigned
791
+ } : pdu;
792
+ const view = stored.room_id ? stored : {
793
+ ...stored,
794
+ room_id: roomId
795
+ };
796
+ const txnToken = opts.txnId && opts.deviceId ? txnTokenOf(sender, opts.deviceId) : null;
797
+ const relation = pdu.content?.["m.relates_to"];
798
+ const redactsId = features.v11 ? pdu.content?.redacts : pdu.redacts;
799
+ const redaction = pdu.type === "m.room.redaction" && typeof redactsId === "string" ? await this.prepareRedaction(redactsId, view, version) : null;
800
+ let newExtremities = null;
801
+ let stateChanges = [];
802
+ let newCurrentIds = null;
803
+ if (!backfill) {
804
+ const prevIds = new Set((pdu.prev_events ?? []).map(eventIdOf));
805
+ newExtremities = [...(await this.extremities(roomId)).filter((id) => !prevIds.has(id)), eventId];
806
+ const oldCurrent = await this.currentStateIds(roomId);
807
+ if (newExtremities.length === 1) newCurrentIds = afterIds;
808
+ else {
809
+ const sets = [afterIds];
810
+ for (const ext of newExtremities) {
811
+ if (ext === eventId) continue;
812
+ const snap = await this.snapshotOf(ext);
813
+ if (snap !== null) sets.push(await this.snapshotIds(snap));
814
+ }
815
+ const createEvent = features.v12 ? beforeMap.get(stateKey("m.room.create", "")) ?? (pdu.type === "m.room.create" ? pdu : void 0) : void 0;
816
+ newCurrentIds = await resolveState(sets, version, async (ids) => {
817
+ const found = await this.getPdus(ids);
818
+ if (ids.includes(eventId)) found.set(eventId, view);
819
+ return found;
820
+ }, createEvent);
821
+ }
822
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(oldCurrent), ...Object.keys(newCurrentIds)])) if (oldCurrent[k] !== newCurrentIds[k]) stateChanges.push({
823
+ key: k,
824
+ eventId: newCurrentIds[k] ?? null
825
+ });
826
+ }
827
+ const changedPdus = await this.getPdus(stateChanges.map((c) => c.eventId).filter((x) => !!x && x !== eventId));
828
+ changedPdus.set(eventId, view);
829
+ await this.hs.transaction(async (db) => {
830
+ if (snapshotRow) await db.create("state_snapshots", snapshotRow);
831
+ await db.create("events", {
832
+ event_id: eventId,
833
+ room_id: roomId,
834
+ stream,
835
+ depth: pdu.depth,
836
+ type: pdu.type,
837
+ state_key: pdu.state_key ?? null,
838
+ sender,
839
+ json: stored,
840
+ state_snapshot: snapshot,
841
+ txn_id: txnToken ? opts.txnId : null,
842
+ txn_token: txnToken,
843
+ redacted_by: null,
844
+ gap: !!opts.gap
845
+ });
846
+ await db.delete("outliers", [{
847
+ field: "event_id",
848
+ value: eventId
849
+ }]);
850
+ const edge = pdu.content?.["m.relationship"];
851
+ if (isObject(edge) && typeof edge.event_id === "string" && typeof edge.rel_type === "string") await db.upsert("event_edges", {
852
+ event_id: eventId,
853
+ room_id: roomId,
854
+ parent_id: edge.event_id,
855
+ rel_type: edge.rel_type,
856
+ ts: pdu.origin_server_ts ?? 0,
857
+ depth: pdu.depth ?? 0
858
+ });
859
+ if (isObject(relation) && typeof relation.event_id === "string" && typeof relation.rel_type === "string") await db.upsert("relations", {
860
+ event_id: eventId,
861
+ room_id: roomId,
862
+ parent_id: relation.event_id,
863
+ rel_type: relation.rel_type,
864
+ type: pdu.type,
865
+ sender,
866
+ stream
867
+ });
868
+ if (newExtremities) {
869
+ for (const prev of (pdu.prev_events ?? []).map(eventIdOf)) await db.delete("forward_extremities", [{
870
+ field: "room_id",
871
+ value: roomId
872
+ }, {
873
+ field: "event_id",
874
+ value: prev
875
+ }]);
876
+ await db.upsert("forward_extremities", {
877
+ room_id: roomId,
878
+ event_id: eventId
879
+ });
880
+ }
881
+ for (const change of stateChanges) {
882
+ const [type, sk] = splitStateKey(change.key);
883
+ const where = [
884
+ {
885
+ field: "room_id",
886
+ value: roomId
887
+ },
888
+ {
889
+ field: "type",
890
+ value: type
891
+ },
892
+ {
893
+ field: "state_key",
894
+ value: sk
895
+ }
896
+ ];
897
+ if (change.eventId === null) {
898
+ await db.delete("current_state", where);
899
+ if (type === "m.room.member") await db.delete("memberships", [{
900
+ field: "room_id",
901
+ value: roomId
902
+ }, {
903
+ field: "user_id",
904
+ value: sk
905
+ }]);
906
+ continue;
907
+ }
908
+ await db.upsert("current_state", {
909
+ room_id: roomId,
910
+ type,
911
+ state_key: sk,
912
+ event_id: change.eventId
913
+ });
914
+ if (type === "m.room.member") {
915
+ const member = changedPdus.get(change.eventId);
916
+ await db.upsert("memberships", {
917
+ room_id: roomId,
918
+ user_id: sk,
919
+ membership: member?.content?.membership ?? "leave",
920
+ event_id: change.eventId,
921
+ stream,
922
+ forgotten: false
923
+ });
924
+ }
925
+ }
926
+ if (redaction) {
927
+ await db.update("events", [{
928
+ field: "event_id",
929
+ value: redaction.targetId
930
+ }], {
931
+ json: redaction.redacted,
932
+ redacted_by: eventId
933
+ });
934
+ await db.delete("relations", [{
935
+ field: "event_id",
936
+ value: redaction.targetId
937
+ }]);
938
+ }
939
+ });
940
+ this.outlierCache.delete(eventId);
941
+ if (snapshotRow) {
942
+ this.snapshotCache.set(snapshot, afterIds);
943
+ this.snapshotDepths.set(snapshot, snapshotRow.depth);
944
+ }
945
+ this.eventCache.set(eventId, {
946
+ pdu: view,
947
+ stream,
948
+ snapshot,
949
+ txnId: txnToken ? opts.txnId : null,
950
+ txnToken,
951
+ gap: !!opts.gap
952
+ });
953
+ if (newExtremities) this.extremityCache.set(roomId, newExtremities);
954
+ let newState = await this.currentState(roomId);
955
+ if (stateChanges.length) {
956
+ newState = new Map(newState);
957
+ for (const c of stateChanges) if (c.eventId === null) newState.delete(c.key);
958
+ else newState.set(c.key, changedPdus.get(c.eventId));
959
+ this.stateCache.set(roomId, newState);
960
+ }
961
+ if (redaction) this.commitRedaction(redaction, roomId);
962
+ this.invalidate({
963
+ rooms: newExtremities ? [roomId] : [],
964
+ events: [eventId, ...redaction ? [redaction.targetId] : []],
965
+ sharing: stateChanges.some((c) => c.key.startsWith("m.room.member\0"))
966
+ });
967
+ const memberChanges = stateChanges.filter((c) => c.key.startsWith("m.room.member\0"));
968
+ if (memberChanges.length) {
969
+ this.sharingCache.clear();
970
+ for (const c of memberChanges) {
971
+ const userId = splitStateKey(c.key)[1];
972
+ if (this.hs.isLocalUser(userId) || c.eventId === eventId) await this.hs.keys.markDeviceListChanged(userId, false);
973
+ if (!this.hs.isLocalUser(userId)) {
974
+ const m = changedPdus.get(c.eventId ?? "")?.content?.membership;
975
+ if (m === "join" && prevState?.content?.membership !== "join" && c.eventId === eventId) await this.hs.keys.dropRemoteCache(userId);
976
+ else if (m !== "join" && !await this.hs.keys.isTracked(userId)) await this.hs.keys.dropRemoteCache(userId);
977
+ }
978
+ const joinedNow = changedPdus.get(c.eventId ?? "")?.content?.membership === "join";
979
+ const wasJoined = c.eventId === eventId && prevState?.content?.membership === "join";
980
+ if (this.hs.isLocalUser(userId) && joinedNow && !wasJoined) {
981
+ const to = this.hs.federation.serversInRoom(newState);
982
+ if (to.size) this.hs.federation.onDeviceListChange(userId, void 0, to).catch(() => {});
983
+ }
984
+ }
985
+ }
986
+ if (!backfill) {
987
+ const storedEvent = this.eventCache.get(eventId);
988
+ try {
989
+ await this.hs.push.onEvent(storedEvent, newState, version);
990
+ } catch (err) {
991
+ this.hs.logger.error("Push processing failed for", eventId, err);
992
+ }
993
+ if (opts.local && !opts.noFederate) this.hs.federation.onLocalEvent(view, newState);
994
+ if (this.hs.appservices.registrations.length) await this.hs.appservices.onEvent(view, newState).catch((err) => this.hs.logger.warn("Appservice push failed", err));
995
+ if (isState) await this.hs.delayedEvents.onStateEvent(roomId, pdu.type, pdu.state_key, sender).catch(() => {});
996
+ wake = async () => {
997
+ this.hs.notifier.notify(await this.interestedUsers(roomId));
998
+ this.hs.emit("event", toClientEvent(view));
999
+ if (this.hs.adminRoom) this.hs.adminRoom.onEvent(view).catch((err) => this.hs.logger.error("Admin command failed", err));
1000
+ };
1001
+ }
1002
+ } finally {
1003
+ await lease?.done();
1004
+ }
1005
+ await wake?.();
1006
+ return {
1007
+ eventId,
1008
+ status: "persisted"
1009
+ };
1010
+ }
1011
+ async snapshotDepth(id) {
1012
+ const cached = this.snapshotDepths.get(id);
1013
+ if (cached !== void 0) return cached;
1014
+ const depth = (await this.hs.db.findOne("state_snapshots", [{
1015
+ field: "id",
1016
+ value: id
1017
+ }]))?.depth ?? 0;
1018
+ this.snapshotDepths.set(id, depth);
1019
+ return depth;
1020
+ }
1021
+ async membership(roomId, userId) {
1022
+ return membershipOf(await this.currentState(roomId), userId);
1023
+ }
1024
+ async requireJoined(roomId, userId) {
1025
+ if (!await this.exists(roomId)) throw errors.forbidden("You are not in this room");
1026
+ const state = await this.currentState(roomId);
1027
+ if (membershipOf(state, userId) !== "join") throw errors.forbidden("You are not joined to this room");
1028
+ return state;
1029
+ }
1030
+ async joinedMembers(roomId) {
1031
+ return (await this.hs.db.findMany("memberships", { where: [{
1032
+ field: "room_id",
1033
+ value: roomId
1034
+ }, {
1035
+ field: "membership",
1036
+ value: "join"
1037
+ }] })).map((r) => r.user_id);
1038
+ }
1039
+ /** Users that should be woken up for events in this room. */
1040
+ async interestedUsers(roomId) {
1041
+ return (await this.hs.db.findMany("memberships", { where: [{
1042
+ field: "room_id",
1043
+ value: roomId
1044
+ }] })).map((r) => r.user_id);
1045
+ }
1046
+ async roomsForUser(userId, membership = "join") {
1047
+ return (await this.hs.db.findMany("memberships", { where: [{
1048
+ field: "user_id",
1049
+ value: userId
1050
+ }, {
1051
+ field: "membership",
1052
+ value: membership
1053
+ }] })).map((r) => r.room_id);
1054
+ }
1055
+ /** All users sharing at least one joined room with `userId` (including themselves). */
1056
+ async usersSharingRooms(userId) {
1057
+ const cached = this.sharingCache.get(userId);
1058
+ if (cached) return new Set(cached);
1059
+ const out = /* @__PURE__ */ new Set([userId]);
1060
+ for (const roomId of await this.roomsForUser(userId)) for (const u of await this.joinedMembers(roomId)) out.add(u);
1061
+ if (this.sharingCache.size > 1e4) this.sharingCache.clear();
1062
+ this.sharingCache.set(userId, out);
1063
+ return new Set(out);
1064
+ }
1065
+ async prepareRedaction(targetId, redaction, version) {
1066
+ const target = await this.getEvent(targetId);
1067
+ if (!target || target.pdu.room_id !== redaction.room_id) return null;
1068
+ const state = await this.currentState(redaction.room_id);
1069
+ const pl = powerLevels(state, version);
1070
+ if (target.pdu.sender !== redaction.sender && userLevel(pl, redaction.sender) < pl.redact) return null;
1071
+ const redacted = redact(target.pdu, version);
1072
+ redacted.unsigned = { redacted_because: toClientEvent(redaction) };
1073
+ if (target.pdu.unsigned?.prev_content) redacted.unsigned.prev_content = target.pdu.unsigned.prev_content;
1074
+ return {
1075
+ targetId,
1076
+ target,
1077
+ redacted
1078
+ };
1079
+ }
1080
+ commitRedaction(r, roomId) {
1081
+ const view = r.redacted.room_id ? r.redacted : {
1082
+ ...r.redacted,
1083
+ room_id: roomId
1084
+ };
1085
+ this.eventCache.set(r.targetId, {
1086
+ ...r.target,
1087
+ pdu: view
1088
+ });
1089
+ if (r.target.pdu.state_key !== void 0) {
1090
+ const key = stateKey(r.target.pdu.type, r.target.pdu.state_key);
1091
+ const cur = this.stateCache.get(roomId);
1092
+ if (cur?.get(key)?.event_id === r.targetId) cur.set(key, view);
1093
+ }
1094
+ }
1095
+ /** Check whether a redaction would be permitted, before sending it. */
1096
+ async checkRedactionAllowed(roomId, sender, targetId) {
1097
+ const version = await this.roomVersion(roomId);
1098
+ const state = await this.currentState(roomId);
1099
+ const pl = powerLevels(state, version);
1100
+ const target = await this.getEvent(targetId);
1101
+ if (target && target.pdu.room_id === roomId && target.pdu.sender !== sender && userLevel(pl, sender) < pl.redact) throw errors.forbidden("Insufficient power level to redact this event");
1102
+ }
1103
+ /**
1104
+ * Room state shared with invitees and knockers (MSC4311): full PDUs in federation
1105
+ * format, including the create event. Clients receive stripped versions.
1106
+ */
1107
+ strippedState(state) {
1108
+ const types = [
1109
+ "m.room.create",
1110
+ "m.room.join_rules",
1111
+ "m.room.name",
1112
+ "m.room.avatar",
1113
+ "m.room.canonical_alias",
1114
+ "m.room.encryption",
1115
+ "m.room.topic"
1116
+ ];
1117
+ const create = getState(state, "m.room.create");
1118
+ const version = typeof create?.content?.room_version === "string" ? create.content.room_version : "1";
1119
+ const f = roomVersionFeatures(version);
1120
+ const out = [];
1121
+ for (const t of types) {
1122
+ const ev = getState(state, t);
1123
+ if (!ev) continue;
1124
+ const { unsigned, ...full } = ev;
1125
+ const wire = { ...full };
1126
+ if (!f.legacyEventIds) delete wire.event_id;
1127
+ if (f.v12 && t === "m.room.create") delete wire.room_id;
1128
+ out.push(wire);
1129
+ }
1130
+ return out;
1131
+ }
1132
+ /** Filter events down to those `userId` may see according to history visibility. */
1133
+ async filterVisible(userId, roomId, events) {
1134
+ if (events.length === 0) return events;
1135
+ let memberHistory = null;
1136
+ const loadHistory = async () => {
1137
+ if (memberHistory) return memberHistory;
1138
+ if (!userId) return memberHistory = [];
1139
+ memberHistory = (await this.hs.db.findMany("events", { where: [
1140
+ {
1141
+ field: "room_id",
1142
+ value: roomId
1143
+ },
1144
+ {
1145
+ field: "type",
1146
+ value: "m.room.member"
1147
+ },
1148
+ {
1149
+ field: "state_key",
1150
+ value: userId
1151
+ }
1152
+ ] })).map((r) => ({
1153
+ stream: r.stream,
1154
+ membership: r.json.content?.membership
1155
+ }));
1156
+ return memberHistory;
1157
+ };
1158
+ const out = [];
1159
+ for (const raw of events) {
1160
+ const ids = await this.snapshotIds(raw.snapshot);
1161
+ let membership = "leave";
1162
+ if (userId) {
1163
+ const memId = ids[stateKey("m.room.member", userId)];
1164
+ membership = memId ? (await this.getEvent(memId))?.pdu.content?.membership ?? "leave" : "leave";
1165
+ }
1166
+ const ev = userId ? {
1167
+ ...raw,
1168
+ viewerMembership: membership
1169
+ } : raw;
1170
+ if (userId && ev.pdu.type === "m.room.member" && ev.pdu.state_key === userId) {
1171
+ out.push(ev);
1172
+ continue;
1173
+ }
1174
+ const hvId = ids[stateKey("m.room.history_visibility", "")];
1175
+ const hv = hvId ? (await this.getEvent(hvId))?.pdu.content?.history_visibility ?? "shared" : "shared";
1176
+ if (hv === "world_readable") {
1177
+ out.push(ev);
1178
+ continue;
1179
+ }
1180
+ if (!userId) continue;
1181
+ if (membership === "join") {
1182
+ out.push(ev);
1183
+ continue;
1184
+ }
1185
+ if (hv === "invited" && membership === "invite") {
1186
+ out.push(ev);
1187
+ continue;
1188
+ }
1189
+ if (hv === "shared") {
1190
+ const history = await loadHistory();
1191
+ if (history.reduce((a, h) => !a || h.stream > a.stream ? h : a, null)?.membership === "join" || history.some((h) => h.stream >= ev.stream && h.membership === "join")) {
1192
+ out.push(ev);
1193
+ continue;
1194
+ }
1195
+ }
1196
+ }
1197
+ return out;
1198
+ }
1199
+ /** Whether the user may view the room's current state / history at all (`world_readable` or member). */
1200
+ async canPeek(roomId, userId) {
1201
+ const state = await this.currentState(roomId);
1202
+ const membership = membershipOf(state, userId);
1203
+ const hv = getState(state, "m.room.history_visibility")?.content?.history_visibility ?? "shared";
1204
+ return {
1205
+ joined: membership === "join",
1206
+ worldReadable: hv === "world_readable",
1207
+ membership
1208
+ };
1209
+ }
1210
+ /** The state snapshot visible to a user who is no longer joined (state at their leave). */
1211
+ async membershipEvent(roomId, userId) {
1212
+ const state = await this.currentState(roomId);
1213
+ const ev = getState(state, "m.room.member", userId);
1214
+ return ev?.event_id ? this.getEvent(ev.event_id) : null;
1215
+ }
1216
+ formatEvent(ev, viewer, opts = {}) {
1217
+ const client = toClientEvent(ev.pdu, opts);
1218
+ if (viewer && ev.txnToken && ev.txnToken === txnTokenOf(viewer.userId, viewer.deviceId)) client.unsigned = {
1219
+ ...client.unsigned,
1220
+ transaction_id: ev.txnId
1221
+ };
1222
+ if (ev.viewerMembership) client.unsigned = {
1223
+ ...client.unsigned,
1224
+ membership: ev.viewerMembership
1225
+ };
1226
+ return client;
1227
+ }
1228
+ stateToList(state) {
1229
+ return [...state.values()];
1230
+ }
1231
+ async createRoom(sender, body) {
1232
+ if (body.room_version !== void 0 && typeof body.room_version !== "string") throw errors.badJson("room_version must be a string");
1233
+ const version = body.room_version ?? this.hs.config.defaultRoomVersion;
1234
+ if (!SUPPORTED_ROOM_VERSIONS.includes(version)) throw errors.unsupportedRoomVersion();
1235
+ const features = roomVersionFeatures(version);
1236
+ const visibility = body.visibility === "public" ? "public" : "private";
1237
+ const preset = body.preset ?? (visibility === "public" ? "public_chat" : "private_chat");
1238
+ if (![
1239
+ "public_chat",
1240
+ "private_chat",
1241
+ "trusted_private_chat"
1242
+ ].includes(preset)) throw errors.invalidParam("Unknown preset");
1243
+ if (body.creation_content !== void 0 && !isObject(body.creation_content)) throw errors.badJson("creation_content must be an object");
1244
+ if (body.initial_state !== void 0 && !Array.isArray(body.initial_state)) throw errors.badJson("initial_state must be an array");
1245
+ if (body.invite !== void 0 && (!Array.isArray(body.invite) || !body.invite.every(isValidUserId))) throw errors.invalidParam("invite must be a list of user IDs");
1246
+ if (body.name !== void 0 && typeof body.name !== "string") throw errors.badJson("name must be a string");
1247
+ if (body.topic !== void 0 && typeof body.topic !== "string") throw errors.badJson("topic must be a string");
1248
+ if (Array.isArray(body.invite_3pid) && body.invite_3pid.length > 0) throw errors.threepidDenied();
1249
+ let alias;
1250
+ if (body.room_alias_name !== void 0) {
1251
+ if (typeof body.room_alias_name !== "string" || /[:\s#]/.test(body.room_alias_name) || body.room_alias_name.length === 0) throw errors.invalidParam("Invalid room_alias_name");
1252
+ alias = `#${body.room_alias_name}:${this.hs.serverName}`;
1253
+ if (await this.hs.db.findOne("room_aliases", [{
1254
+ field: "alias",
1255
+ value: alias
1256
+ }])) throw errors.roomInUse();
1257
+ }
1258
+ const createContent = {
1259
+ ...body.creation_content,
1260
+ room_version: version
1261
+ };
1262
+ if (!features.v11) createContent.creator = sender;
1263
+ else delete createContent.creator;
1264
+ const creators = /* @__PURE__ */ new Set();
1265
+ if (features.v12) {
1266
+ const extra = createContent.additional_creators;
1267
+ if (extra !== void 0 && (!Array.isArray(extra) || !extra.every(isValidUserId))) throw errors.badJson("additional_creators must be a list of user IDs");
1268
+ const additional = new Set(extra ?? []);
1269
+ if (preset === "trusted_private_chat") for (const u of body.invite ?? []) additional.add(u);
1270
+ additional.delete(sender);
1271
+ if (additional.size) createContent.additional_creators = [...additional];
1272
+ else delete createContent.additional_creators;
1273
+ creators.add(sender);
1274
+ for (const u of additional) creators.add(u);
1275
+ } else delete createContent.additional_creators;
1276
+ let roomId;
1277
+ let prebuiltCreate;
1278
+ if (features.v12) {
1279
+ prebuiltCreate = finalisePdu({
1280
+ sender,
1281
+ type: "m.room.create",
1282
+ state_key: "",
1283
+ content: createContent,
1284
+ origin_server_ts: Date.now(),
1285
+ depth: 1,
1286
+ prev_events: [],
1287
+ auth_events: []
1288
+ }, version, this.hs.serverName, this.hs.signingKey);
1289
+ roomId = `!${prebuiltCreate.event_id.slice(1)}`;
1290
+ } else roomId = generateRoomId(this.hs.serverName);
1291
+ await this.hs.db.create("rooms", {
1292
+ room_id: roomId,
1293
+ room_version: version,
1294
+ creator: sender,
1295
+ is_public: visibility === "public",
1296
+ created_ts: Date.now()
1297
+ });
1298
+ this.versions.set(roomId, version);
1299
+ const send = (draft) => this.sendEvent(roomId, sender, draft);
1300
+ if (prebuiltCreate) {
1301
+ const pdu = prebuiltCreate;
1302
+ await this.withLock(roomId, () => this.persistEvent(roomId, version, pdu, { local: true }));
1303
+ } else await send({
1304
+ type: "m.room.create",
1305
+ state_key: "",
1306
+ content: createContent
1307
+ });
1308
+ await send({
1309
+ type: "m.room.member",
1310
+ state_key: sender,
1311
+ content: {
1312
+ membership: "join",
1313
+ ...await this.hs.profile(sender)
1314
+ }
1315
+ });
1316
+ const pl = {
1317
+ users: features.v12 ? {} : { [sender]: 100 },
1318
+ users_default: 0,
1319
+ events: {
1320
+ "m.room.name": 50,
1321
+ "m.room.power_levels": 100,
1322
+ "m.room.history_visibility": 100,
1323
+ "m.room.canonical_alias": 50,
1324
+ "m.room.avatar": 50,
1325
+ "m.room.tombstone": features.v12 ? 150 : 100,
1326
+ "m.room.server_acl": 100,
1327
+ "m.room.encryption": 100
1328
+ },
1329
+ events_default: 0,
1330
+ state_default: 50,
1331
+ ban: 50,
1332
+ kick: 50,
1333
+ redact: 50,
1334
+ invite: preset === "public_chat" ? 50 : 0,
1335
+ notifications: { room: 50 }
1336
+ };
1337
+ if (preset === "trusted_private_chat" && !features.v12) for (const u of body.invite ?? []) pl.users[u] = 100;
1338
+ const override = body.power_level_content_override;
1339
+ if (override !== void 0 && !isObject(override)) throw errors.badJson("power_level_content_override must be an object");
1340
+ const initialState = body.initial_state ?? [];
1341
+ const initialPl = initialState.find((e) => e?.type === "m.room.power_levels" && (e.state_key ?? "") === "");
1342
+ const predecessor = createContent.predecessor?.room_id;
1343
+ if (typeof predecessor === "string") await copyRoomPushRules(this.hs, sender, predecessor, roomId);
1344
+ const plContent = {
1345
+ ...pl,
1346
+ ...override,
1347
+ ...initialPl?.content
1348
+ };
1349
+ validatePowerLevelContent(plContent, creators);
1350
+ await send({
1351
+ type: "m.room.power_levels",
1352
+ state_key: "",
1353
+ content: plContent
1354
+ });
1355
+ if (alias) {
1356
+ await this.hs.db.create("room_aliases", {
1357
+ alias,
1358
+ room_id: roomId,
1359
+ creator: sender
1360
+ });
1361
+ await send({
1362
+ type: "m.room.canonical_alias",
1363
+ state_key: "",
1364
+ content: { alias }
1365
+ });
1366
+ }
1367
+ const presetState = [["m.room.join_rules", { join_rule: preset === "public_chat" ? "public" : "invite" }], ["m.room.history_visibility", { history_visibility: "shared" }]];
1368
+ if (preset !== "public_chat") presetState.push(["m.room.guest_access", { guest_access: "can_join" }]);
1369
+ const initialKeys = new Set(initialState.map((e) => stateKey(e?.type, e?.state_key ?? "")));
1370
+ for (const [type, content] of presetState) if (!initialKeys.has(stateKey(type, ""))) await send({
1371
+ type,
1372
+ state_key: "",
1373
+ content
1374
+ });
1375
+ for (const ev of initialState) {
1376
+ if (!isObject(ev) || typeof ev.type !== "string" || !isObject(ev.content)) throw errors.badJson("Invalid initial_state event");
1377
+ if (ev.type === "m.room.power_levels" || ev.type === "m.room.create" || ev.type === "m.room.member") continue;
1378
+ await send({
1379
+ type: ev.type,
1380
+ state_key: ev.state_key ?? "",
1381
+ content: ev.content
1382
+ });
1383
+ }
1384
+ if (body.name !== void 0) await send({
1385
+ type: "m.room.name",
1386
+ state_key: "",
1387
+ content: { name: body.name }
1388
+ });
1389
+ if (body.topic !== void 0) await send({
1390
+ type: "m.room.topic",
1391
+ state_key: "",
1392
+ content: {
1393
+ topic: body.topic,
1394
+ "m.topic": { "m.text": [{
1395
+ mimetype: "text/plain",
1396
+ body: body.topic
1397
+ }] }
1398
+ }
1399
+ });
1400
+ for (const invitee of body.invite ?? []) await this.invite(roomId, sender, invitee, { isDirect: !!body.is_direct });
1401
+ return alias ? {
1402
+ room_id: roomId,
1403
+ room_alias: alias
1404
+ } : { room_id: roomId };
1405
+ }
1406
+ async resolveRoomIdOrAlias(idOrAlias) {
1407
+ return (await this.resolveRoom(idOrAlias)).roomId;
1408
+ }
1409
+ /** Resolve a room ID or alias, including remote aliases; returns candidate servers for joining. */
1410
+ async resolveRoom(idOrAlias) {
1411
+ if (idOrAlias.startsWith("!")) return {
1412
+ roomId: idOrAlias,
1413
+ servers: []
1414
+ };
1415
+ if (idOrAlias.startsWith("#")) {
1416
+ const row = await this.hs.db.findOne("room_aliases", [{
1417
+ field: "alias",
1418
+ value: idOrAlias
1419
+ }]);
1420
+ if (row) return {
1421
+ roomId: row.room_id,
1422
+ servers: [this.hs.serverName]
1423
+ };
1424
+ const fromAppservice = this.hs.isLocalUser(idOrAlias) ? await this.hs.appservices.queryAlias(idOrAlias) : null;
1425
+ if (fromAppservice) return {
1426
+ roomId: fromAppservice,
1427
+ servers: [this.hs.serverName]
1428
+ };
1429
+ const server = parseId(idOrAlias, "#")?.server;
1430
+ if (server && server !== this.hs.serverName && this.hs.federation.enabled) try {
1431
+ const res = await this.hs.federation.queryAlias(idOrAlias);
1432
+ return {
1433
+ roomId: res.room_id,
1434
+ servers: Array.isArray(res.servers) ? res.servers : [server]
1435
+ };
1436
+ } catch {
1437
+ throw errors.notFound("Room alias not found");
1438
+ }
1439
+ throw errors.notFound("Room alias not found");
1440
+ }
1441
+ throw errors.invalidParam("Invalid room ID or alias");
1442
+ }
1443
+ /** Whether the room was created on this server and we still have its state. */
1444
+ async isLocallyHosted(roomId) {
1445
+ if (!await this.exists(roomId)) return false;
1446
+ const create = await this.createEventOf(roomId);
1447
+ return !!create && this.hs.isLocalUser(create.sender);
1448
+ }
1449
+ /** Whether any local user is joined to the room (i.e. this server participates in it). */
1450
+ async serverIsJoined(roomId) {
1451
+ if (!await this.exists(roomId)) return false;
1452
+ for (const [key, ev] of await this.currentState(roomId)) {
1453
+ const [type, userId] = splitStateKey(key);
1454
+ if (type === "m.room.member" && ev.content?.membership === "join" && this.hs.isLocalUser(userId)) return true;
1455
+ }
1456
+ return false;
1457
+ }
1458
+ /** Servers that might help with a remote membership change for `userId` in `roomId`. */
1459
+ async remoteServersFor(roomId, userId, extra = []) {
1460
+ const servers = [...extra];
1461
+ const row = await this.hs.db.findOne("memberships", [{
1462
+ field: "room_id",
1463
+ value: roomId
1464
+ }, {
1465
+ field: "user_id",
1466
+ value: userId
1467
+ }]);
1468
+ const ev = row ? await this.getPdu(row.event_id) : null;
1469
+ if (ev && !this.hs.isLocalUser(ev.sender)) servers.push(ev.sender.slice(ev.sender.indexOf(":") + 1));
1470
+ if (!extra.length) {
1471
+ const roomServer = parseId(roomId, "!")?.server;
1472
+ if (roomServer) servers.push(roomServer);
1473
+ if (await this.exists(roomId)) for (const s of this.hs.federation.serversInRoom(await this.currentState(roomId))) servers.push(s);
1474
+ }
1475
+ return [...new Set(servers)].filter((s) => s !== this.hs.serverName);
1476
+ }
1477
+ /**
1478
+ * Record a membership event for a room this server may not participate in (remote
1479
+ * invites, rejections, knocks) so that it shows up in /sync.
1480
+ */
1481
+ async recordRemoteMembership(pdu, version) {
1482
+ await this.hs.db.upsert("outliers", {
1483
+ event_id: pdu.event_id,
1484
+ room_id: pdu.room_id,
1485
+ json: pdu,
1486
+ state_snapshot: null,
1487
+ rejected: null,
1488
+ soft_failed: false
1489
+ });
1490
+ this.outlierCache.delete(pdu.event_id);
1491
+ if (!await this.serverIsJoined(pdu.room_id)) await this.hs.streams.write((stream) => this.hs.db.upsert("memberships", {
1492
+ room_id: pdu.room_id,
1493
+ user_id: pdu.state_key,
1494
+ membership: pdu.content?.membership,
1495
+ event_id: pdu.event_id,
1496
+ stream,
1497
+ forgotten: false
1498
+ }));
1499
+ this.sharingCache.clear();
1500
+ this.invalidate({
1501
+ events: [pdu.event_id],
1502
+ sharing: true
1503
+ });
1504
+ this.hs.notifier.notify([pdu.state_key]);
1505
+ }
1506
+ async join(userId, roomId, extra = {}) {
1507
+ const version = await this.roomVersion(roomId);
1508
+ if (version && !await this.serverIsJoined(roomId) && extra.servers?.includes(this.hs.serverName) && await this.isLocallyHosted(roomId)) return this.joinLocal(userId, roomId, version, extra);
1509
+ if (!version || !await this.serverIsJoined(roomId)) {
1510
+ if (version && await this.membership(roomId, userId) === "ban") throw errors.forbidden("You are banned from this room");
1511
+ const servers = await this.remoteServersFor(roomId, userId, extra.servers);
1512
+ if (!servers.length || !this.hs.federation.enabled) {
1513
+ if (version) return this.joinLocal(userId, roomId, version, extra);
1514
+ throw errors.notFound("Room not found");
1515
+ }
1516
+ const content = {
1517
+ ...extra.content,
1518
+ membership: "join",
1519
+ ...await this.hs.profile(userId)
1520
+ };
1521
+ if (extra.reason) content.reason = extra.reason;
1522
+ this.hs.logger.debug("Remote join", userId, roomId, "via", servers);
1523
+ await this.hs.federation.joinRemoteRoom(userId, roomId, servers, content);
1524
+ const predecessor = (await this.createEventOf(roomId))?.content?.predecessor?.room_id;
1525
+ if (typeof predecessor === "string") await copyRoomPushRules(this.hs, userId, predecessor, roomId);
1526
+ return roomId;
1527
+ }
1528
+ return this.joinLocal(userId, roomId, version, extra);
1529
+ }
1530
+ async joinLocal(userId, roomId, version, extra) {
1531
+ this.hs.logger.debug("Local join", userId, roomId);
1532
+ const state = await this.currentState(roomId);
1533
+ const content = {
1534
+ ...extra.content,
1535
+ membership: "join",
1536
+ ...await this.hs.profile(userId)
1537
+ };
1538
+ if (extra.reason) content.reason = extra.reason;
1539
+ const existing = getState(state, "m.room.member", userId);
1540
+ if (existing?.content?.membership === "join" && JSON.stringify(existing.content) === JSON.stringify(content)) return roomId;
1541
+ const joinRule = getState(state, "m.room.join_rules")?.content?.join_rule;
1542
+ const current = membershipOf(state, userId);
1543
+ if ((joinRule === "restricted" || joinRule === "knock_restricted") && current !== "join" && current !== "invite") {
1544
+ const via = await this.findRestrictedAuthoriser(userId, state, version);
1545
+ if (via) content.join_authorised_via_users_server = via;
1546
+ else if (this.hs.federation.enabled && await this.mayQualifyRemotely(userId, state)) {
1547
+ const pl = powerLevels(state, version);
1548
+ const servers = /* @__PURE__ */ new Set();
1549
+ for (const [k, ev] of state) {
1550
+ const [type, member] = splitStateKey(k);
1551
+ if (type === "m.room.member" && ev.content?.membership === "join" && !this.hs.isLocalUser(member) && userLevel(pl, member) >= pl.invite) servers.add(member.slice(member.indexOf(":") + 1));
1552
+ }
1553
+ if (servers.size) {
1554
+ await this.hs.federation.joinRemoteRoom(userId, roomId, [...servers], content);
1555
+ return roomId;
1556
+ }
1557
+ }
1558
+ }
1559
+ await this.sendEvent(roomId, userId, {
1560
+ type: "m.room.member",
1561
+ state_key: userId,
1562
+ content
1563
+ });
1564
+ const predecessor = getState(state, "m.room.create")?.content?.predecessor?.room_id;
1565
+ if (typeof predecessor === "string") await copyRoomPushRules(this.hs, userId, predecessor, roomId);
1566
+ return roomId;
1567
+ }
1568
+ /** Whether an allowed room exists that we can't check (or the user is joined to), so another server might authorise. */
1569
+ async mayQualifyRemotely(userId, state) {
1570
+ const allow = getState(state, "m.room.join_rules")?.content?.allow;
1571
+ if (!Array.isArray(allow)) return false;
1572
+ for (const rule of allow) {
1573
+ if (rule?.type !== "m.room_membership" || typeof rule.room_id !== "string") continue;
1574
+ if (!await this.serverIsJoined(rule.room_id)) return true;
1575
+ if (await this.membership(rule.room_id, userId) === "join") return true;
1576
+ }
1577
+ return false;
1578
+ }
1579
+ async findRestrictedAuthoriser(userId, state, version) {
1580
+ const allow = getState(state, "m.room.join_rules")?.content?.allow;
1581
+ if (!Array.isArray(allow)) return null;
1582
+ let permitted = false;
1583
+ for (const rule of allow) if (rule?.type === "m.room_membership" && typeof rule.room_id === "string" && await this.exists(rule.room_id)) {
1584
+ if (await this.membership(rule.room_id, userId) === "join") {
1585
+ permitted = true;
1586
+ break;
1587
+ }
1588
+ }
1589
+ if (!permitted) return null;
1590
+ const pl = powerLevels(state, version);
1591
+ for (const [k, ev] of state) {
1592
+ const [type, key] = splitStateKey(k);
1593
+ if (type === "m.room.member" && ev.content?.membership === "join" && this.hs.isLocalUser(key) && userLevel(pl, key) >= pl.invite) return key;
1594
+ }
1595
+ return null;
1596
+ }
1597
+ async invite(roomId, sender, target, opts = {}) {
1598
+ if (!isValidUserId(target)) throw errors.invalidParam("Invalid user ID");
1599
+ const local = this.hs.isLocalUser(target);
1600
+ if (local && !await this.hs.db.findOne("users", [{
1601
+ field: "user_id",
1602
+ value: target
1603
+ }]) && !await this.hs.appservices.queryUser(target)) throw errors.notFound("Unknown user");
1604
+ if (local && await inviteDecision(this.hs, target, sender) === "block") throw new MatrixError(403, "M_FORBIDDEN", "This user does not accept invites from you", { "org.matrix.msc4155.errcode": "M_INVITE_BLOCKED" });
1605
+ await this.requireJoined(roomId, sender);
1606
+ const state = await this.currentState(roomId);
1607
+ if (membershipOf(state, target) === "invite") return;
1608
+ const content = { membership: "invite" };
1609
+ if (local) Object.assign(content, await this.hs.profile(target));
1610
+ if (opts.reason) content.reason = opts.reason;
1611
+ if (opts.isDirect) content.is_direct = true;
1612
+ if (opts.thirdPartyInvite) content.third_party_invite = opts.thirdPartyInvite;
1613
+ if (local) {
1614
+ await this.sendEvent(roomId, sender, {
1615
+ type: "m.room.member",
1616
+ state_key: target,
1617
+ content
1618
+ });
1619
+ return;
1620
+ }
1621
+ if (!this.hs.federation.enabled) throw errors.forbidden("Federation is disabled");
1622
+ await this.withLock(roomId, async () => {
1623
+ const version = await this.roomVersion(roomId);
1624
+ const pdu = await this.buildLocalEvent(roomId, version, sender, {
1625
+ type: "m.room.member",
1626
+ state_key: target,
1627
+ content
1628
+ });
1629
+ const inviteState = [...this.strippedState(await this.currentState(roomId))];
1630
+ const senderMember = getState(await this.currentState(roomId), "m.room.member", sender);
1631
+ if (senderMember) {
1632
+ const { unsigned, ...full } = senderMember;
1633
+ inviteState.push(this.hs.federation.toFederationPdu(full, version));
1634
+ }
1635
+ const signed = await this.hs.federation.sendInvite(pdu, version, inviteState);
1636
+ await this.persistEvent(roomId, version, signed, { local: true });
1637
+ });
1638
+ }
1639
+ async setMembership(roomId, sender, target, membership, reason, via = []) {
1640
+ const remote = !await this.serverIsJoined(roomId) && (membership === "knock" || !await this.isLocallyHosted(roomId));
1641
+ if (sender === target && (membership === "leave" || membership === "knock") && remote) {
1642
+ await this.remoteMembership(roomId, target, membership, reason, via);
1643
+ return;
1644
+ }
1645
+ if (!await this.exists(roomId)) throw errors.notFound("Room not found");
1646
+ const content = { membership };
1647
+ if (reason) content.reason = reason;
1648
+ if (membership === "ban" || membership === "leave" && sender !== target) {
1649
+ const prev = getState(await this.currentState(roomId), "m.room.member", target);
1650
+ if (prev?.content?.displayname) content.displayname = prev.content.displayname;
1651
+ }
1652
+ await this.sendEvent(roomId, sender, {
1653
+ type: "m.room.member",
1654
+ state_key: target,
1655
+ content
1656
+ });
1657
+ }
1658
+ async remoteMembership(roomId, userId, membership, reason, via = []) {
1659
+ const row = await this.hs.db.findOne("memberships", [{
1660
+ field: "room_id",
1661
+ value: roomId
1662
+ }, {
1663
+ field: "user_id",
1664
+ value: userId
1665
+ }]);
1666
+ if (membership === "leave" && (!row || row.membership === "leave" || row.membership === "ban")) {
1667
+ if (!await this.exists(roomId)) throw errors.notFound("Room not found");
1668
+ if (row?.membership === "ban") throw errors.forbidden("You are banned from this room");
1669
+ return;
1670
+ }
1671
+ const servers = await this.remoteServersFor(roomId, userId, via);
1672
+ if (membership === "knock") {
1673
+ if (!servers.length || !this.hs.federation.enabled) throw errors.notFound("Room not found");
1674
+ const { pdu, knockState } = await this.hs.federation.knockRemoteRoom(userId, roomId, servers, reason);
1675
+ await this.recordRemoteMembership({
1676
+ ...pdu,
1677
+ unsigned: { knock_room_state: knockState }
1678
+ }, "");
1679
+ return;
1680
+ }
1681
+ let leave;
1682
+ try {
1683
+ if (!servers.length || !this.hs.federation.enabled) throw new Error("No servers to contact");
1684
+ leave = await this.hs.federation.leaveRemoteRoom(userId, roomId, servers, reason);
1685
+ } catch (err) {
1686
+ this.hs.logger.debug("Remote leave failed, rejecting locally", err);
1687
+ const content = { membership: "leave" };
1688
+ if (reason) content.reason = reason;
1689
+ leave = finalisePdu({
1690
+ room_id: roomId,
1691
+ sender: userId,
1692
+ type: "m.room.member",
1693
+ state_key: userId,
1694
+ content,
1695
+ origin_server_ts: Date.now(),
1696
+ depth: 1,
1697
+ prev_events: row ? [row.event_id] : [],
1698
+ auth_events: []
1699
+ }, "10", this.hs.serverName, this.hs.signingKey);
1700
+ }
1701
+ await this.recordRemoteMembership(leave, "");
1702
+ }
1703
+ /** Update the member event in every joined room after a profile change. */
1704
+ async propagateProfile(userId) {
1705
+ const profile = await this.hs.profile(userId);
1706
+ for (const roomId of await this.roomsForUser(userId)) {
1707
+ const prev = getState(await this.currentState(roomId), "m.room.member", userId);
1708
+ if (!prev) continue;
1709
+ const content = {
1710
+ ...prev.content,
1711
+ membership: "join",
1712
+ ...profile
1713
+ };
1714
+ if (profile.displayname === void 0) delete content.displayname;
1715
+ if (profile.avatar_url === void 0) delete content.avatar_url;
1716
+ try {
1717
+ await this.sendEvent(roomId, userId, {
1718
+ type: "m.room.member",
1719
+ state_key: userId,
1720
+ content
1721
+ });
1722
+ } catch (err) {
1723
+ this.hs.logger.warn("Failed to propagate profile to", roomId, err);
1724
+ }
1725
+ }
1726
+ }
1727
+ };
1728
+ //#endregion
1729
+ export { MissingStateError, RoomService, txnTokenOf, validatePowerLevelContent };