matrix-nodejs-homeserver 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +75 -0
- package/README.md +457 -2
- package/dist/adapters/auth.d.ts +50 -0
- package/dist/adapters/auth.js +18 -0
- package/dist/adapters/cluster.d.ts +55 -0
- package/dist/adapters/cluster.js +249 -0
- package/dist/adapters/media.d.ts +50 -0
- package/dist/adapters/media.js +63 -0
- package/dist/adapters/s3.d.ts +33 -0
- package/dist/adapters/s3.js +95 -0
- package/dist/api/account.js +560 -0
- package/dist/api/admin.js +251 -0
- package/dist/api/appservice.js +33 -0
- package/dist/api/backup.js +269 -0
- package/dist/api/federation.js +792 -0
- package/dist/api/filters.js +87 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +170 -0
- package/dist/api/media.js +255 -0
- package/dist/api/misc.js +86 -0
- package/dist/api/preview.js +200 -0
- package/dist/api/profile.js +165 -0
- package/dist/api/push.js +187 -0
- package/dist/api/relationships.js +264 -0
- package/dist/api/rooms.js +1364 -0
- package/dist/api/rtc.d.ts +24 -0
- package/dist/api/rtc.js +52 -0
- package/dist/api/search.js +155 -0
- package/dist/api/slidingsync.js +464 -0
- package/dist/api/spaces.js +172 -0
- package/dist/api/sync.js +600 -0
- package/dist/api/threadsubs.js +129 -0
- package/dist/api/threepid.js +122 -0
- package/dist/api/uia.js +126 -0
- package/dist/api/user.js +286 -0
- package/dist/core/admin.d.ts +85 -0
- package/dist/core/admin.js +307 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +482 -0
- package/dist/core/appservices.d.ts +73 -0
- package/dist/core/appservices.js +368 -0
- package/dist/core/audit.d.ts +51 -0
- package/dist/core/audit.js +86 -0
- package/dist/core/auth.d.ts +48 -0
- package/dist/core/auth.js +237 -0
- package/dist/core/authrules.d.ts +4 -0
- package/dist/core/authrules.js +347 -0
- package/dist/core/cluster.d.ts +66 -0
- package/dist/core/cluster.js +191 -0
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/crypto.js +100 -0
- package/dist/core/delayed.d.ts +39 -0
- package/dist/core/delayed.js +159 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/events.d.ts +32 -0
- package/dist/core/events.js +185 -0
- package/dist/core/glob.js +66 -0
- package/dist/core/identity.d.ts +24 -0
- package/dist/core/identity.js +96 -0
- package/dist/core/ids.js +36 -0
- package/dist/core/invitefilter.js +35 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +449 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +54 -0
- package/dist/core/oauth.d.ts +38 -0
- package/dist/core/oauth.js +108 -0
- package/dist/core/partialstate.d.ts +32 -0
- package/dist/core/partialstate.js +197 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +96 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +47 -0
- package/dist/core/push.js +306 -0
- package/dist/core/pushrules.d.ts +1 -0
- package/dist/core/pushrules.js +228 -0
- package/dist/core/ratelimit.d.ts +23 -0
- package/dist/core/ratelimit.js +77 -0
- package/dist/core/rooms.d.ts +215 -0
- package/dist/core/rooms.js +1729 -0
- package/dist/core/settings.d.ts +11 -0
- package/dist/core/settings.js +29 -0
- package/dist/core/sso.d.ts +46 -0
- package/dist/core/sso.js +165 -0
- package/dist/core/stateres.d.ts +4 -0
- package/dist/core/stateres.js +208 -0
- package/dist/core/threepid.d.ts +80 -0
- package/dist/core/threepid.js +275 -0
- package/dist/core/typing.d.ts +16 -0
- package/dist/core/typing.js +60 -0
- package/dist/federation/client.d.ts +51 -0
- package/dist/federation/client.js +278 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1058 -0
- package/dist/federation/keyring.d.ts +26 -0
- package/dist/federation/keyring.js +122 -0
- package/dist/federation/sender.d.ts +26 -0
- package/dist/federation/sender.js +164 -0
- package/dist/homeserver.d.ts +223 -0
- package/dist/homeserver.js +301 -0
- package/dist/http/adapters.d.ts +40 -0
- package/dist/http/adapters.js +130 -0
- package/dist/http/handler.d.ts +8 -0
- package/dist/http/handler.js +202 -0
- package/dist/http/input.js +24 -0
- package/dist/http/router.d.ts +60 -0
- package/dist/http/router.js +76 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +14 -0
- package/dist/storage/memory.d.ts +5 -0
- package/dist/storage/memory.js +122 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.js +664 -0
- package/dist/storage/sql.d.ts +72 -0
- package/dist/storage/sql.js +274 -0
- package/dist/storage/types.d.ts +55 -0
- package/package.json +67 -4
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { errors } from "../core/errors.js";
|
|
2
|
+
import { body, fail, lenient } from "../http/input.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/api/admin.ts
|
|
5
|
+
const required = fail("M_BAD_JSON", "user_id and content are required");
|
|
6
|
+
const noticeBody = body({
|
|
7
|
+
user_id: z.string(required),
|
|
8
|
+
content: z.looseObject({}, required),
|
|
9
|
+
type: lenient(z.string())
|
|
10
|
+
});
|
|
11
|
+
const optionalInt = (name) => z.number(fail("M_INVALID_PARAM", `${name} must be an integer`)).int(`M_INVALID_PARAM: ${name} must be an integer`);
|
|
12
|
+
const registrationTokenBody = body({
|
|
13
|
+
token: z.string(fail("M_INVALID_PARAM", "Invalid token")).optional(),
|
|
14
|
+
uses_allowed: optionalInt("uses_allowed").nullable().optional(),
|
|
15
|
+
expiry_time: optionalInt("expiry_time").nullable().optional(),
|
|
16
|
+
length: optionalInt("length").optional()
|
|
17
|
+
});
|
|
18
|
+
async function requireAdmin(ctx) {
|
|
19
|
+
const s = await ctx.auth();
|
|
20
|
+
if (!s.isAdmin) throw errors.forbidden("You are not a server admin");
|
|
21
|
+
if (ctx.method !== "GET") await ctx.hs.audit.record({
|
|
22
|
+
action: "admin_action",
|
|
23
|
+
actor: s.userId,
|
|
24
|
+
ip: ctx.ip,
|
|
25
|
+
details: {
|
|
26
|
+
method: ctx.method,
|
|
27
|
+
path: ctx.url.pathname
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
/** Admin HTTP API using Synapse-compatible paths, so existing tooling keeps working. */
|
|
33
|
+
function registerAdminRoutes(r) {
|
|
34
|
+
const notice = async (ctx) => {
|
|
35
|
+
await requireAdmin(ctx);
|
|
36
|
+
const body = ctx.input;
|
|
37
|
+
return { event_id: await ctx.hs.admin.sendServerNotice(body.user_id, body.content, {
|
|
38
|
+
type: body.type,
|
|
39
|
+
txnId: ctx.params.txnId
|
|
40
|
+
}) };
|
|
41
|
+
};
|
|
42
|
+
const moderation = (field) => {
|
|
43
|
+
const target = async (ctx) => {
|
|
44
|
+
await requireAdmin(ctx);
|
|
45
|
+
const userId = ctx.params.userId;
|
|
46
|
+
if (!ctx.hs.isLocalUser(userId)) throw errors.invalidParam("Not a local user");
|
|
47
|
+
const user = await ctx.hs.db.findOne("users", [{
|
|
48
|
+
field: "user_id",
|
|
49
|
+
value: userId
|
|
50
|
+
}]);
|
|
51
|
+
if (!user) throw errors.notFound("Unknown user");
|
|
52
|
+
return user;
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
get: async (ctx) => ({ [field]: !!(await target(ctx))[field] }),
|
|
56
|
+
put: async (ctx) => {
|
|
57
|
+
await target(ctx);
|
|
58
|
+
const s = await ctx.auth();
|
|
59
|
+
const body = await ctx.json();
|
|
60
|
+
if (typeof body[field] !== "boolean") throw errors.badJson(`${field} must be a boolean`);
|
|
61
|
+
if (field === "locked") await ctx.hs.admin.setLocked(ctx.params.userId, body.locked, s.userId);
|
|
62
|
+
else await ctx.hs.admin.setSuspended(ctx.params.userId, body.suspended, s.userId);
|
|
63
|
+
return { [field]: body[field] };
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const lock = moderation("locked"), suspend = moderation("suspended");
|
|
68
|
+
r.get("/_matrix/client/v1/admin/lock/:userId", lock.get);
|
|
69
|
+
r.put("/_matrix/client/v1/admin/lock/:userId", lock.put);
|
|
70
|
+
r.get("/_matrix/client/v1/admin/suspend/:userId", suspend.get);
|
|
71
|
+
r.put("/_matrix/client/v1/admin/suspend/:userId", suspend.put);
|
|
72
|
+
r.post("/_synapse/admin/v1/send_server_notice", { input: noticeBody }, notice);
|
|
73
|
+
r.put("/_synapse/admin/v1/send_server_notice/:txnId", { input: noticeBody }, notice);
|
|
74
|
+
r.get("/_synapse/admin/v2/users", async (ctx) => {
|
|
75
|
+
await requireAdmin(ctx);
|
|
76
|
+
const from = Number(ctx.query.get("from") ?? 0) || 0;
|
|
77
|
+
const res = await ctx.hs.admin.listUsers({
|
|
78
|
+
from,
|
|
79
|
+
limit: Number(ctx.query.get("limit") ?? 100) || 100,
|
|
80
|
+
search: ctx.query.get("name") ?? void 0,
|
|
81
|
+
includeDeactivated: ctx.query.get("deactivated") === "true"
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
users: res.users,
|
|
85
|
+
total: res.total,
|
|
86
|
+
...res.next !== null ? { next_token: String(res.next) } : {}
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
r.get("/_synapse/admin/v2/users/:userId", async (ctx) => {
|
|
90
|
+
await requireAdmin(ctx);
|
|
91
|
+
const user = await ctx.hs.admin.getUser(ctx.params.userId);
|
|
92
|
+
if (!user) throw errors.notFound("Unknown user");
|
|
93
|
+
return user;
|
|
94
|
+
});
|
|
95
|
+
r.put("/_synapse/admin/v2/users/:userId", async (ctx) => {
|
|
96
|
+
await requireAdmin(ctx);
|
|
97
|
+
const body = await ctx.json();
|
|
98
|
+
const userId = ctx.params.userId;
|
|
99
|
+
if (!ctx.hs.isLocalUser(userId)) throw errors.invalidParam("Can only manage local users");
|
|
100
|
+
if (!await ctx.hs.admin.getUser(userId)) {
|
|
101
|
+
await ctx.hs.admin.createUser(userId.slice(1).split(":")[0], {
|
|
102
|
+
password: body.password,
|
|
103
|
+
admin: body.admin === true,
|
|
104
|
+
displayname: body.displayname
|
|
105
|
+
});
|
|
106
|
+
return new Response(JSON.stringify(await ctx.hs.admin.getUser(userId)), {
|
|
107
|
+
status: 201,
|
|
108
|
+
headers: { "content-type": "application/json" }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (typeof body.password === "string") await ctx.hs.admin.resetPassword(userId, body.password, body.logout_devices !== false);
|
|
112
|
+
if (typeof body.admin === "boolean") await ctx.hs.admin.setAdmin(userId, body.admin);
|
|
113
|
+
if (typeof body.displayname === "string") {
|
|
114
|
+
await ctx.hs.db.update("users", [{
|
|
115
|
+
field: "user_id",
|
|
116
|
+
value: userId
|
|
117
|
+
}], { displayname: body.displayname });
|
|
118
|
+
await ctx.hs.rooms.propagateProfile(userId);
|
|
119
|
+
}
|
|
120
|
+
if (body.deactivated === true) await ctx.hs.admin.deactivateUser(userId);
|
|
121
|
+
return ctx.hs.admin.getUser(userId);
|
|
122
|
+
});
|
|
123
|
+
r.post("/_synapse/admin/v1/deactivate/:userId", { input: body({ erase: lenient(z.boolean()) }) }, async (ctx) => {
|
|
124
|
+
await requireAdmin(ctx);
|
|
125
|
+
const body = ctx.input;
|
|
126
|
+
await ctx.hs.admin.deactivateUser(ctx.params.userId, { erase: body.erase === true });
|
|
127
|
+
return { id_server_unbind_result: "no-support" };
|
|
128
|
+
});
|
|
129
|
+
const resetPasswordBody = body({
|
|
130
|
+
new_password: z.string(fail("M_MISSING_PARAM", "Missing new_password")),
|
|
131
|
+
logout_devices: lenient(z.boolean())
|
|
132
|
+
});
|
|
133
|
+
r.post("/_synapse/admin/v1/reset_password/:userId", { input: resetPasswordBody }, async (ctx) => {
|
|
134
|
+
await requireAdmin(ctx);
|
|
135
|
+
const body = ctx.input;
|
|
136
|
+
await ctx.hs.admin.resetPassword(ctx.params.userId, body.new_password, body.logout_devices !== false);
|
|
137
|
+
return {};
|
|
138
|
+
});
|
|
139
|
+
r.get("/_synapse/admin/v1/registration_tokens", async (ctx) => {
|
|
140
|
+
await requireAdmin(ctx);
|
|
141
|
+
return { registration_tokens: await ctx.hs.admin.listRegistrationTokens() };
|
|
142
|
+
});
|
|
143
|
+
r.post("/_synapse/admin/v1/registration_tokens/new", { input: registrationTokenBody }, async (ctx) => {
|
|
144
|
+
await requireAdmin(ctx);
|
|
145
|
+
const body = ctx.input;
|
|
146
|
+
return ctx.hs.admin.createRegistrationToken({
|
|
147
|
+
token: body.token,
|
|
148
|
+
usesAllowed: body.uses_allowed,
|
|
149
|
+
expiryTime: body.expiry_time,
|
|
150
|
+
length: body.length
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
r.delete("/_synapse/admin/v1/registration_tokens/:token", async (ctx) => {
|
|
154
|
+
await requireAdmin(ctx);
|
|
155
|
+
await ctx.hs.admin.deleteRegistrationToken(ctx.params.token);
|
|
156
|
+
return {};
|
|
157
|
+
});
|
|
158
|
+
r.get("/_synapse/admin/v1/audit_log", async (ctx) => {
|
|
159
|
+
await requireAdmin(ctx);
|
|
160
|
+
if (!ctx.hs.audit.enabled) throw errors.unrecognized("The audit log is disabled");
|
|
161
|
+
const rows = await ctx.hs.audit.query({
|
|
162
|
+
actor: ctx.query.get("actor") ?? void 0,
|
|
163
|
+
target: ctx.query.get("target") ?? void 0,
|
|
164
|
+
action: ctx.query.get("action") ?? void 0,
|
|
165
|
+
since: Number(ctx.query.get("since")) || void 0,
|
|
166
|
+
before: Number(ctx.query.get("from")) || void 0,
|
|
167
|
+
limit: Number(ctx.query.get("limit")) || 100
|
|
168
|
+
});
|
|
169
|
+
const out = { entries: rows };
|
|
170
|
+
if (rows.length) out.next_token = String(rows[rows.length - 1].id);
|
|
171
|
+
return out;
|
|
172
|
+
});
|
|
173
|
+
r.get("/_synapse/admin/v1/event_reports", async (ctx) => {
|
|
174
|
+
await requireAdmin(ctx);
|
|
175
|
+
const rows = await ctx.hs.db.findMany("event_reports", {
|
|
176
|
+
sortBy: {
|
|
177
|
+
field: "id",
|
|
178
|
+
direction: "desc"
|
|
179
|
+
},
|
|
180
|
+
limit: Number(ctx.query.get("limit")) || 100
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
event_reports: rows.map((r) => ({
|
|
184
|
+
id: r.id,
|
|
185
|
+
received_ts: r.ts,
|
|
186
|
+
room_id: r.room_id,
|
|
187
|
+
event_id: r.event_id,
|
|
188
|
+
user_id: r.reporter,
|
|
189
|
+
reason: r.reason,
|
|
190
|
+
score: r.score,
|
|
191
|
+
resolved: r.resolved
|
|
192
|
+
})),
|
|
193
|
+
total: rows.length
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
r.post("/_synapse/admin/v1/event_reports/:id/resolve", async (ctx) => {
|
|
197
|
+
await requireAdmin(ctx);
|
|
198
|
+
if (!await ctx.hs.db.update("event_reports", [{
|
|
199
|
+
field: "id",
|
|
200
|
+
value: Number(ctx.params.id)
|
|
201
|
+
}], { resolved: true })) throw errors.notFound("Unknown report");
|
|
202
|
+
return {};
|
|
203
|
+
});
|
|
204
|
+
r.get("/_synapse/admin/v1/rooms", async (ctx) => {
|
|
205
|
+
await requireAdmin(ctx);
|
|
206
|
+
const res = await ctx.hs.admin.listRooms({
|
|
207
|
+
from: Number(ctx.query.get("from") ?? 0) || 0,
|
|
208
|
+
limit: Number(ctx.query.get("limit") ?? 100) || 100
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
rooms: res.rooms,
|
|
212
|
+
total_rooms: res.total
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
r.delete("/_synapse/admin/v1/rooms/:roomId", async (ctx) => {
|
|
216
|
+
const s = await requireAdmin(ctx);
|
|
217
|
+
const body = await ctx.json();
|
|
218
|
+
const { kicked } = await ctx.hs.admin.deleteRoom(ctx.params.roomId, {
|
|
219
|
+
actor: s.userId,
|
|
220
|
+
reason: body.message,
|
|
221
|
+
purge: body.purge !== false
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
kicked_users: kicked,
|
|
225
|
+
failed_to_kick_users: [],
|
|
226
|
+
local_aliases: [],
|
|
227
|
+
new_room_id: null
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
r.delete("/_synapse/admin/v1/media/:serverName/:mediaId", async (ctx) => {
|
|
231
|
+
await requireAdmin(ctx);
|
|
232
|
+
if (ctx.params.serverName !== ctx.hs.serverName) throw errors.notFound("Remote media");
|
|
233
|
+
await ctx.hs.admin.deleteMedia(ctx.params.mediaId);
|
|
234
|
+
return {
|
|
235
|
+
deleted_media: [ctx.params.mediaId],
|
|
236
|
+
total: 1
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
r.post("/_synapse/admin/v1/media/delete", async (ctx) => {
|
|
240
|
+
await requireAdmin(ctx);
|
|
241
|
+
const before = Number(ctx.query.get("before_ts"));
|
|
242
|
+
if (!Number.isFinite(before)) throw errors.missingParam("Missing before_ts");
|
|
243
|
+
const deleted = await ctx.hs.admin.purgeMediaBefore(before);
|
|
244
|
+
return {
|
|
245
|
+
deleted_media: deleted,
|
|
246
|
+
total: deleted.length
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
export { registerAdminRoutes };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { errors } from "../core/errors.js";
|
|
2
|
+
//#region src/api/appservice.ts
|
|
3
|
+
function registerAppServiceRoutes(r) {
|
|
4
|
+
r.client("POST", "/appservice/:appserviceId/ping", async (ctx) => {
|
|
5
|
+
const s = await ctx.auth();
|
|
6
|
+
if (!s.appservice) throw errors.forbidden("An application service token is required");
|
|
7
|
+
if (s.appservice.id !== ctx.params.appserviceId) throw errors.forbidden("Token does not belong to this application service");
|
|
8
|
+
const body = await ctx.json();
|
|
9
|
+
return { duration_ms: await ctx.hs.appservices.ping(s.appservice, typeof body.transaction_id === "string" ? body.transaction_id : void 0) };
|
|
10
|
+
}, ["v1"]);
|
|
11
|
+
r.client("GET", "/thirdparty/protocols", async (ctx) => {
|
|
12
|
+
await ctx.auth();
|
|
13
|
+
return ctx.hs.appservices.protocols();
|
|
14
|
+
});
|
|
15
|
+
r.client("GET", "/thirdparty/protocol/:protocol", async (ctx) => {
|
|
16
|
+
await ctx.auth();
|
|
17
|
+
const p = (await ctx.hs.appservices.protocols())[ctx.params.protocol];
|
|
18
|
+
if (!p) throw errors.notFound("Unknown protocol");
|
|
19
|
+
return p;
|
|
20
|
+
});
|
|
21
|
+
for (const kind of ["location", "user"]) {
|
|
22
|
+
r.client("GET", `/thirdparty/${kind}/:protocol`, async (ctx) => {
|
|
23
|
+
await ctx.auth();
|
|
24
|
+
return ctx.hs.appservices.thirdParty(`${kind}/${encodeURIComponent(ctx.params.protocol)}`, ctx.query);
|
|
25
|
+
});
|
|
26
|
+
r.client("GET", `/thirdparty/${kind}`, async (ctx) => {
|
|
27
|
+
await ctx.auth();
|
|
28
|
+
return ctx.hs.appservices.thirdParty(kind, ctx.query);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { registerAppServiceRoutes };
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { isObject } from "../core/json.js";
|
|
2
|
+
import { MatrixError, errors } from "../core/errors.js";
|
|
3
|
+
import { body, fail } from "../http/input.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/api/backup.ts
|
|
6
|
+
const required = fail("M_BAD_JSON", "algorithm and auth_data are required");
|
|
7
|
+
const createVersionBody = body({
|
|
8
|
+
algorithm: z.string(required),
|
|
9
|
+
auth_data: z.looseObject({}, required)
|
|
10
|
+
});
|
|
11
|
+
/** Server-side room key backups (`/room_keys`). */
|
|
12
|
+
async function latestVersion(hs, userId) {
|
|
13
|
+
return (await hs.db.findMany("backup_versions", {
|
|
14
|
+
where: [{
|
|
15
|
+
field: "user_id",
|
|
16
|
+
value: userId
|
|
17
|
+
}, {
|
|
18
|
+
field: "deleted",
|
|
19
|
+
value: false
|
|
20
|
+
}],
|
|
21
|
+
sortBy: {
|
|
22
|
+
field: "version",
|
|
23
|
+
direction: "desc"
|
|
24
|
+
},
|
|
25
|
+
limit: 1
|
|
26
|
+
}))[0] ?? null;
|
|
27
|
+
}
|
|
28
|
+
async function getVersion(hs, userId, version) {
|
|
29
|
+
if (version === null) return latestVersion(hs, userId);
|
|
30
|
+
const n = Number(version);
|
|
31
|
+
if (!Number.isInteger(n)) return null;
|
|
32
|
+
const row = await hs.db.findOne("backup_versions", [{
|
|
33
|
+
field: "user_id",
|
|
34
|
+
value: userId
|
|
35
|
+
}, {
|
|
36
|
+
field: "version",
|
|
37
|
+
value: n
|
|
38
|
+
}]);
|
|
39
|
+
return row && !row.deleted ? row : null;
|
|
40
|
+
}
|
|
41
|
+
async function countKeys(hs, userId, version) {
|
|
42
|
+
return (await hs.db.findMany("backup_keys", { where: [{
|
|
43
|
+
field: "user_id",
|
|
44
|
+
value: userId
|
|
45
|
+
}, {
|
|
46
|
+
field: "version",
|
|
47
|
+
value: version
|
|
48
|
+
}] })).length;
|
|
49
|
+
}
|
|
50
|
+
async function describe(hs, row) {
|
|
51
|
+
return {
|
|
52
|
+
algorithm: row.algorithm,
|
|
53
|
+
auth_data: row.auth_data,
|
|
54
|
+
version: String(row.version),
|
|
55
|
+
etag: String(row.etag),
|
|
56
|
+
count: await countKeys(hs, row.user_id, row.version)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Whether `incoming` should replace `existing` according to the spec's key-selection rules. */
|
|
60
|
+
function better(incoming, existing) {
|
|
61
|
+
if (!existing) return true;
|
|
62
|
+
if (!!incoming.is_verified !== !!existing.is_verified) return !!incoming.is_verified;
|
|
63
|
+
if (incoming.first_message_index !== existing.first_message_index) return incoming.first_message_index < existing.first_message_index;
|
|
64
|
+
return incoming.forwarded_count < existing.forwarded_count;
|
|
65
|
+
}
|
|
66
|
+
async function requireCurrentVersion(ctx, userId) {
|
|
67
|
+
const version = ctx.query.get("version");
|
|
68
|
+
if (!version) throw errors.missingParam("Missing version");
|
|
69
|
+
const row = await getVersion(ctx.hs, userId, version);
|
|
70
|
+
if (!row) throw errors.notFound("Unknown backup version");
|
|
71
|
+
return {
|
|
72
|
+
row,
|
|
73
|
+
isLatest: (await latestVersion(ctx.hs, userId))?.version === row.version
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function registerBackupRoutes(r) {
|
|
77
|
+
r.client("POST", "/room_keys/version", { input: createVersionBody }, async (ctx) => {
|
|
78
|
+
const s = await ctx.auth();
|
|
79
|
+
const body = ctx.input;
|
|
80
|
+
const max = (await ctx.hs.db.findMany("backup_versions", { where: [{
|
|
81
|
+
field: "user_id",
|
|
82
|
+
value: s.userId
|
|
83
|
+
}] })).reduce((m, v) => Math.max(m, v.version), 0);
|
|
84
|
+
await ctx.hs.db.create("backup_versions", {
|
|
85
|
+
user_id: s.userId,
|
|
86
|
+
version: max + 1,
|
|
87
|
+
algorithm: body.algorithm,
|
|
88
|
+
auth_data: body.auth_data,
|
|
89
|
+
etag: 0,
|
|
90
|
+
deleted: false
|
|
91
|
+
});
|
|
92
|
+
return { version: String(max + 1) };
|
|
93
|
+
});
|
|
94
|
+
const getVersionHandler = async (ctx) => {
|
|
95
|
+
const s = await ctx.auth();
|
|
96
|
+
const row = await getVersion(ctx.hs, s.userId, ctx.params.version ?? null);
|
|
97
|
+
if (!row) throw errors.notFound("No backup found");
|
|
98
|
+
return describe(ctx.hs, row);
|
|
99
|
+
};
|
|
100
|
+
r.client("GET", "/room_keys/version", getVersionHandler);
|
|
101
|
+
r.client("GET", "/room_keys/version/:version", getVersionHandler);
|
|
102
|
+
r.client("PUT", "/room_keys/version/:version", async (ctx) => {
|
|
103
|
+
const s = await ctx.auth();
|
|
104
|
+
const body = await ctx.json();
|
|
105
|
+
const row = await getVersion(ctx.hs, s.userId, ctx.params.version);
|
|
106
|
+
if (!row) throw errors.notFound("Unknown backup version");
|
|
107
|
+
if (body.version !== void 0 && String(body.version) !== ctx.params.version) throw errors.invalidParam("Version mismatch");
|
|
108
|
+
if (body.algorithm !== row.algorithm) throw errors.invalidParam("Algorithm cannot be changed");
|
|
109
|
+
if (!isObject(body.auth_data)) throw errors.badJson("auth_data is required");
|
|
110
|
+
await ctx.hs.db.update("backup_versions", [{
|
|
111
|
+
field: "user_id",
|
|
112
|
+
value: s.userId
|
|
113
|
+
}, {
|
|
114
|
+
field: "version",
|
|
115
|
+
value: row.version
|
|
116
|
+
}], { auth_data: body.auth_data });
|
|
117
|
+
return {};
|
|
118
|
+
});
|
|
119
|
+
r.client("DELETE", "/room_keys/version/:version", async (ctx) => {
|
|
120
|
+
const s = await ctx.auth();
|
|
121
|
+
const row = await getVersion(ctx.hs, s.userId, ctx.params.version);
|
|
122
|
+
if (!row) throw errors.notFound("Unknown backup version");
|
|
123
|
+
await ctx.hs.db.update("backup_versions", [{
|
|
124
|
+
field: "user_id",
|
|
125
|
+
value: s.userId
|
|
126
|
+
}, {
|
|
127
|
+
field: "version",
|
|
128
|
+
value: row.version
|
|
129
|
+
}], { deleted: true });
|
|
130
|
+
await ctx.hs.db.delete("backup_keys", [{
|
|
131
|
+
field: "user_id",
|
|
132
|
+
value: s.userId
|
|
133
|
+
}, {
|
|
134
|
+
field: "version",
|
|
135
|
+
value: row.version
|
|
136
|
+
}]);
|
|
137
|
+
return {};
|
|
138
|
+
});
|
|
139
|
+
const putKeys = async (ctx) => {
|
|
140
|
+
const s = await ctx.auth();
|
|
141
|
+
const hs = ctx.hs;
|
|
142
|
+
const { row, isLatest } = await requireCurrentVersion(ctx, s.userId);
|
|
143
|
+
if (!isLatest) {
|
|
144
|
+
const latest = await latestVersion(hs, s.userId);
|
|
145
|
+
throw new MatrixError(403, "M_WRONG_ROOM_KEYS_VERSION", "Wrong backup version", { current_version: String(latest?.version ?? "") });
|
|
146
|
+
}
|
|
147
|
+
const body = await ctx.json();
|
|
148
|
+
let rooms;
|
|
149
|
+
if (ctx.params.sessionId) rooms = { [ctx.params.roomId]: { sessions: { [ctx.params.sessionId]: body } } };
|
|
150
|
+
else if (ctx.params.roomId) rooms = { [ctx.params.roomId]: body };
|
|
151
|
+
else rooms = body.rooms ?? {};
|
|
152
|
+
let changed = false;
|
|
153
|
+
for (const [roomId, room] of Object.entries(rooms)) {
|
|
154
|
+
if (!isObject(room) || !isObject(room.sessions)) continue;
|
|
155
|
+
for (const [sessionId, data] of Object.entries(room.sessions)) {
|
|
156
|
+
const where = [
|
|
157
|
+
{
|
|
158
|
+
field: "user_id",
|
|
159
|
+
value: s.userId
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
field: "version",
|
|
163
|
+
value: row.version
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
field: "room_id",
|
|
167
|
+
value: roomId
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
field: "session_id",
|
|
171
|
+
value: sessionId
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
if (!better(data, (await hs.db.findOne("backup_keys", where))?.data ?? null)) continue;
|
|
175
|
+
await hs.db.upsert("backup_keys", {
|
|
176
|
+
user_id: s.userId,
|
|
177
|
+
version: row.version,
|
|
178
|
+
room_id: roomId,
|
|
179
|
+
session_id: sessionId,
|
|
180
|
+
data
|
|
181
|
+
});
|
|
182
|
+
changed = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const etag = changed ? row.etag + 1 : row.etag;
|
|
186
|
+
if (changed) await hs.db.update("backup_versions", [{
|
|
187
|
+
field: "user_id",
|
|
188
|
+
value: s.userId
|
|
189
|
+
}, {
|
|
190
|
+
field: "version",
|
|
191
|
+
value: row.version
|
|
192
|
+
}], { etag });
|
|
193
|
+
return {
|
|
194
|
+
etag: String(etag),
|
|
195
|
+
count: await countKeys(hs, s.userId, row.version)
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
const getKeys = async (ctx) => {
|
|
199
|
+
const s = await ctx.auth();
|
|
200
|
+
const { row } = await requireCurrentVersion(ctx, s.userId);
|
|
201
|
+
const where = [{
|
|
202
|
+
field: "user_id",
|
|
203
|
+
value: s.userId
|
|
204
|
+
}, {
|
|
205
|
+
field: "version",
|
|
206
|
+
value: row.version
|
|
207
|
+
}];
|
|
208
|
+
if (ctx.params.roomId) where.push({
|
|
209
|
+
field: "room_id",
|
|
210
|
+
value: ctx.params.roomId
|
|
211
|
+
});
|
|
212
|
+
if (ctx.params.sessionId) where.push({
|
|
213
|
+
field: "session_id",
|
|
214
|
+
value: ctx.params.sessionId
|
|
215
|
+
});
|
|
216
|
+
const keys = await ctx.hs.db.findMany("backup_keys", { where });
|
|
217
|
+
if (ctx.params.sessionId) {
|
|
218
|
+
if (!keys[0]) throw errors.notFound("Key not found");
|
|
219
|
+
return keys[0].data;
|
|
220
|
+
}
|
|
221
|
+
const rooms = {};
|
|
222
|
+
for (const k of keys) (rooms[k.room_id] ??= { sessions: {} }).sessions[k.session_id] = k.data;
|
|
223
|
+
if (ctx.params.roomId) return rooms[ctx.params.roomId] ?? { sessions: {} };
|
|
224
|
+
return { rooms };
|
|
225
|
+
};
|
|
226
|
+
const deleteKeys = async (ctx) => {
|
|
227
|
+
const s = await ctx.auth();
|
|
228
|
+
const { row } = await requireCurrentVersion(ctx, s.userId);
|
|
229
|
+
const where = [{
|
|
230
|
+
field: "user_id",
|
|
231
|
+
value: s.userId
|
|
232
|
+
}, {
|
|
233
|
+
field: "version",
|
|
234
|
+
value: row.version
|
|
235
|
+
}];
|
|
236
|
+
if (ctx.params.roomId) where.push({
|
|
237
|
+
field: "room_id",
|
|
238
|
+
value: ctx.params.roomId
|
|
239
|
+
});
|
|
240
|
+
if (ctx.params.sessionId) where.push({
|
|
241
|
+
field: "session_id",
|
|
242
|
+
value: ctx.params.sessionId
|
|
243
|
+
});
|
|
244
|
+
await ctx.hs.db.delete("backup_keys", where);
|
|
245
|
+
const etag = row.etag + 1;
|
|
246
|
+
await ctx.hs.db.update("backup_versions", [{
|
|
247
|
+
field: "user_id",
|
|
248
|
+
value: s.userId
|
|
249
|
+
}, {
|
|
250
|
+
field: "version",
|
|
251
|
+
value: row.version
|
|
252
|
+
}], { etag });
|
|
253
|
+
return {
|
|
254
|
+
etag: String(etag),
|
|
255
|
+
count: await countKeys(ctx.hs, s.userId, row.version)
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
for (const path of [
|
|
259
|
+
"/room_keys/keys",
|
|
260
|
+
"/room_keys/keys/:roomId",
|
|
261
|
+
"/room_keys/keys/:roomId/:sessionId"
|
|
262
|
+
]) {
|
|
263
|
+
r.client("PUT", path, putKeys);
|
|
264
|
+
r.client("GET", path, getKeys);
|
|
265
|
+
r.client("DELETE", path, deleteKeys);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
export { registerBackupRoutes };
|