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.
Files changed (120) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +457 -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 +560 -0
  12. package/dist/api/admin.js +251 -0
  13. package/dist/api/appservice.js +33 -0
  14. package/dist/api/backup.js +269 -0
  15. package/dist/api/federation.js +792 -0
  16. package/dist/api/filters.js +87 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +170 -0
  19. package/dist/api/media.js +255 -0
  20. package/dist/api/misc.js +86 -0
  21. package/dist/api/preview.js +200 -0
  22. package/dist/api/profile.js +165 -0
  23. package/dist/api/push.js +187 -0
  24. package/dist/api/relationships.js +264 -0
  25. package/dist/api/rooms.js +1364 -0
  26. package/dist/api/rtc.d.ts +24 -0
  27. package/dist/api/rtc.js +52 -0
  28. package/dist/api/search.js +155 -0
  29. package/dist/api/slidingsync.js +464 -0
  30. package/dist/api/spaces.js +172 -0
  31. package/dist/api/sync.js +600 -0
  32. package/dist/api/threadsubs.js +129 -0
  33. package/dist/api/threepid.js +122 -0
  34. package/dist/api/uia.js +126 -0
  35. package/dist/api/user.js +286 -0
  36. package/dist/core/admin.d.ts +85 -0
  37. package/dist/core/admin.js +307 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +482 -0
  40. package/dist/core/appservices.d.ts +73 -0
  41. package/dist/core/appservices.js +368 -0
  42. package/dist/core/audit.d.ts +51 -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 +347 -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 +159 -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 +185 -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 +35 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +449 -0
  66. package/dist/core/notifier.d.ts +17 -0
  67. package/dist/core/notifier.js +54 -0
  68. package/dist/core/oauth.d.ts +38 -0
  69. package/dist/core/oauth.js +108 -0
  70. package/dist/core/partialstate.d.ts +32 -0
  71. package/dist/core/partialstate.js +197 -0
  72. package/dist/core/phone.js +268 -0
  73. package/dist/core/policy.d.ts +29 -0
  74. package/dist/core/policy.js +96 -0
  75. package/dist/core/profile.js +39 -0
  76. package/dist/core/push.d.ts +47 -0
  77. package/dist/core/push.js +306 -0
  78. package/dist/core/pushrules.d.ts +1 -0
  79. package/dist/core/pushrules.js +228 -0
  80. package/dist/core/ratelimit.d.ts +23 -0
  81. package/dist/core/ratelimit.js +77 -0
  82. package/dist/core/rooms.d.ts +215 -0
  83. package/dist/core/rooms.js +1729 -0
  84. package/dist/core/settings.d.ts +11 -0
  85. package/dist/core/settings.js +29 -0
  86. package/dist/core/sso.d.ts +46 -0
  87. package/dist/core/sso.js +165 -0
  88. package/dist/core/stateres.d.ts +4 -0
  89. package/dist/core/stateres.js +208 -0
  90. package/dist/core/threepid.d.ts +80 -0
  91. package/dist/core/threepid.js +275 -0
  92. package/dist/core/typing.d.ts +16 -0
  93. package/dist/core/typing.js +60 -0
  94. package/dist/federation/client.d.ts +51 -0
  95. package/dist/federation/client.js +278 -0
  96. package/dist/federation/index.d.ts +153 -0
  97. package/dist/federation/index.js +1058 -0
  98. package/dist/federation/keyring.d.ts +26 -0
  99. package/dist/federation/keyring.js +122 -0
  100. package/dist/federation/sender.d.ts +26 -0
  101. package/dist/federation/sender.js +164 -0
  102. package/dist/homeserver.d.ts +223 -0
  103. package/dist/homeserver.js +301 -0
  104. package/dist/http/adapters.d.ts +40 -0
  105. package/dist/http/adapters.js +130 -0
  106. package/dist/http/handler.d.ts +8 -0
  107. package/dist/http/handler.js +202 -0
  108. package/dist/http/input.js +24 -0
  109. package/dist/http/router.d.ts +60 -0
  110. package/dist/http/router.js +76 -0
  111. package/dist/index.d.ts +25 -0
  112. package/dist/index.js +14 -0
  113. package/dist/storage/memory.d.ts +5 -0
  114. package/dist/storage/memory.js +122 -0
  115. package/dist/storage/schema.d.ts +4 -0
  116. package/dist/storage/schema.js +664 -0
  117. package/dist/storage/sql.d.ts +72 -0
  118. package/dist/storage/sql.js +274 -0
  119. package/dist/storage/types.d.ts +55 -0
  120. package/package.json +67 -4
@@ -0,0 +1,87 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { errors } from "../core/errors.js";
3
+ import { globMatch as globMatch$1 } from "../core/glob.js";
4
+ //#region src/api/filters.ts
5
+ function globMatch(pattern, value) {
6
+ return globMatch$1(pattern, value, { questionMark: false });
7
+ }
8
+ function matchesFilter(filter, ev) {
9
+ if (!filter) return true;
10
+ if (filter.not_types?.some((t) => globMatch(t, ev.type))) return false;
11
+ if (filter.types && !filter.types.some((t) => globMatch(t, ev.type))) return false;
12
+ if (ev.sender !== void 0) {
13
+ if (filter.not_senders?.includes(ev.sender)) return false;
14
+ if (filter.senders && !filter.senders.includes(ev.sender)) return false;
15
+ }
16
+ if (ev.room_id !== void 0) {
17
+ if (filter.not_rooms?.includes(ev.room_id)) return false;
18
+ if (filter.rooms && !filter.rooms.includes(ev.room_id)) return false;
19
+ }
20
+ const relTypes = filter.rel_types ?? filter["org.matrix.msc3874.rel_types"];
21
+ const notRelTypes = filter.not_rel_types ?? filter["org.matrix.msc3874.not_rel_types"];
22
+ if (relTypes || notRelTypes) {
23
+ const relType = ev.content?.["m.relates_to"]?.rel_type;
24
+ if (relTypes && !relTypes.includes(relType)) return false;
25
+ if (notRelTypes && typeof relType === "string" && notRelTypes.includes(relType)) return false;
26
+ }
27
+ if (filter.contains_url !== void 0) {
28
+ if (typeof ev.content?.url === "string" !== filter.contains_url) return false;
29
+ }
30
+ return true;
31
+ }
32
+ function roomAllowed(filter, roomId) {
33
+ const r = filter.room;
34
+ if (!r) return true;
35
+ if (r.not_rooms?.includes(roomId)) return false;
36
+ if (r.rooms && !r.rooms.includes(roomId)) return false;
37
+ return true;
38
+ }
39
+ function validateEventFilter(f, name) {
40
+ if (f === void 0) return;
41
+ if (!isObject(f)) throw errors.badJson(`${name} must be an object`);
42
+ for (const key of [
43
+ "types",
44
+ "not_types",
45
+ "senders",
46
+ "not_senders",
47
+ "rooms",
48
+ "not_rooms"
49
+ ]) if (f[key] !== void 0 && (!Array.isArray(f[key]) || !f[key].every((x) => typeof x === "string"))) throw errors.badJson(`${name}.${key} must be a list of strings`);
50
+ if (f.rooms?.some((r) => !r.startsWith("!")) || f.not_rooms?.some((r) => !r.startsWith("!"))) throw errors.badJson(`${name}: rooms must be room IDs`);
51
+ if (f.senders?.some((u) => !u.startsWith("@")) || f.not_senders?.some((u) => !u.startsWith("@"))) throw errors.badJson(`${name}: senders must be user IDs`);
52
+ if (f.limit !== void 0 && (!Number.isInteger(f.limit) || f.limit < 0)) throw errors.badJson(`${name}.limit must be a non-negative integer`);
53
+ }
54
+ function validateSyncFilter(f) {
55
+ if (!isObject(f)) throw errors.badJson("Filter must be an object");
56
+ for (const k of ["presence", "account_data"]) validateEventFilter(f[k], k);
57
+ if (f.room !== void 0) {
58
+ if (!isObject(f.room)) throw errors.badJson("room must be an object");
59
+ for (const k of [
60
+ "timeline",
61
+ "state",
62
+ "ephemeral",
63
+ "account_data"
64
+ ]) validateEventFilter(f.room[k], `room.${k}`);
65
+ validateEventFilter({
66
+ rooms: f.room.rooms,
67
+ not_rooms: f.room.not_rooms
68
+ }, "room");
69
+ }
70
+ if (f.event_format !== void 0 && !["client", "federation"].includes(f.event_format)) throw errors.badJson("Invalid event_format");
71
+ return f;
72
+ }
73
+ /** Parse the `filter` query param of /messages and /context (a JSON RoomEventFilter). */
74
+ function parseRoomEventFilter(ctx) {
75
+ const raw = ctx.query.get("filter");
76
+ if (!raw) return void 0;
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(raw);
80
+ } catch {
81
+ throw errors.badJson("Invalid filter JSON");
82
+ }
83
+ validateEventFilter(parsed, "filter");
84
+ return parsed;
85
+ }
86
+ //#endregion
87
+ export { matchesFilter, parseRoomEventFilter, roomAllowed, validateSyncFilter };
@@ -0,0 +1,47 @@
1
+ import { registerThreadSubscriptionRoutes } from "./threadsubs.js";
2
+ import { registerAccountRoutes } from "./account.js";
3
+ import { registerBackupRoutes } from "./backup.js";
4
+ import { registerRoomRoutes } from "./rooms.js";
5
+ import { registerSearchRoutes } from "./search.js";
6
+ import { registerPreviewRoutes } from "./preview.js";
7
+ import { registerSpaceRoutes } from "./spaces.js";
8
+ import { registerAdminRoutes } from "./admin.js";
9
+ import { registerSyncRoutes } from "./sync.js";
10
+ import { registerSlidingSyncRoutes } from "./slidingsync.js";
11
+ import { registerRelationshipRoutes } from "./relationships.js";
12
+ import { registerFederationRoutes } from "./federation.js";
13
+ import { registerRtcRoutes } from "./rtc.js";
14
+ import { registerAppServiceRoutes } from "./appservice.js";
15
+ import { registerMediaRoutes } from "./media.js";
16
+ import { registerLegacyRoutes } from "./legacy.js";
17
+ import { registerThreepidRoutes } from "./threepid.js";
18
+ import { registerMiscRoutes } from "./misc.js";
19
+ import { registerProfileRoutes } from "./profile.js";
20
+ import { registerPushRoutes } from "./push.js";
21
+ import { registerUserRoutes } from "./user.js";
22
+ //#region src/api/index.ts
23
+ function registerRoutes(r) {
24
+ registerMiscRoutes(r);
25
+ registerAccountRoutes(r);
26
+ registerProfileRoutes(r);
27
+ registerRoomRoutes(r);
28
+ registerLegacyRoutes(r);
29
+ registerThreepidRoutes(r);
30
+ registerSyncRoutes(r);
31
+ registerUserRoutes(r);
32
+ registerPushRoutes(r);
33
+ registerMediaRoutes(r);
34
+ registerSearchRoutes(r);
35
+ registerBackupRoutes(r);
36
+ registerPreviewRoutes(r);
37
+ registerSpaceRoutes(r);
38
+ registerAdminRoutes(r);
39
+ registerSlidingSyncRoutes(r);
40
+ registerFederationRoutes(r);
41
+ registerRtcRoutes(r);
42
+ registerAppServiceRoutes(r);
43
+ registerThreadSubscriptionRoutes(r);
44
+ registerRelationshipRoutes(r);
45
+ }
46
+ //#endregion
47
+ export { registerRoutes };
@@ -0,0 +1,170 @@
1
+ import { toClientEvent } from "../core/events.js";
2
+ import { errors } from "../core/errors.js";
3
+ import { isValidRoomId } from "../core/ids.js";
4
+ import { paginate, visibleState } from "./rooms.js";
5
+ //#region src/api/legacy.ts
6
+ const viewerOf = (s) => ({
7
+ userId: s.userId,
8
+ deviceId: s.deviceId
9
+ });
10
+ const parseStreamToken = (raw, fallback) => {
11
+ if (raw === null) return fallback;
12
+ const m = /^s(\d+)/.exec(raw);
13
+ if (!m) throw errors.invalidParam("Invalid from token");
14
+ return Number(m[1]);
15
+ };
16
+ async function roomAccountData(hs, userId, roomId) {
17
+ return (await hs.db.findMany("account_data", { where: [{
18
+ field: "user_id",
19
+ value: userId
20
+ }, {
21
+ field: "room_id",
22
+ value: roomId
23
+ }] })).map((r) => ({
24
+ type: r.type,
25
+ content: r.content
26
+ }));
27
+ }
28
+ /** Current state and latest messages of a room the user may see (also for peeking). */
29
+ async function roomInfo(hs, s, roomId, limit) {
30
+ const state = await visibleState(hs, roomId, s.userId);
31
+ const membership = (await hs.db.findOne("memberships", [{
32
+ field: "room_id",
33
+ value: roomId
34
+ }, {
35
+ field: "user_id",
36
+ value: s.userId
37
+ }]))?.membership ?? void 0;
38
+ const position = await hs.streams.position();
39
+ const { events, end } = await paginate(hs, s.userId, roomId, {
40
+ from: {
41
+ depth: null,
42
+ stream: position
43
+ },
44
+ to: null,
45
+ dir: "b",
46
+ limit
47
+ });
48
+ const chunk = events.reverse();
49
+ const room = await hs.db.findOne("rooms", [{
50
+ field: "room_id",
51
+ value: roomId
52
+ }]);
53
+ return {
54
+ room_id: roomId,
55
+ ...membership ? { membership } : {},
56
+ messages: {
57
+ chunk: chunk.map((e) => hs.rooms.formatEvent(e, viewerOf(s))),
58
+ start: end ?? (chunk.length ? `t${chunk[0].pdu.depth}_${chunk[0].stream}` : `s${position}`),
59
+ end: `s${position}`
60
+ },
61
+ state: [...state.values()].map((e) => toClientEvent(e)),
62
+ visibility: room?.is_public ? "public" : "private",
63
+ account_data: membership ? await roomAccountData(hs, s.userId, roomId) : [],
64
+ presence: [],
65
+ receipts: []
66
+ };
67
+ }
68
+ /** Events after `from` in the given rooms, oldest first. */
69
+ async function eventsSince(hs, s, roomIds, from, limit) {
70
+ const all = [];
71
+ for (const roomId of roomIds) {
72
+ const { events } = await paginate(hs, s.userId, roomId, {
73
+ from: {
74
+ depth: null,
75
+ stream: from
76
+ },
77
+ to: null,
78
+ dir: "f",
79
+ limit
80
+ });
81
+ all.push(...events);
82
+ }
83
+ return all.sort((a, b) => a.stream - b.stream).slice(0, limit);
84
+ }
85
+ function registerLegacyRoutes(r) {
86
+ r.client("GET", "/initialSync", async (ctx) => {
87
+ const s = await ctx.auth();
88
+ const hs = ctx.hs;
89
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 0), 100);
90
+ const memberships = [
91
+ "join",
92
+ "invite",
93
+ ...ctx.query.get("archived") === "true" ? ["leave", "ban"] : []
94
+ ];
95
+ const rooms = [];
96
+ for (const membership of memberships) for (const roomId of await hs.rooms.roomsForUser(s.userId, membership)) {
97
+ if (membership === "invite") {
98
+ const invite = await hs.rooms.membershipEvent(roomId, s.userId);
99
+ rooms.push({
100
+ room_id: roomId,
101
+ membership,
102
+ ...invite ? { invite: hs.rooms.formatEvent(invite, viewerOf(s)) } : {}
103
+ });
104
+ continue;
105
+ }
106
+ try {
107
+ rooms.push(await roomInfo(hs, s, roomId, limit));
108
+ } catch {}
109
+ }
110
+ const account = await hs.db.findMany("account_data", { where: [{
111
+ field: "user_id",
112
+ value: s.userId
113
+ }, {
114
+ field: "room_id",
115
+ value: ""
116
+ }] });
117
+ return {
118
+ end: `s${await hs.streams.position()}`,
119
+ rooms,
120
+ presence: [],
121
+ account_data: account.map((a) => ({
122
+ type: a.type,
123
+ content: a.content
124
+ }))
125
+ };
126
+ });
127
+ r.client("GET", "/rooms/:roomId/initialSync", async (ctx) => {
128
+ const s = await ctx.auth();
129
+ if (!isValidRoomId(ctx.params.roomId)) throw errors.invalidParam("Invalid room ID");
130
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 0), 100);
131
+ return roomInfo(ctx.hs, s, ctx.params.roomId, limit);
132
+ });
133
+ r.client("GET", "/events/:eventId", async (ctx) => {
134
+ const s = await ctx.auth();
135
+ const ev = await ctx.hs.rooms.getEvent(ctx.params.eventId);
136
+ if (!ev || !(await ctx.hs.rooms.filterVisible(s.userId, ev.pdu.room_id, [ev])).length) throw errors.notFound("Event not found");
137
+ return ctx.hs.rooms.formatEvent(ev, viewerOf(s), { includeRoomId: true });
138
+ });
139
+ r.client("GET", "/events", async (ctx) => {
140
+ const s = await ctx.auth();
141
+ const hs = ctx.hs;
142
+ const now = await hs.streams.position();
143
+ const from = parseStreamToken(ctx.query.get("from"), now);
144
+ const timeout = Math.min(Math.max(Number(ctx.query.get("timeout") ?? 3e4) || 0, 0), hs.config.maxSyncTimeoutMs);
145
+ const peekRoom = ctx.query.get("room_id");
146
+ let roomIds;
147
+ if (peekRoom !== null) {
148
+ if (!isValidRoomId(peekRoom)) throw errors.invalidParam("Invalid room ID");
149
+ const { joined, worldReadable } = await hs.rooms.canPeek(peekRoom, s.userId);
150
+ if (!joined && !worldReadable) throw errors.forbidden("You cannot peek into this room");
151
+ roomIds = [peekRoom];
152
+ } else roomIds = await hs.rooms.roomsForUser(s.userId);
153
+ const deadline = Date.now() + timeout;
154
+ for (;;) {
155
+ const events = await eventsSince(hs, s, roomIds, from, 100);
156
+ const remaining = deadline - Date.now();
157
+ if (events.length || remaining <= 0 || ctx.request.signal.aborted) {
158
+ const end = events.length ? events[events.length - 1].stream : Math.max(from, await hs.streams.position());
159
+ return {
160
+ start: `s${from}`,
161
+ end: `s${end}`,
162
+ chunk: events.map((e) => hs.rooms.formatEvent(e, viewerOf(s), { includeRoomId: true }))
163
+ };
164
+ }
165
+ await Promise.race([hs.notifier.wait(s.userId, remaining, ctx.request.signal), new Promise((resolve) => setTimeout(resolve, Math.min(remaining, peekRoom !== null ? 1e3 : remaining)).unref?.())]);
166
+ }
167
+ });
168
+ }
169
+ //#endregion
170
+ export { registerLegacyRoutes };
@@ -0,0 +1,255 @@
1
+ import { randomString } from "../core/crypto.js";
2
+ import { MatrixError, errors } from "../core/errors.js";
3
+ //#region src/api/media.ts
4
+ const DEFAULT_MAX_UPLOAD = 52428800;
5
+ const INLINE_TYPES = /^(image\/(png|jpeg|gif|webp|avif|apng)|video\/(mp4|webm|ogg)|audio\/(mpeg|ogg|wav|webm|mp4|aac|flac)|text\/plain)$/;
6
+ const maxUpload = (hs) => hs.media.maxUploadSize ?? DEFAULT_MAX_UPLOAD;
7
+ async function storeUpload(ctx, mediaId) {
8
+ const hs = ctx.hs;
9
+ const data = await ctx.bytes();
10
+ if (data.length > maxUpload(hs)) throw errors.tooLarge("Upload too large");
11
+ const contentType = ctx.request.headers.get("content-type");
12
+ const filename = ctx.query.get("filename");
13
+ await hs.media.put(mediaId, data, {
14
+ contentType,
15
+ filename,
16
+ size: data.length
17
+ });
18
+ await hs.db.upsert("media", {
19
+ media_id: mediaId,
20
+ content_type: contentType,
21
+ filename,
22
+ size: data.length,
23
+ uploader: (await ctx.auth()).userId,
24
+ created_ts: Date.now(),
25
+ pending: false
26
+ });
27
+ }
28
+ function contentDisposition(contentType, filename) {
29
+ const type = INLINE_TYPES.test(contentType ?? "") ? "inline" : "attachment";
30
+ return filename ? `${type}; filename*=utf-8''${encodeURIComponent(filename)}` : type;
31
+ }
32
+ /** Fetch (and cache) media from another server. */
33
+ async function remoteMedia(ctx) {
34
+ const hs = ctx.hs;
35
+ const { serverName, mediaId } = ctx.params;
36
+ if (ctx.query.get("allow_remote") === "false") throw errors.notFound("Remote media not allowed");
37
+ const cached = await hs.db.findOne("remote_media", [{
38
+ field: "server_name",
39
+ value: serverName
40
+ }, {
41
+ field: "media_id",
42
+ value: mediaId
43
+ }]);
44
+ if (cached) {
45
+ const data = await hs.media.get(cached.local_id);
46
+ if (data) return {
47
+ data,
48
+ contentType: cached.content_type,
49
+ filename: cached.filename
50
+ };
51
+ }
52
+ let fetched;
53
+ try {
54
+ fetched = await hs.federation.fetchMedia(serverName, mediaId);
55
+ } catch (err) {
56
+ if (err instanceof MatrixError) throw err;
57
+ throw new MatrixError(502, "M_UNKNOWN", "Could not fetch remote media");
58
+ }
59
+ if (fetched.data.length > maxUpload(hs)) throw errors.tooLarge("Remote media too large");
60
+ const localId = `remote_${randomString(24)}`;
61
+ await hs.media.put(localId, fetched.data, {
62
+ contentType: fetched.contentType,
63
+ filename: fetched.filename,
64
+ size: fetched.data.length
65
+ });
66
+ await hs.db.upsert("remote_media", {
67
+ server_name: serverName,
68
+ media_id: mediaId,
69
+ local_id: localId,
70
+ content_type: fetched.contentType,
71
+ filename: fetched.filename,
72
+ size: fetched.data.length,
73
+ fetched_ts: Date.now()
74
+ });
75
+ return fetched;
76
+ }
77
+ async function download(ctx) {
78
+ const hs = ctx.hs;
79
+ const { serverName, mediaId } = ctx.params;
80
+ if (serverName !== hs.serverName) {
81
+ const remote = await remoteMedia(ctx);
82
+ const filename = ctx.params.fileName ?? remote.filename;
83
+ return new Response(remote.data, {
84
+ status: 200,
85
+ headers: {
86
+ "content-type": remote.contentType ?? "application/octet-stream",
87
+ "content-disposition": contentDisposition(remote.contentType, filename),
88
+ "content-security-policy": "sandbox; default-src 'none'; script-src 'none'; plugin-types application/pdf; style-src 'unsafe-inline'; media-src 'self'; object-src 'self';",
89
+ "cross-origin-resource-policy": "cross-origin",
90
+ "cache-control": "public, max-age=86400, immutable"
91
+ }
92
+ });
93
+ }
94
+ const meta = await hs.db.findOne("media", [{
95
+ field: "media_id",
96
+ value: mediaId
97
+ }]);
98
+ if (!meta) throw errors.notFound("Media not found");
99
+ if (meta.pending) throw new MatrixError(504, "M_NOT_YET_UPLOADED", "Media has not been uploaded yet");
100
+ const data = await hs.media.get(mediaId);
101
+ if (!data) throw errors.notFound("Media not found");
102
+ const filename = ctx.params.fileName ?? meta.filename;
103
+ return new Response(data, {
104
+ status: 200,
105
+ headers: {
106
+ "content-type": meta.content_type ?? "application/octet-stream",
107
+ "content-disposition": contentDisposition(meta.content_type, filename),
108
+ "content-security-policy": "sandbox; default-src 'none'; script-src 'none'; plugin-types application/pdf; style-src 'unsafe-inline'; media-src 'self'; object-src 'self';",
109
+ "cross-origin-resource-policy": "cross-origin",
110
+ "cache-control": "public, max-age=86400, immutable"
111
+ }
112
+ });
113
+ }
114
+ /** Serve a thumbnail, generating and caching it with the configured thumbnailer. */
115
+ async function thumbnail(ctx) {
116
+ const hs = ctx.hs;
117
+ const width = Number.parseInt(ctx.query.get("width") ?? "", 10);
118
+ const height = Number.parseInt(ctx.query.get("height") ?? "", 10);
119
+ if (!(width > 0) || !(height > 0)) throw errors.invalidParam("width and height must be positive integers");
120
+ const method = ctx.query.get("method") === "crop" ? "crop" : "scale";
121
+ const animated = ctx.query.get("animated") === "true";
122
+ const { serverName, mediaId } = ctx.params;
123
+ if (serverName !== hs.serverName || !hs.config.thumbnailer) return download(ctx);
124
+ const meta = await hs.db.findOne("media", [{
125
+ field: "media_id",
126
+ value: mediaId
127
+ }]);
128
+ if (!meta || meta.pending || !meta.content_type?.startsWith("image/")) return download(ctx);
129
+ const sizes = [
130
+ 32,
131
+ 96,
132
+ 320,
133
+ 640,
134
+ 800,
135
+ 1280
136
+ ];
137
+ const w = sizes.find((s) => s >= width) ?? 1280;
138
+ const h = sizes.find((s) => s >= height) ?? 1280;
139
+ const thumbId = `${mediaId}_thumb_${w}x${h}_${method}${animated ? "_a" : ""}`;
140
+ let thumb = await hs.media.get(thumbId);
141
+ let contentType = (await hs.db.findOne("media", [{
142
+ field: "media_id",
143
+ value: thumbId
144
+ }]))?.content_type ?? null;
145
+ if (!thumb || !contentType) {
146
+ const original = await hs.media.get(mediaId);
147
+ if (!original) throw errors.notFound("Media not found");
148
+ const made = await hs.config.thumbnailer(original, meta.content_type, {
149
+ width: w,
150
+ height: h,
151
+ method,
152
+ animated
153
+ }).catch((err) => {
154
+ hs.logger.warn("Thumbnailing failed", mediaId, err);
155
+ return null;
156
+ });
157
+ if (!made) return download(ctx);
158
+ thumb = made.data;
159
+ contentType = made.contentType;
160
+ await hs.media.put(thumbId, thumb, {
161
+ contentType,
162
+ filename: null,
163
+ size: thumb.length
164
+ });
165
+ await hs.db.upsert("media", {
166
+ media_id: thumbId,
167
+ content_type: contentType,
168
+ filename: null,
169
+ size: thumb.length,
170
+ uploader: "",
171
+ created_ts: Date.now(),
172
+ pending: false
173
+ });
174
+ }
175
+ return new Response(thumb, {
176
+ status: 200,
177
+ headers: {
178
+ "content-type": contentType,
179
+ "content-disposition": "inline",
180
+ "cross-origin-resource-policy": "cross-origin",
181
+ "cache-control": "public, max-age=86400, immutable"
182
+ }
183
+ });
184
+ }
185
+ function registerMediaRoutes(r) {
186
+ const config = (ctx) => ({ "m.upload.size": maxUpload(ctx.hs) });
187
+ r.get("/_matrix/media/v3/config", async (ctx) => {
188
+ await ctx.auth();
189
+ return config(ctx);
190
+ });
191
+ r.get("/_matrix/media/r0/config", async (ctx) => {
192
+ await ctx.auth();
193
+ return config(ctx);
194
+ });
195
+ r.get("/_matrix/client/v1/media/config", async (ctx) => {
196
+ await ctx.auth();
197
+ return config(ctx);
198
+ });
199
+ const upload = async (ctx) => {
200
+ await ctx.auth();
201
+ await ctx.rateLimit("upload");
202
+ const mediaId = randomString(24);
203
+ await storeUpload(ctx, mediaId);
204
+ return { content_uri: `mxc://${ctx.hs.serverName}/${mediaId}` };
205
+ };
206
+ r.post("/_matrix/media/v3/upload", upload);
207
+ r.post("/_matrix/media/r0/upload", upload);
208
+ r.post("/_matrix/media/v1/create", async (ctx) => {
209
+ const s = await ctx.auth();
210
+ const mediaId = randomString(24);
211
+ await ctx.hs.db.create("media", {
212
+ media_id: mediaId,
213
+ content_type: null,
214
+ filename: null,
215
+ size: 0,
216
+ uploader: s.userId,
217
+ created_ts: Date.now(),
218
+ pending: true
219
+ });
220
+ return {
221
+ content_uri: `mxc://${ctx.hs.serverName}/${mediaId}`,
222
+ unused_expires_at: Date.now() + 864e5
223
+ };
224
+ });
225
+ r.put("/_matrix/media/v3/upload/:serverName/:mediaId", async (ctx) => {
226
+ const s = await ctx.auth();
227
+ if (ctx.params.serverName !== ctx.hs.serverName) throw errors.notFound("Unknown media");
228
+ const meta = await ctx.hs.db.findOne("media", [{
229
+ field: "media_id",
230
+ value: ctx.params.mediaId
231
+ }]);
232
+ if (!meta) throw errors.notFound("Unknown media");
233
+ if (meta.uploader !== s.userId) throw errors.forbidden("You did not create this media ID");
234
+ if (!meta.pending) throw new MatrixError(409, "M_CANNOT_OVERWRITE_MEDIA", "Media already uploaded");
235
+ await storeUpload(ctx, ctx.params.mediaId);
236
+ return {};
237
+ });
238
+ for (const prefix of ["/_matrix/media/v3", "/_matrix/media/r0"]) {
239
+ r.get(`${prefix}/download/:serverName/:mediaId`, download);
240
+ r.get(`${prefix}/download/:serverName/:mediaId/:fileName`, download);
241
+ r.get(`${prefix}/thumbnail/:serverName/:mediaId`, thumbnail);
242
+ }
243
+ const authed = async (ctx) => {
244
+ await ctx.auth();
245
+ return download(ctx);
246
+ };
247
+ r.get("/_matrix/client/v1/media/download/:serverName/:mediaId", authed);
248
+ r.get("/_matrix/client/v1/media/download/:serverName/:mediaId/:fileName", authed);
249
+ r.get("/_matrix/client/v1/media/thumbnail/:serverName/:mediaId", async (ctx) => {
250
+ await ctx.auth();
251
+ return thumbnail(ctx);
252
+ });
253
+ }
254
+ //#endregion
255
+ export { registerMediaRoutes };
@@ -0,0 +1,86 @@
1
+ import { signJson } from "../core/crypto.js";
2
+ import { SUPPORTED_ROOM_VERSIONS } from "../core/events.js";
3
+ import { errors } from "../core/errors.js";
4
+ import { wellKnown } from "./account.js";
5
+ import { canonicalJsonResponse } from "../http/handler.js";
6
+ //#region src/api/misc.ts
7
+ const SPEC_VERSIONS = [
8
+ "r0.0.1",
9
+ "r0.1.0",
10
+ "r0.2.0",
11
+ "r0.3.0",
12
+ "r0.4.0",
13
+ "r0.5.0",
14
+ "r0.6.0",
15
+ "r0.6.1",
16
+ "v1.1",
17
+ "v1.2",
18
+ "v1.3",
19
+ "v1.4",
20
+ "v1.5",
21
+ "v1.6",
22
+ "v1.7",
23
+ "v1.8",
24
+ "v1.9",
25
+ "v1.10",
26
+ "v1.11"
27
+ ];
28
+ function registerMiscRoutes(r) {
29
+ r.get("/_matrix/client/versions", () => ({
30
+ versions: SPEC_VERSIONS,
31
+ unstable_features: {
32
+ "org.matrix.simplified_msc3575": true,
33
+ "org.matrix.msc3916.stable": true,
34
+ "org.matrix.msc4140": true,
35
+ "org.matrix.msc4143": true,
36
+ "org.matrix.msc4222": true,
37
+ "org.matrix.msc4306": true
38
+ }
39
+ }));
40
+ r.get("/.well-known/matrix/client", (ctx) => {
41
+ const wk = wellKnown(ctx.hs);
42
+ if (!wk) throw errors.notFound("No well-known configured");
43
+ return wk;
44
+ });
45
+ r.get("/.well-known/matrix/server", (ctx) => {
46
+ if (!ctx.hs.config.federationAddress) throw errors.notFound("No delegation configured");
47
+ return { "m.server": ctx.hs.config.federationAddress };
48
+ });
49
+ r.get("/.well-known/matrix/support", (ctx) => {
50
+ if (!ctx.hs.config.support) throw errors.notFound("No support information configured");
51
+ return ctx.hs.config.support;
52
+ });
53
+ r.client("GET", "/capabilities", async (ctx) => {
54
+ await ctx.auth();
55
+ const available = {};
56
+ for (const v of SUPPORTED_ROOM_VERSIONS) available[v] = /^\d+$/.test(v) ? "stable" : "unstable";
57
+ return { capabilities: {
58
+ "m.change_password": { enabled: !ctx.hs.oauth && ctx.hs.userAuth.passwordLoginEnabled },
59
+ "m.room_versions": {
60
+ default: ctx.hs.config.defaultRoomVersion,
61
+ available
62
+ },
63
+ "m.set_displayname": { enabled: true },
64
+ "m.set_avatar_url": { enabled: true },
65
+ "m.profile_fields": { enabled: true },
66
+ "m.3pid_changes": { enabled: ctx.hs.threepids.enabled && !ctx.hs.oauth },
67
+ "m.get_login_token": { enabled: ctx.hs.config.loginViaExistingSession && !ctx.hs.oauth }
68
+ } };
69
+ });
70
+ r.get("/_matrix/key/v2/server", (ctx) => {
71
+ const hs = ctx.hs;
72
+ const key = hs.signingKey;
73
+ return canonicalJsonResponse(signJson({
74
+ server_name: hs.serverName,
75
+ valid_until_ts: Date.now() + 864e5,
76
+ verify_keys: { [`ed25519:${key.keyId}`]: { key: key.publicKeyBase64 } },
77
+ old_verify_keys: {}
78
+ }, hs.serverName, key));
79
+ });
80
+ r.get("/_matrix/federation/v1/version", () => ({ server: {
81
+ name: "matrix-nodejs-homeserver",
82
+ version: "0.1.0"
83
+ } }));
84
+ }
85
+ //#endregion
86
+ export { SPEC_VERSIONS, registerMiscRoutes };