@openclaw/crabline 0.1.8 → 0.1.10
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 +52 -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 +4 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +38 -0
- package/dist/src/openclaw/artifact-generation.js +314 -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 +26 -0
- package/dist/src/openclaw/private-file.js +411 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +23 -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 +949 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +195 -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 +539 -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.d.ts +2 -0
- package/dist/src/servers/matrix.js +466 -52
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +7 -0
- package/dist/src/servers/mattermost.js +307 -59
- 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 +12 -0
- package/dist/src/servers/recorder.js +51 -0
- package/dist/src/servers/recorder.js.map +1 -0
- package/dist/src/servers/signal.d.ts +4 -0
- package/dist/src/servers/signal.js +445 -57
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +7 -0
- package/dist/src/servers/slack.js +516 -50
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +113 -0
- package/dist/src/servers/telegram.js +814 -155
- 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 +14 -7
- package/dist/src/servers/whatsapp.js +272 -110
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +16 -0
- package/dist/src/servers/zalo.js +524 -91
- 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,21 +1,123 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { adminAuthError, hasAdminToken, jsonResponse,
|
|
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
|
-
|
|
10
|
-
|
|
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));
|
|
11
98
|
}
|
|
12
99
|
function authorized(request, token) {
|
|
13
|
-
const
|
|
14
|
-
return
|
|
100
|
+
const match = /^Bearer\s+(\S+)$/iu.exec(request.headers.authorization ?? "");
|
|
101
|
+
return match?.[1] === token;
|
|
15
102
|
}
|
|
16
103
|
function matrixError(errcode, error, status) {
|
|
17
104
|
return jsonResponse({ errcode, error }, status);
|
|
18
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
|
+
}
|
|
19
121
|
function eventId(state) {
|
|
20
122
|
return matrixId("$", `event-${state.nextEvent++}`, state.serverName);
|
|
21
123
|
}
|
|
@@ -53,10 +155,15 @@ function createEvent(params) {
|
|
|
53
155
|
function createRoom(params) {
|
|
54
156
|
const room = {
|
|
55
157
|
createdSequence: params.createdSequence,
|
|
158
|
+
ephemeral: [],
|
|
56
159
|
id: params.id,
|
|
160
|
+
lastDroppedTimelineSequence: undefined,
|
|
57
161
|
name: params.name,
|
|
58
162
|
state: [],
|
|
163
|
+
stateBeforeTimeline: new Map(),
|
|
59
164
|
timeline: [],
|
|
165
|
+
typingTimeouts: new Map(),
|
|
166
|
+
typingUsers: new Set(),
|
|
60
167
|
users: new Map([[params.botUserId, { display_name: "OpenClaw QA" }]]),
|
|
61
168
|
};
|
|
62
169
|
room.state.push(createEvent({
|
|
@@ -85,35 +192,151 @@ function createRoom(params) {
|
|
|
85
192
|
stateKey: "",
|
|
86
193
|
type: "m.room.name",
|
|
87
194
|
}));
|
|
195
|
+
for (const event of room.state) {
|
|
196
|
+
room.stateBeforeTimeline.set(matrixStateKey(event), event);
|
|
197
|
+
}
|
|
88
198
|
return room;
|
|
89
199
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|
|
94
295
|
return {
|
|
95
296
|
account_data: { events: [] },
|
|
96
|
-
ephemeral: {
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
},
|
|
99
310
|
unread_notifications: { highlight_count: 0, notification_count: 0 },
|
|
100
311
|
};
|
|
101
312
|
}
|
|
102
313
|
function parseSyncToken(value) {
|
|
103
|
-
if (
|
|
314
|
+
if (value === null) {
|
|
104
315
|
return undefined;
|
|
105
316
|
}
|
|
106
317
|
const match = /^s(\d+)$/u.exec(value);
|
|
107
|
-
|
|
318
|
+
if (!match) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const sequence = Number(match[1]);
|
|
322
|
+
return Number.isSafeInteger(sequence) ? sequence : null;
|
|
108
323
|
}
|
|
109
324
|
async function handleSync(url, state) {
|
|
110
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);
|
|
111
334
|
const timeout = Math.min(readInteger(url.searchParams.get("timeout")) ?? 0, 1_000);
|
|
112
|
-
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));
|
|
113
336
|
if (since !== undefined && !hasNewEvents && timeout > 0) {
|
|
114
337
|
await waitForSyncEvent(state, timeout);
|
|
115
338
|
}
|
|
116
|
-
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)]));
|
|
117
340
|
return jsonResponse({
|
|
118
341
|
account_data: { events: [] },
|
|
119
342
|
device_lists: { changed: [], left: [] },
|
|
@@ -125,7 +348,7 @@ async function handleSync(url, state) {
|
|
|
125
348
|
});
|
|
126
349
|
}
|
|
127
350
|
function findRoom(state, encodedRoomId) {
|
|
128
|
-
return state.rooms.get(
|
|
351
|
+
return state.rooms.get(decodeMatrixPathSegment(encodedRoomId));
|
|
129
352
|
}
|
|
130
353
|
async function handleAdminInbound(params) {
|
|
131
354
|
const roomId = readTrimmedString(params.body.roomId);
|
|
@@ -134,6 +357,12 @@ async function handleAdminInbound(params) {
|
|
|
134
357
|
if (!roomId || !sender || !text) {
|
|
135
358
|
return jsonResponse({ error: "roomId, senderId, and text are required", ok: false }, 400);
|
|
136
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
|
+
}
|
|
137
366
|
const direct = params.body.direct === true;
|
|
138
367
|
const room = params.state.rooms.get(roomId) ??
|
|
139
368
|
createRoom({
|
|
@@ -147,7 +376,8 @@ async function handleAdminInbound(params) {
|
|
|
147
376
|
});
|
|
148
377
|
params.state.rooms.set(roomId, room);
|
|
149
378
|
const senderName = readTrimmedString(params.body.senderName);
|
|
150
|
-
|
|
379
|
+
const existingProfile = room.users.get(sender);
|
|
380
|
+
if (existingProfile === undefined) {
|
|
151
381
|
room.users.set(sender, senderName ? { display_name: senderName } : {});
|
|
152
382
|
const membership = createEvent({
|
|
153
383
|
content: {
|
|
@@ -161,13 +391,31 @@ async function handleAdminInbound(params) {
|
|
|
161
391
|
type: "m.room.member",
|
|
162
392
|
});
|
|
163
393
|
room.state.push(membership);
|
|
164
|
-
room
|
|
394
|
+
appendTimelineEvent(room, { event: membership, sequence: params.state.nextSequence++ });
|
|
165
395
|
}
|
|
166
|
-
else if (senderName) {
|
|
167
|
-
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
|
+
});
|
|
168
417
|
}
|
|
169
418
|
const content = { body: text, msgtype: "m.text" };
|
|
170
|
-
const threadId = readTrimmedString(params.body.threadId);
|
|
171
419
|
if (threadId) {
|
|
172
420
|
content["m.relates_to"] = {
|
|
173
421
|
event_id: threadId,
|
|
@@ -183,7 +431,7 @@ async function handleAdminInbound(params) {
|
|
|
183
431
|
state: params.state,
|
|
184
432
|
type: "m.room.message",
|
|
185
433
|
});
|
|
186
|
-
room
|
|
434
|
+
appendTimelineEvent(room, { event, sequence: params.state.nextSequence++ });
|
|
187
435
|
notifySyncWaiters(params.state);
|
|
188
436
|
return jsonResponse({ event, ok: true });
|
|
189
437
|
}
|
|
@@ -204,7 +452,7 @@ async function handleMatrixApi(params) {
|
|
|
204
452
|
}
|
|
205
453
|
let match = /^\/profile\/([^/]+)$/u.exec(relativePath);
|
|
206
454
|
if (params.method === "GET" && match) {
|
|
207
|
-
const userId =
|
|
455
|
+
const userId = decodeMatrixPathSegment(match[1]);
|
|
208
456
|
const member = [...params.state.rooms.values()]
|
|
209
457
|
.map((room) => room.users.get(userId))
|
|
210
458
|
.find((profile) => profile !== undefined);
|
|
@@ -224,18 +472,28 @@ async function handleMatrixApi(params) {
|
|
|
224
472
|
}
|
|
225
473
|
match = /^\/user\/([^/]+)\/filter$/u.exec(relativePath);
|
|
226
474
|
if (params.method === "POST" && match) {
|
|
227
|
-
const userId =
|
|
475
|
+
const userId = decodeMatrixPathSegment(match[1]);
|
|
228
476
|
if (userId !== params.state.botUserId) {
|
|
229
477
|
return matrixError("M_FORBIDDEN", "Cannot create a filter for another user", 403);
|
|
230
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
|
+
}
|
|
231
484
|
const filterId = String(params.state.nextFilter++);
|
|
232
|
-
params.state.filters.set(filterId, params.body);
|
|
485
|
+
params.state.filters.set(filterId, { body: params.body, bytes });
|
|
486
|
+
params.state.filterBytes += bytes;
|
|
233
487
|
return jsonResponse({ filter_id: filterId });
|
|
234
488
|
}
|
|
235
489
|
match = /^\/user\/([^/]+)\/filter\/([^/]+)$/u.exec(relativePath);
|
|
236
490
|
if (params.method === "GET" && match) {
|
|
237
|
-
const
|
|
238
|
-
|
|
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);
|
|
239
497
|
}
|
|
240
498
|
match = /^\/rooms\/([^/]+)\/joined_members$/u.exec(relativePath);
|
|
241
499
|
if (params.method === "GET" && match) {
|
|
@@ -250,8 +508,8 @@ async function handleMatrixApi(params) {
|
|
|
250
508
|
if (!room) {
|
|
251
509
|
return matrixError("M_NOT_FOUND", "Unknown room", 404);
|
|
252
510
|
}
|
|
253
|
-
const eventType =
|
|
254
|
-
const stateKey =
|
|
511
|
+
const eventType = decodeMatrixPathSegment(match[2]);
|
|
512
|
+
const stateKey = decodeMatrixPathSegment(match[3] ?? "");
|
|
255
513
|
if (eventType === "m.room.name" && stateKey === "") {
|
|
256
514
|
return jsonResponse({ name: room.name });
|
|
257
515
|
}
|
|
@@ -270,28 +528,125 @@ async function handleMatrixApi(params) {
|
|
|
270
528
|
}
|
|
271
529
|
match = /^\/rooms\/([^/]+)\/send\/([^/]+)\/([^/]+)$/u.exec(relativePath);
|
|
272
530
|
if (params.method === "PUT" && match) {
|
|
273
|
-
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);
|
|
274
551
|
if (!room) {
|
|
275
|
-
|
|
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);
|
|
276
560
|
}
|
|
277
561
|
const event = createEvent({
|
|
278
562
|
content: params.body,
|
|
279
563
|
roomId: room.id,
|
|
280
564
|
sender: params.state.botUserId,
|
|
281
565
|
state: params.state,
|
|
282
|
-
transactionId
|
|
283
|
-
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,
|
|
284
575
|
});
|
|
285
|
-
room.timeline.push({ event, sequence: params.state.nextSequence++ });
|
|
286
576
|
notifySyncWaiters(params.state);
|
|
287
|
-
|
|
577
|
+
params.recorderEvent.accepted = true;
|
|
578
|
+
return jsonResponse(body);
|
|
288
579
|
}
|
|
289
580
|
match = /^\/rooms\/([^/]+)\/typing\/([^/]+)$/u.exec(relativePath);
|
|
290
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);
|
|
291
622
|
return jsonResponse({});
|
|
292
623
|
}
|
|
293
624
|
match = /^\/rooms\/([^/]+)\/receipt\/m\.read\/([^/]+)$/u.exec(relativePath);
|
|
294
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);
|
|
295
650
|
return jsonResponse({});
|
|
296
651
|
}
|
|
297
652
|
return matrixError("M_UNRECOGNIZED", "Unrecognized request", 404);
|
|
@@ -304,14 +659,17 @@ export async function startMatrixServer(params = {}) {
|
|
|
304
659
|
adminToken: params.adminToken ?? randomBytes(24).toString("hex"),
|
|
305
660
|
botUserId: params.botUserId ?? `@openclaw:${serverName}`,
|
|
306
661
|
deviceId: params.deviceId ?? "CRABLINE",
|
|
662
|
+
filterBytes: 0,
|
|
307
663
|
filters: new Map(),
|
|
308
664
|
nextEvent: 1,
|
|
309
665
|
nextFilter: 1,
|
|
310
666
|
nextSequence: 1,
|
|
667
|
+
onEvent: params.onEvent,
|
|
311
668
|
recorderPath: params.recorderPath ?? path.resolve("artifacts/crabline/matrix.jsonl"),
|
|
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,
|