matrix-nodejs-homeserver 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +479 -2
  3. package/dist/adapters/auth.d.ts +50 -0
  4. package/dist/adapters/auth.js +18 -0
  5. package/dist/adapters/cluster.d.ts +55 -0
  6. package/dist/adapters/cluster.js +249 -0
  7. package/dist/adapters/media.d.ts +50 -0
  8. package/dist/adapters/media.js +63 -0
  9. package/dist/adapters/s3.d.ts +33 -0
  10. package/dist/adapters/s3.js +95 -0
  11. package/dist/api/account.js +594 -0
  12. package/dist/api/admin.js +267 -0
  13. package/dist/api/appservice.js +45 -0
  14. package/dist/api/backup.js +280 -0
  15. package/dist/api/federation.js +826 -0
  16. package/dist/api/filters.js +88 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +183 -0
  19. package/dist/api/media.js +271 -0
  20. package/dist/api/misc.js +95 -0
  21. package/dist/api/preview.js +201 -0
  22. package/dist/api/profile.js +176 -0
  23. package/dist/api/push.js +202 -0
  24. package/dist/api/relationships.js +270 -0
  25. package/dist/api/rooms.js +1487 -0
  26. package/dist/api/rtc.d.ts +23 -0
  27. package/dist/api/rtc.js +66 -0
  28. package/dist/api/search.js +161 -0
  29. package/dist/api/slidingsync.js +474 -0
  30. package/dist/api/spaces.js +177 -0
  31. package/dist/api/sync.js +609 -0
  32. package/dist/api/threadsubs.js +140 -0
  33. package/dist/api/threepid.js +155 -0
  34. package/dist/api/uia.js +127 -0
  35. package/dist/api/user.js +329 -0
  36. package/dist/core/admin.d.ts +84 -0
  37. package/dist/core/admin.js +308 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +483 -0
  40. package/dist/core/appservices.d.ts +74 -0
  41. package/dist/core/appservices.js +369 -0
  42. package/dist/core/audit.d.ts +50 -0
  43. package/dist/core/audit.js +86 -0
  44. package/dist/core/auth.d.ts +48 -0
  45. package/dist/core/auth.js +237 -0
  46. package/dist/core/authrules.d.ts +4 -0
  47. package/dist/core/authrules.js +348 -0
  48. package/dist/core/cluster.d.ts +66 -0
  49. package/dist/core/cluster.js +191 -0
  50. package/dist/core/crypto.d.ts +10 -0
  51. package/dist/core/crypto.js +100 -0
  52. package/dist/core/delayed.d.ts +39 -0
  53. package/dist/core/delayed.js +160 -0
  54. package/dist/core/errors.d.ts +9 -0
  55. package/dist/core/errors.js +45 -0
  56. package/dist/core/events.d.ts +32 -0
  57. package/dist/core/events.js +186 -0
  58. package/dist/core/glob.js +66 -0
  59. package/dist/core/identity.d.ts +24 -0
  60. package/dist/core/identity.js +96 -0
  61. package/dist/core/ids.js +36 -0
  62. package/dist/core/invitefilter.js +36 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +450 -0
  66. package/dist/core/m.d.ts +209 -0
  67. package/dist/core/m.js +182 -0
  68. package/dist/core/notifier.d.ts +17 -0
  69. package/dist/core/notifier.js +54 -0
  70. package/dist/core/oauth.d.ts +38 -0
  71. package/dist/core/oauth.js +108 -0
  72. package/dist/core/partialstate.d.ts +32 -0
  73. package/dist/core/partialstate.js +198 -0
  74. package/dist/core/phone.js +268 -0
  75. package/dist/core/policy.d.ts +29 -0
  76. package/dist/core/policy.js +97 -0
  77. package/dist/core/profile.js +39 -0
  78. package/dist/core/push.d.ts +46 -0
  79. package/dist/core/push.js +307 -0
  80. package/dist/core/pushrules.js +229 -0
  81. package/dist/core/ratelimit.d.ts +23 -0
  82. package/dist/core/ratelimit.js +77 -0
  83. package/dist/core/rooms.d.ts +215 -0
  84. package/dist/core/rooms.js +1731 -0
  85. package/dist/core/settings.d.ts +11 -0
  86. package/dist/core/settings.js +29 -0
  87. package/dist/core/sso.d.ts +46 -0
  88. package/dist/core/sso.js +165 -0
  89. package/dist/core/stateres.d.ts +4 -0
  90. package/dist/core/stateres.js +209 -0
  91. package/dist/core/threepid.d.ts +80 -0
  92. package/dist/core/threepid.js +275 -0
  93. package/dist/core/typing.d.ts +16 -0
  94. package/dist/core/typing.js +60 -0
  95. package/dist/federation/client.d.ts +51 -0
  96. package/dist/federation/client.js +279 -0
  97. package/dist/federation/index.d.ts +153 -0
  98. package/dist/federation/index.js +1059 -0
  99. package/dist/federation/keyring.d.ts +26 -0
  100. package/dist/federation/keyring.js +122 -0
  101. package/dist/federation/sender.d.ts +26 -0
  102. package/dist/federation/sender.js +164 -0
  103. package/dist/homeserver.d.ts +224 -0
  104. package/dist/homeserver.js +302 -0
  105. package/dist/http/adapters.d.ts +40 -0
  106. package/dist/http/adapters.js +130 -0
  107. package/dist/http/handler.d.ts +8 -0
  108. package/dist/http/handler.js +181 -0
  109. package/dist/http/input.js +24 -0
  110. package/dist/http/procedure.js +25 -0
  111. package/dist/http/router.d.ts +43 -0
  112. package/dist/http/router.js +56 -0
  113. package/dist/index.d.ts +26 -0
  114. package/dist/index.js +15 -0
  115. package/dist/storage/memory.d.ts +5 -0
  116. package/dist/storage/memory.js +122 -0
  117. package/dist/storage/schema.d.ts +4 -0
  118. package/dist/storage/schema.js +664 -0
  119. package/dist/storage/sql.d.ts +72 -0
  120. package/dist/storage/sql.js +274 -0
  121. package/dist/storage/types.d.ts +55 -0
  122. package/dist/vendor/zod/LICENSE +21 -0
  123. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  124. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  125. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  126. package/dist/vendor/zod/v4/core/api.js +512 -0
  127. package/dist/vendor/zod/v4/core/checks.js +300 -0
  128. package/dist/vendor/zod/v4/core/core.js +109 -0
  129. package/dist/vendor/zod/v4/core/doc.js +35 -0
  130. package/dist/vendor/zod/v4/core/errors.js +118 -0
  131. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  132. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  133. package/dist/vendor/zod/v4/core/parse.js +206 -0
  134. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  135. package/dist/vendor/zod/v4/core/registries.js +48 -0
  136. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  137. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  138. package/dist/vendor/zod/v4/core/util.js +537 -0
  139. package/dist/vendor/zod/v4/core/versions.js +8 -0
  140. package/dist/vendor/zod/v4/locales/en.js +110 -0
  141. package/package.json +65 -4
@@ -0,0 +1,483 @@
1
+ import { randomString } from "./crypto.js";
2
+ import { M, UNSTABLE } from "./m.js";
3
+ import { MatrixError } from "./errors.js";
4
+ import { isValidRoomId, isValidUserId } from "./ids.js";
5
+ import { createUser } from "../api/account.js";
6
+ //#region src/core/adminroom.ts
7
+ const HELP = `Admin commands (type them in this room, optionally prefixed with !admin):
8
+
9
+ users list [--all] List accounts (--all includes deactivated)
10
+ users info <user> Account details
11
+ users whois <user> Devices, IPs and last seen
12
+ users create <localpart> [password] [--admin]
13
+ users reset-password <user> [password] Generates a password when omitted; logs out all devices
14
+ users deactivate <user> [--keep-profile]
15
+ users make-admin <user> | users revoke-admin <user>
16
+ users lock <user> | users unlock <user> Lock: the account cannot be used until unlocked
17
+ users suspend <user> | users unsuspend <user> Suspend: read-only (can still leave rooms and log out)
18
+ tokens list | tokens create [uses] [days] | tokens delete <token>
19
+ registration status | open | close | token
20
+ rooms list | rooms info <room> | rooms delete <room> [--purge]
21
+ media delete <mxc://server/id> | media purge <days>
22
+ notice <user> <message> Send a server notice
23
+ audit [user] [limit] Recent audit log entries
24
+ reports list | reports resolve <id> Content reports
25
+ server stats
26
+ help`;
27
+ /** Split a command line into arguments, honouring quotes. */
28
+ function parseArgs(line) {
29
+ const out = [];
30
+ for (const m of line.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)) out.push(m[1] ?? m[2] ?? m[3]);
31
+ return out;
32
+ }
33
+ /**
34
+ * A tuwunel-style admin room: server admins chat with an admin bot to manage the
35
+ * server. The first registered account becomes an admin and is put in the room.
36
+ */
37
+ var AdminRoomService = class {
38
+ hs;
39
+ options;
40
+ roomIdCache = null;
41
+ constructor(hs, options) {
42
+ this.hs = hs;
43
+ this.options = {
44
+ enabled: options.enabled ?? true,
45
+ firstUserIsAdmin: options.firstUserIsAdmin ?? true,
46
+ firstUserCanRegister: options.firstUserCanRegister ?? true,
47
+ botLocalpart: options.botLocalpart ?? "admin",
48
+ botDisplayName: options.botDisplayName ?? "Admin",
49
+ roomName: options.roomName ?? "Admin Room",
50
+ aliasLocalpart: options.aliasLocalpart ?? "admins"
51
+ };
52
+ }
53
+ get botUserId() {
54
+ return `@${this.options.botLocalpart}:${this.hs.serverName}`;
55
+ }
56
+ get alias() {
57
+ return `#${this.options.aliasLocalpart}:${this.hs.serverName}`;
58
+ }
59
+ /** Whether no real (non-bot) account exists yet. */
60
+ async noUsersYet() {
61
+ return (await this.hs.db.findMany("users", { limit: 2 })).every((u) => u.user_id === this.botUserId);
62
+ }
63
+ async roomId() {
64
+ if (this.roomIdCache) return this.roomIdCache;
65
+ const row = await this.hs.db.findOne("room_aliases", [{
66
+ field: "alias",
67
+ value: this.alias
68
+ }]);
69
+ this.roomIdCache = row?.room_id ?? null;
70
+ return this.roomIdCache;
71
+ }
72
+ /** Create the admin bot and room if needed. */
73
+ async ensureRoom() {
74
+ return this.hs.cluster.lock("adminroom", async () => {
75
+ const existing = await this.roomId();
76
+ if (existing) return existing;
77
+ if (!await this.hs.db.findOne("users", [{
78
+ field: "user_id",
79
+ value: this.botUserId
80
+ }])) await createUser(this.hs, this.options.botLocalpart, {
81
+ displayname: this.options.botDisplayName,
82
+ admin: true
83
+ });
84
+ const { room_id } = await this.hs.rooms.createRoom(this.botUserId, {
85
+ preset: "private_chat",
86
+ name: this.options.roomName,
87
+ topic: "Server administration. Type 'help' for commands.",
88
+ room_alias_name: this.options.aliasLocalpart,
89
+ power_level_content_override: {
90
+ users: { [this.botUserId]: 100 },
91
+ events_default: 0,
92
+ invite: 100,
93
+ kick: 100,
94
+ ban: 100
95
+ },
96
+ initial_state: [{
97
+ type: M.ROOM.HISTORY_VISIBILITY,
98
+ state_key: "",
99
+ content: { history_visibility: "joined" }
100
+ }]
101
+ });
102
+ this.roomIdCache = room_id;
103
+ return room_id;
104
+ });
105
+ }
106
+ /** Called after an account was created. */
107
+ async onUserCreated(userId) {
108
+ if (!this.options.enabled || userId === this.botUserId || !this.options.firstUserIsAdmin) return;
109
+ if (!await this.hs.cluster.lock("first-user", async () => {
110
+ return (await this.hs.db.findMany("users", {
111
+ sortBy: {
112
+ field: "created_ts",
113
+ direction: "asc"
114
+ },
115
+ limit: 3
116
+ })).filter((u) => u.user_id !== this.botUserId)[0]?.user_id === userId;
117
+ })) return;
118
+ await this.hs.db.update("users", [{
119
+ field: "user_id",
120
+ value: userId
121
+ }], { is_admin: true });
122
+ await this.hs.auth.invalidateUser(userId);
123
+ await this.addAdmin(userId);
124
+ await this.say(`Welcome ${userId}! As the first user you are a server admin. Type \`help\` for the available commands.`);
125
+ }
126
+ /** Put an admin into the admin room. */
127
+ async addAdmin(userId) {
128
+ if (!this.options.enabled || userId === this.botUserId) return;
129
+ const roomId = await this.ensureRoom();
130
+ const membership = await this.hs.rooms.membership(roomId, userId);
131
+ if (membership === "join") return;
132
+ if (membership !== "invite") await this.hs.rooms.invite(roomId, this.botUserId, userId);
133
+ await this.hs.rooms.join(userId, roomId);
134
+ }
135
+ /** Remove a former admin from the admin room. */
136
+ async removeAdmin(userId) {
137
+ const roomId = await this.roomId();
138
+ if (!roomId || userId === this.botUserId) return;
139
+ const membership = await this.hs.rooms.membership(roomId, userId);
140
+ if (membership === "join" || membership === "invite") await this.hs.rooms.setMembership(roomId, this.botUserId, userId, "leave", "No longer a server admin");
141
+ }
142
+ /** Post a message from the admin bot. */
143
+ async say(text, replyTo) {
144
+ const roomId = await this.roomId();
145
+ if (!roomId) return;
146
+ const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
147
+ const content = {
148
+ msgtype: M.NOTICE,
149
+ body: text,
150
+ format: UNSTABLE.ORG.MATRIX.CUSTOM.HTML,
151
+ formatted_body: text.includes("\n") ? `<pre><code>${escaped}</code></pre>` : escaped.replace(/`([^`]+)`/g, "<code>$1</code>")
152
+ };
153
+ if (replyTo) content[M.RELATES_TO] = { [M.IN_REPLY_TO]: { event_id: replyTo } };
154
+ await this.hs.rooms.sendEvent(roomId, this.botUserId, {
155
+ type: M.ROOM.MESSAGE,
156
+ content
157
+ });
158
+ }
159
+ /** Called for every new event; handles commands posted in the admin room. */
160
+ async onEvent(event) {
161
+ if (!this.options.enabled || event.type !== M.ROOM.MESSAGE || event.sender === this.botUserId) return;
162
+ if (event.room_id !== await this.roomId()) return;
163
+ const body = typeof event.content?.body === "string" ? event.content.body.trim() : "";
164
+ if (!body || event.content?.msgtype === M.NOTICE) return;
165
+ if (!(await this.hs.db.findOne("users", [{
166
+ field: "user_id",
167
+ value: event.sender
168
+ }]))?.is_admin) return;
169
+ const line = body.replace(/^!admin\s*/i, "").replace(new RegExp(`^${this.botUserId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:?\\s*`), "");
170
+ let reply;
171
+ try {
172
+ reply = await this.execute(event.sender, parseArgs(line));
173
+ } catch (err) {
174
+ reply = `Error: ${err instanceof MatrixError ? err.message : err instanceof Error ? err.message : String(err)}`;
175
+ }
176
+ await this.hs.audit.record({
177
+ action: "admin_command",
178
+ actor: event.sender,
179
+ details: { command: line.split(/\s+/).slice(0, 2).join(" ") }
180
+ });
181
+ await this.say(reply, event.event_id);
182
+ }
183
+ resolveUser(arg) {
184
+ if (!arg) throw new Error("Missing user");
185
+ const id = arg.startsWith("@") ? arg : `@${arg.toLowerCase()}:${this.hs.serverName}`;
186
+ if (!isValidUserId(id)) throw new Error(`Invalid user ID: ${arg}`);
187
+ return id;
188
+ }
189
+ async resolveRoom(arg) {
190
+ if (!arg) throw new Error("Missing room");
191
+ if (arg.startsWith("#")) return this.hs.rooms.resolveRoomIdOrAlias(arg);
192
+ if (!isValidRoomId(arg)) throw new Error(`Invalid room: ${arg}`);
193
+ return arg;
194
+ }
195
+ /** Run one admin command; returns the reply text. */
196
+ async execute(actor, args) {
197
+ const hs = this.hs;
198
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
199
+ const pos = args.filter((a) => !a.startsWith("--"));
200
+ const [group, sub, a1, a2] = pos;
201
+ const time = (ts) => ts ? new Date(ts).toISOString() : "never";
202
+ switch (group?.toLowerCase()) {
203
+ case void 0:
204
+ case "help": return HELP;
205
+ case "users":
206
+ switch (sub) {
207
+ case "list": {
208
+ const { users, total } = await hs.admin.listUsers({
209
+ includeDeactivated: flags.has("--all"),
210
+ limit: 200
211
+ });
212
+ return `${total} account(s):\n${users.map((u) => `${u.user_id}${u.is_admin ? " [admin]" : ""}${u.deactivated ? " [deactivated]" : ""}${u.locked ? " [locked]" : ""}${u.suspended ? " [suspended]" : ""}`).join("\n")}`;
213
+ }
214
+ case "info": {
215
+ const user = await hs.admin.getUser(this.resolveUser(a1));
216
+ if (!user) return "No such user";
217
+ return Object.entries(user).map(([k, v]) => `${k}: ${k.endsWith("_ts") ? time(v) : v}`).join("\n");
218
+ }
219
+ case "whois": {
220
+ const userId = this.resolveUser(a1);
221
+ const devices = await hs.db.findMany("devices", { where: [{
222
+ field: "user_id",
223
+ value: userId
224
+ }] });
225
+ if (!devices.length) return `${userId} has no devices`;
226
+ return devices.map((d) => `${d.device_id} ${d.display_name ?? ""}\n last seen ${time(d.last_seen_ts)} from ${d.last_seen_ip ?? "unknown"}`).join("\n");
227
+ }
228
+ case "create": {
229
+ if (!a1) return "Usage: users create <localpart> [password] [--admin]";
230
+ const password = a2 ?? randomString(16);
231
+ const userId = await hs.admin.createUser(a1, {
232
+ password,
233
+ admin: flags.has("--admin")
234
+ });
235
+ if (flags.has("--admin")) await this.addAdmin(userId);
236
+ await hs.audit.record({
237
+ action: "admin_action",
238
+ actor,
239
+ target: userId,
240
+ details: {
241
+ op: "create_user",
242
+ admin: flags.has("--admin")
243
+ }
244
+ });
245
+ return `Created ${userId}${a2 ? "" : ` with password: ${password}`}`;
246
+ }
247
+ case "reset-password": {
248
+ const userId = this.resolveUser(a1);
249
+ const password = a2 ?? randomString(16);
250
+ await hs.admin.resetPassword(userId, password, true);
251
+ await hs.audit.record({
252
+ action: "admin_action",
253
+ actor,
254
+ target: userId,
255
+ details: { op: "reset_password" }
256
+ });
257
+ return `Password for ${userId} reset${a2 ? "" : ` to: ${password}`}. All devices were logged out.`;
258
+ }
259
+ case "deactivate": {
260
+ const userId = this.resolveUser(a1);
261
+ if (userId === actor) return "You cannot deactivate yourself here";
262
+ await hs.admin.deactivateUser(userId, { erase: !flags.has("--keep-profile") });
263
+ await hs.audit.record({
264
+ action: "admin_action",
265
+ actor,
266
+ target: userId,
267
+ details: { op: "deactivate" }
268
+ });
269
+ return `Deactivated ${userId}`;
270
+ }
271
+ case "make-admin":
272
+ case "revoke-admin": {
273
+ const userId = this.resolveUser(a1);
274
+ if (sub === "revoke-admin" && userId === actor) return "You cannot revoke your own admin rights";
275
+ await hs.admin.setAdmin(userId, sub === "make-admin");
276
+ await hs.audit.record({
277
+ action: "admin_action",
278
+ actor,
279
+ target: userId,
280
+ details: { op: sub }
281
+ });
282
+ return `${userId} is ${sub === "make-admin" ? "now" : "no longer"} a server admin`;
283
+ }
284
+ case "lock":
285
+ case "unlock": {
286
+ const userId = this.resolveUser(a1);
287
+ if (userId === actor) return "You cannot lock yourself";
288
+ await hs.admin.setLocked(userId, sub === "lock", actor);
289
+ return `${userId} is ${sub === "lock" ? "now" : "no longer"} locked`;
290
+ }
291
+ case "suspend":
292
+ case "unsuspend": {
293
+ const userId = this.resolveUser(a1);
294
+ if (userId === actor) return "You cannot suspend yourself";
295
+ await hs.admin.setSuspended(userId, sub === "suspend", actor);
296
+ return `${userId} is ${sub === "suspend" ? "now" : "no longer"} suspended`;
297
+ }
298
+ }
299
+ return "Usage: users list|info|whois|create|reset-password|deactivate|make-admin|revoke-admin|lock|unlock|suspend|unsuspend";
300
+ case "tokens":
301
+ switch (sub) {
302
+ case "list": {
303
+ const tokens = await hs.admin.listRegistrationTokens();
304
+ if (!tokens.length) return "No registration tokens";
305
+ return tokens.map((t) => `${t.token} used ${t.completed}/${t.uses_allowed ?? "∞"} expires ${time(t.expiry_time)}`).join("\n");
306
+ }
307
+ case "create": {
308
+ const uses = a1 ? Number(a1) : null;
309
+ const days = a2 ? Number(a2) : null;
310
+ const t = await hs.admin.createRegistrationToken({
311
+ usesAllowed: uses,
312
+ expiryTime: days ? Date.now() + days * 864e5 : null
313
+ });
314
+ await hs.audit.record({
315
+ action: "registration_token_create",
316
+ actor,
317
+ target: t.token
318
+ });
319
+ return `Registration token: ${t.token}${uses ? ` (${uses} uses)` : ""}${days ? ` (expires in ${days} days)` : ""}`;
320
+ }
321
+ case "delete":
322
+ if (!a1) return "Usage: tokens delete <token>";
323
+ await hs.admin.deleteRegistrationToken(a1);
324
+ await hs.audit.record({
325
+ action: "registration_token_delete",
326
+ actor,
327
+ target: a1
328
+ });
329
+ return `Deleted token ${a1}`;
330
+ }
331
+ return "Usage: tokens list|create [uses] [days]|delete <token>";
332
+ case "registration": {
333
+ if (!sub || sub === "status") return `Registration is ${await hs.registrationMode()}`;
334
+ if (![
335
+ "open",
336
+ "close",
337
+ "closed",
338
+ "token"
339
+ ].includes(sub)) return "Usage: registration status|open|close|token";
340
+ const mode = sub === "close" ? "closed" : sub;
341
+ await hs.settings.set("registration", mode);
342
+ await hs.audit.record({
343
+ action: "registration_setting",
344
+ actor,
345
+ details: { mode }
346
+ });
347
+ return `Registration is now ${mode}`;
348
+ }
349
+ case "rooms":
350
+ switch (sub) {
351
+ case "list": {
352
+ const { rooms, total } = await hs.admin.listRooms({ limit: 200 });
353
+ return `${total} room(s):\n${(await Promise.all(rooms.map(async (r) => {
354
+ const name = (await hs.rooms.currentState(r.room_id)).get("m.room.name\0")?.content?.name;
355
+ return `${r.room_id} ${r.joined_members} member(s)${name ? ` "${name}"` : ""}${r.is_public ? " [public]" : ""}`;
356
+ }))).join("\n")}`;
357
+ }
358
+ case "info": {
359
+ const roomId = await this.resolveRoom(a1);
360
+ const state = await hs.rooms.currentState(roomId);
361
+ const members = await hs.rooms.joinedMembers(roomId);
362
+ return [
363
+ `Room ${roomId} (version ${await hs.rooms.roomVersion(roomId)})`,
364
+ `Name: ${state.get("m.room.name\0")?.content?.name ?? "-"}`,
365
+ `Creator: ${state.get("m.room.create\0")?.sender}`,
366
+ `Join rule: ${state.get("m.room.join_rules\0")?.content?.join_rule ?? "invite"}`,
367
+ `Members (${members.length}): ${members.slice(0, 50).join(", ")}${members.length > 50 ? ", …" : ""}`
368
+ ].join("\n");
369
+ }
370
+ case "delete": {
371
+ const roomId = await this.resolveRoom(a1);
372
+ if (roomId === await this.roomId()) return "Refusing to delete the admin room";
373
+ const { kicked } = await hs.admin.deleteRoom(roomId, {
374
+ actor,
375
+ purge: flags.has("--purge"),
376
+ reason: "Room deleted by a server admin"
377
+ });
378
+ await hs.audit.record({
379
+ action: "room_delete",
380
+ actor,
381
+ target: roomId,
382
+ details: { purge: flags.has("--purge") }
383
+ });
384
+ return `Deleted ${roomId}; removed ${kicked.length} local member(s)${flags.has("--purge") ? " and purged its history" : ""}`;
385
+ }
386
+ }
387
+ return "Usage: rooms list|info <room>|delete <room> [--purge]";
388
+ case "media":
389
+ if (sub === "delete") {
390
+ const m = /^mxc:\/\/([^/]+)\/(.+)$/.exec(a1 ?? "");
391
+ if (!m || m[1] !== hs.serverName) return "Usage: media delete mxc://<this server>/<id>";
392
+ await hs.admin.deleteMedia(m[2]);
393
+ await hs.audit.record({
394
+ action: "admin_action",
395
+ actor,
396
+ target: a1,
397
+ details: { op: "delete_media" }
398
+ });
399
+ return `Deleted ${a1}`;
400
+ }
401
+ if (sub === "purge") {
402
+ const days = Number(a1);
403
+ if (!(days > 0)) return "Usage: media purge <days>";
404
+ const deleted = await hs.admin.purgeMediaBefore(Date.now() - days * 864e5);
405
+ await hs.audit.record({
406
+ action: "admin_action",
407
+ actor,
408
+ details: {
409
+ op: "purge_media",
410
+ days,
411
+ count: deleted.length
412
+ }
413
+ });
414
+ return `Deleted ${deleted.length} media file(s) older than ${days} day(s)`;
415
+ }
416
+ return "Usage: media delete <mxc>|purge <days>";
417
+ case "notice": {
418
+ const target = this.resolveUser(sub);
419
+ const message = pos.slice(2).join(" ");
420
+ if (!message) return "Usage: notice <user> <message>";
421
+ await hs.admin.sendServerNotice(target, {
422
+ msgtype: M.TEXT,
423
+ body: message
424
+ });
425
+ await hs.audit.record({
426
+ action: "admin_action",
427
+ actor,
428
+ target,
429
+ details: { op: "server_notice" }
430
+ });
431
+ return `Notice sent to ${target}`;
432
+ }
433
+ case "audit": {
434
+ if (!hs.audit.enabled) return "The audit log is disabled (enable it with the `audit` option)";
435
+ const user = sub && !/^\d+$/.test(sub) ? this.resolveUser(sub) : void 0;
436
+ const limit = Number(user ? a1 : sub) || 20;
437
+ const rows = await hs.audit.query({
438
+ actor: user,
439
+ limit
440
+ });
441
+ if (!rows.length) return "No audit entries";
442
+ return rows.map((r) => `${time(r.ts)} ${r.action}${r.outcome === "failure" ? " (failed)" : ""} by ${r.actor ?? "-"}${r.target ? ` → ${r.target}` : ""}${r.ip ? ` from ${r.ip}` : ""}`).join("\n");
443
+ }
444
+ case "reports": {
445
+ if (sub === "resolve") {
446
+ if (!await hs.db.update("event_reports", [{
447
+ field: "id",
448
+ value: Number(a1)
449
+ }], { resolved: true })) return "No such report";
450
+ return `Report ${a1} resolved`;
451
+ }
452
+ const rows = await hs.db.findMany("event_reports", {
453
+ where: [{
454
+ field: "resolved",
455
+ value: false
456
+ }],
457
+ sortBy: {
458
+ field: "id",
459
+ direction: "desc"
460
+ },
461
+ limit: 50
462
+ });
463
+ if (!rows.length) return "No open reports";
464
+ return rows.map((r) => `#${r.id} ${time(r.ts)} ${r.reporter} reported ${r.event_id ?? "the room"} in ${r.room_id}: ${r.reason ?? "(no reason)"}`).join("\n");
465
+ }
466
+ case "server": {
467
+ const users = await hs.db.findMany("users");
468
+ const rooms = await hs.db.findMany("rooms");
469
+ return [
470
+ `Server: ${hs.serverName}`,
471
+ `Users: ${users.filter((u) => !u.deactivated).length} active, ${users.filter((u) => u.deactivated).length} deactivated`,
472
+ `Rooms: ${rooms.length}`,
473
+ `Registration: ${await hs.registrationMode()}`,
474
+ `Federation: ${hs.federation.enabled ? "enabled" : "disabled"}`,
475
+ `Instance: ${hs.instanceId}`
476
+ ].join("\n");
477
+ }
478
+ }
479
+ return `Unknown command "${group}". Type help for the list of commands.`;
480
+ }
481
+ };
482
+ //#endregion
483
+ export { AdminRoomService, parseArgs };
@@ -0,0 +1,74 @@
1
+ import { PDU } from "./events.js";
2
+ import { StateMap } from "./authrules.js";
3
+ import { UNSTABLE } from "./m.js";
4
+ import { Homeserver } from "../homeserver.js";
5
+ //#region src/core/appservices.d.ts
6
+ export interface AppServiceNamespace {
7
+ exclusive: boolean;
8
+ regex: string;
9
+ }
10
+ /** An application service registration (bridges, bots, integrations). */
11
+ export interface AppServiceRegistration {
12
+ id: string;
13
+ /** Base URL of the application service, or null when it receives nothing. */
14
+ url: string | null;
15
+ as_token: string;
16
+ hs_token: string;
17
+ sender_localpart: string;
18
+ namespaces: {
19
+ users?: AppServiceNamespace[];
20
+ aliases?: AppServiceNamespace[];
21
+ rooms?: AppServiceNamespace[];
22
+ };
23
+ rate_limited?: boolean;
24
+ protocols?: string[];
25
+ /** MSC2409: also push ephemeral events (typing, receipts, presence). */
26
+ [UNSTABLE.DE.SORUNOME.MSC2409.PUSH_EPHEMERAL]?: boolean;
27
+ receive_ephemeral?: boolean;
28
+ }
29
+ /**
30
+ * Parse an application service registration file. Accepts JSON or the YAML subset
31
+ * used by registration files (nested mappings and lists of mappings, scalars).
32
+ */
33
+ export declare function parseAppServiceRegistration(text: string): AppServiceRegistration;
34
+ /** Load every *.yaml / *.yml / *.json registration in a directory. */
35
+ export declare function loadAppServiceRegistrations(directory: string): Promise<AppServiceRegistration[]>;
36
+ /** Application Service API: authentication, namespaces, event push and queries. */
37
+ export declare class AppServiceService {
38
+ private hs;
39
+ private services;
40
+ private timers;
41
+ constructor(hs: Homeserver, registrations: AppServiceRegistration[]);
42
+ get registrations(): AppServiceRegistration[];
43
+ byToken(token: string): AppServiceRegistration | null;
44
+ byId(id: string): AppServiceRegistration | null;
45
+ senderOf(reg: AppServiceRegistration): string;
46
+ private compiled;
47
+ /** Whether the AS owns this user ID (its sender or its user namespace). */
48
+ isInterestedUser(reg: AppServiceRegistration, userId: string): boolean;
49
+ /** An exclusive namespace owned by some AS (other than `except`) covers this user ID. */
50
+ exclusiveUserOwner(userId: string): AppServiceRegistration | null;
51
+ exclusiveAliasOwner(alias: string): AppServiceRegistration | null;
52
+ /** Resolve the user an AS request acts as (masquerading with `user_id`). */
53
+ masquerade(reg: AppServiceRegistration, userIdParam: string | null): Promise<string>;
54
+ private interestedInEvent;
55
+ /** Queue an event for every interested application service. */
56
+ onEvent(pdu: PDU, state: StateMap): Promise<void>;
57
+ /** Queue an ephemeral event (typing/receipt/presence) for interested services that asked for them. */
58
+ onEphemeral(roomId: string | null, event: Record<string, any>, users: string[]): Promise<void>;
59
+ private enqueue;
60
+ private schedule;
61
+ private retries;
62
+ private flush;
63
+ start(): void;
64
+ stop(): void;
65
+ private query;
66
+ /** Ask the owning AS whether a user exists (it may create it). */
67
+ queryUser(userId: string): Promise<boolean>;
68
+ /** Ask the owning AS about an alias; returns the room ID if it now exists. */
69
+ queryAlias(alias: string): Promise<string | null>;
70
+ ping(reg: AppServiceRegistration, transactionId?: string): Promise<number>;
71
+ thirdParty(path: string, query: URLSearchParams): Promise<unknown[]>;
72
+ protocols(): Promise<Record<string, unknown>>;
73
+ }
74
+ //#endregion