@openclaw/slack 2026.7.1 → 2026.7.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts.runtime-KvftRRek.js +2 -0
- package/dist/{action-runtime-DoQGk_8D.js → action-runtime-p6M2ULpv.js} +205 -29
- package/dist/action-runtime.runtime-DQstDkHG.js +2 -0
- package/dist/{actions-DMpDmKEA.js → actions-DkTcWmpT.js} +87 -138
- package/dist/{actions.runtime-C103_BPW.js → actions.runtime-BSxpwgUc.js} +1 -1
- package/dist/api.js +14 -13
- package/dist/{approval-handler.runtime-CIUbOt5T.js → approval-handler.runtime-UGbnz9OC.js} +3 -3
- package/dist/{channel-BjlsaGHn.js → channel-B0_j6C6z.js} +116 -140
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel-type-BpeTezY4.js +109 -0
- package/dist/{channel.setup-Dq6syTNU.js → channel.setup-BQFut21T.js} +2 -2
- package/dist/{client-DGxwSL7f.js → client-D2F6m8CP.js} +38 -2
- package/dist/config-api-Df9a1B8C.js +3 -0
- package/dist/{config-schema-BzqzFW__.js → config-schema-DYI01PV2.js} +20 -37
- package/dist/cursor-pages-DSmTg0su.js +13 -0
- package/dist/{directory-config-BZuS8141.js → directory-config-DRkFujwG.js} +2 -2
- package/dist/directory-contract-api.js +1 -1
- package/dist/{directory-live-BNsxCgqP.js → directory-live-DX7yFsM-.js} +2 -2
- package/dist/{doctor-contract-XL6HeNCb.js → doctor-contract-DddfkMlF.js} +25 -38
- package/dist/doctor-contract-api.js +1 -1
- package/dist/group-policy-C-b2ZnSl.js +2332 -0
- package/dist/{interactive-replies-CXpT-vA_.js → interactive-replies-BilWUdcI.js} +5 -1
- package/dist/interactive-replies-api.js +1 -1
- package/dist/{message-tool-api-BgWKyu2r.js → message-tool-api-CA9jZrCY.js} +1 -1
- package/dist/message-tool-api.js +1 -1
- package/dist/{monitor-Ux-wzUWI.js → monitor-BN_uuvrQ.js} +3 -3
- package/dist/outbound-adapter-ECl0go7d.js +228 -0
- package/dist/{pipeline.runtime-rpVpay59.js → pipeline.runtime-f0eJJoJL.js} +492 -178
- package/dist/{policy-Dj-puVPb.js → policy-f8b0edSi.js} +1 -1
- package/dist/preflight-audio.runtime-D5Bgags0.js +3 -0
- package/dist/{probe-CuwRDE5j.js → probe-CfALk0l4.js} +2 -2
- package/dist/{provider-C1-DFSpw.js → provider-Ds-y-imP.js} +658 -192
- package/dist/{relay-source-D2jv1iGg.js → relay-source-B-6LFUdz.js} +6 -17
- package/dist/replies-BMvf3ihj.js +420 -0
- package/dist/{resolve-channels-CstkChBE.js → resolve-channels-DOMgNCWb.js} +17 -6
- package/dist/{resolve-users-C3NP2c-Q.js → resolve-users-aL6-Ssee.js} +11 -6
- package/dist/{runtime-BOk7xkOl.js → runtime-CauEvswP.js} +1 -1
- package/dist/{runtime-api-CEmqYxR-.js → runtime-api-CVb6CQjX.js} +3 -3
- package/dist/runtime-api.js +11 -11
- package/dist/runtime-setter-api.js +1 -1
- package/dist/{scopes-DCWVD5-8.js → scopes-Dw8wbU0j.js} +2 -2
- package/dist/{secret-contract-C3ChHo5A.js → secret-contract-CMohXFWS.js} +18 -113
- package/dist/secret-contract-api.js +1 -1
- package/dist/{send-DKDXoeoV.js → send-NEfJ0GvM.js} +662 -325
- package/dist/{send.runtime-GPRa63l1.js → send.runtime-BkKpitPu.js} +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{setup-surface-Dv2fIJNB.js → setup-surface-BN2WeG5c.js} +3 -3
- package/dist/{shared-CosfcFri.js → shared-BLnB9R1T.js} +7 -5
- package/dist/{slash-dispatch.runtime-CcMbbT3_.js → slash-dispatch.runtime-C0Otuf_T.js} +1 -1
- package/dist/{streaming-compat-DjlgH-Be.js → streaming-compat-3_X-21wW.js} +1 -0
- package/dist/{target-parsing-3jYH8LfB.js → target-parsing-N6lHogYZ.js} +16 -3
- package/dist/{targets-Dll4NJgs.js → targets-KmjP2E8s.js} +1 -1
- package/node_modules/p-map/index.d.ts +155 -0
- package/node_modules/p-map/index.js +284 -0
- package/node_modules/p-map/license +9 -0
- package/node_modules/p-map/package.json +57 -0
- package/node_modules/p-map/readme.md +190 -0
- package/npm-shrinkwrap.json +16 -3
- package/openclaw.plugin.json +16 -0
- package/package.json +6 -4
- package/dist/accounts.runtime-DWvaCb-X.js +0 -2
- package/dist/action-runtime.runtime-BABj3aM-.js +0 -2
- package/dist/config-api-DnT9ohtE.js +0 -2
- package/dist/format-YCNoNkM2.js +0 -107
- package/dist/outbound-adapter-DVoBfBW5.js +0 -272
- package/dist/replies-DpF38e1y.js +0 -287
- package/dist/reply-blocks-BGQ6zSqM.js +0 -877
- package/dist/resolve-allowlist-common-CP8SH0Zf.js +0 -43
|
@@ -1,81 +1,424 @@
|
|
|
1
1
|
import { a as resolveSlackAccount, l as resolveSlackOperationToken, p as resolveSlackBotToken } from "./accounts-BOJJiHSr.js";
|
|
2
|
-
import {
|
|
3
|
-
import "./
|
|
4
|
-
import { a as SLACK_TEXT_LIMIT, b as truncateSlackText, i as validateSlackBlocksArray } from "./reply-blocks-BGQ6zSqM.js";
|
|
2
|
+
import { i as parseSlackTarget, t as canonicalizeSlackApiTargetId } from "./target-parsing-N6lHogYZ.js";
|
|
3
|
+
import { A as buildSlackCompleteBlocksFallbackText, C as hasSlackNativeDataBlock, D as markdownToSlackMrkdwnChunks, E as chunkSlackMrkdwnText, F as SLACK_TEXT_LIMIT, P as SLACK_MESSAGE_TEXT_HARD_LIMIT, S as createSlackNativeDataBaseTextConsumer, T as stripSlackNativeDataBlocks, Z as validateSlackBlocksArray, b as appendSlackNativeDataPlainTextFallback, i as assertSlackDirectSendAllowed, j as renderSlackBlockFallbackText, w as isSlackInvalidBlocksError, x as buildSlackNativeDataAccessibilityText, z as truncateSlackText } from "./group-policy-C-b2ZnSl.js";
|
|
5
4
|
import { n as resolveSlackThreadTsValue, t as normalizeSlackThreadTsCandidate } from "./thread-ts-DUGhaYKq.js";
|
|
6
|
-
import {
|
|
7
|
-
import { t as getOptionalSlackRuntime } from "./runtime-
|
|
8
|
-
import {
|
|
9
|
-
import { i as loadOutboundMediaFromUrl } from "./runtime-api-CEmqYxR-.js";
|
|
5
|
+
import { i as createSlackWebClient, r as createSlackTokenCacheKey, s as getSlackWriteClient } from "./client-D2F6m8CP.js";
|
|
6
|
+
import { t as getOptionalSlackRuntime } from "./runtime-CauEvswP.js";
|
|
7
|
+
import { i as loadOutboundMediaFromUrl } from "./runtime-api-CVb6CQjX.js";
|
|
10
8
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
11
9
|
import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
|
|
12
10
|
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
13
|
-
import {
|
|
11
|
+
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
12
|
+
import { createHash, createHmac } from "node:crypto";
|
|
14
13
|
import { withTrustedEnvProxyGuardedFetchMode } from "openclaw/plugin-sdk/fetch-runtime";
|
|
15
14
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
15
|
+
import { PlatformMessageNotDispatchedError, extractErrorCode, readErrorName } from "openclaw/plugin-sdk/error-runtime";
|
|
16
16
|
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
17
17
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
18
18
|
import { chunkMarkdownTextWithMode, isSilentReplyText, resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
|
|
19
19
|
import { resolveTextChunksWithFallback } from "openclaw/plugin-sdk/reply-payload";
|
|
20
|
+
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
21
|
+
import { buildTimeoutAbortSignal } from "openclaw/plugin-sdk/extension-shared";
|
|
20
22
|
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
21
23
|
import { createPersistentDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
|
22
|
-
//#region extensions/slack/src/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
function
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
24
|
+
//#region extensions/slack/src/native-data-fallback.ts
|
|
25
|
+
const SLACK_SECTION_PLAIN_TEXT_MAX = 3e3;
|
|
26
|
+
const SLACK_EMPTY_BLOCK_FALLBACK = "Shared a Block Kit message";
|
|
27
|
+
function chunkSlackTextAtHardLimit(text, limit = SLACK_MESSAGE_TEXT_HARD_LIMIT) {
|
|
28
|
+
const effectiveLimit = Math.max(2, Math.floor(limit));
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let offset = 0;
|
|
31
|
+
while (offset < text.length) {
|
|
32
|
+
const chunk = sliceUtf16Safe(text, offset, Math.min(text.length, offset + effectiveLimit));
|
|
33
|
+
if (!chunk) throw new Error("Slack plain-text fallback chunking made no progress.");
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
offset += chunk.length;
|
|
36
|
+
}
|
|
37
|
+
return chunks;
|
|
38
|
+
}
|
|
39
|
+
function buildPlainTextBlocks(text) {
|
|
40
|
+
return chunkSlackTextAtHardLimit(text, SLACK_SECTION_PLAIN_TEXT_MAX).map((chunk, index) => {
|
|
41
|
+
const fallbackBlock = {
|
|
42
|
+
block: {
|
|
43
|
+
type: "section",
|
|
44
|
+
text: {
|
|
45
|
+
type: "plain_text",
|
|
46
|
+
text: chunk
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
text: chunk
|
|
50
|
+
};
|
|
51
|
+
if (index > 0) fallbackBlock.continuesText = true;
|
|
52
|
+
return fallbackBlock;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function renderNativeDataPlainText(block) {
|
|
56
|
+
return appendSlackNativeDataPlainTextFallback("", [block]).trim() || "Slack could not render this chart or table data.";
|
|
57
|
+
}
|
|
58
|
+
function buildOrderedFallbackBlocks(params) {
|
|
59
|
+
const entries = [];
|
|
60
|
+
const consumeFromBase = createSlackNativeDataBaseTextConsumer(params.baseText);
|
|
61
|
+
if (params.baseText) entries.push(...buildPlainTextBlocks(params.baseText));
|
|
62
|
+
for (const block of params.blocks) {
|
|
63
|
+
if (hasSlackNativeDataBlock([block])) {
|
|
64
|
+
const nativeText = renderNativeDataPlainText(block);
|
|
65
|
+
if (!consumeFromBase(nativeText)) entries.push(...buildPlainTextBlocks(nativeText));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const text = renderSlackBlockFallbackText(block, { nativeDataFormat: "plain" });
|
|
69
|
+
entries.push({
|
|
70
|
+
block,
|
|
71
|
+
...text ? { text } : {}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return entries;
|
|
75
|
+
}
|
|
76
|
+
function buildOrderedBlockMessages(entries) {
|
|
77
|
+
const messages = [];
|
|
78
|
+
let blocks = [];
|
|
79
|
+
let text = "";
|
|
80
|
+
const flush = () => {
|
|
81
|
+
if (blocks.length === 0) return;
|
|
82
|
+
messages.push({
|
|
83
|
+
text: text || SLACK_EMPTY_BLOCK_FALLBACK,
|
|
84
|
+
blocks,
|
|
85
|
+
mrkdwn: false
|
|
86
|
+
});
|
|
87
|
+
blocks = [];
|
|
88
|
+
text = "";
|
|
89
|
+
};
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const separator = text && entry.text && !entry.continuesText ? "\n\n" : "";
|
|
92
|
+
const nextText = entry.text ? `${text}${separator}${entry.text}` : text;
|
|
93
|
+
if (blocks.length >= 50 || nextText.length > 4e4) flush();
|
|
94
|
+
const freshSeparator = text && entry.text && !entry.continuesText ? "\n\n" : "";
|
|
95
|
+
const freshText = entry.text ? `${text}${freshSeparator}${entry.text}` : text;
|
|
96
|
+
if (freshText.length > 4e4) throw new Error("One Slack fallback block exceeds the message text hard limit.");
|
|
97
|
+
blocks.push(entry.block);
|
|
98
|
+
text = freshText;
|
|
99
|
+
}
|
|
100
|
+
flush();
|
|
101
|
+
return messages;
|
|
102
|
+
}
|
|
103
|
+
/** Build one complete, ordered retry plan after Slack rejects native data blocks. */
|
|
104
|
+
function buildSlackNativeDataDeliveryPlan(params) {
|
|
105
|
+
const baseText = params.baseText?.trim() ?? "";
|
|
106
|
+
const hasNativeData = hasSlackNativeDataBlock(params.blocks);
|
|
107
|
+
const accessibilityText = buildSlackNativeDataAccessibilityText(baseText, params.blocks) || (hasNativeData ? "Slack could not render this chart or table data." : SLACK_EMPTY_BLOCK_FALLBACK);
|
|
108
|
+
return {
|
|
109
|
+
accessibilityText,
|
|
110
|
+
fallbackMessages: stripSlackNativeDataBlocks(params.blocks).length === 0 ? chunkSlackTextAtHardLimit(accessibilityText).map((text) => ({
|
|
111
|
+
text,
|
|
112
|
+
mrkdwn: false
|
|
113
|
+
})) : buildOrderedBlockMessages(buildOrderedFallbackBlocks({
|
|
114
|
+
baseText,
|
|
115
|
+
blocks: params.blocks
|
|
116
|
+
})),
|
|
117
|
+
skipOriginalBlocks: accessibilityText.length > SLACK_MESSAGE_TEXT_HARD_LIMIT
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region extensions/slack/src/post-message-identity.ts
|
|
122
|
+
function normalizeSlackScopeList$1(value) {
|
|
123
|
+
if (!Array.isArray(value)) return [];
|
|
124
|
+
return value.flatMap((scope) => {
|
|
125
|
+
const normalized = normalizeOptionalString(scope);
|
|
126
|
+
return normalized ? [normalized] : [];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function getSlackWebApiErrorData$1(err) {
|
|
130
|
+
if (!(err instanceof Error)) return;
|
|
131
|
+
const data = err.data;
|
|
132
|
+
return data && typeof data === "object" ? data : void 0;
|
|
133
|
+
}
|
|
134
|
+
function isSlackCustomizeScopeError(err) {
|
|
135
|
+
const data = getSlackWebApiErrorData$1(err);
|
|
136
|
+
if (normalizeLowercaseStringOrEmpty(normalizeOptionalString(data?.error)) !== "missing_scope") return false;
|
|
137
|
+
if (normalizeLowercaseStringOrEmpty(normalizeOptionalString(data?.needed)).includes("chat:write.customize")) return true;
|
|
138
|
+
return [...normalizeSlackScopeList$1(data?.response_metadata?.scopes), ...normalizeSlackScopeList$1(data?.response_metadata?.acceptedScopes)].map((scope) => normalizeLowercaseStringOrEmpty(scope)).includes("chat:write.customize");
|
|
139
|
+
}
|
|
140
|
+
function isSlackCustomIdentityRejectedError(err) {
|
|
141
|
+
if (isSlackCustomizeScopeError(err)) return true;
|
|
142
|
+
const code = normalizeLowercaseStringOrEmpty(normalizeOptionalString(getSlackWebApiErrorData$1(err)?.error));
|
|
143
|
+
return code === "invalid_arguments" || code === "invalid_arg_name";
|
|
144
|
+
}
|
|
145
|
+
function hasCustomIdentity$1(identity) {
|
|
146
|
+
return Boolean(identity?.username || identity?.iconUrl || identity?.iconEmoji);
|
|
147
|
+
}
|
|
148
|
+
/** Post with the requested identity, degrading only on Slack identity-specific errors. */
|
|
149
|
+
async function postSlackMessageWithIdentityFallback(params) {
|
|
150
|
+
const { basePayload, identity, post } = params;
|
|
151
|
+
try {
|
|
152
|
+
if (identity?.iconUrl) return await post({
|
|
153
|
+
...basePayload,
|
|
154
|
+
...identity.username ? { username: identity.username } : {},
|
|
155
|
+
icon_url: identity.iconUrl
|
|
156
|
+
}, identity);
|
|
157
|
+
if (identity?.iconEmoji) return await post({
|
|
158
|
+
...basePayload,
|
|
159
|
+
...identity.username ? { username: identity.username } : {},
|
|
160
|
+
icon_emoji: identity.iconEmoji
|
|
161
|
+
}, identity);
|
|
162
|
+
return await post({
|
|
163
|
+
...basePayload,
|
|
164
|
+
...identity?.username ? { username: identity.username } : {}
|
|
165
|
+
}, identity);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (!identity || !hasCustomIdentity$1(identity) || !isSlackCustomIdentityRejectedError(err)) throw err;
|
|
168
|
+
if (!isSlackCustomizeScopeError(err) && identity.username && (identity.iconUrl || identity.iconEmoji)) {
|
|
169
|
+
logVerbose("slack send: custom icon rejected, retrying with username only");
|
|
170
|
+
try {
|
|
171
|
+
return await post({
|
|
172
|
+
...basePayload,
|
|
173
|
+
username: identity.username
|
|
174
|
+
}, { username: identity.username });
|
|
175
|
+
} catch (retryError) {
|
|
176
|
+
if (!isSlackCustomIdentityRejectedError(retryError)) throw retryError;
|
|
74
177
|
}
|
|
75
|
-
default: break;
|
|
76
178
|
}
|
|
179
|
+
logVerbose("slack send: custom identity rejected, retrying without custom identity");
|
|
180
|
+
return post(basePayload);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region extensions/slack/src/post-message-payload.ts
|
|
185
|
+
function buildSlackUnfurlPayload(options) {
|
|
186
|
+
return {
|
|
187
|
+
unfurl_links: options?.unfurlLinks ?? false,
|
|
188
|
+
...typeof options?.unfurlMedia === "boolean" ? { unfurl_media: options.unfurlMedia } : {}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function buildSlackPostMessagePayload(params) {
|
|
192
|
+
const threadPayload = params.replyBroadcast && params.threadTs ? {
|
|
193
|
+
thread_ts: params.threadTs,
|
|
194
|
+
reply_broadcast: true
|
|
195
|
+
} : params.threadTs ? { thread_ts: params.threadTs } : {};
|
|
196
|
+
const unfurlPayload = buildSlackUnfurlPayload(params.unfurl);
|
|
197
|
+
if (params.blocks?.length) return {
|
|
198
|
+
channel: params.channelId,
|
|
199
|
+
text: params.text,
|
|
200
|
+
blocks: params.blocks,
|
|
201
|
+
...params.metadata ? { metadata: params.metadata } : {},
|
|
202
|
+
...typeof params.mrkdwn === "boolean" ? { mrkdwn: params.mrkdwn } : {},
|
|
203
|
+
...threadPayload,
|
|
204
|
+
...unfurlPayload
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
channel: params.channelId,
|
|
208
|
+
text: params.text,
|
|
209
|
+
...params.metadata ? { metadata: params.metadata } : {},
|
|
210
|
+
...typeof params.mrkdwn === "boolean" ? { mrkdwn: params.mrkdwn } : {},
|
|
211
|
+
...threadPayload,
|
|
212
|
+
...unfurlPayload
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region extensions/slack/src/client-delivery.ts
|
|
217
|
+
const SLACK_COMMERCIAL_API_HOSTNAME = "slack.com";
|
|
218
|
+
const SLACK_COMMERCIAL_UPLOAD_HOSTNAME = "files.slack.com";
|
|
219
|
+
const SLACK_GOV_API_HOSTNAME = "slack-gov.com";
|
|
220
|
+
const SLACK_GOV_UPLOAD_HOSTNAME = "files.slack-gov.com";
|
|
221
|
+
const SLACK_COMMERCIAL_UPLOAD_SSRF_POLICY = {
|
|
222
|
+
hostnameAllowlist: [SLACK_COMMERCIAL_UPLOAD_HOSTNAME],
|
|
223
|
+
allowRfc2544BenchmarkRange: true
|
|
224
|
+
};
|
|
225
|
+
const SLACK_GOV_UPLOAD_SSRF_POLICY = {
|
|
226
|
+
hostnameAllowlist: [SLACK_GOV_UPLOAD_HOSTNAME],
|
|
227
|
+
allowRfc2544BenchmarkRange: true
|
|
228
|
+
};
|
|
229
|
+
const SLACK_UPLOAD_POST_TIMEOUT_MS = 12e4;
|
|
230
|
+
const SLACK_DNS_RETRY_CODES = /* @__PURE__ */ new Set([
|
|
231
|
+
"EAI_AGAIN",
|
|
232
|
+
"ENOTFOUND",
|
|
233
|
+
"UND_ERR_DNS_RESOLVE_FAILED"
|
|
234
|
+
]);
|
|
235
|
+
const SLACK_DNS_RETRY_ATTEMPTS = 2;
|
|
236
|
+
const SLACK_DNS_RETRY_BASE_DELAY_MS = 250;
|
|
237
|
+
function readSlackRequestErrorCode(value) {
|
|
238
|
+
if (!value || typeof value !== "object") return;
|
|
239
|
+
const code = value.code;
|
|
240
|
+
return typeof code === "string" ? code.toUpperCase() : void 0;
|
|
241
|
+
}
|
|
242
|
+
function readSlackRequestErrorMessage$1(value) {
|
|
243
|
+
if (value instanceof Error) return value.message;
|
|
244
|
+
return typeof value === "string" ? value : "";
|
|
245
|
+
}
|
|
246
|
+
function hasSlackDnsRequestSignal(err) {
|
|
247
|
+
let current = err;
|
|
248
|
+
const seen = /* @__PURE__ */ new Set();
|
|
249
|
+
for (let depth = 0; current && typeof current === "object" && depth < 6; depth += 1) {
|
|
250
|
+
if (seen.has(current)) return false;
|
|
251
|
+
seen.add(current);
|
|
252
|
+
const code = readSlackRequestErrorCode(current);
|
|
253
|
+
if (code && SLACK_DNS_RETRY_CODES.has(code)) return true;
|
|
254
|
+
const message = readSlackRequestErrorMessage$1(current);
|
|
255
|
+
if (/\b(EAI_AGAIN|ENOTFOUND|UND_ERR_DNS_RESOLVE_FAILED)\b/i.test(message)) return true;
|
|
256
|
+
current = current.original ?? current.cause;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
function delaySlackDnsRetry(attempt) {
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
setTimeout(resolve, SLACK_DNS_RETRY_BASE_DELAY_MS * Math.max(1, attempt));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function resolveSlackUploadTimeoutLogUrl(url) {
|
|
266
|
+
try {
|
|
267
|
+
return new URL(url).origin;
|
|
268
|
+
} catch {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function buildSlackUploadFailureCause(error) {
|
|
273
|
+
const httpStatus = error instanceof Error ? /^Failed to upload file: HTTP (\d{3})$/u.exec(error.message)?.[1] : void 0;
|
|
274
|
+
const cause = /* @__PURE__ */ new Error(httpStatus ? `Slack external upload returned HTTP ${httpStatus}` : "Slack external upload transfer failed");
|
|
275
|
+
cause.name = readErrorName(error) || cause.name;
|
|
276
|
+
const code = extractErrorCode(error) ?? (httpStatus ? `HTTP_${httpStatus}` : void 0);
|
|
277
|
+
if (code) cause.code = code;
|
|
278
|
+
return cause;
|
|
279
|
+
}
|
|
280
|
+
function parseSlackUploadHttpUrl(value, label) {
|
|
281
|
+
try {
|
|
282
|
+
const parsed = new URL(value);
|
|
283
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed;
|
|
284
|
+
} catch {}
|
|
285
|
+
throw new Error(`${label} must use a valid HTTP or HTTPS URL`);
|
|
286
|
+
}
|
|
287
|
+
function normalizeSlackHostname(hostname) {
|
|
288
|
+
return hostname.trim().toLowerCase().replace(/\.$/, "");
|
|
289
|
+
}
|
|
290
|
+
function resolveSlackOwnedUploadPolicy(url) {
|
|
291
|
+
if (url.protocol !== "https:") return;
|
|
292
|
+
switch (normalizeSlackHostname(url.hostname)) {
|
|
293
|
+
case SLACK_COMMERCIAL_UPLOAD_HOSTNAME: return SLACK_COMMERCIAL_UPLOAD_SSRF_POLICY;
|
|
294
|
+
case SLACK_GOV_UPLOAD_HOSTNAME: return SLACK_GOV_UPLOAD_SSRF_POLICY;
|
|
295
|
+
default: return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function resolveOfficialSlackApiUploadPolicy(url) {
|
|
299
|
+
if (url.protocol !== "https:" || url.port) return;
|
|
300
|
+
switch (normalizeSlackHostname(url.hostname)) {
|
|
301
|
+
case SLACK_COMMERCIAL_API_HOSTNAME: return SLACK_COMMERCIAL_UPLOAD_SSRF_POLICY;
|
|
302
|
+
case SLACK_GOV_API_HOSTNAME: return SLACK_GOV_UPLOAD_SSRF_POLICY;
|
|
303
|
+
default: return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function normalizeSlackOrigin(url) {
|
|
307
|
+
const port = url.port ? `:${url.port}` : "";
|
|
308
|
+
return `${url.protocol}//${normalizeSlackHostname(url.hostname)}${port}`;
|
|
309
|
+
}
|
|
310
|
+
function resolveSlackUploadTransportPolicy(params) {
|
|
311
|
+
if (!params.slackApiUrl) return {
|
|
312
|
+
requireHttps: true,
|
|
313
|
+
policy: SLACK_COMMERCIAL_UPLOAD_SSRF_POLICY
|
|
314
|
+
};
|
|
315
|
+
const apiUrl = parseSlackUploadHttpUrl(params.slackApiUrl, "Configured Slack API URL");
|
|
316
|
+
const officialApiPolicy = resolveOfficialSlackApiUploadPolicy(apiUrl);
|
|
317
|
+
if (officialApiPolicy) return {
|
|
318
|
+
requireHttps: true,
|
|
319
|
+
policy: officialApiPolicy
|
|
320
|
+
};
|
|
321
|
+
const uploadUrl = parseSlackUploadHttpUrl(params.uploadUrl, "Slack external upload URL");
|
|
322
|
+
const slackOwnedUploadPolicy = resolveSlackOwnedUploadPolicy(uploadUrl);
|
|
323
|
+
if (slackOwnedUploadPolicy) return {
|
|
324
|
+
requireHttps: true,
|
|
325
|
+
policy: slackOwnedUploadPolicy
|
|
326
|
+
};
|
|
327
|
+
if (normalizeSlackOrigin(uploadUrl) !== normalizeSlackOrigin(apiUrl)) throw new Error("Slack external upload URL must match the configured Slack API origin");
|
|
328
|
+
return {
|
|
329
|
+
requireHttps: apiUrl.protocol === "https:",
|
|
330
|
+
policy: {
|
|
331
|
+
hostnameAllowlist: [uploadUrl.hostname],
|
|
332
|
+
allowedOrigins: [uploadUrl.origin],
|
|
333
|
+
allowRfc2544BenchmarkRange: true
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
async function withSlackDnsRequestRetry(operation, fn) {
|
|
338
|
+
for (const attempt of Array.from({ length: 3 }, (_, index) => index)) try {
|
|
339
|
+
return await fn();
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (attempt >= SLACK_DNS_RETRY_ATTEMPTS || !hasSlackDnsRequestSignal(err)) throw err;
|
|
342
|
+
logVerbose(`slack send: retrying ${operation} after transient DNS request error (${attempt + 1}/${SLACK_DNS_RETRY_ATTEMPTS})`);
|
|
343
|
+
await delaySlackDnsRetry(attempt + 1);
|
|
77
344
|
}
|
|
78
|
-
|
|
345
|
+
throw new Error("unreachable Slack DNS retry loop exit");
|
|
346
|
+
}
|
|
347
|
+
async function postSlackMessageBestEffort(params) {
|
|
348
|
+
const basePayload = buildSlackPostMessagePayload(params);
|
|
349
|
+
const postChatMessage = params.client.chat.postMessage.bind(params.client.chat);
|
|
350
|
+
const post = async (payload, identity) => ({
|
|
351
|
+
response: await withSlackDnsRequestRetry("chat.postMessage", () => postChatMessage(payload)),
|
|
352
|
+
identity
|
|
353
|
+
});
|
|
354
|
+
return await postSlackMessageWithIdentityFallback({
|
|
355
|
+
basePayload,
|
|
356
|
+
identity: params.identity,
|
|
357
|
+
post
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
async function uploadSlackFile(params) {
|
|
361
|
+
const { buffer, contentType, fileName } = await loadOutboundMediaFromUrl(params.mediaUrl, {
|
|
362
|
+
maxBytes: params.maxBytes,
|
|
363
|
+
mediaAccess: params.mediaAccess,
|
|
364
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
365
|
+
mediaReadFile: params.mediaReadFile
|
|
366
|
+
});
|
|
367
|
+
const uploadFileName = params.uploadFileName ?? fileName ?? "upload";
|
|
368
|
+
const uploadTitle = params.uploadTitle ?? uploadFileName;
|
|
369
|
+
const uploadUrlResp = await withSlackDnsRequestRetry("files.getUploadURLExternal", () => params.client.files.getUploadURLExternal({
|
|
370
|
+
filename: uploadFileName,
|
|
371
|
+
length: buffer.length
|
|
372
|
+
}));
|
|
373
|
+
if (!uploadUrlResp.ok || !uploadUrlResp.upload_url || !uploadUrlResp.file_id) throw new Error(`Failed to get upload URL: ${uploadUrlResp.error ?? "unknown error"}`);
|
|
374
|
+
const uploadFileId = uploadUrlResp.file_id;
|
|
375
|
+
const uploadTransport = resolveSlackUploadTransportPolicy({
|
|
376
|
+
uploadUrl: uploadUrlResp.upload_url,
|
|
377
|
+
slackApiUrl: params.client.slackApiUrl
|
|
378
|
+
});
|
|
379
|
+
const { signal: uploadTimeoutSignal, cleanup: cleanupUploadTimeout } = buildTimeoutAbortSignal({
|
|
380
|
+
timeoutMs: SLACK_UPLOAD_POST_TIMEOUT_MS,
|
|
381
|
+
operation: "slack-upload-file",
|
|
382
|
+
url: resolveSlackUploadTimeoutLogUrl(uploadUrlResp.upload_url)
|
|
383
|
+
});
|
|
384
|
+
try {
|
|
385
|
+
const { response: uploadResp, release } = await fetchWithSsrFGuard(withTrustedEnvProxyGuardedFetchMode({
|
|
386
|
+
url: uploadUrlResp.upload_url,
|
|
387
|
+
init: {
|
|
388
|
+
method: "POST",
|
|
389
|
+
...contentType ? { headers: { "Content-Type": contentType } } : {},
|
|
390
|
+
body: new Uint8Array(buffer)
|
|
391
|
+
},
|
|
392
|
+
signal: uploadTimeoutSignal,
|
|
393
|
+
requireHttps: uploadTransport.requireHttps,
|
|
394
|
+
policy: uploadTransport.policy,
|
|
395
|
+
capture: false,
|
|
396
|
+
auditContext: params.auditContext ?? "slack-upload-file"
|
|
397
|
+
}));
|
|
398
|
+
try {
|
|
399
|
+
if (uploadResp.status !== 200) throw new Error(`Failed to upload file: HTTP ${uploadResp.status}`);
|
|
400
|
+
} finally {
|
|
401
|
+
await uploadResp.body?.cancel().catch(() => void 0);
|
|
402
|
+
await release();
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
throw new PlatformMessageNotDispatchedError(`Slack external upload ${uploadTimeoutSignal?.aborted ? "timed out" : "failed"} before completion dispatch`, { cause: buildSlackUploadFailureCause(error) });
|
|
406
|
+
} finally {
|
|
407
|
+
cleanupUploadTimeout();
|
|
408
|
+
}
|
|
409
|
+
await params.onPlatformSendDispatch?.();
|
|
410
|
+
const completionClient = params.completionClient ?? params.client;
|
|
411
|
+
const completeResp = await withSlackDnsRequestRetry("files.completeUploadExternal", () => completionClient.files.completeUploadExternal({
|
|
412
|
+
files: [{
|
|
413
|
+
id: uploadFileId,
|
|
414
|
+
title: uploadTitle
|
|
415
|
+
}],
|
|
416
|
+
channel_id: params.channelId,
|
|
417
|
+
...params.caption ? { initial_comment: params.caption } : {},
|
|
418
|
+
...params.threadTs ? { thread_ts: params.threadTs } : {}
|
|
419
|
+
}));
|
|
420
|
+
if (!completeResp.ok) throw new Error(`Failed to complete upload: ${completeResp.error ?? "unknown error"}`);
|
|
421
|
+
return uploadFileId;
|
|
79
422
|
}
|
|
80
423
|
//#endregion
|
|
81
424
|
//#region extensions/slack/src/sent-thread-cache.ts
|
|
@@ -97,45 +440,30 @@ const threadParticipation = createPersistentDedupeCache({
|
|
|
97
440
|
}
|
|
98
441
|
}
|
|
99
442
|
});
|
|
100
|
-
function makeKey(accountId, channelId, threadTs) {
|
|
101
|
-
return `${accountId}:${channelId}:${threadTs}`;
|
|
443
|
+
function makeKey(accountId, channelId, threadTs, teamId) {
|
|
444
|
+
return `${accountId}:${teamId ? `${teamId}:` : ""}${channelId}:${threadTs}`;
|
|
102
445
|
}
|
|
103
446
|
function recordSlackThreadParticipation(accountId, channelId, threadTs, opts) {
|
|
104
447
|
if (!accountId || !channelId || !threadTs) return;
|
|
105
|
-
threadParticipation.register(makeKey(accountId, channelId, threadTs), {
|
|
448
|
+
threadParticipation.register(makeKey(accountId, channelId, threadTs, opts?.teamId), {
|
|
106
449
|
...opts?.agentId ? { agentId: opts.agentId } : {},
|
|
107
450
|
repliedAt: Date.now()
|
|
108
451
|
});
|
|
109
452
|
}
|
|
110
|
-
function hasSlackThreadParticipation(accountId, channelId, threadTs) {
|
|
453
|
+
function hasSlackThreadParticipation(accountId, channelId, threadTs, teamId) {
|
|
111
454
|
if (!accountId || !channelId || !threadTs) return false;
|
|
112
|
-
return threadParticipation.peek(makeKey(accountId, channelId, threadTs));
|
|
455
|
+
return threadParticipation.peek(makeKey(accountId, channelId, threadTs, teamId));
|
|
113
456
|
}
|
|
114
457
|
async function hasSlackThreadParticipationWithPersistence(params) {
|
|
115
458
|
if (!params.accountId || !params.channelId || !params.threadTs) return false;
|
|
116
|
-
return await threadParticipation.lookup(makeKey(params.accountId, params.channelId, params.threadTs));
|
|
459
|
+
return await threadParticipation.lookup(makeKey(params.accountId, params.channelId, params.threadTs, params.teamId));
|
|
117
460
|
}
|
|
118
461
|
function clearSlackThreadParticipationCache() {
|
|
119
462
|
threadParticipation.clearForTest();
|
|
120
463
|
}
|
|
121
464
|
//#endregion
|
|
122
465
|
//#region extensions/slack/src/send.ts
|
|
123
|
-
const SLACK_UPLOAD_SSRF_POLICY = {
|
|
124
|
-
allowedHostnames: [
|
|
125
|
-
"*.slack.com",
|
|
126
|
-
"*.slack-edge.com",
|
|
127
|
-
"*.slack-files.com"
|
|
128
|
-
],
|
|
129
|
-
allowRfc2544BenchmarkRange: true
|
|
130
|
-
};
|
|
131
466
|
const SLACK_DM_CHANNEL_CACHE_MAX = 1024;
|
|
132
|
-
const SLACK_DNS_RETRY_CODES = /* @__PURE__ */ new Set([
|
|
133
|
-
"EAI_AGAIN",
|
|
134
|
-
"ENOTFOUND",
|
|
135
|
-
"UND_ERR_DNS_RESOLVE_FAILED"
|
|
136
|
-
]);
|
|
137
|
-
const SLACK_DNS_RETRY_ATTEMPTS = 2;
|
|
138
|
-
const SLACK_DNS_RETRY_BASE_DELAY_MS = 250;
|
|
139
467
|
const SLACK_DELIVERY_METADATA_EVENT = "openclaw_delivery";
|
|
140
468
|
const SLACK_DELIVERY_METADATA_KEY = "openclaw_delivery_id";
|
|
141
469
|
const SLACK_DELIVERY_METADATA_PART_INDEX_KEY = "openclaw_delivery_part_index";
|
|
@@ -145,7 +473,8 @@ const SLACK_RECONCILE_LOOKBACK_MS = 3e4;
|
|
|
145
473
|
const SLACK_RECONCILE_CLOCK_SKEW_MS = 5 * 6e4;
|
|
146
474
|
const SLACK_RECONCILE_LIMIT = 100;
|
|
147
475
|
const SLACK_RECONCILE_MAX_PAGES = 10;
|
|
148
|
-
const
|
|
476
|
+
const SLACK_ENTERPRISE_LISTENER_QUEUE_CREDENTIAL = "listener-scoped-enterprise";
|
|
477
|
+
const slackDmChannelCaches = /* @__PURE__ */ new WeakMap();
|
|
149
478
|
const slackSendQueue = new KeyedAsyncQueue();
|
|
150
479
|
const slackDefaultSendIdentities = /* @__PURE__ */ new Map();
|
|
151
480
|
function hasCustomIdentity(identity) {
|
|
@@ -176,34 +505,6 @@ function getSlackDefaultSendIdentity(accountId) {
|
|
|
176
505
|
function resolveSlackSendIdentity(params) {
|
|
177
506
|
return normalizeSlackSendIdentity(params.explicit) ?? getSlackDefaultSendIdentity(params.accountId);
|
|
178
507
|
}
|
|
179
|
-
function buildSlackUnfurlPayload(options) {
|
|
180
|
-
return {
|
|
181
|
-
unfurl_links: options?.unfurlLinks ?? false,
|
|
182
|
-
...typeof options?.unfurlMedia === "boolean" ? { unfurl_media: options.unfurlMedia } : {}
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
function buildSlackPostMessagePayload(params) {
|
|
186
|
-
const threadPayload = params.replyBroadcast && params.threadTs ? {
|
|
187
|
-
thread_ts: params.threadTs,
|
|
188
|
-
reply_broadcast: true
|
|
189
|
-
} : params.threadTs ? { thread_ts: params.threadTs } : {};
|
|
190
|
-
const unfurlPayload = buildSlackUnfurlPayload(params.unfurl);
|
|
191
|
-
if (params.blocks?.length) return {
|
|
192
|
-
channel: params.channelId,
|
|
193
|
-
text: params.text,
|
|
194
|
-
blocks: params.blocks,
|
|
195
|
-
...params.metadata ? { metadata: params.metadata } : {},
|
|
196
|
-
...threadPayload,
|
|
197
|
-
...unfurlPayload
|
|
198
|
-
};
|
|
199
|
-
return {
|
|
200
|
-
channel: params.channelId,
|
|
201
|
-
text: params.text,
|
|
202
|
-
...params.metadata ? { metadata: params.metadata } : {},
|
|
203
|
-
...threadPayload,
|
|
204
|
-
...unfurlPayload
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
508
|
function normalizeSlackScopeList(value) {
|
|
208
509
|
if (!Array.isArray(value)) return [];
|
|
209
510
|
return value.flatMap((scope) => {
|
|
@@ -236,96 +537,10 @@ function enrichSlackWebApiError(err) {
|
|
|
236
537
|
if (!message || !(err instanceof Error) || message === err.message) return err;
|
|
237
538
|
return new Error(message);
|
|
238
539
|
}
|
|
239
|
-
function readSlackRequestErrorCode(value) {
|
|
240
|
-
if (!value || typeof value !== "object") return;
|
|
241
|
-
const code = value.code;
|
|
242
|
-
return typeof code === "string" ? code.toUpperCase() : void 0;
|
|
243
|
-
}
|
|
244
540
|
function readSlackRequestErrorMessage(value) {
|
|
245
541
|
if (value instanceof Error) return value.message;
|
|
246
542
|
return typeof value === "string" ? value : "";
|
|
247
543
|
}
|
|
248
|
-
function hasSlackDnsRequestSignal(err) {
|
|
249
|
-
let current = err;
|
|
250
|
-
const seen = /* @__PURE__ */ new Set();
|
|
251
|
-
for (let depth = 0; current && typeof current === "object" && depth < 6; depth += 1) {
|
|
252
|
-
if (seen.has(current)) return false;
|
|
253
|
-
seen.add(current);
|
|
254
|
-
const code = readSlackRequestErrorCode(current);
|
|
255
|
-
if (code && SLACK_DNS_RETRY_CODES.has(code)) return true;
|
|
256
|
-
const message = readSlackRequestErrorMessage(current);
|
|
257
|
-
if (/\b(EAI_AGAIN|ENOTFOUND|UND_ERR_DNS_RESOLVE_FAILED)\b/i.test(message)) return true;
|
|
258
|
-
current = current.original ?? current.cause;
|
|
259
|
-
}
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
function delaySlackDnsRetry(attempt) {
|
|
263
|
-
return new Promise((resolve) => {
|
|
264
|
-
setTimeout(resolve, SLACK_DNS_RETRY_BASE_DELAY_MS * Math.max(1, attempt));
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
async function withSlackDnsRequestRetry(operation, fn) {
|
|
268
|
-
for (const attempt of Array.from({ length: 3 }, (_, index) => index)) try {
|
|
269
|
-
return await fn();
|
|
270
|
-
} catch (err) {
|
|
271
|
-
if (attempt >= SLACK_DNS_RETRY_ATTEMPTS || !hasSlackDnsRequestSignal(err)) throw err;
|
|
272
|
-
logVerbose(`slack send: retrying ${operation} after transient DNS request error (${attempt + 1}/${SLACK_DNS_RETRY_ATTEMPTS})`);
|
|
273
|
-
await delaySlackDnsRetry(attempt + 1);
|
|
274
|
-
}
|
|
275
|
-
throw new Error("unreachable Slack DNS retry loop exit");
|
|
276
|
-
}
|
|
277
|
-
function isSlackCustomizeScopeError(err) {
|
|
278
|
-
const data = getSlackWebApiErrorData(err);
|
|
279
|
-
if (normalizeLowercaseStringOrEmpty(normalizeOptionalString(data?.error)) !== "missing_scope") return false;
|
|
280
|
-
if (normalizeLowercaseStringOrEmpty(normalizeOptionalString(data?.needed))?.includes("chat:write.customize")) return true;
|
|
281
|
-
return [...normalizeSlackScopeList(data?.response_metadata?.scopes), ...normalizeSlackScopeList(data?.response_metadata?.acceptedScopes)].map((scope) => normalizeLowercaseStringOrEmpty(scope)).includes("chat:write.customize");
|
|
282
|
-
}
|
|
283
|
-
function isSlackCustomIdentityRejectedError(err) {
|
|
284
|
-
if (isSlackCustomizeScopeError(err)) return true;
|
|
285
|
-
const code = normalizeLowercaseStringOrEmpty(normalizeOptionalString(getSlackWebApiErrorData(err)?.error));
|
|
286
|
-
return code === "invalid_arguments" || code === "invalid_arg_name";
|
|
287
|
-
}
|
|
288
|
-
async function postSlackMessageBestEffort(params) {
|
|
289
|
-
const basePayload = buildSlackPostMessagePayload(params);
|
|
290
|
-
const postChatMessage = params.client.chat.postMessage.bind(params.client.chat);
|
|
291
|
-
const post = async (payload, identity) => ({
|
|
292
|
-
response: await withSlackDnsRequestRetry("chat.postMessage", () => postChatMessage(payload)),
|
|
293
|
-
identity
|
|
294
|
-
});
|
|
295
|
-
try {
|
|
296
|
-
const identity = params.identity;
|
|
297
|
-
if (identity?.iconUrl) return await post({
|
|
298
|
-
...basePayload,
|
|
299
|
-
...identity.username ? { username: identity.username } : {},
|
|
300
|
-
icon_url: identity.iconUrl
|
|
301
|
-
}, identity);
|
|
302
|
-
if (identity?.iconEmoji) return await post({
|
|
303
|
-
...basePayload,
|
|
304
|
-
...identity.username ? { username: identity.username } : {},
|
|
305
|
-
icon_emoji: identity.iconEmoji
|
|
306
|
-
}, identity);
|
|
307
|
-
return await post({
|
|
308
|
-
...basePayload,
|
|
309
|
-
...identity?.username ? { username: identity.username } : {}
|
|
310
|
-
}, identity);
|
|
311
|
-
} catch (err) {
|
|
312
|
-
const identity = params.identity;
|
|
313
|
-
if (!identity || !hasCustomIdentity(identity) || !isSlackCustomIdentityRejectedError(err)) throw err;
|
|
314
|
-
if (!isSlackCustomizeScopeError(err) && identity.username && (identity.iconUrl || identity.iconEmoji)) {
|
|
315
|
-
logVerbose("slack send: custom icon rejected, retrying with username only");
|
|
316
|
-
try {
|
|
317
|
-
return await post({
|
|
318
|
-
...basePayload,
|
|
319
|
-
username: identity.username
|
|
320
|
-
}, { username: identity.username });
|
|
321
|
-
} catch (retryError) {
|
|
322
|
-
if (!isSlackCustomIdentityRejectedError(retryError)) throw retryError;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
logVerbose("slack send: custom identity rejected, retrying without custom identity");
|
|
326
|
-
return post(basePayload);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
544
|
function resolvePostedMessageThreadTs(response) {
|
|
330
545
|
const threadTs = response.message?.thread_ts;
|
|
331
546
|
return typeof threadTs === "string" ? normalizeSlackThreadTsCandidate(threadTs) : void 0;
|
|
@@ -359,12 +574,54 @@ function parseRecipient(raw) {
|
|
|
359
574
|
if (!target) throw new Error("Recipient is required for Slack sends");
|
|
360
575
|
return {
|
|
361
576
|
kind: target.kind,
|
|
362
|
-
id: target.id
|
|
577
|
+
id: canonicalizeSlackApiTargetId(target.kind, target.id, raw)
|
|
363
578
|
};
|
|
364
579
|
}
|
|
580
|
+
function parseEnterpriseEventRecipient(raw) {
|
|
581
|
+
const match = /^(?:channel:)?([CDG][A-Z0-9]+)$/i.exec(raw.trim());
|
|
582
|
+
if (!match?.[1]) throw new Error("unsupported_enterprise_slack_delivery_target");
|
|
583
|
+
return {
|
|
584
|
+
kind: "channel",
|
|
585
|
+
id: canonicalizeSlackApiTargetId("channel", match[1])
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function resolveEnterpriseEventScope(params) {
|
|
589
|
+
const scope = params.opts.enterpriseEventScope;
|
|
590
|
+
if (!scope) {
|
|
591
|
+
assertSlackDirectSendAllowed(params.account);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (params.account.config.enterpriseOrgInstall !== true) throw new Error("unexpected_enterprise_slack_listener_scope");
|
|
595
|
+
if (!scope.isEnterpriseInstall || !normalizeOptionalString(scope.apiAppId) || !normalizeOptionalString(scope.enterpriseId) || !/^T[A-Z0-9]+$/i.test(scope.teamId) || !scope.client || params.opts.client !== scope.client) throw new Error("invalid_enterprise_slack_listener_scope");
|
|
596
|
+
return scope;
|
|
597
|
+
}
|
|
598
|
+
function resolveSlackTextChunks(params) {
|
|
599
|
+
const text = params.preservePlainText ? params.text : params.text.trim();
|
|
600
|
+
const configuredLimit = params.textLimit ?? resolveTextChunkLimit(params.cfg, "slack", params.accountId, { fallbackLimit: 8e3 });
|
|
601
|
+
const chunkLimit = Math.min(configuredLimit, SLACK_TEXT_LIMIT);
|
|
602
|
+
if (params.preservePlainText) {
|
|
603
|
+
const chunks = [];
|
|
604
|
+
let remaining = text;
|
|
605
|
+
while (remaining) {
|
|
606
|
+
const chunk = sliceUtf16Safe(remaining, 0, chunkLimit) || Array.from(remaining)[0] || "";
|
|
607
|
+
chunks.push(chunk);
|
|
608
|
+
remaining = remaining.slice(chunk.length);
|
|
609
|
+
}
|
|
610
|
+
return chunks;
|
|
611
|
+
}
|
|
612
|
+
if (params.textIsSlackMrkdwn) return resolveTextChunksWithFallback(text, chunkSlackMrkdwnText(text, chunkLimit));
|
|
613
|
+
const tableMode = resolveMarkdownTableMode({
|
|
614
|
+
cfg: params.cfg,
|
|
615
|
+
channel: "slack",
|
|
616
|
+
...params.accountId ? { accountId: params.accountId } : {}
|
|
617
|
+
});
|
|
618
|
+
const chunkMode = resolveChunkMode(params.cfg, "slack", params.accountId);
|
|
619
|
+
return resolveTextChunksWithFallback(text, (chunkMode === "newline" ? chunkMarkdownTextWithMode(text, chunkLimit, chunkMode) : [text]).flatMap((markdown) => markdownToSlackMrkdwnChunks(markdown, chunkLimit, { tableMode })));
|
|
620
|
+
}
|
|
365
621
|
function createSlackSendQueueKey(params) {
|
|
366
|
-
const recipientKey = `${params.recipient.kind
|
|
367
|
-
|
|
622
|
+
const recipientKey = `${params.recipient.kind}:${params.recipient.id}`;
|
|
623
|
+
const workspaceScope = params.teamId ? `:${params.teamId}` : "";
|
|
624
|
+
return `${params.accountId}:${createSlackTokenCacheKey(params.token)}${workspaceScope}:${recipientKey}:${params.threadTs ?? ""}`;
|
|
368
625
|
}
|
|
369
626
|
async function runQueuedSlackSend(key, task) {
|
|
370
627
|
return await slackSendQueue.enqueue(key, task);
|
|
@@ -372,16 +629,23 @@ async function runQueuedSlackSend(key, task) {
|
|
|
372
629
|
function createSlackDmCacheKey(params) {
|
|
373
630
|
return `${params.accountId ?? "default"}:${createSlackTokenCacheKey(params.token)}:${params.recipientId}`;
|
|
374
631
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
632
|
+
function getSlackDmChannelCache(client) {
|
|
633
|
+
const existing = slackDmChannelCaches.get(client);
|
|
634
|
+
if (existing) return existing;
|
|
635
|
+
const cache = /* @__PURE__ */ new Map();
|
|
636
|
+
slackDmChannelCaches.set(client, cache);
|
|
637
|
+
return cache;
|
|
638
|
+
}
|
|
639
|
+
function setSlackDmChannelCache(cache, key, channelId) {
|
|
640
|
+
if (cache.has(key)) cache.delete(key);
|
|
641
|
+
else if (cache.size >= SLACK_DM_CHANNEL_CACHE_MAX) {
|
|
642
|
+
const oldest = cache.keys().next().value;
|
|
643
|
+
if (oldest) cache.delete(oldest);
|
|
380
644
|
}
|
|
381
|
-
|
|
645
|
+
cache.set(key, channelId);
|
|
382
646
|
}
|
|
383
647
|
function isSlackUserRecipient(recipient) {
|
|
384
|
-
return recipient.kind === "user"
|
|
648
|
+
return recipient.kind === "user";
|
|
385
649
|
}
|
|
386
650
|
function resolveDirectUserPostChannelId(params) {
|
|
387
651
|
if (!isSlackUserRecipient(params.recipient) || params.hasMedia || params.threadTs) return;
|
|
@@ -397,7 +661,8 @@ async function resolveChannelId(client, recipient, params) {
|
|
|
397
661
|
token: params.token,
|
|
398
662
|
recipientId: recipient.id
|
|
399
663
|
});
|
|
400
|
-
const
|
|
664
|
+
const cache = getSlackDmChannelCache(client);
|
|
665
|
+
const cachedChannelId = cache.get(cacheKey);
|
|
401
666
|
if (cachedChannelId) return {
|
|
402
667
|
channelId: cachedChannelId,
|
|
403
668
|
isDm: true,
|
|
@@ -405,7 +670,7 @@ async function resolveChannelId(client, recipient, params) {
|
|
|
405
670
|
};
|
|
406
671
|
const channelId = (await withSlackDnsRequestRetry("conversations.open", () => client.conversations.open({ users: recipient.id }))).channel?.id;
|
|
407
672
|
if (!channelId) throw new Error("Failed to open Slack DM channel");
|
|
408
|
-
setSlackDmChannelCache(cacheKey, channelId);
|
|
673
|
+
setSlackDmChannelCache(cache, cacheKey, channelId);
|
|
409
674
|
return {
|
|
410
675
|
channelId,
|
|
411
676
|
isDm: true,
|
|
@@ -435,10 +700,6 @@ function createSlackDeliveryMetadataSignature(params) {
|
|
|
435
700
|
params.partCount
|
|
436
701
|
])).digest("base64url");
|
|
437
702
|
}
|
|
438
|
-
function matchesSlackDeliveryMetadataSignature(actual, expected) {
|
|
439
|
-
if (typeof actual !== "string" || actual.length !== expected.length) return false;
|
|
440
|
-
return timingSafeEqual(Buffer.from(actual), Buffer.from(expected));
|
|
441
|
-
}
|
|
442
703
|
function withSlackDeliveryMetadata(metadata, params) {
|
|
443
704
|
const queueId = normalizeOptionalString(params.queueId);
|
|
444
705
|
const deliveryId = createSlackDeliveryMetadataId(queueId);
|
|
@@ -497,7 +758,8 @@ function findSlackConversationDeliveryParts(params) {
|
|
|
497
758
|
partIndex,
|
|
498
759
|
partCount
|
|
499
760
|
});
|
|
500
|
-
|
|
761
|
+
const actualSignature = marker[SLACK_DELIVERY_METADATA_SIGNATURE_KEY];
|
|
762
|
+
if (typeof actualSignature !== "string" || !safeEqualSecret(actualSignature, expectedSignature)) continue;
|
|
501
763
|
const messageId = typeof message.ts === "string" ? normalizeSlackThreadTsCandidate(message.ts) : void 0;
|
|
502
764
|
if (!messageId) continue;
|
|
503
765
|
const threadTs = typeof message.thread_ts === "string" ? normalizeSlackThreadTsCandidate(message.thread_ts) : void 0;
|
|
@@ -674,53 +936,24 @@ async function reconcileSlackUnknownSend(ctx, opts) {
|
|
|
674
936
|
};
|
|
675
937
|
}
|
|
676
938
|
}
|
|
677
|
-
async function uploadSlackFile(params) {
|
|
678
|
-
const { buffer, contentType, fileName } = await loadOutboundMediaFromUrl(params.mediaUrl, {
|
|
679
|
-
maxBytes: params.maxBytes,
|
|
680
|
-
mediaAccess: params.mediaAccess,
|
|
681
|
-
mediaLocalRoots: params.mediaLocalRoots,
|
|
682
|
-
mediaReadFile: params.mediaReadFile
|
|
683
|
-
});
|
|
684
|
-
const uploadFileName = params.uploadFileName ?? fileName ?? "upload";
|
|
685
|
-
const uploadTitle = params.uploadTitle ?? uploadFileName;
|
|
686
|
-
const uploadUrlResp = await withSlackDnsRequestRetry("files.getUploadURLExternal", () => params.client.files.getUploadURLExternal({
|
|
687
|
-
filename: uploadFileName,
|
|
688
|
-
length: buffer.length
|
|
689
|
-
}));
|
|
690
|
-
if (!uploadUrlResp.ok || !uploadUrlResp.upload_url || !uploadUrlResp.file_id) throw new Error(`Failed to get upload URL: ${uploadUrlResp.error ?? "unknown error"}`);
|
|
691
|
-
const uploadFileId = uploadUrlResp.file_id;
|
|
692
|
-
const uploadBody = new Uint8Array(buffer);
|
|
693
|
-
const { response: uploadResp, release } = await fetchWithSsrFGuard(withTrustedEnvProxyGuardedFetchMode({
|
|
694
|
-
url: uploadUrlResp.upload_url,
|
|
695
|
-
init: {
|
|
696
|
-
method: "POST",
|
|
697
|
-
...contentType ? { headers: { "Content-Type": contentType } } : {},
|
|
698
|
-
body: uploadBody
|
|
699
|
-
},
|
|
700
|
-
policy: SLACK_UPLOAD_SSRF_POLICY,
|
|
701
|
-
auditContext: "slack-upload-file"
|
|
702
|
-
}));
|
|
703
|
-
try {
|
|
704
|
-
if (!uploadResp.ok) throw new Error(`Failed to upload file: HTTP ${uploadResp.status}`);
|
|
705
|
-
} finally {
|
|
706
|
-
await release();
|
|
707
|
-
}
|
|
708
|
-
await params.onPlatformSendDispatch?.();
|
|
709
|
-
const completeResp = await withSlackDnsRequestRetry("files.completeUploadExternal", () => params.client.files.completeUploadExternal({
|
|
710
|
-
files: [{
|
|
711
|
-
id: uploadFileId,
|
|
712
|
-
title: uploadTitle
|
|
713
|
-
}],
|
|
714
|
-
channel_id: params.channelId,
|
|
715
|
-
...params.caption ? { initial_comment: params.caption } : {},
|
|
716
|
-
...params.threadTs ? { thread_ts: params.threadTs } : {}
|
|
717
|
-
}));
|
|
718
|
-
if (!completeResp.ok) throw new Error(`Failed to complete upload: ${completeResp.error ?? "unknown error"}`);
|
|
719
|
-
return uploadFileId;
|
|
720
|
-
}
|
|
721
939
|
async function sendMessageSlack(to, message, opts) {
|
|
722
|
-
const
|
|
723
|
-
|
|
940
|
+
const normalizedMessage = normalizeOptionalString(message) ?? "";
|
|
941
|
+
const trimmedMessage = opts.textIsSlackPlainText && normalizedMessage ? message : normalizedMessage;
|
|
942
|
+
const cfg = requireRuntimeConfig(opts.cfg, "Slack send");
|
|
943
|
+
const account = resolveSlackAccount({
|
|
944
|
+
cfg,
|
|
945
|
+
accountId: opts.accountId
|
|
946
|
+
});
|
|
947
|
+
const enterpriseEventScope = resolveEnterpriseEventScope({
|
|
948
|
+
account,
|
|
949
|
+
opts
|
|
950
|
+
});
|
|
951
|
+
const enterpriseDelivery = enterpriseEventScope ? Object.freeze({
|
|
952
|
+
client: enterpriseEventScope.client,
|
|
953
|
+
teamId: enterpriseEventScope.teamId,
|
|
954
|
+
...enterpriseEventScope.uploadCompletionClient ? { uploadCompletionClient: enterpriseEventScope.uploadCompletionClient } : {}
|
|
955
|
+
}) : void 0;
|
|
956
|
+
if (isSilentReplyText(normalizedMessage) && !opts.mediaUrl && !opts.blocks) {
|
|
724
957
|
logVerbose("slack send: suppressed NO_REPLY token before API call");
|
|
725
958
|
return {
|
|
726
959
|
messageId: "suppressed",
|
|
@@ -732,35 +965,38 @@ async function sendMessageSlack(to, message, opts) {
|
|
|
732
965
|
};
|
|
733
966
|
}
|
|
734
967
|
const blocks = opts.blocks == null ? void 0 : validateSlackBlocksArray(opts.blocks);
|
|
735
|
-
if (!
|
|
736
|
-
const
|
|
737
|
-
const account = resolveSlackAccount({
|
|
738
|
-
cfg,
|
|
739
|
-
accountId: opts.accountId
|
|
740
|
-
});
|
|
741
|
-
const token = resolveToken({
|
|
968
|
+
if (!normalizedMessage && !opts.mediaUrl && !blocks) throw new Error("Slack send requires text, blocks, or media");
|
|
969
|
+
const token = enterpriseDelivery ? SLACK_ENTERPRISE_LISTENER_QUEUE_CREDENTIAL : resolveToken({
|
|
742
970
|
explicit: opts.token,
|
|
743
971
|
accountId: account.accountId,
|
|
744
972
|
fallbackToken: account.botToken,
|
|
745
973
|
fallbackSource: account.botTokenSource
|
|
746
974
|
});
|
|
747
|
-
const recipient = parseRecipient(to);
|
|
748
|
-
const
|
|
975
|
+
const recipient = enterpriseDelivery ? parseEnterpriseEventRecipient(to) : parseRecipient(to);
|
|
976
|
+
const queueKey = createSlackSendQueueKey({
|
|
749
977
|
accountId: account.accountId,
|
|
750
978
|
token,
|
|
751
979
|
recipient,
|
|
752
|
-
threadTs: opts.threadTs
|
|
753
|
-
|
|
980
|
+
threadTs: opts.threadTs,
|
|
981
|
+
...enterpriseDelivery ? { teamId: enterpriseDelivery.teamId } : {}
|
|
982
|
+
});
|
|
983
|
+
const queuedOpts = enterpriseDelivery ? Object.freeze({
|
|
984
|
+
...opts,
|
|
985
|
+
client: enterpriseDelivery.client
|
|
986
|
+
}) : opts;
|
|
987
|
+
const result = await runQueuedSlackSend(queueKey, () => sendMessageSlackQueued({
|
|
754
988
|
trimmedMessage,
|
|
755
|
-
opts,
|
|
989
|
+
opts: queuedOpts,
|
|
756
990
|
cfg,
|
|
757
991
|
account,
|
|
758
992
|
token,
|
|
759
993
|
recipient,
|
|
760
|
-
blocks
|
|
994
|
+
blocks,
|
|
995
|
+
...enterpriseDelivery ? { enterpriseDelivery } : {}
|
|
761
996
|
}));
|
|
762
|
-
const threadTs = result.threadTs ?? normalizeSlackThreadTsCandidate(
|
|
763
|
-
if (threadTs && result.channelId && account.accountId) recordSlackThreadParticipation(account.accountId, result.channelId, threadTs);
|
|
997
|
+
const threadTs = result.threadTs ?? normalizeSlackThreadTsCandidate(queuedOpts.threadTs);
|
|
998
|
+
if (threadTs && result.channelId && account.accountId) if (enterpriseDelivery) recordSlackThreadParticipation(account.accountId, result.channelId, threadTs, { teamId: enterpriseDelivery.teamId });
|
|
999
|
+
else recordSlackThreadParticipation(account.accountId, result.channelId, threadTs);
|
|
764
1000
|
return result;
|
|
765
1001
|
}
|
|
766
1002
|
async function sendMessageSlackQueued(params) {
|
|
@@ -771,14 +1007,14 @@ async function sendMessageSlackQueued(params) {
|
|
|
771
1007
|
}
|
|
772
1008
|
}
|
|
773
1009
|
async function sendMessageSlackQueuedInner(params) {
|
|
774
|
-
const { opts, cfg, account, token, recipient, blocks, trimmedMessage } = params;
|
|
775
|
-
const client = opts.client ?? getSlackWriteClient(token);
|
|
776
|
-
const identity = resolveSlackSendIdentity({
|
|
1010
|
+
const { opts, cfg, account, token, recipient, blocks, trimmedMessage, enterpriseDelivery } = params;
|
|
1011
|
+
const client = enterpriseDelivery?.client ?? opts.client ?? getSlackWriteClient(token);
|
|
1012
|
+
const identity = enterpriseDelivery ? normalizeSlackSendIdentity(opts.identity) : resolveSlackSendIdentity({
|
|
777
1013
|
accountId: account.accountId,
|
|
778
1014
|
explicit: opts.identity
|
|
779
1015
|
});
|
|
780
1016
|
if (opts.replyBroadcast && opts.mediaUrl) throw new Error("Slack replyBroadcast is only supported for text or block thread replies.");
|
|
781
|
-
const unfurl = {
|
|
1017
|
+
const unfurl = enterpriseDelivery ? { unfurlMedia: account.config.unfurlMedia } : {
|
|
782
1018
|
unfurlLinks: account.config.unfurlLinks,
|
|
783
1019
|
unfurlMedia: account.config.unfurlMedia
|
|
784
1020
|
};
|
|
@@ -795,55 +1031,153 @@ async function sendMessageSlackQueuedInner(params) {
|
|
|
795
1031
|
await opts.onDeliveryResult?.(result);
|
|
796
1032
|
return result;
|
|
797
1033
|
};
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1034
|
+
let didDispatch = false;
|
|
1035
|
+
const dispatchOnce = async () => {
|
|
1036
|
+
if (didDispatch) return;
|
|
1037
|
+
didDispatch = true;
|
|
801
1038
|
await opts.onPlatformSendDispatch?.();
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
messageId,
|
|
818
|
-
channelId: deliveredChannelId,
|
|
819
|
-
threadTs: deliveredThreadTs,
|
|
820
|
-
receipt: createSlackSendReceipt({
|
|
821
|
-
platformMessageIds: [messageId],
|
|
822
|
-
channelId: deliveredChannelId,
|
|
823
|
-
kind: "card",
|
|
824
|
-
threadTs: deliveredThreadTs
|
|
825
|
-
})
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId, { fallbackLimit: SLACK_TEXT_LIMIT });
|
|
829
|
-
const chunkLimit = Math.min(textLimit, SLACK_TEXT_LIMIT);
|
|
830
|
-
const tableMode = resolveMarkdownTableMode({
|
|
831
|
-
cfg,
|
|
832
|
-
channel: "slack",
|
|
833
|
-
accountId: account.accountId
|
|
834
|
-
});
|
|
835
|
-
const chunkMode = resolveChunkMode(cfg, "slack", account.accountId);
|
|
836
|
-
const resolvedChunks = resolveTextChunksWithFallback(trimmedMessage, (chunkMode === "newline" ? chunkMarkdownTextWithMode(trimmedMessage, chunkLimit, chunkMode) : [trimmedMessage]).flatMap((markdown) => markdownToSlackMrkdwnChunks(markdown, chunkLimit, { tableMode })));
|
|
837
|
-
const mediaMaxBytes = typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb * 1024 * 1024 : void 0;
|
|
1039
|
+
};
|
|
1040
|
+
const explicitNativeDataFallbackBase = Object.hasOwn(opts, "nativeDataFallbackBaseText") ? opts.nativeDataFallbackBaseText?.trim() ?? "" : void 0;
|
|
1041
|
+
const authoredTextPlacement = opts.authoredTextPlacement ?? "outside-blocks";
|
|
1042
|
+
const nativeDataFallbackBase = blocks && authoredTextPlacement === "outside-blocks" ? explicitNativeDataFallbackBase ?? trimmedMessage : "";
|
|
1043
|
+
const hasNativeData = Boolean(blocks && hasSlackNativeDataBlock(blocks));
|
|
1044
|
+
const usesOrderedBlockAccessibility = Boolean(blocks && (hasNativeData || opts.authoredTextPlacement !== void 0));
|
|
1045
|
+
const orderedBlockDeliveryPlan = blocks && usesOrderedBlockAccessibility ? buildSlackNativeDataDeliveryPlan({
|
|
1046
|
+
baseText: nativeDataFallbackBase,
|
|
1047
|
+
blocks
|
|
1048
|
+
}) : void 0;
|
|
1049
|
+
const orderedBlockAccessibilityText = blocks && usesOrderedBlockAccessibility ? orderedBlockDeliveryPlan?.accessibilityText ?? (buildSlackNativeDataAccessibilityText(nativeDataFallbackBase, blocks) || "Slack could not render this Block Kit message.") : void 0;
|
|
1050
|
+
const completeBlockFallbackText = blocks ? buildSlackCompleteBlocksFallbackText(blocks) : "";
|
|
1051
|
+
const rawBlockAccessibilityText = trimmedMessage ? [trimmedMessage, ...completeBlockFallbackText.includes(trimmedMessage) ? [] : [completeBlockFallbackText]].join("\n\n") : completeBlockFallbackText;
|
|
1052
|
+
const blockAccessibilityText = blocks ? orderedBlockAccessibilityText ?? rawBlockAccessibilityText : void 0;
|
|
1053
|
+
let pendingBlockFallback = (hasNativeData || opts.authoredTextPlacement !== void 0) && orderedBlockDeliveryPlan?.skipOriginalBlocks ? orderedBlockDeliveryPlan : void 0;
|
|
838
1054
|
const sentMessageIds = [];
|
|
839
1055
|
let lastMessageId = "";
|
|
840
1056
|
let deliveredChannelId = channelId;
|
|
841
1057
|
let canonicalDeliveredThreadTs;
|
|
1058
|
+
let sendIdentity = identity;
|
|
1059
|
+
if (blocks && opts.mediaUrl) throw new Error("Slack send does not support blocks with mediaUrl");
|
|
1060
|
+
if (blocks) {
|
|
1061
|
+
if (pendingBlockFallback) logVerbose("slack send: native data accessibility exceeds hard limit, using text fallback");
|
|
1062
|
+
else {
|
|
1063
|
+
const accessibilityText = hasNativeData ? blockAccessibilityText ?? "Slack could not render this Block Kit message." : usesOrderedBlockAccessibility ? blockAccessibilityText ?? "Slack could not render this Block Kit message." : truncateSlackText(blockAccessibilityText ?? "", SLACK_TEXT_LIMIT);
|
|
1064
|
+
const initialBlockMetadata = withSlackDeliveryMetadata(opts.metadata, {
|
|
1065
|
+
queueId: opts.deliveryQueueId,
|
|
1066
|
+
channelId,
|
|
1067
|
+
threadTs: opts.threadTs,
|
|
1068
|
+
partIndex: 0,
|
|
1069
|
+
partCount: 1
|
|
1070
|
+
});
|
|
1071
|
+
await dispatchOnce();
|
|
1072
|
+
try {
|
|
1073
|
+
const { response } = await postSlackMessageBestEffort({
|
|
1074
|
+
client,
|
|
1075
|
+
channelId,
|
|
1076
|
+
text: accessibilityText,
|
|
1077
|
+
threadTs: opts.threadTs,
|
|
1078
|
+
replyBroadcast: opts.replyBroadcast,
|
|
1079
|
+
identity,
|
|
1080
|
+
blocks,
|
|
1081
|
+
metadata: initialBlockMetadata,
|
|
1082
|
+
...usesOrderedBlockAccessibility ? { mrkdwn: false } : {},
|
|
1083
|
+
unfurl
|
|
1084
|
+
});
|
|
1085
|
+
if (enterpriseDelivery && (!response.ok || !response.ts)) throw new Error(response.ok ? "Slack chat.postMessage returned no message timestamp" : `Slack chat.postMessage failed: ${response.error ?? "unknown error"}`);
|
|
1086
|
+
const messageId = response.ts ?? "unknown";
|
|
1087
|
+
deliveredChannelId = resolvePostedMessageChannelId(response, channelId);
|
|
1088
|
+
const deliveredThreadTs = resolvePostedMessageThreadTs(response) ?? normalizeSlackThreadTsCandidate(opts.threadTs);
|
|
1089
|
+
return await reportDelivery({
|
|
1090
|
+
messageId,
|
|
1091
|
+
channelId: deliveredChannelId,
|
|
1092
|
+
threadTs: deliveredThreadTs,
|
|
1093
|
+
receipt: createSlackSendReceipt({
|
|
1094
|
+
platformMessageIds: [messageId],
|
|
1095
|
+
channelId: deliveredChannelId,
|
|
1096
|
+
kind: "card",
|
|
1097
|
+
threadTs: deliveredThreadTs
|
|
1098
|
+
})
|
|
1099
|
+
});
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (!hasNativeData || !isSlackInvalidBlocksError(error)) throw error;
|
|
1102
|
+
logVerbose("slack send: native data rejected, delivering complete text fallback");
|
|
1103
|
+
pendingBlockFallback = orderedBlockDeliveryPlan;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (pendingBlockFallback) {
|
|
1107
|
+
const fallbackMessages = pendingBlockFallback.fallbackMessages;
|
|
1108
|
+
for (const [partIndex, fallback] of fallbackMessages.entries()) {
|
|
1109
|
+
const metadata = withSlackDeliveryMetadata(partIndex === 0 ? opts.metadata : void 0, {
|
|
1110
|
+
queueId: opts.deliveryQueueId,
|
|
1111
|
+
channelId,
|
|
1112
|
+
threadTs: opts.threadTs,
|
|
1113
|
+
partIndex,
|
|
1114
|
+
partCount: fallbackMessages.length
|
|
1115
|
+
});
|
|
1116
|
+
if (partIndex === 0) await dispatchOnce();
|
|
1117
|
+
const posted = await postSlackMessageBestEffort({
|
|
1118
|
+
client,
|
|
1119
|
+
channelId,
|
|
1120
|
+
text: fallback.text,
|
|
1121
|
+
threadTs: opts.threadTs,
|
|
1122
|
+
replyBroadcast: partIndex === 0 ? opts.replyBroadcast : void 0,
|
|
1123
|
+
identity: sendIdentity,
|
|
1124
|
+
...fallback.blocks ? { blocks: fallback.blocks } : {},
|
|
1125
|
+
metadata,
|
|
1126
|
+
mrkdwn: false,
|
|
1127
|
+
unfurl
|
|
1128
|
+
});
|
|
1129
|
+
const response = posted.response;
|
|
1130
|
+
if (enterpriseDelivery && (!response.ok || !response.ts)) throw new Error(response.ok ? "Slack chat.postMessage returned no message timestamp" : `Slack chat.postMessage failed: ${response.error ?? "unknown error"}`);
|
|
1131
|
+
sendIdentity = posted.identity;
|
|
1132
|
+
lastMessageId = response.ts ?? lastMessageId;
|
|
1133
|
+
deliveredChannelId = resolvePostedMessageChannelId(response, deliveredChannelId);
|
|
1134
|
+
canonicalDeliveredThreadTs ??= resolvePostedMessageThreadTs(response);
|
|
1135
|
+
if (!response.ts) continue;
|
|
1136
|
+
sentMessageIds.push(response.ts);
|
|
1137
|
+
const deliveredThreadTs = resolvePostedMessageThreadTs(response) ?? normalizeSlackThreadTsCandidate(opts.threadTs);
|
|
1138
|
+
await reportDelivery({
|
|
1139
|
+
messageId: response.ts,
|
|
1140
|
+
channelId: deliveredChannelId,
|
|
1141
|
+
threadTs: deliveredThreadTs,
|
|
1142
|
+
receipt: createSlackSendReceipt({
|
|
1143
|
+
platformMessageIds: [response.ts],
|
|
1144
|
+
channelId: deliveredChannelId,
|
|
1145
|
+
kind: fallback.blocks ? "card" : "text",
|
|
1146
|
+
threadTs: deliveredThreadTs
|
|
1147
|
+
})
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
const messageId = lastMessageId || "unknown";
|
|
1151
|
+
const deliveredThreadTs = canonicalDeliveredThreadTs ?? normalizeSlackThreadTsCandidate(opts.threadTs);
|
|
1152
|
+
return {
|
|
1153
|
+
messageId,
|
|
1154
|
+
channelId: deliveredChannelId,
|
|
1155
|
+
threadTs: deliveredThreadTs,
|
|
1156
|
+
receipt: createSlackSendReceipt({
|
|
1157
|
+
platformMessageIds: sentMessageIds.length ? sentMessageIds : [messageId],
|
|
1158
|
+
channelId: deliveredChannelId,
|
|
1159
|
+
kind: fallbackMessages.some((message) => message.blocks) ? "card" : "text",
|
|
1160
|
+
threadTs: deliveredThreadTs
|
|
1161
|
+
})
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const resolvedChunks = resolveSlackTextChunks({
|
|
1166
|
+
cfg,
|
|
1167
|
+
accountId: account.accountId,
|
|
1168
|
+
text: trimmedMessage,
|
|
1169
|
+
...opts.textLimit !== void 0 ? { textLimit: opts.textLimit } : {},
|
|
1170
|
+
...opts.textIsSlackMrkdwn ? { textIsSlackMrkdwn: true } : {},
|
|
1171
|
+
...opts.textIsSlackPlainText ? { preservePlainText: true } : {}
|
|
1172
|
+
});
|
|
1173
|
+
const mediaMaxBytes = opts.mediaMaxBytes ?? (typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb * 1024 * 1024 : void 0);
|
|
842
1174
|
let chunksToPost;
|
|
843
1175
|
if (opts.mediaUrl) {
|
|
1176
|
+
if (enterpriseDelivery && !enterpriseDelivery.uploadCompletionClient) throw new Error("missing_enterprise_slack_upload_completion_client");
|
|
844
1177
|
const [firstChunk, ...rest] = resolvedChunks;
|
|
845
1178
|
lastMessageId = await uploadSlackFile({
|
|
846
1179
|
client,
|
|
1180
|
+
...enterpriseDelivery?.uploadCompletionClient ? { completionClient: enterpriseDelivery.uploadCompletionClient } : {},
|
|
847
1181
|
channelId,
|
|
848
1182
|
mediaUrl: opts.mediaUrl,
|
|
849
1183
|
mediaAccess: opts.mediaAccess,
|
|
@@ -854,7 +1188,8 @@ async function sendMessageSlackQueuedInner(params) {
|
|
|
854
1188
|
caption: firstChunk,
|
|
855
1189
|
threadTs: opts.threadTs,
|
|
856
1190
|
maxBytes: mediaMaxBytes,
|
|
857
|
-
onPlatformSendDispatch:
|
|
1191
|
+
onPlatformSendDispatch: dispatchOnce,
|
|
1192
|
+
...enterpriseDelivery ? { auditContext: "slack-enterprise-immediate-upload" } : {}
|
|
858
1193
|
});
|
|
859
1194
|
sentMessageIds.push(lastMessageId);
|
|
860
1195
|
await reportDelivery({
|
|
@@ -870,9 +1205,9 @@ async function sendMessageSlackQueuedInner(params) {
|
|
|
870
1205
|
});
|
|
871
1206
|
chunksToPost = rest;
|
|
872
1207
|
} else chunksToPost = resolvedChunks.length ? resolvedChunks : [""];
|
|
873
|
-
let sendIdentity = identity;
|
|
874
1208
|
for (const [partIndex, chunk] of chunksToPost.entries()) {
|
|
875
|
-
const
|
|
1209
|
+
const carriesPrimaryMessageOptions = partIndex === 0 && !opts.mediaUrl && sentMessageIds.length === 0;
|
|
1210
|
+
const baseMetadata = carriesPrimaryMessageOptions ? opts.metadata : void 0;
|
|
876
1211
|
const metadata = opts.mediaUrl ? baseMetadata : withSlackDeliveryMetadata(baseMetadata, {
|
|
877
1212
|
queueId: opts.deliveryQueueId,
|
|
878
1213
|
channelId,
|
|
@@ -880,18 +1215,20 @@ async function sendMessageSlackQueuedInner(params) {
|
|
|
880
1215
|
partIndex,
|
|
881
1216
|
partCount: chunksToPost.length
|
|
882
1217
|
});
|
|
883
|
-
if (partIndex === 0 && !opts.mediaUrl) await
|
|
1218
|
+
if (partIndex === 0 && !opts.mediaUrl) await dispatchOnce();
|
|
884
1219
|
const posted = await postSlackMessageBestEffort({
|
|
885
1220
|
client,
|
|
886
1221
|
channelId,
|
|
887
1222
|
text: chunk,
|
|
888
1223
|
threadTs: opts.threadTs,
|
|
889
|
-
replyBroadcast:
|
|
1224
|
+
replyBroadcast: carriesPrimaryMessageOptions ? opts.replyBroadcast : void 0,
|
|
890
1225
|
identity: sendIdentity,
|
|
891
1226
|
metadata,
|
|
1227
|
+
...opts.textIsSlackPlainText ? { mrkdwn: false } : {},
|
|
892
1228
|
unfurl
|
|
893
1229
|
});
|
|
894
1230
|
const response = posted.response;
|
|
1231
|
+
if (enterpriseDelivery && (!response.ok || !response.ts)) throw new Error(response.ok ? "Slack chat.postMessage returned no message timestamp" : `Slack chat.postMessage failed: ${response.error ?? "unknown error"}`);
|
|
895
1232
|
sendIdentity = posted.identity;
|
|
896
1233
|
lastMessageId = response.ts ?? lastMessageId;
|
|
897
1234
|
deliveredChannelId = resolvePostedMessageChannelId(response, deliveredChannelId);
|
|
@@ -926,4 +1263,4 @@ async function sendMessageSlackQueuedInner(params) {
|
|
|
926
1263
|
};
|
|
927
1264
|
}
|
|
928
1265
|
//#endregion
|
|
929
|
-
export { clearSlackThreadParticipationCache as a, recordSlackThreadParticipation as c, setSlackDefaultSendIdentity as i,
|
|
1266
|
+
export { clearSlackThreadParticipationCache as a, recordSlackThreadParticipation as c, setSlackDefaultSendIdentity as i, buildSlackNativeDataDeliveryPlan as l, resolveSlackDmChannelId as n, hasSlackThreadParticipation as o, sendMessageSlack as r, hasSlackThreadParticipationWithPersistence as s, reconcileSlackUnknownSend as t, chunkSlackTextAtHardLimit as u };
|