matrix-nodejs-homeserver 0.2.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -11
- package/dist/adapters/cluster.d.ts +1 -26
- package/dist/adapters/cluster.js +50 -103
- package/dist/api/account.js +407 -354
- package/dist/api/admin.js +82 -61
- package/dist/api/appservice.js +37 -25
- package/dist/api/backup.js +73 -62
- package/dist/api/federation.js +172 -138
- package/dist/api/filters.js +4 -3
- package/dist/api/legacy.js +91 -78
- package/dist/api/media.js +71 -55
- package/dist/api/misc.js +46 -37
- package/dist/api/preview.js +37 -73
- package/dist/api/profile.js +108 -97
- package/dist/api/push.js +114 -99
- package/dist/api/relationships.js +64 -58
- package/dist/api/rooms.js +703 -580
- package/dist/api/rtc.d.ts +0 -1
- package/dist/api/rtc.js +48 -34
- package/dist/api/search.js +133 -127
- package/dist/api/slidingsync.js +41 -31
- package/dist/api/spaces.js +82 -77
- package/dist/api/sync.js +325 -135
- package/dist/api/threadsubs.js +72 -61
- package/dist/api/threepid.js +99 -66
- package/dist/api/uia.js +17 -16
- package/dist/api/user.js +221 -178
- package/dist/core/admin.d.ts +0 -1
- package/dist/core/admin.js +6 -5
- package/dist/core/adminroom.js +9 -8
- package/dist/core/appservices.d.ts +2 -1
- package/dist/core/appservices.js +5 -4
- package/dist/core/audit.d.ts +0 -1
- package/dist/core/authrules.js +26 -25
- package/dist/core/cluster.d.ts +12 -5
- package/dist/core/cluster.js +46 -29
- package/dist/core/delayed.js +2 -1
- package/dist/core/events.js +11 -10
- package/dist/core/html.js +13 -0
- package/dist/core/identity.js +1 -1
- package/dist/core/invitefilter.js +2 -1
- package/dist/core/json.js +10 -1
- package/dist/core/keys.d.ts +10 -1
- package/dist/core/keys.js +62 -42
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/outbound.d.ts +16 -0
- package/dist/core/outbound.js +172 -0
- package/dist/core/partialstate.js +2 -1
- package/dist/core/policy.js +4 -3
- package/dist/core/push.d.ts +0 -1
- package/dist/core/push.js +41 -28
- package/dist/core/pushrules.js +36 -35
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/rooms.d.ts +10 -1
- package/dist/core/rooms.js +101 -74
- package/dist/core/semaphore.js +23 -0
- package/dist/core/sso.d.ts +15 -1
- package/dist/core/sso.js +38 -2
- package/dist/core/stateres.js +7 -6
- package/dist/core/threepid.js +1 -9
- package/dist/federation/client.js +9 -1
- package/dist/federation/index.d.ts +2 -2
- package/dist/federation/index.js +43 -35
- package/dist/homeserver.d.ts +23 -4
- package/dist/homeserver.js +12 -3
- package/dist/http/handler.js +2 -23
- package/dist/http/input.js +2 -2
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +9 -26
- package/dist/http/router.js +10 -30
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/storage/batch.js +11 -0
- package/dist/storage/memory.js +1 -0
- package/dist/storage/schema.js +1 -1
- package/dist/storage/sql.js +20 -1
- package/dist/storage/types.d.ts +16 -0
- package/dist/vendor/zod/LICENSE +21 -0
- package/dist/vendor/zod/v4/classic/errors.js +54 -0
- package/dist/vendor/zod/v4/classic/parse.js +17 -0
- package/dist/vendor/zod/v4/classic/schemas.js +865 -0
- package/dist/vendor/zod/v4/core/api.js +512 -0
- package/dist/vendor/zod/v4/core/checks.js +300 -0
- package/dist/vendor/zod/v4/core/core.js +109 -0
- package/dist/vendor/zod/v4/core/doc.js +35 -0
- package/dist/vendor/zod/v4/core/errors.js +118 -0
- package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
- package/dist/vendor/zod/v4/core/memoizer.js +255 -0
- package/dist/vendor/zod/v4/core/parse.js +206 -0
- package/dist/vendor/zod/v4/core/regexes.js +73 -0
- package/dist/vendor/zod/v4/core/registries.js +48 -0
- package/dist/vendor/zod/v4/core/schemas.js +1378 -0
- package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
- package/dist/vendor/zod/v4/core/util.js +537 -0
- package/dist/vendor/zod/v4/core/versions.js +8 -0
- package/dist/vendor/zod/v4/locales/en.js +110 -0
- package/package.json +8 -6
- package/dist/core/pushrules.d.ts +0 -1
package/dist/api/user.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { canonicalJson, isObject } from "../core/json.js";
|
|
2
|
+
import { M } from "../core/m.js";
|
|
2
3
|
import { errors } from "../core/errors.js";
|
|
3
|
-
import {
|
|
4
|
+
import { array, record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
|
|
4
5
|
import { body, fail } from "../http/input.js";
|
|
5
|
-
import {
|
|
6
|
+
import { procedure } from "../http/procedure.js";
|
|
7
|
+
import { requireUia } from "./uia.js";
|
|
6
8
|
//#region src/api/user.ts
|
|
7
|
-
const deleteDevicesBody = body({ devices:
|
|
9
|
+
const deleteDevicesBody = body({ devices: array(unknown(), fail("M_BAD_JSON", "devices must be a list")) });
|
|
8
10
|
const keysQueryBody = body({
|
|
9
|
-
device_keys:
|
|
10
|
-
timeout:
|
|
11
|
+
device_keys: record(string(), array(string(), fail("M_BAD_JSON", "device_keys values must be lists of device IDs")), fail("M_BAD_JSON", "device_keys must be an object")),
|
|
12
|
+
timeout: unknown().optional()
|
|
11
13
|
});
|
|
12
|
-
const keysClaimBody = body({ one_time_keys:
|
|
13
|
-
const SERVER_MANAGED_TYPES = [
|
|
14
|
+
const keysClaimBody = body({ one_time_keys: record(string(), unknown(), fail("M_BAD_JSON", "one_time_keys must be an object")) });
|
|
15
|
+
const SERVER_MANAGED_TYPES = [M.FULLY_READ, M.PUSH_RULES];
|
|
14
16
|
async function setAccountData(hs, userId, roomId, type, content) {
|
|
15
17
|
await hs.streams.write((stream) => hs.db.upsert("account_data", {
|
|
16
18
|
user_id: userId,
|
|
@@ -48,39 +50,39 @@ async function checkRoom(ctx) {
|
|
|
48
50
|
if (!ctx.params.roomId.startsWith("!")) throw errors.invalidParam("Invalid room ID");
|
|
49
51
|
}
|
|
50
52
|
function registerUserRoutes(r) {
|
|
51
|
-
r.client("PUT", "/user/:userId/account_data/:type", async (ctx) => {
|
|
53
|
+
r.client("PUT", "/user/:userId/account_data/:type", procedure({ handler: async (ctx) => {
|
|
52
54
|
const s = await checkUser(ctx);
|
|
53
55
|
if (SERVER_MANAGED_TYPES.includes(ctx.params.type)) throw errors.badJson("This type is managed by the server");
|
|
54
56
|
await setAccountData(ctx.hs, s.userId, "", ctx.params.type, await ctx.json());
|
|
55
57
|
return {};
|
|
56
|
-
});
|
|
57
|
-
r.client("GET", "/user/:userId/account_data/:type", async (ctx) => {
|
|
58
|
+
} }));
|
|
59
|
+
r.client("GET", "/user/:userId/account_data/:type", procedure({ handler: async (ctx) => {
|
|
58
60
|
const s = await checkUser(ctx);
|
|
59
61
|
return getAccountData(ctx.hs, s.userId, "", ctx.params.type);
|
|
60
|
-
});
|
|
61
|
-
r.client("PUT", "/user/:userId/rooms/:roomId/account_data/:type", async (ctx) => {
|
|
62
|
+
} }));
|
|
63
|
+
r.client("PUT", "/user/:userId/rooms/:roomId/account_data/:type", procedure({ handler: async (ctx) => {
|
|
62
64
|
const s = await checkUser(ctx);
|
|
63
65
|
await checkRoom(ctx);
|
|
64
66
|
if (SERVER_MANAGED_TYPES.includes(ctx.params.type)) throw errors.badJson("This type is managed by the server");
|
|
65
67
|
await setAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type, await ctx.json());
|
|
66
68
|
return {};
|
|
67
|
-
});
|
|
68
|
-
r.client("GET", "/user/:userId/rooms/:roomId/account_data/:type", async (ctx) => {
|
|
69
|
+
} }));
|
|
70
|
+
r.client("GET", "/user/:userId/rooms/:roomId/account_data/:type", procedure({ handler: async (ctx) => {
|
|
69
71
|
const s = await checkUser(ctx);
|
|
70
72
|
await checkRoom(ctx);
|
|
71
73
|
return getAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type);
|
|
72
|
-
});
|
|
74
|
+
} }));
|
|
73
75
|
const del = "/_matrix/client/unstable/org.matrix.msc3391";
|
|
74
|
-
r.delete(`${del}/user/:userId/account_data/:type`, async (ctx) => {
|
|
76
|
+
r.delete(`${del}/user/:userId/account_data/:type`, procedure({ handler: async (ctx) => {
|
|
75
77
|
const s = await checkUser(ctx);
|
|
76
78
|
await setAccountData(ctx.hs, s.userId, "", ctx.params.type, {});
|
|
77
79
|
return {};
|
|
78
|
-
});
|
|
79
|
-
r.delete(`${del}/user/:userId/rooms/:roomId/account_data/:type`, async (ctx) => {
|
|
80
|
+
} }));
|
|
81
|
+
r.delete(`${del}/user/:userId/rooms/:roomId/account_data/:type`, procedure({ handler: async (ctx) => {
|
|
80
82
|
const s = await checkUser(ctx);
|
|
81
83
|
await setAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type, {});
|
|
82
84
|
return {};
|
|
83
|
-
});
|
|
85
|
+
} }));
|
|
84
86
|
const readTags = async (hs, userId, roomId) => {
|
|
85
87
|
const row = await hs.db.findOne("account_data", [
|
|
86
88
|
{
|
|
@@ -93,194 +95,235 @@ function registerUserRoutes(r) {
|
|
|
93
95
|
},
|
|
94
96
|
{
|
|
95
97
|
field: "type",
|
|
96
|
-
value:
|
|
98
|
+
value: M.TAG
|
|
97
99
|
}
|
|
98
100
|
]);
|
|
99
101
|
return isObject(row?.content?.tags) ? row.content.tags : {};
|
|
100
102
|
};
|
|
101
|
-
r.client("GET", "/user/:userId/rooms/:roomId/tags", async (ctx) => {
|
|
103
|
+
r.client("GET", "/user/:userId/rooms/:roomId/tags", procedure({ handler: async (ctx) => {
|
|
102
104
|
const s = await checkUser(ctx);
|
|
103
105
|
return { tags: await readTags(ctx.hs, s.userId, ctx.params.roomId) };
|
|
104
|
-
});
|
|
105
|
-
r.client("PUT", "/user/:userId/rooms/:roomId/tags/:tag", async (ctx) => {
|
|
106
|
+
} }));
|
|
107
|
+
r.client("PUT", "/user/:userId/rooms/:roomId/tags/:tag", procedure({ handler: async (ctx) => {
|
|
106
108
|
const s = await checkUser(ctx);
|
|
107
109
|
const body = await ctx.json();
|
|
108
110
|
if (body.order !== void 0 && typeof body.order !== "number") throw errors.badJson("order must be a number");
|
|
109
111
|
const tags = await readTags(ctx.hs, s.userId, ctx.params.roomId);
|
|
110
112
|
tags[ctx.params.tag] = body;
|
|
111
|
-
await setAccountData(ctx.hs, s.userId, ctx.params.roomId,
|
|
113
|
+
await setAccountData(ctx.hs, s.userId, ctx.params.roomId, M.TAG, { tags });
|
|
112
114
|
return {};
|
|
113
|
-
});
|
|
114
|
-
r.client("DELETE", "/user/:userId/rooms/:roomId/tags/:tag", async (ctx) => {
|
|
115
|
+
} }));
|
|
116
|
+
r.client("DELETE", "/user/:userId/rooms/:roomId/tags/:tag", procedure({ handler: async (ctx) => {
|
|
115
117
|
const s = await checkUser(ctx);
|
|
116
118
|
const tags = await readTags(ctx.hs, s.userId, ctx.params.roomId);
|
|
117
119
|
delete tags[ctx.params.tag];
|
|
118
|
-
await setAccountData(ctx.hs, s.userId, ctx.params.roomId,
|
|
120
|
+
await setAccountData(ctx.hs, s.userId, ctx.params.roomId, M.TAG, { tags });
|
|
119
121
|
return {};
|
|
120
|
-
});
|
|
122
|
+
} }));
|
|
121
123
|
const formatDevice = (d) => ({
|
|
122
124
|
device_id: d.device_id,
|
|
123
125
|
display_name: d.display_name ?? void 0,
|
|
124
126
|
last_seen_ip: d.last_seen_ip ?? void 0,
|
|
125
127
|
last_seen_ts: d.last_seen_ts ?? void 0
|
|
126
128
|
});
|
|
127
|
-
r.client("GET", "/devices",
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const s = await ctx.auth();
|
|
136
|
-
const row = await ctx.hs.db.findOne("devices", [{
|
|
137
|
-
field: "user_id",
|
|
138
|
-
value: s.userId
|
|
139
|
-
}, {
|
|
140
|
-
field: "device_id",
|
|
141
|
-
value: ctx.params.deviceId
|
|
142
|
-
}]);
|
|
143
|
-
if (!row) throw errors.notFound("Device not found");
|
|
144
|
-
return formatDevice(row);
|
|
145
|
-
});
|
|
146
|
-
r.client("PUT", "/devices/:deviceId", async (ctx) => {
|
|
147
|
-
const s = await ctx.auth();
|
|
148
|
-
const body = await ctx.json();
|
|
149
|
-
const where = [{
|
|
150
|
-
field: "user_id",
|
|
151
|
-
value: s.userId
|
|
152
|
-
}, {
|
|
153
|
-
field: "device_id",
|
|
154
|
-
value: ctx.params.deviceId
|
|
155
|
-
}];
|
|
156
|
-
if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
|
|
157
|
-
if (body.display_name !== void 0) {
|
|
158
|
-
if (typeof body.display_name !== "string") throw errors.badJson("display_name must be a string");
|
|
159
|
-
await ctx.hs.db.update("devices", where, { display_name: body.display_name });
|
|
160
|
-
await ctx.hs.keys.deviceChanged(s.userId);
|
|
129
|
+
r.client("GET", "/devices", procedure({
|
|
130
|
+
auth: "user",
|
|
131
|
+
handler: async (ctx) => {
|
|
132
|
+
const s = ctx.session;
|
|
133
|
+
return { devices: (await ctx.hs.db.findMany("devices", { where: [{
|
|
134
|
+
field: "user_id",
|
|
135
|
+
value: s.userId
|
|
136
|
+
}] })).map(formatDevice) };
|
|
161
137
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
field: "user_id",
|
|
169
|
-
value: s.userId
|
|
170
|
-
}, {
|
|
171
|
-
field: "device_id",
|
|
172
|
-
value: ctx.params.deviceId
|
|
173
|
-
}];
|
|
174
|
-
if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
|
|
175
|
-
await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
|
|
176
|
-
await ctx.hs.auth.deleteDevice(s.userId, ctx.params.deviceId);
|
|
177
|
-
return {};
|
|
178
|
-
});
|
|
179
|
-
r.client("POST", "/delete_devices", { input: deleteDevicesBody }, async (ctx) => {
|
|
180
|
-
const s = await ctx.auth();
|
|
181
|
-
const body = ctx.input;
|
|
182
|
-
await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
|
|
183
|
-
for (const d of body.devices) if (typeof d === "string") await ctx.hs.auth.deleteDevice(s.userId, d);
|
|
184
|
-
return {};
|
|
185
|
-
});
|
|
186
|
-
r.client("POST", "/keys/upload", { input: body({}) }, async (ctx) => {
|
|
187
|
-
const s = await ctx.auth();
|
|
188
|
-
return { one_time_key_counts: await ctx.hs.keys.upload(s.userId, s.deviceId, ctx.input) };
|
|
189
|
-
});
|
|
190
|
-
r.client("POST", "/keys/query", { input: keysQueryBody }, async (ctx) => {
|
|
191
|
-
const s = await ctx.auth();
|
|
192
|
-
await ctx.rateLimit("keyQuery");
|
|
193
|
-
const body = ctx.input;
|
|
194
|
-
return ctx.hs.keys.query(s.userId, body.device_keys, Math.min(Number(body.timeout ?? 1e4) || 1e4, 3e4));
|
|
195
|
-
});
|
|
196
|
-
r.client("POST", "/keys/claim", { input: keysClaimBody }, async (ctx) => {
|
|
197
|
-
await ctx.auth();
|
|
198
|
-
const body = ctx.input;
|
|
199
|
-
const local = {};
|
|
200
|
-
const remote = {};
|
|
201
|
-
for (const [userId, devices] of Object.entries(body.one_time_keys)) (ctx.hs.isLocalUser(userId) ? local : remote)[userId] = devices;
|
|
202
|
-
const claimed = await ctx.hs.keys.claim(local);
|
|
203
|
-
const fed = Object.keys(remote).length ? await ctx.hs.federation.claimKeys(remote) : {
|
|
204
|
-
results: {},
|
|
205
|
-
failures: {}
|
|
206
|
-
};
|
|
207
|
-
return {
|
|
208
|
-
one_time_keys: {
|
|
209
|
-
...claimed,
|
|
210
|
-
...fed.results
|
|
211
|
-
},
|
|
212
|
-
failures: fed.failures
|
|
213
|
-
};
|
|
214
|
-
});
|
|
215
|
-
r.client("GET", "/keys/changes", async (ctx) => {
|
|
216
|
-
const s = await ctx.auth();
|
|
217
|
-
const parse = (v) => {
|
|
218
|
-
const m = /^s?(\d+)/.exec(v ?? "");
|
|
219
|
-
if (!m) throw errors.invalidParam("Invalid token");
|
|
220
|
-
return Number(m[1]);
|
|
221
|
-
};
|
|
222
|
-
return ctx.hs.keys.changesSince(s.userId, parse(ctx.query.get("from")), parse(ctx.query.get("to")));
|
|
223
|
-
});
|
|
224
|
-
r.client("POST", "/keys/device_signing/upload", { input: body({}) }, async (ctx) => {
|
|
225
|
-
const s = await ctx.auth();
|
|
226
|
-
const body = ctx.input;
|
|
227
|
-
if (await ctx.hs.db.findOne("cross_signing_keys", [{
|
|
228
|
-
field: "user_id",
|
|
229
|
-
value: s.userId
|
|
230
|
-
}, {
|
|
231
|
-
field: "key_type",
|
|
232
|
-
value: "master"
|
|
233
|
-
}])) {
|
|
234
|
-
const stored = new Map((await ctx.hs.db.findMany("cross_signing_keys", { where: [{
|
|
138
|
+
}));
|
|
139
|
+
r.client("GET", "/devices/:deviceId", procedure({
|
|
140
|
+
auth: "user",
|
|
141
|
+
handler: async (ctx) => {
|
|
142
|
+
const s = ctx.session;
|
|
143
|
+
const row = await ctx.hs.db.findOne("devices", [{
|
|
235
144
|
field: "user_id",
|
|
236
145
|
value: s.userId
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
|
|
146
|
+
}, {
|
|
147
|
+
field: "device_id",
|
|
148
|
+
value: ctx.params.deviceId
|
|
149
|
+
}]);
|
|
150
|
+
if (!row) throw errors.notFound("Device not found");
|
|
151
|
+
return formatDevice(row);
|
|
244
152
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
r.client("PUT", "/sendToDevice/:eventType/:txnId", async (ctx) => {
|
|
253
|
-
const s = await ctx.auth();
|
|
254
|
-
const hs = ctx.hs;
|
|
255
|
-
const body = await ctx.json();
|
|
256
|
-
if (!isObject(body.messages)) throw errors.badJson("messages must be an object");
|
|
257
|
-
const txnKey = `${s.userId}|${s.deviceId}|${ctx.params.txnId}`;
|
|
258
|
-
if (!await hs.cluster.setIfAbsent(`todevice-txn:${txnKey}`, true, 864e5)) return {};
|
|
259
|
-
const recipients = /* @__PURE__ */ new Set();
|
|
260
|
-
for (const [userId, devices] of Object.entries(body.messages)) {
|
|
261
|
-
if (!isObject(devices) || !hs.isLocalUser(userId)) continue;
|
|
262
|
-
let targets = Object.keys(devices);
|
|
263
|
-
if (targets.includes("*")) targets = (await hs.db.findMany("devices", { where: [{
|
|
153
|
+
}));
|
|
154
|
+
r.client("PUT", "/devices/:deviceId", procedure({
|
|
155
|
+
auth: "user",
|
|
156
|
+
handler: async (ctx) => {
|
|
157
|
+
const s = ctx.session;
|
|
158
|
+
const body = await ctx.json();
|
|
159
|
+
const where = [{
|
|
264
160
|
field: "user_id",
|
|
265
|
-
value: userId
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
content
|
|
276
|
-
}));
|
|
161
|
+
value: s.userId
|
|
162
|
+
}, {
|
|
163
|
+
field: "device_id",
|
|
164
|
+
value: ctx.params.deviceId
|
|
165
|
+
}];
|
|
166
|
+
if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
|
|
167
|
+
if (body.display_name !== void 0) {
|
|
168
|
+
if (typeof body.display_name !== "string") throw errors.badJson("display_name must be a string");
|
|
169
|
+
await ctx.hs.db.update("devices", where, { display_name: body.display_name });
|
|
170
|
+
await ctx.hs.keys.deviceChanged(s.userId);
|
|
277
171
|
}
|
|
278
|
-
|
|
172
|
+
return {};
|
|
279
173
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
174
|
+
}));
|
|
175
|
+
r.client("DELETE", "/devices/:deviceId", procedure({
|
|
176
|
+
auth: "user",
|
|
177
|
+
handler: async (ctx) => {
|
|
178
|
+
const s = ctx.session;
|
|
179
|
+
const body = await ctx.json();
|
|
180
|
+
const where = [{
|
|
181
|
+
field: "user_id",
|
|
182
|
+
value: s.userId
|
|
183
|
+
}, {
|
|
184
|
+
field: "device_id",
|
|
185
|
+
value: ctx.params.deviceId
|
|
186
|
+
}];
|
|
187
|
+
if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
|
|
188
|
+
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
|
|
189
|
+
await ctx.hs.auth.deleteDevice(s.userId, ctx.params.deviceId);
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
}));
|
|
193
|
+
r.client("POST", "/delete_devices", procedure({
|
|
194
|
+
auth: "user",
|
|
195
|
+
input: deleteDevicesBody,
|
|
196
|
+
handler: async (ctx) => {
|
|
197
|
+
const s = ctx.session;
|
|
198
|
+
const body = ctx.input;
|
|
199
|
+
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
|
|
200
|
+
for (const d of body.devices) if (typeof d === "string") await ctx.hs.auth.deleteDevice(s.userId, d);
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
}));
|
|
204
|
+
r.client("POST", "/keys/upload", procedure({
|
|
205
|
+
auth: "user",
|
|
206
|
+
input: body({}),
|
|
207
|
+
handler: async (ctx) => {
|
|
208
|
+
const s = ctx.session;
|
|
209
|
+
return { one_time_key_counts: await ctx.hs.keys.upload(s.userId, s.deviceId, ctx.input) };
|
|
210
|
+
}
|
|
211
|
+
}));
|
|
212
|
+
r.client("POST", "/keys/query", procedure({
|
|
213
|
+
auth: "user",
|
|
214
|
+
rateLimit: "keyQuery",
|
|
215
|
+
input: keysQueryBody,
|
|
216
|
+
handler: async (ctx) => {
|
|
217
|
+
const s = ctx.session;
|
|
218
|
+
const body = ctx.input;
|
|
219
|
+
return ctx.hs.keys.query(s.userId, body.device_keys, Math.min(Number(body.timeout ?? 1e4) || 1e4, 3e4));
|
|
220
|
+
}
|
|
221
|
+
}));
|
|
222
|
+
r.client("POST", "/keys/claim", procedure({
|
|
223
|
+
auth: "user",
|
|
224
|
+
input: keysClaimBody,
|
|
225
|
+
handler: async (ctx) => {
|
|
226
|
+
const body = ctx.input;
|
|
227
|
+
const local = {};
|
|
228
|
+
const remote = {};
|
|
229
|
+
for (const [userId, devices] of Object.entries(body.one_time_keys)) (ctx.hs.isLocalUser(userId) ? local : remote)[userId] = devices;
|
|
230
|
+
const claimed = await ctx.hs.keys.claim(local);
|
|
231
|
+
const fed = Object.keys(remote).length ? await ctx.hs.federation.claimKeys(remote) : {
|
|
232
|
+
results: {},
|
|
233
|
+
failures: {}
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
one_time_keys: {
|
|
237
|
+
...claimed,
|
|
238
|
+
...fed.results
|
|
239
|
+
},
|
|
240
|
+
failures: fed.failures
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}));
|
|
244
|
+
r.client("GET", "/keys/changes", procedure({
|
|
245
|
+
auth: "user",
|
|
246
|
+
handler: async (ctx) => {
|
|
247
|
+
const s = ctx.session;
|
|
248
|
+
const parse = (v) => {
|
|
249
|
+
const m = /^s?(\d+)/.exec(v ?? "");
|
|
250
|
+
if (!m) throw errors.invalidParam("Invalid token");
|
|
251
|
+
return Number(m[1]);
|
|
252
|
+
};
|
|
253
|
+
return ctx.hs.keys.changesSince(s.userId, parse(ctx.query.get("from")), parse(ctx.query.get("to")));
|
|
254
|
+
}
|
|
255
|
+
}));
|
|
256
|
+
r.client("POST", "/keys/device_signing/upload", procedure({
|
|
257
|
+
auth: "user",
|
|
258
|
+
input: body({}),
|
|
259
|
+
handler: async (ctx) => {
|
|
260
|
+
const s = ctx.session;
|
|
261
|
+
const body = ctx.input;
|
|
262
|
+
if (await ctx.hs.db.findOne("cross_signing_keys", [{
|
|
263
|
+
field: "user_id",
|
|
264
|
+
value: s.userId
|
|
265
|
+
}, {
|
|
266
|
+
field: "key_type",
|
|
267
|
+
value: "master"
|
|
268
|
+
}])) {
|
|
269
|
+
const stored = new Map((await ctx.hs.db.findMany("cross_signing_keys", { where: [{
|
|
270
|
+
field: "user_id",
|
|
271
|
+
value: s.userId
|
|
272
|
+
}] })).map((row) => [row.key_type, canonicalJson(row.key)]));
|
|
273
|
+
if ([
|
|
274
|
+
"master",
|
|
275
|
+
"self_signing",
|
|
276
|
+
"user_signing"
|
|
277
|
+
].filter((type) => body[`${type}_key`] !== void 0).every((type) => stored.get(type) === canonicalJson(body[`${type}_key`]))) return {};
|
|
278
|
+
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
|
|
279
|
+
}
|
|
280
|
+
await ctx.hs.keys.uploadCrossSigning(s.userId, body);
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
284
|
+
r.client("POST", "/keys/signatures/upload", procedure({
|
|
285
|
+
auth: "user",
|
|
286
|
+
input: body({}),
|
|
287
|
+
handler: async (ctx) => {
|
|
288
|
+
const s = ctx.session;
|
|
289
|
+
return ctx.hs.keys.uploadSignatures(s.userId, ctx.input);
|
|
290
|
+
}
|
|
291
|
+
}));
|
|
292
|
+
r.client("PUT", "/sendToDevice/:eventType/:txnId", procedure({
|
|
293
|
+
auth: "user",
|
|
294
|
+
handler: async (ctx) => {
|
|
295
|
+
const s = ctx.session;
|
|
296
|
+
const hs = ctx.hs;
|
|
297
|
+
const body = await ctx.json();
|
|
298
|
+
if (!isObject(body.messages)) throw errors.badJson("messages must be an object");
|
|
299
|
+
const txnKey = `${s.userId}|${s.deviceId}|${ctx.params.txnId}`;
|
|
300
|
+
if (!await hs.cluster.setIfAbsent(`todevice-txn:${txnKey}`, true, 864e5)) return {};
|
|
301
|
+
const recipients = /* @__PURE__ */ new Set();
|
|
302
|
+
for (const [userId, devices] of Object.entries(body.messages)) {
|
|
303
|
+
if (!isObject(devices) || !hs.isLocalUser(userId)) continue;
|
|
304
|
+
let targets = Object.keys(devices);
|
|
305
|
+
if (targets.includes("*")) targets = (await hs.db.findMany("devices", { where: [{
|
|
306
|
+
field: "user_id",
|
|
307
|
+
value: userId
|
|
308
|
+
}] })).map((d) => d.device_id);
|
|
309
|
+
for (const deviceId of targets) {
|
|
310
|
+
const content = devices[deviceId] ?? devices["*"];
|
|
311
|
+
await hs.streams.write((stream) => hs.db.create("to_device", {
|
|
312
|
+
stream,
|
|
313
|
+
user_id: userId,
|
|
314
|
+
device_id: deviceId,
|
|
315
|
+
sender: s.userId,
|
|
316
|
+
type: ctx.params.eventType,
|
|
317
|
+
content
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
recipients.add(userId);
|
|
321
|
+
}
|
|
322
|
+
hs.notifier.notify(recipients);
|
|
323
|
+
await hs.federation.sendToDevice(s.userId, ctx.params.eventType, body.messages, `${s.deviceId}-${ctx.params.txnId}`);
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
}));
|
|
284
327
|
}
|
|
285
328
|
//#endregion
|
|
286
329
|
export { registerUserRoutes };
|
package/dist/core/admin.d.ts
CHANGED
package/dist/core/admin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomString } from "./crypto.js";
|
|
2
|
+
import { M } from "./m.js";
|
|
2
3
|
import { errors } from "./errors.js";
|
|
3
4
|
import { LOCALPART_RE, isValidUserId } from "./ids.js";
|
|
4
5
|
import { createUser } from "../api/account.js";
|
|
@@ -259,7 +260,7 @@ var AdminService = class {
|
|
|
259
260
|
const membership = await this.hs.rooms.membership(roomId, userId);
|
|
260
261
|
if (membership !== "join" && membership !== "invite") await this.hs.rooms.invite(roomId, noticeUser, userId);
|
|
261
262
|
const eventId = await this.hs.rooms.sendEvent(roomId, noticeUser, {
|
|
262
|
-
type: opts.type ??
|
|
263
|
+
type: opts.type ?? M.ROOM.MESSAGE,
|
|
263
264
|
content
|
|
264
265
|
});
|
|
265
266
|
if (txnKey) await this.hs.cluster.set(txnKey, eventId, 864e5);
|
|
@@ -282,9 +283,9 @@ var AdminService = class {
|
|
|
282
283
|
},
|
|
283
284
|
{
|
|
284
285
|
field: "type",
|
|
285
|
-
value:
|
|
286
|
+
value: M.TAG
|
|
286
287
|
}
|
|
287
|
-
]))?.content?.tags?.[
|
|
288
|
+
]))?.content?.tags?.[M.SERVER_NOTICE]) return room.room_id;
|
|
288
289
|
const { room_id } = await this.hs.rooms.createRoom(noticeUser, {
|
|
289
290
|
preset: "private_chat",
|
|
290
291
|
name: this.hs.config.serverNotices.roomName,
|
|
@@ -296,8 +297,8 @@ var AdminService = class {
|
|
|
296
297
|
await this.hs.streams.write((stream) => this.hs.db.upsert("account_data", {
|
|
297
298
|
user_id: userId,
|
|
298
299
|
room_id,
|
|
299
|
-
type:
|
|
300
|
-
content: { tags: {
|
|
300
|
+
type: M.TAG,
|
|
301
|
+
content: { tags: { [M.SERVER_NOTICE]: { order: .5 } } },
|
|
301
302
|
stream
|
|
302
303
|
}));
|
|
303
304
|
return room_id;
|
package/dist/core/adminroom.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomString } from "./crypto.js";
|
|
2
|
+
import { M, UNSTABLE } from "./m.js";
|
|
2
3
|
import { MatrixError } from "./errors.js";
|
|
3
4
|
import { isValidRoomId, isValidUserId } from "./ids.js";
|
|
4
5
|
import { createUser } from "../api/account.js";
|
|
@@ -93,7 +94,7 @@ var AdminRoomService = class {
|
|
|
93
94
|
ban: 100
|
|
94
95
|
},
|
|
95
96
|
initial_state: [{
|
|
96
|
-
type:
|
|
97
|
+
type: M.ROOM.HISTORY_VISIBILITY,
|
|
97
98
|
state_key: "",
|
|
98
99
|
content: { history_visibility: "joined" }
|
|
99
100
|
}]
|
|
@@ -144,23 +145,23 @@ var AdminRoomService = class {
|
|
|
144
145
|
if (!roomId) return;
|
|
145
146
|
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
146
147
|
const content = {
|
|
147
|
-
msgtype:
|
|
148
|
+
msgtype: M.NOTICE,
|
|
148
149
|
body: text,
|
|
149
|
-
format:
|
|
150
|
+
format: UNSTABLE.ORG.MATRIX.CUSTOM.HTML,
|
|
150
151
|
formatted_body: text.includes("\n") ? `<pre><code>${escaped}</code></pre>` : escaped.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
151
152
|
};
|
|
152
|
-
if (replyTo) content[
|
|
153
|
+
if (replyTo) content[M.RELATES_TO] = { [M.IN_REPLY_TO]: { event_id: replyTo } };
|
|
153
154
|
await this.hs.rooms.sendEvent(roomId, this.botUserId, {
|
|
154
|
-
type:
|
|
155
|
+
type: M.ROOM.MESSAGE,
|
|
155
156
|
content
|
|
156
157
|
});
|
|
157
158
|
}
|
|
158
159
|
/** Called for every new event; handles commands posted in the admin room. */
|
|
159
160
|
async onEvent(event) {
|
|
160
|
-
if (!this.options.enabled || event.type !==
|
|
161
|
+
if (!this.options.enabled || event.type !== M.ROOM.MESSAGE || event.sender === this.botUserId) return;
|
|
161
162
|
if (event.room_id !== await this.roomId()) return;
|
|
162
163
|
const body = typeof event.content?.body === "string" ? event.content.body.trim() : "";
|
|
163
|
-
if (!body || event.content?.msgtype ===
|
|
164
|
+
if (!body || event.content?.msgtype === M.NOTICE) return;
|
|
164
165
|
if (!(await this.hs.db.findOne("users", [{
|
|
165
166
|
field: "user_id",
|
|
166
167
|
value: event.sender
|
|
@@ -418,7 +419,7 @@ var AdminRoomService = class {
|
|
|
418
419
|
const message = pos.slice(2).join(" ");
|
|
419
420
|
if (!message) return "Usage: notice <user> <message>";
|
|
420
421
|
await hs.admin.sendServerNotice(target, {
|
|
421
|
-
msgtype:
|
|
422
|
+
msgtype: M.TEXT,
|
|
422
423
|
body: message
|
|
423
424
|
});
|
|
424
425
|
await hs.audit.record({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PDU } from "./events.js";
|
|
2
2
|
import { StateMap } from "./authrules.js";
|
|
3
|
+
import { UNSTABLE } from "./m.js";
|
|
3
4
|
import { Homeserver } from "../homeserver.js";
|
|
4
5
|
//#region src/core/appservices.d.ts
|
|
5
6
|
export interface AppServiceNamespace {
|
|
@@ -22,7 +23,7 @@ export interface AppServiceRegistration {
|
|
|
22
23
|
rate_limited?: boolean;
|
|
23
24
|
protocols?: string[];
|
|
24
25
|
/** MSC2409: also push ephemeral events (typing, receipts, presence). */
|
|
25
|
-
|
|
26
|
+
[UNSTABLE.DE.SORUNOME.MSC2409.PUSH_EPHEMERAL]?: boolean;
|
|
26
27
|
receive_ephemeral?: boolean;
|
|
27
28
|
}
|
|
28
29
|
/**
|
package/dist/core/appservices.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { M, UNSTABLE } from "./m.js";
|
|
1
2
|
import { splitStateKey, toClientEvent } from "./events.js";
|
|
2
3
|
import { MatrixError, errors } from "./errors.js";
|
|
3
4
|
import { readFile, readdir } from "node:fs/promises";
|
|
@@ -159,7 +160,7 @@ var AppServiceService = class {
|
|
|
159
160
|
async interestedInEvent(svc, pdu, state) {
|
|
160
161
|
const reg = svc.reg;
|
|
161
162
|
if (this.isInterestedUser(reg, pdu.sender)) return true;
|
|
162
|
-
if (pdu.type ===
|
|
163
|
+
if (pdu.type === M.ROOM.MEMBER && typeof pdu.state_key === "string" && this.isInterestedUser(reg, pdu.state_key)) return true;
|
|
163
164
|
if (svc.rooms.some((n) => n.re.test(pdu.room_id))) return true;
|
|
164
165
|
if (svc.aliases.length) {
|
|
165
166
|
if ((await this.hs.db.findMany("room_aliases", { where: [{
|
|
@@ -169,7 +170,7 @@ var AppServiceService = class {
|
|
|
169
170
|
}
|
|
170
171
|
for (const [key, ev] of state) {
|
|
171
172
|
const [type, userId] = splitStateKey(key);
|
|
172
|
-
if (type ===
|
|
173
|
+
if (type === M.ROOM.MEMBER && ev.content?.membership === "join" && this.isInterestedUser(reg, userId)) return true;
|
|
173
174
|
}
|
|
174
175
|
return false;
|
|
175
176
|
}
|
|
@@ -184,7 +185,7 @@ var AppServiceService = class {
|
|
|
184
185
|
async onEphemeral(roomId, event, users) {
|
|
185
186
|
for (const svc of this.services) {
|
|
186
187
|
const reg = svc.reg;
|
|
187
|
-
if (!reg.url || !(reg.receive_ephemeral || reg[
|
|
188
|
+
if (!reg.url || !(reg.receive_ephemeral || reg[UNSTABLE.DE.SORUNOME.MSC2409.PUSH_EPHEMERAL])) continue;
|
|
188
189
|
let interested = users.some((u) => this.isInterestedUser(reg, u)) || roomId !== null && svc.rooms.some((n) => n.re.test(roomId));
|
|
189
190
|
if (!interested && roomId) {
|
|
190
191
|
for (const u of await this.hs.rooms.joinedMembers(roomId)) if (this.isInterestedUser(reg, u)) {
|
|
@@ -242,7 +243,7 @@ var AppServiceService = class {
|
|
|
242
243
|
const ephemeral = rows.filter((r) => r.kind === "ephemeral").map((r) => r.payload);
|
|
243
244
|
if (ephemeral.length) {
|
|
244
245
|
body.ephemeral = ephemeral;
|
|
245
|
-
body[
|
|
246
|
+
body[UNSTABLE.DE.SORUNOME.MSC2409.EPHEMERAL] = ephemeral;
|
|
246
247
|
}
|
|
247
248
|
const txnId = String(rows[rows.length - 1].id);
|
|
248
249
|
try {
|
package/dist/core/audit.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Row } from "../storage/types.js";
|
|
2
2
|
import { Homeserver } from "../homeserver.js";
|
|
3
|
-
import "../index.js";
|
|
4
3
|
//#region src/core/audit.d.ts
|
|
5
4
|
/** Security-relevant actions recorded in the audit log. */
|
|
6
5
|
export type AuditAction = "login" | "login_failed" | "logout" | "register" | "password_change" | "deactivate" | "device_create" | "device_delete" | "token_refresh" | "room_create" | "room_delete" | "member_kick" | "member_ban" | "member_unban" | "admin_action" | "admin_command" | "registration_token_create" | "registration_token_delete" | "registration_setting" | "event_report" | "user_report" | "sso_login" | "oauth_login" | "user_lock" | "user_unlock" | "user_suspend" | "user_unsuspend";
|