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.
- package/LICENSE +75 -0
- package/README.md +457 -2
- package/dist/adapters/auth.d.ts +50 -0
- package/dist/adapters/auth.js +18 -0
- package/dist/adapters/cluster.d.ts +55 -0
- package/dist/adapters/cluster.js +249 -0
- package/dist/adapters/media.d.ts +50 -0
- package/dist/adapters/media.js +63 -0
- package/dist/adapters/s3.d.ts +33 -0
- package/dist/adapters/s3.js +95 -0
- package/dist/api/account.js +560 -0
- package/dist/api/admin.js +251 -0
- package/dist/api/appservice.js +33 -0
- package/dist/api/backup.js +269 -0
- package/dist/api/federation.js +792 -0
- package/dist/api/filters.js +87 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +170 -0
- package/dist/api/media.js +255 -0
- package/dist/api/misc.js +86 -0
- package/dist/api/preview.js +200 -0
- package/dist/api/profile.js +165 -0
- package/dist/api/push.js +187 -0
- package/dist/api/relationships.js +264 -0
- package/dist/api/rooms.js +1364 -0
- package/dist/api/rtc.d.ts +24 -0
- package/dist/api/rtc.js +52 -0
- package/dist/api/search.js +155 -0
- package/dist/api/slidingsync.js +464 -0
- package/dist/api/spaces.js +172 -0
- package/dist/api/sync.js +600 -0
- package/dist/api/threadsubs.js +129 -0
- package/dist/api/threepid.js +122 -0
- package/dist/api/uia.js +126 -0
- package/dist/api/user.js +286 -0
- package/dist/core/admin.d.ts +85 -0
- package/dist/core/admin.js +307 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +482 -0
- package/dist/core/appservices.d.ts +73 -0
- package/dist/core/appservices.js +368 -0
- package/dist/core/audit.d.ts +51 -0
- package/dist/core/audit.js +86 -0
- package/dist/core/auth.d.ts +48 -0
- package/dist/core/auth.js +237 -0
- package/dist/core/authrules.d.ts +4 -0
- package/dist/core/authrules.js +347 -0
- package/dist/core/cluster.d.ts +66 -0
- package/dist/core/cluster.js +191 -0
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/crypto.js +100 -0
- package/dist/core/delayed.d.ts +39 -0
- package/dist/core/delayed.js +159 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/events.d.ts +32 -0
- package/dist/core/events.js +185 -0
- package/dist/core/glob.js +66 -0
- package/dist/core/identity.d.ts +24 -0
- package/dist/core/identity.js +96 -0
- package/dist/core/ids.js +36 -0
- package/dist/core/invitefilter.js +35 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +449 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +54 -0
- package/dist/core/oauth.d.ts +38 -0
- package/dist/core/oauth.js +108 -0
- package/dist/core/partialstate.d.ts +32 -0
- package/dist/core/partialstate.js +197 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +96 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +47 -0
- package/dist/core/push.js +306 -0
- package/dist/core/pushrules.d.ts +1 -0
- package/dist/core/pushrules.js +228 -0
- package/dist/core/ratelimit.d.ts +23 -0
- package/dist/core/ratelimit.js +77 -0
- package/dist/core/rooms.d.ts +215 -0
- package/dist/core/rooms.js +1729 -0
- package/dist/core/settings.d.ts +11 -0
- package/dist/core/settings.js +29 -0
- package/dist/core/sso.d.ts +46 -0
- package/dist/core/sso.js +165 -0
- package/dist/core/stateres.d.ts +4 -0
- package/dist/core/stateres.js +208 -0
- package/dist/core/threepid.d.ts +80 -0
- package/dist/core/threepid.js +275 -0
- package/dist/core/typing.d.ts +16 -0
- package/dist/core/typing.js +60 -0
- package/dist/federation/client.d.ts +51 -0
- package/dist/federation/client.js +278 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1058 -0
- package/dist/federation/keyring.d.ts +26 -0
- package/dist/federation/keyring.js +122 -0
- package/dist/federation/sender.d.ts +26 -0
- package/dist/federation/sender.js +164 -0
- package/dist/homeserver.d.ts +223 -0
- package/dist/homeserver.js +301 -0
- package/dist/http/adapters.d.ts +40 -0
- package/dist/http/adapters.js +130 -0
- package/dist/http/handler.d.ts +8 -0
- package/dist/http/handler.js +202 -0
- package/dist/http/input.js +24 -0
- package/dist/http/router.d.ts +60 -0
- package/dist/http/router.js +76 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +14 -0
- package/dist/storage/memory.d.ts +5 -0
- package/dist/storage/memory.js +122 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.js +664 -0
- package/dist/storage/sql.d.ts +72 -0
- package/dist/storage/sql.js +274 -0
- package/dist/storage/types.d.ts +55 -0
- 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 };
|