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.
- package/LICENSE +75 -0
- package/README.md +479 -2
- package/dist/adapters/auth.d.ts +50 -0
- package/dist/adapters/auth.js +18 -0
- package/dist/adapters/cluster.d.ts +55 -0
- package/dist/adapters/cluster.js +249 -0
- package/dist/adapters/media.d.ts +50 -0
- package/dist/adapters/media.js +63 -0
- package/dist/adapters/s3.d.ts +33 -0
- package/dist/adapters/s3.js +95 -0
- package/dist/api/account.js +594 -0
- package/dist/api/admin.js +267 -0
- package/dist/api/appservice.js +45 -0
- package/dist/api/backup.js +280 -0
- package/dist/api/federation.js +826 -0
- package/dist/api/filters.js +88 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +183 -0
- package/dist/api/media.js +271 -0
- package/dist/api/misc.js +95 -0
- package/dist/api/preview.js +201 -0
- package/dist/api/profile.js +176 -0
- package/dist/api/push.js +202 -0
- package/dist/api/relationships.js +270 -0
- package/dist/api/rooms.js +1487 -0
- package/dist/api/rtc.d.ts +23 -0
- package/dist/api/rtc.js +66 -0
- package/dist/api/search.js +161 -0
- package/dist/api/slidingsync.js +474 -0
- package/dist/api/spaces.js +177 -0
- package/dist/api/sync.js +609 -0
- package/dist/api/threadsubs.js +140 -0
- package/dist/api/threepid.js +155 -0
- package/dist/api/uia.js +127 -0
- package/dist/api/user.js +329 -0
- package/dist/core/admin.d.ts +84 -0
- package/dist/core/admin.js +308 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +483 -0
- package/dist/core/appservices.d.ts +74 -0
- package/dist/core/appservices.js +369 -0
- package/dist/core/audit.d.ts +50 -0
- package/dist/core/audit.js +86 -0
- package/dist/core/auth.d.ts +48 -0
- package/dist/core/auth.js +237 -0
- package/dist/core/authrules.d.ts +4 -0
- package/dist/core/authrules.js +348 -0
- package/dist/core/cluster.d.ts +66 -0
- package/dist/core/cluster.js +191 -0
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/crypto.js +100 -0
- package/dist/core/delayed.d.ts +39 -0
- package/dist/core/delayed.js +160 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/events.d.ts +32 -0
- package/dist/core/events.js +186 -0
- package/dist/core/glob.js +66 -0
- package/dist/core/identity.d.ts +24 -0
- package/dist/core/identity.js +96 -0
- package/dist/core/ids.js +36 -0
- package/dist/core/invitefilter.js +36 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +450 -0
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +54 -0
- package/dist/core/oauth.d.ts +38 -0
- package/dist/core/oauth.js +108 -0
- package/dist/core/partialstate.d.ts +32 -0
- package/dist/core/partialstate.js +198 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +97 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +46 -0
- package/dist/core/push.js +307 -0
- package/dist/core/pushrules.js +229 -0
- package/dist/core/ratelimit.d.ts +23 -0
- package/dist/core/ratelimit.js +77 -0
- package/dist/core/rooms.d.ts +215 -0
- package/dist/core/rooms.js +1731 -0
- package/dist/core/settings.d.ts +11 -0
- package/dist/core/settings.js +29 -0
- package/dist/core/sso.d.ts +46 -0
- package/dist/core/sso.js +165 -0
- package/dist/core/stateres.d.ts +4 -0
- package/dist/core/stateres.js +209 -0
- package/dist/core/threepid.d.ts +80 -0
- package/dist/core/threepid.js +275 -0
- package/dist/core/typing.d.ts +16 -0
- package/dist/core/typing.js +60 -0
- package/dist/federation/client.d.ts +51 -0
- package/dist/federation/client.js +279 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1059 -0
- package/dist/federation/keyring.d.ts +26 -0
- package/dist/federation/keyring.js +122 -0
- package/dist/federation/sender.d.ts +26 -0
- package/dist/federation/sender.js +164 -0
- package/dist/homeserver.d.ts +224 -0
- package/dist/homeserver.js +302 -0
- package/dist/http/adapters.d.ts +40 -0
- package/dist/http/adapters.js +130 -0
- package/dist/http/handler.d.ts +8 -0
- package/dist/http/handler.js +181 -0
- package/dist/http/input.js +24 -0
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +43 -0
- package/dist/http/router.js +56 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +15 -0
- package/dist/storage/memory.d.ts +5 -0
- package/dist/storage/memory.js +122 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.js +664 -0
- package/dist/storage/sql.d.ts +72 -0
- package/dist/storage/sql.js +274 -0
- package/dist/storage/types.d.ts +55 -0
- package/dist/vendor/zod/LICENSE +21 -0
- package/dist/vendor/zod/v4/classic/errors.js +54 -0
- package/dist/vendor/zod/v4/classic/parse.js +17 -0
- package/dist/vendor/zod/v4/classic/schemas.js +865 -0
- package/dist/vendor/zod/v4/core/api.js +512 -0
- package/dist/vendor/zod/v4/core/checks.js +300 -0
- package/dist/vendor/zod/v4/core/core.js +109 -0
- package/dist/vendor/zod/v4/core/doc.js +35 -0
- package/dist/vendor/zod/v4/core/errors.js +118 -0
- package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
- package/dist/vendor/zod/v4/core/memoizer.js +255 -0
- package/dist/vendor/zod/v4/core/parse.js +206 -0
- package/dist/vendor/zod/v4/core/regexes.js +73 -0
- package/dist/vendor/zod/v4/core/registries.js +48 -0
- package/dist/vendor/zod/v4/core/schemas.js +1378 -0
- package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
- package/dist/vendor/zod/v4/core/util.js +537 -0
- package/dist/vendor/zod/v4/core/versions.js +8 -0
- package/dist/vendor/zod/v4/locales/en.js +110 -0
- package/package.json +65 -4
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { isObject } from "../core/json.js";
|
|
2
|
+
import { M, UNSTABLE } from "../core/m.js";
|
|
3
|
+
import { splitStateKey, toClientEvent } from "../core/events.js";
|
|
4
|
+
import { MatrixError } from "../core/errors.js";
|
|
5
|
+
import { getState } from "../core/authrules.js";
|
|
6
|
+
import { record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
|
|
7
|
+
import { body, lenient } from "../http/input.js";
|
|
8
|
+
import { procedure } from "../http/procedure.js";
|
|
9
|
+
import { roomEphemeral, roomTimeline, toStripped } from "./sync.js";
|
|
10
|
+
//#region src/api/slidingsync.ts
|
|
11
|
+
const slidingSyncBody = body({
|
|
12
|
+
conn_id: lenient(string()),
|
|
13
|
+
lists: lenient(record(string(), unknown())),
|
|
14
|
+
room_subscriptions: lenient(record(string(), unknown())),
|
|
15
|
+
extensions: lenient(record(string(), unknown()))
|
|
16
|
+
});
|
|
17
|
+
const BUMP_TYPES = /* @__PURE__ */ new Set([
|
|
18
|
+
M.ROOM.CREATE,
|
|
19
|
+
M.ROOM.MESSAGE,
|
|
20
|
+
M.ROOM.ENCRYPTED,
|
|
21
|
+
M.STICKER,
|
|
22
|
+
M.CALL.INVITE,
|
|
23
|
+
M.POLL.START,
|
|
24
|
+
M.BEACON_INFO
|
|
25
|
+
]);
|
|
26
|
+
async function loadConnection(hs, s, connId) {
|
|
27
|
+
return hs.db.findOne("sliding_sync_connections", [
|
|
28
|
+
{
|
|
29
|
+
field: "user_id",
|
|
30
|
+
value: s.userId
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
field: "device_id",
|
|
34
|
+
value: s.deviceId
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
field: "conn_id",
|
|
38
|
+
value: connId
|
|
39
|
+
}
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
/** Stream position of the latest "interesting" event in a room. */
|
|
43
|
+
async function bumpStamp(hs, roomId, upTo) {
|
|
44
|
+
const rows = await hs.db.findMany("events", {
|
|
45
|
+
where: [{
|
|
46
|
+
field: "room_id",
|
|
47
|
+
value: roomId
|
|
48
|
+
}, {
|
|
49
|
+
field: "stream",
|
|
50
|
+
operator: "lte",
|
|
51
|
+
value: upTo
|
|
52
|
+
}],
|
|
53
|
+
sortBy: {
|
|
54
|
+
field: "stream",
|
|
55
|
+
direction: "desc"
|
|
56
|
+
},
|
|
57
|
+
limit: 50
|
|
58
|
+
});
|
|
59
|
+
for (const r of rows) if (BUMP_TYPES.has(r.type)) return r.stream;
|
|
60
|
+
return rows[rows.length - 1]?.stream ?? 0;
|
|
61
|
+
}
|
|
62
|
+
function matchesRequiredState(required, type, key, userId, lazyMembers) {
|
|
63
|
+
for (const [t, k] of required) {
|
|
64
|
+
if (t !== "*" && t !== type) continue;
|
|
65
|
+
if (k === "*" || k === key) return true;
|
|
66
|
+
if (k === "$ME" && key === userId) return true;
|
|
67
|
+
if (k === "$LAZY" && type === M.ROOM.MEMBER && lazyMembers.has(key)) return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
function registerSlidingSyncRoutes(r) {
|
|
72
|
+
r.post("/_matrix/client/unstable/org.matrix.simplified_msc3575/sync", procedure({
|
|
73
|
+
auth: "user",
|
|
74
|
+
input: slidingSyncBody,
|
|
75
|
+
handler: slidingSync
|
|
76
|
+
}));
|
|
77
|
+
r.post("/_matrix/client/v5/sync", procedure({
|
|
78
|
+
auth: "user",
|
|
79
|
+
input: slidingSyncBody,
|
|
80
|
+
handler: slidingSync
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
async function slidingSync(ctx) {
|
|
84
|
+
const hs = ctx.hs;
|
|
85
|
+
const s = ctx.session;
|
|
86
|
+
const body = ctx.input;
|
|
87
|
+
const connId = body.conn_id ?? "";
|
|
88
|
+
const posParam = ctx.query.get("pos");
|
|
89
|
+
const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
|
|
90
|
+
let conn = {
|
|
91
|
+
rooms: {},
|
|
92
|
+
prevPos: null,
|
|
93
|
+
toDeviceSince: 0
|
|
94
|
+
};
|
|
95
|
+
let since = null;
|
|
96
|
+
if (posParam !== null) {
|
|
97
|
+
const row = await loadConnection(hs, s, connId);
|
|
98
|
+
const pos = Number(posParam);
|
|
99
|
+
if (!row || !Number.isInteger(pos) || pos !== row.pos && pos !== row.state.prevPos) throw new MatrixError(400, "M_UNKNOWN_POS", "Unknown position; start a new connection");
|
|
100
|
+
conn = row.state;
|
|
101
|
+
since = pos;
|
|
102
|
+
if (pos === row.state.prevPos && pos !== row.pos) conn = {
|
|
103
|
+
...conn,
|
|
104
|
+
rooms: {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const deadline = Date.now() + timeout;
|
|
108
|
+
for (;;) {
|
|
109
|
+
const to = await hs.streams.position();
|
|
110
|
+
const { response, empty, next } = await buildResponse(hs, s, body, conn, since, to);
|
|
111
|
+
const remaining = deadline - Date.now();
|
|
112
|
+
if (since === null || !empty || remaining <= 0) {
|
|
113
|
+
await hs.db.upsert("sliding_sync_connections", {
|
|
114
|
+
user_id: s.userId,
|
|
115
|
+
device_id: s.deviceId,
|
|
116
|
+
conn_id: connId,
|
|
117
|
+
pos: to,
|
|
118
|
+
state: {
|
|
119
|
+
...next,
|
|
120
|
+
prevPos: since
|
|
121
|
+
},
|
|
122
|
+
updated_ts: Date.now()
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
pos: String(to),
|
|
126
|
+
...response
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (await hs.streams.position() > to) continue;
|
|
130
|
+
await hs.notifier.wait(s.userId, remaining, ctx.request.signal);
|
|
131
|
+
if (ctx.request.signal?.aborted) return {
|
|
132
|
+
pos: String(since),
|
|
133
|
+
lists: {},
|
|
134
|
+
rooms: {},
|
|
135
|
+
extensions: {}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function buildResponse(hs, s, body, conn, since, to) {
|
|
140
|
+
const userId = s.userId;
|
|
141
|
+
const memberships = await hs.db.findMany("memberships", { where: [{
|
|
142
|
+
field: "user_id",
|
|
143
|
+
value: userId
|
|
144
|
+
}] });
|
|
145
|
+
const direct = await hs.db.findOne("account_data", [
|
|
146
|
+
{
|
|
147
|
+
field: "user_id",
|
|
148
|
+
value: userId
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
field: "room_id",
|
|
152
|
+
value: ""
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
field: "type",
|
|
156
|
+
value: M.DIRECT
|
|
157
|
+
}
|
|
158
|
+
]);
|
|
159
|
+
const dmRooms = new Set(Object.values(direct?.content ?? {}).flat().filter((x) => typeof x === "string"));
|
|
160
|
+
const candidates = memberships.filter((m) => m.stream <= to && ([
|
|
161
|
+
"join",
|
|
162
|
+
"invite",
|
|
163
|
+
"knock"
|
|
164
|
+
].includes(m.membership) || since !== null && m.stream > since && !m.forgotten));
|
|
165
|
+
const infos = [];
|
|
166
|
+
for (const m of candidates) {
|
|
167
|
+
const state = await hs.rooms.currentState(m.room_id);
|
|
168
|
+
const bump = m.membership === "join" ? await bumpStamp(hs, m.room_id, to) : m.stream;
|
|
169
|
+
infos.push({
|
|
170
|
+
roomId: m.room_id,
|
|
171
|
+
membership: m.membership,
|
|
172
|
+
bump,
|
|
173
|
+
state
|
|
174
|
+
});
|
|
175
|
+
const invite = getState(state, M.ROOM.MEMBER, userId);
|
|
176
|
+
if (m.membership === "invite" && invite?.content?.is_direct) dmRooms.add(m.room_id);
|
|
177
|
+
}
|
|
178
|
+
infos.sort((a, b) => b.bump - a.bump);
|
|
179
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
180
|
+
const lists = {};
|
|
181
|
+
for (const [name, raw] of Object.entries(isObject(body.lists) ? body.lists : {})) {
|
|
182
|
+
const list = raw;
|
|
183
|
+
const f = list.filters ?? {};
|
|
184
|
+
let rooms = infos;
|
|
185
|
+
if (f.is_dm !== void 0) rooms = rooms.filter((r) => dmRooms.has(r.roomId) === f.is_dm);
|
|
186
|
+
if (f.is_invite !== void 0) rooms = rooms.filter((r) => r.membership === "invite" === f.is_invite);
|
|
187
|
+
if (f.is_encrypted !== void 0) rooms = rooms.filter((r) => !!getState(r.state, M.ROOM.ENCRYPTION) === f.is_encrypted);
|
|
188
|
+
const roomType = (r) => getState(r.state, M.ROOM.CREATE)?.content?.type ?? null;
|
|
189
|
+
if (f.room_types) rooms = rooms.filter((r) => f.room_types.includes(roomType(r)));
|
|
190
|
+
if (f.not_room_types) rooms = rooms.filter((r) => !f.not_room_types.includes(roomType(r)));
|
|
191
|
+
if (f.spaces) {
|
|
192
|
+
const children = /* @__PURE__ */ new Set();
|
|
193
|
+
for (const space of f.spaces) {
|
|
194
|
+
if (!await hs.rooms.exists(space)) continue;
|
|
195
|
+
for (const key of (await hs.rooms.currentState(space)).keys()) {
|
|
196
|
+
const [t, k] = splitStateKey(key);
|
|
197
|
+
if (t === M.SPACE.CHILD) children.add(k);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
rooms = rooms.filter((r) => children.has(r.roomId));
|
|
201
|
+
}
|
|
202
|
+
lists[name] = { count: rooms.length };
|
|
203
|
+
for (const [start, end] of list.ranges ?? [[0, rooms.length - 1]]) for (const room of rooms.slice(Math.max(0, start), Math.max(0, end) + 1)) {
|
|
204
|
+
const prev = wanted.get(room.roomId);
|
|
205
|
+
wanted.set(room.roomId, mergeConfig(prev, list));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const [roomId, raw] of Object.entries(isObject(body.room_subscriptions) ? body.room_subscriptions : {})) {
|
|
209
|
+
if (!infos.find((i) => i.roomId === roomId)) {
|
|
210
|
+
if (!await hs.rooms.exists(roomId) || !(await hs.rooms.canPeek(roomId, userId)).worldReadable) continue;
|
|
211
|
+
infos.push({
|
|
212
|
+
roomId,
|
|
213
|
+
membership: "leave",
|
|
214
|
+
bump: 0,
|
|
215
|
+
state: await hs.rooms.currentState(roomId)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
wanted.set(roomId, mergeConfig(wanted.get(roomId), raw));
|
|
219
|
+
}
|
|
220
|
+
const next = {
|
|
221
|
+
rooms: { ...conn.rooms },
|
|
222
|
+
prevPos: conn.prevPos,
|
|
223
|
+
toDeviceSince: conn.toDeviceSince
|
|
224
|
+
};
|
|
225
|
+
const rooms = {};
|
|
226
|
+
for (const [roomId, config] of wanted) {
|
|
227
|
+
const info = infos.find((i) => i.roomId === roomId);
|
|
228
|
+
const configKey = JSON.stringify(config);
|
|
229
|
+
const known = since !== null ? conn.rooms[roomId] : void 0;
|
|
230
|
+
const initial = !known || known.config !== configKey;
|
|
231
|
+
const room = await buildRoom(hs, s, info, config, initial ? null : since, to, dmRooms.has(roomId));
|
|
232
|
+
const counts = JSON.stringify([
|
|
233
|
+
room.notification_count,
|
|
234
|
+
room.highlight_count,
|
|
235
|
+
room.joined_count,
|
|
236
|
+
room.invited_count
|
|
237
|
+
]);
|
|
238
|
+
if (!initial && room.timeline.length === 0 && room.required_state.length === 0 && known.counts === counts) continue;
|
|
239
|
+
if (initial) room.initial = true;
|
|
240
|
+
if (since !== null && !initial) room.num_live = room.timeline.filter((e) => e.__stream > since).length;
|
|
241
|
+
for (const e of room.timeline) delete e.__stream;
|
|
242
|
+
rooms[roomId] = room;
|
|
243
|
+
next.rooms[roomId] = {
|
|
244
|
+
config: configKey,
|
|
245
|
+
counts
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const extensions = await buildExtensions(hs, s, body.extensions ?? {}, since, to, Object.keys(rooms), next);
|
|
249
|
+
const empty = Object.keys(rooms).length === 0 && Object.values(extensions).every((e) => isEmptyExtension(e));
|
|
250
|
+
return {
|
|
251
|
+
response: {
|
|
252
|
+
lists,
|
|
253
|
+
rooms,
|
|
254
|
+
extensions
|
|
255
|
+
},
|
|
256
|
+
empty,
|
|
257
|
+
next
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function mergeConfig(a, b) {
|
|
261
|
+
if (!a) return {
|
|
262
|
+
required_state: b.required_state ?? [],
|
|
263
|
+
timeline_limit: b.timeline_limit ?? 0
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
required_state: [...a.required_state ?? [], ...b.required_state ?? []],
|
|
267
|
+
timeline_limit: Math.max(a.timeline_limit ?? 0, b.timeline_limit ?? 0)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async function buildRoom(hs, s, info, config, since, to, isDm) {
|
|
271
|
+
const { roomId, state } = info;
|
|
272
|
+
const userId = s.userId;
|
|
273
|
+
const room = {
|
|
274
|
+
bump_stamp: info.bump,
|
|
275
|
+
timeline: [],
|
|
276
|
+
required_state: []
|
|
277
|
+
};
|
|
278
|
+
const name = getState(state, M.ROOM.NAME)?.content?.name;
|
|
279
|
+
if (typeof name === "string") room.name = name;
|
|
280
|
+
const avatar = getState(state, M.ROOM.AVATAR)?.content?.url;
|
|
281
|
+
if (typeof avatar === "string") room.avatar = avatar;
|
|
282
|
+
if (isDm) room.is_dm = true;
|
|
283
|
+
if (info.membership === "invite" || info.membership === "knock") {
|
|
284
|
+
const own = getState(state, M.ROOM.MEMBER, userId);
|
|
285
|
+
const ev = own?.event_id ? await hs.rooms.getEvent(own.event_id) : null;
|
|
286
|
+
if (ev) {
|
|
287
|
+
const key = info.membership === "invite" ? "invite_room_state" : "knock_room_state";
|
|
288
|
+
room[info.membership === "invite" ? "invite_state" : "knock_state"] = [...ev.pdu.unsigned?.[key] ?? [], ev.pdu].map(toStripped);
|
|
289
|
+
}
|
|
290
|
+
return room;
|
|
291
|
+
}
|
|
292
|
+
const p = {
|
|
293
|
+
session: s,
|
|
294
|
+
ignored: /* @__PURE__ */ new Set(),
|
|
295
|
+
since: since === null ? null : {
|
|
296
|
+
pos: since,
|
|
297
|
+
toDevice: 0
|
|
298
|
+
},
|
|
299
|
+
to,
|
|
300
|
+
filter: { room: { timeline: { limit: config.timeline_limit ?? 0 } } },
|
|
301
|
+
fullState: false
|
|
302
|
+
};
|
|
303
|
+
const upTo = info.membership === "join" ? to : Math.min(to, (await hs.db.findOne("memberships", [{
|
|
304
|
+
field: "room_id",
|
|
305
|
+
value: roomId
|
|
306
|
+
}, {
|
|
307
|
+
field: "user_id",
|
|
308
|
+
value: userId
|
|
309
|
+
}]))?.stream ?? to);
|
|
310
|
+
if ((config.timeline_limit ?? 0) > 0) {
|
|
311
|
+
const { timeline, timelineEvents } = await roomTimeline(hs, p, roomId, since, upTo);
|
|
312
|
+
room.timeline = timeline.events.map((e, i) => ({
|
|
313
|
+
...e,
|
|
314
|
+
room_id: void 0,
|
|
315
|
+
__stream: timelineEvents[i].stream
|
|
316
|
+
}));
|
|
317
|
+
room.limited = timeline.limited;
|
|
318
|
+
room.prev_batch = timeline.prev_batch;
|
|
319
|
+
}
|
|
320
|
+
const lazy = /* @__PURE__ */ new Set([userId, ...room.timeline.map((e) => e.sender)]);
|
|
321
|
+
const required = config.required_state ?? [];
|
|
322
|
+
for (const [key, ev] of state) {
|
|
323
|
+
const [type, sk] = splitStateKey(key);
|
|
324
|
+
if (!matchesRequiredState(required, type, sk, userId, lazy)) continue;
|
|
325
|
+
if (since !== null) {
|
|
326
|
+
const stored = ev.event_id ? await hs.rooms.getEvent(ev.event_id) : null;
|
|
327
|
+
if (!stored || stored.stream <= since) continue;
|
|
328
|
+
}
|
|
329
|
+
room.required_state.push(toClientEvent(ev, { includeRoomId: false }));
|
|
330
|
+
}
|
|
331
|
+
let joined = 0;
|
|
332
|
+
let invited = 0;
|
|
333
|
+
const heroes = [];
|
|
334
|
+
for (const [key, ev] of state) {
|
|
335
|
+
const [type, sk] = splitStateKey(key);
|
|
336
|
+
if (type !== M.ROOM.MEMBER) continue;
|
|
337
|
+
const m = ev.content?.membership;
|
|
338
|
+
if (m === "join") joined++;
|
|
339
|
+
if (m === "invite") invited++;
|
|
340
|
+
if ((m === "join" || m === "invite") && sk !== userId && heroes.length < 5) heroes.push({
|
|
341
|
+
user_id: sk,
|
|
342
|
+
displayname: ev.content?.displayname,
|
|
343
|
+
avatar_url: ev.content?.avatar_url
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
room.joined_count = joined;
|
|
347
|
+
room.invited_count = invited;
|
|
348
|
+
if (!room.name) room.heroes = heroes;
|
|
349
|
+
const counts = await hs.push.unreadCounts(userId, roomId);
|
|
350
|
+
room.notification_count = counts.room.notification_count + Object.values(counts.threads).reduce((n, t) => n + t.notification_count, 0);
|
|
351
|
+
room.highlight_count = counts.room.highlight_count + Object.values(counts.threads).reduce((n, t) => n + t.highlight_count, 0);
|
|
352
|
+
return room;
|
|
353
|
+
}
|
|
354
|
+
async function buildExtensions(hs, s, ext, since, to, roomIds, next) {
|
|
355
|
+
const out = {};
|
|
356
|
+
const userId = s.userId;
|
|
357
|
+
if (ext.to_device?.enabled) {
|
|
358
|
+
const where = [{
|
|
359
|
+
field: "user_id",
|
|
360
|
+
value: userId
|
|
361
|
+
}, {
|
|
362
|
+
field: "device_id",
|
|
363
|
+
value: s.deviceId
|
|
364
|
+
}];
|
|
365
|
+
const ackUpTo = Number(ext.to_device.since ?? next.toDeviceSince) || 0;
|
|
366
|
+
if (ackUpTo) await hs.db.delete("to_device", [...where, {
|
|
367
|
+
field: "stream",
|
|
368
|
+
operator: "lte",
|
|
369
|
+
value: ackUpTo
|
|
370
|
+
}]);
|
|
371
|
+
const rows = (await hs.db.findMany("to_device", {
|
|
372
|
+
where: [...where, {
|
|
373
|
+
field: "stream",
|
|
374
|
+
operator: "gt",
|
|
375
|
+
value: ackUpTo
|
|
376
|
+
}],
|
|
377
|
+
sortBy: {
|
|
378
|
+
field: "stream",
|
|
379
|
+
direction: "asc"
|
|
380
|
+
},
|
|
381
|
+
limit: Math.min(Number(ext.to_device.limit ?? 100) || 100, 1e3)
|
|
382
|
+
})).filter((r) => r.stream <= to);
|
|
383
|
+
const nextBatch = rows.length ? rows[rows.length - 1].stream : ackUpTo;
|
|
384
|
+
next.toDeviceSince = nextBatch;
|
|
385
|
+
out.to_device = {
|
|
386
|
+
next_batch: String(nextBatch),
|
|
387
|
+
events: rows.map((r) => ({
|
|
388
|
+
sender: r.sender,
|
|
389
|
+
type: r.type,
|
|
390
|
+
content: r.content
|
|
391
|
+
}))
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (ext.e2ee?.enabled) {
|
|
395
|
+
const e2ee = {
|
|
396
|
+
device_one_time_keys_count: await hs.keys.oneTimeKeyCounts(userId, s.deviceId),
|
|
397
|
+
device_unused_fallback_key_types: await hs.keys.unusedFallbackKeyTypes(userId, s.deviceId)
|
|
398
|
+
};
|
|
399
|
+
if (since !== null) e2ee.device_lists = await hs.keys.changesSince(userId, since, to);
|
|
400
|
+
out.e2ee = e2ee;
|
|
401
|
+
}
|
|
402
|
+
if (ext.account_data?.enabled) {
|
|
403
|
+
const where = [{
|
|
404
|
+
field: "user_id",
|
|
405
|
+
value: userId
|
|
406
|
+
}];
|
|
407
|
+
if (since !== null) where.push({
|
|
408
|
+
field: "stream",
|
|
409
|
+
operator: "gt",
|
|
410
|
+
value: since
|
|
411
|
+
});
|
|
412
|
+
const rows = (await hs.db.findMany("account_data", { where })).filter((r) => r.stream <= to);
|
|
413
|
+
const rooms = {};
|
|
414
|
+
const global = [];
|
|
415
|
+
for (const r of rows) {
|
|
416
|
+
const ev = {
|
|
417
|
+
type: r.type,
|
|
418
|
+
content: r.content
|
|
419
|
+
};
|
|
420
|
+
if (r.room_id === "") global.push(ev);
|
|
421
|
+
else if (roomIds.includes(r.room_id) || since !== null) (rooms[r.room_id] ??= []).push(ev);
|
|
422
|
+
}
|
|
423
|
+
if (since === null && !global.some((e) => e.type === M.PUSH_RULES)) {
|
|
424
|
+
const { loadRules } = await import("../core/pushrules.js");
|
|
425
|
+
global.push({
|
|
426
|
+
type: M.PUSH_RULES,
|
|
427
|
+
content: { global: await loadRules(hs, userId) }
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
out.account_data = {
|
|
431
|
+
global,
|
|
432
|
+
rooms
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (ext[UNSTABLE.IO.ELEMENT.MSC4308.THREAD_SUBSCRIPTIONS]?.enabled) {
|
|
436
|
+
const { threadSubscriptionsExtension } = await import("./threadsubs.js");
|
|
437
|
+
out[UNSTABLE.IO.ELEMENT.MSC4308.THREAD_SUBSCRIPTIONS] = await threadSubscriptionsExtension(hs, userId, ext[UNSTABLE.IO.ELEMENT.MSC4308.THREAD_SUBSCRIPTIONS], since, to);
|
|
438
|
+
}
|
|
439
|
+
if (ext.receipts?.enabled || ext.typing?.enabled) {
|
|
440
|
+
const p = {
|
|
441
|
+
session: s,
|
|
442
|
+
ignored: /* @__PURE__ */ new Set(),
|
|
443
|
+
since: since === null ? null : {
|
|
444
|
+
pos: since,
|
|
445
|
+
toDevice: 0
|
|
446
|
+
},
|
|
447
|
+
to,
|
|
448
|
+
filter: {},
|
|
449
|
+
fullState: false
|
|
450
|
+
};
|
|
451
|
+
const receipts = {};
|
|
452
|
+
const typing = {};
|
|
453
|
+
const joined = new Set(await hs.rooms.roomsForUser(userId));
|
|
454
|
+
for (const roomId of since === null ? roomIds : [...joined]) {
|
|
455
|
+
if (!joined.has(roomId)) continue;
|
|
456
|
+
for (const e of await roomEphemeral(hs, p, roomId)) {
|
|
457
|
+
if (e.type === M.RECEIPT && ext.receipts?.enabled) receipts[roomId] = e;
|
|
458
|
+
if (e.type === M.TYPING && ext.typing?.enabled && (since !== null || e.content.user_ids.length)) typing[roomId] = e;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (ext.receipts?.enabled) out.receipts = { rooms: receipts };
|
|
462
|
+
if (ext.typing?.enabled) out.typing = { rooms: typing };
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
function isEmptyExtension(e) {
|
|
467
|
+
if (Array.isArray(e.events)) return e.events.length === 0;
|
|
468
|
+
if (e.rooms && Object.keys(e.rooms).length) return false;
|
|
469
|
+
if (e.global && e.global.length) return false;
|
|
470
|
+
if (e.device_lists && (e.device_lists.changed.length || e.device_lists.left.length)) return false;
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
//#endregion
|
|
474
|
+
export { registerSlidingSyncRoutes };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { M } from "../core/m.js";
|
|
2
|
+
import { splitStateKey, stripState } from "../core/events.js";
|
|
3
|
+
import { errors } from "../core/errors.js";
|
|
4
|
+
import { getState, membershipOf } from "../core/authrules.js";
|
|
5
|
+
import { procedure } from "../http/procedure.js";
|
|
6
|
+
//#region src/api/spaces.ts
|
|
7
|
+
/** Room-level details shared by /hierarchy, /room_summary and /publicRooms. */
|
|
8
|
+
async function roomDetails(hs, roomId) {
|
|
9
|
+
const state = await hs.rooms.currentState(roomId);
|
|
10
|
+
const get = (t) => getState(state, t)?.content;
|
|
11
|
+
const out = {
|
|
12
|
+
room_id: roomId,
|
|
13
|
+
num_joined_members: (await hs.rooms.joinedMembers(roomId)).length,
|
|
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"
|
|
17
|
+
};
|
|
18
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
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;
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/** Whether a user may see a room's summary (spec: "accessible" rooms). */
|
|
27
|
+
async function canAccess(hs, roomId, userId) {
|
|
28
|
+
const state = await hs.rooms.currentState(roomId);
|
|
29
|
+
if (userId && [
|
|
30
|
+
"join",
|
|
31
|
+
"invite",
|
|
32
|
+
"knock"
|
|
33
|
+
].includes(membershipOf(state, userId))) return true;
|
|
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;
|
|
36
|
+
const rule = joinRules?.join_rule ?? "invite";
|
|
37
|
+
if (rule === "public" || rule === "knock") return true;
|
|
38
|
+
if ((rule === "restricted" || rule === "knock_restricted") && userId) {
|
|
39
|
+
if (rule === "knock_restricted") return true;
|
|
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)) {
|
|
41
|
+
if (await hs.rooms.membership(allow.room_id, userId) === "join") return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
function allowedRoomIds(joinRules) {
|
|
47
|
+
if (!joinRules || !["restricted", "knock_restricted"].includes(joinRules.join_rule)) return void 0;
|
|
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);
|
|
49
|
+
}
|
|
50
|
+
/** Spec ordering of space children: valid `order` strings first, then child event timestamp, then room ID. */
|
|
51
|
+
function childOrder(a, b) {
|
|
52
|
+
const valid = (o) => typeof o === "string" && o.length <= 50 && /^[\x20-\x7E]*$/.test(o) ? o : null;
|
|
53
|
+
const oa = valid(a.content?.order);
|
|
54
|
+
const ob = valid(b.content?.order);
|
|
55
|
+
if (oa !== null && ob === null) return -1;
|
|
56
|
+
if (oa === null && ob !== null) return 1;
|
|
57
|
+
if (oa !== null && ob !== null && oa !== ob) return oa < ob ? -1 : 1;
|
|
58
|
+
return (a.origin_server_ts ?? 0) - (b.origin_server_ts ?? 0) || (a.state_key < b.state_key ? -1 : a.state_key > b.state_key ? 1 : 0);
|
|
59
|
+
}
|
|
60
|
+
/** Fetch a room's summary from another server via federation /hierarchy, if the user may see it. */
|
|
61
|
+
async function remoteSummary(hs, roomId, via, suggestedOnly, userId) {
|
|
62
|
+
if (!hs.federation.enabled) return null;
|
|
63
|
+
for (const server of via.filter((v) => v !== hs.serverName)) try {
|
|
64
|
+
const room = (await hs.federation.queryHierarchy(server, roomId, suggestedOnly)).room;
|
|
65
|
+
if (!room || room.room_id !== roomId) continue;
|
|
66
|
+
const rule = room.join_rule ?? "invite";
|
|
67
|
+
let accessible = room.world_readable === true || [
|
|
68
|
+
"public",
|
|
69
|
+
"knock",
|
|
70
|
+
"knock_restricted"
|
|
71
|
+
].includes(rule);
|
|
72
|
+
if (!accessible && rule === "restricted" && Array.isArray(room.allowed_room_ids)) {
|
|
73
|
+
for (const allowed of room.allowed_room_ids) if (await hs.rooms.exists(allowed) && await hs.rooms.membership(allowed, userId) === "join") {
|
|
74
|
+
accessible = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return accessible ? room : null;
|
|
79
|
+
} catch {}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function registerSpaceRoutes(r) {
|
|
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({
|
|
138
|
+
roomId: child.state_key,
|
|
139
|
+
depth: depth + 1,
|
|
140
|
+
via: child.content.via
|
|
141
|
+
});
|
|
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);
|
|
147
|
+
}
|
|
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;
|
|
151
|
+
}
|
|
152
|
+
}), ["v1"]);
|
|
153
|
+
const summary = async (ctx) => {
|
|
154
|
+
const s = await ctx.optionalAuth();
|
|
155
|
+
const hs = ctx.hs;
|
|
156
|
+
let roomId;
|
|
157
|
+
try {
|
|
158
|
+
roomId = await hs.rooms.resolveRoomIdOrAlias(ctx.params.roomIdOrAlias);
|
|
159
|
+
} catch {
|
|
160
|
+
throw errors.notFound("Room not found");
|
|
161
|
+
}
|
|
162
|
+
if (!await hs.rooms.exists(roomId) || !await canAccess(hs, roomId, s?.userId ?? null)) throw errors.notFound("Room not found");
|
|
163
|
+
const state = await hs.rooms.currentState(roomId);
|
|
164
|
+
const out = await roomDetails(hs, roomId);
|
|
165
|
+
out.room_version = await hs.rooms.roomVersion(roomId);
|
|
166
|
+
const enc = getState(state, M.ROOM.ENCRYPTION)?.content?.algorithm;
|
|
167
|
+
if (typeof enc === "string") out.encryption = enc;
|
|
168
|
+
if (s) out.membership = membershipOf(state, s.userId);
|
|
169
|
+
const allowed = allowedRoomIds(getState(state, M.ROOM.JOIN_RULES)?.content);
|
|
170
|
+
if (allowed) out.allowed_room_ids = allowed;
|
|
171
|
+
return out;
|
|
172
|
+
};
|
|
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 }));
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
export { registerSpaceRoutes, roomDetails };
|