@openclaw/slack 2026.7.1-beta.6 → 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,13 +1,13 @@
|
|
|
1
1
|
import { n as registerSlackHttpHandler, r as normalizeSlackWebhookPath } from "./registry-bbjH7IHX.js";
|
|
2
|
-
import { a as resolveSlackAccount, d as formatSlackBotTokenIdentityWarning, f as resolveSlackAppToken, o as resolveSlackAccountAllowFrom, p as resolveSlackBotToken, s as resolveSlackAccountDmPolicy } from "./accounts-BOJJiHSr.js";
|
|
3
|
-
import {
|
|
4
|
-
import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-
|
|
5
|
-
import {
|
|
6
|
-
import { t as getOptionalSlackRuntime } from "./runtime-
|
|
7
|
-
import { i as setSlackDefaultSendIdentity } from "./send-
|
|
8
|
-
import { t as resolveSlackChannelAllowlist } from "./resolve-channels-
|
|
9
|
-
import { t as resolveSlackUserAllowlist } from "./resolve-users-
|
|
10
|
-
import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-
|
|
2
|
+
import { a as resolveSlackAccount, d as formatSlackBotTokenIdentityWarning, f as resolveSlackAppToken, i as resolveDefaultSlackAccountId, o as resolveSlackAccountAllowFrom, p as resolveSlackBotToken, s as resolveSlackAccountDmPolicy } from "./accounts-BOJJiHSr.js";
|
|
3
|
+
import { $ as isSlackAnyNativeApprovalClientEnabled, F as SLACK_TEXT_LIMIT, K as escapeSlackMrkdwn, U as isSlackApprovalActionId, W as isSlackCallbackActionId, a as allowListMatches, c as normalizeSlackAllowOwnerEntry, dt as isSlackExecApprovalAuthorizedSender, l as normalizeSlackSlug, lt as isSlackApprovalAuthorizedSender, o as normalizeAllowList, q as decodeSlackApprovalAction, r as formatSlackError, s as normalizeAllowListLower, u as resolveSlackAllowListMatch, z as truncateSlackText } from "./group-policy-C-b2ZnSl.js";
|
|
4
|
+
import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-BilWUdcI.js";
|
|
5
|
+
import { o as getSlackListenerUploadCompletionClient, u as resolveSlackWebClientOptions } from "./client-D2F6m8CP.js";
|
|
6
|
+
import { t as getOptionalSlackRuntime } from "./runtime-CauEvswP.js";
|
|
7
|
+
import { i as setSlackDefaultSendIdentity } from "./send-NEfJ0GvM.js";
|
|
8
|
+
import { t as resolveSlackChannelAllowlist } from "./resolve-channels-DOMgNCWb.js";
|
|
9
|
+
import { t as resolveSlackUserAllowlist } from "./resolve-users-aL6-Ssee.js";
|
|
10
|
+
import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-f8b0edSi.js";
|
|
11
11
|
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
12
12
|
import { asOptionalRecord, isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, normalizeStringEntriesLower, normalizeUniqueTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
13
13
|
import { normalizeAccountId, normalizeMainKey, resolveAgentRoute, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
|
@@ -20,9 +20,10 @@ import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plu
|
|
|
20
20
|
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
|
|
21
21
|
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
22
22
|
import { addAllowlistUserEntriesFromConfigEntry, buildAllowlistResolutionSummary, formatAllowlistMatchMeta, mergeAllowlist, patchAllowlistUsersInConfigEntries, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
|
|
23
|
-
import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
23
|
+
import { collectErrorGraphCandidates, formatErrorMessage, isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
|
|
24
24
|
import { asDateTimestampMs, parseFiniteNumber, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
25
25
|
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
|
|
26
|
+
import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime";
|
|
26
27
|
import { createDedupeCache, createPersistentDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
|
27
28
|
import { buildPluginBindingResolvedText, parsePluginBindingApprovalCustomId, recordInboundSession, resolveConversationLabel as resolveConversationLabel$1, resolvePluginConversationBindingApproval, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
28
29
|
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "openclaw/plugin-sdk/native-command-config-runtime";
|
|
@@ -45,7 +46,6 @@ import { createInteractiveConversationBindingHelpers, dispatchPluginInteractiveH
|
|
|
45
46
|
import { createChannelIngressResolver, defineStableChannelIngressIdentity, readChannelIngressStoreAllowFromForDmPolicy } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
46
47
|
import { createChannelInboundDebouncer, shouldDebounceTextInbound } from "openclaw/plugin-sdk/channel-inbound";
|
|
47
48
|
import { generateSecureToken } from "openclaw/plugin-sdk/secure-random-runtime";
|
|
48
|
-
import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime";
|
|
49
49
|
//#region extensions/slack/src/monitor/commands.ts
|
|
50
50
|
/**
|
|
51
51
|
* Strip Slack mentions (<@U123>, <@U123|name>) so command detection works on
|
|
@@ -96,6 +96,9 @@ function resolveSlackChatType(channelType) {
|
|
|
96
96
|
//#endregion
|
|
97
97
|
//#region extensions/slack/src/monitor/context.ts
|
|
98
98
|
const SLACK_ASSISTANT_THREAD_CONTEXT_METADATA_EVENT = "assistant_thread_context";
|
|
99
|
+
const SLACK_CHANNEL_CACHE_MAX_ENTRIES = 1024;
|
|
100
|
+
const SLACK_CHANNEL_DENIAL_WARNING_TTL_MS = 5 * 6e4;
|
|
101
|
+
const SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES = 1024;
|
|
99
102
|
function buildSlackAssistantThreadMetadata(context) {
|
|
100
103
|
const eventPayload = {};
|
|
101
104
|
if (context.channelId) eventPayload.channel_id = context.channelId;
|
|
@@ -134,23 +137,61 @@ function createSlackMonitorContext(params) {
|
|
|
134
137
|
ttlMs: 6e4,
|
|
135
138
|
maxSize: 500
|
|
136
139
|
});
|
|
140
|
+
const channelDenialWarnings = createDedupeCache({
|
|
141
|
+
ttlMs: SLACK_CHANNEL_DENIAL_WARNING_TTL_MS,
|
|
142
|
+
maxSize: SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES
|
|
143
|
+
});
|
|
137
144
|
const assistantThreadContexts = /* @__PURE__ */ new Map();
|
|
138
145
|
let lastAssistantContextCleanupAt = Date.now();
|
|
139
146
|
const allowFrom = normalizeAllowList(params.allowFrom);
|
|
140
147
|
const groupDmChannels = normalizeAllowList(params.groupDmChannels);
|
|
141
|
-
const groupDmChannelsLower = normalizeAllowListLower(groupDmChannels);
|
|
148
|
+
const groupDmChannelsLower = new Set(normalizeAllowListLower(groupDmChannels).map((entry) => entry.replace(/^channel:/, "")));
|
|
142
149
|
const defaultRequireMention = params.defaultRequireMention ?? true;
|
|
143
150
|
const hasChannelAllowlistConfig = Object.keys(params.channelsConfig ?? {}).length > 0;
|
|
144
151
|
const channelsConfigKeys = Object.keys(params.channelsConfig ?? {});
|
|
145
|
-
const
|
|
152
|
+
const scopedKey = (key, eventScope) => eventScope ? `${params.accountId}:${eventScope.teamId}:${key}` : key;
|
|
153
|
+
const readCachedChannel = (cacheKey) => {
|
|
154
|
+
const cached = channelCache.get(cacheKey);
|
|
155
|
+
if (cached) {
|
|
156
|
+
channelCache.delete(cacheKey);
|
|
157
|
+
channelCache.set(cacheKey, cached);
|
|
158
|
+
}
|
|
159
|
+
return cached;
|
|
160
|
+
};
|
|
161
|
+
const writeCachedChannel = (cacheKey, entry) => {
|
|
162
|
+
channelCache.delete(cacheKey);
|
|
163
|
+
channelCache.set(cacheKey, entry);
|
|
164
|
+
pruneMapToMaxSize(channelCache, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
|
|
165
|
+
};
|
|
166
|
+
const rememberSlackChannelType = (channelId, channelType, eventScope) => {
|
|
167
|
+
const id = normalizeOptionalString(channelId);
|
|
168
|
+
const normalizedType = normalizeOptionalString(channelType)?.toLowerCase();
|
|
169
|
+
if (!id || normalizedType !== "im" && normalizedType !== "mpim" && normalizedType !== "channel" && normalizedType !== "group") return;
|
|
170
|
+
const cacheKey = scopedKey(id, eventScope);
|
|
171
|
+
const cached = readCachedChannel(cacheKey);
|
|
172
|
+
const type = normalizeSlackChannelType(normalizedType, id);
|
|
173
|
+
if (cached?.info.type === type) return;
|
|
174
|
+
writeCachedChannel(cacheKey, {
|
|
175
|
+
info: {
|
|
176
|
+
...cached?.info,
|
|
177
|
+
type
|
|
178
|
+
},
|
|
179
|
+
metadataLoaded: cached?.metadataLoaded ?? false
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
const recallSlackChannelType = (channelId, eventScope) => {
|
|
183
|
+
const id = normalizeOptionalString(channelId);
|
|
184
|
+
return id ? readCachedChannel(scopedKey(id, eventScope))?.info.type : void 0;
|
|
185
|
+
};
|
|
186
|
+
const markMessageSeen = (channelId, ts, eventScope) => {
|
|
146
187
|
if (!channelId || !ts) return false;
|
|
147
|
-
return seenMessages.check(`${channelId}:${ts}
|
|
188
|
+
return seenMessages.check(scopedKey(`${channelId}:${ts}`, eventScope));
|
|
148
189
|
};
|
|
149
|
-
const releaseSeenMessage = (channelId, ts) => {
|
|
190
|
+
const releaseSeenMessage = (channelId, ts, eventScope) => {
|
|
150
191
|
if (!channelId || !ts) return;
|
|
151
|
-
seenMessages.delete(`${channelId}:${ts}
|
|
192
|
+
seenMessages.delete(scopedKey(`${channelId}:${ts}`, eventScope));
|
|
152
193
|
};
|
|
153
|
-
const assistantContextKey = (channelId, threadTs) => `${channelId}:${threadTs}
|
|
194
|
+
const assistantContextKey = (channelId, threadTs, eventScope) => scopedKey(`${channelId}:${threadTs}`, eventScope);
|
|
154
195
|
const cleanupAssistantThreadContexts = () => {
|
|
155
196
|
const now = Date.now();
|
|
156
197
|
if (now - lastAssistantContextCleanupAt < SLACK_ASSISTANT_CONTEXT_CLEANUP_INTERVAL_MS) return;
|
|
@@ -158,9 +199,9 @@ function createSlackMonitorContext(params) {
|
|
|
158
199
|
const cutoff = now - SLACK_ASSISTANT_CONTEXT_TTL_MS;
|
|
159
200
|
for (const [key, entry] of assistantThreadContexts) if (entry.updatedAt < cutoff) assistantThreadContexts.delete(key);
|
|
160
201
|
};
|
|
161
|
-
const getSlackAssistantThreadContext = (channelId, threadTs) => {
|
|
202
|
+
const getSlackAssistantThreadContext = (channelId, threadTs, eventScope) => {
|
|
162
203
|
if (!channelId || !threadTs) return;
|
|
163
|
-
const key = assistantContextKey(channelId, threadTs);
|
|
204
|
+
const key = assistantContextKey(channelId, threadTs, eventScope);
|
|
164
205
|
const entry = assistantThreadContexts.get(key);
|
|
165
206
|
if (!entry) return;
|
|
166
207
|
if (Date.now() - entry.updatedAt > SLACK_ASSISTANT_CONTEXT_TTL_MS) {
|
|
@@ -169,9 +210,9 @@ function createSlackMonitorContext(params) {
|
|
|
169
210
|
}
|
|
170
211
|
return entry;
|
|
171
212
|
};
|
|
172
|
-
const saveSlackAssistantThreadContext = (context) => {
|
|
213
|
+
const saveSlackAssistantThreadContext = (context, eventScope) => {
|
|
173
214
|
cleanupAssistantThreadContexts();
|
|
174
|
-
assistantThreadContexts.set(assistantContextKey(context.assistantChannelId, context.threadTs), {
|
|
215
|
+
assistantThreadContexts.set(assistantContextKey(context.assistantChannelId, context.threadTs, eventScope), {
|
|
175
216
|
...context,
|
|
176
217
|
updatedAt: Date.now()
|
|
177
218
|
});
|
|
@@ -179,7 +220,7 @@ function createSlackMonitorContext(params) {
|
|
|
179
220
|
const resolveSlackSystemEventSessionKey = (p) => {
|
|
180
221
|
const channelId = normalizeOptionalString(p.channelId) ?? "";
|
|
181
222
|
const senderId = normalizeOptionalString(p.senderId) ?? "";
|
|
182
|
-
const channelType = normalizeSlackChannelType(p.channelType, channelId);
|
|
223
|
+
const channelType = normalizeSlackChannelType(p.channelType ?? recallSlackChannelType(channelId), channelId);
|
|
183
224
|
const isDirectMessage = channelType === "im";
|
|
184
225
|
if (!channelId && (!isDirectMessage || !senderId)) return params.mainKey;
|
|
185
226
|
const isGroup = channelType === "mpim";
|
|
@@ -237,39 +278,47 @@ function createSlackMonitorContext(params) {
|
|
|
237
278
|
parentSessionKey: normalizeOptionalString(p.threadTs) && params.threadInheritParent ? legacySessionKey : void 0
|
|
238
279
|
}).sessionKey;
|
|
239
280
|
};
|
|
240
|
-
const resolveChannelName = async (channelId) => {
|
|
241
|
-
const
|
|
242
|
-
|
|
281
|
+
const resolveChannelName = async (channelId, eventScope) => {
|
|
282
|
+
const cacheKey = scopedKey(channelId, eventScope);
|
|
283
|
+
const cached = readCachedChannel(cacheKey);
|
|
284
|
+
if (cached?.metadataLoaded) return cached.info;
|
|
243
285
|
try {
|
|
244
|
-
const info = await params.app.client.conversations.info({
|
|
286
|
+
const info = await (eventScope?.client ?? params.app.client).conversations.info({
|
|
245
287
|
token: params.botToken,
|
|
246
288
|
channel: channelId
|
|
247
289
|
});
|
|
248
290
|
const name = info.channel && "name" in info.channel ? info.channel.name : void 0;
|
|
249
291
|
const channel = info.channel ?? void 0;
|
|
292
|
+
const type = channel?.is_im ? "im" : channel?.is_mpim ? "mpim" : channel?.is_channel ? "channel" : channel?.is_group ? "group" : void 0;
|
|
293
|
+
const topic = channel && "topic" in channel ? channel.topic?.value ?? void 0 : void 0;
|
|
294
|
+
const purpose = channel && "purpose" in channel ? channel.purpose?.value ?? void 0 : void 0;
|
|
250
295
|
const entry = {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
296
|
+
info: {
|
|
297
|
+
name,
|
|
298
|
+
type: cached?.info.type ?? type,
|
|
299
|
+
topic,
|
|
300
|
+
purpose
|
|
301
|
+
},
|
|
302
|
+
metadataLoaded: true
|
|
255
303
|
};
|
|
256
|
-
|
|
257
|
-
return entry;
|
|
304
|
+
writeCachedChannel(cacheKey, entry);
|
|
305
|
+
return entry.info;
|
|
258
306
|
} catch {
|
|
259
|
-
return {};
|
|
307
|
+
return cached?.info ?? {};
|
|
260
308
|
}
|
|
261
309
|
};
|
|
262
|
-
const resolveUserName = async (userId) => {
|
|
263
|
-
const
|
|
310
|
+
const resolveUserName = async (userId, eventScope) => {
|
|
311
|
+
const cacheKey = scopedKey(userId, eventScope);
|
|
312
|
+
const cached = userCache.get(cacheKey);
|
|
264
313
|
if (cached) return cached;
|
|
265
314
|
try {
|
|
266
|
-
const info = await params.app.client.users.info({
|
|
315
|
+
const info = await (eventScope?.client ?? params.app.client).users.info({
|
|
267
316
|
token: params.botToken,
|
|
268
317
|
user: userId
|
|
269
318
|
});
|
|
270
319
|
const profile = info.user?.profile;
|
|
271
320
|
const entry = { name: profile?.display_name || profile?.real_name || info.user?.name || void 0 };
|
|
272
|
-
userCache.set(
|
|
321
|
+
userCache.set(cacheKey, entry);
|
|
273
322
|
return entry;
|
|
274
323
|
} catch {
|
|
275
324
|
return {};
|
|
@@ -278,7 +327,7 @@ function createSlackMonitorContext(params) {
|
|
|
278
327
|
const setSlackThreadStatus = async (p) => {
|
|
279
328
|
if (!p.threadTs) return;
|
|
280
329
|
try {
|
|
281
|
-
await params.app.client.assistant.threads.setStatus({
|
|
330
|
+
await (p.eventScope?.client ?? params.app.client).assistant.threads.setStatus({
|
|
282
331
|
token: params.botToken,
|
|
283
332
|
channel_id: p.channelId,
|
|
284
333
|
thread_ts: p.threadTs,
|
|
@@ -323,7 +372,7 @@ function createSlackMonitorContext(params) {
|
|
|
323
372
|
p.channelName,
|
|
324
373
|
p.channelName ? normalizeSlackSlug(p.channelName) : void 0
|
|
325
374
|
].filter((value) => Boolean(value)).map((value) => normalizeLowercaseStringOrEmpty(value));
|
|
326
|
-
if (!(groupDmChannelsLower.
|
|
375
|
+
if (!(groupDmChannelsLower.has("*") || candidates.some((candidate) => groupDmChannelsLower.has(candidate)))) return false;
|
|
327
376
|
}
|
|
328
377
|
if (isRoom && p.channelId) {
|
|
329
378
|
const channelConfig = resolveSlackChannelConfig({
|
|
@@ -337,19 +386,33 @@ function createSlackMonitorContext(params) {
|
|
|
337
386
|
const channelMatchMeta = formatAllowlistMatchMeta(channelConfig);
|
|
338
387
|
const channelAllowed = channelConfig?.allowed !== false;
|
|
339
388
|
const channelAllowlistConfigured = hasChannelAllowlistConfig;
|
|
340
|
-
|
|
389
|
+
const allowedByPolicy = isSlackChannelAllowedByPolicy({
|
|
341
390
|
groupPolicy: params.groupPolicy,
|
|
342
391
|
channelAllowlistConfigured,
|
|
343
392
|
channelAllowed
|
|
344
|
-
})
|
|
393
|
+
});
|
|
394
|
+
const explicitlyDisabled = params.groupPolicy !== "disabled" && channelConfig?.allowed === false && channelConfig.matchSource !== void 0;
|
|
395
|
+
if (!allowedByPolicy || params.groupPolicy === "open" && explicitlyDisabled) {
|
|
396
|
+
if (explicitlyDisabled) {
|
|
397
|
+
const reason = "channel_not_allowed";
|
|
398
|
+
const warningKey = `${params.accountId}:${p.channelId}:${reason}`;
|
|
399
|
+
if (!channelDenialWarnings.peek(warningKey)) {
|
|
400
|
+
channelDenialWarnings.check(warningKey);
|
|
401
|
+
logger.warn({
|
|
402
|
+
provider: "slack",
|
|
403
|
+
accountId: params.accountId,
|
|
404
|
+
channelId: p.channelId,
|
|
405
|
+
reason,
|
|
406
|
+
cause: "channel_disabled",
|
|
407
|
+
groupPolicy: params.groupPolicy,
|
|
408
|
+
matchSource: channelConfig.matchSource,
|
|
409
|
+
matchKey: channelConfig.matchKey
|
|
410
|
+
}, "Slack channel denied by configuration");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
345
413
|
logVerbose(`slack: drop channel ${p.channelId} (groupPolicy=${params.groupPolicy}, ${channelMatchMeta})`);
|
|
346
414
|
return false;
|
|
347
415
|
}
|
|
348
|
-
const hasExplicitConfig = Boolean(channelConfig?.matchSource);
|
|
349
|
-
if (!channelAllowed && (params.groupPolicy !== "open" || hasExplicitConfig)) {
|
|
350
|
-
logVerbose(`slack: drop channel ${p.channelId} (${channelMatchMeta})`);
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
416
|
logVerbose(`slack: allow channel ${p.channelId} (${channelMatchMeta})`);
|
|
354
417
|
}
|
|
355
418
|
return true;
|
|
@@ -380,6 +443,10 @@ function createSlackMonitorContext(params) {
|
|
|
380
443
|
botId: params.botId,
|
|
381
444
|
teamId: params.teamId,
|
|
382
445
|
apiAppId: params.apiAppId,
|
|
446
|
+
installationIdentity: params.installationIdentity ?? {
|
|
447
|
+
kind: "degraded",
|
|
448
|
+
reason: "auth_test_failed"
|
|
449
|
+
},
|
|
383
450
|
historyLimit: params.historyLimit,
|
|
384
451
|
dmHistoryLimit: Math.max(0, params.dmHistoryLimit ?? 0),
|
|
385
452
|
channelHistories,
|
|
@@ -415,6 +482,8 @@ function createSlackMonitorContext(params) {
|
|
|
415
482
|
resolveSlackSystemEventSessionKey,
|
|
416
483
|
isChannelAllowed,
|
|
417
484
|
resolveChannelName,
|
|
485
|
+
rememberSlackChannelType,
|
|
486
|
+
recallSlackChannelType,
|
|
418
487
|
resolveUserName,
|
|
419
488
|
setSlackThreadStatus,
|
|
420
489
|
getSlackAssistantThreadContext,
|
|
@@ -423,6 +492,121 @@ function createSlackMonitorContext(params) {
|
|
|
423
492
|
};
|
|
424
493
|
}
|
|
425
494
|
//#endregion
|
|
495
|
+
//#region extensions/slack/src/monitor/enterprise-install.ts
|
|
496
|
+
const SLACK_CHANNEL_ID_RE = /^[CDG][A-Z0-9]{8,}$/;
|
|
497
|
+
const SLACK_USER_ID_RE = /^[UW][A-Z0-9]{8,}$/;
|
|
498
|
+
function isStableSlackChannelEntry(value, options) {
|
|
499
|
+
if (typeof value !== "string") return false;
|
|
500
|
+
const normalized = value.trim();
|
|
501
|
+
if (normalized === "*") return options?.allowWildcard === true;
|
|
502
|
+
if (/^channel:([CDG][A-Z0-9]{8,})$/.exec(normalized)?.[1]) return true;
|
|
503
|
+
return SLACK_CHANNEL_ID_RE.test(normalized);
|
|
504
|
+
}
|
|
505
|
+
function isStableSlackAllowlistUserEntry(value) {
|
|
506
|
+
if (typeof value !== "string") return false;
|
|
507
|
+
const normalized = value.trim();
|
|
508
|
+
if (normalized === "*") return true;
|
|
509
|
+
const prefixed = /^(?:slack|user):([UW][A-Z0-9]{8,})$/.exec(normalized);
|
|
510
|
+
return Boolean(prefixed?.[1]) || SLACK_USER_ID_RE.test(normalized);
|
|
511
|
+
}
|
|
512
|
+
function isStableSlackToolsBySenderEntry(value) {
|
|
513
|
+
if (typeof value !== "string") return false;
|
|
514
|
+
const normalized = value.trim();
|
|
515
|
+
if (normalized === "*") return true;
|
|
516
|
+
const prefixed = /^(?:id:|channel:slack:)([UW][A-Z0-9]{8,})$/.exec(normalized);
|
|
517
|
+
return Boolean(prefixed?.[1]) || SLACK_USER_ID_RE.test(normalized);
|
|
518
|
+
}
|
|
519
|
+
function assertStableEntries(params) {
|
|
520
|
+
const invalid = params.values?.find((value) => !params.predicate(value));
|
|
521
|
+
if (invalid !== void 0) throw new Error(`Slack Enterprise Grid org installs require stable Slack IDs in ${params.path}; invalid entry ${JSON.stringify(invalid)}`);
|
|
522
|
+
}
|
|
523
|
+
/** Validate every policy surface that would otherwise require name resolution. */
|
|
524
|
+
function assertEnterpriseSlackPolicyConfig(params) {
|
|
525
|
+
const { config, accountId } = params;
|
|
526
|
+
if (config.dangerouslyAllowNameMatching === true) throw new Error(`Slack Enterprise Grid org account "${accountId}" cannot use dangerouslyAllowNameMatching`);
|
|
527
|
+
if (config.mentionPatterns?.allowIn !== void 0 || config.mentionPatterns?.denyIn !== void 0) throw new Error(`Slack Enterprise Grid org account "${accountId}" cannot use mentionPatterns.allowIn or mentionPatterns.denyIn because Slack channel IDs are not workspace-qualified`);
|
|
528
|
+
assertStableEntries({
|
|
529
|
+
values: config.allowFrom,
|
|
530
|
+
path: `channels.slack.accounts.${accountId}.allowFrom`,
|
|
531
|
+
predicate: isStableSlackAllowlistUserEntry
|
|
532
|
+
});
|
|
533
|
+
assertStableEntries({
|
|
534
|
+
values: config.dm?.allowFrom,
|
|
535
|
+
path: `channels.slack.accounts.${accountId}.dm.allowFrom`,
|
|
536
|
+
predicate: isStableSlackAllowlistUserEntry
|
|
537
|
+
});
|
|
538
|
+
assertStableEntries({
|
|
539
|
+
values: config.dm?.groupChannels,
|
|
540
|
+
path: `channels.slack.accounts.${accountId}.dm.groupChannels`,
|
|
541
|
+
predicate: (value) => isStableSlackChannelEntry(value)
|
|
542
|
+
});
|
|
543
|
+
if (config.reactionNotifications === "allowlist") assertStableEntries({
|
|
544
|
+
values: config.reactionAllowlist,
|
|
545
|
+
path: `channels.slack.accounts.${accountId}.reactionAllowlist`,
|
|
546
|
+
predicate: isStableSlackAllowlistUserEntry
|
|
547
|
+
});
|
|
548
|
+
for (const [channelKey, channel] of Object.entries(config.channels ?? {})) {
|
|
549
|
+
if (!isStableSlackChannelEntry(channelKey, { allowWildcard: true })) throw new Error(`Slack Enterprise Grid org installs require stable Slack channel IDs; invalid channels key ${JSON.stringify(channelKey)}`);
|
|
550
|
+
assertStableEntries({
|
|
551
|
+
values: channel?.users,
|
|
552
|
+
path: `channels.slack.accounts.${accountId}.channels.${channelKey}.users`,
|
|
553
|
+
predicate: isStableSlackAllowlistUserEntry
|
|
554
|
+
});
|
|
555
|
+
assertStableEntries({
|
|
556
|
+
values: Object.keys(channel?.toolsBySender ?? {}),
|
|
557
|
+
path: `channels.slack.accounts.${accountId}.channels.${channelKey}.toolsBySender`,
|
|
558
|
+
predicate: isStableSlackToolsBySenderEntry
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/** Prevent account-wide user authorization state from crossing workspace boundaries. */
|
|
563
|
+
function assertEnterpriseSlackDmPolicy(params) {
|
|
564
|
+
if (!params.dmEnabled || params.dmPolicy === "disabled") return;
|
|
565
|
+
if (params.dmPolicy === "open" && params.allowFrom?.includes("*")) return;
|
|
566
|
+
throw new Error(`Slack Enterprise Grid org account "${params.accountId}" supports DMs only with dm.enabled=false, dmPolicy="disabled", or dmPolicy="open" with effective allowFrom containing "*"; dmPolicy=${JSON.stringify(params.dmPolicy)} and allowFrom=${JSON.stringify(params.allowFrom ?? [])} would share per-user authorization across workspaces`);
|
|
567
|
+
}
|
|
568
|
+
function assertNoEnterpriseSlackBindings(params) {
|
|
569
|
+
const defaultAccountId = resolveDefaultSlackAccountId(params.cfg);
|
|
570
|
+
if (params.cfg.bindings?.find((binding) => {
|
|
571
|
+
if (binding.match.channel.trim().toLowerCase() !== "slack") return false;
|
|
572
|
+
const accountId = binding.match.accountId?.trim();
|
|
573
|
+
return accountId === "*" || accountId === params.accountId || !accountId && params.accountId === defaultAccountId;
|
|
574
|
+
})) throw new Error(`Slack Enterprise Grid org account "${params.accountId}" cannot use configured Slack bindings`);
|
|
575
|
+
}
|
|
576
|
+
function resolveSlackInstallationIdentity(params) {
|
|
577
|
+
const auth = params.auth;
|
|
578
|
+
if (!auth) {
|
|
579
|
+
if (params.enterpriseOrgInstall) throw new Error(`Slack enterpriseOrgInstall=true requires a successful auth.test (${formatSlackError(params.authError)})`);
|
|
580
|
+
return {
|
|
581
|
+
kind: "degraded",
|
|
582
|
+
reason: "auth_test_failed"
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const isEnterpriseInstall = auth.is_enterprise_install === true;
|
|
586
|
+
if (isEnterpriseInstall !== params.enterpriseOrgInstall) throw new Error(isEnterpriseInstall ? "Slack auth.test detected an org-wide installation; set enterpriseOrgInstall=true" : "Slack enterpriseOrgInstall=true requires an org-wide bot installation");
|
|
587
|
+
const apiAppId = normalizeOptionalString(auth.app_id);
|
|
588
|
+
const enterpriseId = normalizeOptionalString(auth.enterprise_id);
|
|
589
|
+
if (params.enterpriseOrgInstall) {
|
|
590
|
+
if (!enterpriseId) throw new Error("Slack org-wide auth.test returned no enterprise_id");
|
|
591
|
+
const transportApiAppId = normalizeOptionalString(params.transportApiAppId);
|
|
592
|
+
if (apiAppId && transportApiAppId && apiAppId !== transportApiAppId) throw new Error(`Slack token mismatch: bot token app_id=${apiAppId} but transport app_id=${transportApiAppId}`);
|
|
593
|
+
const effectiveApiAppId = apiAppId ?? transportApiAppId;
|
|
594
|
+
return {
|
|
595
|
+
kind: "enterprise",
|
|
596
|
+
...effectiveApiAppId ? { apiAppId: effectiveApiAppId } : {},
|
|
597
|
+
enterpriseId
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const teamId = normalizeOptionalString(auth.team_id);
|
|
601
|
+
if (!teamId) throw new Error("Slack workspace auth.test returned no team_id");
|
|
602
|
+
return {
|
|
603
|
+
kind: "workspace",
|
|
604
|
+
teamId,
|
|
605
|
+
...apiAppId ? { apiAppId } : {},
|
|
606
|
+
...enterpriseId ? { enterpriseId } : {}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
//#endregion
|
|
426
610
|
//#region extensions/slack/src/monitor/events/assistant.ts
|
|
427
611
|
const DEFAULT_ASSISTANT_PROMPTS = [
|
|
428
612
|
{
|
|
@@ -557,7 +741,12 @@ function migrateSlackChannelsInPlace(channels, oldChannelId, newChannelId) {
|
|
|
557
741
|
migrated: false,
|
|
558
742
|
skippedExisting: true
|
|
559
743
|
};
|
|
560
|
-
|
|
744
|
+
const channelConfig = channels[oldChannelId];
|
|
745
|
+
if (!channelConfig) return {
|
|
746
|
+
migrated: false,
|
|
747
|
+
skippedExisting: false
|
|
748
|
+
};
|
|
749
|
+
channels[newChannelId] = channelConfig;
|
|
561
750
|
delete channels[oldChannelId];
|
|
562
751
|
return {
|
|
563
752
|
migrated: true,
|
|
@@ -702,7 +891,7 @@ function registerSlackChannelEvents(params) {
|
|
|
702
891
|
}
|
|
703
892
|
//#endregion
|
|
704
893
|
//#region extensions/slack/src/monitor/events/home.ts
|
|
705
|
-
function buildSlackHomeView() {
|
|
894
|
+
function buildSlackHomeView(slashCommandName) {
|
|
706
895
|
return {
|
|
707
896
|
type: "home",
|
|
708
897
|
callback_id: "openclaw:home",
|
|
@@ -718,7 +907,7 @@ function buildSlackHomeView() {
|
|
|
718
907
|
type: "section",
|
|
719
908
|
text: {
|
|
720
909
|
type: "mrkdwn",
|
|
721
|
-
text:
|
|
910
|
+
text: slashCommandName ? `Send a DM, mention OpenClaw in a channel, or use \`/${slashCommandName}\` to start a session.` : "Send a DM or mention OpenClaw in a channel to start a session."
|
|
722
911
|
}
|
|
723
912
|
},
|
|
724
913
|
{
|
|
@@ -732,7 +921,7 @@ function buildSlackHomeView() {
|
|
|
732
921
|
};
|
|
733
922
|
}
|
|
734
923
|
function registerSlackHomeEvents(params) {
|
|
735
|
-
const { ctx, trackEvent } = params;
|
|
924
|
+
const { ctx, slashCommandName, trackEvent } = params;
|
|
736
925
|
ctx.app.event("app_home_opened", async ({ event, body }) => {
|
|
737
926
|
try {
|
|
738
927
|
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
@@ -742,7 +931,7 @@ function registerSlackHomeEvents(params) {
|
|
|
742
931
|
await ctx.app.client.views.publish({
|
|
743
932
|
token: ctx.botToken,
|
|
744
933
|
user_id: payload.user,
|
|
745
|
-
view: buildSlackHomeView()
|
|
934
|
+
view: buildSlackHomeView(slashCommandName)
|
|
746
935
|
});
|
|
747
936
|
} catch (err) {
|
|
748
937
|
ctx.runtime.error?.(danger(`slack app home handler failed: ${formatErrorMessage(err)}`));
|
|
@@ -783,7 +972,7 @@ async function dispatchSlackPluginInteractiveHandler(params) {
|
|
|
783
972
|
}
|
|
784
973
|
//#endregion
|
|
785
974
|
//#region extensions/slack/src/monitor/auth.ts
|
|
786
|
-
|
|
975
|
+
const slackChannelMembersCache = /* @__PURE__ */ new WeakMap();
|
|
787
976
|
const DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS = 6e4;
|
|
788
977
|
const CHANNEL_MEMBERS_CACHE_MAX = 512;
|
|
789
978
|
const SLACK_CHANNEL_ID = "slack";
|
|
@@ -891,11 +1080,11 @@ async function resolveSlackEffectiveAllowFrom(ctx, options) {
|
|
|
891
1080
|
}
|
|
892
1081
|
return normalizeAllowListLower([...base, ...storeAllowFrom]);
|
|
893
1082
|
}
|
|
894
|
-
async function fetchSlackChannelMemberIds(ctx, channelId) {
|
|
1083
|
+
async function fetchSlackChannelMemberIds(ctx, channelId, eventScope) {
|
|
895
1084
|
const members = /* @__PURE__ */ new Set();
|
|
896
1085
|
let cursor;
|
|
897
1086
|
do {
|
|
898
|
-
const response = await ctx.app.client.conversations.members({
|
|
1087
|
+
const response = await (eventScope?.client ?? ctx.app.client).conversations.members({
|
|
899
1088
|
token: ctx.botToken,
|
|
900
1089
|
channel: channelId,
|
|
901
1090
|
limit: 999,
|
|
@@ -907,9 +1096,9 @@ async function fetchSlackChannelMemberIds(ctx, channelId) {
|
|
|
907
1096
|
} while (cursor);
|
|
908
1097
|
return members;
|
|
909
1098
|
}
|
|
910
|
-
async function resolveSlackChannelMemberIds(ctx, channelId) {
|
|
1099
|
+
async function resolveSlackChannelMemberIds(ctx, channelId, eventScope) {
|
|
911
1100
|
const cache = getChannelMembersCache(ctx);
|
|
912
|
-
const key = `${ctx.accountId}:${channelId}`;
|
|
1101
|
+
const key = `${ctx.accountId}:${eventScope ? `${eventScope.teamId}:` : ""}${channelId}`;
|
|
913
1102
|
const ttlMs = readSlackCacheTtlMs("OPENCLAW_SLACK_CHANNEL_MEMBERS_CACHE_TTL_MS", DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS);
|
|
914
1103
|
const rawNowMs = Date.now();
|
|
915
1104
|
const nowMs = asDateTimestampMs(rawNowMs);
|
|
@@ -919,7 +1108,7 @@ async function resolveSlackChannelMemberIds(ctx, channelId) {
|
|
|
919
1108
|
cache.delete(key);
|
|
920
1109
|
}
|
|
921
1110
|
if (cached?.pending) return await cached.pending;
|
|
922
|
-
const pending = fetchSlackChannelMemberIds(ctx, channelId);
|
|
1111
|
+
const pending = fetchSlackChannelMemberIds(ctx, channelId, eventScope);
|
|
923
1112
|
const pendingExpiresAtMs = ttlMs > 0 ? resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNowMs }) : void 0;
|
|
924
1113
|
cache.set(key, {
|
|
925
1114
|
expiresAtMs: pendingExpiresAtMs ?? 0,
|
|
@@ -963,7 +1152,7 @@ async function authorizeSlackBotRoomMessage(params) {
|
|
|
963
1152
|
return false;
|
|
964
1153
|
}
|
|
965
1154
|
try {
|
|
966
|
-
const channelMemberIds = await resolveSlackChannelMemberIds(params.ctx, params.channelId);
|
|
1155
|
+
const channelMemberIds = await resolveSlackChannelMemberIds(params.ctx, params.channelId, params.eventScope);
|
|
967
1156
|
if (explicitOwnerIds.some((ownerId) => channelMemberIds.has(ownerId))) return true;
|
|
968
1157
|
logVerbose(`slack: drop bot message ${params.senderId} in ${params.channelId} (no owner present)`);
|
|
969
1158
|
} catch (error) {
|
|
@@ -979,8 +1168,10 @@ function slackIngressConversationKind(channelType) {
|
|
|
979
1168
|
}
|
|
980
1169
|
async function resolveSlackCommandIngress(params) {
|
|
981
1170
|
const isDirectMessage = params.channelType === "im";
|
|
1171
|
+
const isGroupDm = params.channelType === "mpim";
|
|
982
1172
|
const channelUsers = normalizeAllowListLower(params.channelUsers);
|
|
983
|
-
const channelUsersConfigured = !isDirectMessage && channelUsers.length > 0;
|
|
1173
|
+
const channelUsersConfigured = !isDirectMessage && !isGroupDm && channelUsers.length > 0;
|
|
1174
|
+
const groupAllowFrom = isGroupDm ? params.ownerAllowFromLower : channelUsersConfigured ? channelUsers : [];
|
|
984
1175
|
return await createSlackIngressResolver(params.ctx).message({
|
|
985
1176
|
subject: createSlackIngressSubject({
|
|
986
1177
|
senderId: params.senderId,
|
|
@@ -996,7 +1187,7 @@ async function resolveSlackCommandIngress(params) {
|
|
|
996
1187
|
mayPair: false
|
|
997
1188
|
},
|
|
998
1189
|
dmPolicy: isDirectMessage ? "open" : "disabled",
|
|
999
|
-
groupPolicy: channelUsersConfigured ? "allowlist" : "open",
|
|
1190
|
+
groupPolicy: isGroupDm || channelUsersConfigured ? "allowlist" : "open",
|
|
1000
1191
|
policy: {
|
|
1001
1192
|
groupAllowFromFallbackToAllowFrom: false,
|
|
1002
1193
|
mutableIdentifierMatching: params.ctx.allowNameMatching ? "enabled" : "disabled",
|
|
@@ -1004,7 +1195,7 @@ async function resolveSlackCommandIngress(params) {
|
|
|
1004
1195
|
},
|
|
1005
1196
|
mentionFacts: params.mentionFacts,
|
|
1006
1197
|
allowFrom: isDirectMessage ? ["*"] : params.ownerAllowFromLower,
|
|
1007
|
-
groupAllowFrom
|
|
1198
|
+
groupAllowFrom,
|
|
1008
1199
|
command: {
|
|
1009
1200
|
allowTextCommands: params.allowTextCommands,
|
|
1010
1201
|
hasControlCommand: params.hasControlCommand,
|
|
@@ -1015,12 +1206,14 @@ async function resolveSlackCommandIngress(params) {
|
|
|
1015
1206
|
}
|
|
1016
1207
|
async function decideSlackSystemIngress(params) {
|
|
1017
1208
|
const isDirectMessage = params.channelType === "im";
|
|
1209
|
+
const isGroupDm = params.channelType === "mpim";
|
|
1018
1210
|
const channelUsers = normalizeAllowListLower(params.channelUsers);
|
|
1019
|
-
const channelUsersConfigured = !isDirectMessage && channelUsers.length > 0;
|
|
1211
|
+
const channelUsersConfigured = !isDirectMessage && !isGroupDm && channelUsers.length > 0;
|
|
1020
1212
|
const ownerAllowFrom = params.interactiveEvent && channelUsersConfigured ? params.ownerAllowFromLower.filter((entry) => entry !== "*") : params.ownerAllowFromLower;
|
|
1021
1213
|
const hasAnyCommandAllowlist = ownerAllowFrom.length > 0 || channelUsersConfigured;
|
|
1022
1214
|
const groupAllowFrom = (() => {
|
|
1023
1215
|
if (isDirectMessage) return [];
|
|
1216
|
+
if (isGroupDm) return ownerAllowFrom;
|
|
1024
1217
|
if (params.interactiveEvent && hasAnyCommandAllowlist) return channelUsersConfigured ? channelUsers : [];
|
|
1025
1218
|
if (channelUsersConfigured) return channelUsers;
|
|
1026
1219
|
return params.channelId ? ["*"] : wildcardWhenOpen(params.ownerAllowFromLower);
|
|
@@ -1040,7 +1233,7 @@ async function decideSlackSystemIngress(params) {
|
|
|
1040
1233
|
mayPair: false
|
|
1041
1234
|
},
|
|
1042
1235
|
dmPolicy: isDirectMessage ? "open" : "disabled",
|
|
1043
|
-
groupPolicy: params.interactiveEvent && hasAnyCommandAllowlist ? "open" : channelUsersConfigured || !params.channelId && params.ownerAllowFromLower.length > 0 ? "allowlist" : "open",
|
|
1236
|
+
groupPolicy: isGroupDm ? "allowlist" : params.interactiveEvent && hasAnyCommandAllowlist ? "open" : channelUsersConfigured || !params.channelId && params.ownerAllowFromLower.length > 0 ? "allowlist" : "open",
|
|
1044
1237
|
policy: {
|
|
1045
1238
|
groupAllowFromFallbackToAllowFrom: false,
|
|
1046
1239
|
mutableIdentifierMatching: params.ctx.allowNameMatching ? "enabled" : "disabled"
|
|
@@ -1149,11 +1342,6 @@ async function authorizeSlackSystemEventSender(params) {
|
|
|
1149
1342
|
};
|
|
1150
1343
|
}
|
|
1151
1344
|
//#endregion
|
|
1152
|
-
//#region extensions/slack/src/monitor/mrkdwn.ts
|
|
1153
|
-
function escapeSlackMrkdwn(value) {
|
|
1154
|
-
return value.replaceAll("\\", "\\\\").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replace(/([*_`~])/g, "\\$1");
|
|
1155
|
-
}
|
|
1156
|
-
//#endregion
|
|
1157
1345
|
//#region extensions/slack/src/monitor/events/interactions.block-actions.ts
|
|
1158
1346
|
function readOptionValues(options) {
|
|
1159
1347
|
if (!Array.isArray(options)) return;
|
|
@@ -1235,9 +1423,6 @@ function summarizeAction(action) {
|
|
|
1235
1423
|
workflowId: typed.workflow?.workflow_id
|
|
1236
1424
|
};
|
|
1237
1425
|
}
|
|
1238
|
-
function isBulkActionsBlock(block) {
|
|
1239
|
-
return block.type === "actions" && Array.isArray(block.elements) && block.elements.length > 0 && block.elements.every((el) => typeof el.action_id === "string" && el.action_id.includes("_all_"));
|
|
1240
|
-
}
|
|
1241
1426
|
function formatInteractionSelectionLabel(params) {
|
|
1242
1427
|
if (params.summary.actionType === "button" && params.buttonText?.trim()) return params.buttonText.trim();
|
|
1243
1428
|
if (params.summary.selectedLabels?.length) {
|
|
@@ -1267,12 +1452,15 @@ function buildSlackPluginInteractionData(params) {
|
|
|
1267
1452
|
const actionId = normalizeOptionalString(params.actionId) ?? "";
|
|
1268
1453
|
if (!actionId) return null;
|
|
1269
1454
|
const payload = normalizeOptionalString(params.summary.value) || params.summary.selectedValues?.map((value) => normalizeOptionalString(value)).find(Boolean) || "";
|
|
1270
|
-
if (actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`)) return payload || null;
|
|
1455
|
+
if (actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || isSlackCallbackActionId(actionId) || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`)) return payload || null;
|
|
1271
1456
|
return payload ? `${actionId}:${payload}` : actionId;
|
|
1272
1457
|
}
|
|
1273
1458
|
function isSlackReplyActionId(actionId) {
|
|
1274
1459
|
return actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`);
|
|
1275
1460
|
}
|
|
1461
|
+
function readSlackApprovalAction(parsed) {
|
|
1462
|
+
return decodeSlackApprovalAction(normalizeOptionalString(parsed.actionSummary.value) ?? parsed.actionSummary.selectedValues?.map((entry) => normalizeOptionalString(entry)).find((entry) => Boolean(entry)));
|
|
1463
|
+
}
|
|
1276
1464
|
function isSlackReplyLinkAction(parsed) {
|
|
1277
1465
|
if (parsed.actionId === "openclaw:reply_link" || parsed.actionId.startsWith(`openclaw:reply_link:`)) return true;
|
|
1278
1466
|
const legacyUrl = normalizeOptionalString(parsed.typedAction.url);
|
|
@@ -1328,6 +1516,37 @@ async function updateSlackInteractionMessage(params) {
|
|
|
1328
1516
|
...params.blocks ? { blocks: params.blocks } : {}
|
|
1329
1517
|
});
|
|
1330
1518
|
}
|
|
1519
|
+
function resolveSlackApprovalTerminalLabel(approval) {
|
|
1520
|
+
if (approval.status === "allowed") return approval.decision === "allow-always" ? "Allowed always" : "Allowed once";
|
|
1521
|
+
if (approval.status === "denied") return "Denied";
|
|
1522
|
+
if (approval.status === "expired") return "Expired";
|
|
1523
|
+
return "Cancelled";
|
|
1524
|
+
}
|
|
1525
|
+
function removeSlackApprovalControls(blocks) {
|
|
1526
|
+
return blocks.flatMap((block) => {
|
|
1527
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) return [block];
|
|
1528
|
+
const typedBlock = block;
|
|
1529
|
+
if (typedBlock.type !== "actions" || !Array.isArray(typedBlock.elements)) return [block];
|
|
1530
|
+
const elements = typedBlock.elements.filter((element) => typeof element.action_id !== "string" || !isSlackApprovalActionId(element.action_id));
|
|
1531
|
+
return elements.length > 0 ? [{
|
|
1532
|
+
...block,
|
|
1533
|
+
elements
|
|
1534
|
+
}] : [];
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
function buildSlackApprovalTerminalBlocks(params) {
|
|
1538
|
+
const blocks = removeSlackApprovalControls(params.blocks ?? []).filter((block) => {
|
|
1539
|
+
const text = block.text?.text;
|
|
1540
|
+
return !(block.type === "section" && typeof text === "string" && /^\*(?:Exec|Plugin) approval required\*/u.test(text));
|
|
1541
|
+
});
|
|
1542
|
+
return [{
|
|
1543
|
+
type: "section",
|
|
1544
|
+
text: {
|
|
1545
|
+
type: "mrkdwn",
|
|
1546
|
+
text: `*${params.prefix}: ${params.label}*`
|
|
1547
|
+
}
|
|
1548
|
+
}, ...blocks];
|
|
1549
|
+
}
|
|
1331
1550
|
async function authorizeSlackBlockAction(params) {
|
|
1332
1551
|
const auth = await authorizeSlackSystemEventSender({
|
|
1333
1552
|
ctx: params.ctx,
|
|
@@ -1361,9 +1580,7 @@ async function handleSlackPluginBindingApproval(params) {
|
|
|
1361
1580
|
await respondEphemeral(params.respond, buildPluginBindingResolvedText(resolved));
|
|
1362
1581
|
return true;
|
|
1363
1582
|
}
|
|
1364
|
-
async function
|
|
1365
|
-
const approval = parseExecApprovalCommandText(params.pluginInteractionData);
|
|
1366
|
-
if (!approval) return false;
|
|
1583
|
+
async function handleSlackApprovalInteraction(params) {
|
|
1367
1584
|
const pluginApprovalAuthorizedSender = isSlackApprovalAuthorizedSender({
|
|
1368
1585
|
cfg: params.ctx.cfg,
|
|
1369
1586
|
accountId: params.ctx.accountId,
|
|
@@ -1374,38 +1591,95 @@ async function handleSlackExecApprovalInteraction(params) {
|
|
|
1374
1591
|
accountId: params.ctx.accountId,
|
|
1375
1592
|
senderId: params.parsed.userId
|
|
1376
1593
|
});
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
const authorized = isPluginApproval ? pluginApprovalAuthorizedSender : execApprovalAuthorizedSender || resolveUnprefixedAsPlugin;
|
|
1380
|
-
const allowPluginFallback = !isPluginApproval && execApprovalAuthorizedSender && pluginApprovalAuthorizedSender;
|
|
1381
|
-
if (!authorized) {
|
|
1382
|
-
params.ctx.runtime.log?.(`slack:interaction drop exec approval user=${params.parsed.userId} (not authorized)`);
|
|
1594
|
+
if (!(params.approval.approvalKind === "plugin" ? pluginApprovalAuthorizedSender : execApprovalAuthorizedSender)) {
|
|
1595
|
+
params.ctx.runtime.log?.(`slack:interaction drop ${params.approval.approvalKind} approval user=${params.parsed.userId} (not authorized)`);
|
|
1383
1596
|
await respondEphemeral(params.respond, "You are not authorized to approve this request.");
|
|
1384
1597
|
return true;
|
|
1385
1598
|
}
|
|
1386
1599
|
try {
|
|
1387
|
-
await resolveApprovalOverGateway({
|
|
1600
|
+
const result = await resolveApprovalOverGateway({
|
|
1388
1601
|
cfg: params.ctx.cfg,
|
|
1389
|
-
approvalId: approval.approvalId,
|
|
1390
|
-
|
|
1602
|
+
approvalId: params.approval.approvalId,
|
|
1603
|
+
approvalKind: params.approval.approvalKind,
|
|
1604
|
+
decision: params.approval.decision,
|
|
1391
1605
|
senderId: params.parsed.userId,
|
|
1392
|
-
allowPluginFallback,
|
|
1393
|
-
...resolveUnprefixedAsPlugin ? { resolveMethod: "plugin" } : {},
|
|
1394
1606
|
clientDisplayName: `Slack approval (${params.parsed.userId.trim() || "unknown"})`
|
|
1395
1607
|
});
|
|
1608
|
+
const terminalLabel = resolveSlackApprovalTerminalLabel(result.approval);
|
|
1609
|
+
const prefix = result.applied ? "Resolved" : "Already resolved";
|
|
1610
|
+
let terminalized = false;
|
|
1611
|
+
try {
|
|
1612
|
+
const terminalText = `${prefix}: ${terminalLabel}`;
|
|
1613
|
+
await updateSlackInteractionMessage({
|
|
1614
|
+
ctx: params.ctx,
|
|
1615
|
+
channelId: params.parsed.channelId,
|
|
1616
|
+
messageTs: params.parsed.messageTs,
|
|
1617
|
+
text: truncateSlackText(terminalText, 4e3),
|
|
1618
|
+
blocks: buildSlackApprovalTerminalBlocks({
|
|
1619
|
+
blocks: params.parsed.typedBody.message?.blocks,
|
|
1620
|
+
label: terminalLabel,
|
|
1621
|
+
prefix
|
|
1622
|
+
})
|
|
1623
|
+
});
|
|
1624
|
+
terminalized = true;
|
|
1625
|
+
} catch {}
|
|
1626
|
+
if (!terminalized || !result.applied) await respondEphemeral(params.respond, result.applied ? `Approval resolved: ${terminalLabel}.` : `This approval was already resolved: ${terminalLabel}.`);
|
|
1396
1627
|
} catch (error) {
|
|
1397
|
-
params.ctx.runtime.log?.(`slack:interaction
|
|
1628
|
+
params.ctx.runtime.log?.(`slack:interaction approval resolve failed id=${params.approval.approvalId}: ${String(error)}`);
|
|
1629
|
+
if (isApprovalNotFoundError(error)) {
|
|
1630
|
+
await respondEphemeral(params.respond, "This approval is no longer pending.");
|
|
1631
|
+
return true;
|
|
1632
|
+
}
|
|
1633
|
+
await respondEphemeral(params.respond, "Could not reach the Gateway to resolve this approval. Try again.");
|
|
1398
1634
|
throw error;
|
|
1399
1635
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1636
|
+
return true;
|
|
1637
|
+
}
|
|
1638
|
+
async function handleSlackLegacyApprovalInteraction(params) {
|
|
1639
|
+
const parsedApproval = parseExecApprovalCommandText(params.pluginInteractionData);
|
|
1640
|
+
if (!parsedApproval) return false;
|
|
1641
|
+
const pluginAuthorized = isSlackApprovalAuthorizedSender({
|
|
1642
|
+
cfg: params.ctx.cfg,
|
|
1643
|
+
accountId: params.ctx.accountId,
|
|
1644
|
+
senderId: params.parsed.userId
|
|
1645
|
+
});
|
|
1646
|
+
const execAuthorized = isSlackExecApprovalAuthorizedSender({
|
|
1647
|
+
cfg: params.ctx.cfg,
|
|
1648
|
+
accountId: params.ctx.accountId,
|
|
1649
|
+
senderId: params.parsed.userId
|
|
1650
|
+
});
|
|
1651
|
+
const resolveMethods = [];
|
|
1652
|
+
if (execAuthorized) resolveMethods.push("exec");
|
|
1653
|
+
if (pluginAuthorized) resolveMethods.push("plugin");
|
|
1654
|
+
if (resolveMethods.length === 0) {
|
|
1655
|
+
params.ctx.runtime.log?.(`slack:interaction drop legacy approval user=${params.parsed.userId} (not authorized)`);
|
|
1656
|
+
await respondEphemeral(params.respond, "You are not authorized to approve this request.");
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
for (const [index, resolveMethod] of resolveMethods.entries()) try {
|
|
1660
|
+
await resolveApprovalOverGateway({
|
|
1661
|
+
cfg: params.ctx.cfg,
|
|
1662
|
+
approvalId: parsedApproval.approvalId,
|
|
1663
|
+
decision: parsedApproval.decision,
|
|
1664
|
+
senderId: params.parsed.userId,
|
|
1665
|
+
resolveMethod,
|
|
1666
|
+
clientDisplayName: `Slack approval (${params.parsed.userId.trim() || "unknown"})`
|
|
1407
1667
|
});
|
|
1408
|
-
|
|
1668
|
+
try {
|
|
1669
|
+
await updateSlackInteractionMessage({
|
|
1670
|
+
ctx: params.ctx,
|
|
1671
|
+
channelId: params.parsed.channelId,
|
|
1672
|
+
messageTs: params.parsed.messageTs,
|
|
1673
|
+
text: params.parsed.typedBody.message?.text ?? "",
|
|
1674
|
+
blocks: []
|
|
1675
|
+
});
|
|
1676
|
+
} catch {}
|
|
1677
|
+
return true;
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
if (index + 1 < resolveMethods.length && isApprovalNotFoundError(error)) continue;
|
|
1680
|
+
params.ctx.runtime.log?.(`slack:interaction legacy approval resolve failed id=${parsedApproval.approvalId}: ${String(error)}`);
|
|
1681
|
+
throw error;
|
|
1682
|
+
}
|
|
1409
1683
|
return true;
|
|
1410
1684
|
}
|
|
1411
1685
|
async function dispatchSlackPluginInteraction(params) {
|
|
@@ -1572,7 +1846,7 @@ function buildSlackConfirmationBlocks(params) {
|
|
|
1572
1846
|
summary: params.parsed.actionSummary,
|
|
1573
1847
|
buttonText: params.parsed.typedActionWithText.text?.text
|
|
1574
1848
|
});
|
|
1575
|
-
|
|
1849
|
+
return params.originalBlocks.map((block) => {
|
|
1576
1850
|
const typedBlock = block;
|
|
1577
1851
|
if (typedBlock.type === "actions" && typedBlock.block_id === params.parsed.blockId) return {
|
|
1578
1852
|
type: "context",
|
|
@@ -1586,17 +1860,6 @@ function buildSlackConfirmationBlocks(params) {
|
|
|
1586
1860
|
};
|
|
1587
1861
|
return block;
|
|
1588
1862
|
});
|
|
1589
|
-
if (!updatedBlocks.some((block) => {
|
|
1590
|
-
const typedBlock = block;
|
|
1591
|
-
return typedBlock.type === "actions" && !isBulkActionsBlock(typedBlock);
|
|
1592
|
-
})) updatedBlocks = updatedBlocks.filter((block, index) => {
|
|
1593
|
-
const typedBlock = block;
|
|
1594
|
-
if (isBulkActionsBlock(typedBlock)) return false;
|
|
1595
|
-
if (typedBlock.type !== "divider") return true;
|
|
1596
|
-
const next = updatedBlocks[index + 1];
|
|
1597
|
-
return !next || !isBulkActionsBlock(next);
|
|
1598
|
-
});
|
|
1599
|
-
return updatedBlocks;
|
|
1600
1863
|
}
|
|
1601
1864
|
async function updateSlackLegacyBlockAction(params) {
|
|
1602
1865
|
const originalBlocks = params.parsed.typedBody.message?.blocks;
|
|
@@ -1631,12 +1894,27 @@ async function handleSlackBlockAction(params) {
|
|
|
1631
1894
|
if (!parsed) return;
|
|
1632
1895
|
if (isSlackReplyLinkAction(parsed)) return;
|
|
1633
1896
|
params.trackEvent?.();
|
|
1897
|
+
if (isSlackApprovalActionId(parsed.actionId)) {
|
|
1898
|
+
const approval = readSlackApprovalAction(parsed);
|
|
1899
|
+
if (!approval) {
|
|
1900
|
+
params.ctx.runtime.log?.(`slack:interaction drop malformed approval action user=${parsed.userId} channel=${parsed.channelId ?? "unknown"}`);
|
|
1901
|
+
await respondEphemeral(respond, "This approval action is invalid or expired.");
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
await handleSlackApprovalInteraction({
|
|
1905
|
+
ctx: params.ctx,
|
|
1906
|
+
parsed,
|
|
1907
|
+
approval,
|
|
1908
|
+
respond
|
|
1909
|
+
});
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1634
1912
|
const pluginInteractionData = buildSlackPluginInteractionData({
|
|
1635
1913
|
actionId: parsed.actionId,
|
|
1636
1914
|
summary: parsed.actionSummary
|
|
1637
1915
|
});
|
|
1638
1916
|
if (pluginInteractionData && isSlackReplyActionId(parsed.actionId)) {
|
|
1639
|
-
if (await
|
|
1917
|
+
if (await handleSlackLegacyApprovalInteraction({
|
|
1640
1918
|
ctx: params.ctx,
|
|
1641
1919
|
parsed,
|
|
1642
1920
|
pluginInteractionData,
|
|
@@ -2293,6 +2571,66 @@ function registerSlackMemberEvents(params) {
|
|
|
2293
2571
|
});
|
|
2294
2572
|
}
|
|
2295
2573
|
//#endregion
|
|
2574
|
+
//#region extensions/slack/src/monitor/event-scope.ts
|
|
2575
|
+
function resolveSlackEventScope(params) {
|
|
2576
|
+
const context = params.context ?? {};
|
|
2577
|
+
if (params.identity.kind !== "enterprise") return context.isEnterpriseInstall === true ? {
|
|
2578
|
+
ok: false,
|
|
2579
|
+
reason: "enterprise_event_for_workspace_account"
|
|
2580
|
+
} : { ok: true };
|
|
2581
|
+
const apiAppId = normalizeOptionalString((params.body && typeof params.body === "object" ? params.body : {}).api_app_id);
|
|
2582
|
+
if (!apiAppId) return {
|
|
2583
|
+
ok: false,
|
|
2584
|
+
reason: "missing_api_app_id"
|
|
2585
|
+
};
|
|
2586
|
+
if (params.identity.apiAppId && apiAppId !== params.identity.apiAppId) return {
|
|
2587
|
+
ok: false,
|
|
2588
|
+
reason: "wrong_app"
|
|
2589
|
+
};
|
|
2590
|
+
if (context.isEnterpriseInstall !== true) return {
|
|
2591
|
+
ok: false,
|
|
2592
|
+
reason: "not_enterprise_install"
|
|
2593
|
+
};
|
|
2594
|
+
const enterpriseId = normalizeOptionalString(context.enterpriseId);
|
|
2595
|
+
if (!enterpriseId) return {
|
|
2596
|
+
ok: false,
|
|
2597
|
+
reason: "missing_enterprise_id"
|
|
2598
|
+
};
|
|
2599
|
+
if (enterpriseId !== params.identity.enterpriseId) return {
|
|
2600
|
+
ok: false,
|
|
2601
|
+
reason: "wrong_enterprise"
|
|
2602
|
+
};
|
|
2603
|
+
const teamId = normalizeOptionalString(context.teamId);
|
|
2604
|
+
if (!teamId) return {
|
|
2605
|
+
ok: false,
|
|
2606
|
+
reason: "missing_team_id"
|
|
2607
|
+
};
|
|
2608
|
+
if (!/^T[A-Z0-9]+$/i.test(teamId)) return {
|
|
2609
|
+
ok: false,
|
|
2610
|
+
reason: "invalid_team_id"
|
|
2611
|
+
};
|
|
2612
|
+
if (!params.client) return {
|
|
2613
|
+
ok: false,
|
|
2614
|
+
reason: "missing_listener_client"
|
|
2615
|
+
};
|
|
2616
|
+
const uploadCompletionClient = getSlackListenerUploadCompletionClient({
|
|
2617
|
+
listenerClient: params.client,
|
|
2618
|
+
teamId,
|
|
2619
|
+
clientOptions: params.clientOptions
|
|
2620
|
+
});
|
|
2621
|
+
return {
|
|
2622
|
+
ok: true,
|
|
2623
|
+
scope: {
|
|
2624
|
+
apiAppId,
|
|
2625
|
+
enterpriseId,
|
|
2626
|
+
teamId,
|
|
2627
|
+
isEnterpriseInstall: true,
|
|
2628
|
+
client: params.client,
|
|
2629
|
+
...uploadCompletionClient ? { uploadCompletionClient } : {}
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
//#endregion
|
|
2296
2634
|
//#region extensions/slack/src/monitor/events/message-subtype-handlers.ts
|
|
2297
2635
|
const SUBTYPE_HANDLER_REGISTRY = {
|
|
2298
2636
|
message_changed: {
|
|
@@ -2340,6 +2678,9 @@ function formatSlackInboundLogLine(params) {
|
|
|
2340
2678
|
function isSlackUserId(value) {
|
|
2341
2679
|
return /^[UW][A-Z0-9]+$/.test(value);
|
|
2342
2680
|
}
|
|
2681
|
+
function isBotAuthoredEnterpriseEvent(event) {
|
|
2682
|
+
return Boolean(normalizeOptionalString(event.bot_id)) || event.subtype === "bot_message";
|
|
2683
|
+
}
|
|
2343
2684
|
function addUserCandidate(candidates, value, botUserId) {
|
|
2344
2685
|
const id = normalizeOptionalString(value);
|
|
2345
2686
|
if (!id || id === botUserId || !isSlackUserId(id)) return;
|
|
@@ -2396,21 +2737,57 @@ function resolveAssistantMessageChangedInbound(params) {
|
|
|
2396
2737
|
event_ts: changed.event_ts,
|
|
2397
2738
|
assistant_thread: asOptionalRecord(message.assistant_thread) ?? asOptionalRecord(changed.assistant_thread),
|
|
2398
2739
|
files: Array.isArray(message.files) ? message.files : void 0,
|
|
2399
|
-
attachments: Array.isArray(message.attachments) ? message.attachments : void 0
|
|
2740
|
+
attachments: Array.isArray(message.attachments) ? message.attachments : void 0,
|
|
2741
|
+
blocks: Array.isArray(message.blocks) ? message.blocks : void 0
|
|
2400
2742
|
};
|
|
2401
2743
|
}
|
|
2402
2744
|
function registerSlackMessageEvents(params) {
|
|
2403
2745
|
const { ctx, handleSlackMessage } = params;
|
|
2404
|
-
const
|
|
2746
|
+
const resolveEventScope = (args) => {
|
|
2747
|
+
const resolved = resolveSlackEventScope({
|
|
2748
|
+
identity: ctx.installationIdentity,
|
|
2749
|
+
body: args.body,
|
|
2750
|
+
context: args.context,
|
|
2751
|
+
client: args.client,
|
|
2752
|
+
clientOptions: ctx.app.webClientOptions
|
|
2753
|
+
});
|
|
2754
|
+
if (!resolved.ok) {
|
|
2755
|
+
logVerbose(`slack: drop event (${resolved.reason})`);
|
|
2756
|
+
return null;
|
|
2757
|
+
}
|
|
2758
|
+
return resolved.scope;
|
|
2759
|
+
};
|
|
2760
|
+
const handleIncomingMessageEvent = async ({ event, body, context, client }) => {
|
|
2405
2761
|
try {
|
|
2762
|
+
const eventScope = resolveEventScope({
|
|
2763
|
+
body,
|
|
2764
|
+
context,
|
|
2765
|
+
client
|
|
2766
|
+
});
|
|
2767
|
+
if (eventScope === null) return;
|
|
2406
2768
|
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
2407
2769
|
const message = event;
|
|
2770
|
+
ctx.rememberSlackChannelType(message.channel, message.channel_type, eventScope);
|
|
2771
|
+
if (eventScope && isBotAuthoredEnterpriseEvent(message)) {
|
|
2772
|
+
logVerbose("slack: drop enterprise bot-authored message");
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (eventScope && message.subtype && message.subtype !== "file_share") {
|
|
2776
|
+
logVerbose(`slack: drop enterprise message subtype=${message.subtype}`);
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2408
2779
|
const assistantChangedInbound = resolveAssistantMessageChangedInbound({
|
|
2409
2780
|
event: message,
|
|
2410
2781
|
ctx
|
|
2411
2782
|
});
|
|
2412
2783
|
if (assistantChangedInbound) {
|
|
2413
|
-
await handleSlackMessage(assistantChangedInbound, {
|
|
2784
|
+
await handleSlackMessage(assistantChangedInbound, {
|
|
2785
|
+
source: "message",
|
|
2786
|
+
...eventScope ? {
|
|
2787
|
+
eventScope,
|
|
2788
|
+
awaitDispatch: true
|
|
2789
|
+
} : {}
|
|
2790
|
+
});
|
|
2414
2791
|
return;
|
|
2415
2792
|
}
|
|
2416
2793
|
if (message.subtype === "message_changed" && isSelfAttributedMessageChange({
|
|
@@ -2435,25 +2812,39 @@ function registerSlackMessageEvents(params) {
|
|
|
2435
2812
|
});
|
|
2436
2813
|
return;
|
|
2437
2814
|
}
|
|
2438
|
-
await handleSlackMessage(message, {
|
|
2815
|
+
await handleSlackMessage(message, {
|
|
2816
|
+
source: "message",
|
|
2817
|
+
...eventScope ? {
|
|
2818
|
+
eventScope,
|
|
2819
|
+
awaitDispatch: true
|
|
2820
|
+
} : {}
|
|
2821
|
+
});
|
|
2439
2822
|
} catch (err) {
|
|
2440
2823
|
ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
|
|
2441
2824
|
}
|
|
2442
2825
|
};
|
|
2443
|
-
ctx.app.event("message", async (
|
|
2444
|
-
await handleIncomingMessageEvent(
|
|
2445
|
-
event,
|
|
2446
|
-
body
|
|
2447
|
-
});
|
|
2826
|
+
ctx.app.event("message", async (args) => {
|
|
2827
|
+
await handleIncomingMessageEvent(args);
|
|
2448
2828
|
});
|
|
2449
|
-
ctx.app.event("app_mention", async (
|
|
2829
|
+
ctx.app.event("app_mention", async (args) => {
|
|
2830
|
+
const { event, body, context, client } = args;
|
|
2450
2831
|
try {
|
|
2832
|
+
const eventScope = resolveEventScope({
|
|
2833
|
+
body,
|
|
2834
|
+
context,
|
|
2835
|
+
client
|
|
2836
|
+
});
|
|
2837
|
+
if (eventScope === null) return;
|
|
2451
2838
|
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
2452
2839
|
const mention = event;
|
|
2840
|
+
if (eventScope && isBotAuthoredEnterpriseEvent(mention)) {
|
|
2841
|
+
logVerbose("slack: drop enterprise bot-authored app_mention");
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2453
2844
|
const channelType = normalizeSlackChannelType(mention.channel_type, mention.channel);
|
|
2454
2845
|
if (channelType === "im" || channelType === "mpim") return;
|
|
2455
2846
|
slackInboundLog.info(formatSlackInboundLogLine({
|
|
2456
|
-
workspaceId: ctx.teamId,
|
|
2847
|
+
workspaceId: eventScope?.teamId ?? ctx.teamId,
|
|
2457
2848
|
channelId: mention.channel,
|
|
2458
2849
|
channelType: channelType ?? "channel",
|
|
2459
2850
|
userId: normalizeOptionalString(mention.user) ?? "unknown",
|
|
@@ -2462,7 +2853,11 @@ function registerSlackMessageEvents(params) {
|
|
|
2462
2853
|
}));
|
|
2463
2854
|
await handleSlackMessage(mention, {
|
|
2464
2855
|
source: "app_mention",
|
|
2465
|
-
wasMentioned: true
|
|
2856
|
+
wasMentioned: true,
|
|
2857
|
+
...eventScope ? {
|
|
2858
|
+
eventScope,
|
|
2859
|
+
awaitDispatch: true
|
|
2860
|
+
} : {}
|
|
2466
2861
|
});
|
|
2467
2862
|
} catch (err) {
|
|
2468
2863
|
ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
|
|
@@ -2591,6 +2986,7 @@ function registerSlackMonitorEvents(params) {
|
|
|
2591
2986
|
ctx: params.ctx,
|
|
2592
2987
|
handleSlackMessage: params.handleSlackMessage
|
|
2593
2988
|
});
|
|
2989
|
+
if (params.ctx.installationIdentity.kind === "enterprise") return;
|
|
2594
2990
|
registerSlackReactionEvents({
|
|
2595
2991
|
ctx: params.ctx,
|
|
2596
2992
|
trackEvent: params.trackEvent
|
|
@@ -2609,6 +3005,7 @@ function registerSlackMonitorEvents(params) {
|
|
|
2609
3005
|
});
|
|
2610
3006
|
registerSlackHomeEvents({
|
|
2611
3007
|
ctx: params.ctx,
|
|
3008
|
+
slashCommandName: params.appHomeSlashCommandName,
|
|
2612
3009
|
trackEvent: params.trackEvent
|
|
2613
3010
|
});
|
|
2614
3011
|
registerSlackInteractionEvents({
|
|
@@ -2640,12 +3037,12 @@ const deliveredMessages = createPersistentDedupeCache({
|
|
|
2640
3037
|
}
|
|
2641
3038
|
}
|
|
2642
3039
|
});
|
|
2643
|
-
function makeKey(accountId, channelId, ts) {
|
|
2644
|
-
return `${accountId}:${channelId}:${ts}`;
|
|
3040
|
+
function makeKey(accountId, channelId, ts, teamId) {
|
|
3041
|
+
return `${accountId}:${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
|
|
2645
3042
|
}
|
|
2646
3043
|
async function hasSlackInboundMessageDelivery(params) {
|
|
2647
3044
|
if (!params.accountId || !params.channelId || !params.ts) return false;
|
|
2648
|
-
return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts));
|
|
3045
|
+
return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts, params.teamId));
|
|
2649
3046
|
}
|
|
2650
3047
|
async function recordSlackInboundMessageDeliveries(params) {
|
|
2651
3048
|
if (!params.accountId || params.messages.length === 0) return;
|
|
@@ -2653,7 +3050,7 @@ async function recordSlackInboundMessageDeliveries(params) {
|
|
|
2653
3050
|
const keys = /* @__PURE__ */ new Set();
|
|
2654
3051
|
for (const message of params.messages) {
|
|
2655
3052
|
if (!message.channel || !message.ts) continue;
|
|
2656
|
-
keys.add(makeKey(params.accountId, message.channel, message.ts));
|
|
3053
|
+
keys.add(makeKey(params.accountId, message.channel, message.ts, params.teamId));
|
|
2657
3054
|
}
|
|
2658
3055
|
await Promise.all(Array.from(keys, (key) => deliveredMessages.register(key, { deliveredAt }, { at: deliveredAt })));
|
|
2659
3056
|
}
|
|
@@ -2668,17 +3065,18 @@ function isSlackDirectMessageChannel(channelId) {
|
|
|
2668
3065
|
function isTopLevelSlackMessage(message) {
|
|
2669
3066
|
return !message.thread_ts && !message.parent_user_id;
|
|
2670
3067
|
}
|
|
2671
|
-
function buildTopLevelSlackConversationKey(message, accountId) {
|
|
3068
|
+
function buildTopLevelSlackConversationKey(message, accountId, teamId) {
|
|
2672
3069
|
if (!isTopLevelSlackMessage(message)) return null;
|
|
2673
3070
|
const senderId = resolveSlackSenderId(message);
|
|
2674
3071
|
if (!senderId) return null;
|
|
2675
|
-
return `slack:${accountId}:${message.channel}:${senderId}`;
|
|
3072
|
+
return `slack:${accountId}:${teamId ? `${teamId}:` : ""}${message.channel}:${senderId}`;
|
|
2676
3073
|
}
|
|
2677
|
-
function buildSlackDebounceKey(message, accountId) {
|
|
3074
|
+
function buildSlackDebounceKey(message, accountId, teamId) {
|
|
2678
3075
|
const senderId = resolveSlackSenderId(message);
|
|
2679
3076
|
if (!senderId) return null;
|
|
2680
3077
|
const messageTs = message.ts ?? message.event_ts;
|
|
2681
|
-
|
|
3078
|
+
const threadKey = message.thread_ts ? `${message.channel}:${message.thread_ts}` : message.parent_user_id && messageTs ? `${message.channel}:maybe-thread:${messageTs}` : messageTs && !isSlackDirectMessageChannel(message.channel) ? `${message.channel}:${messageTs}` : message.channel;
|
|
3079
|
+
return `slack:${accountId}:${teamId ? `${teamId}:` : ""}${threadKey}:${senderId}`;
|
|
2682
3080
|
}
|
|
2683
3081
|
//#endregion
|
|
2684
3082
|
//#region extensions/slack/src/monitor/thread-resolution.ts
|
|
@@ -2782,7 +3180,7 @@ function createSlackThreadTsResolver(params) {
|
|
|
2782
3180
|
}
|
|
2783
3181
|
//#endregion
|
|
2784
3182
|
//#region extensions/slack/src/monitor/message-handler.ts
|
|
2785
|
-
const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-
|
|
3183
|
+
const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-f0eJJoJL.js"));
|
|
2786
3184
|
function createSlackDispatchCompletion() {
|
|
2787
3185
|
let resolve;
|
|
2788
3186
|
let reject;
|
|
@@ -2799,14 +3197,7 @@ const APP_MENTION_RETRY_TTL_MS = 6e4;
|
|
|
2799
3197
|
const RETRYABLE_FLUSH_MAX_ATTEMPTS = 3;
|
|
2800
3198
|
const RETRYABLE_FLUSH_RETRY_DELAY_MS = 1e3;
|
|
2801
3199
|
const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
|
|
2802
|
-
var SlackRetryableInboundError = class extends Error {
|
|
2803
|
-
constructor(message, options) {
|
|
2804
|
-
super(message, options);
|
|
2805
|
-
this.name = "SlackRetryableInboundError";
|
|
2806
|
-
}
|
|
2807
|
-
};
|
|
2808
3200
|
function isRetryableSlackInboundError(error) {
|
|
2809
|
-
if (error instanceof SlackRetryableInboundError) return true;
|
|
2810
3201
|
return collectErrorGraphCandidates(error, (current) => [current.cause, current.error]).some((candidate) => REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE.test(formatErrorMessage(candidate)));
|
|
2811
3202
|
}
|
|
2812
3203
|
function shouldDebounceSlackMessage(message, cfg) {
|
|
@@ -2816,20 +3207,20 @@ function shouldDebounceSlackMessage(message, cfg) {
|
|
|
2816
3207
|
hasMedia: Boolean(message.files && message.files.length > 0)
|
|
2817
3208
|
});
|
|
2818
3209
|
}
|
|
2819
|
-
function buildSeenMessageKey(channelId, ts) {
|
|
3210
|
+
function buildSeenMessageKey(channelId, ts, teamId) {
|
|
2820
3211
|
if (!channelId || !ts) return null;
|
|
2821
|
-
return `${channelId}:${ts}`;
|
|
3212
|
+
return `${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
|
|
2822
3213
|
}
|
|
2823
3214
|
function createSlackMessageHandler(params) {
|
|
2824
3215
|
const { ctx, account, trackEvent } = params;
|
|
2825
3216
|
const { debounceMs, debouncer } = createChannelInboundDebouncer({
|
|
2826
3217
|
cfg: ctx.cfg,
|
|
2827
3218
|
channel: "slack",
|
|
2828
|
-
buildKey: (entry) => buildSlackDebounceKey(entry.message, ctx.accountId),
|
|
2829
|
-
shouldDebounce: (entry) => shouldDebounceSlackMessage(entry.message, ctx.cfg),
|
|
3219
|
+
buildKey: (entry) => buildSlackDebounceKey(entry.message, ctx.accountId, entry.opts.eventScope?.teamId),
|
|
3220
|
+
shouldDebounce: (entry) => !entry.opts.eventScope && shouldDebounceSlackMessage(entry.message, ctx.cfg),
|
|
2830
3221
|
onFlush: async (entries) => {
|
|
2831
3222
|
const retryEntries = (sourceError) => {
|
|
2832
|
-
if (!isRetryableSlackInboundError(sourceError)) return false;
|
|
3223
|
+
if (!isRetryableSlackInboundError(sourceError) || entries.some((entry) => entry.opts.eventScope)) return false;
|
|
2833
3224
|
const nextEntries = entries.map((entry) => {
|
|
2834
3225
|
if (entry.opts.dispatchCompletion) return null;
|
|
2835
3226
|
const retryAttempt = entry.opts.retryAttempt ?? 0;
|
|
@@ -2856,8 +3247,9 @@ function createSlackMessageHandler(params) {
|
|
|
2856
3247
|
await (async () => {
|
|
2857
3248
|
const last = entries.at(-1);
|
|
2858
3249
|
if (!last) return;
|
|
2859
|
-
const
|
|
2860
|
-
const
|
|
3250
|
+
const teamId = last.opts.eventScope?.teamId;
|
|
3251
|
+
const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId, teamId);
|
|
3252
|
+
const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId, teamId);
|
|
2861
3253
|
if (flushedKey && topLevelConversationKey) {
|
|
2862
3254
|
const pendingKeys = pendingTopLevelDebounceKeys.get(topLevelConversationKey);
|
|
2863
3255
|
if (pendingKeys) {
|
|
@@ -2871,7 +3263,7 @@ function createSlackMessageHandler(params) {
|
|
|
2871
3263
|
...last.message,
|
|
2872
3264
|
text: combinedText
|
|
2873
3265
|
};
|
|
2874
|
-
const seenMessageKey = buildSeenMessageKey(last.message.channel, last.message.ts);
|
|
3266
|
+
const seenMessageKey = buildSeenMessageKey(last.message.channel, last.message.ts, last.opts.eventScope?.teamId);
|
|
2875
3267
|
try {
|
|
2876
3268
|
const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
|
|
2877
3269
|
const { dispatchCompletion: _completion, awaitDispatch: _awaitDispatch, ...lastOpts } = last.opts;
|
|
@@ -2917,20 +3309,22 @@ function createSlackMessageHandler(params) {
|
|
|
2917
3309
|
await dispatchPreparedSlackMessage(prepared);
|
|
2918
3310
|
await recordSlackInboundMessageDeliveries({
|
|
2919
3311
|
accountId: ctx.accountId,
|
|
3312
|
+
...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
|
|
2920
3313
|
messages: entries.map((entry) => entry.message)
|
|
2921
3314
|
});
|
|
2922
3315
|
} catch (error) {
|
|
2923
3316
|
if (!isRetryableSlackInboundError(error)) await recordSlackInboundMessageDeliveries({
|
|
2924
3317
|
accountId: ctx.accountId,
|
|
3318
|
+
...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
|
|
2925
3319
|
messages: entries.map((entry) => entry.message)
|
|
2926
3320
|
});
|
|
2927
3321
|
throw error;
|
|
2928
3322
|
}
|
|
2929
3323
|
} catch (error) {
|
|
2930
3324
|
if (isRetryableSlackInboundError(error)) for (const entry of entries) {
|
|
2931
|
-
const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts);
|
|
3325
|
+
const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts, entry.opts.eventScope?.teamId);
|
|
2932
3326
|
if (entrySeenKey) appMentionDispatchedKeys.delete(entrySeenKey);
|
|
2933
|
-
ctx.releaseSeenMessage(entry.message.channel, entry.message.ts);
|
|
3327
|
+
ctx.releaseSeenMessage(entry.message.channel, entry.message.ts, entry.opts.eventScope);
|
|
2934
3328
|
}
|
|
2935
3329
|
throw error;
|
|
2936
3330
|
}
|
|
@@ -2980,25 +3374,28 @@ function createSlackMessageHandler(params) {
|
|
|
2980
3374
|
async function enqueueSlackMessage(message, opts) {
|
|
2981
3375
|
if (opts.source === "message" && message.type !== "message") return;
|
|
2982
3376
|
if (opts.source === "message" && message.subtype && message.subtype !== "file_share" && message.subtype !== "bot_message" && message.subtype !== "thread_broadcast") return;
|
|
2983
|
-
|
|
3377
|
+
ctx.rememberSlackChannelType(message.channel, message.channel_type, opts.eventScope);
|
|
3378
|
+
const seenMessageKey = buildSeenMessageKey(message.channel, message.ts, opts.eventScope?.teamId);
|
|
2984
3379
|
if (seenMessageKey && await hasSlackInboundMessageDelivery({
|
|
2985
3380
|
accountId: ctx.accountId,
|
|
2986
3381
|
channelId: message.channel,
|
|
2987
|
-
ts: message.ts
|
|
3382
|
+
ts: message.ts,
|
|
3383
|
+
...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
|
|
2988
3384
|
})) return;
|
|
2989
|
-
const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts) : false;
|
|
3385
|
+
const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts, opts.eventScope) : false;
|
|
2990
3386
|
if (seenMessageKey && opts.source === "message" && !wasSeen) rememberAppMentionRetryKey(seenMessageKey);
|
|
2991
3387
|
if (seenMessageKey && wasSeen) {
|
|
2992
3388
|
if (opts.source !== "app_mention" || !consumeAppMentionRetryKey(seenMessageKey)) return;
|
|
2993
3389
|
}
|
|
2994
3390
|
trackEvent?.();
|
|
2995
|
-
const resolvedMessage = await threadTsResolver.resolve({
|
|
3391
|
+
const resolvedMessage = await (opts.eventScope ? createSlackThreadTsResolver({ client: opts.eventScope.client }) : threadTsResolver).resolve({
|
|
2996
3392
|
message,
|
|
2997
3393
|
source: opts.source
|
|
2998
3394
|
});
|
|
2999
|
-
const
|
|
3000
|
-
const
|
|
3001
|
-
const
|
|
3395
|
+
const teamId = opts.eventScope?.teamId;
|
|
3396
|
+
const debounceKey = buildSlackDebounceKey(resolvedMessage, ctx.accountId, teamId);
|
|
3397
|
+
const conversationKey = buildTopLevelSlackConversationKey(resolvedMessage, ctx.accountId, teamId);
|
|
3398
|
+
const canDebounce = !opts.eventScope && debounceMs > 0 && shouldDebounceSlackMessage(resolvedMessage, ctx.cfg);
|
|
3002
3399
|
if (!canDebounce && conversationKey) {
|
|
3003
3400
|
const pendingKeys = pendingTopLevelDebounceKeys.get(conversationKey);
|
|
3004
3401
|
if (pendingKeys && pendingKeys.size > 0) {
|
|
@@ -3389,8 +3786,11 @@ async function gracefulStopSlackApp(app) {
|
|
|
3389
3786
|
}
|
|
3390
3787
|
function formatSlackResolvedLabel(params) {
|
|
3391
3788
|
const extras = params.extra?.filter(Boolean) ?? [];
|
|
3392
|
-
const
|
|
3393
|
-
|
|
3789
|
+
const display = params.name ?? params.id;
|
|
3790
|
+
if (params.input === params.id && !params.name && extras.length === 0) return null;
|
|
3791
|
+
const details = [...params.input === params.id || display === params.id ? [] : [`id:${params.id}`], ...extras];
|
|
3792
|
+
const suffix = details.length > 0 ? ` (${details.join(", ")})` : "";
|
|
3793
|
+
return `${params.input}→${display}${suffix}`;
|
|
3394
3794
|
}
|
|
3395
3795
|
function formatSlackChannelResolved(entry) {
|
|
3396
3796
|
const id = entry.id ?? entry.input;
|
|
@@ -3511,6 +3911,25 @@ function createSlackExternalArgMenuStore() {
|
|
|
3511
3911
|
};
|
|
3512
3912
|
}
|
|
3513
3913
|
//#endregion
|
|
3914
|
+
//#region extensions/slack/src/monitor/response-url-budget.ts
|
|
3915
|
+
const SLACK_RESPONSE_URL_MAX_CALLS = 5;
|
|
3916
|
+
var SlackResponseAlreadyReportedError = class extends Error {};
|
|
3917
|
+
function isSlackResponseAlreadyReportedError(error) {
|
|
3918
|
+
return error instanceof SlackResponseAlreadyReportedError;
|
|
3919
|
+
}
|
|
3920
|
+
/** Count every response_url attempt, including requests Slack rejects. */
|
|
3921
|
+
function createSlackResponseUrlBudget(respond, maxCalls = SLACK_RESPONSE_URL_MAX_CALLS) {
|
|
3922
|
+
let remaining = maxCalls;
|
|
3923
|
+
return {
|
|
3924
|
+
remaining: () => remaining,
|
|
3925
|
+
respond: async (payload) => {
|
|
3926
|
+
if (remaining <= 0) throw new Error(`Slack response_url cannot be used more than ${String(maxCalls)} times.`);
|
|
3927
|
+
remaining -= 1;
|
|
3928
|
+
return await respond(payload);
|
|
3929
|
+
}
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3932
|
+
//#endregion
|
|
3514
3933
|
//#region extensions/slack/src/monitor/room-context.ts
|
|
3515
3934
|
function resolveSlackRoomContextHints(params) {
|
|
3516
3935
|
const untrustedChannelMetadata = params.isRoomish ? buildUntrustedChannelMetadata({
|
|
@@ -3541,7 +3960,7 @@ const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
|
|
|
3541
3960
|
const SLACK_HEADER_TEXT_MAX = 150;
|
|
3542
3961
|
const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = 47;
|
|
3543
3962
|
const loadSlashCommandsRuntime = createLazyRuntimeModule(() => import("./slash-commands.runtime-BTTMlmwy.js"));
|
|
3544
|
-
const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-
|
|
3963
|
+
const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-C0Otuf_T.js"));
|
|
3545
3964
|
const loadSlackPluginCommandsRuntime = createLazyRuntimeModule(() => import("./slash-plugin-commands.runtime-Dosfcx3X.js"));
|
|
3546
3965
|
const loadSlashSkillCommandsRuntime = createLazyRuntimeModule(() => import("./slash-skill-commands.runtime-CD1J2ODY.js"));
|
|
3547
3966
|
function resolveSlackCommandMenuModelContext(params) {
|
|
@@ -3771,7 +4190,12 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
3771
4190
|
let supportsExternalArgMenus = typeof ctx.app.options === "function";
|
|
3772
4191
|
const slashCommand = resolveSlackSlashCommandConfig(ctx.slashCommand ?? account.config.slashCommand);
|
|
3773
4192
|
const handleSlashCommand = async (p) => {
|
|
3774
|
-
const { command, ack, respond, body, prompt, commandArgs, commandDefinition } = p;
|
|
4193
|
+
const { command, ack, respond: respondWithoutBudget, body, prompt, commandArgs, commandDefinition } = p;
|
|
4194
|
+
const responseBudget = p.responseTransport === "web-api" ? {
|
|
4195
|
+
respond: respondWithoutBudget,
|
|
4196
|
+
remaining: () => void 0
|
|
4197
|
+
} : createSlackResponseUrlBudget(respondWithoutBudget);
|
|
4198
|
+
const respond = responseBudget.respond;
|
|
3775
4199
|
const cfg = getRuntimeConfigSnapshot() ?? ctx.cfg;
|
|
3776
4200
|
try {
|
|
3777
4201
|
if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
@@ -3887,7 +4311,7 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
3887
4311
|
modeWhenAccessGroupsOff: "configured"
|
|
3888
4312
|
});
|
|
3889
4313
|
const senderGate = slashIngress.senderAccess.gate;
|
|
3890
|
-
if (
|
|
4314
|
+
if (isRoomish && senderGate?.allowed === false) {
|
|
3891
4315
|
await respond({
|
|
3892
4316
|
text: "You are not authorized to use this command here.",
|
|
3893
4317
|
response_type: "ephemeral"
|
|
@@ -4062,15 +4486,19 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4062
4486
|
cfg,
|
|
4063
4487
|
channel: "slack",
|
|
4064
4488
|
accountId: route.accountId
|
|
4065
|
-
})
|
|
4489
|
+
}),
|
|
4490
|
+
responseBudget
|
|
4066
4491
|
});
|
|
4067
4492
|
};
|
|
4493
|
+
const pendingSlashReplies = [];
|
|
4068
4494
|
const { counts } = await dispatchReplyWithDispatcher({
|
|
4069
4495
|
ctx: ctxPayload,
|
|
4070
4496
|
cfg,
|
|
4071
4497
|
dispatcherOptions: {
|
|
4072
4498
|
...replyPipeline,
|
|
4073
|
-
deliver: async (payload) =>
|
|
4499
|
+
deliver: async (payload) => {
|
|
4500
|
+
pendingSlashReplies.push(payload);
|
|
4501
|
+
},
|
|
4074
4502
|
onError: (err, info) => {
|
|
4075
4503
|
runtime.error?.(danger(`slack slash ${info.kind} reply failed: ${formatSlackError(err)}`));
|
|
4076
4504
|
}
|
|
@@ -4080,10 +4508,11 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4080
4508
|
onModelSelected
|
|
4081
4509
|
}
|
|
4082
4510
|
});
|
|
4083
|
-
if (
|
|
4511
|
+
if (pendingSlashReplies.length > 0) await deliverSlashPayloads(pendingSlashReplies);
|
|
4512
|
+
else if (counts.final + counts.tool + counts.block === 0) await deliverSlashPayloads([]);
|
|
4084
4513
|
} catch (err) {
|
|
4085
4514
|
runtime.error?.(danger(`slack slash handler failed: ${formatErrorMessage(err)}`));
|
|
4086
|
-
await respond({
|
|
4515
|
+
if (!isSlackResponseAlreadyReportedError(err) && responseBudget.remaining() !== 0) await respond({
|
|
4087
4516
|
text: "Sorry, something went wrong handling that command.",
|
|
4088
4517
|
response_type: "ephemeral"
|
|
4089
4518
|
});
|
|
@@ -4117,7 +4546,16 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4117
4546
|
nativeCommands.push(pluginCommand);
|
|
4118
4547
|
}
|
|
4119
4548
|
}
|
|
4120
|
-
if (
|
|
4549
|
+
if (slashCommand.enabled) ctx.app.command(buildSlackSlashCommandMatcher(slashCommand.name), async ({ command, ack, respond, body }) => {
|
|
4550
|
+
await handleSlashCommand({
|
|
4551
|
+
command,
|
|
4552
|
+
ack,
|
|
4553
|
+
respond,
|
|
4554
|
+
body,
|
|
4555
|
+
prompt: command.text?.trim() ?? ""
|
|
4556
|
+
});
|
|
4557
|
+
});
|
|
4558
|
+
else if (nativeCommands.length > 0) {
|
|
4121
4559
|
if (!slashCommandsRuntime) throw new Error("Missing commands runtime for native Slack commands.");
|
|
4122
4560
|
for (const command of nativeCommands) ctx.app.command(`/${command.name}`, async ({ command: cmd, ack, respond, body }) => {
|
|
4123
4561
|
const commandDefinition = slashCommandsRuntime.findCommandByNativeName(command.name, "slack");
|
|
@@ -4134,17 +4572,12 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4134
4572
|
commandDefinition: commandDefinition ?? void 0
|
|
4135
4573
|
});
|
|
4136
4574
|
});
|
|
4137
|
-
} else
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
prompt: command.text?.trim() ?? ""
|
|
4144
|
-
});
|
|
4145
|
-
});
|
|
4146
|
-
else logVerbose("slack: slash commands disabled");
|
|
4147
|
-
if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) return;
|
|
4575
|
+
} else logVerbose("slack: slash commands disabled");
|
|
4576
|
+
const registration = nativeCommands.length > 0 ? { mode: "native" } : slashCommand.enabled ? {
|
|
4577
|
+
mode: "single",
|
|
4578
|
+
name: slashCommand.name
|
|
4579
|
+
} : { mode: "disabled" };
|
|
4580
|
+
if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) return registration;
|
|
4148
4581
|
const registerArgOptions = () => {
|
|
4149
4582
|
const appWithOptions = ctx.app;
|
|
4150
4583
|
if (typeof appWithOptions.options !== "function") return;
|
|
@@ -4190,21 +4623,24 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4190
4623
|
}
|
|
4191
4624
|
const registerArgAction = (actionId) => {
|
|
4192
4625
|
ctx.app.action(actionId, async (args) => {
|
|
4193
|
-
const { ack, body
|
|
4626
|
+
const { ack, body } = args;
|
|
4627
|
+
const respond = args.respond;
|
|
4194
4628
|
const action = args.action;
|
|
4195
4629
|
await ack();
|
|
4196
4630
|
if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
4197
4631
|
runtime.log?.("slack: drop slash arg action payload (mismatched app/team)");
|
|
4198
4632
|
return;
|
|
4199
4633
|
}
|
|
4200
|
-
const respondFn = respond ?? (async (
|
|
4634
|
+
const respondFn = respond ?? (async (message) => {
|
|
4201
4635
|
if (!body.channel?.id || !body.user?.id) return;
|
|
4636
|
+
const payload = typeof message === "string" ? { text: message } : message;
|
|
4202
4637
|
await ctx.app.client.chat.postEphemeral({
|
|
4203
4638
|
token: ctx.botToken,
|
|
4204
4639
|
channel: body.channel.id,
|
|
4205
4640
|
user: body.user.id,
|
|
4206
|
-
text: payload.text,
|
|
4207
|
-
blocks: payload.blocks
|
|
4641
|
+
text: payload.text ?? "",
|
|
4642
|
+
...payload.blocks ? { blocks: payload.blocks } : {},
|
|
4643
|
+
...typeof payload.mrkdwn === "boolean" ? { mrkdwn: payload.mrkdwn } : {}
|
|
4208
4644
|
});
|
|
4209
4645
|
});
|
|
4210
4646
|
const parsed = parseSlackCommandArgValue(action?.value ?? action?.selected_option?.value);
|
|
@@ -4240,6 +4676,7 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4240
4676
|
command: commandPayload,
|
|
4241
4677
|
ack: async () => {},
|
|
4242
4678
|
respond: respondFn,
|
|
4679
|
+
responseTransport: respond ? "response-url" : "web-api",
|
|
4243
4680
|
body,
|
|
4244
4681
|
prompt,
|
|
4245
4682
|
commandArgs,
|
|
@@ -4248,6 +4685,7 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4248
4685
|
});
|
|
4249
4686
|
};
|
|
4250
4687
|
registerArgAction(SLACK_COMMAND_ARG_ACTION_LISTENER);
|
|
4688
|
+
return registration;
|
|
4251
4689
|
}
|
|
4252
4690
|
//#endregion
|
|
4253
4691
|
//#region extensions/slack/src/monitor/provider.ts
|
|
@@ -4262,7 +4700,7 @@ async function getSlackBoltInterop() {
|
|
|
4262
4700
|
}
|
|
4263
4701
|
return slackBoltInterop;
|
|
4264
4702
|
}
|
|
4265
|
-
const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-
|
|
4703
|
+
const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-B-6LFUdz.js"));
|
|
4266
4704
|
const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
4267
4705
|
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
|
|
4268
4706
|
function resolveStableSlackUserIdEntry(raw) {
|
|
@@ -4336,6 +4774,19 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4336
4774
|
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
|
4337
4775
|
const mainKey = normalizeMainKey(sessionCfg?.mainKey);
|
|
4338
4776
|
const slackMode = opts.mode ?? account.config.mode ?? "socket";
|
|
4777
|
+
const enterpriseOrgInstall = account.config.enterpriseOrgInstall === true;
|
|
4778
|
+
if (enterpriseOrgInstall && slackMode === "relay") throw new Error(`Slack Enterprise Grid org account "${account.accountId}" requires direct socket or HTTP delivery; relay mode is unsupported`);
|
|
4779
|
+
if (enterpriseOrgInstall && account.config.execApprovals?.enabled === true) throw new Error(`Slack Enterprise Grid org account "${account.accountId}" does not support Slack-native exec approvals`);
|
|
4780
|
+
if (enterpriseOrgInstall) {
|
|
4781
|
+
assertEnterpriseSlackPolicyConfig({
|
|
4782
|
+
config: account.config,
|
|
4783
|
+
accountId: account.accountId
|
|
4784
|
+
});
|
|
4785
|
+
assertNoEnterpriseSlackBindings({
|
|
4786
|
+
cfg,
|
|
4787
|
+
accountId: account.accountId
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4339
4790
|
const slackWebhookPath = normalizeSlackWebhookPath(account.config.webhookPath);
|
|
4340
4791
|
const signingSecret = normalizeResolvedSecretInputString({
|
|
4341
4792
|
value: account.config.signingSecret,
|
|
@@ -4363,6 +4814,12 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4363
4814
|
cfg,
|
|
4364
4815
|
accountId: account.accountId
|
|
4365
4816
|
});
|
|
4817
|
+
if (enterpriseOrgInstall) assertEnterpriseSlackDmPolicy({
|
|
4818
|
+
accountId: account.accountId,
|
|
4819
|
+
dmEnabled,
|
|
4820
|
+
dmPolicy,
|
|
4821
|
+
allowFrom
|
|
4822
|
+
});
|
|
4366
4823
|
const groupDmEnabled = dmConfig?.groupEnabled ?? false;
|
|
4367
4824
|
const groupDmChannels = dmConfig?.groupChannels;
|
|
4368
4825
|
let channelsConfig = slackCfg.channels;
|
|
@@ -4432,24 +4889,22 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4432
4889
|
}) : () => {};
|
|
4433
4890
|
let botUserId = "";
|
|
4434
4891
|
let botId = "";
|
|
4435
|
-
let teamId = "";
|
|
4436
|
-
let apiAppId = "";
|
|
4437
4892
|
const expectedApiAppIdFromAppToken = slackMode === "socket" ? parseApiAppIdFromAppToken(appToken) : void 0;
|
|
4438
4893
|
let authTestFailed = false;
|
|
4439
4894
|
let authTestError;
|
|
4440
4895
|
let authIdentityWarning;
|
|
4896
|
+
let authTestIdentity;
|
|
4441
4897
|
try {
|
|
4442
4898
|
const auth = await app.client.auth.test();
|
|
4443
4899
|
const authUserId = normalizeOptionalString(auth.user_id) ?? "";
|
|
4444
4900
|
botId = normalizeOptionalString(auth.bot_id) ?? "";
|
|
4445
4901
|
botUserId = botId ? authUserId : "";
|
|
4446
|
-
|
|
4447
|
-
apiAppId = auth.api_app_id ?? "";
|
|
4902
|
+
authTestIdentity = auth;
|
|
4448
4903
|
authIdentityWarning = formatSlackBotTokenIdentityWarning({
|
|
4449
4904
|
auth,
|
|
4450
4905
|
accountId: account.accountId
|
|
4451
4906
|
});
|
|
4452
|
-
if (!authUserId) {
|
|
4907
|
+
if (!authUserId && !enterpriseOrgInstall) {
|
|
4453
4908
|
authTestFailed = true;
|
|
4454
4909
|
authTestError = "auth.test returned no user_id";
|
|
4455
4910
|
}
|
|
@@ -4457,9 +4912,17 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4457
4912
|
authTestFailed = true;
|
|
4458
4913
|
authTestError = err instanceof Error ? err.message : String(err);
|
|
4459
4914
|
}
|
|
4915
|
+
const installationIdentity = resolveSlackInstallationIdentity({
|
|
4916
|
+
enterpriseOrgInstall,
|
|
4917
|
+
auth: authTestFailed ? void 0 : authTestIdentity,
|
|
4918
|
+
authError: authTestError,
|
|
4919
|
+
transportApiAppId: expectedApiAppIdFromAppToken
|
|
4920
|
+
});
|
|
4921
|
+
const teamId = installationIdentity.kind === "workspace" ? installationIdentity.teamId : "";
|
|
4922
|
+
const apiAppId = installationIdentity.kind === "degraded" ? "" : installationIdentity.apiAppId ?? "";
|
|
4460
4923
|
if (authTestFailed) runtime.log?.(warn(`[${account.accountId}] slack auth.test failed at boot (${authTestError ?? "unknown error"}); explicit bot-mention detection will be disabled until restart with a valid bot token; required-mention channels will fail closed without another trusted activation signal`));
|
|
4461
4924
|
if (authIdentityWarning) runtime.log?.(warn(authIdentityWarning));
|
|
4462
|
-
if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) runtime.error?.(`slack token mismatch: bot token
|
|
4925
|
+
if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) runtime.error?.(`slack token mismatch: bot token app_id=${apiAppId} but app token looks like app_id=${expectedApiAppIdFromAppToken}`);
|
|
4463
4926
|
const ctx = createSlackMonitorContext({
|
|
4464
4927
|
cfg,
|
|
4465
4928
|
accountId: account.accountId,
|
|
@@ -4471,6 +4934,7 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4471
4934
|
botId,
|
|
4472
4935
|
teamId,
|
|
4473
4936
|
apiAppId,
|
|
4937
|
+
installationIdentity,
|
|
4474
4938
|
historyLimit,
|
|
4475
4939
|
dmHistoryLimit,
|
|
4476
4940
|
sessionScope,
|
|
@@ -4509,7 +4973,7 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4509
4973
|
account,
|
|
4510
4974
|
trackEvent
|
|
4511
4975
|
});
|
|
4512
|
-
if (isSlackAnyNativeApprovalClientEnabled({
|
|
4976
|
+
if (installationIdentity.kind !== "enterprise" && isSlackAnyNativeApprovalClientEnabled({
|
|
4513
4977
|
cfg,
|
|
4514
4978
|
accountId: account.accountId
|
|
4515
4979
|
})) registerChannelRuntimeContext({
|
|
@@ -4523,15 +4987,16 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4523
4987
|
},
|
|
4524
4988
|
abortSignal: opts.abortSignal
|
|
4525
4989
|
});
|
|
4526
|
-
|
|
4990
|
+
const commandRegistration = installationIdentity.kind === "enterprise" ? { mode: "disabled" } : await registerSlackMonitorSlashCommands({
|
|
4527
4991
|
ctx,
|
|
4528
4992
|
account,
|
|
4529
|
-
handleSlackMessage,
|
|
4530
4993
|
trackEvent
|
|
4531
4994
|
});
|
|
4532
|
-
|
|
4995
|
+
registerSlackMonitorEvents({
|
|
4533
4996
|
ctx,
|
|
4534
4997
|
account,
|
|
4998
|
+
handleSlackMessage,
|
|
4999
|
+
appHomeSlashCommandName: commandRegistration.mode === "single" ? commandRegistration.name : void 0,
|
|
4535
5000
|
trackEvent
|
|
4536
5001
|
});
|
|
4537
5002
|
if (slackMode === "http" && slackHttpHandler) unregisterHttpHandler = registerSlackHttpHandler({
|
|
@@ -4540,7 +5005,7 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4540
5005
|
log: runtime.log,
|
|
4541
5006
|
accountId: account.accountId
|
|
4542
5007
|
});
|
|
4543
|
-
if (resolveToken) (async () => {
|
|
5008
|
+
if (resolveToken && installationIdentity.kind !== "enterprise") (async () => {
|
|
4544
5009
|
if (opts.abortSignal?.aborted) return;
|
|
4545
5010
|
if (channelsConfig && Object.keys(channelsConfig).length > 0) try {
|
|
4546
5011
|
const entries = Object.keys(channelsConfig).filter((key) => key !== "*");
|
|
@@ -4559,7 +5024,8 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4559
5024
|
unresolved.push(entry.input);
|
|
4560
5025
|
continue;
|
|
4561
5026
|
}
|
|
4562
|
-
|
|
5027
|
+
const resolvedLabel = formatSlackChannelResolved(entry);
|
|
5028
|
+
if (resolvedLabel) mapping.push(resolvedLabel);
|
|
4563
5029
|
const existing = nextChannels[entry.id] ?? {};
|
|
4564
5030
|
nextChannels[entry.id] = {
|
|
4565
5031
|
...source,
|
|
@@ -4721,4 +5187,4 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4721
5187
|
}
|
|
4722
5188
|
const resolveSlackRuntimeGroupPolicy = resolveOpenProviderRuntimeGroupPolicy$1;
|
|
4723
5189
|
//#endregion
|
|
4724
|
-
export {
|
|
5190
|
+
export { buildSlackSlashCommandMatcher as C, updateLastRoute as S, resolveSlackChatType as _, createSlackResponseUrlBudget as a, resolveChannelResetConfig as b, formatUnknownError as c, authorizeSlackBotRoomMessage as d, resolveSlackCommandIngress as f, normalizeSlackChannelType as g, parseSlackAssistantThreadMetadata as h, SlackResponseAlreadyReportedError as i, recordInboundSession as l, buildSlackAssistantThreadMetadata as m, resolveSlackRuntimeGroupPolicy as n, authorizeSlackDirectMessage as o, resolveSlackEffectiveAllowFrom as p, resolveSlackRoomContextHints as r, SLACK_SOCKET_RECONNECT_POLICY as s, monitorSlackProvider as t, resolveConversationLabel$1 as u, readSessionUpdatedAt as v, stripSlackMentionsForCommandDetection as w, resolveStorePath$1 as x, resolveChannelContextVisibilityMode as y };
|