matrix-nodejs-homeserver 0.0.0-stage → 0.3.1

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