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.
- package/LICENSE +75 -0
- package/README.md +479 -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 +594 -0
- package/dist/api/admin.js +267 -0
- package/dist/api/appservice.js +45 -0
- package/dist/api/backup.js +280 -0
- package/dist/api/federation.js +826 -0
- package/dist/api/filters.js +88 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +183 -0
- package/dist/api/media.js +271 -0
- package/dist/api/misc.js +95 -0
- package/dist/api/preview.js +201 -0
- package/dist/api/profile.js +176 -0
- package/dist/api/push.js +202 -0
- package/dist/api/relationships.js +270 -0
- package/dist/api/rooms.js +1487 -0
- package/dist/api/rtc.d.ts +23 -0
- package/dist/api/rtc.js +66 -0
- package/dist/api/search.js +161 -0
- package/dist/api/slidingsync.js +474 -0
- package/dist/api/spaces.js +177 -0
- package/dist/api/sync.js +609 -0
- package/dist/api/threadsubs.js +140 -0
- package/dist/api/threepid.js +155 -0
- package/dist/api/uia.js +127 -0
- package/dist/api/user.js +329 -0
- package/dist/core/admin.d.ts +84 -0
- package/dist/core/admin.js +308 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +483 -0
- package/dist/core/appservices.d.ts +74 -0
- package/dist/core/appservices.js +369 -0
- package/dist/core/audit.d.ts +50 -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 +348 -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 +160 -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 +186 -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 +36 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +450 -0
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -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 +198 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +97 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +46 -0
- package/dist/core/push.js +307 -0
- package/dist/core/pushrules.js +229 -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 +1731 -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 +209 -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 +279 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1059 -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 +224 -0
- package/dist/homeserver.js +302 -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 +181 -0
- package/dist/http/input.js +24 -0
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +43 -0
- package/dist/http/router.js +56 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +15 -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/dist/vendor/zod/LICENSE +21 -0
- package/dist/vendor/zod/v4/classic/errors.js +54 -0
- package/dist/vendor/zod/v4/classic/parse.js +17 -0
- package/dist/vendor/zod/v4/classic/schemas.js +865 -0
- package/dist/vendor/zod/v4/core/api.js +512 -0
- package/dist/vendor/zod/v4/core/checks.js +300 -0
- package/dist/vendor/zod/v4/core/core.js +109 -0
- package/dist/vendor/zod/v4/core/doc.js +35 -0
- package/dist/vendor/zod/v4/core/errors.js +118 -0
- package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
- package/dist/vendor/zod/v4/core/memoizer.js +255 -0
- package/dist/vendor/zod/v4/core/parse.js +206 -0
- package/dist/vendor/zod/v4/core/regexes.js +73 -0
- package/dist/vendor/zod/v4/core/registries.js +48 -0
- package/dist/vendor/zod/v4/core/schemas.js +1378 -0
- package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
- package/dist/vendor/zod/v4/core/util.js +537 -0
- package/dist/vendor/zod/v4/core/versions.js +8 -0
- package/dist/vendor/zod/v4/locales/en.js +110 -0
- package/package.json +65 -4
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
import { isObject } from "../core/json.js";
|
|
2
|
+
import { unpaddedBase64, urlSafeBase64 } from "../core/crypto.js";
|
|
3
|
+
import { M } from "../core/m.js";
|
|
4
|
+
import { SUPPORTED_ROOM_VERSIONS, computeContentHash, computeReferenceHash, eventIdOf, finalisePdu, redact, roomVersionFeatures, splitStateKey, stateKey, stripState } from "../core/events.js";
|
|
5
|
+
import { MatrixError, errors } from "../core/errors.js";
|
|
6
|
+
import { parseId } from "../core/ids.js";
|
|
7
|
+
import { authStateFromAuthEvents, checkAuth, getState, membershipOf } from "../core/authrules.js";
|
|
8
|
+
import { globMatch } from "../core/glob.js";
|
|
9
|
+
import { resolveState } from "../core/stateres.js";
|
|
10
|
+
import { MissingStateError } from "../core/rooms.js";
|
|
11
|
+
import { FederationClient } from "./client.js";
|
|
12
|
+
import { KeyRing } from "./keyring.js";
|
|
13
|
+
import { TransactionSender } from "./sender.js";
|
|
14
|
+
//#region src/federation/index.ts
|
|
15
|
+
const domainOf = (id) => id.slice(id.indexOf(":") + 1);
|
|
16
|
+
/** Server ACL check (m.room.server_acl). */
|
|
17
|
+
function serverAllowedByAcl(state, serverName) {
|
|
18
|
+
const acl = getState(state, M.ROOM.SERVER_ACL)?.content;
|
|
19
|
+
if (!isObject(acl)) return true;
|
|
20
|
+
const host = serverName.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
|
|
21
|
+
if (acl.allow_ip_literals === false && (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":"))) return false;
|
|
22
|
+
const matches = (p) => typeof p === "string" && globMatch(p, host, { caseInsensitive: true });
|
|
23
|
+
if (Array.isArray(acl.deny) && acl.deny.some(matches)) return false;
|
|
24
|
+
if (!Array.isArray(acl.allow)) return false;
|
|
25
|
+
return acl.allow.some(matches);
|
|
26
|
+
}
|
|
27
|
+
/** Server-server API: verification, inbound processing and outbound requests. */
|
|
28
|
+
var FederationService = class {
|
|
29
|
+
hs;
|
|
30
|
+
enabled;
|
|
31
|
+
fasterJoins;
|
|
32
|
+
client;
|
|
33
|
+
keyring;
|
|
34
|
+
sender;
|
|
35
|
+
options;
|
|
36
|
+
seenTxns = /* @__PURE__ */ new Map();
|
|
37
|
+
deviceStreams = /* @__PURE__ */ new Map();
|
|
38
|
+
constructor(hs, options = {}) {
|
|
39
|
+
this.hs = hs;
|
|
40
|
+
this.options = options;
|
|
41
|
+
this.enabled = options.enabled === true;
|
|
42
|
+
this.fasterJoins = options.fasterJoins !== false;
|
|
43
|
+
this.client = new FederationClient(hs, { rejectUnauthorized: options.verifyTls !== false });
|
|
44
|
+
this.keyring = new KeyRing(hs);
|
|
45
|
+
this.sender = new TransactionSender(hs);
|
|
46
|
+
}
|
|
47
|
+
serverAllowed(serverName) {
|
|
48
|
+
if (this.options.blockedServers?.includes(serverName)) return false;
|
|
49
|
+
if (this.options.allowedServers && !this.options.allowedServers.includes(serverName)) return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
requireEnabled() {
|
|
53
|
+
if (!this.enabled) throw errors.forbidden("Federation is disabled");
|
|
54
|
+
}
|
|
55
|
+
/** Convert a stored event into its federation (wire) format. */
|
|
56
|
+
toFederationPdu(pdu, version) {
|
|
57
|
+
const { unsigned, ...rest } = pdu;
|
|
58
|
+
const out = { ...rest };
|
|
59
|
+
const f = roomVersionFeatures(version);
|
|
60
|
+
if (!f.legacyEventIds) delete out.event_id;
|
|
61
|
+
if (f.v12 && out.type === M.ROOM.CREATE) delete out.room_id;
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
computeEventId(raw, version) {
|
|
65
|
+
const f = roomVersionFeatures(version);
|
|
66
|
+
if (f.legacyEventIds) return raw.event_id;
|
|
67
|
+
const hash = computeReferenceHash(raw, version);
|
|
68
|
+
return `$${f.urlSafeEventIds ? urlSafeBase64(hash) : unpaddedBase64(hash)}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verify an incoming PDU: event ID, signatures and content hash. Events with a bad
|
|
72
|
+
* content hash are redacted (as the spec requires) rather than rejected.
|
|
73
|
+
*/
|
|
74
|
+
async verifyPdu(raw, version, roomId) {
|
|
75
|
+
if (!isObject(raw) || typeof raw.type !== "string" || typeof raw.sender !== "string" || !isObject(raw.content)) throw errors.badJson("Malformed PDU");
|
|
76
|
+
const f = roomVersionFeatures(version);
|
|
77
|
+
if (!f.v12 || raw.type !== M.ROOM.CREATE) {
|
|
78
|
+
if (raw.room_id !== roomId) throw errors.badJson("PDU room_id mismatch");
|
|
79
|
+
}
|
|
80
|
+
const eventId = this.computeEventId(raw, version);
|
|
81
|
+
if (typeof eventId !== "string") throw errors.badJson("Missing event ID");
|
|
82
|
+
const redacted = redact(raw, version);
|
|
83
|
+
const senderDomain = domainOf(raw.sender);
|
|
84
|
+
if (!await this.keyring.verifySignedBy(redacted, senderDomain, raw.origin_server_ts)) throw errors.forbidden(`Event ${eventId} is not signed by ${senderDomain}`);
|
|
85
|
+
if (f.legacyEventIds && domainOf(eventId) !== senderDomain && !await this.keyring.verifySignedBy(redacted, domainOf(eventId))) throw errors.forbidden(`Event ${eventId} is not signed by its origin`);
|
|
86
|
+
let joinAuthorisedSignatureValid;
|
|
87
|
+
const via = raw.content?.join_authorised_via_users_server;
|
|
88
|
+
if (f.restricted && raw.type === M.ROOM.MEMBER && raw.content?.membership === "join" && typeof via === "string" && via.includes(":")) joinAuthorisedSignatureValid = await this.keyring.verifySignedBy(redacted, domainOf(via));
|
|
89
|
+
let pdu = {
|
|
90
|
+
...raw,
|
|
91
|
+
event_id: eventId
|
|
92
|
+
};
|
|
93
|
+
if (raw.hashes?.sha256 !== computeContentHash(raw)) pdu = {
|
|
94
|
+
...redact(raw, version),
|
|
95
|
+
event_id: eventId
|
|
96
|
+
};
|
|
97
|
+
if (f.v12 && pdu.type === M.ROOM.CREATE) pdu.room_id = roomId;
|
|
98
|
+
return {
|
|
99
|
+
pdu,
|
|
100
|
+
joinAuthorisedSignatureValid
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Servers with joined members in a room (excluding ourselves). */
|
|
104
|
+
serversInRoom(state, applyAcl = true) {
|
|
105
|
+
const servers = /* @__PURE__ */ new Set();
|
|
106
|
+
for (const [key, ev] of state) {
|
|
107
|
+
const [type, userId] = splitStateKey(key);
|
|
108
|
+
if (type === M.ROOM.MEMBER && ev.content?.membership === "join") servers.add(domainOf(userId));
|
|
109
|
+
}
|
|
110
|
+
servers.delete(this.hs.serverName);
|
|
111
|
+
for (const s of servers) if (!this.serverAllowed(s) || applyAcl && !serverAllowedByAcl(state, s)) servers.delete(s);
|
|
112
|
+
return servers;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Servers to send a room's traffic to: joined servers per the state, plus, while the room's
|
|
116
|
+
* state is partial (MSC3902), the servers the resident server said were in the room.
|
|
117
|
+
*/
|
|
118
|
+
async roomDestinations(roomId, state, applyAcl = true) {
|
|
119
|
+
const destinations = this.serversInRoom(state, applyAcl);
|
|
120
|
+
for (const s of await this.hs.partialState.servers(roomId)) if (s !== this.hs.serverName && this.serverAllowed(s) && (!applyAcl || serverAllowedByAcl(state, s))) destinations.add(s);
|
|
121
|
+
return destinations;
|
|
122
|
+
}
|
|
123
|
+
/** Send a newly created local event to every server in the room. */
|
|
124
|
+
onLocalEvent(pdu, state) {
|
|
125
|
+
if (!this.enabled) return;
|
|
126
|
+
(async () => {
|
|
127
|
+
const destinations = await this.roomDestinations(pdu.room_id, state, pdu.type !== M.ROOM.SERVER_ACL);
|
|
128
|
+
if (pdu.type === M.ROOM.MEMBER && typeof pdu.state_key === "string" && !this.hs.isLocalUser(pdu.state_key)) destinations.add(domainOf(pdu.state_key));
|
|
129
|
+
if (!destinations.size) return;
|
|
130
|
+
const version = await this.hs.rooms.roomVersion(pdu.room_id) ?? "10";
|
|
131
|
+
await this.sender.sendPdu(destinations, this.toFederationPdu(pdu, version));
|
|
132
|
+
})().catch((err) => this.hs.logger.warn("Failed to queue PDU", err));
|
|
133
|
+
}
|
|
134
|
+
/** Send an EDU to every server in the given rooms. */
|
|
135
|
+
async sendEduToRooms(roomIds, eduType, content, durable = true) {
|
|
136
|
+
if (!this.enabled) return;
|
|
137
|
+
const destinations = /* @__PURE__ */ new Set();
|
|
138
|
+
for (const roomId of roomIds) {
|
|
139
|
+
if (!await this.hs.rooms.exists(roomId)) continue;
|
|
140
|
+
for (const s of await this.roomDestinations(roomId, await this.hs.rooms.currentState(roomId))) destinations.add(s);
|
|
141
|
+
}
|
|
142
|
+
if (destinations.size) await this.sender.sendEdu(destinations, eduType, content, durable);
|
|
143
|
+
}
|
|
144
|
+
/** Servers sharing a room with a local user. */
|
|
145
|
+
async serversSharingRooms(userId) {
|
|
146
|
+
const out = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const roomId of await this.hs.rooms.roomsForUser(userId)) for (const s of await this.roomDestinations(roomId, await this.hs.rooms.currentState(roomId))) out.add(s);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
async onTyping(roomId, userId, typing) {
|
|
151
|
+
if (this.hs.isLocalUser(userId)) await this.sendEduToRooms([roomId], M.TYPING, {
|
|
152
|
+
room_id: roomId,
|
|
153
|
+
user_id: userId,
|
|
154
|
+
typing
|
|
155
|
+
}, false);
|
|
156
|
+
}
|
|
157
|
+
async onReceipt(roomId, userId, receiptType, eventId, ts, threadId) {
|
|
158
|
+
if (!this.hs.isLocalUser(userId) || receiptType === M.READ.PRIVATE) return;
|
|
159
|
+
const data = { ts };
|
|
160
|
+
if (threadId) data.thread_id = threadId;
|
|
161
|
+
await this.sendEduToRooms([roomId], M.RECEIPT, { [roomId]: { [receiptType]: { [userId]: {
|
|
162
|
+
event_ids: [eventId],
|
|
163
|
+
data
|
|
164
|
+
} } } });
|
|
165
|
+
}
|
|
166
|
+
async onPresence(userId, presence, statusMsg) {
|
|
167
|
+
if (!this.enabled || !this.hs.isLocalUser(userId)) return;
|
|
168
|
+
const servers = await this.serversSharingRooms(userId);
|
|
169
|
+
if (!servers.size) return;
|
|
170
|
+
await this.sender.sendEdu(servers, M.PRESENCE, { push: [{
|
|
171
|
+
user_id: userId,
|
|
172
|
+
presence,
|
|
173
|
+
last_active_ago: 0,
|
|
174
|
+
currently_active: presence === "online",
|
|
175
|
+
...statusMsg ? { status_msg: statusMsg } : {}
|
|
176
|
+
}] }, false);
|
|
177
|
+
}
|
|
178
|
+
/** Tell servers sharing rooms with a local user that their device list changed. */
|
|
179
|
+
async onDeviceListChange(userId, deletedDeviceId, to) {
|
|
180
|
+
if (!this.enabled || !this.hs.isLocalUser(userId)) return;
|
|
181
|
+
const servers = to ?? await this.serversSharingRooms(userId);
|
|
182
|
+
if (!servers.size) return;
|
|
183
|
+
const devices = await this.hs.db.findMany("devices", { where: [{
|
|
184
|
+
field: "user_id",
|
|
185
|
+
value: userId
|
|
186
|
+
}] });
|
|
187
|
+
const updates = devices.map((d) => ({
|
|
188
|
+
deviceId: d.device_id,
|
|
189
|
+
deleted: false
|
|
190
|
+
}));
|
|
191
|
+
if (deletedDeviceId) updates.push({
|
|
192
|
+
deviceId: deletedDeviceId,
|
|
193
|
+
deleted: true
|
|
194
|
+
});
|
|
195
|
+
for (const u of updates) {
|
|
196
|
+
const prev = this.deviceStreams.get(userId) ?? (await this.hs.db.findOne("device_list_changes", [{
|
|
197
|
+
field: "user_id",
|
|
198
|
+
value: userId
|
|
199
|
+
}]))?.stream ?? 0;
|
|
200
|
+
const streamId = Math.max(await this.hs.streams.write(async (id) => id), prev + 1);
|
|
201
|
+
this.deviceStreams.set(userId, streamId);
|
|
202
|
+
const content = {
|
|
203
|
+
user_id: userId,
|
|
204
|
+
device_id: u.deviceId,
|
|
205
|
+
stream_id: streamId,
|
|
206
|
+
prev_id: prev ? [prev] : []
|
|
207
|
+
};
|
|
208
|
+
if (u.deleted) content.deleted = true;
|
|
209
|
+
else {
|
|
210
|
+
const keys = await this.hs.db.findOne("device_keys", [{
|
|
211
|
+
field: "user_id",
|
|
212
|
+
value: userId
|
|
213
|
+
}, {
|
|
214
|
+
field: "device_id",
|
|
215
|
+
value: u.deviceId
|
|
216
|
+
}]);
|
|
217
|
+
if (keys) content.keys = keys.keys;
|
|
218
|
+
const d = devices.find((x) => x.device_id === u.deviceId);
|
|
219
|
+
if (d?.display_name) content.device_display_name = d.display_name;
|
|
220
|
+
}
|
|
221
|
+
await this.sender.sendEdu(servers, M.DEVICE_LIST_UPDATE, content);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async onSigningKeyUpdate(userId) {
|
|
225
|
+
if (!this.enabled || !this.hs.isLocalUser(userId)) return;
|
|
226
|
+
const servers = await this.serversSharingRooms(userId);
|
|
227
|
+
if (!servers.size) return;
|
|
228
|
+
const rows = await this.hs.db.findMany("cross_signing_keys", { where: [{
|
|
229
|
+
field: "user_id",
|
|
230
|
+
value: userId
|
|
231
|
+
}] });
|
|
232
|
+
const content = { user_id: userId };
|
|
233
|
+
for (const r of rows) if (r.key_type !== "user_signing") content[`${r.key_type}_key`] = r.key;
|
|
234
|
+
await this.sender.sendEdu(servers, M.SIGNING_KEY_UPDATE, content);
|
|
235
|
+
}
|
|
236
|
+
/** Deliver to-device messages addressed to remote users. */
|
|
237
|
+
async sendToDevice(sender, type, messages, messageId) {
|
|
238
|
+
if (!this.enabled) return;
|
|
239
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const [userId, devices] of Object.entries(messages)) {
|
|
241
|
+
const server = domainOf(userId);
|
|
242
|
+
if (server === this.hs.serverName || !this.serverAllowed(server)) continue;
|
|
243
|
+
(byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
|
|
244
|
+
}
|
|
245
|
+
for (const [server, msgs] of byServer) await this.sender.sendEdu([server], M.DIRECT_TO_DEVICE, {
|
|
246
|
+
sender,
|
|
247
|
+
type,
|
|
248
|
+
message_id: `${messageId}-${server}`,
|
|
249
|
+
messages: msgs
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async receiveTransaction(origin, txnId, body) {
|
|
253
|
+
const key = `fedtxn:${origin}|${txnId}`;
|
|
254
|
+
this.sender.wake(origin);
|
|
255
|
+
const local = this.seenTxns.get(key);
|
|
256
|
+
if (local) return local;
|
|
257
|
+
const done = await this.hs.cluster.get(key);
|
|
258
|
+
if (done) return done;
|
|
259
|
+
const p = this.hs.cluster.lock(key, async () => {
|
|
260
|
+
const again = await this.hs.cluster.get(key);
|
|
261
|
+
if (again) return again;
|
|
262
|
+
const result = await this.processTransaction(origin, body);
|
|
263
|
+
await this.hs.cluster.set(key, result, 864e5);
|
|
264
|
+
return result;
|
|
265
|
+
});
|
|
266
|
+
this.seenTxns.set(key, p);
|
|
267
|
+
if (this.seenTxns.size > 1e3) this.seenTxns.delete(this.seenTxns.keys().next().value);
|
|
268
|
+
return p;
|
|
269
|
+
}
|
|
270
|
+
async processTransaction(origin, body) {
|
|
271
|
+
const pdus = Array.isArray(body.pdus) ? body.pdus.slice(0, 50) : [];
|
|
272
|
+
const edus = Array.isArray(body.edus) ? body.edus.slice(0, 100) : [];
|
|
273
|
+
const results = {};
|
|
274
|
+
for (const raw of pdus) {
|
|
275
|
+
const roomId = typeof raw?.room_id === "string" ? raw.room_id : null;
|
|
276
|
+
let eventId = "unknown";
|
|
277
|
+
try {
|
|
278
|
+
if (!roomId) throw errors.badJson("PDU without room_id");
|
|
279
|
+
const version = await this.hs.rooms.roomVersion(roomId);
|
|
280
|
+
if (!version) {
|
|
281
|
+
eventId = await this.handleOutOfRoomMembership(origin, roomId, raw);
|
|
282
|
+
results[eventId] = {};
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
eventId = this.computeEventId(raw, version);
|
|
286
|
+
await this.hs.rooms.withLock(roomId, () => this.handlePdu(origin, roomId, version, raw));
|
|
287
|
+
results[eventId] = {};
|
|
288
|
+
} catch (err) {
|
|
289
|
+
results[eventId] = { error: err instanceof Error ? err.message : String(err) };
|
|
290
|
+
this.hs.logger.debug("PDU from", origin, "failed", err);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const edu of edus) try {
|
|
294
|
+
await this.handleEdu(origin, edu);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
this.hs.logger.debug("EDU from", origin, "failed", err);
|
|
297
|
+
}
|
|
298
|
+
return { pdus: results };
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* A PDU for a room we are not in can still matter: a local user's invite being
|
|
302
|
+
* rescinded or their knock being accepted/rejected.
|
|
303
|
+
*/
|
|
304
|
+
async handleOutOfRoomMembership(origin, roomId, raw) {
|
|
305
|
+
if (raw?.type !== M.ROOM.MEMBER || typeof raw.state_key !== "string" || !this.hs.isLocalUser(raw.state_key)) throw errors.notFound("Unknown room");
|
|
306
|
+
const row = await this.hs.db.findOne("memberships", [{
|
|
307
|
+
field: "room_id",
|
|
308
|
+
value: roomId
|
|
309
|
+
}, {
|
|
310
|
+
field: "user_id",
|
|
311
|
+
value: raw.state_key
|
|
312
|
+
}]);
|
|
313
|
+
if (!row) throw errors.notFound("Unknown room");
|
|
314
|
+
const previous = await this.hs.rooms.getPdu(row.event_id);
|
|
315
|
+
const create = (previous?.unsigned?.invite_room_state ?? previous?.unsigned?.knock_room_state ?? []).find((e) => e?.type === M.ROOM.CREATE);
|
|
316
|
+
const version = typeof create?.content?.room_version === "string" ? create.content.room_version : "1";
|
|
317
|
+
const { pdu } = await this.verifyPdu(raw, version, roomId);
|
|
318
|
+
if (domainOf(pdu.sender) !== origin && domainOf(pdu.sender) !== domainOf(raw.state_key)) throw errors.forbidden("Unexpected sender");
|
|
319
|
+
const membership = pdu.content?.membership;
|
|
320
|
+
if (row.membership === "invite" && (membership === "leave" || membership === "ban") && previous && pdu.sender !== previous.sender && pdu.sender !== raw.state_key) return pdu.event_id;
|
|
321
|
+
if (membership === "invite" || membership === "join") pdu.unsigned = {
|
|
322
|
+
...pdu.unsigned,
|
|
323
|
+
invite_room_state: previous?.unsigned?.knock_room_state ?? previous?.unsigned?.invite_room_state ?? []
|
|
324
|
+
};
|
|
325
|
+
if (membership !== "join") await this.hs.rooms.recordRemoteMembership(pdu, version);
|
|
326
|
+
return pdu.event_id;
|
|
327
|
+
}
|
|
328
|
+
/** Process one incoming PDU (caller holds the room lock). */
|
|
329
|
+
async handlePdu(origin, roomId, version, raw, depth = 0) {
|
|
330
|
+
if (!serverAllowedByAcl(await this.hs.rooms.currentState(roomId), origin)) throw errors.forbidden("Server is denied by the room's ACL");
|
|
331
|
+
const { pdu, joinAuthorisedSignatureValid } = await this.verifyPdu(raw, version, roomId);
|
|
332
|
+
if (await this.hs.rooms.getEvent(pdu.event_id)) return;
|
|
333
|
+
for (const id of (pdu.auth_events ?? []).map(eventIdOf)) if ((await this.hs.rooms.getOutlier(id))?.rejected) {
|
|
334
|
+
await this.hs.db.upsert("outliers", {
|
|
335
|
+
event_id: pdu.event_id,
|
|
336
|
+
room_id: roomId,
|
|
337
|
+
json: pdu,
|
|
338
|
+
state_snapshot: null,
|
|
339
|
+
soft_failed: false,
|
|
340
|
+
rejected: `Auth event ${id} was rejected`
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
let stateBefore;
|
|
345
|
+
const prevIds = (pdu.prev_events ?? []).map(eventIdOf);
|
|
346
|
+
const unknownPrevs = async () => {
|
|
347
|
+
const out = [];
|
|
348
|
+
for (const id of prevIds) {
|
|
349
|
+
if (await this.hs.rooms.snapshotOf(id) !== null) continue;
|
|
350
|
+
if ((await this.hs.rooms.getOutlier(id))?.rejected) continue;
|
|
351
|
+
out.push(id);
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
};
|
|
355
|
+
let missing = await unknownPrevs();
|
|
356
|
+
if (missing.length && depth < 3) {
|
|
357
|
+
if (!await this.fetchMissingPrevEvents(origin, roomId, version, pdu)) return;
|
|
358
|
+
missing = await unknownPrevs();
|
|
359
|
+
}
|
|
360
|
+
if (missing.length && depth === 0) throw errors.forbidden("Your server isn't divulging details about prev_events referenced in this event.");
|
|
361
|
+
if (missing.length) stateBefore = await this.stateFromPrevs(origin, roomId, version, prevIds);
|
|
362
|
+
await this.fetchMissingAuthEvents(origin, roomId, version, pdu);
|
|
363
|
+
for (const id of (pdu.auth_events ?? []).map(eventIdOf)) {
|
|
364
|
+
const outlier = await this.hs.rooms.getOutlier(id);
|
|
365
|
+
if (outlier?.rejected || !await this.hs.rooms.knowsEvent(id)) {
|
|
366
|
+
await this.hs.db.upsert("outliers", {
|
|
367
|
+
event_id: pdu.event_id,
|
|
368
|
+
room_id: roomId,
|
|
369
|
+
json: pdu,
|
|
370
|
+
state_snapshot: null,
|
|
371
|
+
soft_failed: false,
|
|
372
|
+
rejected: outlier?.rejected ? `Auth event ${id} was rejected` : `Auth event ${id} is unavailable`
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const result = await this.hs.rooms.persistEvent(roomId, version, pdu, {
|
|
379
|
+
joinAuthorisedSignatureValid,
|
|
380
|
+
stateBefore,
|
|
381
|
+
gap: !!stateBefore
|
|
382
|
+
});
|
|
383
|
+
if (result.status === "rejected") this.hs.logger.debug("Rejected", pdu.event_id, result.reason);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (err instanceof MissingStateError && !stateBefore && depth > 0) {
|
|
386
|
+
const sb = await this.stateFromPrevs(origin, roomId, version, prevIds);
|
|
387
|
+
await this.hs.rooms.persistEvent(roomId, version, pdu, {
|
|
388
|
+
joinAuthorisedSignatureValid,
|
|
389
|
+
stateBefore: sb,
|
|
390
|
+
gap: true
|
|
391
|
+
});
|
|
392
|
+
} else throw err;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* State before an event whose predecessors we lack: for each unknown prev event,
|
|
397
|
+
* fetch the state at it (/state_ids) plus the event itself, then resolve all
|
|
398
|
+
* predecessors' states. Throws if a required event cannot be fetched.
|
|
399
|
+
*/
|
|
400
|
+
async stateFromPrevs(origin, roomId, version, prevIds, opts = {}) {
|
|
401
|
+
const sets = [];
|
|
402
|
+
for (const prev of prevIds) {
|
|
403
|
+
const snap = opts.remote ? null : await this.hs.rooms.snapshotOf(prev);
|
|
404
|
+
if (snap !== null) {
|
|
405
|
+
sets.push(await this.hs.rooms.snapshotIds(snap));
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if ((await this.hs.rooms.getOutlier(prev))?.rejected) continue;
|
|
409
|
+
const before = await this.fetchStateBefore(origin, roomId, version, prev);
|
|
410
|
+
if (!await this.hs.rooms.getPdu(prev)) await this.fetchEvent(origin, roomId, version, prev);
|
|
411
|
+
const prevPdu = await this.hs.rooms.getPdu(prev);
|
|
412
|
+
if (!prevPdu) throw errors.notFound(`Could not fetch prev event ${prev}`);
|
|
413
|
+
sets.push(prevPdu.state_key !== void 0 ? {
|
|
414
|
+
...before,
|
|
415
|
+
[stateKey(prevPdu.type, prevPdu.state_key)]: prev
|
|
416
|
+
} : before);
|
|
417
|
+
}
|
|
418
|
+
if (!sets.length) throw errors.forbidden("All predecessors were rejected");
|
|
419
|
+
if (sets.length === 1) return sets[0];
|
|
420
|
+
const create = roomVersionFeatures(version).v12 ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
|
|
421
|
+
return resolveState(sets, version, (ids) => this.hs.rooms.getPdus(ids), create);
|
|
422
|
+
}
|
|
423
|
+
async fetchMissingAuthEvents(origin, roomId, version, pdu) {
|
|
424
|
+
const authIds = (pdu.auth_events ?? []).map(eventIdOf);
|
|
425
|
+
const missing = [];
|
|
426
|
+
for (const id of authIds) if (!await this.hs.rooms.knowsEvent(id)) missing.push(id);
|
|
427
|
+
if (!missing.length) return;
|
|
428
|
+
try {
|
|
429
|
+
const res = await this.client.request(origin, "GET", `/_matrix/federation/v1/event_auth/${encodeURIComponent(roomId)}/${encodeURIComponent(pdu.event_id)}`);
|
|
430
|
+
await this.storeOutliers(roomId, version, Array.isArray(res.auth_chain) ? res.auth_chain : []);
|
|
431
|
+
} catch {
|
|
432
|
+
const seen = /* @__PURE__ */ new Set();
|
|
433
|
+
const raws = [];
|
|
434
|
+
let queue = missing;
|
|
435
|
+
while (queue.length && seen.size < 100) {
|
|
436
|
+
const next = [];
|
|
437
|
+
for (const id of queue) {
|
|
438
|
+
if (seen.has(id) || await this.hs.rooms.knowsEvent(id)) continue;
|
|
439
|
+
seen.add(id);
|
|
440
|
+
const raw = await this.fetchRawEvent(origin, id).catch(() => null);
|
|
441
|
+
if (!raw) continue;
|
|
442
|
+
raws.push(raw);
|
|
443
|
+
for (const a of (raw.auth_events ?? []).map(eventIdOf)) next.push(a);
|
|
444
|
+
}
|
|
445
|
+
queue = next;
|
|
446
|
+
}
|
|
447
|
+
await this.storeOutliers(roomId, version, raws);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Fill a gap with /get_missing_events. Returns false when the response contained invalid events. */
|
|
451
|
+
async fetchMissingPrevEvents(origin, roomId, version, pdu) {
|
|
452
|
+
const earliest = await this.hs.rooms.extremities(roomId);
|
|
453
|
+
let res;
|
|
454
|
+
try {
|
|
455
|
+
res = await this.client.request(origin, "POST", `/_matrix/federation/v1/get_missing_events/${encodeURIComponent(roomId)}`, { body: {
|
|
456
|
+
earliest_events: earliest,
|
|
457
|
+
latest_events: [pdu.event_id],
|
|
458
|
+
limit: 10,
|
|
459
|
+
min_depth: 0
|
|
460
|
+
} });
|
|
461
|
+
} catch {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
const events = (Array.isArray(res.events) ? res.events : []).filter(isObject);
|
|
465
|
+
for (const ev of events) try {
|
|
466
|
+
this.computeEventId(ev, version);
|
|
467
|
+
} catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
events.sort((a, b) => (a.depth ?? 0) - (b.depth ?? 0));
|
|
471
|
+
const failed = /* @__PURE__ */ new Set();
|
|
472
|
+
for (const ev of events) try {
|
|
473
|
+
await this.handlePdu(origin, roomId, version, ev, 3);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
failed.add(this.computeEventId(ev, version));
|
|
476
|
+
this.hs.logger.debug("Missing event failed", err);
|
|
477
|
+
}
|
|
478
|
+
return !(pdu.prev_events ?? []).map(eventIdOf).some((id) => failed.has(id));
|
|
479
|
+
}
|
|
480
|
+
/** Fetch one event from a remote server and store it as an outlier. */
|
|
481
|
+
async fetchEvent(origin, roomId, version, eventId) {
|
|
482
|
+
const raw = await this.fetchRawEvent(origin, eventId);
|
|
483
|
+
if (raw) await this.storeOutliers(roomId, version, [raw]);
|
|
484
|
+
}
|
|
485
|
+
async fetchRawEvent(origin, eventId) {
|
|
486
|
+
const r = await this.client.request(origin, "GET", `/_matrix/federation/v1/event/${encodeURIComponent(eventId)}`);
|
|
487
|
+
return Array.isArray(r.pdus) && isObject(r.pdus[0]) ? r.pdus[0] : null;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Fetch the state before an event from a remote server. Uses /state_ids and
|
|
491
|
+
* fetches only unknown events (falling back to /state), storing them as outliers.
|
|
492
|
+
*/
|
|
493
|
+
async fetchStateBefore(origin, roomId, version, eventId) {
|
|
494
|
+
let stateIds;
|
|
495
|
+
let res = null;
|
|
496
|
+
const timeoutMs = 3e5;
|
|
497
|
+
try {
|
|
498
|
+
res = await this.client.request(origin, "GET", `/_matrix/federation/v1/state_ids/${encodeURIComponent(roomId)}`, {
|
|
499
|
+
query: { event_id: eventId },
|
|
500
|
+
timeoutMs
|
|
501
|
+
});
|
|
502
|
+
} catch (err) {
|
|
503
|
+
this.hs.logger.debug("/state_ids failed, falling back to /state", origin, err);
|
|
504
|
+
}
|
|
505
|
+
if (res && (!Array.isArray(res.pdu_ids) || !res.pdu_ids.length)) throw errors.notFound("Remote server returned no state");
|
|
506
|
+
if (res) {
|
|
507
|
+
stateIds = res.pdu_ids;
|
|
508
|
+
const wanted = [...Array.isArray(res.auth_chain_ids) ? res.auth_chain_ids : [], ...stateIds];
|
|
509
|
+
const missing = [];
|
|
510
|
+
for (const id of wanted) if (typeof id === "string" && !await this.hs.rooms.knowsEvent(id)) missing.push(id);
|
|
511
|
+
const raws = [];
|
|
512
|
+
for (let i = 0; i < missing.length; i += 20) {
|
|
513
|
+
const batch = await Promise.all(missing.slice(i, i + 20).map((id) => this.fetchRawEvent(origin, id).catch(() => null)));
|
|
514
|
+
for (const raw of batch) if (raw) raws.push(raw);
|
|
515
|
+
}
|
|
516
|
+
await this.storeOutliers(roomId, version, raws);
|
|
517
|
+
for (const id of stateIds) if (!await this.hs.rooms.knowsEvent(id)) throw errors.notFound(`Could not fetch state event ${id}`);
|
|
518
|
+
} else {
|
|
519
|
+
const res = await this.client.request(origin, "GET", `/_matrix/federation/v1/state/${encodeURIComponent(roomId)}`, {
|
|
520
|
+
query: { event_id: eventId },
|
|
521
|
+
timeoutMs
|
|
522
|
+
});
|
|
523
|
+
const pdus = Array.isArray(res.pdus) ? res.pdus : [];
|
|
524
|
+
if (!pdus.length) throw errors.notFound("Remote server returned no state");
|
|
525
|
+
await this.storeOutliers(roomId, version, [...Array.isArray(res.auth_chain) ? res.auth_chain : [], ...pdus]);
|
|
526
|
+
stateIds = pdus.filter(isObject).map((raw) => this.computeEventId(raw, version));
|
|
527
|
+
}
|
|
528
|
+
const ids = {};
|
|
529
|
+
const pdus = await this.hs.rooms.getPdus(stateIds);
|
|
530
|
+
for (const id of stateIds) {
|
|
531
|
+
const pdu = pdus.get(id);
|
|
532
|
+
if (pdu && pdu.state_key !== void 0) ids[stateKey(pdu.type, pdu.state_key)] = id;
|
|
533
|
+
}
|
|
534
|
+
return ids;
|
|
535
|
+
}
|
|
536
|
+
/** Verify and store events that are not (yet) part of the timeline. */
|
|
537
|
+
async storeOutliers(roomId, version, raws) {
|
|
538
|
+
const verified = [];
|
|
539
|
+
for (const raw of raws) {
|
|
540
|
+
if (!isObject(raw)) continue;
|
|
541
|
+
try {
|
|
542
|
+
const { pdu } = await this.verifyPdu(raw, version, roomId);
|
|
543
|
+
if (!await this.hs.rooms.knowsEvent(pdu.event_id)) verified.push(pdu);
|
|
544
|
+
} catch (err) {
|
|
545
|
+
this.hs.logger.debug("Dropping invalid outlier", err);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
verified.sort((a, b) => a.depth - b.depth);
|
|
549
|
+
const seen = /* @__PURE__ */ new Set();
|
|
550
|
+
for (const pdu of verified) {
|
|
551
|
+
if (seen.has(pdu.event_id)) continue;
|
|
552
|
+
seen.add(pdu.event_id);
|
|
553
|
+
const rejected = await this.checkOutlierAuth(roomId, version, pdu);
|
|
554
|
+
await this.hs.db.upsert("outliers", {
|
|
555
|
+
event_id: pdu.event_id,
|
|
556
|
+
room_id: roomId,
|
|
557
|
+
json: pdu,
|
|
558
|
+
state_snapshot: null,
|
|
559
|
+
rejected,
|
|
560
|
+
soft_failed: false
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/** Check an outlier against its own auth events. Returns a rejection reason, or null. */
|
|
565
|
+
async checkOutlierAuth(roomId, version, pdu) {
|
|
566
|
+
const authIds = (pdu.auth_events ?? []).map(eventIdOf);
|
|
567
|
+
const authPdus = [];
|
|
568
|
+
for (const id of authIds) {
|
|
569
|
+
if ((await this.hs.rooms.getOutlier(id))?.rejected) return `Auth event ${id} was rejected`;
|
|
570
|
+
const p = await this.hs.rooms.getPdu(id);
|
|
571
|
+
if (!p) return `Auth event ${id} is unavailable`;
|
|
572
|
+
authPdus.push(p);
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
const create = roomVersionFeatures(version).v12 && pdu.type !== M.ROOM.CREATE ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
|
|
576
|
+
checkAuth(pdu, authStateFromAuthEvents(pdu, authPdus, version, create), version);
|
|
577
|
+
return null;
|
|
578
|
+
} catch (err) {
|
|
579
|
+
return err instanceof Error ? err.message : String(err);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Whether a remote user's ephemeral activity (typing, receipts) in a room is accepted: they are
|
|
584
|
+
* joined, or the room's state is partial (MSC3902) and their server is one of the room's servers.
|
|
585
|
+
*/
|
|
586
|
+
async mayActInRoom(roomId, userId, origin) {
|
|
587
|
+
const membership = await this.hs.rooms.membership(roomId, userId);
|
|
588
|
+
if (membership === "join") return true;
|
|
589
|
+
if (membership !== "leave" || !await this.hs.partialState.isPartial(roomId)) return false;
|
|
590
|
+
return (await this.hs.partialState.servers(roomId)).includes(origin);
|
|
591
|
+
}
|
|
592
|
+
async handleEdu(origin, edu) {
|
|
593
|
+
const content = edu?.content;
|
|
594
|
+
if (!isObject(content)) return;
|
|
595
|
+
switch (edu.edu_type) {
|
|
596
|
+
case M.TYPING: {
|
|
597
|
+
const { room_id, user_id, typing } = content;
|
|
598
|
+
if (typeof user_id !== "string" || domainOf(user_id) !== origin || typeof room_id !== "string") return;
|
|
599
|
+
if (!await this.hs.rooms.exists(room_id) || !await this.mayActInRoom(room_id, user_id, origin)) return;
|
|
600
|
+
if (!serverAllowedByAcl(await this.hs.rooms.currentState(room_id), origin)) return;
|
|
601
|
+
await this.hs.typing.setTyping(room_id, user_id, !!typing, typeof content.timeout === "number" ? content.timeout : 3e4);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
case M.RECEIPT:
|
|
605
|
+
for (const [roomId, byType] of Object.entries(content)) {
|
|
606
|
+
if (!isObject(byType) || !await this.hs.rooms.exists(roomId)) continue;
|
|
607
|
+
if (!serverAllowedByAcl(await this.hs.rooms.currentState(roomId), origin)) continue;
|
|
608
|
+
for (const [receiptType, byUser] of Object.entries(byType)) {
|
|
609
|
+
if (receiptType !== M.READ._ && receiptType !== M.READ.PRIVATE || !isObject(byUser)) continue;
|
|
610
|
+
for (const [userId, r] of Object.entries(byUser)) {
|
|
611
|
+
if (domainOf(userId) !== origin || !isObject(r) || !Array.isArray(r.event_ids)) continue;
|
|
612
|
+
if (!await this.mayActInRoom(roomId, userId, origin)) continue;
|
|
613
|
+
const eventId = r.event_ids[r.event_ids.length - 1];
|
|
614
|
+
if (typeof eventId !== "string") continue;
|
|
615
|
+
await this.hs.streams.write((stream) => this.hs.db.upsert("receipts", {
|
|
616
|
+
room_id: roomId,
|
|
617
|
+
user_id: userId,
|
|
618
|
+
receipt_type: receiptType,
|
|
619
|
+
thread_id: typeof r.data?.thread_id === "string" ? r.data.thread_id : "",
|
|
620
|
+
event_id: eventId,
|
|
621
|
+
ts: Number(r.data?.ts) || Date.now(),
|
|
622
|
+
stream
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
this.hs.notifier.notify(await this.hs.rooms.joinedMembers(roomId));
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
case M.PRESENCE:
|
|
630
|
+
for (const p of Array.isArray(content.push) ? content.push : []) {
|
|
631
|
+
if (!isObject(p) || typeof p.user_id !== "string" || domainOf(p.user_id) !== origin) continue;
|
|
632
|
+
if (![
|
|
633
|
+
"online",
|
|
634
|
+
"offline",
|
|
635
|
+
"unavailable"
|
|
636
|
+
].includes(p.presence)) continue;
|
|
637
|
+
await this.hs.streams.write((stream) => this.hs.db.upsert("presence", {
|
|
638
|
+
user_id: p.user_id,
|
|
639
|
+
presence: p.presence,
|
|
640
|
+
status_msg: typeof p.status_msg === "string" ? p.status_msg : null,
|
|
641
|
+
last_active_ts: Date.now() - (Number(p.last_active_ago) || 0),
|
|
642
|
+
stream
|
|
643
|
+
}));
|
|
644
|
+
this.hs.notifier.notify(await this.hs.rooms.usersSharingRooms(p.user_id));
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
case M.DEVICE_LIST_UPDATE: {
|
|
648
|
+
const userId = content.user_id;
|
|
649
|
+
if (typeof userId !== "string" || domainOf(userId) !== origin || typeof content.device_id !== "string") return;
|
|
650
|
+
const known = await this.hs.db.findOne("remote_device_lists", [{
|
|
651
|
+
field: "user_id",
|
|
652
|
+
value: userId
|
|
653
|
+
}]);
|
|
654
|
+
if (!await this.hs.keys.isTracked(userId)) await this.hs.partialState.notePendingDeviceUpdate(userId, origin);
|
|
655
|
+
await this.hs.keys.markRemoteStale(userId);
|
|
656
|
+
if (known || content.deleted || isObject(content.keys)) await this.hs.keys.markDeviceListChanged(userId);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
case M.SIGNING_KEY_UPDATE: {
|
|
660
|
+
const userId = content.user_id;
|
|
661
|
+
if (typeof userId !== "string" || domainOf(userId) !== origin) return;
|
|
662
|
+
for (const type of ["master", "self_signing"]) {
|
|
663
|
+
const key = content[`${type}_key`];
|
|
664
|
+
if (isObject(key)) await this.hs.streams.write((stream) => this.hs.db.upsert("cross_signing_keys", {
|
|
665
|
+
user_id: userId,
|
|
666
|
+
key_type: type,
|
|
667
|
+
key,
|
|
668
|
+
stream
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
await this.hs.keys.markRemoteStale(userId);
|
|
672
|
+
await this.hs.keys.markDeviceListChanged(userId);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
case M.DIRECT_TO_DEVICE: {
|
|
676
|
+
const { sender, type, message_id, messages } = content;
|
|
677
|
+
if (typeof sender !== "string" || domainOf(sender) !== origin || typeof type !== "string" || !isObject(messages)) return;
|
|
678
|
+
if (!await this.hs.cluster.setIfAbsent(`fedtodevice:${origin}|${message_id}`, true, 864e5)) return;
|
|
679
|
+
const recipients = /* @__PURE__ */ new Set();
|
|
680
|
+
for (const [userId, devices] of Object.entries(messages)) {
|
|
681
|
+
if (!this.hs.isLocalUser(userId) || !isObject(devices)) continue;
|
|
682
|
+
let targets = Object.keys(devices);
|
|
683
|
+
if (targets.includes("*")) targets = (await this.hs.db.findMany("devices", { where: [{
|
|
684
|
+
field: "user_id",
|
|
685
|
+
value: userId
|
|
686
|
+
}] })).map((d) => d.device_id);
|
|
687
|
+
for (const deviceId of targets) await this.hs.streams.write((stream) => this.hs.db.create("to_device", {
|
|
688
|
+
stream,
|
|
689
|
+
user_id: userId,
|
|
690
|
+
device_id: deviceId,
|
|
691
|
+
sender,
|
|
692
|
+
type,
|
|
693
|
+
content: devices[deviceId] ?? devices["*"]
|
|
694
|
+
}));
|
|
695
|
+
recipients.add(userId);
|
|
696
|
+
}
|
|
697
|
+
this.hs.notifier.notify(recipients);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/** Build a full event from a make_join/make_leave/make_knock template and sign it. */
|
|
703
|
+
finaliseTemplate(template, version, extraContent) {
|
|
704
|
+
const { event_id, hashes, signatures, unsigned, ...rest } = template;
|
|
705
|
+
const pdu = {
|
|
706
|
+
...rest,
|
|
707
|
+
content: {
|
|
708
|
+
...rest.content,
|
|
709
|
+
...extraContent
|
|
710
|
+
},
|
|
711
|
+
origin_server_ts: Date.now()
|
|
712
|
+
};
|
|
713
|
+
if (roomVersionFeatures(version).legacyEventIds) pdu.origin = this.hs.serverName;
|
|
714
|
+
return finalisePdu(pdu, version, this.hs.serverName, this.hs.signingKey);
|
|
715
|
+
}
|
|
716
|
+
/** Join a room hosted on other servers. Returns the room ID. */
|
|
717
|
+
async joinRemoteRoom(userId, roomId, servers, content) {
|
|
718
|
+
this.requireEnabled();
|
|
719
|
+
let lastError = errors.notFound("No server could be reached to join the room");
|
|
720
|
+
for (const server of [...new Set(servers)].filter((s) => s !== this.hs.serverName && this.serverAllowed(s))) try {
|
|
721
|
+
const tmpl = await this.client.request(server, "GET", `/_matrix/federation/v1/make_join/${encodeURIComponent(roomId)}/${encodeURIComponent(userId)}`, { query: { ver: SUPPORTED_ROOM_VERSIONS } });
|
|
722
|
+
const version = typeof tmpl.room_version === "string" ? tmpl.room_version : "1";
|
|
723
|
+
if (!SUPPORTED_ROOM_VERSIONS.includes(version)) throw errors.unsupportedRoomVersion();
|
|
724
|
+
if (!isObject(tmpl.event) || tmpl.event.content?.membership !== "join" || tmpl.event.state_key !== userId || tmpl.event.sender !== userId) throw errors.badJson("Invalid make_join template");
|
|
725
|
+
const joinPdu = this.finaliseTemplate(tmpl.event, version, content);
|
|
726
|
+
const res = await this.client.request(server, "PUT", `/_matrix/federation/v2/send_join/${encodeURIComponent(roomId)}/${encodeURIComponent(joinPdu.event_id)}`, {
|
|
727
|
+
body: this.toFederationPdu(joinPdu, version),
|
|
728
|
+
query: this.fasterJoins ? { omit_members: "true" } : void 0,
|
|
729
|
+
timeoutMs: 12e4
|
|
730
|
+
});
|
|
731
|
+
if (isObject(res.event?.signatures)) joinPdu.signatures = {
|
|
732
|
+
...joinPdu.signatures,
|
|
733
|
+
...res.event.signatures
|
|
734
|
+
};
|
|
735
|
+
await this.completeJoin(roomId, version, joinPdu, res, server);
|
|
736
|
+
return roomId;
|
|
737
|
+
} catch (err) {
|
|
738
|
+
lastError = err;
|
|
739
|
+
this.hs.logger.debug("Joining", roomId, "via", server, "failed", err);
|
|
740
|
+
}
|
|
741
|
+
throw lastError;
|
|
742
|
+
}
|
|
743
|
+
async completeJoin(roomId, version, joinPdu, res, origin) {
|
|
744
|
+
const statePdus = Array.isArray(res.state) ? res.state : [];
|
|
745
|
+
const authChain = Array.isArray(res.auth_chain) ? res.auth_chain : [];
|
|
746
|
+
await this.storeOutliers(roomId, version, [...authChain, ...statePdus]);
|
|
747
|
+
const stateIds = {};
|
|
748
|
+
let create;
|
|
749
|
+
for (const raw of statePdus) {
|
|
750
|
+
if (!isObject(raw) || typeof raw.state_key !== "string") continue;
|
|
751
|
+
const id = this.computeEventId(raw, version);
|
|
752
|
+
const pdu = await this.hs.rooms.getPdu(id);
|
|
753
|
+
if (!pdu) continue;
|
|
754
|
+
stateIds[stateKey(pdu.type, pdu.state_key)] = id;
|
|
755
|
+
if (pdu.type === M.ROOM.CREATE) create = pdu;
|
|
756
|
+
}
|
|
757
|
+
if (!create) throw errors.badJson("send_join response lacks the create event");
|
|
758
|
+
if (!await this.hs.rooms.exists(roomId)) await this.hs.db.upsert("rooms", {
|
|
759
|
+
room_id: roomId,
|
|
760
|
+
room_version: version,
|
|
761
|
+
creator: create.sender,
|
|
762
|
+
is_public: false,
|
|
763
|
+
created_ts: Date.now()
|
|
764
|
+
});
|
|
765
|
+
if (await this.hs.rooms.serverIsJoined(roomId)) {
|
|
766
|
+
await this.hs.rooms.withLock(roomId, () => this.hs.rooms.persistEvent(roomId, version, joinPdu, {
|
|
767
|
+
local: true,
|
|
768
|
+
noFederate: true
|
|
769
|
+
}));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
let gap = false;
|
|
773
|
+
for (const prev of (joinPdu.prev_events ?? []).map(eventIdOf)) if (!await this.hs.rooms.getEvent(prev)) gap = true;
|
|
774
|
+
await this.hs.rooms.withLock(roomId, () => this.hs.rooms.persistEvent(roomId, version, joinPdu, {
|
|
775
|
+
local: true,
|
|
776
|
+
stateBefore: stateIds,
|
|
777
|
+
createEvent: roomVersionFeatures(version).v12 ? create : void 0,
|
|
778
|
+
noFederate: true,
|
|
779
|
+
gap
|
|
780
|
+
}));
|
|
781
|
+
if (res.members_omitted === true) {
|
|
782
|
+
const servers = Array.isArray(res.servers_in_room) ? res.servers_in_room.filter((s) => typeof s === "string") : [];
|
|
783
|
+
await this.hs.partialState.markPartial(roomId, joinPdu.event_id, origin, servers);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/** Leave (or reject an invite to) a room hosted elsewhere. */
|
|
787
|
+
async leaveRemoteRoom(userId, roomId, servers, reason) {
|
|
788
|
+
this.requireEnabled();
|
|
789
|
+
let lastError = errors.notFound("No server could be reached to leave the room");
|
|
790
|
+
for (const server of [...new Set(servers)].filter((s) => s !== this.hs.serverName)) try {
|
|
791
|
+
const tmpl = await this.client.request(server, "GET", `/_matrix/federation/v1/make_leave/${encodeURIComponent(roomId)}/${encodeURIComponent(userId)}`);
|
|
792
|
+
const version = typeof tmpl.room_version === "string" ? tmpl.room_version : "1";
|
|
793
|
+
const pdu = this.finaliseTemplate(tmpl.event, version, reason ? { reason } : {});
|
|
794
|
+
await this.client.request(server, "PUT", `/_matrix/federation/v2/send_leave/${encodeURIComponent(roomId)}/${encodeURIComponent(pdu.event_id)}`, { body: this.toFederationPdu(pdu, version) });
|
|
795
|
+
return pdu;
|
|
796
|
+
} catch (err) {
|
|
797
|
+
lastError = err;
|
|
798
|
+
}
|
|
799
|
+
throw lastError;
|
|
800
|
+
}
|
|
801
|
+
async knockRemoteRoom(userId, roomId, servers, reason) {
|
|
802
|
+
this.requireEnabled();
|
|
803
|
+
let lastError = errors.notFound("No server could be reached to knock on the room");
|
|
804
|
+
for (const server of [...new Set(servers)].filter((s) => s !== this.hs.serverName)) try {
|
|
805
|
+
const tmpl = await this.client.request(server, "GET", `/_matrix/federation/v1/make_knock/${encodeURIComponent(roomId)}/${encodeURIComponent(userId)}`, { query: { ver: SUPPORTED_ROOM_VERSIONS } });
|
|
806
|
+
const version = typeof tmpl.room_version === "string" ? tmpl.room_version : "1";
|
|
807
|
+
const pdu = this.finaliseTemplate(tmpl.event, version, reason ? { reason } : {});
|
|
808
|
+
const res = await this.client.request(server, "PUT", `/_matrix/federation/v1/send_knock/${encodeURIComponent(roomId)}/${encodeURIComponent(pdu.event_id)}`, { body: this.toFederationPdu(pdu, version) });
|
|
809
|
+
return {
|
|
810
|
+
pdu,
|
|
811
|
+
knockState: Array.isArray(res.knock_room_state) ? res.knock_room_state : []
|
|
812
|
+
};
|
|
813
|
+
} catch (err) {
|
|
814
|
+
lastError = err;
|
|
815
|
+
}
|
|
816
|
+
throw lastError;
|
|
817
|
+
}
|
|
818
|
+
/** Ask the invitee's server to sign an invite; returns the countersigned event. */
|
|
819
|
+
async sendInvite(pdu, version, inviteRoomState) {
|
|
820
|
+
this.requireEnabled();
|
|
821
|
+
const server = domainOf(pdu.state_key);
|
|
822
|
+
if (!this.serverAllowed(server)) throw errors.forbidden("Federation with this server is not allowed");
|
|
823
|
+
const res = await this.client.request(server, "PUT", `/_matrix/federation/v2/invite/${encodeURIComponent(pdu.room_id)}/${encodeURIComponent(pdu.event_id)}`, { body: {
|
|
824
|
+
room_version: version,
|
|
825
|
+
event: this.toFederationPdu(pdu, version),
|
|
826
|
+
invite_room_state: inviteRoomState
|
|
827
|
+
} });
|
|
828
|
+
if (!isObject(res.event)) throw errors.unknown("Invalid invite response");
|
|
829
|
+
const signed = {
|
|
830
|
+
...pdu,
|
|
831
|
+
signatures: {
|
|
832
|
+
...pdu.signatures,
|
|
833
|
+
...res.event.signatures
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
if (!await this.keyring.verifySignedBy(redact(this.toFederationPdu(signed, version), version), server)) throw errors.forbidden("Invite was not signed by the invitee's server");
|
|
837
|
+
return signed;
|
|
838
|
+
}
|
|
839
|
+
async queryProfile(userId, field) {
|
|
840
|
+
this.requireEnabled();
|
|
841
|
+
return this.client.request(domainOf(userId), "GET", "/_matrix/federation/v1/query/profile", { query: {
|
|
842
|
+
user_id: userId,
|
|
843
|
+
field
|
|
844
|
+
} });
|
|
845
|
+
}
|
|
846
|
+
async queryAlias(alias) {
|
|
847
|
+
this.requireEnabled();
|
|
848
|
+
return this.client.request(domainOf(alias), "GET", "/_matrix/federation/v1/query/directory", { query: { room_alias: alias } });
|
|
849
|
+
}
|
|
850
|
+
async queryKeys(deviceKeys, timeoutMs = 1e4) {
|
|
851
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
852
|
+
for (const [userId, devices] of Object.entries(deviceKeys)) {
|
|
853
|
+
const server = domainOf(userId);
|
|
854
|
+
if (server === this.hs.serverName) continue;
|
|
855
|
+
(byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
|
|
856
|
+
}
|
|
857
|
+
const results = [];
|
|
858
|
+
const failures = {};
|
|
859
|
+
await Promise.all([...byServer].map(async ([server, query]) => {
|
|
860
|
+
try {
|
|
861
|
+
if (!this.enabled || !this.serverAllowed(server)) throw new Error("Federation disabled");
|
|
862
|
+
results.push(await this.client.request(server, "POST", "/_matrix/federation/v1/user/keys/query", {
|
|
863
|
+
body: { device_keys: query },
|
|
864
|
+
timeoutMs
|
|
865
|
+
}));
|
|
866
|
+
} catch (err) {
|
|
867
|
+
failures[server] = {
|
|
868
|
+
status: 503,
|
|
869
|
+
message: err instanceof Error ? err.message : String(err)
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}));
|
|
873
|
+
return {
|
|
874
|
+
results,
|
|
875
|
+
failures
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
async claimKeys(oneTimeKeys, timeoutMs = 1e4) {
|
|
879
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
880
|
+
for (const [userId, devices] of Object.entries(oneTimeKeys)) {
|
|
881
|
+
const server = domainOf(userId);
|
|
882
|
+
if (server === this.hs.serverName) continue;
|
|
883
|
+
(byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
|
|
884
|
+
}
|
|
885
|
+
const results = {};
|
|
886
|
+
const failures = {};
|
|
887
|
+
await Promise.all([...byServer].map(async ([server, query]) => {
|
|
888
|
+
try {
|
|
889
|
+
if (!this.enabled) throw new Error("Federation disabled");
|
|
890
|
+
const res = await this.client.request(server, "POST", "/_matrix/federation/v1/user/keys/claim", {
|
|
891
|
+
body: { one_time_keys: query },
|
|
892
|
+
timeoutMs
|
|
893
|
+
});
|
|
894
|
+
Object.assign(results, res.one_time_keys ?? {});
|
|
895
|
+
} catch (err) {
|
|
896
|
+
failures[server] = {
|
|
897
|
+
status: 503,
|
|
898
|
+
message: err instanceof Error ? err.message : String(err)
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}));
|
|
902
|
+
return {
|
|
903
|
+
results,
|
|
904
|
+
failures
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
async queryPublicRooms(server, body) {
|
|
908
|
+
this.requireEnabled();
|
|
909
|
+
return this.client.request(server, "POST", "/_matrix/federation/v1/publicRooms", { body });
|
|
910
|
+
}
|
|
911
|
+
async queryHierarchy(server, roomId, suggestedOnly) {
|
|
912
|
+
this.requireEnabled();
|
|
913
|
+
return this.client.request(server, "GET", `/_matrix/federation/v1/hierarchy/${encodeURIComponent(roomId)}`, { query: { suggested_only: String(suggestedOnly) } });
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Fetch history preceding the oldest events we have. Returns the number of new
|
|
917
|
+
* events stored. Caller should not hold the room lock.
|
|
918
|
+
*/
|
|
919
|
+
async backfill(roomId, fromEventIds, limit = 50) {
|
|
920
|
+
if (!this.enabled || !fromEventIds.length) return 0;
|
|
921
|
+
const version = await this.hs.rooms.roomVersion(roomId);
|
|
922
|
+
if (!version) return 0;
|
|
923
|
+
const servers = [...this.serversInRoom(await this.hs.rooms.currentState(roomId))];
|
|
924
|
+
for (const server of servers) try {
|
|
925
|
+
const res = await this.client.request(server, "GET", `/_matrix/federation/v1/backfill/${encodeURIComponent(roomId)}`, { query: {
|
|
926
|
+
v: fromEventIds,
|
|
927
|
+
limit: String(limit)
|
|
928
|
+
} });
|
|
929
|
+
const verified = [];
|
|
930
|
+
for (const raw of Array.isArray(res.pdus) ? res.pdus : []) try {
|
|
931
|
+
const { pdu } = await this.verifyPdu(raw, version, roomId);
|
|
932
|
+
if (!await this.hs.rooms.getEvent(pdu.event_id)) verified.push(pdu);
|
|
933
|
+
} catch {}
|
|
934
|
+
if (!verified.length) return 0;
|
|
935
|
+
verified.sort((a, b) => b.depth - a.depth);
|
|
936
|
+
const positions = /* @__PURE__ */ new Map();
|
|
937
|
+
for (const p of verified) positions.set(p.event_id, await this.hs.streams.backfillId());
|
|
938
|
+
verified.reverse();
|
|
939
|
+
let stored = 0;
|
|
940
|
+
await this.hs.rooms.withLock(roomId, async () => {
|
|
941
|
+
for (const pdu of verified) try {
|
|
942
|
+
await this.fetchMissingAuthEvents(server, roomId, version, pdu);
|
|
943
|
+
let stateBefore;
|
|
944
|
+
const prevIds = (pdu.prev_events ?? []).map(eventIdOf);
|
|
945
|
+
const unknown = [];
|
|
946
|
+
for (const id of prevIds) if (await this.hs.rooms.snapshotOf(id) === null) unknown.push(id);
|
|
947
|
+
if (unknown.length) stateBefore = await this.fetchStateBefore(server, roomId, version, pdu.event_id);
|
|
948
|
+
if ((await this.hs.rooms.persistEvent(roomId, version, pdu, {
|
|
949
|
+
backfill: true,
|
|
950
|
+
stateBefore,
|
|
951
|
+
stream: positions.get(pdu.event_id)
|
|
952
|
+
})).status === "persisted") stored++;
|
|
953
|
+
} catch (err) {
|
|
954
|
+
this.hs.logger.debug("Backfilled event failed", pdu.event_id, err);
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
return stored;
|
|
958
|
+
} catch (err) {
|
|
959
|
+
this.hs.logger.debug("Backfill from", server, "failed", err);
|
|
960
|
+
}
|
|
961
|
+
return 0;
|
|
962
|
+
}
|
|
963
|
+
/** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
|
|
964
|
+
async fetchMedia(server, mediaId) {
|
|
965
|
+
this.requireEnabled();
|
|
966
|
+
const res = await this.client.requestRaw(server, "GET", `/_matrix/federation/v1/media/download/${encodeURIComponent(mediaId)}`, { timeoutMs: 6e4 });
|
|
967
|
+
if (res.status === 200) {
|
|
968
|
+
const ct = String(res.headers["content-type"] ?? "");
|
|
969
|
+
const m = /boundary=("?)([^";]+)\1/.exec(ct);
|
|
970
|
+
if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2], this);
|
|
971
|
+
}
|
|
972
|
+
const legacy = await this.client.requestRaw(server, "GET", `/_matrix/media/v3/download/${encodeURIComponent(server)}/${encodeURIComponent(mediaId)}`, {
|
|
973
|
+
query: { allow_remote: "false" },
|
|
974
|
+
signed: false,
|
|
975
|
+
timeoutMs: 6e4
|
|
976
|
+
});
|
|
977
|
+
if (legacy.status !== 200) throw errors.notFound("Remote media not found");
|
|
978
|
+
return {
|
|
979
|
+
data: legacy.body,
|
|
980
|
+
contentType: legacy.headers["content-type"] ?? null,
|
|
981
|
+
filename: filenameFromDisposition(legacy.headers["content-disposition"])
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/** Remote members who should receive the invitee's stripped state. */
|
|
985
|
+
strippedStateFor(state) {
|
|
986
|
+
return this.hs.rooms.strippedState(state);
|
|
987
|
+
}
|
|
988
|
+
/** Servers that may be able to help us join `roomId`. */
|
|
989
|
+
candidateServers(roomId, extra = []) {
|
|
990
|
+
const fromId = parseId(roomId, "!")?.server;
|
|
991
|
+
return [.../* @__PURE__ */ new Set([...extra, ...fromId ? [fromId] : []])];
|
|
992
|
+
}
|
|
993
|
+
static memberServers(state) {
|
|
994
|
+
const out = /* @__PURE__ */ new Set();
|
|
995
|
+
for (const [key, ev] of state) {
|
|
996
|
+
const [type, userId] = splitStateKey(key);
|
|
997
|
+
if (type === M.ROOM.MEMBER && ev.content?.membership === "join") out.add(domainOf(userId));
|
|
998
|
+
}
|
|
999
|
+
return [...out];
|
|
1000
|
+
}
|
|
1001
|
+
isMembershipVisibleTo(state, server) {
|
|
1002
|
+
for (const [key] of state) {
|
|
1003
|
+
const [type, userId] = splitStateKey(key);
|
|
1004
|
+
if (type === M.ROOM.MEMBER && domainOf(userId) === server && membershipOf(state, userId) === "join") return true;
|
|
1005
|
+
}
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
stripped(pdu) {
|
|
1009
|
+
return stripState(pdu);
|
|
1010
|
+
}
|
|
1011
|
+
error(status, errcode, message) {
|
|
1012
|
+
return new MatrixError(status, errcode, message);
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
function filenameFromDisposition(header) {
|
|
1016
|
+
if (!header) return null;
|
|
1017
|
+
const star = /filename\*=utf-8''([^;]+)/i.exec(header);
|
|
1018
|
+
if (star) return decodeURIComponent(star[1]);
|
|
1019
|
+
const plain = /filename="?([^";]+)"?/i.exec(header);
|
|
1020
|
+
return plain ? plain[1] : null;
|
|
1021
|
+
}
|
|
1022
|
+
/** Parse a federation media multipart/mixed response: a JSON part then the file (or a redirect). */
|
|
1023
|
+
async function parseMultipartMedia(body, boundary, fed) {
|
|
1024
|
+
const delimiter = Buffer.from(`--${boundary}`);
|
|
1025
|
+
const parts = [];
|
|
1026
|
+
let idx = body.indexOf(delimiter);
|
|
1027
|
+
while (idx !== -1) {
|
|
1028
|
+
const next = body.indexOf(delimiter, idx + delimiter.length);
|
|
1029
|
+
if (next === -1) break;
|
|
1030
|
+
parts.push(body.subarray(idx + delimiter.length, next));
|
|
1031
|
+
idx = next;
|
|
1032
|
+
}
|
|
1033
|
+
const media = parts[1];
|
|
1034
|
+
if (!media) throw errors.notFound("Malformed media response");
|
|
1035
|
+
const headerEnd = media.indexOf("\r\n\r\n");
|
|
1036
|
+
const headerText = media.subarray(0, headerEnd).toString("utf8");
|
|
1037
|
+
const data = media.subarray(headerEnd + 4, media.length - 2);
|
|
1038
|
+
const headers = {};
|
|
1039
|
+
for (const line of headerText.split("\r\n")) {
|
|
1040
|
+
const i = line.indexOf(":");
|
|
1041
|
+
if (i > 0) headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
|
|
1042
|
+
}
|
|
1043
|
+
if (headers.location) {
|
|
1044
|
+
const res = await fetch(headers.location, { signal: AbortSignal.timeout(6e4) });
|
|
1045
|
+
if (!res.ok) throw errors.notFound("Remote media redirect failed");
|
|
1046
|
+
return {
|
|
1047
|
+
data: new Uint8Array(await res.arrayBuffer()),
|
|
1048
|
+
contentType: res.headers.get("content-type"),
|
|
1049
|
+
filename: filenameFromDisposition(res.headers.get("content-disposition") ?? void 0)
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
data: new Uint8Array(data),
|
|
1054
|
+
contentType: headers["content-type"] ?? null,
|
|
1055
|
+
filename: filenameFromDisposition(headers["content-disposition"])
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
//#endregion
|
|
1059
|
+
export { FederationService, domainOf, serverAllowedByAcl };
|