@openclaw/crabline 0.1.9 → 0.1.11
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 +47 -22
- package/assets/crabline-banner.svg +20 -0
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +25 -1
- package/dist/src/cli/program.js +579 -82
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +59 -6
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +116 -92
- package/dist/src/config/schema.js +179 -73
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.js +9 -2
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/exit-codes.js.map +1 -1
- package/dist/src/core/matcher.js +7 -6
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/message-template.js.map +1 -1
- package/dist/src/core/nonces.d.ts +1 -0
- package/dist/src/core/nonces.js +4 -2
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +1 -0
- package/dist/src/core/reporters.js +42 -4
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -0
- package/dist/src/core/run.js +415 -107
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/core/safe-regex.d.ts +3 -0
- package/dist/src/core/safe-regex.js +21 -0
- package/dist/src/core/safe-regex.js.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +43 -0
- package/dist/src/openclaw/artifact-generation.js +363 -0
- package/dist/src/openclaw/artifact-generation.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +97 -12
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +32 -10
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/signal.js +55 -18
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +85 -7
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +124 -30
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +78 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +14 -5
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.d.ts +5 -0
- package/dist/src/openclaw/outbound-contract.js +40 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -0
- package/dist/src/openclaw/private-file.d.ts +28 -0
- package/dist/src/openclaw/private-file.js +465 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +21 -2
- package/dist/src/openclaw/shared.js +115 -18
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +63 -0
- package/dist/src/openclaw/smoke-lock.js +1004 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +334 -52
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/builtin/discord.d.ts +10 -1
- package/dist/src/providers/builtin/discord.js +144 -18
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
- package/dist/src/providers/builtin/feishu.d.ts +19 -2
- package/dist/src/providers/builtin/feishu.js +307 -25
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +20 -1
- package/dist/src/providers/builtin/googlechat.js +178 -19
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.d.ts +4 -0
- package/dist/src/providers/builtin/imessage.js +26 -11
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +92 -30
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +11 -1
- package/dist/src/providers/builtin/matrix.js +53 -29
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +10 -0
- package/dist/src/providers/builtin/mattermost.js +38 -15
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +17 -1
- package/dist/src/providers/builtin/msteams.js +113 -14
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
- package/dist/src/providers/builtin/native-local-mock.js +12 -35
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.d.ts +11 -15
- package/dist/src/providers/builtin/script.js +566 -104
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +6 -0
- package/dist/src/providers/builtin/slack.js +99 -40
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +6 -0
- package/dist/src/providers/builtin/telegram.js +64 -43
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
- package/dist/src/providers/builtin/whatsapp.js +567 -30
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.js +30 -19
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +3 -3
- package/dist/src/providers/catalog.js +3 -3
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +11 -2
- package/dist/src/providers/local-mock.js +322 -96
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +1 -0
- package/dist/src/providers/native-ids.js +6 -1
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +30 -2
- package/dist/src/providers/recorder.js +549 -70
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +25 -1
- package/dist/src/providers/registry.js +280 -40
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +29 -0
- package/dist/src/providers/signed-jwt.js +202 -0
- package/dist/src/providers/signed-jwt.js.map +1 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +26 -0
- package/dist/src/providers/target-normalizers.js +276 -0
- package/dist/src/providers/target-normalizers.js.map +1 -0
- package/dist/src/providers/types.d.ts +5 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +2 -0
- package/dist/src/providers/webhook-server.js +72 -25
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +23 -3
- package/dist/src/servers/http.js +213 -20
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.js +20 -21
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.js +465 -51
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +306 -58
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/pending-events.d.ts +2 -0
- package/dist/src/servers/pending-events.js +11 -0
- package/dist/src/servers/pending-events.js.map +1 -0
- package/dist/src/servers/recorder.d.ts +5 -0
- package/dist/src/servers/recorder.js +262 -3
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +2 -0
- package/dist/src/servers/signal.js +444 -56
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +5 -0
- package/dist/src/servers/slack.js +515 -49
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +111 -0
- package/dist/src/servers/telegram.js +830 -158
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +40 -0
- package/dist/src/servers/webhook-target.js +349 -0
- package/dist/src/servers/webhook-target.js.map +1 -0
- package/dist/src/servers/websocket.d.ts +2 -0
- package/dist/src/servers/websocket.js +29 -0
- package/dist/src/servers/websocket.js.map +1 -0
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +5 -0
- package/dist/src/servers/whatsapp-jid.js +32 -0
- package/dist/src/servers/whatsapp-jid.js.map +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
- package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +12 -7
- package/dist/src/servers/whatsapp.js +269 -107
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +14 -0
- package/dist/src/servers/zalo.js +521 -88
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +197 -19
- package/fixtures/examples/crabline.example.yaml +10 -0
- package/package.json +20 -15
|
@@ -1,20 +1,123 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { isIP } from "node:net";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import { adminAuthError, hasAdminToken, jsonResponse,
|
|
4
|
-
import { recordServerEvent } from "./recorder.js";
|
|
4
|
+
import { adminAuthError, hasAdminToken, InvalidJsonBodyError, isJsonObject, jsonResponse, parseUnknownRequestBody, queryRecord, readInteger, readTrimmedString, RequestBodyTooLargeError, startHttpJsonServer, } from "./http.js";
|
|
5
|
+
import { recordCommittedServerEvent, recordServerEvent, } from "./recorder.js";
|
|
6
|
+
const MAX_MATRIX_FILTER_BYTES = 1024 * 1024;
|
|
7
|
+
const MAX_MATRIX_FILTERS = 100;
|
|
8
|
+
const MAX_MATRIX_IDENTIFIER_BYTES = 255;
|
|
9
|
+
const MAX_MATRIX_TIMELINE_EVENTS = 1_000;
|
|
10
|
+
const MAX_MATRIX_TRANSACTION_RESPONSES = 1_000;
|
|
11
|
+
const MATRIX_TRANSACTION_RETENTION_MS = 10 * 60_000;
|
|
12
|
+
const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647;
|
|
13
|
+
class InvalidMatrixPathEncodingError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("Matrix request path contains invalid percent encoding.");
|
|
16
|
+
this.name = "InvalidMatrixPathEncodingError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
5
19
|
function matrixId(prefix, value, serverName) {
|
|
6
20
|
return `${prefix}${createHash("sha256").update(value).digest("hex").slice(0, 16)}:${serverName}`;
|
|
7
21
|
}
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
function isMatrixIpv4Address(value) {
|
|
23
|
+
const octets = value.split(".");
|
|
24
|
+
return (octets.length === 4 && octets.every((octet) => /^\d{1,3}$/u.test(octet) && Number(octet) <= 255));
|
|
25
|
+
}
|
|
26
|
+
function isMatrixServerName(value) {
|
|
27
|
+
const ipv6 = /^\[([^\]]+)\](?::(\d{1,5}))?$/u.exec(value);
|
|
28
|
+
if (ipv6) {
|
|
29
|
+
return isIP(ipv6[1]) === 6;
|
|
30
|
+
}
|
|
31
|
+
const hostAndPort = /^([^:]+?)(?::(\d{1,5}))?$/u.exec(value);
|
|
32
|
+
if (!hostAndPort) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const hostname = hostAndPort[1];
|
|
36
|
+
if (isMatrixIpv4Address(hostname)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (/^\d+\.\d+\.\d+\.\d+$/u.test(hostname)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return hostname.length <= 255 && /^[A-Za-z0-9.-]+$/u.test(hostname);
|
|
43
|
+
}
|
|
44
|
+
function hasLoneSurrogate(value) {
|
|
45
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
46
|
+
const code = value.charCodeAt(index);
|
|
47
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
48
|
+
const next = value.charCodeAt(index + 1);
|
|
49
|
+
if (next < 0xdc00 || next > 0xdfff) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
index += 1;
|
|
53
|
+
}
|
|
54
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function isMatrixScopedIdentifier(value, sigil) {
|
|
61
|
+
const separator = value.indexOf(":");
|
|
62
|
+
const localpart = value.slice(1, separator);
|
|
63
|
+
return (value.startsWith(sigil) &&
|
|
64
|
+
Buffer.byteLength(value, "utf8") <= MAX_MATRIX_IDENTIFIER_BYTES &&
|
|
65
|
+
separator >= (sigil === "@" ? 1 : 2) &&
|
|
66
|
+
!localpart.includes("\0") &&
|
|
67
|
+
!hasLoneSurrogate(localpart) &&
|
|
68
|
+
isMatrixServerName(value.slice(separator + 1)));
|
|
69
|
+
}
|
|
70
|
+
function isMatrixHashIdentifier(value, sigil, allowLegacyBase64) {
|
|
71
|
+
if (!value.startsWith(sigil) || Buffer.byteLength(value, "utf8") > MAX_MATRIX_IDENTIFIER_BYTES) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const opaqueId = value.slice(1);
|
|
75
|
+
const encoding = allowLegacyBase64 && /^[A-Za-z0-9+/]{43}$/u.test(opaqueId)
|
|
76
|
+
? "base64"
|
|
77
|
+
: /^[A-Za-z0-9_-]{43}$/u.test(opaqueId)
|
|
78
|
+
? "base64url"
|
|
79
|
+
: undefined;
|
|
80
|
+
if (!encoding) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const decoded = Buffer.from(opaqueId, encoding);
|
|
84
|
+
return decoded.length === 32 && decoded.toString(encoding).replace(/=+$/u, "") === opaqueId;
|
|
85
|
+
}
|
|
86
|
+
function isMatrixRoomId(value) {
|
|
87
|
+
return isMatrixScopedIdentifier(value, "!") || isMatrixHashIdentifier(value, "!", false);
|
|
88
|
+
}
|
|
89
|
+
function isMatrixEventId(value) {
|
|
90
|
+
return isMatrixScopedIdentifier(value, "$") || isMatrixHashIdentifier(value, "$", true);
|
|
91
|
+
}
|
|
92
|
+
function isMatrixUserId(value) {
|
|
93
|
+
return isMatrixScopedIdentifier(value, "@");
|
|
94
|
+
}
|
|
95
|
+
async function appendEvent(state, event, committed = false) {
|
|
96
|
+
const params = { event, onEvent: state.onEvent, recorderPath: state.recorderPath };
|
|
97
|
+
await (committed ? recordCommittedServerEvent(params) : recordServerEvent(params));
|
|
10
98
|
}
|
|
11
99
|
function authorized(request, token) {
|
|
12
|
-
const
|
|
13
|
-
return
|
|
100
|
+
const match = /^Bearer\s+(\S+)$/iu.exec(request.headers.authorization ?? "");
|
|
101
|
+
return match?.[1] === token;
|
|
14
102
|
}
|
|
15
103
|
function matrixError(errcode, error, status) {
|
|
16
104
|
return jsonResponse({ errcode, error }, status);
|
|
17
105
|
}
|
|
106
|
+
function matrixResourceLimitError(error) {
|
|
107
|
+
return jsonResponse({
|
|
108
|
+
admin_contact: "mailto:admin@localhost",
|
|
109
|
+
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
|
110
|
+
error,
|
|
111
|
+
}, 503);
|
|
112
|
+
}
|
|
113
|
+
function decodeMatrixPathSegment(value) {
|
|
114
|
+
try {
|
|
115
|
+
return decodeURIComponent(value);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new InvalidMatrixPathEncodingError();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
18
121
|
function eventId(state) {
|
|
19
122
|
return matrixId("$", `event-${state.nextEvent++}`, state.serverName);
|
|
20
123
|
}
|
|
@@ -52,10 +155,15 @@ function createEvent(params) {
|
|
|
52
155
|
function createRoom(params) {
|
|
53
156
|
const room = {
|
|
54
157
|
createdSequence: params.createdSequence,
|
|
158
|
+
ephemeral: [],
|
|
55
159
|
id: params.id,
|
|
160
|
+
lastDroppedTimelineSequence: undefined,
|
|
56
161
|
name: params.name,
|
|
57
162
|
state: [],
|
|
163
|
+
stateBeforeTimeline: new Map(),
|
|
58
164
|
timeline: [],
|
|
165
|
+
typingTimeouts: new Map(),
|
|
166
|
+
typingUsers: new Set(),
|
|
59
167
|
users: new Map([[params.botUserId, { display_name: "OpenClaw QA" }]]),
|
|
60
168
|
};
|
|
61
169
|
room.state.push(createEvent({
|
|
@@ -84,35 +192,151 @@ function createRoom(params) {
|
|
|
84
192
|
stateKey: "",
|
|
85
193
|
type: "m.room.name",
|
|
86
194
|
}));
|
|
195
|
+
for (const event of room.state) {
|
|
196
|
+
room.stateBeforeTimeline.set(matrixStateKey(event), event);
|
|
197
|
+
}
|
|
87
198
|
return room;
|
|
88
199
|
}
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
200
|
+
function matrixStateKey(event) {
|
|
201
|
+
return JSON.stringify([event.type, event.state_key ?? ""]);
|
|
202
|
+
}
|
|
203
|
+
function appendTimelineEvent(room, entry) {
|
|
204
|
+
room.timeline.push(entry);
|
|
205
|
+
if (room.timeline.length > MAX_MATRIX_TIMELINE_EVENTS) {
|
|
206
|
+
const dropped = room.timeline.splice(0, room.timeline.length - MAX_MATRIX_TIMELINE_EVENTS);
|
|
207
|
+
room.lastDroppedTimelineSequence = dropped.at(-1)?.sequence;
|
|
208
|
+
for (const droppedEntry of dropped) {
|
|
209
|
+
if (droppedEntry.event.state_key !== undefined) {
|
|
210
|
+
room.stateBeforeTimeline.set(matrixStateKey(droppedEntry.event), droppedEntry.event);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function appendEphemeralEvent(room, entry) {
|
|
216
|
+
room.ephemeral.push(entry);
|
|
217
|
+
if (room.ephemeral.length > MAX_MATRIX_TIMELINE_EVENTS) {
|
|
218
|
+
room.ephemeral.splice(0, room.ephemeral.length - MAX_MATRIX_TIMELINE_EVENTS);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function publishTypingState(state, room) {
|
|
222
|
+
appendEphemeralEvent(room, {
|
|
223
|
+
event: { content: { user_ids: [...room.typingUsers] }, type: "m.typing" },
|
|
224
|
+
sequence: state.nextSequence++,
|
|
225
|
+
});
|
|
226
|
+
notifySyncWaiters(state);
|
|
227
|
+
}
|
|
228
|
+
function rememberTransaction(state, key, response) {
|
|
229
|
+
state.transactions.set(key, response);
|
|
230
|
+
}
|
|
231
|
+
function forgetExpiredTransactions(state) {
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
for (const [key, response] of state.transactions) {
|
|
234
|
+
if (response.expiresAt > now) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
state.transactions.delete(key);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function transactionResponse(state, key) {
|
|
241
|
+
forgetExpiredTransactions(state);
|
|
242
|
+
const response = state.transactions.get(key);
|
|
243
|
+
if (!response) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
state.transactions.delete(key);
|
|
247
|
+
response.expiresAt = Date.now() + MATRIX_TRANSACTION_RETENTION_MS;
|
|
248
|
+
state.transactions.set(key, response);
|
|
249
|
+
return response;
|
|
250
|
+
}
|
|
251
|
+
function readTimelineLimit(filter) {
|
|
252
|
+
const room = filter?.room;
|
|
253
|
+
const timeline = isJsonObject(room) ? room.timeline : undefined;
|
|
254
|
+
const limit = isJsonObject(timeline) ? readInteger(timeline.limit) : undefined;
|
|
255
|
+
return limit === undefined ? undefined : Math.max(0, limit);
|
|
256
|
+
}
|
|
257
|
+
function resolveSyncFilter(url, state) {
|
|
258
|
+
const value = url.searchParams.get("filter");
|
|
259
|
+
if (value === null) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
const stored = state.filters.get(value);
|
|
263
|
+
if (stored) {
|
|
264
|
+
return stored.body;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(value);
|
|
268
|
+
return isJsonObject(parsed) ? parsed : matrixError("M_INVALID_PARAM", "Invalid filter", 400);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return matrixError("M_INVALID_PARAM", "Unknown filter", 400);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function syncRoom(room, since, timelineLimit) {
|
|
275
|
+
const available = room.timeline.filter((entry) => since === undefined || entry.sequence > since);
|
|
276
|
+
const timeline = timelineLimit === undefined
|
|
277
|
+
? available
|
|
278
|
+
: available.slice(Math.max(0, available.length - timelineLimit));
|
|
279
|
+
const firstSequence = timeline[0]?.sequence;
|
|
280
|
+
const historyWasTrimmed = room.lastDroppedTimelineSequence !== undefined &&
|
|
281
|
+
(since === undefined || since < room.lastDroppedTimelineSequence);
|
|
282
|
+
const limited = historyWasTrimmed || timeline.length < available.length;
|
|
283
|
+
const omittedTimeline = available.slice(0, available.length - timeline.length);
|
|
284
|
+
const omittedState = new Map();
|
|
285
|
+
if (since === undefined || room.createdSequence > since || historyWasTrimmed) {
|
|
286
|
+
for (const [key, event] of room.stateBeforeTimeline) {
|
|
287
|
+
omittedState.set(key, event);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const entry of omittedTimeline) {
|
|
291
|
+
if (entry.event.state_key !== undefined) {
|
|
292
|
+
omittedState.set(matrixStateKey(entry.event), entry.event);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
93
295
|
return {
|
|
94
296
|
account_data: { events: [] },
|
|
95
|
-
ephemeral: {
|
|
96
|
-
|
|
97
|
-
|
|
297
|
+
ephemeral: {
|
|
298
|
+
events: room.ephemeral
|
|
299
|
+
.filter((entry) => since === undefined || entry.sequence > since)
|
|
300
|
+
.map((entry) => entry.event),
|
|
301
|
+
},
|
|
302
|
+
state: {
|
|
303
|
+
events: [...omittedState.values()].filter((event) => !timeline.some((entry) => entry.event.event_id === event.event_id)),
|
|
304
|
+
},
|
|
305
|
+
timeline: {
|
|
306
|
+
events: timeline.map((entry) => entry.event),
|
|
307
|
+
limited,
|
|
308
|
+
prev_batch: `s${firstSequence === undefined ? (since ?? 0) : firstSequence - 1}`,
|
|
309
|
+
},
|
|
98
310
|
unread_notifications: { highlight_count: 0, notification_count: 0 },
|
|
99
311
|
};
|
|
100
312
|
}
|
|
101
313
|
function parseSyncToken(value) {
|
|
102
|
-
if (
|
|
314
|
+
if (value === null) {
|
|
103
315
|
return undefined;
|
|
104
316
|
}
|
|
105
317
|
const match = /^s(\d+)$/u.exec(value);
|
|
106
|
-
|
|
318
|
+
if (!match) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const sequence = Number(match[1]);
|
|
322
|
+
return Number.isSafeInteger(sequence) ? sequence : null;
|
|
107
323
|
}
|
|
108
324
|
async function handleSync(url, state) {
|
|
109
325
|
const since = parseSyncToken(url.searchParams.get("since"));
|
|
326
|
+
if (since === null || (since !== undefined && since > state.nextSequence - 1)) {
|
|
327
|
+
return matrixError("M_UNKNOWN_POS", "Unknown position", 400);
|
|
328
|
+
}
|
|
329
|
+
const filter = resolveSyncFilter(url, state);
|
|
330
|
+
if (filter instanceof Response) {
|
|
331
|
+
return filter;
|
|
332
|
+
}
|
|
333
|
+
const timelineLimit = readTimelineLimit(filter);
|
|
110
334
|
const timeout = Math.min(readInteger(url.searchParams.get("timeout")) ?? 0, 1_000);
|
|
111
|
-
const hasNewEvents = [...state.rooms.values()].some((room) => room.timeline.some((entry) => since === undefined || entry.sequence > since));
|
|
335
|
+
const hasNewEvents = [...state.rooms.values()].some((room) => [...room.timeline, ...room.ephemeral].some((entry) => since === undefined || entry.sequence > since));
|
|
112
336
|
if (since !== undefined && !hasNewEvents && timeout > 0) {
|
|
113
337
|
await waitForSyncEvent(state, timeout);
|
|
114
338
|
}
|
|
115
|
-
const join = Object.fromEntries([...state.rooms.values()].map((room) => [room.id, syncRoom(room, since)]));
|
|
339
|
+
const join = Object.fromEntries([...state.rooms.values()].map((room) => [room.id, syncRoom(room, since, timelineLimit)]));
|
|
116
340
|
return jsonResponse({
|
|
117
341
|
account_data: { events: [] },
|
|
118
342
|
device_lists: { changed: [], left: [] },
|
|
@@ -124,7 +348,7 @@ async function handleSync(url, state) {
|
|
|
124
348
|
});
|
|
125
349
|
}
|
|
126
350
|
function findRoom(state, encodedRoomId) {
|
|
127
|
-
return state.rooms.get(
|
|
351
|
+
return state.rooms.get(decodeMatrixPathSegment(encodedRoomId));
|
|
128
352
|
}
|
|
129
353
|
async function handleAdminInbound(params) {
|
|
130
354
|
const roomId = readTrimmedString(params.body.roomId);
|
|
@@ -133,6 +357,12 @@ async function handleAdminInbound(params) {
|
|
|
133
357
|
if (!roomId || !sender || !text) {
|
|
134
358
|
return jsonResponse({ error: "roomId, senderId, and text are required", ok: false }, 400);
|
|
135
359
|
}
|
|
360
|
+
const threadId = readTrimmedString(params.body.threadId);
|
|
361
|
+
if (!isMatrixRoomId(roomId) ||
|
|
362
|
+
!isMatrixUserId(sender) ||
|
|
363
|
+
(threadId !== undefined && !isMatrixEventId(threadId))) {
|
|
364
|
+
return jsonResponse({ error: "Invalid Matrix identifier", ok: false }, 400);
|
|
365
|
+
}
|
|
136
366
|
const direct = params.body.direct === true;
|
|
137
367
|
const room = params.state.rooms.get(roomId) ??
|
|
138
368
|
createRoom({
|
|
@@ -146,7 +376,8 @@ async function handleAdminInbound(params) {
|
|
|
146
376
|
});
|
|
147
377
|
params.state.rooms.set(roomId, room);
|
|
148
378
|
const senderName = readTrimmedString(params.body.senderName);
|
|
149
|
-
|
|
379
|
+
const existingProfile = room.users.get(sender);
|
|
380
|
+
if (existingProfile === undefined) {
|
|
150
381
|
room.users.set(sender, senderName ? { display_name: senderName } : {});
|
|
151
382
|
const membership = createEvent({
|
|
152
383
|
content: {
|
|
@@ -160,13 +391,31 @@ async function handleAdminInbound(params) {
|
|
|
160
391
|
type: "m.room.member",
|
|
161
392
|
});
|
|
162
393
|
room.state.push(membership);
|
|
163
|
-
room
|
|
394
|
+
appendTimelineEvent(room, { event: membership, sequence: params.state.nextSequence++ });
|
|
164
395
|
}
|
|
165
|
-
else if (senderName) {
|
|
166
|
-
room.users.set(sender, { display_name: senderName });
|
|
396
|
+
else if (senderName && existingProfile.display_name !== senderName) {
|
|
397
|
+
room.users.set(sender, { ...existingProfile, display_name: senderName });
|
|
398
|
+
const membership = createEvent({
|
|
399
|
+
content: { membership: "join", displayname: senderName },
|
|
400
|
+
roomId,
|
|
401
|
+
sender,
|
|
402
|
+
state: params.state,
|
|
403
|
+
stateKey: sender,
|
|
404
|
+
type: "m.room.member",
|
|
405
|
+
});
|
|
406
|
+
const stateIndex = room.state.findIndex((event) => event.type === "m.room.member" && event.state_key === sender);
|
|
407
|
+
if (stateIndex >= 0) {
|
|
408
|
+
room.state[stateIndex] = membership;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
room.state.push(membership);
|
|
412
|
+
}
|
|
413
|
+
appendTimelineEvent(room, {
|
|
414
|
+
event: membership,
|
|
415
|
+
sequence: params.state.nextSequence++,
|
|
416
|
+
});
|
|
167
417
|
}
|
|
168
418
|
const content = { body: text, msgtype: "m.text" };
|
|
169
|
-
const threadId = readTrimmedString(params.body.threadId);
|
|
170
419
|
if (threadId) {
|
|
171
420
|
content["m.relates_to"] = {
|
|
172
421
|
event_id: threadId,
|
|
@@ -182,7 +431,7 @@ async function handleAdminInbound(params) {
|
|
|
182
431
|
state: params.state,
|
|
183
432
|
type: "m.room.message",
|
|
184
433
|
});
|
|
185
|
-
room
|
|
434
|
+
appendTimelineEvent(room, { event, sequence: params.state.nextSequence++ });
|
|
186
435
|
notifySyncWaiters(params.state);
|
|
187
436
|
return jsonResponse({ event, ok: true });
|
|
188
437
|
}
|
|
@@ -203,7 +452,7 @@ async function handleMatrixApi(params) {
|
|
|
203
452
|
}
|
|
204
453
|
let match = /^\/profile\/([^/]+)$/u.exec(relativePath);
|
|
205
454
|
if (params.method === "GET" && match) {
|
|
206
|
-
const userId =
|
|
455
|
+
const userId = decodeMatrixPathSegment(match[1]);
|
|
207
456
|
const member = [...params.state.rooms.values()]
|
|
208
457
|
.map((room) => room.users.get(userId))
|
|
209
458
|
.find((profile) => profile !== undefined);
|
|
@@ -223,18 +472,28 @@ async function handleMatrixApi(params) {
|
|
|
223
472
|
}
|
|
224
473
|
match = /^\/user\/([^/]+)\/filter$/u.exec(relativePath);
|
|
225
474
|
if (params.method === "POST" && match) {
|
|
226
|
-
const userId =
|
|
475
|
+
const userId = decodeMatrixPathSegment(match[1]);
|
|
227
476
|
if (userId !== params.state.botUserId) {
|
|
228
477
|
return matrixError("M_FORBIDDEN", "Cannot create a filter for another user", 403);
|
|
229
478
|
}
|
|
479
|
+
const bytes = Buffer.byteLength(JSON.stringify(params.body), "utf8");
|
|
480
|
+
if (params.state.filters.size >= MAX_MATRIX_FILTERS ||
|
|
481
|
+
params.state.filterBytes + bytes > MAX_MATRIX_FILTER_BYTES) {
|
|
482
|
+
return matrixResourceLimitError("Too many stored filters");
|
|
483
|
+
}
|
|
230
484
|
const filterId = String(params.state.nextFilter++);
|
|
231
|
-
params.state.filters.set(filterId, params.body);
|
|
485
|
+
params.state.filters.set(filterId, { body: params.body, bytes });
|
|
486
|
+
params.state.filterBytes += bytes;
|
|
232
487
|
return jsonResponse({ filter_id: filterId });
|
|
233
488
|
}
|
|
234
489
|
match = /^\/user\/([^/]+)\/filter\/([^/]+)$/u.exec(relativePath);
|
|
235
490
|
if (params.method === "GET" && match) {
|
|
236
|
-
const
|
|
237
|
-
|
|
491
|
+
const userId = decodeMatrixPathSegment(match[1]);
|
|
492
|
+
if (userId !== params.state.botUserId) {
|
|
493
|
+
return matrixError("M_FORBIDDEN", "Cannot get filters for another user", 403);
|
|
494
|
+
}
|
|
495
|
+
const filter = params.state.filters.get(decodeMatrixPathSegment(match[2]));
|
|
496
|
+
return filter ? jsonResponse(filter.body) : matrixError("M_NOT_FOUND", "Unknown filter", 404);
|
|
238
497
|
}
|
|
239
498
|
match = /^\/rooms\/([^/]+)\/joined_members$/u.exec(relativePath);
|
|
240
499
|
if (params.method === "GET" && match) {
|
|
@@ -249,8 +508,8 @@ async function handleMatrixApi(params) {
|
|
|
249
508
|
if (!room) {
|
|
250
509
|
return matrixError("M_NOT_FOUND", "Unknown room", 404);
|
|
251
510
|
}
|
|
252
|
-
const eventType =
|
|
253
|
-
const stateKey =
|
|
511
|
+
const eventType = decodeMatrixPathSegment(match[2]);
|
|
512
|
+
const stateKey = decodeMatrixPathSegment(match[3] ?? "");
|
|
254
513
|
if (eventType === "m.room.name" && stateKey === "") {
|
|
255
514
|
return jsonResponse({ name: room.name });
|
|
256
515
|
}
|
|
@@ -269,28 +528,125 @@ async function handleMatrixApi(params) {
|
|
|
269
528
|
}
|
|
270
529
|
match = /^\/rooms\/([^/]+)\/send\/([^/]+)\/([^/]+)$/u.exec(relativePath);
|
|
271
530
|
if (params.method === "PUT" && match) {
|
|
272
|
-
const
|
|
531
|
+
const roomId = decodeMatrixPathSegment(match[1]);
|
|
532
|
+
const eventType = decodeMatrixPathSegment(match[2]);
|
|
533
|
+
const transactionId = decodeMatrixPathSegment(match[3]);
|
|
534
|
+
const transactionKey = JSON.stringify([
|
|
535
|
+
params.state.botUserId,
|
|
536
|
+
roomId,
|
|
537
|
+
eventType,
|
|
538
|
+
transactionId,
|
|
539
|
+
]);
|
|
540
|
+
const existingResponse = transactionResponse(params.state, transactionKey);
|
|
541
|
+
if (existingResponse) {
|
|
542
|
+
params.recorderEvent.accepted =
|
|
543
|
+
existingResponse.status >= 200 && existingResponse.status < 300;
|
|
544
|
+
return jsonResponse(existingResponse.body, existingResponse.status);
|
|
545
|
+
}
|
|
546
|
+
if (params.state.transactions.size >= MAX_MATRIX_TRANSACTION_RESPONSES) {
|
|
547
|
+
params.recorderEvent.accepted = false;
|
|
548
|
+
return matrixResourceLimitError("Too many retained transaction responses");
|
|
549
|
+
}
|
|
550
|
+
const room = params.state.rooms.get(roomId);
|
|
273
551
|
if (!room) {
|
|
274
|
-
|
|
552
|
+
params.recorderEvent.accepted = false;
|
|
553
|
+
const body = { errcode: "M_NOT_FOUND", error: "Unknown room" };
|
|
554
|
+
rememberTransaction(params.state, transactionKey, {
|
|
555
|
+
body,
|
|
556
|
+
expiresAt: Date.now() + MATRIX_TRANSACTION_RETENTION_MS,
|
|
557
|
+
status: 404,
|
|
558
|
+
});
|
|
559
|
+
return jsonResponse(body, 404);
|
|
275
560
|
}
|
|
276
561
|
const event = createEvent({
|
|
277
562
|
content: params.body,
|
|
278
563
|
roomId: room.id,
|
|
279
564
|
sender: params.state.botUserId,
|
|
280
565
|
state: params.state,
|
|
281
|
-
transactionId
|
|
282
|
-
type:
|
|
566
|
+
transactionId,
|
|
567
|
+
type: eventType,
|
|
568
|
+
});
|
|
569
|
+
appendTimelineEvent(room, { event, sequence: params.state.nextSequence++ });
|
|
570
|
+
const body = { event_id: event.event_id };
|
|
571
|
+
rememberTransaction(params.state, transactionKey, {
|
|
572
|
+
body,
|
|
573
|
+
expiresAt: Date.now() + MATRIX_TRANSACTION_RETENTION_MS,
|
|
574
|
+
status: 200,
|
|
283
575
|
});
|
|
284
|
-
room.timeline.push({ event, sequence: params.state.nextSequence++ });
|
|
285
576
|
notifySyncWaiters(params.state);
|
|
286
|
-
|
|
577
|
+
params.recorderEvent.accepted = true;
|
|
578
|
+
return jsonResponse(body);
|
|
287
579
|
}
|
|
288
580
|
match = /^\/rooms\/([^/]+)\/typing\/([^/]+)$/u.exec(relativePath);
|
|
289
581
|
if (params.method === "PUT" && match) {
|
|
582
|
+
const room = findRoom(params.state, match[1]);
|
|
583
|
+
if (!room) {
|
|
584
|
+
return matrixError("M_NOT_FOUND", "Unknown room", 404);
|
|
585
|
+
}
|
|
586
|
+
const userId = decodeMatrixPathSegment(match[2]);
|
|
587
|
+
if (userId !== params.state.botUserId) {
|
|
588
|
+
return matrixError("M_FORBIDDEN", "Cannot set typing state for another user", 403);
|
|
589
|
+
}
|
|
590
|
+
if (typeof params.body.typing !== "boolean") {
|
|
591
|
+
return matrixError("M_BAD_JSON", "typing must be a boolean", 400);
|
|
592
|
+
}
|
|
593
|
+
const timeout = params.body.typing &&
|
|
594
|
+
typeof params.body.timeout === "number" &&
|
|
595
|
+
Number.isSafeInteger(params.body.timeout)
|
|
596
|
+
? params.body.timeout
|
|
597
|
+
: undefined;
|
|
598
|
+
if (params.body.typing &&
|
|
599
|
+
(timeout === undefined || timeout < 1 || timeout > MAX_NODE_TIMER_DELAY_MS)) {
|
|
600
|
+
return matrixError("M_BAD_JSON", `timeout must be a positive integer no greater than ${MAX_NODE_TIMER_DELAY_MS}`, 400);
|
|
601
|
+
}
|
|
602
|
+
const existingTimeout = room.typingTimeouts.get(userId);
|
|
603
|
+
if (existingTimeout) {
|
|
604
|
+
clearTimeout(existingTimeout);
|
|
605
|
+
room.typingTimeouts.delete(userId);
|
|
606
|
+
}
|
|
607
|
+
if (params.body.typing) {
|
|
608
|
+
room.typingUsers.add(userId);
|
|
609
|
+
const timer = setTimeout(() => {
|
|
610
|
+
room.typingTimeouts.delete(userId);
|
|
611
|
+
if (room.typingUsers.delete(userId)) {
|
|
612
|
+
publishTypingState(params.state, room);
|
|
613
|
+
}
|
|
614
|
+
}, timeout);
|
|
615
|
+
timer.unref();
|
|
616
|
+
room.typingTimeouts.set(userId, timer);
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
room.typingUsers.delete(userId);
|
|
620
|
+
}
|
|
621
|
+
publishTypingState(params.state, room);
|
|
290
622
|
return jsonResponse({});
|
|
291
623
|
}
|
|
292
624
|
match = /^\/rooms\/([^/]+)\/receipt\/m\.read\/([^/]+)$/u.exec(relativePath);
|
|
293
625
|
if (params.method === "POST" && match) {
|
|
626
|
+
const room = findRoom(params.state, match[1]);
|
|
627
|
+
if (!room) {
|
|
628
|
+
return matrixError("M_NOT_FOUND", "Unknown room", 404);
|
|
629
|
+
}
|
|
630
|
+
const receiptEventId = decodeMatrixPathSegment(match[2]);
|
|
631
|
+
appendEphemeralEvent(room, {
|
|
632
|
+
event: {
|
|
633
|
+
content: {
|
|
634
|
+
[receiptEventId]: {
|
|
635
|
+
"m.read": {
|
|
636
|
+
[params.state.botUserId]: {
|
|
637
|
+
ts: Date.now(),
|
|
638
|
+
...(readTrimmedString(params.body.thread_id)
|
|
639
|
+
? { thread_id: readTrimmedString(params.body.thread_id) }
|
|
640
|
+
: {}),
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
type: "m.receipt",
|
|
646
|
+
},
|
|
647
|
+
sequence: params.state.nextSequence++,
|
|
648
|
+
});
|
|
649
|
+
notifySyncWaiters(params.state);
|
|
294
650
|
return jsonResponse({});
|
|
295
651
|
}
|
|
296
652
|
return matrixError("M_UNRECOGNIZED", "Unrecognized request", 404);
|
|
@@ -303,6 +659,7 @@ export async function startMatrixServer(params = {}) {
|
|
|
303
659
|
adminToken: params.adminToken ?? randomBytes(24).toString("hex"),
|
|
304
660
|
botUserId: params.botUserId ?? `@openclaw:${serverName}`,
|
|
305
661
|
deviceId: params.deviceId ?? "CRABLINE",
|
|
662
|
+
filterBytes: 0,
|
|
306
663
|
filters: new Map(),
|
|
307
664
|
nextEvent: 1,
|
|
308
665
|
nextFilter: 1,
|
|
@@ -312,6 +669,7 @@ export async function startMatrixServer(params = {}) {
|
|
|
312
669
|
rooms: new Map(),
|
|
313
670
|
serverName,
|
|
314
671
|
syncWaiters: new Set(),
|
|
672
|
+
transactions: new Map(),
|
|
315
673
|
};
|
|
316
674
|
const roomId = params.roomId ?? matrixId("!", "default-room", serverName);
|
|
317
675
|
state.rooms.set(roomId, createRoom({
|
|
@@ -323,45 +681,101 @@ export async function startMatrixServer(params = {}) {
|
|
|
323
681
|
state,
|
|
324
682
|
}));
|
|
325
683
|
const server = await startHttpJsonServer({
|
|
684
|
+
handleError: (error) => {
|
|
685
|
+
if (error instanceof InvalidJsonBodyError) {
|
|
686
|
+
return matrixError("M_NOT_JSON", "Request body is not valid JSON", 400);
|
|
687
|
+
}
|
|
688
|
+
if (error instanceof RequestBodyTooLargeError) {
|
|
689
|
+
return matrixError("M_TOO_LARGE", "Request body is too large", 413);
|
|
690
|
+
}
|
|
691
|
+
if (error instanceof InvalidMatrixPathEncodingError) {
|
|
692
|
+
return matrixError("M_INVALID_PARAM", "Invalid request path encoding", 400);
|
|
693
|
+
}
|
|
694
|
+
return matrixError("M_UNKNOWN", "Internal server error", 500);
|
|
695
|
+
},
|
|
326
696
|
host,
|
|
327
697
|
port: params.port ?? 0,
|
|
328
698
|
serverName: "Matrix",
|
|
329
699
|
async handle(request) {
|
|
330
700
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
331
701
|
const method = request.method ?? "GET";
|
|
332
|
-
const body = ["POST", "PUT"].includes(method) ? await parseRequestBody(request) : {};
|
|
333
702
|
const type = url.pathname === "/crabline/matrix/inbound" ? "admin" : "api";
|
|
334
|
-
await appendEvent(state, {
|
|
335
|
-
at: new Date().toISOString(),
|
|
336
|
-
...(Object.keys(body).length > 0 ? { body } : {}),
|
|
337
|
-
method,
|
|
338
|
-
path: url.pathname,
|
|
339
|
-
query: queryRecord(url),
|
|
340
|
-
type,
|
|
341
|
-
});
|
|
342
703
|
if (type === "admin") {
|
|
343
704
|
if (method !== "POST") {
|
|
344
705
|
return jsonResponse({ error: "Method not allowed", ok: false }, 405);
|
|
345
706
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
707
|
+
if (!hasAdminToken(request, state.adminToken)) {
|
|
708
|
+
request.resume();
|
|
709
|
+
return adminAuthError();
|
|
710
|
+
}
|
|
711
|
+
const body = await parseUnknownRequestBody(request);
|
|
712
|
+
if (!isJsonObject(body)) {
|
|
713
|
+
return jsonResponse({ error: "Request body must be a JSON object", ok: false }, 400);
|
|
714
|
+
}
|
|
715
|
+
await appendEvent(state, {
|
|
716
|
+
at: new Date().toISOString(),
|
|
717
|
+
...(Object.keys(body).length > 0 ? { body } : {}),
|
|
718
|
+
method,
|
|
719
|
+
path: url.pathname,
|
|
720
|
+
query: queryRecord(url),
|
|
721
|
+
type,
|
|
722
|
+
});
|
|
723
|
+
return await handleAdminInbound({ body, state });
|
|
349
724
|
}
|
|
350
725
|
if (url.pathname === "/_matrix/client/versions" && method === "GET") {
|
|
726
|
+
await appendEvent(state, {
|
|
727
|
+
at: new Date().toISOString(),
|
|
728
|
+
method,
|
|
729
|
+
path: url.pathname,
|
|
730
|
+
query: queryRecord(url),
|
|
731
|
+
type,
|
|
732
|
+
});
|
|
351
733
|
return jsonResponse({ unstable_features: {}, versions: ["v1.11"] });
|
|
352
734
|
}
|
|
353
735
|
if (!url.pathname.startsWith("/_matrix/client/")) {
|
|
354
736
|
return matrixError("M_UNRECOGNIZED", "Unrecognized request", 404);
|
|
355
737
|
}
|
|
356
738
|
if (!authorized(request, state.accessToken)) {
|
|
739
|
+
request.resume();
|
|
357
740
|
return matrixError("M_UNKNOWN_TOKEN", "Invalid access token", 401);
|
|
358
741
|
}
|
|
359
|
-
|
|
742
|
+
const parsedBody = ["POST", "PUT"].includes(method)
|
|
743
|
+
? await parseUnknownRequestBody(request)
|
|
744
|
+
: {};
|
|
745
|
+
if (!isJsonObject(parsedBody)) {
|
|
746
|
+
return matrixError("M_BAD_JSON", "Request body must be a JSON object", 400);
|
|
747
|
+
}
|
|
748
|
+
const event = {
|
|
749
|
+
at: new Date().toISOString(),
|
|
750
|
+
...(Object.keys(parsedBody).length > 0 ? { body: parsedBody } : {}),
|
|
751
|
+
method,
|
|
752
|
+
path: url.pathname,
|
|
753
|
+
query: queryRecord(url),
|
|
754
|
+
type,
|
|
755
|
+
};
|
|
756
|
+
const response = await handleMatrixApi({
|
|
757
|
+
body: parsedBody,
|
|
758
|
+
method,
|
|
759
|
+
path: url.pathname,
|
|
760
|
+
recorderEvent: event,
|
|
761
|
+
state,
|
|
762
|
+
url,
|
|
763
|
+
});
|
|
764
|
+
await appendEvent(state, event, response.ok && ["DELETE", "POST", "PUT"].includes(method));
|
|
765
|
+
return response;
|
|
360
766
|
},
|
|
361
767
|
});
|
|
362
768
|
const clientApiRoot = `${server.baseUrl}/_matrix/client/v3`;
|
|
363
769
|
return {
|
|
364
|
-
close
|
|
770
|
+
async close() {
|
|
771
|
+
for (const room of state.rooms.values()) {
|
|
772
|
+
for (const timer of room.typingTimeouts.values()) {
|
|
773
|
+
clearTimeout(timer);
|
|
774
|
+
}
|
|
775
|
+
room.typingTimeouts.clear();
|
|
776
|
+
}
|
|
777
|
+
await server.close();
|
|
778
|
+
},
|
|
365
779
|
manifest: {
|
|
366
780
|
accessToken: state.accessToken,
|
|
367
781
|
adminToken: state.adminToken,
|