@openclaw/crabline 0.1.10 → 0.1.12
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 +99 -12
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +264 -50
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +12 -2
- package/dist/src/openclaw/artifact-generation.js +436 -15
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/matrix.js +21 -4
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +13 -8
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +15 -6
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +116 -23
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +45 -19
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +32 -16
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +8 -3
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +40 -3
- package/dist/src/openclaw/private-file.js +2070 -95
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/shared.d.ts +1 -0
- package/dist/src/openclaw/shared.js +48 -13
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +9 -0
- package/dist/src/openclaw/smoke-lock.js +696 -187
- package/dist/src/openclaw/smoke-lock.js.map +1 -1
- package/dist/src/openclaw.js +352 -29
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +532 -42
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1241 -22
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +259 -51
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1193 -118
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -0
- package/dist/src/servers/whatsapp.js +149 -24
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +185 -43
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -3,10 +3,27 @@ import path from "node:path";
|
|
|
3
3
|
import { adminAuthError, hasAdminToken, InvalidJsonBodyError, isJsonObject, isLoopbackHost, jsonResponse, parseUnknownRequestBody, queryRecord, readBody, readInteger, readTrimmedString, RequestBodyTooLargeError, startHttpJsonServer, } from "./http.js";
|
|
4
4
|
import { SLACK_CHANNEL_ID_RULE, SLACK_SEND_TARGET_ID_RULE, SLACK_TS_RULE, SLACK_USER_ID_RULE, } from "../providers/slack-ids.js";
|
|
5
5
|
import { recordCommittedServerEvent, recordServerEvent, } from "./recorder.js";
|
|
6
|
+
import { postWebhookRequestWithResponse, validateWebhookTarget, } from "./webhook-target.js";
|
|
6
7
|
const SLACK_SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
|
|
7
8
|
const SLACK_EVENT_MAX_RETRIES = 3;
|
|
8
9
|
const SLACK_EVENT_MAX_REDIRECTS = 2;
|
|
9
10
|
const SLACK_EVENT_RETRY_DELAYS_MS = [0, 60_000, 5 * 60_000];
|
|
11
|
+
class SlackEventHttpError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
constructor(status) {
|
|
14
|
+
super(`Slack Events API delivery failed with HTTP ${status}.`);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.name = "SlackEventHttpError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
class SlackEventTargetError extends Error {
|
|
20
|
+
retryReason;
|
|
21
|
+
constructor(retryReason, targetError) {
|
|
22
|
+
super(`Slack Events API target rejected: ${targetError}`);
|
|
23
|
+
this.retryReason = retryReason;
|
|
24
|
+
this.name = "SlackEventTargetError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
10
27
|
function slackOk(result = {}) {
|
|
11
28
|
return jsonResponse({ ok: true, ...result });
|
|
12
29
|
}
|
|
@@ -162,12 +179,24 @@ function randomDecimalDigits(length) {
|
|
|
162
179
|
return Array.from({ length }, () => randomInt(10)).join("");
|
|
163
180
|
}
|
|
164
181
|
function nextDmChannelId(state) {
|
|
165
|
-
|
|
166
|
-
|
|
182
|
+
for (;;) {
|
|
183
|
+
const index = state.nextDmIndex++;
|
|
184
|
+
const channelId = `D${String(index).padStart(9, "0")}`;
|
|
185
|
+
if (!state.messagesByChannel.has(channelId) &&
|
|
186
|
+
![...state.userDmChannels.values()].includes(channelId)) {
|
|
187
|
+
return channelId;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
167
190
|
}
|
|
168
191
|
function nextMpimChannelId(state) {
|
|
169
|
-
|
|
170
|
-
|
|
192
|
+
for (;;) {
|
|
193
|
+
const index = state.nextMpimIndex++;
|
|
194
|
+
const channelId = `G${String(index).padStart(9, "0")}`;
|
|
195
|
+
if (!state.messagesByChannel.has(channelId) &&
|
|
196
|
+
![...state.userMpimChannels.values()].some((channel) => channel.id === channelId)) {
|
|
197
|
+
return channelId;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
171
200
|
}
|
|
172
201
|
function dmChannelForUser(state, userId) {
|
|
173
202
|
const existing = state.userDmChannels.get(userId);
|
|
@@ -178,6 +207,16 @@ function dmChannelForUser(state, userId) {
|
|
|
178
207
|
state.userDmChannels.set(userId, channelId);
|
|
179
208
|
return channelId;
|
|
180
209
|
}
|
|
210
|
+
function rememberDmChannel(state, userId, channelId) {
|
|
211
|
+
if (!channelId.startsWith("D")) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const mappedUser = [...state.userDmChannels].find(([, value]) => value === channelId)?.[0];
|
|
215
|
+
if (mappedUser && mappedUser !== userId) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
state.userDmChannels.set(userId, channelId);
|
|
219
|
+
}
|
|
181
220
|
function mpimChannelForUsers(state, users) {
|
|
182
221
|
const key = [...users].sort().join(",");
|
|
183
222
|
const existing = state.userMpimChannels.get(key);
|
|
@@ -230,7 +269,10 @@ function decodeSlackCursor(value, kind) {
|
|
|
230
269
|
return slackError("invalid_cursor");
|
|
231
270
|
}
|
|
232
271
|
const boundary = decoded.slice(prefix.length);
|
|
233
|
-
|
|
272
|
+
const validBoundary = kind === "list"
|
|
273
|
+
? SLACK_CHANNEL_ID_RULE.pattern.test(boundary)
|
|
274
|
+
: SLACK_TS_RULE.pattern.test(boundary);
|
|
275
|
+
return validBoundary && encodeSlackCursor(kind, boundary) === cursor
|
|
234
276
|
? boundary
|
|
235
277
|
: slackError("invalid_cursor");
|
|
236
278
|
}
|
|
@@ -247,6 +289,72 @@ function appendMessage(state, message) {
|
|
|
247
289
|
state.messagesByChannel.set(message.channel, messages);
|
|
248
290
|
return message;
|
|
249
291
|
}
|
|
292
|
+
function slackConversation(state, channelId) {
|
|
293
|
+
const dmUser = [...state.userDmChannels].find(([, value]) => value === channelId)?.[0];
|
|
294
|
+
const mpim = [...state.userMpimChannels.values()].find((candidate) => candidate.id === channelId);
|
|
295
|
+
const hasMessages = state.messagesByChannel.has(channelId);
|
|
296
|
+
if (!dmUser && !mpim && !hasMessages) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
if (dmUser) {
|
|
300
|
+
return {
|
|
301
|
+
id: channelId,
|
|
302
|
+
is_channel: false,
|
|
303
|
+
is_group: false,
|
|
304
|
+
is_im: true,
|
|
305
|
+
is_mpim: false,
|
|
306
|
+
user: dmUser,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (mpim) {
|
|
310
|
+
return {
|
|
311
|
+
id: channelId,
|
|
312
|
+
is_channel: false,
|
|
313
|
+
is_group: false,
|
|
314
|
+
is_im: false,
|
|
315
|
+
is_mpim: true,
|
|
316
|
+
is_private: true,
|
|
317
|
+
members: [state.botUserId, ...mpim.users],
|
|
318
|
+
name: "crabline",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
id: channelId,
|
|
323
|
+
is_channel: channelId.startsWith("C"),
|
|
324
|
+
is_group: channelId.startsWith("G"),
|
|
325
|
+
is_im: channelId.startsWith("D"),
|
|
326
|
+
is_mpim: false,
|
|
327
|
+
name: "crabline",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function slackConversations(state) {
|
|
331
|
+
const ids = new Set([
|
|
332
|
+
...state.messagesByChannel.keys(),
|
|
333
|
+
...state.userDmChannels.values(),
|
|
334
|
+
...[...state.userMpimChannels.values()].map((channel) => channel.id),
|
|
335
|
+
]);
|
|
336
|
+
return [...ids]
|
|
337
|
+
.sort()
|
|
338
|
+
.map((channelId) => slackConversation(state, channelId))
|
|
339
|
+
.filter((channel) => channel !== undefined);
|
|
340
|
+
}
|
|
341
|
+
function requireSlackConversationTypes(value) {
|
|
342
|
+
const types = new Set((readTrimmedString(value) ?? "public_channel")
|
|
343
|
+
.split(",")
|
|
344
|
+
.map((entry) => entry.trim())
|
|
345
|
+
.filter(Boolean));
|
|
346
|
+
const supported = new Set(["im", "mpim", "private_channel", "public_channel"]);
|
|
347
|
+
return [...types].every((type) => supported.has(type)) ? types : slackError("invalid_types");
|
|
348
|
+
}
|
|
349
|
+
function slackConversationType(conversation) {
|
|
350
|
+
if (conversation.is_im === true) {
|
|
351
|
+
return "im";
|
|
352
|
+
}
|
|
353
|
+
if (conversation.is_mpim === true) {
|
|
354
|
+
return "mpim";
|
|
355
|
+
}
|
|
356
|
+
return conversation.is_group === true ? "private_channel" : "public_channel";
|
|
357
|
+
}
|
|
250
358
|
async function appendEvent(state, event, committed = false) {
|
|
251
359
|
const params = { event, onEvent: state.onEvent, recorderPath: state.recorderPath };
|
|
252
360
|
await (committed ? recordCommittedServerEvent(params) : recordServerEvent(params));
|
|
@@ -281,10 +389,18 @@ function createSlackMessage(state, params) {
|
|
|
281
389
|
user: params.user ?? (params.bot ? state.botUserId : "UCRABUSER"),
|
|
282
390
|
};
|
|
283
391
|
}
|
|
284
|
-
function asSlackEventCallback(message) {
|
|
392
|
+
function asSlackEventCallback(state, message) {
|
|
285
393
|
return {
|
|
286
394
|
api_app_id: "ACRABLINE",
|
|
287
|
-
authorizations: [
|
|
395
|
+
authorizations: [
|
|
396
|
+
{
|
|
397
|
+
enterprise_id: null,
|
|
398
|
+
is_bot: true,
|
|
399
|
+
is_enterprise_install: false,
|
|
400
|
+
team_id: "TCRABLINE",
|
|
401
|
+
user_id: state.botUserId,
|
|
402
|
+
},
|
|
403
|
+
],
|
|
288
404
|
event: message,
|
|
289
405
|
event_id: `Ev${message.ts.replace(".", "")}`,
|
|
290
406
|
event_time: Number(message.ts.slice(0, 10)),
|
|
@@ -328,7 +444,14 @@ function errorChain(error) {
|
|
|
328
444
|
}
|
|
329
445
|
return chain;
|
|
330
446
|
}
|
|
331
|
-
|
|
447
|
+
/** @internal */
|
|
448
|
+
export function classifySlackRetryReason(error) {
|
|
449
|
+
if (error instanceof SlackEventHttpError) {
|
|
450
|
+
return "http_error";
|
|
451
|
+
}
|
|
452
|
+
if (error instanceof SlackEventTargetError) {
|
|
453
|
+
return error.retryReason;
|
|
454
|
+
}
|
|
332
455
|
const chain = errorChain(error);
|
|
333
456
|
const names = chain.map((entry) => String(entry.name ?? ""));
|
|
334
457
|
const codes = chain.map((entry) => String(entry.code ?? "").toUpperCase());
|
|
@@ -361,6 +484,62 @@ function classifySlackRetryReason(error) {
|
|
|
361
484
|
}
|
|
362
485
|
return "unknown_error";
|
|
363
486
|
}
|
|
487
|
+
function slackResponseHeader(response, name) {
|
|
488
|
+
const value = response.headers[name];
|
|
489
|
+
return Array.isArray(value) ? value[0] : value;
|
|
490
|
+
}
|
|
491
|
+
function slackTargetRetryReason(error) {
|
|
492
|
+
return error === "https-required" ? "ssl_error" : "connection_failed";
|
|
493
|
+
}
|
|
494
|
+
/** @internal */
|
|
495
|
+
export async function postSlackEventToAddresses(params) {
|
|
496
|
+
const addresses = params.addresses && params.addresses.length > 0 ? [...params.addresses] : [undefined];
|
|
497
|
+
const request = params.request ?? postWebhookRequestWithResponse;
|
|
498
|
+
let lastError;
|
|
499
|
+
for (const [index, address] of addresses.entries()) {
|
|
500
|
+
const remainingMs = params.timeoutAt - Date.now();
|
|
501
|
+
if (remainingMs <= 0) {
|
|
502
|
+
throw new DOMException("Slack Events API delivery timed out", "TimeoutError");
|
|
503
|
+
}
|
|
504
|
+
const attemptsRemaining = addresses.length - index;
|
|
505
|
+
try {
|
|
506
|
+
return await request({
|
|
507
|
+
address,
|
|
508
|
+
body: params.body,
|
|
509
|
+
headerEntries: params.headerEntries,
|
|
510
|
+
signal: params.signal,
|
|
511
|
+
timeoutMs: Math.max(1, Math.floor(remainingMs / attemptsRemaining)),
|
|
512
|
+
url: params.url,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
lastError = error;
|
|
517
|
+
if (params.signal.aborted) {
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
throw lastError;
|
|
523
|
+
}
|
|
524
|
+
async function postSlackEventRequest(params) {
|
|
525
|
+
const target = await validateWebhookTarget({
|
|
526
|
+
allowLoopbackHttp: params.state.allowLoopbackHttpEvents,
|
|
527
|
+
restrictPrivateAddresses: params.state.restrictEventTargets,
|
|
528
|
+
signal: params.signal,
|
|
529
|
+
url: params.url,
|
|
530
|
+
});
|
|
531
|
+
if ("error" in target) {
|
|
532
|
+
throw new SlackEventTargetError(slackTargetRetryReason(target.error), target.error);
|
|
533
|
+
}
|
|
534
|
+
return await postSlackEventToAddresses({
|
|
535
|
+
addresses: target.addresses,
|
|
536
|
+
body: params.body,
|
|
537
|
+
headerEntries: params.headerEntries,
|
|
538
|
+
signal: params.signal,
|
|
539
|
+
timeoutAt: params.timeoutAt,
|
|
540
|
+
url: params.url,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
364
543
|
async function waitForSlackRetry(delayMs, signal) {
|
|
365
544
|
if (signal.aborted) {
|
|
366
545
|
return false;
|
|
@@ -397,47 +576,50 @@ async function deliverSlackEvent(state, event, lifecycleSignal) {
|
|
|
397
576
|
}
|
|
398
577
|
try {
|
|
399
578
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
400
|
-
const
|
|
401
|
-
"content-type"
|
|
579
|
+
const headerEntries = [
|
|
580
|
+
["content-type", "application/json"],
|
|
402
581
|
...(attempt > 0
|
|
403
|
-
?
|
|
404
|
-
"x-slack-retry-num"
|
|
405
|
-
"x-slack-retry-reason"
|
|
406
|
-
|
|
407
|
-
:
|
|
408
|
-
"x-slack-request-timestamp"
|
|
409
|
-
"x-slack-signature"
|
|
410
|
-
|
|
582
|
+
? [
|
|
583
|
+
["x-slack-retry-num", String(attempt)],
|
|
584
|
+
["x-slack-retry-reason", retryReason ?? "unknown_error"],
|
|
585
|
+
]
|
|
586
|
+
: []),
|
|
587
|
+
["x-slack-request-timestamp", timestamp],
|
|
588
|
+
["x-slack-signature", slackRequestSignature(state.signingSecret, timestamp, body)],
|
|
589
|
+
];
|
|
411
590
|
const signal = AbortSignal.any([lifecycleSignal, AbortSignal.timeout(3_000)]);
|
|
412
|
-
|
|
591
|
+
const timeoutAt = Date.now() + 3_000;
|
|
592
|
+
let requestUrl = new URL(state.eventsRequestUrl);
|
|
413
593
|
let response;
|
|
414
594
|
for (let redirectCount = 0;; redirectCount += 1) {
|
|
415
|
-
response = await
|
|
595
|
+
response = await postSlackEventRequest({
|
|
416
596
|
body,
|
|
417
|
-
|
|
418
|
-
method: "POST",
|
|
419
|
-
redirect: "manual",
|
|
597
|
+
headerEntries,
|
|
420
598
|
signal,
|
|
599
|
+
state,
|
|
600
|
+
timeoutAt,
|
|
601
|
+
url: requestUrl,
|
|
421
602
|
});
|
|
422
|
-
const
|
|
603
|
+
const noRetry = slackResponseHeader(response, "x-slack-no-retry") === "1";
|
|
604
|
+
if (noRetry) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const location = slackResponseHeader(response, "location");
|
|
423
608
|
if (![301, 302].includes(response.status) || !location) {
|
|
424
609
|
break;
|
|
425
610
|
}
|
|
426
|
-
await response.body?.cancel();
|
|
427
611
|
if (redirectCount >= SLACK_EVENT_MAX_REDIRECTS) {
|
|
428
612
|
response = undefined;
|
|
429
613
|
retryReason = "too_many_redirects";
|
|
430
614
|
break;
|
|
431
615
|
}
|
|
432
|
-
requestUrl = new URL(location, requestUrl)
|
|
616
|
+
requestUrl = new URL(location, requestUrl);
|
|
433
617
|
}
|
|
434
618
|
if (response) {
|
|
435
|
-
|
|
436
|
-
await response.body?.cancel();
|
|
437
|
-
if (response.ok || noRetry) {
|
|
619
|
+
if (response.status >= 200 && response.status < 300) {
|
|
438
620
|
return;
|
|
439
621
|
}
|
|
440
|
-
|
|
622
|
+
throw new SlackEventHttpError(response.status);
|
|
441
623
|
}
|
|
442
624
|
}
|
|
443
625
|
catch (error) {
|
|
@@ -489,10 +671,13 @@ async function handleSlackApi(params) {
|
|
|
489
671
|
if (rateLimit) {
|
|
490
672
|
rateLimit.remaining -= 1;
|
|
491
673
|
}
|
|
492
|
-
const
|
|
493
|
-
if (
|
|
494
|
-
return
|
|
674
|
+
const requestedChannel = requireSlackSendTargetId(params.body.channel);
|
|
675
|
+
if (requestedChannel instanceof Response) {
|
|
676
|
+
return requestedChannel;
|
|
495
677
|
}
|
|
678
|
+
const channel = SLACK_USER_ID_RULE.pattern.test(requestedChannel)
|
|
679
|
+
? dmChannelForUser(params.state, requestedChannel)
|
|
680
|
+
: requestedChannel;
|
|
496
681
|
const text = readSlackText(params.body.text) ?? "";
|
|
497
682
|
const attachments = readStructuredArray(params.body.attachments, "invalid_attachments");
|
|
498
683
|
if (attachments instanceof Response) {
|
|
@@ -566,21 +751,34 @@ async function handleSlackApi(params) {
|
|
|
566
751
|
if (channel instanceof Response) {
|
|
567
752
|
return channel;
|
|
568
753
|
}
|
|
569
|
-
const
|
|
754
|
+
const conversation = slackConversation(params.state, channel);
|
|
755
|
+
return conversation ? slackOk({ channel: conversation }) : slackError("channel_not_found");
|
|
756
|
+
}
|
|
757
|
+
case "conversations.list": {
|
|
758
|
+
const types = requireSlackConversationTypes(params.body.types);
|
|
759
|
+
if (types instanceof Response) {
|
|
760
|
+
return types;
|
|
761
|
+
}
|
|
762
|
+
const limit = requireSlackLimit(params.body.limit);
|
|
763
|
+
if (limit instanceof Response) {
|
|
764
|
+
return limit;
|
|
765
|
+
}
|
|
766
|
+
const boundary = decodeSlackCursor(params.body.cursor, "list");
|
|
767
|
+
if (boundary instanceof Response) {
|
|
768
|
+
return boundary;
|
|
769
|
+
}
|
|
770
|
+
const remaining = slackConversations(params.state).filter((conversation) => {
|
|
771
|
+
const channelId = readTrimmedString(conversation.id);
|
|
772
|
+
return channelId && (boundary === undefined || channelId > boundary);
|
|
773
|
+
});
|
|
774
|
+
const virtualPage = remaining.slice(0, limit);
|
|
775
|
+
const channels = virtualPage.filter((conversation) => types.has(slackConversationType(conversation)));
|
|
776
|
+
const hasMore = virtualPage.length < remaining.length;
|
|
777
|
+
const nextBoundary = readTrimmedString(virtualPage.at(-1)?.id);
|
|
570
778
|
return slackOk({
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
is_group: mpim ? false : channel.startsWith("G"),
|
|
575
|
-
is_im: channel.startsWith("D"),
|
|
576
|
-
...(mpim
|
|
577
|
-
? {
|
|
578
|
-
is_mpim: true,
|
|
579
|
-
is_private: true,
|
|
580
|
-
members: [params.state.botUserId, ...mpim.users],
|
|
581
|
-
}
|
|
582
|
-
: {}),
|
|
583
|
-
name: "crabline",
|
|
779
|
+
channels,
|
|
780
|
+
response_metadata: {
|
|
781
|
+
next_cursor: hasMore && nextBoundary ? encodeSlackCursor("list", nextBoundary) : "",
|
|
584
782
|
},
|
|
585
783
|
});
|
|
586
784
|
}
|
|
@@ -589,6 +787,9 @@ async function handleSlackApi(params) {
|
|
|
589
787
|
if (channel instanceof Response) {
|
|
590
788
|
return channel;
|
|
591
789
|
}
|
|
790
|
+
if (!slackConversation(params.state, channel)) {
|
|
791
|
+
return slackError("channel_not_found");
|
|
792
|
+
}
|
|
592
793
|
const oldest = requireSlackThreadTs(params.body.oldest);
|
|
593
794
|
if (oldest instanceof Response) {
|
|
594
795
|
return oldest;
|
|
@@ -636,6 +837,9 @@ async function handleSlackApi(params) {
|
|
|
636
837
|
if (channel instanceof Response) {
|
|
637
838
|
return channel;
|
|
638
839
|
}
|
|
840
|
+
if (!slackConversation(params.state, channel)) {
|
|
841
|
+
return slackError("channel_not_found");
|
|
842
|
+
}
|
|
639
843
|
const ts = requireSlackThreadTs(params.body.ts);
|
|
640
844
|
if (ts instanceof Response) {
|
|
641
845
|
return ts;
|
|
@@ -693,6 +897,7 @@ async function handleAdminInbound(params) {
|
|
|
693
897
|
if (user instanceof Response) {
|
|
694
898
|
return user;
|
|
695
899
|
}
|
|
900
|
+
rememberDmChannel(params.state, user, channel);
|
|
696
901
|
const threadTs = requireSlackThreadTs(params.body.threadTs ?? params.body.thread_ts);
|
|
697
902
|
if (threadTs instanceof Response) {
|
|
698
903
|
return threadTs;
|
|
@@ -713,7 +918,7 @@ async function handleAdminInbound(params) {
|
|
|
713
918
|
ts,
|
|
714
919
|
user,
|
|
715
920
|
}));
|
|
716
|
-
const event = asSlackEventCallback(message);
|
|
921
|
+
const event = asSlackEventCallback(params.state, message);
|
|
717
922
|
scheduleSlackEventDelivery(params.state, event);
|
|
718
923
|
return slackOk({ event, message });
|
|
719
924
|
}
|
|
@@ -819,17 +1024,18 @@ async function handleRequest(params) {
|
|
|
819
1024
|
state: params.state,
|
|
820
1025
|
});
|
|
821
1026
|
const mutation = ["chat.postMessage", "conversations.open"].includes(methodMatch[1]);
|
|
822
|
-
const
|
|
1027
|
+
const recordsAcceptance = mutation || methodMatch[1] === "auth.test";
|
|
1028
|
+
const payload = recordsAcceptance
|
|
823
1029
|
? (await response
|
|
824
1030
|
.clone()
|
|
825
1031
|
.json()
|
|
826
1032
|
.catch(() => undefined))
|
|
827
1033
|
: undefined;
|
|
828
1034
|
const committed = isJsonObject(payload) && payload.ok === true;
|
|
829
|
-
if (methodMatch[1] === "chat.postMessage") {
|
|
1035
|
+
if (methodMatch[1] === "auth.test" || methodMatch[1] === "chat.postMessage") {
|
|
830
1036
|
event.accepted = committed;
|
|
831
1037
|
}
|
|
832
|
-
await appendEvent(params.state, event, committed);
|
|
1038
|
+
await appendEvent(params.state, event, mutation && committed);
|
|
833
1039
|
return response;
|
|
834
1040
|
}
|
|
835
1041
|
export async function startSlackServer(params = {}) {
|
|
@@ -838,6 +1044,7 @@ export async function startSlackServer(params = {}) {
|
|
|
838
1044
|
const state = {
|
|
839
1045
|
activeEventDeliveries: new Set(),
|
|
840
1046
|
adminToken: params.adminToken ?? randomBytes(24).toString("base64url"),
|
|
1047
|
+
allowLoopbackHttpEvents: isLoopbackHost(host),
|
|
841
1048
|
botId: params.botId ?? "BCRABLINE",
|
|
842
1049
|
botToken: params.botToken ?? "xoxb-crabline-slack-token",
|
|
843
1050
|
botUserId: params.botUserId ?? "UCRABBOT",
|
|
@@ -850,6 +1057,7 @@ export async function startSlackServer(params = {}) {
|
|
|
850
1057
|
nextTsIndex: 100,
|
|
851
1058
|
onEvent: params.onEvent,
|
|
852
1059
|
recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "slack.jsonl"),
|
|
1060
|
+
restrictEventTargets: true,
|
|
853
1061
|
signingSecret: params.signingSecret ?? "crabline-slack-signing-secret",
|
|
854
1062
|
userDmChannels: new Map(),
|
|
855
1063
|
userMpimChannels: new Map(),
|