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.
- package/README.md +29 -7
- package/dist/api/account.js +385 -351
- package/dist/api/admin.js +76 -60
- package/dist/api/appservice.js +37 -25
- package/dist/api/backup.js +73 -62
- package/dist/api/federation.js +172 -138
- package/dist/api/filters.js +4 -3
- package/dist/api/legacy.js +91 -78
- package/dist/api/media.js +70 -54
- package/dist/api/misc.js +46 -37
- package/dist/api/preview.js +4 -3
- package/dist/api/profile.js +108 -97
- package/dist/api/push.js +114 -99
- package/dist/api/relationships.js +64 -58
- package/dist/api/rooms.js +703 -580
- package/dist/api/rtc.d.ts +0 -1
- package/dist/api/rtc.js +48 -34
- package/dist/api/search.js +133 -127
- package/dist/api/slidingsync.js +41 -31
- package/dist/api/spaces.js +82 -77
- package/dist/api/sync.js +59 -50
- package/dist/api/threadsubs.js +72 -61
- package/dist/api/threepid.js +99 -66
- package/dist/api/uia.js +17 -16
- package/dist/api/user.js +221 -178
- package/dist/core/admin.d.ts +0 -1
- package/dist/core/admin.js +6 -5
- package/dist/core/adminroom.js +9 -8
- package/dist/core/appservices.d.ts +2 -1
- package/dist/core/appservices.js +5 -4
- package/dist/core/audit.d.ts +0 -1
- package/dist/core/authrules.js +26 -25
- package/dist/core/delayed.js +2 -1
- package/dist/core/events.js +11 -10
- package/dist/core/invitefilter.js +2 -1
- package/dist/core/keys.js +2 -1
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/partialstate.js +2 -1
- package/dist/core/policy.js +4 -3
- package/dist/core/push.d.ts +0 -1
- package/dist/core/push.js +12 -11
- package/dist/core/pushrules.js +36 -35
- package/dist/core/rooms.js +76 -74
- package/dist/core/stateres.js +7 -6
- package/dist/federation/client.js +2 -1
- package/dist/federation/index.d.ts +1 -1
- package/dist/federation/index.js +30 -29
- package/dist/homeserver.d.ts +2 -1
- package/dist/homeserver.js +1 -0
- package/dist/http/handler.js +2 -23
- package/dist/http/input.js +2 -2
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +9 -26
- package/dist/http/router.js +10 -30
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- 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 +3 -5
- package/dist/core/pushrules.d.ts +0 -1
package/dist/api/spaces.js
CHANGED
|
@@ -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(
|
|
13
|
-
guest_can_join: get(
|
|
14
|
-
join_rule: get(
|
|
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(
|
|
18
|
-
if (str(get(
|
|
19
|
-
if (str(get(
|
|
20
|
-
if (str(get(
|
|
21
|
-
if (str(get(
|
|
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,
|
|
33
|
-
const joinRules = getState(state,
|
|
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 ===
|
|
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 ===
|
|
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",
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 ===
|
|
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(
|
|
171
|
-
if (startState.has(key) || state.some((s) => s.type ===
|
|
172
|
-
const member = [...(await hs.rooms.getPdus((e.pdu.auth_events ?? []).map(eventIdOf))).values()].find((a) => a.type ===
|
|
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 ===
|
|
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:
|
|
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 ===
|
|
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:
|
|
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 !==
|
|
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
|
-
|
|
267
|
-
|
|
268
|
+
[M.JOINED_MEMBER_COUNT]: joined,
|
|
269
|
+
[M.INVITED_MEMBER_COUNT]: invited
|
|
268
270
|
};
|
|
269
|
-
if (!getState(state,
|
|
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 ===
|
|
349
|
-
type:
|
|
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:
|
|
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[
|
|
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:
|
|
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(
|
|
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" :
|
|
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",
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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[
|
|
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;
|
package/dist/api/threadsubs.js
CHANGED
|
@@ -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,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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) {
|