matrix-nodejs-homeserver 0.2.0 → 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 (78) hide show
  1. package/README.md +29 -7
  2. package/dist/api/account.js +385 -351
  3. package/dist/api/admin.js +76 -60
  4. package/dist/api/appservice.js +37 -25
  5. package/dist/api/backup.js +73 -62
  6. package/dist/api/federation.js +172 -138
  7. package/dist/api/filters.js +4 -3
  8. package/dist/api/legacy.js +91 -78
  9. package/dist/api/media.js +70 -54
  10. package/dist/api/misc.js +46 -37
  11. package/dist/api/preview.js +4 -3
  12. package/dist/api/profile.js +108 -97
  13. package/dist/api/push.js +114 -99
  14. package/dist/api/relationships.js +64 -58
  15. package/dist/api/rooms.js +703 -580
  16. package/dist/api/rtc.d.ts +0 -1
  17. package/dist/api/rtc.js +48 -34
  18. package/dist/api/search.js +133 -127
  19. package/dist/api/slidingsync.js +41 -31
  20. package/dist/api/spaces.js +82 -77
  21. package/dist/api/sync.js +59 -50
  22. package/dist/api/threadsubs.js +72 -61
  23. package/dist/api/threepid.js +99 -66
  24. package/dist/api/uia.js +17 -16
  25. package/dist/api/user.js +221 -178
  26. package/dist/core/admin.d.ts +0 -1
  27. package/dist/core/admin.js +6 -5
  28. package/dist/core/adminroom.js +9 -8
  29. package/dist/core/appservices.d.ts +2 -1
  30. package/dist/core/appservices.js +5 -4
  31. package/dist/core/audit.d.ts +0 -1
  32. package/dist/core/authrules.js +26 -25
  33. package/dist/core/delayed.js +2 -1
  34. package/dist/core/events.js +11 -10
  35. package/dist/core/invitefilter.js +2 -1
  36. package/dist/core/keys.js +2 -1
  37. package/dist/core/m.d.ts +209 -0
  38. package/dist/core/m.js +182 -0
  39. package/dist/core/partialstate.js +2 -1
  40. package/dist/core/policy.js +4 -3
  41. package/dist/core/push.d.ts +0 -1
  42. package/dist/core/push.js +12 -11
  43. package/dist/core/pushrules.js +36 -35
  44. package/dist/core/rooms.js +76 -74
  45. package/dist/core/stateres.js +7 -6
  46. package/dist/federation/client.js +2 -1
  47. package/dist/federation/index.d.ts +1 -1
  48. package/dist/federation/index.js +30 -29
  49. package/dist/homeserver.d.ts +2 -1
  50. package/dist/homeserver.js +1 -0
  51. package/dist/http/handler.js +2 -23
  52. package/dist/http/input.js +2 -2
  53. package/dist/http/procedure.js +25 -0
  54. package/dist/http/router.d.ts +9 -26
  55. package/dist/http/router.js +10 -30
  56. package/dist/index.d.ts +2 -1
  57. package/dist/index.js +2 -1
  58. package/dist/vendor/zod/LICENSE +21 -0
  59. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  60. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  61. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  62. package/dist/vendor/zod/v4/core/api.js +512 -0
  63. package/dist/vendor/zod/v4/core/checks.js +300 -0
  64. package/dist/vendor/zod/v4/core/core.js +109 -0
  65. package/dist/vendor/zod/v4/core/doc.js +35 -0
  66. package/dist/vendor/zod/v4/core/errors.js +118 -0
  67. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  68. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  69. package/dist/vendor/zod/v4/core/parse.js +206 -0
  70. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  71. package/dist/vendor/zod/v4/core/registries.js +48 -0
  72. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  73. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  74. package/dist/vendor/zod/v4/core/util.js +537 -0
  75. package/dist/vendor/zod/v4/core/versions.js +8 -0
  76. package/dist/vendor/zod/v4/locales/en.js +110 -0
  77. package/package.json +3 -5
  78. package/dist/core/pushrules.d.ts +0 -1
@@ -1,6 +1,8 @@
1
+ import { M } from "../core/m.js";
1
2
  import { splitStateKey, stripState } from "../core/events.js";
2
3
  import { errors } from "../core/errors.js";
3
4
  import { getState, membershipOf } from "../core/authrules.js";
5
+ import { procedure } from "../http/procedure.js";
4
6
  //#region src/api/spaces.ts
5
7
  /** Room-level details shared by /hierarchy, /room_summary and /publicRooms. */
6
8
  async function roomDetails(hs, roomId) {
@@ -9,16 +11,16 @@ async function roomDetails(hs, roomId) {
9
11
  const out = {
10
12
  room_id: roomId,
11
13
  num_joined_members: (await hs.rooms.joinedMembers(roomId)).length,
12
- world_readable: get("m.room.history_visibility")?.history_visibility === "world_readable",
13
- guest_can_join: get("m.room.guest_access")?.guest_access === "can_join",
14
- join_rule: get("m.room.join_rules")?.join_rule ?? "invite"
14
+ world_readable: get(M.ROOM.HISTORY_VISIBILITY)?.history_visibility === "world_readable",
15
+ guest_can_join: get(M.ROOM.GUEST_ACCESS)?.guest_access === "can_join",
16
+ join_rule: get(M.ROOM.JOIN_RULES)?.join_rule ?? "invite"
15
17
  };
16
18
  const str = (v) => typeof v === "string" ? v : void 0;
17
- if (str(get("m.room.name")?.name)) out.name = get("m.room.name").name;
18
- if (str(get("m.room.topic")?.topic)) out.topic = get("m.room.topic").topic;
19
- if (str(get("m.room.canonical_alias")?.alias)) out.canonical_alias = get("m.room.canonical_alias").alias;
20
- if (str(get("m.room.avatar")?.url)) out.avatar_url = get("m.room.avatar").url;
21
- if (str(get("m.room.create")?.type)) out.room_type = get("m.room.create").type;
19
+ if (str(get(M.ROOM.NAME)?.name)) out.name = get(M.ROOM.NAME).name;
20
+ if (str(get(M.ROOM.TOPIC)?.topic)) out.topic = get(M.ROOM.TOPIC).topic;
21
+ if (str(get(M.ROOM.CANONICAL_ALIAS)?.alias)) out.canonical_alias = get(M.ROOM.CANONICAL_ALIAS).alias;
22
+ if (str(get(M.ROOM.AVATAR)?.url)) out.avatar_url = get(M.ROOM.AVATAR).url;
23
+ if (str(get(M.ROOM.CREATE)?.type)) out.room_type = get(M.ROOM.CREATE).type;
22
24
  return out;
23
25
  }
24
26
  /** Whether a user may see a room's summary (spec: "accessible" rooms). */
@@ -29,13 +31,13 @@ async function canAccess(hs, roomId, userId) {
29
31
  "invite",
30
32
  "knock"
31
33
  ].includes(membershipOf(state, userId))) return true;
32
- if (getState(state, "m.room.history_visibility")?.content?.history_visibility === "world_readable") return true;
33
- const joinRules = getState(state, "m.room.join_rules")?.content;
34
+ if (getState(state, M.ROOM.HISTORY_VISIBILITY)?.content?.history_visibility === "world_readable") return true;
35
+ const joinRules = getState(state, M.ROOM.JOIN_RULES)?.content;
34
36
  const rule = joinRules?.join_rule ?? "invite";
35
37
  if (rule === "public" || rule === "knock") return true;
36
38
  if ((rule === "restricted" || rule === "knock_restricted") && userId) {
37
39
  if (rule === "knock_restricted") return true;
38
- for (const allow of Array.isArray(joinRules?.allow) ? joinRules.allow : []) if (allow?.type === "m.room_membership" && typeof allow.room_id === "string" && await hs.rooms.exists(allow.room_id)) {
40
+ for (const allow of Array.isArray(joinRules?.allow) ? joinRules.allow : []) if (allow?.type === M.ROOM_MEMBERSHIP && typeof allow.room_id === "string" && await hs.rooms.exists(allow.room_id)) {
39
41
  if (await hs.rooms.membership(allow.room_id, userId) === "join") return true;
40
42
  }
41
43
  }
@@ -43,7 +45,7 @@ async function canAccess(hs, roomId, userId) {
43
45
  }
44
46
  function allowedRoomIds(joinRules) {
45
47
  if (!joinRules || !["restricted", "knock_restricted"].includes(joinRules.join_rule)) return void 0;
46
- return (Array.isArray(joinRules.allow) ? joinRules.allow : []).filter((a) => a?.type === "m.room_membership" && typeof a.room_id === "string").map((a) => a.room_id);
48
+ return (Array.isArray(joinRules.allow) ? joinRules.allow : []).filter((a) => a?.type === M.ROOM_MEMBERSHIP && typeof a.room_id === "string").map((a) => a.room_id);
47
49
  }
48
50
  /** Spec ordering of space children: valid `order` strings first, then child event timestamp, then room ID. */
49
51
  function childOrder(a, b) {
@@ -78,73 +80,76 @@ async function remoteSummary(hs, roomId, via, suggestedOnly, userId) {
78
80
  return null;
79
81
  }
80
82
  function registerSpaceRoutes(r) {
81
- r.client("GET", "/rooms/:roomId/hierarchy", async (ctx) => {
82
- const s = await ctx.auth();
83
- const hs = ctx.hs;
84
- const rootId = ctx.params.roomId;
85
- if (!await hs.rooms.exists(rootId) || !await canAccess(hs, rootId, s.userId)) throw errors.forbidden("You cannot view this room");
86
- const suggestedOnly = ctx.query.get("suggested_only") === "true";
87
- const maxDepth = ctx.query.has("max_depth") ? Number.parseInt(ctx.query.get("max_depth"), 10) : Infinity;
88
- const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "50", 10) || 50, 1), 100);
89
- const from = ctx.query.get("from");
90
- let offset = 0;
91
- if (from) {
92
- const m = /^(\d+)_(.+)$/.exec(from);
93
- if (!m || m[2] !== `${rootId}|${suggestedOnly}|${ctx.query.get("max_depth") ?? ""}`) throw errors.invalidParam("Invalid pagination token");
94
- offset = Number(m[1]);
95
- }
96
- const rooms = [];
97
- const seen = /* @__PURE__ */ new Set();
98
- const queue = [{
99
- roomId: rootId,
100
- depth: 0,
101
- via: []
102
- }];
103
- while (queue.length) {
104
- const { roomId, depth, via } = queue.pop();
105
- if (seen.has(roomId)) continue;
106
- seen.add(roomId);
107
- if (!await hs.rooms.serverIsJoined(roomId)) {
108
- const remote = await remoteSummary(hs, roomId, via, suggestedOnly, s.userId);
109
- if (!remote) continue;
110
- rooms.push(remote);
111
- const remoteChildren = (remote.room_type === "m.space" ? remote.children_state ?? [] : []).filter((c) => Array.isArray(c.content?.via) && c.content.via.length).sort(childOrder);
112
- if (depth < maxDepth) for (const child of [...remoteChildren].reverse()) queue.push({
83
+ r.client("GET", "/rooms/:roomId/hierarchy", procedure({
84
+ auth: "user",
85
+ handler: async (ctx) => {
86
+ const s = ctx.session;
87
+ const hs = ctx.hs;
88
+ const rootId = ctx.params.roomId;
89
+ if (!await hs.rooms.exists(rootId) || !await canAccess(hs, rootId, s.userId)) throw errors.forbidden("You cannot view this room");
90
+ const suggestedOnly = ctx.query.get("suggested_only") === "true";
91
+ const maxDepth = ctx.query.has("max_depth") ? Number.parseInt(ctx.query.get("max_depth"), 10) : Infinity;
92
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "50", 10) || 50, 1), 100);
93
+ const from = ctx.query.get("from");
94
+ let offset = 0;
95
+ if (from) {
96
+ const m = /^(\d+)_(.+)$/.exec(from);
97
+ if (!m || m[2] !== `${rootId}|${suggestedOnly}|${ctx.query.get("max_depth") ?? ""}`) throw errors.invalidParam("Invalid pagination token");
98
+ offset = Number(m[1]);
99
+ }
100
+ const rooms = [];
101
+ const seen = /* @__PURE__ */ new Set();
102
+ const queue = [{
103
+ roomId: rootId,
104
+ depth: 0,
105
+ via: []
106
+ }];
107
+ while (queue.length) {
108
+ const { roomId, depth, via } = queue.pop();
109
+ if (seen.has(roomId)) continue;
110
+ seen.add(roomId);
111
+ if (!await hs.rooms.serverIsJoined(roomId)) {
112
+ const remote = await remoteSummary(hs, roomId, via, suggestedOnly, s.userId);
113
+ if (!remote) continue;
114
+ rooms.push(remote);
115
+ const remoteChildren = (remote.room_type === M.SPACE._ ? remote.children_state ?? [] : []).filter((c) => Array.isArray(c.content?.via) && c.content.via.length).sort(childOrder);
116
+ if (depth < maxDepth) for (const child of [...remoteChildren].reverse()) queue.push({
117
+ roomId: child.state_key,
118
+ depth: depth + 1,
119
+ via: child.content.via
120
+ });
121
+ continue;
122
+ }
123
+ if (roomId !== rootId && !await canAccess(hs, roomId, s.userId)) continue;
124
+ const state = await hs.rooms.currentState(roomId);
125
+ const children = [];
126
+ const isSpace = getState(state, M.ROOM.CREATE)?.content?.type === M.SPACE._;
127
+ for (const [key, ev] of isSpace ? state : []) {
128
+ const [type] = splitStateKey(key);
129
+ if (type !== M.SPACE.CHILD || !Array.isArray(ev.content?.via) || ev.content.via.length === 0) continue;
130
+ if (suggestedOnly && ev.content.suggested !== true) continue;
131
+ children.push({
132
+ ...stripState(ev),
133
+ origin_server_ts: ev.origin_server_ts
134
+ });
135
+ }
136
+ children.sort(childOrder);
137
+ if (depth < maxDepth) for (const child of [...children].reverse()) queue.push({
113
138
  roomId: child.state_key,
114
139
  depth: depth + 1,
115
140
  via: child.content.via
116
141
  });
117
- continue;
142
+ const details = await roomDetails(hs, roomId);
143
+ const allowed = allowedRoomIds(getState(state, M.ROOM.JOIN_RULES)?.content);
144
+ if (allowed) details.allowed_room_ids = allowed;
145
+ details.children_state = children;
146
+ rooms.push(details);
118
147
  }
119
- if (roomId !== rootId && !await canAccess(hs, roomId, s.userId)) continue;
120
- const state = await hs.rooms.currentState(roomId);
121
- const children = [];
122
- const isSpace = getState(state, "m.room.create")?.content?.type === "m.space";
123
- for (const [key, ev] of isSpace ? state : []) {
124
- const [type] = splitStateKey(key);
125
- if (type !== "m.space.child" || !Array.isArray(ev.content?.via) || ev.content.via.length === 0) continue;
126
- if (suggestedOnly && ev.content.suggested !== true) continue;
127
- children.push({
128
- ...stripState(ev),
129
- origin_server_ts: ev.origin_server_ts
130
- });
131
- }
132
- children.sort(childOrder);
133
- if (depth < maxDepth) for (const child of [...children].reverse()) queue.push({
134
- roomId: child.state_key,
135
- depth: depth + 1,
136
- via: child.content.via
137
- });
138
- const details = await roomDetails(hs, roomId);
139
- const allowed = allowedRoomIds(getState(state, "m.room.join_rules")?.content);
140
- if (allowed) details.allowed_room_ids = allowed;
141
- details.children_state = children;
142
- rooms.push(details);
148
+ const out = { rooms: rooms.slice(offset, offset + limit) };
149
+ if (offset + limit < rooms.length) out.next_batch = `${offset + limit}_${rootId}|${suggestedOnly}|${ctx.query.get("max_depth") ?? ""}`;
150
+ return out;
143
151
  }
144
- const out = { rooms: rooms.slice(offset, offset + limit) };
145
- if (offset + limit < rooms.length) out.next_batch = `${offset + limit}_${rootId}|${suggestedOnly}|${ctx.query.get("max_depth") ?? ""}`;
146
- return out;
147
- }, ["v1"]);
152
+ }), ["v1"]);
148
153
  const summary = async (ctx) => {
149
154
  const s = await ctx.optionalAuth();
150
155
  const hs = ctx.hs;
@@ -158,15 +163,15 @@ function registerSpaceRoutes(r) {
158
163
  const state = await hs.rooms.currentState(roomId);
159
164
  const out = await roomDetails(hs, roomId);
160
165
  out.room_version = await hs.rooms.roomVersion(roomId);
161
- const enc = getState(state, "m.room.encryption")?.content?.algorithm;
166
+ const enc = getState(state, M.ROOM.ENCRYPTION)?.content?.algorithm;
162
167
  if (typeof enc === "string") out.encryption = enc;
163
168
  if (s) out.membership = membershipOf(state, s.userId);
164
- const allowed = allowedRoomIds(getState(state, "m.room.join_rules")?.content);
169
+ const allowed = allowedRoomIds(getState(state, M.ROOM.JOIN_RULES)?.content);
165
170
  if (allowed) out.allowed_room_ids = allowed;
166
171
  return out;
167
172
  };
168
- r.client("GET", "/room_summary/:roomIdOrAlias", summary, ["v1"]);
169
- r.get("/_matrix/client/unstable/im.nheko.summary/summary/:roomIdOrAlias", summary);
173
+ r.client("GET", "/room_summary/:roomIdOrAlias", procedure({ handler: summary }), ["v1"]);
174
+ r.get("/_matrix/client/unstable/im.nheko.summary/summary/:roomIdOrAlias", procedure({ handler: summary }));
170
175
  }
171
176
  //#endregion
172
177
  export { registerSpaceRoutes, roomDetails };
package/dist/api/sync.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { M, UNSTABLE } from "../core/m.js";
1
2
  import { eventIdOf, splitStateKey, stateKey, toClientEvent } from "../core/events.js";
2
3
  import { errors } from "../core/errors.js";
3
4
  import { getState } from "../core/authrules.js";
4
5
  import { loadRules } from "../core/pushrules.js";
5
6
  import { inviteDecision } from "../core/invitefilter.js";
6
7
  import { body } from "../http/input.js";
8
+ import { procedure } from "../http/procedure.js";
7
9
  import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
8
10
  import { fullProfile } from "../core/profile.js";
9
11
  //#region src/api/sync.ts
@@ -163,13 +165,13 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
163
165
  if (sinceIds[key] === ev.event_id) continue;
164
166
  const [type, sk] = splitStateKey(key);
165
167
  if (!matchesFilter(sf, ev)) continue;
166
- if (lazy && type === "m.room.member" && !senders.has(sk)) continue;
168
+ if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
167
169
  state.push(ev);
168
170
  }
169
171
  if (lazy) for (const e of events) {
170
- const key = stateKey("m.room.member", e.pdu.sender);
171
- if (startState.has(key) || state.some((s) => s.type === "m.room.member" && s.state_key === e.pdu.sender)) continue;
172
- const member = [...(await hs.rooms.getPdus((e.pdu.auth_events ?? []).map(eventIdOf))).values()].find((a) => a.type === "m.room.member" && a.state_key === e.pdu.sender);
172
+ const key = stateKey(M.ROOM.MEMBER, e.pdu.sender);
173
+ if (startState.has(key) || state.some((s) => s.type === M.ROOM.MEMBER && s.state_key === e.pdu.sender)) continue;
174
+ const member = [...(await hs.rooms.getPdus((e.pdu.auth_events ?? []).map(eventIdOf))).values()].find((a) => a.type === M.ROOM.MEMBER && a.state_key === e.pdu.sender);
173
175
  if (member && matchesFilter(sf, member)) state.push(member);
174
176
  }
175
177
  const stateAfter = [];
@@ -179,7 +181,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
179
181
  if (sinceIds[key] === ev.event_id) continue;
180
182
  const [type, sk] = splitStateKey(key);
181
183
  if (!matchesFilter(sf, ev)) continue;
182
- if (lazy && type === "m.room.member" && !senders.has(sk)) continue;
184
+ if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
183
185
  stateAfter.push(ev);
184
186
  }
185
187
  }
@@ -199,7 +201,7 @@ async function roomEphemeral(hs, p, roomId) {
199
201
  const out = [];
200
202
  const typing = hs.typing.typingSince(roomId, since);
201
203
  if (typing !== null && (since !== null || typing.length > 0)) out.push({
202
- type: "m.typing",
204
+ type: M.TYPING,
203
205
  content: { user_ids: typing }
204
206
  });
205
207
  const where = [{
@@ -214,7 +216,7 @@ async function roomEphemeral(hs, p, roomId) {
214
216
  const receipts = (await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
215
217
  const content = {};
216
218
  for (const r of receipts) {
217
- if (r.receipt_type === "m.read.private" && r.user_id !== p.session.userId) continue;
219
+ if (r.receipt_type === M.READ.PRIVATE && r.user_id !== p.session.userId) continue;
218
220
  content[r.event_id] ??= {};
219
221
  content[r.event_id][r.receipt_type] ??= {};
220
222
  if (r.thread_id && content[r.event_id][r.receipt_type][r.user_id] && !content[r.event_id][r.receipt_type][r.user_id].thread_id) continue;
@@ -224,7 +226,7 @@ async function roomEphemeral(hs, p, roomId) {
224
226
  };
225
227
  }
226
228
  if (Object.keys(content).length) out.push({
227
- type: "m.receipt",
229
+ type: M.RECEIPT,
228
230
  content
229
231
  });
230
232
  return out.filter((e) => matchesFilter(p.filter.room?.ephemeral, e));
@@ -256,17 +258,17 @@ async function roomSummary(hs, roomId, userId) {
256
258
  const heroes = [];
257
259
  for (const [key, ev] of state) {
258
260
  const [type, sk] = splitStateKey(key);
259
- if (type !== "m.room.member") continue;
261
+ if (type !== M.ROOM.MEMBER) continue;
260
262
  const m = ev.content?.membership;
261
263
  if (m === "join") joined++;
262
264
  if (m === "invite") invited++;
263
265
  if ((m === "join" || m === "invite") && sk !== userId && heroes.length < 5) heroes.push(sk);
264
266
  }
265
267
  const summary = {
266
- "m.joined_member_count": joined,
267
- "m.invited_member_count": invited
268
+ [M.JOINED_MEMBER_COUNT]: joined,
269
+ [M.INVITED_MEMBER_COUNT]: invited
268
270
  };
269
- if (!getState(state, "m.room.name")?.content?.name && !getState(state, "m.room.canonical_alias")?.content?.alias) summary["m.heroes"] = heroes.sort();
271
+ if (!getState(state, M.ROOM.NAME)?.content?.name && !getState(state, M.ROOM.CANONICAL_ALIAS)?.content?.alias) summary[M.HEROES] = heroes.sort();
270
272
  return summary;
271
273
  }
272
274
  async function buildSync(hs, p) {
@@ -345,8 +347,8 @@ async function buildSync(hs, p) {
345
347
  }
346
348
  }
347
349
  const globalAccountData = await accountData(hs, p, "");
348
- if (since === null && !globalAccountData.some((e) => e.type === "m.push_rules")) globalAccountData.push({
349
- type: "m.push_rules",
350
+ if (since === null && !globalAccountData.some((e) => e.type === M.PUSH_RULES)) globalAccountData.push({
351
+ type: M.PUSH_RULES,
350
352
  content: { global: await loadRules(hs, userId) }
351
353
  });
352
354
  if (globalAccountData.length) empty = false;
@@ -408,7 +410,7 @@ async function buildSync(hs, p) {
408
410
  if (row.status_msg) content.status_msg = row.status_msg;
409
411
  if (row.presence === "online") content.currently_active = true;
410
412
  const ev = {
411
- type: "m.presence",
413
+ type: M.PRESENCE,
412
414
  sender: row.user_id,
413
415
  content
414
416
  };
@@ -439,7 +441,7 @@ async function buildSync(hs, p) {
439
441
  device_one_time_keys_count: await hs.keys.oneTimeKeyCounts(userId, p.session.deviceId),
440
442
  device_unused_fallback_key_types: await hs.keys.unusedFallbackKeyTypes(userId, p.session.deviceId)
441
443
  };
442
- if (profileUpdates) body["org.matrix.msc4429.users"] = profileUpdates;
444
+ if (profileUpdates) body[UNSTABLE.ORG.MATRIX.MSC4429.USERS] = profileUpdates;
443
445
  if (since !== null) {
444
446
  const lists = await hs.keys.changesSince(userId, since, p.to);
445
447
  body.device_lists = lists;
@@ -474,12 +476,12 @@ async function sync(ctx) {
474
476
  },
475
477
  {
476
478
  field: "type",
477
- value: "m.ignored_user_list"
479
+ value: M.IGNORED_USER_LIST
478
480
  }
479
481
  ]);
480
482
  const ignored = new Set(Object.keys(ignoredRow?.content?.ignored_users ?? {}));
481
483
  const stable = ctx.query.get("use_state_after") === "true";
482
- const unstable = ctx.query.get("org.matrix.msc4222.use_state_after") === "true";
484
+ const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
483
485
  const { body, empty } = await buildSync(hs, {
484
486
  session,
485
487
  ignored,
@@ -488,7 +490,7 @@ async function sync(ctx) {
488
490
  filter,
489
491
  fullState,
490
492
  useStateAfter: stable || unstable,
491
- stateAfterKey: stable ? "state_after" : "org.matrix.msc4222.state_after"
493
+ stateAfterKey: stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER
492
494
  });
493
495
  const remaining = deadline - Date.now();
494
496
  if (!since || !empty || remaining <= 0 || fullState) return body;
@@ -520,43 +522,50 @@ async function touchPresence(hs, userId, presence) {
520
522
  await hs.federation.onPresence(userId, state, row?.status_msg ?? null).catch(() => {});
521
523
  }
522
524
  function registerSyncRoutes(r) {
523
- r.client("GET", "/sync", sync);
524
- r.client("POST", "/user/:userId/filter", { input: body({}) }, async (ctx) => {
525
- const s = await ctx.auth();
526
- if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot create filters for other users");
527
- const filter = validateSyncFilter(ctx.input);
528
- const existing = await ctx.hs.db.findMany("filters", { where: [{
529
- field: "user_id",
530
- value: s.userId
531
- }] });
532
- const filterId = String(existing.reduce((max, f) => Math.max(max, Number(f.filter_id) || 0), 0) + 1);
533
- await ctx.hs.db.create("filters", {
534
- user_id: s.userId,
535
- filter_id: filterId,
536
- filter
537
- });
538
- return { filter_id: filterId };
539
- });
540
- r.client("GET", "/user/:userId/filter/:filterId", async (ctx) => {
541
- const s = await ctx.auth();
542
- if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot read filters of other users");
543
- const row = await ctx.hs.db.findOne("filters", [{
544
- field: "user_id",
545
- value: s.userId
546
- }, {
547
- field: "filter_id",
548
- value: ctx.params.filterId
549
- }]);
550
- if (!row) throw errors.notFound("Unknown filter");
551
- return row.filter;
552
- });
525
+ r.client("GET", "/sync", procedure({ handler: sync }));
526
+ r.client("POST", "/user/:userId/filter", procedure({
527
+ auth: "user",
528
+ input: body({}),
529
+ handler: async (ctx) => {
530
+ const s = ctx.session;
531
+ if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot create filters for other users");
532
+ const filter = validateSyncFilter(ctx.input);
533
+ const existing = await ctx.hs.db.findMany("filters", { where: [{
534
+ field: "user_id",
535
+ value: s.userId
536
+ }] });
537
+ const filterId = String(existing.reduce((max, f) => Math.max(max, Number(f.filter_id) || 0), 0) + 1);
538
+ await ctx.hs.db.create("filters", {
539
+ user_id: s.userId,
540
+ filter_id: filterId,
541
+ filter
542
+ });
543
+ return { filter_id: filterId };
544
+ }
545
+ }));
546
+ r.client("GET", "/user/:userId/filter/:filterId", procedure({
547
+ auth: "user",
548
+ handler: async (ctx) => {
549
+ const s = ctx.session;
550
+ if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot read filters of other users");
551
+ const row = await ctx.hs.db.findOne("filters", [{
552
+ field: "user_id",
553
+ value: s.userId
554
+ }, {
555
+ field: "filter_id",
556
+ value: ctx.params.filterId
557
+ }]);
558
+ if (!row) throw errors.notFound("Unknown filter");
559
+ return row.filter;
560
+ }
561
+ }));
553
562
  }
554
563
  /**
555
564
  * MSC4429: profile changes of users sharing a room, limited to the fields the filter asks for.
556
565
  * The initial sync carries current values; users we no longer share any room with get `null`.
557
566
  */
558
567
  async function profileUpdatesFor(hs, p, memberships) {
559
- const ids = (p.filter.profile_fields ?? p.filter["org.matrix.msc4429.profile_fields"])?.ids;
568
+ const ids = (p.filter.profile_fields ?? p.filter[UNSTABLE.ORG.MATRIX.MSC4429.PROFILE_FIELDS])?.ids;
560
569
  if (!Array.isArray(ids) || !ids.length) return null;
561
570
  const wanted = new Set(ids.filter((x) => typeof x === "string"));
562
571
  const since = p.since?.pos ?? null;
@@ -1,5 +1,7 @@
1
1
  import { isObject } from "../core/json.js";
2
+ import { M } from "../core/m.js";
2
3
  import { MatrixError, errors } from "../core/errors.js";
4
+ import { procedure } from "../http/procedure.js";
3
5
  //#region src/api/threadsubs.ts
4
6
  const VERSIONS = ["unstable/io.element.msc4306"];
5
7
  const notInThread = () => new MatrixError(400, "IO.ELEMENT.MSC4306.M_NOT_IN_THREAD", "Event is not in this thread");
@@ -38,68 +40,77 @@ async function threadRoot(ctx, userId) {
38
40
  }
39
41
  function registerThreadSubscriptionRoutes(r) {
40
42
  const path = "/rooms/:roomId/thread/:threadId/subscription";
41
- r.client("GET", path, async (ctx) => {
42
- const s = await ctx.auth();
43
- await threadRoot(ctx, s.userId);
44
- const row = await ctx.hs.db.findOne("thread_subscriptions", key(s.userId, ctx.params.roomId, ctx.params.threadId));
45
- if (!row?.subscribed) throw errors.notFound("Not subscribed to this thread");
46
- return { automatic: !!row.automatic };
47
- }, VERSIONS);
48
- r.client("PUT", path, async (ctx) => {
49
- const s = await ctx.auth();
50
- const hs = ctx.hs;
51
- const { roomId, threadId } = ctx.params;
52
- await threadRoot(ctx, s.userId);
53
- const body = await ctx.json();
54
- const cause = isObject(body) ? body.automatic : void 0;
55
- if (cause !== void 0 && typeof cause !== "string") throw errors.badJson("automatic must be an event ID");
56
- let causeStream = null;
57
- if (typeof cause === "string") {
58
- const ev = await visibleEvent(ctx, s.userId, roomId, cause);
59
- const rel = ev?.pdu.content?.["m.relates_to"];
60
- if (!ev || !isObject(rel) || rel.rel_type !== "m.thread" || rel.event_id !== threadId) throw notInThread();
61
- causeStream = ev.stream;
43
+ r.client("GET", path, procedure({
44
+ auth: "user",
45
+ handler: async (ctx) => {
46
+ const s = ctx.session;
47
+ await threadRoot(ctx, s.userId);
48
+ const row = await ctx.hs.db.findOne("thread_subscriptions", key(s.userId, ctx.params.roomId, ctx.params.threadId));
49
+ if (!row?.subscribed) throw errors.notFound("Not subscribed to this thread");
50
+ return { automatic: !!row.automatic };
62
51
  }
63
- await hs.cluster.lock(`threadsub:${s.userId}:${roomId}:${threadId}`, async () => {
64
- const row = await hs.db.findOne("thread_subscriptions", key(s.userId, roomId, threadId));
65
- if (causeStream !== null) {
66
- if (row && row.unsubscribed_after !== null && causeStream <= row.unsubscribed_after) throw conflicting();
67
- if (row?.subscribed) return;
68
- } else if (row?.subscribed && !row.automatic) return;
69
- await hs.streams.write((stream) => hs.db.upsert("thread_subscriptions", {
70
- user_id: s.userId,
71
- room_id: roomId,
72
- thread_id: threadId,
73
- subscribed: true,
74
- automatic: causeStream !== null,
75
- unsubscribed_after: row?.unsubscribed_after ?? null,
76
- stream
77
- }));
78
- });
79
- hs.notifier.notify([s.userId]);
80
- return {};
81
- }, VERSIONS);
82
- r.client("DELETE", path, async (ctx) => {
83
- const s = await ctx.auth();
84
- const hs = ctx.hs;
85
- const { roomId, threadId } = ctx.params;
86
- await threadRoot(ctx, s.userId);
87
- await hs.cluster.lock(`threadsub:${s.userId}:${roomId}:${threadId}`, async () => {
88
- if (!(await hs.db.findOne("thread_subscriptions", key(s.userId, roomId, threadId)))?.subscribed) return;
89
- const position = await hs.streams.position();
90
- await hs.streams.write((stream) => hs.db.upsert("thread_subscriptions", {
91
- user_id: s.userId,
92
- room_id: roomId,
93
- thread_id: threadId,
94
- subscribed: false,
95
- automatic: false,
96
- unsubscribed_after: position,
97
- stream
98
- }));
99
- });
100
- hs.notifier.notify([s.userId]);
101
- return {};
102
- }, VERSIONS);
52
+ }), VERSIONS);
53
+ r.client("PUT", path, procedure({
54
+ auth: "user",
55
+ handler: async (ctx) => {
56
+ const s = ctx.session;
57
+ const hs = ctx.hs;
58
+ const { roomId, threadId } = ctx.params;
59
+ await threadRoot(ctx, s.userId);
60
+ const body = await ctx.json();
61
+ const cause = isObject(body) ? body.automatic : void 0;
62
+ if (cause !== void 0 && typeof cause !== "string") throw errors.badJson("automatic must be an event ID");
63
+ let causeStream = null;
64
+ if (typeof cause === "string") {
65
+ const ev = await visibleEvent(ctx, s.userId, roomId, cause);
66
+ const rel = ev?.pdu.content?.[M.RELATES_TO];
67
+ if (!ev || !isObject(rel) || rel.rel_type !== M.THREAD || rel.event_id !== threadId) throw notInThread();
68
+ causeStream = ev.stream;
69
+ }
70
+ await hs.cluster.lock(`threadsub:${s.userId}:${roomId}:${threadId}`, async () => {
71
+ const row = await hs.db.findOne("thread_subscriptions", key(s.userId, roomId, threadId));
72
+ if (causeStream !== null) {
73
+ if (row && row.unsubscribed_after !== null && causeStream <= row.unsubscribed_after) throw conflicting();
74
+ if (row?.subscribed) return;
75
+ } else if (row?.subscribed && !row.automatic) return;
76
+ await hs.streams.write((stream) => hs.db.upsert("thread_subscriptions", {
77
+ user_id: s.userId,
78
+ room_id: roomId,
79
+ thread_id: threadId,
80
+ subscribed: true,
81
+ automatic: causeStream !== null,
82
+ unsubscribed_after: row?.unsubscribed_after ?? null,
83
+ stream
84
+ }));
85
+ });
86
+ hs.notifier.notify([s.userId]);
87
+ return {};
88
+ }
89
+ }), VERSIONS);
90
+ r.client("DELETE", path, procedure({
91
+ auth: "user",
92
+ handler: async (ctx) => {
93
+ const s = ctx.session;
94
+ const hs = ctx.hs;
95
+ const { roomId, threadId } = ctx.params;
96
+ await threadRoot(ctx, s.userId);
97
+ await hs.cluster.lock(`threadsub:${s.userId}:${roomId}:${threadId}`, async () => {
98
+ if (!(await hs.db.findOne("thread_subscriptions", key(s.userId, roomId, threadId)))?.subscribed) return;
99
+ const position = await hs.streams.position();
100
+ await hs.streams.write((stream) => hs.db.upsert("thread_subscriptions", {
101
+ user_id: s.userId,
102
+ room_id: roomId,
103
+ thread_id: threadId,
104
+ subscribed: false,
105
+ automatic: false,
106
+ unsubscribed_after: position,
107
+ stream
108
+ }));
109
+ });
110
+ hs.notifier.notify([s.userId]);
111
+ return {};
112
+ }
113
+ }), VERSIONS);
103
114
  }
104
115
  /** MSC4308 sliding sync extension: subscription changes in (since, to]. */
105
116
  async function threadSubscriptionsExtension(hs, userId, ext, since, to) {