matrix-nodejs-homeserver 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +75 -0
- package/README.md +457 -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 +560 -0
- package/dist/api/admin.js +251 -0
- package/dist/api/appservice.js +33 -0
- package/dist/api/backup.js +269 -0
- package/dist/api/federation.js +792 -0
- package/dist/api/filters.js +87 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +170 -0
- package/dist/api/media.js +255 -0
- package/dist/api/misc.js +86 -0
- package/dist/api/preview.js +200 -0
- package/dist/api/profile.js +165 -0
- package/dist/api/push.js +187 -0
- package/dist/api/relationships.js +264 -0
- package/dist/api/rooms.js +1364 -0
- package/dist/api/rtc.d.ts +24 -0
- package/dist/api/rtc.js +52 -0
- package/dist/api/search.js +155 -0
- package/dist/api/slidingsync.js +464 -0
- package/dist/api/spaces.js +172 -0
- package/dist/api/sync.js +600 -0
- package/dist/api/threadsubs.js +129 -0
- package/dist/api/threepid.js +122 -0
- package/dist/api/uia.js +126 -0
- package/dist/api/user.js +286 -0
- package/dist/core/admin.d.ts +85 -0
- package/dist/core/admin.js +307 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +482 -0
- package/dist/core/appservices.d.ts +73 -0
- package/dist/core/appservices.js +368 -0
- package/dist/core/audit.d.ts +51 -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 +347 -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 +159 -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 +185 -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 +35 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +449 -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 +197 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +96 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +47 -0
- package/dist/core/push.js +306 -0
- package/dist/core/pushrules.d.ts +1 -0
- package/dist/core/pushrules.js +228 -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 +1729 -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 +208 -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 +278 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1058 -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 +223 -0
- package/dist/homeserver.js +301 -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 +202 -0
- package/dist/http/input.js +24 -0
- package/dist/http/router.d.ts +60 -0
- package/dist/http/router.js +76 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +14 -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/package.json +67 -4
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import "../http/router.js";
|
|
2
|
+
//#region src/api/rtc.d.ts
|
|
3
|
+
export interface TurnOptions {
|
|
4
|
+
uris: string[];
|
|
5
|
+
/** coturn `static-auth-secret`: time-limited credentials are derived from it. */
|
|
6
|
+
sharedSecret?: string;
|
|
7
|
+
/** Static credentials, when not using a shared secret. */
|
|
8
|
+
username?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
/** Credential lifetime in seconds. Default: 86400. */
|
|
11
|
+
ttlSeconds?: number;
|
|
12
|
+
/** Whether guests may obtain credentials. Default: false. */
|
|
13
|
+
allowGuests?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface RtcOptions {
|
|
16
|
+
/** VoIP TURN servers. */
|
|
17
|
+
turn?: TurnOptions;
|
|
18
|
+
/**
|
|
19
|
+
* MatrixRTC transports (MSC4143), e.g. `[{ type: "livekit", livekit_service_url: "https://lk-jwt.example.com" }]`.
|
|
20
|
+
* Advertised at /rtc/transports and in /.well-known/matrix/client.
|
|
21
|
+
*/
|
|
22
|
+
transports?: Record<string, unknown>[];
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
package/dist/api/rtc.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { errors } from "../core/errors.js";
|
|
2
|
+
import { body, fail } from "../http/input.js";
|
|
3
|
+
import { createHmac } from "node:crypto";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/api/rtc.ts
|
|
6
|
+
function registerRtcRoutes(r) {
|
|
7
|
+
r.client("GET", "/voip/turnServer", async (ctx) => {
|
|
8
|
+
const s = await ctx.auth();
|
|
9
|
+
const turn = ctx.hs.config.rtc.turn;
|
|
10
|
+
if (!turn || !turn.uris.length) return {};
|
|
11
|
+
if (s.isGuest && !turn.allowGuests) throw errors.forbidden("Guests may not use TURN");
|
|
12
|
+
const ttl = turn.ttlSeconds ?? 86400;
|
|
13
|
+
if (turn.sharedSecret) {
|
|
14
|
+
const username = `${Math.floor(Date.now() / 1e3) + ttl}:${s.userId}`;
|
|
15
|
+
return {
|
|
16
|
+
username,
|
|
17
|
+
password: createHmac("sha1", turn.sharedSecret).update(username).digest("base64"),
|
|
18
|
+
uris: turn.uris,
|
|
19
|
+
ttl
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
username: turn.username ?? "",
|
|
24
|
+
password: turn.password ?? "",
|
|
25
|
+
uris: turn.uris,
|
|
26
|
+
ttl
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const transports = async (ctx) => {
|
|
30
|
+
await ctx.auth();
|
|
31
|
+
return { rtc_transports: ctx.hs.config.rtc.transports ?? [] };
|
|
32
|
+
};
|
|
33
|
+
r.client("GET", "/rtc/transports", transports, ["v1"]);
|
|
34
|
+
r.get("/_matrix/client/unstable/org.matrix.msc4143/rtc/transports", transports);
|
|
35
|
+
const base = "/_matrix/client/unstable/org.matrix.msc4140/delayed_events";
|
|
36
|
+
r.get(base, async (ctx) => {
|
|
37
|
+
const s = await ctx.auth();
|
|
38
|
+
return { delayed_events: await ctx.hs.delayedEvents.list(s.userId) };
|
|
39
|
+
});
|
|
40
|
+
r.post(`${base}/:delayId`, { input: body({ action: z.string(fail("M_MISSING_PARAM", "Missing action")) }) }, async (ctx) => {
|
|
41
|
+
const s = await ctx.auth();
|
|
42
|
+
await ctx.hs.delayedEvents.update(s.userId, ctx.params.delayId, ctx.input.action);
|
|
43
|
+
return {};
|
|
44
|
+
});
|
|
45
|
+
r.post(`${base}/:delayId/:action`, async (ctx) => {
|
|
46
|
+
const s = await ctx.auth();
|
|
47
|
+
await ctx.hs.delayedEvents.update(s.userId, ctx.params.delayId, ctx.params.action);
|
|
48
|
+
return {};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { registerRtcRoutes };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { isObject } from "../core/json.js";
|
|
2
|
+
import { getState } from "../core/authrules.js";
|
|
3
|
+
import { body, fail } from "../http/input.js";
|
|
4
|
+
import { matchesFilter } from "./filters.js";
|
|
5
|
+
import { paginate, topoToken } from "./rooms.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
//#region src/api/search.ts
|
|
8
|
+
const DEFAULT_KEYS = [
|
|
9
|
+
"content.body",
|
|
10
|
+
"content.name",
|
|
11
|
+
"content.topic"
|
|
12
|
+
];
|
|
13
|
+
function valueAt(obj, path) {
|
|
14
|
+
return path.split(".").reduce((o, k) => isObject(o) ? o[k] : void 0, obj);
|
|
15
|
+
}
|
|
16
|
+
function registerSearchRoutes(r) {
|
|
17
|
+
const missingTerm = fail("M_BAD_JSON", "Missing search_categories.room_events.search_term");
|
|
18
|
+
const searchBody = body({ search_categories: z.looseObject({ room_events: z.looseObject({
|
|
19
|
+
search_term: z.string(missingTerm),
|
|
20
|
+
keys: z.unknown().optional(),
|
|
21
|
+
filter: z.unknown().optional(),
|
|
22
|
+
order_by: z.unknown().optional()
|
|
23
|
+
}, missingTerm) }, missingTerm) });
|
|
24
|
+
r.client("POST", "/search", { input: searchBody }, async (ctx) => {
|
|
25
|
+
const s = await ctx.auth();
|
|
26
|
+
const hs = ctx.hs;
|
|
27
|
+
const criteria = ctx.input.search_categories.room_events;
|
|
28
|
+
const terms = criteria.search_term.toLowerCase().split(/\s+/).filter(Boolean);
|
|
29
|
+
const keys = Array.isArray(criteria.keys) && criteria.keys.length ? criteria.keys : DEFAULT_KEYS;
|
|
30
|
+
const filter = isObject(criteria.filter) ? criteria.filter : {};
|
|
31
|
+
const limit = Math.min(filter.limit ?? 10, 100);
|
|
32
|
+
const orderBy = criteria.order_by === "recent" ? "recent" : "rank";
|
|
33
|
+
const offset = Number.parseInt(ctx.query.get("next_batch") ?? "0", 10) || 0;
|
|
34
|
+
let rooms = await hs.rooms.roomsForUser(s.userId);
|
|
35
|
+
if (filter.rooms) rooms = rooms.filter((r) => filter.rooms.includes(r));
|
|
36
|
+
if (filter.not_rooms) rooms = rooms.filter((r) => !filter.not_rooms.includes(r));
|
|
37
|
+
const hits = [];
|
|
38
|
+
for (const roomId of rooms) {
|
|
39
|
+
const rows = await hs.db.findMany("events", {
|
|
40
|
+
where: [{
|
|
41
|
+
field: "room_id",
|
|
42
|
+
value: roomId
|
|
43
|
+
}],
|
|
44
|
+
sortBy: {
|
|
45
|
+
field: "stream",
|
|
46
|
+
direction: "desc"
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const candidates = [];
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
if (row.state_key !== null || !matchesFilter(filter, row.json)) continue;
|
|
52
|
+
let rank = 0;
|
|
53
|
+
const words = [];
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
const v = valueAt(row.json, key);
|
|
56
|
+
if (typeof v === "string") words.push(...v.toLowerCase().split(/[^\p{L}\p{N}]+/u));
|
|
57
|
+
}
|
|
58
|
+
if (terms.every((t) => words.includes(t))) for (const t of terms) rank += words.filter((w) => w === t).length;
|
|
59
|
+
if (rank > 0) candidates.push({
|
|
60
|
+
ev: hs.rooms.rowToStored(row),
|
|
61
|
+
rank
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const visible = new Set((await hs.rooms.filterVisible(s.userId, roomId, candidates.map((c) => c.ev))).map((v) => v.pdu.event_id));
|
|
65
|
+
hits.push(...candidates.filter((c) => visible.has(c.ev.pdu.event_id)));
|
|
66
|
+
}
|
|
67
|
+
hits.sort((a, b) => orderBy === "rank" && b.rank !== a.rank ? b.rank - a.rank : b.ev.stream - a.ev.stream);
|
|
68
|
+
const page = hits.slice(offset, offset + limit);
|
|
69
|
+
const viewer = {
|
|
70
|
+
userId: s.userId,
|
|
71
|
+
deviceId: s.deviceId
|
|
72
|
+
};
|
|
73
|
+
const ec = isObject(criteria.event_context) ? criteria.event_context : null;
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const hit of page) {
|
|
76
|
+
const result = {
|
|
77
|
+
rank: hit.rank,
|
|
78
|
+
result: hs.rooms.formatEvent(hit.ev, viewer)
|
|
79
|
+
};
|
|
80
|
+
if (ec) {
|
|
81
|
+
const roomId = hit.ev.pdu.room_id;
|
|
82
|
+
const here = {
|
|
83
|
+
depth: hit.ev.pdu.depth,
|
|
84
|
+
stream: hit.ev.stream
|
|
85
|
+
};
|
|
86
|
+
const before = await paginate(hs, s.userId, roomId, {
|
|
87
|
+
from: here,
|
|
88
|
+
to: null,
|
|
89
|
+
dir: "b",
|
|
90
|
+
limit: ec.before_limit ?? 5
|
|
91
|
+
});
|
|
92
|
+
const after = await paginate(hs, s.userId, roomId, {
|
|
93
|
+
from: here,
|
|
94
|
+
to: null,
|
|
95
|
+
dir: "f",
|
|
96
|
+
limit: ec.after_limit ?? 5
|
|
97
|
+
});
|
|
98
|
+
const context = {
|
|
99
|
+
events_before: before.events.map((e) => hs.rooms.formatEvent(e, viewer)),
|
|
100
|
+
events_after: after.events.map((e) => hs.rooms.formatEvent(e, viewer)),
|
|
101
|
+
start: before.end ?? topoToken(before.events.at(-1) ?? hit.ev),
|
|
102
|
+
end: topoToken(after.events.at(-1) ?? hit.ev)
|
|
103
|
+
};
|
|
104
|
+
if (ec.include_profile) {
|
|
105
|
+
const state = await hs.rooms.currentState(roomId);
|
|
106
|
+
const profile_info = {};
|
|
107
|
+
for (const e of [
|
|
108
|
+
...before.events,
|
|
109
|
+
hit.ev,
|
|
110
|
+
...after.events
|
|
111
|
+
]) {
|
|
112
|
+
const m = getState(state, "m.room.member", e.pdu.sender)?.content;
|
|
113
|
+
profile_info[e.pdu.sender] = {
|
|
114
|
+
displayname: m?.displayname,
|
|
115
|
+
avatar_url: m?.avatar_url
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
context.profile_info = profile_info;
|
|
119
|
+
}
|
|
120
|
+
result.context = context;
|
|
121
|
+
}
|
|
122
|
+
results.push(result);
|
|
123
|
+
}
|
|
124
|
+
const out = {
|
|
125
|
+
results,
|
|
126
|
+
count: hits.length,
|
|
127
|
+
highlights: terms
|
|
128
|
+
};
|
|
129
|
+
if (page.length === limit && offset + limit <= hits.length) out.next_batch = String(offset + limit);
|
|
130
|
+
if (criteria.include_state) {
|
|
131
|
+
out.state = {};
|
|
132
|
+
for (const roomId of new Set(page.map((h) => h.ev.pdu.room_id))) out.state[roomId] = [...(await hs.rooms.currentState(roomId)).values()].map((e) => hs.rooms.formatEvent({
|
|
133
|
+
pdu: e,
|
|
134
|
+
stream: 0,
|
|
135
|
+
snapshot: 0,
|
|
136
|
+
txnId: null,
|
|
137
|
+
txnToken: null
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
if (criteria.groupings?.group_by?.some?.((g) => g?.key === "room_id")) {
|
|
141
|
+
const groups = {};
|
|
142
|
+
hits.forEach((h, i) => {
|
|
143
|
+
(groups[h.ev.pdu.room_id] ??= {
|
|
144
|
+
results: [],
|
|
145
|
+
order: i,
|
|
146
|
+
next_batch: null
|
|
147
|
+
}).results.push(h.ev.pdu.event_id);
|
|
148
|
+
});
|
|
149
|
+
out.groups = { room_id: groups };
|
|
150
|
+
}
|
|
151
|
+
return { search_categories: { room_events: out } };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
export { registerSearchRoutes };
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { isObject } from "../core/json.js";
|
|
2
|
+
import { splitStateKey, toClientEvent } from "../core/events.js";
|
|
3
|
+
import { MatrixError } from "../core/errors.js";
|
|
4
|
+
import { getState } from "../core/authrules.js";
|
|
5
|
+
import { body, lenient } from "../http/input.js";
|
|
6
|
+
import { roomEphemeral, roomTimeline, toStripped } from "./sync.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
//#region src/api/slidingsync.ts
|
|
9
|
+
const slidingSyncBody = body({
|
|
10
|
+
conn_id: lenient(z.string()),
|
|
11
|
+
lists: lenient(z.record(z.string(), z.unknown())),
|
|
12
|
+
room_subscriptions: lenient(z.record(z.string(), z.unknown())),
|
|
13
|
+
extensions: lenient(z.record(z.string(), z.unknown()))
|
|
14
|
+
});
|
|
15
|
+
const BUMP_TYPES = /* @__PURE__ */ new Set([
|
|
16
|
+
"m.room.create",
|
|
17
|
+
"m.room.message",
|
|
18
|
+
"m.room.encrypted",
|
|
19
|
+
"m.sticker",
|
|
20
|
+
"m.call.invite",
|
|
21
|
+
"m.poll.start",
|
|
22
|
+
"m.beacon_info"
|
|
23
|
+
]);
|
|
24
|
+
async function loadConnection(hs, s, connId) {
|
|
25
|
+
return hs.db.findOne("sliding_sync_connections", [
|
|
26
|
+
{
|
|
27
|
+
field: "user_id",
|
|
28
|
+
value: s.userId
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
field: "device_id",
|
|
32
|
+
value: s.deviceId
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
field: "conn_id",
|
|
36
|
+
value: connId
|
|
37
|
+
}
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
/** Stream position of the latest "interesting" event in a room. */
|
|
41
|
+
async function bumpStamp(hs, roomId, upTo) {
|
|
42
|
+
const rows = await hs.db.findMany("events", {
|
|
43
|
+
where: [{
|
|
44
|
+
field: "room_id",
|
|
45
|
+
value: roomId
|
|
46
|
+
}, {
|
|
47
|
+
field: "stream",
|
|
48
|
+
operator: "lte",
|
|
49
|
+
value: upTo
|
|
50
|
+
}],
|
|
51
|
+
sortBy: {
|
|
52
|
+
field: "stream",
|
|
53
|
+
direction: "desc"
|
|
54
|
+
},
|
|
55
|
+
limit: 50
|
|
56
|
+
});
|
|
57
|
+
for (const r of rows) if (BUMP_TYPES.has(r.type)) return r.stream;
|
|
58
|
+
return rows[rows.length - 1]?.stream ?? 0;
|
|
59
|
+
}
|
|
60
|
+
function matchesRequiredState(required, type, key, userId, lazyMembers) {
|
|
61
|
+
for (const [t, k] of required) {
|
|
62
|
+
if (t !== "*" && t !== type) continue;
|
|
63
|
+
if (k === "*" || k === key) return true;
|
|
64
|
+
if (k === "$ME" && key === userId) return true;
|
|
65
|
+
if (k === "$LAZY" && type === "m.room.member" && lazyMembers.has(key)) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function registerSlidingSyncRoutes(r) {
|
|
70
|
+
r.post("/_matrix/client/unstable/org.matrix.simplified_msc3575/sync", { input: slidingSyncBody }, slidingSync);
|
|
71
|
+
r.post("/_matrix/client/v5/sync", { input: slidingSyncBody }, slidingSync);
|
|
72
|
+
}
|
|
73
|
+
async function slidingSync(ctx) {
|
|
74
|
+
const hs = ctx.hs;
|
|
75
|
+
const s = await ctx.auth();
|
|
76
|
+
const body = ctx.input;
|
|
77
|
+
const connId = body.conn_id ?? "";
|
|
78
|
+
const posParam = ctx.query.get("pos");
|
|
79
|
+
const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
|
|
80
|
+
let conn = {
|
|
81
|
+
rooms: {},
|
|
82
|
+
prevPos: null,
|
|
83
|
+
toDeviceSince: 0
|
|
84
|
+
};
|
|
85
|
+
let since = null;
|
|
86
|
+
if (posParam !== null) {
|
|
87
|
+
const row = await loadConnection(hs, s, connId);
|
|
88
|
+
const pos = Number(posParam);
|
|
89
|
+
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");
|
|
90
|
+
conn = row.state;
|
|
91
|
+
since = pos;
|
|
92
|
+
if (pos === row.state.prevPos && pos !== row.pos) conn = {
|
|
93
|
+
...conn,
|
|
94
|
+
rooms: {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const deadline = Date.now() + timeout;
|
|
98
|
+
for (;;) {
|
|
99
|
+
const to = await hs.streams.position();
|
|
100
|
+
const { response, empty, next } = await buildResponse(hs, s, body, conn, since, to);
|
|
101
|
+
const remaining = deadline - Date.now();
|
|
102
|
+
if (since === null || !empty || remaining <= 0) {
|
|
103
|
+
await hs.db.upsert("sliding_sync_connections", {
|
|
104
|
+
user_id: s.userId,
|
|
105
|
+
device_id: s.deviceId,
|
|
106
|
+
conn_id: connId,
|
|
107
|
+
pos: to,
|
|
108
|
+
state: {
|
|
109
|
+
...next,
|
|
110
|
+
prevPos: since
|
|
111
|
+
},
|
|
112
|
+
updated_ts: Date.now()
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
pos: String(to),
|
|
116
|
+
...response
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (await hs.streams.position() > to) continue;
|
|
120
|
+
await hs.notifier.wait(s.userId, remaining, ctx.request.signal);
|
|
121
|
+
if (ctx.request.signal?.aborted) return {
|
|
122
|
+
pos: String(since),
|
|
123
|
+
lists: {},
|
|
124
|
+
rooms: {},
|
|
125
|
+
extensions: {}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function buildResponse(hs, s, body, conn, since, to) {
|
|
130
|
+
const userId = s.userId;
|
|
131
|
+
const memberships = await hs.db.findMany("memberships", { where: [{
|
|
132
|
+
field: "user_id",
|
|
133
|
+
value: userId
|
|
134
|
+
}] });
|
|
135
|
+
const direct = await hs.db.findOne("account_data", [
|
|
136
|
+
{
|
|
137
|
+
field: "user_id",
|
|
138
|
+
value: userId
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
field: "room_id",
|
|
142
|
+
value: ""
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
field: "type",
|
|
146
|
+
value: "m.direct"
|
|
147
|
+
}
|
|
148
|
+
]);
|
|
149
|
+
const dmRooms = new Set(Object.values(direct?.content ?? {}).flat().filter((x) => typeof x === "string"));
|
|
150
|
+
const candidates = memberships.filter((m) => m.stream <= to && ([
|
|
151
|
+
"join",
|
|
152
|
+
"invite",
|
|
153
|
+
"knock"
|
|
154
|
+
].includes(m.membership) || since !== null && m.stream > since && !m.forgotten));
|
|
155
|
+
const infos = [];
|
|
156
|
+
for (const m of candidates) {
|
|
157
|
+
const state = await hs.rooms.currentState(m.room_id);
|
|
158
|
+
const bump = m.membership === "join" ? await bumpStamp(hs, m.room_id, to) : m.stream;
|
|
159
|
+
infos.push({
|
|
160
|
+
roomId: m.room_id,
|
|
161
|
+
membership: m.membership,
|
|
162
|
+
bump,
|
|
163
|
+
state
|
|
164
|
+
});
|
|
165
|
+
const invite = getState(state, "m.room.member", userId);
|
|
166
|
+
if (m.membership === "invite" && invite?.content?.is_direct) dmRooms.add(m.room_id);
|
|
167
|
+
}
|
|
168
|
+
infos.sort((a, b) => b.bump - a.bump);
|
|
169
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
170
|
+
const lists = {};
|
|
171
|
+
for (const [name, raw] of Object.entries(isObject(body.lists) ? body.lists : {})) {
|
|
172
|
+
const list = raw;
|
|
173
|
+
const f = list.filters ?? {};
|
|
174
|
+
let rooms = infos;
|
|
175
|
+
if (f.is_dm !== void 0) rooms = rooms.filter((r) => dmRooms.has(r.roomId) === f.is_dm);
|
|
176
|
+
if (f.is_invite !== void 0) rooms = rooms.filter((r) => r.membership === "invite" === f.is_invite);
|
|
177
|
+
if (f.is_encrypted !== void 0) rooms = rooms.filter((r) => !!getState(r.state, "m.room.encryption") === f.is_encrypted);
|
|
178
|
+
const roomType = (r) => getState(r.state, "m.room.create")?.content?.type ?? null;
|
|
179
|
+
if (f.room_types) rooms = rooms.filter((r) => f.room_types.includes(roomType(r)));
|
|
180
|
+
if (f.not_room_types) rooms = rooms.filter((r) => !f.not_room_types.includes(roomType(r)));
|
|
181
|
+
if (f.spaces) {
|
|
182
|
+
const children = /* @__PURE__ */ new Set();
|
|
183
|
+
for (const space of f.spaces) {
|
|
184
|
+
if (!await hs.rooms.exists(space)) continue;
|
|
185
|
+
for (const key of (await hs.rooms.currentState(space)).keys()) {
|
|
186
|
+
const [t, k] = splitStateKey(key);
|
|
187
|
+
if (t === "m.space.child") children.add(k);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
rooms = rooms.filter((r) => children.has(r.roomId));
|
|
191
|
+
}
|
|
192
|
+
lists[name] = { count: rooms.length };
|
|
193
|
+
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)) {
|
|
194
|
+
const prev = wanted.get(room.roomId);
|
|
195
|
+
wanted.set(room.roomId, mergeConfig(prev, list));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const [roomId, raw] of Object.entries(isObject(body.room_subscriptions) ? body.room_subscriptions : {})) {
|
|
199
|
+
if (!infos.find((i) => i.roomId === roomId)) {
|
|
200
|
+
if (!await hs.rooms.exists(roomId) || !(await hs.rooms.canPeek(roomId, userId)).worldReadable) continue;
|
|
201
|
+
infos.push({
|
|
202
|
+
roomId,
|
|
203
|
+
membership: "leave",
|
|
204
|
+
bump: 0,
|
|
205
|
+
state: await hs.rooms.currentState(roomId)
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
wanted.set(roomId, mergeConfig(wanted.get(roomId), raw));
|
|
209
|
+
}
|
|
210
|
+
const next = {
|
|
211
|
+
rooms: { ...conn.rooms },
|
|
212
|
+
prevPos: conn.prevPos,
|
|
213
|
+
toDeviceSince: conn.toDeviceSince
|
|
214
|
+
};
|
|
215
|
+
const rooms = {};
|
|
216
|
+
for (const [roomId, config] of wanted) {
|
|
217
|
+
const info = infos.find((i) => i.roomId === roomId);
|
|
218
|
+
const configKey = JSON.stringify(config);
|
|
219
|
+
const known = since !== null ? conn.rooms[roomId] : void 0;
|
|
220
|
+
const initial = !known || known.config !== configKey;
|
|
221
|
+
const room = await buildRoom(hs, s, info, config, initial ? null : since, to, dmRooms.has(roomId));
|
|
222
|
+
const counts = JSON.stringify([
|
|
223
|
+
room.notification_count,
|
|
224
|
+
room.highlight_count,
|
|
225
|
+
room.joined_count,
|
|
226
|
+
room.invited_count
|
|
227
|
+
]);
|
|
228
|
+
if (!initial && room.timeline.length === 0 && room.required_state.length === 0 && known.counts === counts) continue;
|
|
229
|
+
if (initial) room.initial = true;
|
|
230
|
+
if (since !== null && !initial) room.num_live = room.timeline.filter((e) => e.__stream > since).length;
|
|
231
|
+
for (const e of room.timeline) delete e.__stream;
|
|
232
|
+
rooms[roomId] = room;
|
|
233
|
+
next.rooms[roomId] = {
|
|
234
|
+
config: configKey,
|
|
235
|
+
counts
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const extensions = await buildExtensions(hs, s, body.extensions ?? {}, since, to, Object.keys(rooms), next);
|
|
239
|
+
const empty = Object.keys(rooms).length === 0 && Object.values(extensions).every((e) => isEmptyExtension(e));
|
|
240
|
+
return {
|
|
241
|
+
response: {
|
|
242
|
+
lists,
|
|
243
|
+
rooms,
|
|
244
|
+
extensions
|
|
245
|
+
},
|
|
246
|
+
empty,
|
|
247
|
+
next
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function mergeConfig(a, b) {
|
|
251
|
+
if (!a) return {
|
|
252
|
+
required_state: b.required_state ?? [],
|
|
253
|
+
timeline_limit: b.timeline_limit ?? 0
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
required_state: [...a.required_state ?? [], ...b.required_state ?? []],
|
|
257
|
+
timeline_limit: Math.max(a.timeline_limit ?? 0, b.timeline_limit ?? 0)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async function buildRoom(hs, s, info, config, since, to, isDm) {
|
|
261
|
+
const { roomId, state } = info;
|
|
262
|
+
const userId = s.userId;
|
|
263
|
+
const room = {
|
|
264
|
+
bump_stamp: info.bump,
|
|
265
|
+
timeline: [],
|
|
266
|
+
required_state: []
|
|
267
|
+
};
|
|
268
|
+
const name = getState(state, "m.room.name")?.content?.name;
|
|
269
|
+
if (typeof name === "string") room.name = name;
|
|
270
|
+
const avatar = getState(state, "m.room.avatar")?.content?.url;
|
|
271
|
+
if (typeof avatar === "string") room.avatar = avatar;
|
|
272
|
+
if (isDm) room.is_dm = true;
|
|
273
|
+
if (info.membership === "invite" || info.membership === "knock") {
|
|
274
|
+
const own = getState(state, "m.room.member", userId);
|
|
275
|
+
const ev = own?.event_id ? await hs.rooms.getEvent(own.event_id) : null;
|
|
276
|
+
if (ev) {
|
|
277
|
+
const key = info.membership === "invite" ? "invite_room_state" : "knock_room_state";
|
|
278
|
+
room[info.membership === "invite" ? "invite_state" : "knock_state"] = [...ev.pdu.unsigned?.[key] ?? [], ev.pdu].map(toStripped);
|
|
279
|
+
}
|
|
280
|
+
return room;
|
|
281
|
+
}
|
|
282
|
+
const p = {
|
|
283
|
+
session: s,
|
|
284
|
+
ignored: /* @__PURE__ */ new Set(),
|
|
285
|
+
since: since === null ? null : {
|
|
286
|
+
pos: since,
|
|
287
|
+
toDevice: 0
|
|
288
|
+
},
|
|
289
|
+
to,
|
|
290
|
+
filter: { room: { timeline: { limit: config.timeline_limit ?? 0 } } },
|
|
291
|
+
fullState: false
|
|
292
|
+
};
|
|
293
|
+
const upTo = info.membership === "join" ? to : Math.min(to, (await hs.db.findOne("memberships", [{
|
|
294
|
+
field: "room_id",
|
|
295
|
+
value: roomId
|
|
296
|
+
}, {
|
|
297
|
+
field: "user_id",
|
|
298
|
+
value: userId
|
|
299
|
+
}]))?.stream ?? to);
|
|
300
|
+
if ((config.timeline_limit ?? 0) > 0) {
|
|
301
|
+
const { timeline, timelineEvents } = await roomTimeline(hs, p, roomId, since, upTo);
|
|
302
|
+
room.timeline = timeline.events.map((e, i) => ({
|
|
303
|
+
...e,
|
|
304
|
+
room_id: void 0,
|
|
305
|
+
__stream: timelineEvents[i].stream
|
|
306
|
+
}));
|
|
307
|
+
room.limited = timeline.limited;
|
|
308
|
+
room.prev_batch = timeline.prev_batch;
|
|
309
|
+
}
|
|
310
|
+
const lazy = /* @__PURE__ */ new Set([userId, ...room.timeline.map((e) => e.sender)]);
|
|
311
|
+
const required = config.required_state ?? [];
|
|
312
|
+
for (const [key, ev] of state) {
|
|
313
|
+
const [type, sk] = splitStateKey(key);
|
|
314
|
+
if (!matchesRequiredState(required, type, sk, userId, lazy)) continue;
|
|
315
|
+
if (since !== null) {
|
|
316
|
+
const stored = ev.event_id ? await hs.rooms.getEvent(ev.event_id) : null;
|
|
317
|
+
if (!stored || stored.stream <= since) continue;
|
|
318
|
+
}
|
|
319
|
+
room.required_state.push(toClientEvent(ev, { includeRoomId: false }));
|
|
320
|
+
}
|
|
321
|
+
let joined = 0;
|
|
322
|
+
let invited = 0;
|
|
323
|
+
const heroes = [];
|
|
324
|
+
for (const [key, ev] of state) {
|
|
325
|
+
const [type, sk] = splitStateKey(key);
|
|
326
|
+
if (type !== "m.room.member") continue;
|
|
327
|
+
const m = ev.content?.membership;
|
|
328
|
+
if (m === "join") joined++;
|
|
329
|
+
if (m === "invite") invited++;
|
|
330
|
+
if ((m === "join" || m === "invite") && sk !== userId && heroes.length < 5) heroes.push({
|
|
331
|
+
user_id: sk,
|
|
332
|
+
displayname: ev.content?.displayname,
|
|
333
|
+
avatar_url: ev.content?.avatar_url
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
room.joined_count = joined;
|
|
337
|
+
room.invited_count = invited;
|
|
338
|
+
if (!room.name) room.heroes = heroes;
|
|
339
|
+
const counts = await hs.push.unreadCounts(userId, roomId);
|
|
340
|
+
room.notification_count = counts.room.notification_count + Object.values(counts.threads).reduce((n, t) => n + t.notification_count, 0);
|
|
341
|
+
room.highlight_count = counts.room.highlight_count + Object.values(counts.threads).reduce((n, t) => n + t.highlight_count, 0);
|
|
342
|
+
return room;
|
|
343
|
+
}
|
|
344
|
+
async function buildExtensions(hs, s, ext, since, to, roomIds, next) {
|
|
345
|
+
const out = {};
|
|
346
|
+
const userId = s.userId;
|
|
347
|
+
if (ext.to_device?.enabled) {
|
|
348
|
+
const where = [{
|
|
349
|
+
field: "user_id",
|
|
350
|
+
value: userId
|
|
351
|
+
}, {
|
|
352
|
+
field: "device_id",
|
|
353
|
+
value: s.deviceId
|
|
354
|
+
}];
|
|
355
|
+
const ackUpTo = Number(ext.to_device.since ?? next.toDeviceSince) || 0;
|
|
356
|
+
if (ackUpTo) await hs.db.delete("to_device", [...where, {
|
|
357
|
+
field: "stream",
|
|
358
|
+
operator: "lte",
|
|
359
|
+
value: ackUpTo
|
|
360
|
+
}]);
|
|
361
|
+
const rows = (await hs.db.findMany("to_device", {
|
|
362
|
+
where: [...where, {
|
|
363
|
+
field: "stream",
|
|
364
|
+
operator: "gt",
|
|
365
|
+
value: ackUpTo
|
|
366
|
+
}],
|
|
367
|
+
sortBy: {
|
|
368
|
+
field: "stream",
|
|
369
|
+
direction: "asc"
|
|
370
|
+
},
|
|
371
|
+
limit: Math.min(Number(ext.to_device.limit ?? 100) || 100, 1e3)
|
|
372
|
+
})).filter((r) => r.stream <= to);
|
|
373
|
+
const nextBatch = rows.length ? rows[rows.length - 1].stream : ackUpTo;
|
|
374
|
+
next.toDeviceSince = nextBatch;
|
|
375
|
+
out.to_device = {
|
|
376
|
+
next_batch: String(nextBatch),
|
|
377
|
+
events: rows.map((r) => ({
|
|
378
|
+
sender: r.sender,
|
|
379
|
+
type: r.type,
|
|
380
|
+
content: r.content
|
|
381
|
+
}))
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (ext.e2ee?.enabled) {
|
|
385
|
+
const e2ee = {
|
|
386
|
+
device_one_time_keys_count: await hs.keys.oneTimeKeyCounts(userId, s.deviceId),
|
|
387
|
+
device_unused_fallback_key_types: await hs.keys.unusedFallbackKeyTypes(userId, s.deviceId)
|
|
388
|
+
};
|
|
389
|
+
if (since !== null) e2ee.device_lists = await hs.keys.changesSince(userId, since, to);
|
|
390
|
+
out.e2ee = e2ee;
|
|
391
|
+
}
|
|
392
|
+
if (ext.account_data?.enabled) {
|
|
393
|
+
const where = [{
|
|
394
|
+
field: "user_id",
|
|
395
|
+
value: userId
|
|
396
|
+
}];
|
|
397
|
+
if (since !== null) where.push({
|
|
398
|
+
field: "stream",
|
|
399
|
+
operator: "gt",
|
|
400
|
+
value: since
|
|
401
|
+
});
|
|
402
|
+
const rows = (await hs.db.findMany("account_data", { where })).filter((r) => r.stream <= to);
|
|
403
|
+
const rooms = {};
|
|
404
|
+
const global = [];
|
|
405
|
+
for (const r of rows) {
|
|
406
|
+
const ev = {
|
|
407
|
+
type: r.type,
|
|
408
|
+
content: r.content
|
|
409
|
+
};
|
|
410
|
+
if (r.room_id === "") global.push(ev);
|
|
411
|
+
else if (roomIds.includes(r.room_id) || since !== null) (rooms[r.room_id] ??= []).push(ev);
|
|
412
|
+
}
|
|
413
|
+
if (since === null && !global.some((e) => e.type === "m.push_rules")) {
|
|
414
|
+
const { loadRules } = await import("../core/pushrules.js");
|
|
415
|
+
global.push({
|
|
416
|
+
type: "m.push_rules",
|
|
417
|
+
content: { global: await loadRules(hs, userId) }
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
out.account_data = {
|
|
421
|
+
global,
|
|
422
|
+
rooms
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (ext["io.element.msc4308.thread_subscriptions"]?.enabled) {
|
|
426
|
+
const { threadSubscriptionsExtension } = await import("./threadsubs.js");
|
|
427
|
+
out["io.element.msc4308.thread_subscriptions"] = await threadSubscriptionsExtension(hs, userId, ext["io.element.msc4308.thread_subscriptions"], since, to);
|
|
428
|
+
}
|
|
429
|
+
if (ext.receipts?.enabled || ext.typing?.enabled) {
|
|
430
|
+
const p = {
|
|
431
|
+
session: s,
|
|
432
|
+
ignored: /* @__PURE__ */ new Set(),
|
|
433
|
+
since: since === null ? null : {
|
|
434
|
+
pos: since,
|
|
435
|
+
toDevice: 0
|
|
436
|
+
},
|
|
437
|
+
to,
|
|
438
|
+
filter: {},
|
|
439
|
+
fullState: false
|
|
440
|
+
};
|
|
441
|
+
const receipts = {};
|
|
442
|
+
const typing = {};
|
|
443
|
+
const joined = new Set(await hs.rooms.roomsForUser(userId));
|
|
444
|
+
for (const roomId of since === null ? roomIds : [...joined]) {
|
|
445
|
+
if (!joined.has(roomId)) continue;
|
|
446
|
+
for (const e of await roomEphemeral(hs, p, roomId)) {
|
|
447
|
+
if (e.type === "m.receipt" && ext.receipts?.enabled) receipts[roomId] = e;
|
|
448
|
+
if (e.type === "m.typing" && ext.typing?.enabled && (since !== null || e.content.user_ids.length)) typing[roomId] = e;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (ext.receipts?.enabled) out.receipts = { rooms: receipts };
|
|
452
|
+
if (ext.typing?.enabled) out.typing = { rooms: typing };
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
function isEmptyExtension(e) {
|
|
457
|
+
if (Array.isArray(e.events)) return e.events.length === 0;
|
|
458
|
+
if (e.rooms && Object.keys(e.rooms).length) return false;
|
|
459
|
+
if (e.global && e.global.length) return false;
|
|
460
|
+
if (e.device_lists && (e.device_lists.changed.length || e.device_lists.left.length)) return false;
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
//#endregion
|
|
464
|
+
export { registerSlidingSyncRoutes };
|