matrix-nodejs-homeserver 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +89 -11
  2. package/dist/adapters/cluster.d.ts +1 -26
  3. package/dist/adapters/cluster.js +50 -103
  4. package/dist/api/account.js +407 -354
  5. package/dist/api/admin.js +82 -61
  6. package/dist/api/appservice.js +37 -25
  7. package/dist/api/backup.js +73 -62
  8. package/dist/api/federation.js +172 -138
  9. package/dist/api/filters.js +4 -3
  10. package/dist/api/legacy.js +91 -78
  11. package/dist/api/media.js +71 -55
  12. package/dist/api/misc.js +46 -37
  13. package/dist/api/preview.js +37 -73
  14. package/dist/api/profile.js +108 -97
  15. package/dist/api/push.js +114 -99
  16. package/dist/api/relationships.js +64 -58
  17. package/dist/api/rooms.js +703 -580
  18. package/dist/api/rtc.d.ts +0 -1
  19. package/dist/api/rtc.js +48 -34
  20. package/dist/api/search.js +133 -127
  21. package/dist/api/slidingsync.js +41 -31
  22. package/dist/api/spaces.js +82 -77
  23. package/dist/api/sync.js +325 -135
  24. package/dist/api/threadsubs.js +72 -61
  25. package/dist/api/threepid.js +99 -66
  26. package/dist/api/uia.js +17 -16
  27. package/dist/api/user.js +221 -178
  28. package/dist/core/admin.d.ts +0 -1
  29. package/dist/core/admin.js +6 -5
  30. package/dist/core/adminroom.js +9 -8
  31. package/dist/core/appservices.d.ts +2 -1
  32. package/dist/core/appservices.js +5 -4
  33. package/dist/core/audit.d.ts +0 -1
  34. package/dist/core/authrules.js +26 -25
  35. package/dist/core/cluster.d.ts +12 -5
  36. package/dist/core/cluster.js +46 -29
  37. package/dist/core/delayed.js +2 -1
  38. package/dist/core/events.js +11 -10
  39. package/dist/core/html.js +13 -0
  40. package/dist/core/identity.js +1 -1
  41. package/dist/core/invitefilter.js +2 -1
  42. package/dist/core/json.js +10 -1
  43. package/dist/core/keys.d.ts +10 -1
  44. package/dist/core/keys.js +62 -42
  45. package/dist/core/m.d.ts +209 -0
  46. package/dist/core/m.js +182 -0
  47. package/dist/core/outbound.d.ts +16 -0
  48. package/dist/core/outbound.js +172 -0
  49. package/dist/core/partialstate.js +2 -1
  50. package/dist/core/policy.js +4 -3
  51. package/dist/core/push.d.ts +0 -1
  52. package/dist/core/push.js +41 -28
  53. package/dist/core/pushrules.js +36 -35
  54. package/dist/core/ratelimit.d.ts +1 -1
  55. package/dist/core/rooms.d.ts +10 -1
  56. package/dist/core/rooms.js +101 -74
  57. package/dist/core/semaphore.js +23 -0
  58. package/dist/core/sso.d.ts +15 -1
  59. package/dist/core/sso.js +38 -2
  60. package/dist/core/stateres.js +7 -6
  61. package/dist/core/threepid.js +1 -9
  62. package/dist/federation/client.js +9 -1
  63. package/dist/federation/index.d.ts +2 -2
  64. package/dist/federation/index.js +43 -35
  65. package/dist/homeserver.d.ts +23 -4
  66. package/dist/homeserver.js +12 -3
  67. package/dist/http/handler.js +2 -23
  68. package/dist/http/input.js +2 -2
  69. package/dist/http/procedure.js +25 -0
  70. package/dist/http/router.d.ts +9 -26
  71. package/dist/http/router.js +10 -30
  72. package/dist/index.d.ts +4 -3
  73. package/dist/index.js +3 -2
  74. package/dist/storage/batch.js +11 -0
  75. package/dist/storage/memory.js +1 -0
  76. package/dist/storage/schema.js +1 -1
  77. package/dist/storage/sql.js +20 -1
  78. package/dist/storage/types.d.ts +16 -0
  79. package/dist/vendor/zod/LICENSE +21 -0
  80. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  81. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  82. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  83. package/dist/vendor/zod/v4/core/api.js +512 -0
  84. package/dist/vendor/zod/v4/core/checks.js +300 -0
  85. package/dist/vendor/zod/v4/core/core.js +109 -0
  86. package/dist/vendor/zod/v4/core/doc.js +35 -0
  87. package/dist/vendor/zod/v4/core/errors.js +118 -0
  88. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  89. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  90. package/dist/vendor/zod/v4/core/parse.js +206 -0
  91. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  92. package/dist/vendor/zod/v4/core/registries.js +48 -0
  93. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  94. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  95. package/dist/vendor/zod/v4/core/util.js +537 -0
  96. package/dist/vendor/zod/v4/core/versions.js +8 -0
  97. package/dist/vendor/zod/v4/locales/en.js +110 -0
  98. package/package.json +8 -6
  99. package/dist/core/pushrules.d.ts +0 -1
package/dist/core/push.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isObject } from "./json.js";
2
+ import { M, UNSTABLE } from "./m.js";
2
3
  import { splitStateKey, toClientEvent } from "./events.js";
3
4
  import { getState, powerLevels, userLevel } from "./authrules.js";
4
5
  import { globMatch, globSearchWords } from "./glob.js";
@@ -47,7 +48,7 @@ function conditionMatches(cond, ctx) {
47
48
  }
48
49
  return false;
49
50
  }
50
- case "io.element.msc4306.thread_subscription": return ctx.threadSubscribed !== void 0 && ctx.threadSubscribed !== null && ctx.threadSubscribed === cond.subscribed;
51
+ case UNSTABLE.IO.ELEMENT.MSC4306.THREAD_SUBSCRIPTION: return ctx.threadSubscribed !== void 0 && ctx.threadSubscribed !== null && ctx.threadSubscribed === cond.subscribed;
51
52
  case "sender_notification_permission": return typeof cond.key === "string" && ctx.senderLevel >= (ctx.notificationLevels[cond.key] ?? 50);
52
53
  default: return false;
53
54
  }
@@ -80,8 +81,8 @@ function tweaksOf(actions) {
80
81
  }
81
82
  /** Thread an event belongs to for notification purposes ("main" for the main timeline). */
82
83
  function threadIdOf(event) {
83
- const rel = event.content?.["m.relates_to"];
84
- return isObject(rel) && rel.rel_type === "m.thread" && typeof rel.event_id === "string" ? rel.event_id : "main";
84
+ const rel = event.content?.[M.RELATES_TO];
85
+ return isObject(rel) && rel.rel_type === M.THREAD && typeof rel.event_id === "string" ? rel.event_id : "main";
85
86
  }
86
87
  /**
87
88
  * Push rule evaluation and push gateway delivery. Every new event is evaluated for
@@ -97,20 +98,21 @@ var PushService = class {
97
98
  const members = [];
98
99
  for (const [key, member] of state) {
99
100
  const [type, userId] = splitStateKey(key);
100
- if (type !== "m.room.member") continue;
101
+ if (type !== M.ROOM.MEMBER) continue;
101
102
  const m = member.content?.membership;
102
- if (m === "join" || m === "invite" && event.type === "m.room.member" && event.state_key === userId) members.push(userId);
103
+ if (m === "join" || m === "invite" && event.type === M.ROOM.MEMBER && event.state_key === userId) members.push(userId);
103
104
  }
104
- const joined = members.filter((u) => getState(state, "m.room.member", u)?.content?.membership === "join");
105
+ const joined = members.filter((u) => getState(state, M.ROOM.MEMBER, u)?.content?.membership === "join");
105
106
  const pl = powerLevels(state, version);
106
107
  const senderLevel = userLevel(pl, event.sender);
107
108
  const thread = threadIdOf(event);
109
+ const notified = [];
108
110
  for (const userId of members) {
109
111
  if (userId === event.sender || !this.hs.isLocalUser(userId)) continue;
110
112
  const actions = evaluateRules(await loadRules(this.hs, userId), {
111
113
  event,
112
114
  userId,
113
- displayName: getState(state, "m.room.member", userId)?.content?.displayname ?? null,
115
+ displayName: getState(state, M.ROOM.MEMBER, userId)?.content?.displayname ?? null,
114
116
  memberCount: joined.length,
115
117
  senderLevel: Number.isFinite(senderLevel) ? senderLevel : Number.MAX_SAFE_INTEGER,
116
118
  notificationLevels: pl.notifications,
@@ -118,17 +120,33 @@ var PushService = class {
118
120
  });
119
121
  if (!actions || !actions.includes("notify")) continue;
120
122
  const tweaks = tweaksOf(actions);
121
- await this.hs.db.upsert("notifications", {
122
- user_id: userId,
123
- room_id: event.room_id,
124
- event_id: event.event_id,
125
- stream: ev.stream,
126
- highlight: tweaks.highlight === true,
127
- thread_id: threadIdOf(event),
128
- actions,
129
- ts: Date.now()
123
+ notified.push({
124
+ userId,
125
+ tweaks,
126
+ row: {
127
+ user_id: userId,
128
+ room_id: event.room_id,
129
+ event_id: event.event_id,
130
+ stream: ev.stream,
131
+ highlight: tweaks.highlight === true,
132
+ thread_id: thread,
133
+ actions,
134
+ ts: Date.now()
135
+ }
130
136
  });
131
- this.deliver(userId, ev, state, tweaks).catch((err) => this.hs.logger.warn("Push delivery failed", err));
137
+ }
138
+ if (notified.length === 0) return;
139
+ await this.hs.transaction(async (db) => {
140
+ for (const n of notified) await db.upsert("notifications", n.row);
141
+ });
142
+ const pushers = await this.hs.db.findMany("pushers", { where: [{
143
+ field: "user_id",
144
+ operator: "in",
145
+ value: notified.map((n) => n.userId)
146
+ }] });
147
+ for (const n of notified) {
148
+ const own = pushers.filter((p) => p.user_id === n.userId);
149
+ if (own.length) this.deliver(n.userId, ev, state, n.tweaks, own).catch((err) => this.hs.logger.warn("Push delivery failed", err));
132
150
  }
133
151
  }
134
152
  /** Unread notification counts since the user's read receipts. */
@@ -144,7 +162,7 @@ var PushService = class {
144
162
  let unthreaded = 0;
145
163
  const threadRead = /* @__PURE__ */ new Map();
146
164
  for (const r of receipts) {
147
- if (r.receipt_type !== "m.read" && r.receipt_type !== "m.read.private") continue;
165
+ if (r.receipt_type !== M.READ._ && r.receipt_type !== M.READ.PRIVATE) continue;
148
166
  const target = await this.hs.rooms.getEvent(r.event_id);
149
167
  if (!target) continue;
150
168
  if (r.thread_id === "") unthreaded = Math.max(unthreaded, target.stream);
@@ -195,12 +213,7 @@ var PushService = class {
195
213
  threads
196
214
  };
197
215
  }
198
- async deliver(userId, ev, state, tweaks) {
199
- const pushers = await this.hs.db.findMany("pushers", { where: [{
200
- field: "user_id",
201
- value: userId
202
- }] });
203
- if (pushers.length === 0) return;
216
+ async deliver(userId, ev, state, tweaks, pushers) {
204
217
  const event = ev.pdu;
205
218
  const unread = (await this.unreadCounts(userId, event.room_id)).room.notification_count;
206
219
  for (const row of pushers) {
@@ -224,16 +237,16 @@ var PushService = class {
224
237
  notification.type = event.type;
225
238
  notification.sender = event.sender;
226
239
  notification.content = event.content;
227
- const name = getState(state, "m.room.name")?.content?.name;
240
+ const name = getState(state, M.ROOM.NAME)?.content?.name;
228
241
  if (typeof name === "string") notification.room_name = name;
229
- const senderName = getState(state, "m.room.member", event.sender)?.content?.displayname;
242
+ const senderName = getState(state, M.ROOM.MEMBER, event.sender)?.content?.displayname;
230
243
  if (typeof senderName === "string") notification.sender_display_name = senderName;
231
- if (event.type === "m.room.member") {
244
+ if (event.type === M.ROOM.MEMBER) {
232
245
  notification.membership = event.content?.membership;
233
246
  notification.user_is_target = event.state_key === userId;
234
247
  }
235
248
  }
236
- const res = await this.hs.fetch(url, {
249
+ const res = await this.hs.publicFetch(url, {
237
250
  method: "POST",
238
251
  headers: { "content-type": "application/json" },
239
252
  body: JSON.stringify({ notification }),
@@ -1,4 +1,5 @@
1
1
  import { isObject } from "./json.js";
2
+ import { M, UNSTABLE } from "./m.js";
2
3
  //#region src/core/pushrules.ts
3
4
  const KINDS = [
4
5
  "override",
@@ -36,15 +37,15 @@ function defaultRules(userId) {
36
37
  });
37
38
  return {
38
39
  override: [
39
- r(".m.rule.master", [], [], { enabled: false }),
40
- r(".m.rule.suppress_notices", [em("content.msgtype", "m.notice")], []),
41
- r(".m.rule.invite_for_me", [
42
- em("type", "m.room.member"),
40
+ r(M.RULE.MASTER, [], [], { enabled: false }),
41
+ r(M.RULE.SUPPRESS_NOTICES, [em("content.msgtype", M.NOTICE)], []),
42
+ r(M.RULE.INVITE_FOR_ME, [
43
+ em("type", M.ROOM.MEMBER),
43
44
  em("content.membership", "invite"),
44
45
  em("state_key", userId)
45
46
  ], [...notify, sound("default")]),
46
- r(".m.rule.member_event", [em("type", "m.room.member")], []),
47
- r(".m.rule.is_user_mention", [{
47
+ r(M.RULE.MEMBER_EVENT, [em("type", M.ROOM.MEMBER)], []),
48
+ r(M.RULE.IS_USER_MENTION, [{
48
49
  kind: "event_property_contains",
49
50
  key: "content.m\\.mentions.user_ids",
50
51
  value: userId
@@ -53,61 +54,61 @@ function defaultRules(userId) {
53
54
  sound("default"),
54
55
  highlight
55
56
  ]),
56
- r(".m.rule.contains_display_name", [{ kind: "contains_display_name" }], [
57
+ r(M.RULE.CONTAINS_DISPLAY_NAME, [{ kind: "contains_display_name" }], [
57
58
  ...notify,
58
59
  sound("default"),
59
60
  highlight
60
61
  ]),
61
- r(".m.rule.is_room_mention", [ep("content.m\\.mentions.room", true), {
62
+ r(M.RULE.IS_ROOM_MENTION, [ep("content.m\\.mentions.room", true), {
62
63
  kind: "sender_notification_permission",
63
64
  key: "room"
64
65
  }], [...notify, highlight]),
65
- r(".m.rule.roomnotif", [em("content.body", "@room"), {
66
+ r(M.RULE.ROOMNOTIF, [em("content.body", "@room"), {
66
67
  kind: "sender_notification_permission",
67
68
  key: "room"
68
69
  }], [...notify, highlight]),
69
- r(".m.rule.tombstone", [em("type", "m.room.tombstone"), em("state_key", "")], [...notify, highlight]),
70
- r(".m.rule.reaction", [em("type", "m.reaction")], []),
71
- r(".m.rule.room.server_acl", [em("type", "m.room.server_acl"), em("state_key", "")], []),
72
- r(".m.rule.suppress_edits", [ep("content.m\\.relates_to.rel_type", "m.replace")], []),
73
- r(".org.matrix.msc3930.rule.poll_response", [em("type", "org.matrix.msc3381.poll.response")], [])
70
+ r(M.RULE.TOMBSTONE, [em("type", M.ROOM.TOMBSTONE), em("state_key", "")], [...notify, highlight]),
71
+ r(M.RULE.REACTION, [em("type", M.REACTION)], []),
72
+ r(M.RULE.ROOM.SERVER_ACL, [em("type", M.ROOM.SERVER_ACL), em("state_key", "")], []),
73
+ r(M.RULE.SUPPRESS_EDITS, [ep("content.m\\.relates_to.rel_type", M.REPLACE)], []),
74
+ r(UNSTABLE.ORG.MATRIX.MSC3930.RULE.POLL_RESPONSE, [em("type", UNSTABLE.ORG.MATRIX.MSC3381.POLL.RESPONSE)], [])
74
75
  ],
75
- content: [r(".m.rule.contains_user_name", void 0, [
76
+ content: [r(M.RULE.CONTAINS_USER_NAME, void 0, [
76
77
  ...notify,
77
78
  sound("default"),
78
79
  highlight
79
80
  ], { pattern: localpart })],
80
- postcontent: [r(".io.element.msc4306.rule.unsubscribed_thread", [{
81
- kind: "io.element.msc4306.thread_subscription",
81
+ postcontent: [r(UNSTABLE.IO.ELEMENT.MSC4306.RULE.UNSUBSCRIBED_THREAD, [{
82
+ kind: UNSTABLE.IO.ELEMENT.MSC4306.THREAD_SUBSCRIPTION,
82
83
  subscribed: false
83
- }], []), r(".io.element.msc4306.rule.subscribed_thread", [{
84
- kind: "io.element.msc4306.thread_subscription",
84
+ }], []), r(UNSTABLE.IO.ELEMENT.MSC4306.RULE.SUBSCRIBED_THREAD, [{
85
+ kind: UNSTABLE.IO.ELEMENT.MSC4306.THREAD_SUBSCRIPTION,
85
86
  subscribed: true
86
87
  }], [...notify, sound("default")])],
87
88
  room: [],
88
89
  sender: [],
89
90
  underride: [
90
- r(".m.rule.call", [em("type", "m.call.invite")], [...notify, sound("ring")]),
91
- r(".m.rule.encrypted_room_one_to_one", [{
91
+ r(M.RULE.CALL, [em("type", M.CALL.INVITE)], [...notify, sound("ring")]),
92
+ r(M.RULE.ENCRYPTED_ROOM_ONE_TO_ONE, [{
92
93
  kind: "room_member_count",
93
94
  is: "2"
94
- }, em("type", "m.room.encrypted")], [...notify, sound("default")]),
95
- r(".m.rule.room_one_to_one", [{
95
+ }, em("type", M.ROOM.ENCRYPTED)], [...notify, sound("default")]),
96
+ r(M.RULE.ROOM_ONE_TO_ONE, [{
96
97
  kind: "room_member_count",
97
98
  is: "2"
98
- }, em("type", "m.room.message")], [...notify, sound("default")]),
99
- r(".m.rule.message", [em("type", "m.room.message")], notify),
100
- r(".org.matrix.msc3930.rule.poll_start_one_to_one", [{
99
+ }, em("type", M.ROOM.MESSAGE)], [...notify, sound("default")]),
100
+ r(M.RULE.MESSAGE, [em("type", M.ROOM.MESSAGE)], notify),
101
+ r(UNSTABLE.ORG.MATRIX.MSC3930.RULE.POLL_START_ONE_TO_ONE, [{
101
102
  kind: "room_member_count",
102
103
  is: "2"
103
- }, em("type", "org.matrix.msc3381.poll.start")], [...notify, sound("default")]),
104
- r(".org.matrix.msc3930.rule.poll_end_one_to_one", [{
104
+ }, em("type", UNSTABLE.ORG.MATRIX.MSC3381.POLL.START)], [...notify, sound("default")]),
105
+ r(UNSTABLE.ORG.MATRIX.MSC3930.RULE.POLL_END_ONE_TO_ONE, [{
105
106
  kind: "room_member_count",
106
107
  is: "2"
107
- }, em("type", "org.matrix.msc3381.poll.end")], [...notify, sound("default")]),
108
- r(".org.matrix.msc3930.rule.poll_start", [em("type", "org.matrix.msc3381.poll.start")], notify),
109
- r(".org.matrix.msc3930.rule.poll_end", [em("type", "org.matrix.msc3381.poll.end")], notify),
110
- r(".m.rule.encrypted", [em("type", "m.room.encrypted")], notify)
108
+ }, em("type", UNSTABLE.ORG.MATRIX.MSC3381.POLL.END)], [...notify, sound("default")]),
109
+ r(UNSTABLE.ORG.MATRIX.MSC3930.RULE.POLL_START, [em("type", UNSTABLE.ORG.MATRIX.MSC3381.POLL.START)], notify),
110
+ r(UNSTABLE.ORG.MATRIX.MSC3930.RULE.POLL_END, [em("type", UNSTABLE.ORG.MATRIX.MSC3381.POLL.END)], notify),
111
+ r(M.RULE.ENCRYPTED, [em("type", M.ROOM.ENCRYPTED)], notify)
111
112
  ]
112
113
  };
113
114
  }
@@ -151,7 +152,7 @@ async function readRules(hs, userId) {
151
152
  },
152
153
  {
153
154
  field: "type",
154
- value: "m.push_rules"
155
+ value: M.PUSH_RULES
155
156
  }
156
157
  ]);
157
158
  if (!isObject(row?.content?.global)) return defaultRules(userId);
@@ -169,7 +170,7 @@ async function saveRules(hs, userId, rules) {
169
170
  await hs.streams.write((stream) => hs.db.upsert("account_data", {
170
171
  user_id: userId,
171
172
  room_id: "",
172
- type: "m.push_rules",
173
+ type: M.PUSH_RULES,
173
174
  content: { global: rules },
174
175
  stream
175
176
  }));
@@ -191,7 +192,7 @@ async function copyRules(hs, userId, oldRoomId, newRoomId) {
191
192
  },
192
193
  {
193
194
  field: "type",
194
- value: "m.push_rules"
195
+ value: M.PUSH_RULES
195
196
  }
196
197
  ]);
197
198
  if (!isObject(row?.content?.global)) return;
@@ -1,5 +1,5 @@
1
1
  //#region src/core/ratelimit.d.ts
2
- /** A rate limiter keyed by an arbitrary string. Implement this to share limits across instances (e.g. Redis). */
2
+ /** A rate limiter keyed by an arbitrary string. Implement this to share limits across instances. */
3
3
  export interface RateLimiter {
4
4
  /** Consume one unit for `key` under `rule`; resolve with the delay to wait (0 when allowed). */
5
5
  consume(key: string, rule: RateLimitRule): Promise<number>;
@@ -1,7 +1,7 @@
1
1
  import { ClientEvent, PDU } from "./events.js";
2
2
  import { StateMap } from "./authrules.js";
3
- import { StateIds } from "./stateres.js";
4
3
  import { Row } from "../storage/types.js";
4
+ import { StateIds } from "./stateres.js";
5
5
  import { Homeserver } from "../homeserver.js";
6
6
  //#region src/core/rooms.d.ts
7
7
  export interface StoredEvent {
@@ -140,6 +140,15 @@ export declare class RoomService {
140
140
  /** Users that should be woken up for events in this room. */
141
141
  private interestedUsers;
142
142
  roomsForUser(userId: string, membership?: string): Promise<string[]>;
143
+ /**
144
+ * Membership changes after `since` in the rooms where `userId` has a membership (in any state),
145
+ * given the user's own membership rows if the caller has them.
146
+ *
147
+ * https://github.com/k18k/matrix-nodejs-homeserver/issues/31: /sync and device-list tracking
148
+ * read every membership change on the server since their token and kept those in the user's
149
+ * rooms; while many users joined, each sync parsed thousands of rows. This reads those rooms only.
150
+ */
151
+ membershipChangesSince(userId: string, since: number, mine?: Row[]): Promise<Row[]>;
143
152
  /** All users sharing at least one joined room with `userId` (including themselves). */
144
153
  usersSharingRooms(userId: string): Promise<Set<string>>;
145
154
  private prepareRedaction;