@openclaw/slack 2026.7.1 → 2026.7.2-beta.2
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-Cee2ih0s.js} +209 -30
- package/dist/action-runtime.runtime-DZfd7IFr.js +2 -0
- package/dist/{actions-DMpDmKEA.js → actions-BZiuSTIq.js} +88 -143
- package/dist/{actions.runtime-C103_BPW.js → actions.runtime-DoD0-gxw.js} +1 -1
- package/dist/api.js +14 -13
- package/dist/{approval-handler.runtime-CIUbOt5T.js → approval-handler.runtime-CUPtIM4k.js} +3 -3
- package/dist/{channel-BjlsaGHn.js → channel-DaIiGUOp.js} +119 -141
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/channel-type-Cfm6_6n1.js +109 -0
- package/dist/{channel.setup-Dq6syTNU.js → channel.setup-C-mLz50O.js} +3 -3
- package/dist/{client-DGxwSL7f.js → client-WIPAS3Yr.js} +46 -2
- package/dist/config-api-Df9a1B8C.js +3 -0
- package/dist/{config-schema-BzqzFW__.js → config-schema-xPSnK2VK.js} +32 -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-D_yZhjgp.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-85UWiriS.js +2345 -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-CalGciTs.js} +3 -3
- package/dist/outbound-adapter-BiLKcnyH.js +228 -0
- package/dist/{pipeline.runtime-rpVpay59.js → pipeline.runtime-DofAvTtb.js} +657 -221
- package/dist/{policy-Dj-puVPb.js → policy-BVs4LiSa.js} +7 -7
- package/dist/preflight-audio.runtime-D5Bgags0.js +3 -0
- package/dist/{probe-CuwRDE5j.js → probe-CN0ssfLN.js} +2 -2
- package/dist/{provider-C1-DFSpw.js → provider-CmeoUiIM.js} +907 -206
- package/dist/{relay-source-D2jv1iGg.js → relay-source-DMmgtvg4.js} +6 -17
- package/dist/replies-CJJCp6qz.js +420 -0
- package/dist/{resolve-channels-CstkChBE.js → resolve-channels-Rfts6POK.js} +17 -6
- package/dist/{resolve-users-C3NP2c-Q.js → resolve-users-y6wAxR8E.js} +11 -6
- package/dist/runtime-JSVZSWAj.js +8 -0
- package/dist/{runtime-api-CEmqYxR-.js → runtime-api-BGDyOt1D.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-CuxNQZ6v.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-DYp8ca-l.js} +662 -325
- package/dist/{send.runtime-GPRa63l1.js → send.runtime-CJLxAt26.js} +1 -1
- package/dist/{setup-core-fGd6Gquy.js → setup-core-CkwVLzFO.js} +15 -21
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{setup-surface-Dv2fIJNB.js → setup-surface-oSd3NX9J.js} +4 -4
- package/dist/{shared-CosfcFri.js → shared-D9yz5Otg.js} +17 -38
- package/dist/{slash-dispatch.runtime-CcMbbT3_.js → slash-dispatch.runtime-CSja9VuD.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 +84 -0
- package/package.json +7 -7
- 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/configured-state.js +0 -11
- 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
- package/dist/runtime-BOk7xkOl.js +0 -8
|
@@ -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 { B as
|
|
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 { B as truncateSlackText, G as isSlackCallbackActionId, I as SLACK_TEXT_LIMIT, J as decodeSlackApprovalAction, W as isSlackApprovalActionId, c as normalizeAllowListLower, d as resolveSlackAllowListMatch, et as isSlackAnyNativeApprovalClientEnabled, ft as isSlackExecApprovalAuthorizedSender, i as formatSlackError, l as normalizeSlackAllowOwnerEntry, o as allowListMatches, q as escapeSlackMrkdwn, s as normalizeAllowList, u as normalizeSlackSlug, ut as isSlackApprovalAuthorizedSender } from "./group-policy-85UWiriS.js";
|
|
4
|
+
import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-BilWUdcI.js";
|
|
5
|
+
import { d as resolveSlackWebClientOptions, r as createSlackStartupAuthClient, s as getSlackListenerUploadCompletionClient } from "./client-WIPAS3Yr.js";
|
|
6
|
+
import { n as getSlackRuntime, t as getOptionalSlackRuntime } from "./runtime-JSVZSWAj.js";
|
|
7
|
+
import { i as setSlackDefaultSendIdentity } from "./send-DYp8ca-l.js";
|
|
8
|
+
import { t as resolveSlackChannelAllowlist } from "./resolve-channels-Rfts6POK.js";
|
|
9
|
+
import { t as resolveSlackUserAllowlist } from "./resolve-users-y6wAxR8E.js";
|
|
10
|
+
import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-BVs4LiSa.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
|
|
@@ -94,8 +94,28 @@ function resolveSlackChatType(channelType) {
|
|
|
94
94
|
return "channel";
|
|
95
95
|
}
|
|
96
96
|
//#endregion
|
|
97
|
+
//#region extensions/slack/src/monitor/lru-map-cache.ts
|
|
98
|
+
/** LRU-touch read: move hit to newest so pruneMapToMaxSize keeps active keys. */
|
|
99
|
+
function readLruMapEntry(cache, cacheKey) {
|
|
100
|
+
const cached = cache.get(cacheKey);
|
|
101
|
+
if (cached) {
|
|
102
|
+
cache.delete(cacheKey);
|
|
103
|
+
cache.set(cacheKey, cached);
|
|
104
|
+
}
|
|
105
|
+
return cached;
|
|
106
|
+
}
|
|
107
|
+
function writeLruMapEntry(cache, cacheKey, entry, maxEntries) {
|
|
108
|
+
cache.delete(cacheKey);
|
|
109
|
+
cache.set(cacheKey, entry);
|
|
110
|
+
pruneMapToMaxSize(cache, maxEntries);
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
97
113
|
//#region extensions/slack/src/monitor/context.ts
|
|
98
114
|
const SLACK_ASSISTANT_THREAD_CONTEXT_METADATA_EVENT = "assistant_thread_context";
|
|
115
|
+
const SLACK_CHANNEL_CACHE_MAX_ENTRIES = 1024;
|
|
116
|
+
const SLACK_USER_CACHE_MAX_ENTRIES = 2048;
|
|
117
|
+
const SLACK_CHANNEL_DENIAL_WARNING_TTL_MS = 5 * 6e4;
|
|
118
|
+
const SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES = 1024;
|
|
99
119
|
function buildSlackAssistantThreadMetadata(context) {
|
|
100
120
|
const eventPayload = {};
|
|
101
121
|
if (context.channelId) eventPayload.channel_id = context.channelId;
|
|
@@ -134,23 +154,48 @@ function createSlackMonitorContext(params) {
|
|
|
134
154
|
ttlMs: 6e4,
|
|
135
155
|
maxSize: 500
|
|
136
156
|
});
|
|
157
|
+
const channelDenialWarnings = createDedupeCache({
|
|
158
|
+
ttlMs: SLACK_CHANNEL_DENIAL_WARNING_TTL_MS,
|
|
159
|
+
maxSize: SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES
|
|
160
|
+
});
|
|
137
161
|
const assistantThreadContexts = /* @__PURE__ */ new Map();
|
|
138
162
|
let lastAssistantContextCleanupAt = Date.now();
|
|
139
163
|
const allowFrom = normalizeAllowList(params.allowFrom);
|
|
140
164
|
const groupDmChannels = normalizeAllowList(params.groupDmChannels);
|
|
141
|
-
const groupDmChannelsLower = normalizeAllowListLower(groupDmChannels);
|
|
165
|
+
const groupDmChannelsLower = new Set(normalizeAllowListLower(groupDmChannels).map((entry) => entry.replace(/^channel:/, "")));
|
|
142
166
|
const defaultRequireMention = params.defaultRequireMention ?? true;
|
|
143
167
|
const hasChannelAllowlistConfig = Object.keys(params.channelsConfig ?? {}).length > 0;
|
|
144
168
|
const channelsConfigKeys = Object.keys(params.channelsConfig ?? {});
|
|
145
|
-
const
|
|
169
|
+
const scopedKey = (key, eventScope) => eventScope ? `${params.accountId}:${eventScope.teamId}:${key}` : key;
|
|
170
|
+
const rememberSlackChannelType = (channelId, channelType, eventScope) => {
|
|
171
|
+
const id = normalizeOptionalString(channelId);
|
|
172
|
+
const normalizedType = normalizeOptionalString(channelType)?.toLowerCase();
|
|
173
|
+
if (!id || normalizedType !== "im" && normalizedType !== "mpim" && normalizedType !== "channel" && normalizedType !== "group") return;
|
|
174
|
+
const cacheKey = scopedKey(id, eventScope);
|
|
175
|
+
const cached = readLruMapEntry(channelCache, cacheKey);
|
|
176
|
+
const type = normalizeSlackChannelType(normalizedType, id);
|
|
177
|
+
if (cached?.info.type === type) return;
|
|
178
|
+
writeLruMapEntry(channelCache, cacheKey, {
|
|
179
|
+
info: {
|
|
180
|
+
...cached?.info,
|
|
181
|
+
type
|
|
182
|
+
},
|
|
183
|
+
metadataLoaded: cached?.metadataLoaded ?? false
|
|
184
|
+
}, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
|
|
185
|
+
};
|
|
186
|
+
const recallSlackChannelType = (channelId, eventScope) => {
|
|
187
|
+
const id = normalizeOptionalString(channelId);
|
|
188
|
+
return id ? readLruMapEntry(channelCache, scopedKey(id, eventScope))?.info.type : void 0;
|
|
189
|
+
};
|
|
190
|
+
const markMessageSeen = (channelId, ts, eventScope) => {
|
|
146
191
|
if (!channelId || !ts) return false;
|
|
147
|
-
return seenMessages.check(`${channelId}:${ts}
|
|
192
|
+
return seenMessages.check(scopedKey(`${channelId}:${ts}`, eventScope));
|
|
148
193
|
};
|
|
149
|
-
const releaseSeenMessage = (channelId, ts) => {
|
|
194
|
+
const releaseSeenMessage = (channelId, ts, eventScope) => {
|
|
150
195
|
if (!channelId || !ts) return;
|
|
151
|
-
seenMessages.delete(`${channelId}:${ts}
|
|
196
|
+
seenMessages.delete(scopedKey(`${channelId}:${ts}`, eventScope));
|
|
152
197
|
};
|
|
153
|
-
const assistantContextKey = (channelId, threadTs) => `${channelId}:${threadTs}
|
|
198
|
+
const assistantContextKey = (channelId, threadTs, eventScope) => scopedKey(`${channelId}:${threadTs}`, eventScope);
|
|
154
199
|
const cleanupAssistantThreadContexts = () => {
|
|
155
200
|
const now = Date.now();
|
|
156
201
|
if (now - lastAssistantContextCleanupAt < SLACK_ASSISTANT_CONTEXT_CLEANUP_INTERVAL_MS) return;
|
|
@@ -158,9 +203,9 @@ function createSlackMonitorContext(params) {
|
|
|
158
203
|
const cutoff = now - SLACK_ASSISTANT_CONTEXT_TTL_MS;
|
|
159
204
|
for (const [key, entry] of assistantThreadContexts) if (entry.updatedAt < cutoff) assistantThreadContexts.delete(key);
|
|
160
205
|
};
|
|
161
|
-
const getSlackAssistantThreadContext = (channelId, threadTs) => {
|
|
206
|
+
const getSlackAssistantThreadContext = (channelId, threadTs, eventScope) => {
|
|
162
207
|
if (!channelId || !threadTs) return;
|
|
163
|
-
const key = assistantContextKey(channelId, threadTs);
|
|
208
|
+
const key = assistantContextKey(channelId, threadTs, eventScope);
|
|
164
209
|
const entry = assistantThreadContexts.get(key);
|
|
165
210
|
if (!entry) return;
|
|
166
211
|
if (Date.now() - entry.updatedAt > SLACK_ASSISTANT_CONTEXT_TTL_MS) {
|
|
@@ -169,9 +214,9 @@ function createSlackMonitorContext(params) {
|
|
|
169
214
|
}
|
|
170
215
|
return entry;
|
|
171
216
|
};
|
|
172
|
-
const saveSlackAssistantThreadContext = (context) => {
|
|
217
|
+
const saveSlackAssistantThreadContext = (context, eventScope) => {
|
|
173
218
|
cleanupAssistantThreadContexts();
|
|
174
|
-
assistantThreadContexts.set(assistantContextKey(context.assistantChannelId, context.threadTs), {
|
|
219
|
+
assistantThreadContexts.set(assistantContextKey(context.assistantChannelId, context.threadTs, eventScope), {
|
|
175
220
|
...context,
|
|
176
221
|
updatedAt: Date.now()
|
|
177
222
|
});
|
|
@@ -179,7 +224,7 @@ function createSlackMonitorContext(params) {
|
|
|
179
224
|
const resolveSlackSystemEventSessionKey = (p) => {
|
|
180
225
|
const channelId = normalizeOptionalString(p.channelId) ?? "";
|
|
181
226
|
const senderId = normalizeOptionalString(p.senderId) ?? "";
|
|
182
|
-
const channelType = normalizeSlackChannelType(p.channelType, channelId);
|
|
227
|
+
const channelType = normalizeSlackChannelType(p.channelType ?? recallSlackChannelType(channelId), channelId);
|
|
183
228
|
const isDirectMessage = channelType === "im";
|
|
184
229
|
if (!channelId && (!isDirectMessage || !senderId)) return params.mainKey;
|
|
185
230
|
const isGroup = channelType === "mpim";
|
|
@@ -237,39 +282,47 @@ function createSlackMonitorContext(params) {
|
|
|
237
282
|
parentSessionKey: normalizeOptionalString(p.threadTs) && params.threadInheritParent ? legacySessionKey : void 0
|
|
238
283
|
}).sessionKey;
|
|
239
284
|
};
|
|
240
|
-
const resolveChannelName = async (channelId) => {
|
|
241
|
-
const
|
|
242
|
-
|
|
285
|
+
const resolveChannelName = async (channelId, eventScope) => {
|
|
286
|
+
const cacheKey = scopedKey(channelId, eventScope);
|
|
287
|
+
const cached = readLruMapEntry(channelCache, cacheKey);
|
|
288
|
+
if (cached?.metadataLoaded) return cached.info;
|
|
243
289
|
try {
|
|
244
|
-
const info = await params.app.client.conversations.info({
|
|
290
|
+
const info = await (eventScope?.client ?? params.app.client).conversations.info({
|
|
245
291
|
token: params.botToken,
|
|
246
292
|
channel: channelId
|
|
247
293
|
});
|
|
248
294
|
const name = info.channel && "name" in info.channel ? info.channel.name : void 0;
|
|
249
295
|
const channel = info.channel ?? void 0;
|
|
296
|
+
const type = channel?.is_im ? "im" : channel?.is_mpim ? "mpim" : channel?.is_channel ? "channel" : channel?.is_group ? "group" : void 0;
|
|
297
|
+
const topic = channel && "topic" in channel ? channel.topic?.value ?? void 0 : void 0;
|
|
298
|
+
const purpose = channel && "purpose" in channel ? channel.purpose?.value ?? void 0 : void 0;
|
|
250
299
|
const entry = {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
300
|
+
info: {
|
|
301
|
+
name,
|
|
302
|
+
type: cached?.info.type ?? type,
|
|
303
|
+
topic,
|
|
304
|
+
purpose
|
|
305
|
+
},
|
|
306
|
+
metadataLoaded: true
|
|
255
307
|
};
|
|
256
|
-
channelCache
|
|
257
|
-
return entry;
|
|
308
|
+
writeLruMapEntry(channelCache, cacheKey, entry, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
|
|
309
|
+
return entry.info;
|
|
258
310
|
} catch {
|
|
259
|
-
return {};
|
|
311
|
+
return cached?.info ?? {};
|
|
260
312
|
}
|
|
261
313
|
};
|
|
262
|
-
const resolveUserName = async (userId) => {
|
|
263
|
-
const
|
|
314
|
+
const resolveUserName = async (userId, eventScope) => {
|
|
315
|
+
const cacheKey = scopedKey(userId, eventScope);
|
|
316
|
+
const cached = readLruMapEntry(userCache, cacheKey);
|
|
264
317
|
if (cached) return cached;
|
|
265
318
|
try {
|
|
266
|
-
const info = await params.app.client.users.info({
|
|
319
|
+
const info = await (eventScope?.client ?? params.app.client).users.info({
|
|
267
320
|
token: params.botToken,
|
|
268
321
|
user: userId
|
|
269
322
|
});
|
|
270
323
|
const profile = info.user?.profile;
|
|
271
324
|
const entry = { name: profile?.display_name || profile?.real_name || info.user?.name || void 0 };
|
|
272
|
-
userCache
|
|
325
|
+
writeLruMapEntry(userCache, cacheKey, entry, SLACK_USER_CACHE_MAX_ENTRIES);
|
|
273
326
|
return entry;
|
|
274
327
|
} catch {
|
|
275
328
|
return {};
|
|
@@ -278,7 +331,7 @@ function createSlackMonitorContext(params) {
|
|
|
278
331
|
const setSlackThreadStatus = async (p) => {
|
|
279
332
|
if (!p.threadTs) return;
|
|
280
333
|
try {
|
|
281
|
-
await params.app.client.assistant.threads.setStatus({
|
|
334
|
+
await (p.eventScope?.client ?? params.app.client).assistant.threads.setStatus({
|
|
282
335
|
token: params.botToken,
|
|
283
336
|
channel_id: p.channelId,
|
|
284
337
|
thread_ts: p.threadTs,
|
|
@@ -323,7 +376,7 @@ function createSlackMonitorContext(params) {
|
|
|
323
376
|
p.channelName,
|
|
324
377
|
p.channelName ? normalizeSlackSlug(p.channelName) : void 0
|
|
325
378
|
].filter((value) => Boolean(value)).map((value) => normalizeLowercaseStringOrEmpty(value));
|
|
326
|
-
if (!(groupDmChannelsLower.
|
|
379
|
+
if (!(groupDmChannelsLower.has("*") || candidates.some((candidate) => groupDmChannelsLower.has(candidate)))) return false;
|
|
327
380
|
}
|
|
328
381
|
if (isRoom && p.channelId) {
|
|
329
382
|
const channelConfig = resolveSlackChannelConfig({
|
|
@@ -337,19 +390,33 @@ function createSlackMonitorContext(params) {
|
|
|
337
390
|
const channelMatchMeta = formatAllowlistMatchMeta(channelConfig);
|
|
338
391
|
const channelAllowed = channelConfig?.allowed !== false;
|
|
339
392
|
const channelAllowlistConfigured = hasChannelAllowlistConfig;
|
|
340
|
-
|
|
393
|
+
const allowedByPolicy = isSlackChannelAllowedByPolicy({
|
|
341
394
|
groupPolicy: params.groupPolicy,
|
|
342
395
|
channelAllowlistConfigured,
|
|
343
396
|
channelAllowed
|
|
344
|
-
})
|
|
397
|
+
});
|
|
398
|
+
const explicitlyDisabled = params.groupPolicy !== "disabled" && channelConfig?.allowed === false && channelConfig.matchSource !== void 0;
|
|
399
|
+
if (!allowedByPolicy || params.groupPolicy === "open" && explicitlyDisabled) {
|
|
400
|
+
if (explicitlyDisabled) {
|
|
401
|
+
const reason = "channel_not_allowed";
|
|
402
|
+
const warningKey = `${params.accountId}:${p.channelId}:${reason}`;
|
|
403
|
+
if (!channelDenialWarnings.peek(warningKey)) {
|
|
404
|
+
channelDenialWarnings.check(warningKey);
|
|
405
|
+
logger.warn({
|
|
406
|
+
provider: "slack",
|
|
407
|
+
accountId: params.accountId,
|
|
408
|
+
channelId: p.channelId,
|
|
409
|
+
reason,
|
|
410
|
+
cause: "channel_disabled",
|
|
411
|
+
groupPolicy: params.groupPolicy,
|
|
412
|
+
matchSource: channelConfig.matchSource,
|
|
413
|
+
matchKey: channelConfig.matchKey
|
|
414
|
+
}, "Slack channel denied by configuration");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
345
417
|
logVerbose(`slack: drop channel ${p.channelId} (groupPolicy=${params.groupPolicy}, ${channelMatchMeta})`);
|
|
346
418
|
return false;
|
|
347
419
|
}
|
|
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
420
|
logVerbose(`slack: allow channel ${p.channelId} (${channelMatchMeta})`);
|
|
354
421
|
}
|
|
355
422
|
return true;
|
|
@@ -380,6 +447,10 @@ function createSlackMonitorContext(params) {
|
|
|
380
447
|
botId: params.botId,
|
|
381
448
|
teamId: params.teamId,
|
|
382
449
|
apiAppId: params.apiAppId,
|
|
450
|
+
installationIdentity: params.installationIdentity ?? {
|
|
451
|
+
kind: "degraded",
|
|
452
|
+
reason: "auth_test_failed"
|
|
453
|
+
},
|
|
383
454
|
historyLimit: params.historyLimit,
|
|
384
455
|
dmHistoryLimit: Math.max(0, params.dmHistoryLimit ?? 0),
|
|
385
456
|
channelHistories,
|
|
@@ -415,6 +486,8 @@ function createSlackMonitorContext(params) {
|
|
|
415
486
|
resolveSlackSystemEventSessionKey,
|
|
416
487
|
isChannelAllowed,
|
|
417
488
|
resolveChannelName,
|
|
489
|
+
rememberSlackChannelType,
|
|
490
|
+
recallSlackChannelType,
|
|
418
491
|
resolveUserName,
|
|
419
492
|
setSlackThreadStatus,
|
|
420
493
|
getSlackAssistantThreadContext,
|
|
@@ -423,6 +496,135 @@ function createSlackMonitorContext(params) {
|
|
|
423
496
|
};
|
|
424
497
|
}
|
|
425
498
|
//#endregion
|
|
499
|
+
//#region extensions/slack/src/monitor/enterprise-install.ts
|
|
500
|
+
const SLACK_CHANNEL_ID_RE = /^[CDG][A-Z0-9]{8,}$/;
|
|
501
|
+
const SLACK_USER_ID_RE = /^[UW][A-Z0-9]{8,}$/;
|
|
502
|
+
function isStableSlackChannelEntry(value, options) {
|
|
503
|
+
if (typeof value !== "string") return false;
|
|
504
|
+
const normalized = value.trim();
|
|
505
|
+
if (normalized === "*") return options?.allowWildcard === true;
|
|
506
|
+
if (/^channel:([CDG][A-Z0-9]{8,})$/.exec(normalized)?.[1]) return true;
|
|
507
|
+
return SLACK_CHANNEL_ID_RE.test(normalized);
|
|
508
|
+
}
|
|
509
|
+
function isStableSlackAllowlistUserEntry(value) {
|
|
510
|
+
if (typeof value !== "string") return false;
|
|
511
|
+
const normalized = value.trim();
|
|
512
|
+
if (normalized === "*") return true;
|
|
513
|
+
const prefixed = /^(?:slack|user):([UW][A-Z0-9]{8,})$/.exec(normalized);
|
|
514
|
+
return Boolean(prefixed?.[1]) || SLACK_USER_ID_RE.test(normalized);
|
|
515
|
+
}
|
|
516
|
+
function isStableSlackToolsBySenderEntry(value) {
|
|
517
|
+
if (typeof value !== "string") return false;
|
|
518
|
+
const normalized = value.trim();
|
|
519
|
+
if (normalized === "*") return true;
|
|
520
|
+
const prefixed = /^(?:id:|channel:slack:)([UW][A-Z0-9]{8,})$/.exec(normalized);
|
|
521
|
+
return Boolean(prefixed?.[1]) || SLACK_USER_ID_RE.test(normalized);
|
|
522
|
+
}
|
|
523
|
+
function assertStableEntries(params) {
|
|
524
|
+
const invalid = params.values?.find((value) => !params.predicate(value));
|
|
525
|
+
if (invalid !== void 0) throw new Error(`Slack Enterprise Grid org installs require stable Slack IDs in ${params.path}; invalid entry ${JSON.stringify(invalid)}`);
|
|
526
|
+
}
|
|
527
|
+
/** Validate every policy surface that would otherwise require name resolution. */
|
|
528
|
+
function assertEnterpriseSlackPolicyConfig(params) {
|
|
529
|
+
const { config, accountId } = params;
|
|
530
|
+
if (config.dangerouslyAllowNameMatching === true) throw new Error(`Slack Enterprise Grid org account "${accountId}" cannot use dangerouslyAllowNameMatching`);
|
|
531
|
+
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`);
|
|
532
|
+
assertStableEntries({
|
|
533
|
+
values: config.allowFrom,
|
|
534
|
+
path: `channels.slack.accounts.${accountId}.allowFrom`,
|
|
535
|
+
predicate: isStableSlackAllowlistUserEntry
|
|
536
|
+
});
|
|
537
|
+
assertStableEntries({
|
|
538
|
+
values: config.dm?.allowFrom,
|
|
539
|
+
path: `channels.slack.accounts.${accountId}.dm.allowFrom`,
|
|
540
|
+
predicate: isStableSlackAllowlistUserEntry
|
|
541
|
+
});
|
|
542
|
+
assertStableEntries({
|
|
543
|
+
values: config.dm?.groupChannels,
|
|
544
|
+
path: `channels.slack.accounts.${accountId}.dm.groupChannels`,
|
|
545
|
+
predicate: (value) => isStableSlackChannelEntry(value)
|
|
546
|
+
});
|
|
547
|
+
if (config.reactionNotifications === "allowlist") assertStableEntries({
|
|
548
|
+
values: config.reactionAllowlist,
|
|
549
|
+
path: `channels.slack.accounts.${accountId}.reactionAllowlist`,
|
|
550
|
+
predicate: isStableSlackAllowlistUserEntry
|
|
551
|
+
});
|
|
552
|
+
for (const [channelKey, channel] of Object.entries(config.channels ?? {})) {
|
|
553
|
+
if (!isStableSlackChannelEntry(channelKey, { allowWildcard: true })) throw new Error(`Slack Enterprise Grid org installs require stable Slack channel IDs; invalid channels key ${JSON.stringify(channelKey)}`);
|
|
554
|
+
assertStableEntries({
|
|
555
|
+
values: channel?.users,
|
|
556
|
+
path: `channels.slack.accounts.${accountId}.channels.${channelKey}.users`,
|
|
557
|
+
predicate: isStableSlackAllowlistUserEntry
|
|
558
|
+
});
|
|
559
|
+
assertStableEntries({
|
|
560
|
+
values: Object.keys(channel?.toolsBySender ?? {}),
|
|
561
|
+
path: `channels.slack.accounts.${accountId}.channels.${channelKey}.toolsBySender`,
|
|
562
|
+
predicate: isStableSlackToolsBySenderEntry
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** Prevent account-wide user authorization state from crossing workspace boundaries. */
|
|
567
|
+
function assertEnterpriseSlackDmPolicy(params) {
|
|
568
|
+
if (!params.dmEnabled || params.dmPolicy === "disabled") return;
|
|
569
|
+
if (params.dmPolicy === "open" && params.allowFrom?.includes("*")) return;
|
|
570
|
+
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`);
|
|
571
|
+
}
|
|
572
|
+
function assertNoEnterpriseSlackBindings(params) {
|
|
573
|
+
const defaultAccountId = resolveDefaultSlackAccountId(params.cfg);
|
|
574
|
+
if (params.cfg.bindings?.find((binding) => {
|
|
575
|
+
if (binding.match.channel.trim().toLowerCase() !== "slack") return false;
|
|
576
|
+
const accountId = binding.match.accountId?.trim();
|
|
577
|
+
return accountId === "*" || accountId === params.accountId || !accountId && params.accountId === defaultAccountId;
|
|
578
|
+
})) throw new Error(`Slack Enterprise Grid org account "${params.accountId}" cannot use configured Slack bindings`);
|
|
579
|
+
}
|
|
580
|
+
function resolveSlackInstallationIdentity(params) {
|
|
581
|
+
const auth = params.auth;
|
|
582
|
+
if (!auth) {
|
|
583
|
+
if (params.enterpriseOrgInstall) throw new Error(`Slack enterpriseOrgInstall=true requires a successful auth.test (${formatSlackError(params.authError)})`);
|
|
584
|
+
return {
|
|
585
|
+
kind: "degraded",
|
|
586
|
+
reason: "auth_test_failed"
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const isEnterpriseInstall = auth.is_enterprise_install === true;
|
|
590
|
+
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");
|
|
591
|
+
const apiAppId = normalizeOptionalString(auth.app_id);
|
|
592
|
+
const enterpriseId = normalizeOptionalString(auth.enterprise_id);
|
|
593
|
+
if (params.enterpriseOrgInstall) {
|
|
594
|
+
if (!enterpriseId) throw new Error("Slack org-wide auth.test returned no enterprise_id");
|
|
595
|
+
const transportApiAppId = normalizeOptionalString(params.transportApiAppId);
|
|
596
|
+
if (apiAppId && transportApiAppId && apiAppId !== transportApiAppId) throw new Error(`Slack token mismatch: bot token app_id=${apiAppId} but transport app_id=${transportApiAppId}`);
|
|
597
|
+
const effectiveApiAppId = apiAppId ?? transportApiAppId;
|
|
598
|
+
return {
|
|
599
|
+
kind: "enterprise",
|
|
600
|
+
...effectiveApiAppId ? { apiAppId: effectiveApiAppId } : {},
|
|
601
|
+
enterpriseId
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const teamId = normalizeOptionalString(auth.team_id);
|
|
605
|
+
if (!teamId) throw new Error("Slack workspace auth.test returned no team_id");
|
|
606
|
+
return {
|
|
607
|
+
kind: "workspace",
|
|
608
|
+
teamId,
|
|
609
|
+
...apiAppId ? { apiAppId } : {},
|
|
610
|
+
...enterpriseId ? { enterpriseId } : {}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function resolveSlackIdentityHealth(params) {
|
|
614
|
+
if (params.installationIdentity.kind === "enterprise") return {
|
|
615
|
+
healthState: "healthy",
|
|
616
|
+
lastError: null
|
|
617
|
+
};
|
|
618
|
+
const lastError = normalizeOptionalString(params.authTestError) ?? normalizeOptionalString(params.authIdentityWarning) ?? (params.installationIdentity.kind === "degraded" || !params.botUserId.trim() ? "slack bot identity unavailable" : void 0);
|
|
619
|
+
return lastError ? {
|
|
620
|
+
healthState: "degraded",
|
|
621
|
+
lastError
|
|
622
|
+
} : {
|
|
623
|
+
healthState: "healthy",
|
|
624
|
+
lastError: null
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
//#endregion
|
|
426
628
|
//#region extensions/slack/src/monitor/events/assistant.ts
|
|
427
629
|
const DEFAULT_ASSISTANT_PROMPTS = [
|
|
428
630
|
{
|
|
@@ -557,7 +759,12 @@ function migrateSlackChannelsInPlace(channels, oldChannelId, newChannelId) {
|
|
|
557
759
|
migrated: false,
|
|
558
760
|
skippedExisting: true
|
|
559
761
|
};
|
|
560
|
-
|
|
762
|
+
const channelConfig = channels[oldChannelId];
|
|
763
|
+
if (!channelConfig) return {
|
|
764
|
+
migrated: false,
|
|
765
|
+
skippedExisting: false
|
|
766
|
+
};
|
|
767
|
+
channels[newChannelId] = channelConfig;
|
|
561
768
|
delete channels[oldChannelId];
|
|
562
769
|
return {
|
|
563
770
|
migrated: true,
|
|
@@ -702,7 +909,7 @@ function registerSlackChannelEvents(params) {
|
|
|
702
909
|
}
|
|
703
910
|
//#endregion
|
|
704
911
|
//#region extensions/slack/src/monitor/events/home.ts
|
|
705
|
-
function buildSlackHomeView() {
|
|
912
|
+
function buildSlackHomeView(slashCommandName) {
|
|
706
913
|
return {
|
|
707
914
|
type: "home",
|
|
708
915
|
callback_id: "openclaw:home",
|
|
@@ -718,7 +925,7 @@ function buildSlackHomeView() {
|
|
|
718
925
|
type: "section",
|
|
719
926
|
text: {
|
|
720
927
|
type: "mrkdwn",
|
|
721
|
-
text:
|
|
928
|
+
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
929
|
}
|
|
723
930
|
},
|
|
724
931
|
{
|
|
@@ -732,7 +939,7 @@ function buildSlackHomeView() {
|
|
|
732
939
|
};
|
|
733
940
|
}
|
|
734
941
|
function registerSlackHomeEvents(params) {
|
|
735
|
-
const { ctx, trackEvent } = params;
|
|
942
|
+
const { ctx, slashCommandName, trackEvent } = params;
|
|
736
943
|
ctx.app.event("app_home_opened", async ({ event, body }) => {
|
|
737
944
|
try {
|
|
738
945
|
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
@@ -742,7 +949,7 @@ function registerSlackHomeEvents(params) {
|
|
|
742
949
|
await ctx.app.client.views.publish({
|
|
743
950
|
token: ctx.botToken,
|
|
744
951
|
user_id: payload.user,
|
|
745
|
-
view: buildSlackHomeView()
|
|
952
|
+
view: buildSlackHomeView(slashCommandName)
|
|
746
953
|
});
|
|
747
954
|
} catch (err) {
|
|
748
955
|
ctx.runtime.error?.(danger(`slack app home handler failed: ${formatErrorMessage(err)}`));
|
|
@@ -783,7 +990,7 @@ async function dispatchSlackPluginInteractiveHandler(params) {
|
|
|
783
990
|
}
|
|
784
991
|
//#endregion
|
|
785
992
|
//#region extensions/slack/src/monitor/auth.ts
|
|
786
|
-
|
|
993
|
+
const slackChannelMembersCache = /* @__PURE__ */ new WeakMap();
|
|
787
994
|
const DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS = 6e4;
|
|
788
995
|
const CHANNEL_MEMBERS_CACHE_MAX = 512;
|
|
789
996
|
const SLACK_CHANNEL_ID = "slack";
|
|
@@ -891,11 +1098,11 @@ async function resolveSlackEffectiveAllowFrom(ctx, options) {
|
|
|
891
1098
|
}
|
|
892
1099
|
return normalizeAllowListLower([...base, ...storeAllowFrom]);
|
|
893
1100
|
}
|
|
894
|
-
async function fetchSlackChannelMemberIds(ctx, channelId) {
|
|
1101
|
+
async function fetchSlackChannelMemberIds(ctx, channelId, eventScope) {
|
|
895
1102
|
const members = /* @__PURE__ */ new Set();
|
|
896
1103
|
let cursor;
|
|
897
1104
|
do {
|
|
898
|
-
const response = await ctx.app.client.conversations.members({
|
|
1105
|
+
const response = await (eventScope?.client ?? ctx.app.client).conversations.members({
|
|
899
1106
|
token: ctx.botToken,
|
|
900
1107
|
channel: channelId,
|
|
901
1108
|
limit: 999,
|
|
@@ -907,9 +1114,9 @@ async function fetchSlackChannelMemberIds(ctx, channelId) {
|
|
|
907
1114
|
} while (cursor);
|
|
908
1115
|
return members;
|
|
909
1116
|
}
|
|
910
|
-
async function resolveSlackChannelMemberIds(ctx, channelId) {
|
|
1117
|
+
async function resolveSlackChannelMemberIds(ctx, channelId, eventScope) {
|
|
911
1118
|
const cache = getChannelMembersCache(ctx);
|
|
912
|
-
const key = `${ctx.accountId}:${channelId}`;
|
|
1119
|
+
const key = `${ctx.accountId}:${eventScope ? `${eventScope.teamId}:` : ""}${channelId}`;
|
|
913
1120
|
const ttlMs = readSlackCacheTtlMs("OPENCLAW_SLACK_CHANNEL_MEMBERS_CACHE_TTL_MS", DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS);
|
|
914
1121
|
const rawNowMs = Date.now();
|
|
915
1122
|
const nowMs = asDateTimestampMs(rawNowMs);
|
|
@@ -919,7 +1126,7 @@ async function resolveSlackChannelMemberIds(ctx, channelId) {
|
|
|
919
1126
|
cache.delete(key);
|
|
920
1127
|
}
|
|
921
1128
|
if (cached?.pending) return await cached.pending;
|
|
922
|
-
const pending = fetchSlackChannelMemberIds(ctx, channelId);
|
|
1129
|
+
const pending = fetchSlackChannelMemberIds(ctx, channelId, eventScope);
|
|
923
1130
|
const pendingExpiresAtMs = ttlMs > 0 ? resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNowMs }) : void 0;
|
|
924
1131
|
cache.set(key, {
|
|
925
1132
|
expiresAtMs: pendingExpiresAtMs ?? 0,
|
|
@@ -963,7 +1170,7 @@ async function authorizeSlackBotRoomMessage(params) {
|
|
|
963
1170
|
return false;
|
|
964
1171
|
}
|
|
965
1172
|
try {
|
|
966
|
-
const channelMemberIds = await resolveSlackChannelMemberIds(params.ctx, params.channelId);
|
|
1173
|
+
const channelMemberIds = await resolveSlackChannelMemberIds(params.ctx, params.channelId, params.eventScope);
|
|
967
1174
|
if (explicitOwnerIds.some((ownerId) => channelMemberIds.has(ownerId))) return true;
|
|
968
1175
|
logVerbose(`slack: drop bot message ${params.senderId} in ${params.channelId} (no owner present)`);
|
|
969
1176
|
} catch (error) {
|
|
@@ -979,8 +1186,10 @@ function slackIngressConversationKind(channelType) {
|
|
|
979
1186
|
}
|
|
980
1187
|
async function resolveSlackCommandIngress(params) {
|
|
981
1188
|
const isDirectMessage = params.channelType === "im";
|
|
1189
|
+
const isGroupDm = params.channelType === "mpim";
|
|
982
1190
|
const channelUsers = normalizeAllowListLower(params.channelUsers);
|
|
983
|
-
const channelUsersConfigured = !isDirectMessage && channelUsers.length > 0;
|
|
1191
|
+
const channelUsersConfigured = !isDirectMessage && !isGroupDm && channelUsers.length > 0;
|
|
1192
|
+
const groupAllowFrom = isGroupDm ? params.ownerAllowFromLower : channelUsersConfigured ? channelUsers : [];
|
|
984
1193
|
return await createSlackIngressResolver(params.ctx).message({
|
|
985
1194
|
subject: createSlackIngressSubject({
|
|
986
1195
|
senderId: params.senderId,
|
|
@@ -996,7 +1205,7 @@ async function resolveSlackCommandIngress(params) {
|
|
|
996
1205
|
mayPair: false
|
|
997
1206
|
},
|
|
998
1207
|
dmPolicy: isDirectMessage ? "open" : "disabled",
|
|
999
|
-
groupPolicy: channelUsersConfigured ? "allowlist" : "open",
|
|
1208
|
+
groupPolicy: isGroupDm || channelUsersConfigured ? "allowlist" : "open",
|
|
1000
1209
|
policy: {
|
|
1001
1210
|
groupAllowFromFallbackToAllowFrom: false,
|
|
1002
1211
|
mutableIdentifierMatching: params.ctx.allowNameMatching ? "enabled" : "disabled",
|
|
@@ -1004,7 +1213,7 @@ async function resolveSlackCommandIngress(params) {
|
|
|
1004
1213
|
},
|
|
1005
1214
|
mentionFacts: params.mentionFacts,
|
|
1006
1215
|
allowFrom: isDirectMessage ? ["*"] : params.ownerAllowFromLower,
|
|
1007
|
-
groupAllowFrom
|
|
1216
|
+
groupAllowFrom,
|
|
1008
1217
|
command: {
|
|
1009
1218
|
allowTextCommands: params.allowTextCommands,
|
|
1010
1219
|
hasControlCommand: params.hasControlCommand,
|
|
@@ -1015,12 +1224,14 @@ async function resolveSlackCommandIngress(params) {
|
|
|
1015
1224
|
}
|
|
1016
1225
|
async function decideSlackSystemIngress(params) {
|
|
1017
1226
|
const isDirectMessage = params.channelType === "im";
|
|
1227
|
+
const isGroupDm = params.channelType === "mpim";
|
|
1018
1228
|
const channelUsers = normalizeAllowListLower(params.channelUsers);
|
|
1019
|
-
const channelUsersConfigured = !isDirectMessage && channelUsers.length > 0;
|
|
1229
|
+
const channelUsersConfigured = !isDirectMessage && !isGroupDm && channelUsers.length > 0;
|
|
1020
1230
|
const ownerAllowFrom = params.interactiveEvent && channelUsersConfigured ? params.ownerAllowFromLower.filter((entry) => entry !== "*") : params.ownerAllowFromLower;
|
|
1021
1231
|
const hasAnyCommandAllowlist = ownerAllowFrom.length > 0 || channelUsersConfigured;
|
|
1022
1232
|
const groupAllowFrom = (() => {
|
|
1023
1233
|
if (isDirectMessage) return [];
|
|
1234
|
+
if (isGroupDm) return ownerAllowFrom;
|
|
1024
1235
|
if (params.interactiveEvent && hasAnyCommandAllowlist) return channelUsersConfigured ? channelUsers : [];
|
|
1025
1236
|
if (channelUsersConfigured) return channelUsers;
|
|
1026
1237
|
return params.channelId ? ["*"] : wildcardWhenOpen(params.ownerAllowFromLower);
|
|
@@ -1040,7 +1251,7 @@ async function decideSlackSystemIngress(params) {
|
|
|
1040
1251
|
mayPair: false
|
|
1041
1252
|
},
|
|
1042
1253
|
dmPolicy: isDirectMessage ? "open" : "disabled",
|
|
1043
|
-
groupPolicy: params.interactiveEvent && hasAnyCommandAllowlist ? "open" : channelUsersConfigured || !params.channelId && params.ownerAllowFromLower.length > 0 ? "allowlist" : "open",
|
|
1254
|
+
groupPolicy: isGroupDm ? "allowlist" : params.interactiveEvent && hasAnyCommandAllowlist ? "open" : channelUsersConfigured || !params.channelId && params.ownerAllowFromLower.length > 0 ? "allowlist" : "open",
|
|
1044
1255
|
policy: {
|
|
1045
1256
|
groupAllowFromFallbackToAllowFrom: false,
|
|
1046
1257
|
mutableIdentifierMatching: params.ctx.allowNameMatching ? "enabled" : "disabled"
|
|
@@ -1149,11 +1360,6 @@ async function authorizeSlackSystemEventSender(params) {
|
|
|
1149
1360
|
};
|
|
1150
1361
|
}
|
|
1151
1362
|
//#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
1363
|
//#region extensions/slack/src/monitor/events/interactions.block-actions.ts
|
|
1158
1364
|
function readOptionValues(options) {
|
|
1159
1365
|
if (!Array.isArray(options)) return;
|
|
@@ -1235,9 +1441,6 @@ function summarizeAction(action) {
|
|
|
1235
1441
|
workflowId: typed.workflow?.workflow_id
|
|
1236
1442
|
};
|
|
1237
1443
|
}
|
|
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
1444
|
function formatInteractionSelectionLabel(params) {
|
|
1242
1445
|
if (params.summary.actionType === "button" && params.buttonText?.trim()) return params.buttonText.trim();
|
|
1243
1446
|
if (params.summary.selectedLabels?.length) {
|
|
@@ -1267,12 +1470,15 @@ function buildSlackPluginInteractionData(params) {
|
|
|
1267
1470
|
const actionId = normalizeOptionalString(params.actionId) ?? "";
|
|
1268
1471
|
if (!actionId) return null;
|
|
1269
1472
|
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;
|
|
1473
|
+
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
1474
|
return payload ? `${actionId}:${payload}` : actionId;
|
|
1272
1475
|
}
|
|
1273
1476
|
function isSlackReplyActionId(actionId) {
|
|
1274
1477
|
return actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`);
|
|
1275
1478
|
}
|
|
1479
|
+
function readSlackApprovalAction(parsed) {
|
|
1480
|
+
return decodeSlackApprovalAction(normalizeOptionalString(parsed.actionSummary.value) ?? parsed.actionSummary.selectedValues?.map((entry) => normalizeOptionalString(entry)).find((entry) => Boolean(entry)));
|
|
1481
|
+
}
|
|
1276
1482
|
function isSlackReplyLinkAction(parsed) {
|
|
1277
1483
|
if (parsed.actionId === "openclaw:reply_link" || parsed.actionId.startsWith(`openclaw:reply_link:`)) return true;
|
|
1278
1484
|
const legacyUrl = normalizeOptionalString(parsed.typedAction.url);
|
|
@@ -1328,6 +1534,37 @@ async function updateSlackInteractionMessage(params) {
|
|
|
1328
1534
|
...params.blocks ? { blocks: params.blocks } : {}
|
|
1329
1535
|
});
|
|
1330
1536
|
}
|
|
1537
|
+
function resolveSlackApprovalTerminalLabel(approval) {
|
|
1538
|
+
if (approval.status === "allowed") return approval.decision === "allow-always" ? "Allowed always" : "Allowed once";
|
|
1539
|
+
if (approval.status === "denied") return "Denied";
|
|
1540
|
+
if (approval.status === "expired") return "Expired";
|
|
1541
|
+
return "Cancelled";
|
|
1542
|
+
}
|
|
1543
|
+
function removeSlackApprovalControls(blocks) {
|
|
1544
|
+
return blocks.flatMap((block) => {
|
|
1545
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) return [block];
|
|
1546
|
+
const typedBlock = block;
|
|
1547
|
+
if (typedBlock.type !== "actions" || !Array.isArray(typedBlock.elements)) return [block];
|
|
1548
|
+
const elements = typedBlock.elements.filter((element) => typeof element.action_id !== "string" || !isSlackApprovalActionId(element.action_id));
|
|
1549
|
+
return elements.length > 0 ? [{
|
|
1550
|
+
...block,
|
|
1551
|
+
elements
|
|
1552
|
+
}] : [];
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
function buildSlackApprovalTerminalBlocks(params) {
|
|
1556
|
+
const blocks = removeSlackApprovalControls(params.blocks ?? []).filter((block) => {
|
|
1557
|
+
const text = block.text?.text;
|
|
1558
|
+
return !(block.type === "section" && typeof text === "string" && /^\*(?:Exec|Plugin) approval required\*/u.test(text));
|
|
1559
|
+
});
|
|
1560
|
+
return [{
|
|
1561
|
+
type: "section",
|
|
1562
|
+
text: {
|
|
1563
|
+
type: "mrkdwn",
|
|
1564
|
+
text: `*${params.prefix}: ${params.label}*`
|
|
1565
|
+
}
|
|
1566
|
+
}, ...blocks];
|
|
1567
|
+
}
|
|
1331
1568
|
async function authorizeSlackBlockAction(params) {
|
|
1332
1569
|
const auth = await authorizeSlackSystemEventSender({
|
|
1333
1570
|
ctx: params.ctx,
|
|
@@ -1361,9 +1598,7 @@ async function handleSlackPluginBindingApproval(params) {
|
|
|
1361
1598
|
await respondEphemeral(params.respond, buildPluginBindingResolvedText(resolved));
|
|
1362
1599
|
return true;
|
|
1363
1600
|
}
|
|
1364
|
-
async function
|
|
1365
|
-
const approval = parseExecApprovalCommandText(params.pluginInteractionData);
|
|
1366
|
-
if (!approval) return false;
|
|
1601
|
+
async function handleSlackApprovalInteraction(params) {
|
|
1367
1602
|
const pluginApprovalAuthorizedSender = isSlackApprovalAuthorizedSender({
|
|
1368
1603
|
cfg: params.ctx.cfg,
|
|
1369
1604
|
accountId: params.ctx.accountId,
|
|
@@ -1374,38 +1609,95 @@ async function handleSlackExecApprovalInteraction(params) {
|
|
|
1374
1609
|
accountId: params.ctx.accountId,
|
|
1375
1610
|
senderId: params.parsed.userId
|
|
1376
1611
|
});
|
|
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)`);
|
|
1612
|
+
if (!(params.approval.approvalKind === "plugin" ? pluginApprovalAuthorizedSender : execApprovalAuthorizedSender)) {
|
|
1613
|
+
params.ctx.runtime.log?.(`slack:interaction drop ${params.approval.approvalKind} approval user=${params.parsed.userId} (not authorized)`);
|
|
1383
1614
|
await respondEphemeral(params.respond, "You are not authorized to approve this request.");
|
|
1384
1615
|
return true;
|
|
1385
1616
|
}
|
|
1386
1617
|
try {
|
|
1387
|
-
await resolveApprovalOverGateway({
|
|
1618
|
+
const result = await resolveApprovalOverGateway({
|
|
1388
1619
|
cfg: params.ctx.cfg,
|
|
1389
|
-
approvalId: approval.approvalId,
|
|
1390
|
-
|
|
1620
|
+
approvalId: params.approval.approvalId,
|
|
1621
|
+
approvalKind: params.approval.approvalKind,
|
|
1622
|
+
decision: params.approval.decision,
|
|
1391
1623
|
senderId: params.parsed.userId,
|
|
1392
|
-
allowPluginFallback,
|
|
1393
|
-
...resolveUnprefixedAsPlugin ? { resolveMethod: "plugin" } : {},
|
|
1394
1624
|
clientDisplayName: `Slack approval (${params.parsed.userId.trim() || "unknown"})`
|
|
1395
1625
|
});
|
|
1626
|
+
const terminalLabel = resolveSlackApprovalTerminalLabel(result.approval);
|
|
1627
|
+
const prefix = result.applied ? "Resolved" : "Already resolved";
|
|
1628
|
+
let terminalized = false;
|
|
1629
|
+
try {
|
|
1630
|
+
const terminalText = `${prefix}: ${terminalLabel}`;
|
|
1631
|
+
await updateSlackInteractionMessage({
|
|
1632
|
+
ctx: params.ctx,
|
|
1633
|
+
channelId: params.parsed.channelId,
|
|
1634
|
+
messageTs: params.parsed.messageTs,
|
|
1635
|
+
text: truncateSlackText(terminalText, 4e3),
|
|
1636
|
+
blocks: buildSlackApprovalTerminalBlocks({
|
|
1637
|
+
blocks: params.parsed.typedBody.message?.blocks,
|
|
1638
|
+
label: terminalLabel,
|
|
1639
|
+
prefix
|
|
1640
|
+
})
|
|
1641
|
+
});
|
|
1642
|
+
terminalized = true;
|
|
1643
|
+
} catch {}
|
|
1644
|
+
if (!terminalized || !result.applied) await respondEphemeral(params.respond, result.applied ? `Approval resolved: ${terminalLabel}.` : `This approval was already resolved: ${terminalLabel}.`);
|
|
1396
1645
|
} catch (error) {
|
|
1397
|
-
params.ctx.runtime.log?.(`slack:interaction
|
|
1646
|
+
params.ctx.runtime.log?.(`slack:interaction approval resolve failed id=${params.approval.approvalId}: ${String(error)}`);
|
|
1647
|
+
if (isApprovalNotFoundError(error)) {
|
|
1648
|
+
await respondEphemeral(params.respond, "This approval is no longer pending.");
|
|
1649
|
+
return true;
|
|
1650
|
+
}
|
|
1651
|
+
await respondEphemeral(params.respond, "Could not reach the Gateway to resolve this approval. Try again.");
|
|
1398
1652
|
throw error;
|
|
1399
1653
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
async function handleSlackLegacyApprovalInteraction(params) {
|
|
1657
|
+
const parsedApproval = parseExecApprovalCommandText(params.pluginInteractionData);
|
|
1658
|
+
if (!parsedApproval) return false;
|
|
1659
|
+
const pluginAuthorized = isSlackApprovalAuthorizedSender({
|
|
1660
|
+
cfg: params.ctx.cfg,
|
|
1661
|
+
accountId: params.ctx.accountId,
|
|
1662
|
+
senderId: params.parsed.userId
|
|
1663
|
+
});
|
|
1664
|
+
const execAuthorized = isSlackExecApprovalAuthorizedSender({
|
|
1665
|
+
cfg: params.ctx.cfg,
|
|
1666
|
+
accountId: params.ctx.accountId,
|
|
1667
|
+
senderId: params.parsed.userId
|
|
1668
|
+
});
|
|
1669
|
+
const resolveMethods = [];
|
|
1670
|
+
if (execAuthorized) resolveMethods.push("exec");
|
|
1671
|
+
if (pluginAuthorized) resolveMethods.push("plugin");
|
|
1672
|
+
if (resolveMethods.length === 0) {
|
|
1673
|
+
params.ctx.runtime.log?.(`slack:interaction drop legacy approval user=${params.parsed.userId} (not authorized)`);
|
|
1674
|
+
await respondEphemeral(params.respond, "You are not authorized to approve this request.");
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
for (const [index, resolveMethod] of resolveMethods.entries()) try {
|
|
1678
|
+
await resolveApprovalOverGateway({
|
|
1679
|
+
cfg: params.ctx.cfg,
|
|
1680
|
+
approvalId: parsedApproval.approvalId,
|
|
1681
|
+
decision: parsedApproval.decision,
|
|
1682
|
+
senderId: params.parsed.userId,
|
|
1683
|
+
resolveMethod,
|
|
1684
|
+
clientDisplayName: `Slack approval (${params.parsed.userId.trim() || "unknown"})`
|
|
1407
1685
|
});
|
|
1408
|
-
|
|
1686
|
+
try {
|
|
1687
|
+
await updateSlackInteractionMessage({
|
|
1688
|
+
ctx: params.ctx,
|
|
1689
|
+
channelId: params.parsed.channelId,
|
|
1690
|
+
messageTs: params.parsed.messageTs,
|
|
1691
|
+
text: params.parsed.typedBody.message?.text ?? "",
|
|
1692
|
+
blocks: []
|
|
1693
|
+
});
|
|
1694
|
+
} catch {}
|
|
1695
|
+
return true;
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
if (index + 1 < resolveMethods.length && isApprovalNotFoundError(error)) continue;
|
|
1698
|
+
params.ctx.runtime.log?.(`slack:interaction legacy approval resolve failed id=${parsedApproval.approvalId}: ${String(error)}`);
|
|
1699
|
+
throw error;
|
|
1700
|
+
}
|
|
1409
1701
|
return true;
|
|
1410
1702
|
}
|
|
1411
1703
|
async function dispatchSlackPluginInteraction(params) {
|
|
@@ -1572,7 +1864,7 @@ function buildSlackConfirmationBlocks(params) {
|
|
|
1572
1864
|
summary: params.parsed.actionSummary,
|
|
1573
1865
|
buttonText: params.parsed.typedActionWithText.text?.text
|
|
1574
1866
|
});
|
|
1575
|
-
|
|
1867
|
+
return params.originalBlocks.map((block) => {
|
|
1576
1868
|
const typedBlock = block;
|
|
1577
1869
|
if (typedBlock.type === "actions" && typedBlock.block_id === params.parsed.blockId) return {
|
|
1578
1870
|
type: "context",
|
|
@@ -1586,17 +1878,6 @@ function buildSlackConfirmationBlocks(params) {
|
|
|
1586
1878
|
};
|
|
1587
1879
|
return block;
|
|
1588
1880
|
});
|
|
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
1881
|
}
|
|
1601
1882
|
async function updateSlackLegacyBlockAction(params) {
|
|
1602
1883
|
const originalBlocks = params.parsed.typedBody.message?.blocks;
|
|
@@ -1631,12 +1912,27 @@ async function handleSlackBlockAction(params) {
|
|
|
1631
1912
|
if (!parsed) return;
|
|
1632
1913
|
if (isSlackReplyLinkAction(parsed)) return;
|
|
1633
1914
|
params.trackEvent?.();
|
|
1915
|
+
if (isSlackApprovalActionId(parsed.actionId)) {
|
|
1916
|
+
const approval = readSlackApprovalAction(parsed);
|
|
1917
|
+
if (!approval) {
|
|
1918
|
+
params.ctx.runtime.log?.(`slack:interaction drop malformed approval action user=${parsed.userId} channel=${parsed.channelId ?? "unknown"}`);
|
|
1919
|
+
await respondEphemeral(respond, "This approval action is invalid or expired.");
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
await handleSlackApprovalInteraction({
|
|
1923
|
+
ctx: params.ctx,
|
|
1924
|
+
parsed,
|
|
1925
|
+
approval,
|
|
1926
|
+
respond
|
|
1927
|
+
});
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1634
1930
|
const pluginInteractionData = buildSlackPluginInteractionData({
|
|
1635
1931
|
actionId: parsed.actionId,
|
|
1636
1932
|
summary: parsed.actionSummary
|
|
1637
1933
|
});
|
|
1638
1934
|
if (pluginInteractionData && isSlackReplyActionId(parsed.actionId)) {
|
|
1639
|
-
if (await
|
|
1935
|
+
if (await handleSlackLegacyApprovalInteraction({
|
|
1640
1936
|
ctx: params.ctx,
|
|
1641
1937
|
parsed,
|
|
1642
1938
|
pluginInteractionData,
|
|
@@ -2293,6 +2589,66 @@ function registerSlackMemberEvents(params) {
|
|
|
2293
2589
|
});
|
|
2294
2590
|
}
|
|
2295
2591
|
//#endregion
|
|
2592
|
+
//#region extensions/slack/src/monitor/event-scope.ts
|
|
2593
|
+
function resolveSlackEventScope(params) {
|
|
2594
|
+
const context = params.context ?? {};
|
|
2595
|
+
if (params.identity.kind !== "enterprise") return context.isEnterpriseInstall === true ? {
|
|
2596
|
+
ok: false,
|
|
2597
|
+
reason: "enterprise_event_for_workspace_account"
|
|
2598
|
+
} : { ok: true };
|
|
2599
|
+
const apiAppId = normalizeOptionalString((params.body && typeof params.body === "object" ? params.body : {}).api_app_id);
|
|
2600
|
+
if (!apiAppId) return {
|
|
2601
|
+
ok: false,
|
|
2602
|
+
reason: "missing_api_app_id"
|
|
2603
|
+
};
|
|
2604
|
+
if (params.identity.apiAppId && apiAppId !== params.identity.apiAppId) return {
|
|
2605
|
+
ok: false,
|
|
2606
|
+
reason: "wrong_app"
|
|
2607
|
+
};
|
|
2608
|
+
if (context.isEnterpriseInstall !== true) return {
|
|
2609
|
+
ok: false,
|
|
2610
|
+
reason: "not_enterprise_install"
|
|
2611
|
+
};
|
|
2612
|
+
const enterpriseId = normalizeOptionalString(context.enterpriseId);
|
|
2613
|
+
if (!enterpriseId) return {
|
|
2614
|
+
ok: false,
|
|
2615
|
+
reason: "missing_enterprise_id"
|
|
2616
|
+
};
|
|
2617
|
+
if (enterpriseId !== params.identity.enterpriseId) return {
|
|
2618
|
+
ok: false,
|
|
2619
|
+
reason: "wrong_enterprise"
|
|
2620
|
+
};
|
|
2621
|
+
const teamId = normalizeOptionalString(context.teamId);
|
|
2622
|
+
if (!teamId) return {
|
|
2623
|
+
ok: false,
|
|
2624
|
+
reason: "missing_team_id"
|
|
2625
|
+
};
|
|
2626
|
+
if (!/^T[A-Z0-9]+$/i.test(teamId)) return {
|
|
2627
|
+
ok: false,
|
|
2628
|
+
reason: "invalid_team_id"
|
|
2629
|
+
};
|
|
2630
|
+
if (!params.client) return {
|
|
2631
|
+
ok: false,
|
|
2632
|
+
reason: "missing_listener_client"
|
|
2633
|
+
};
|
|
2634
|
+
const uploadCompletionClient = getSlackListenerUploadCompletionClient({
|
|
2635
|
+
listenerClient: params.client,
|
|
2636
|
+
teamId,
|
|
2637
|
+
clientOptions: params.clientOptions
|
|
2638
|
+
});
|
|
2639
|
+
return {
|
|
2640
|
+
ok: true,
|
|
2641
|
+
scope: {
|
|
2642
|
+
apiAppId,
|
|
2643
|
+
enterpriseId,
|
|
2644
|
+
teamId,
|
|
2645
|
+
isEnterpriseInstall: true,
|
|
2646
|
+
client: params.client,
|
|
2647
|
+
...uploadCompletionClient ? { uploadCompletionClient } : {}
|
|
2648
|
+
}
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
//#endregion
|
|
2296
2652
|
//#region extensions/slack/src/monitor/events/message-subtype-handlers.ts
|
|
2297
2653
|
const SUBTYPE_HANDLER_REGISTRY = {
|
|
2298
2654
|
message_changed: {
|
|
@@ -2340,6 +2696,9 @@ function formatSlackInboundLogLine(params) {
|
|
|
2340
2696
|
function isSlackUserId(value) {
|
|
2341
2697
|
return /^[UW][A-Z0-9]+$/.test(value);
|
|
2342
2698
|
}
|
|
2699
|
+
function isBotAuthoredEnterpriseEvent(event) {
|
|
2700
|
+
return Boolean(normalizeOptionalString(event.bot_id)) || event.subtype === "bot_message";
|
|
2701
|
+
}
|
|
2343
2702
|
function addUserCandidate(candidates, value, botUserId) {
|
|
2344
2703
|
const id = normalizeOptionalString(value);
|
|
2345
2704
|
if (!id || id === botUserId || !isSlackUserId(id)) return;
|
|
@@ -2396,21 +2755,57 @@ function resolveAssistantMessageChangedInbound(params) {
|
|
|
2396
2755
|
event_ts: changed.event_ts,
|
|
2397
2756
|
assistant_thread: asOptionalRecord(message.assistant_thread) ?? asOptionalRecord(changed.assistant_thread),
|
|
2398
2757
|
files: Array.isArray(message.files) ? message.files : void 0,
|
|
2399
|
-
attachments: Array.isArray(message.attachments) ? message.attachments : void 0
|
|
2758
|
+
attachments: Array.isArray(message.attachments) ? message.attachments : void 0,
|
|
2759
|
+
blocks: Array.isArray(message.blocks) ? message.blocks : void 0
|
|
2400
2760
|
};
|
|
2401
2761
|
}
|
|
2402
2762
|
function registerSlackMessageEvents(params) {
|
|
2403
2763
|
const { ctx, handleSlackMessage } = params;
|
|
2404
|
-
const
|
|
2764
|
+
const resolveEventScope = (args) => {
|
|
2765
|
+
const resolved = resolveSlackEventScope({
|
|
2766
|
+
identity: ctx.installationIdentity,
|
|
2767
|
+
body: args.body,
|
|
2768
|
+
context: args.context,
|
|
2769
|
+
client: args.client,
|
|
2770
|
+
clientOptions: ctx.app.webClientOptions
|
|
2771
|
+
});
|
|
2772
|
+
if (!resolved.ok) {
|
|
2773
|
+
logVerbose(`slack: drop event (${resolved.reason})`);
|
|
2774
|
+
return null;
|
|
2775
|
+
}
|
|
2776
|
+
return resolved.scope;
|
|
2777
|
+
};
|
|
2778
|
+
const handleIncomingMessageEvent = async ({ event, body, context, client }) => {
|
|
2405
2779
|
try {
|
|
2780
|
+
const eventScope = resolveEventScope({
|
|
2781
|
+
body,
|
|
2782
|
+
context,
|
|
2783
|
+
client
|
|
2784
|
+
});
|
|
2785
|
+
if (eventScope === null) return;
|
|
2406
2786
|
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
2407
2787
|
const message = event;
|
|
2788
|
+
ctx.rememberSlackChannelType(message.channel, message.channel_type, eventScope);
|
|
2789
|
+
if (eventScope && isBotAuthoredEnterpriseEvent(message)) {
|
|
2790
|
+
logVerbose("slack: drop enterprise bot-authored message");
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
if (eventScope && message.subtype && message.subtype !== "file_share") {
|
|
2794
|
+
logVerbose(`slack: drop enterprise message subtype=${message.subtype}`);
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2408
2797
|
const assistantChangedInbound = resolveAssistantMessageChangedInbound({
|
|
2409
2798
|
event: message,
|
|
2410
2799
|
ctx
|
|
2411
2800
|
});
|
|
2412
2801
|
if (assistantChangedInbound) {
|
|
2413
|
-
await handleSlackMessage(assistantChangedInbound, {
|
|
2802
|
+
await handleSlackMessage(assistantChangedInbound, {
|
|
2803
|
+
source: "message",
|
|
2804
|
+
...eventScope ? {
|
|
2805
|
+
eventScope,
|
|
2806
|
+
awaitDispatch: true
|
|
2807
|
+
} : {}
|
|
2808
|
+
});
|
|
2414
2809
|
return;
|
|
2415
2810
|
}
|
|
2416
2811
|
if (message.subtype === "message_changed" && isSelfAttributedMessageChange({
|
|
@@ -2435,25 +2830,39 @@ function registerSlackMessageEvents(params) {
|
|
|
2435
2830
|
});
|
|
2436
2831
|
return;
|
|
2437
2832
|
}
|
|
2438
|
-
await handleSlackMessage(message, {
|
|
2833
|
+
await handleSlackMessage(message, {
|
|
2834
|
+
source: "message",
|
|
2835
|
+
...eventScope ? {
|
|
2836
|
+
eventScope,
|
|
2837
|
+
awaitDispatch: true
|
|
2838
|
+
} : {}
|
|
2839
|
+
});
|
|
2439
2840
|
} catch (err) {
|
|
2440
2841
|
ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
|
|
2441
2842
|
}
|
|
2442
2843
|
};
|
|
2443
|
-
ctx.app.event("message", async (
|
|
2444
|
-
await handleIncomingMessageEvent(
|
|
2445
|
-
event,
|
|
2446
|
-
body
|
|
2447
|
-
});
|
|
2844
|
+
ctx.app.event("message", async (args) => {
|
|
2845
|
+
await handleIncomingMessageEvent(args);
|
|
2448
2846
|
});
|
|
2449
|
-
ctx.app.event("app_mention", async (
|
|
2847
|
+
ctx.app.event("app_mention", async (args) => {
|
|
2848
|
+
const { event, body, context, client } = args;
|
|
2450
2849
|
try {
|
|
2850
|
+
const eventScope = resolveEventScope({
|
|
2851
|
+
body,
|
|
2852
|
+
context,
|
|
2853
|
+
client
|
|
2854
|
+
});
|
|
2855
|
+
if (eventScope === null) return;
|
|
2451
2856
|
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
2452
2857
|
const mention = event;
|
|
2858
|
+
if (eventScope && isBotAuthoredEnterpriseEvent(mention)) {
|
|
2859
|
+
logVerbose("slack: drop enterprise bot-authored app_mention");
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2453
2862
|
const channelType = normalizeSlackChannelType(mention.channel_type, mention.channel);
|
|
2454
2863
|
if (channelType === "im" || channelType === "mpim") return;
|
|
2455
2864
|
slackInboundLog.info(formatSlackInboundLogLine({
|
|
2456
|
-
workspaceId: ctx.teamId,
|
|
2865
|
+
workspaceId: eventScope?.teamId ?? ctx.teamId,
|
|
2457
2866
|
channelId: mention.channel,
|
|
2458
2867
|
channelType: channelType ?? "channel",
|
|
2459
2868
|
userId: normalizeOptionalString(mention.user) ?? "unknown",
|
|
@@ -2462,7 +2871,11 @@ function registerSlackMessageEvents(params) {
|
|
|
2462
2871
|
}));
|
|
2463
2872
|
await handleSlackMessage(mention, {
|
|
2464
2873
|
source: "app_mention",
|
|
2465
|
-
wasMentioned: true
|
|
2874
|
+
wasMentioned: true,
|
|
2875
|
+
...eventScope ? {
|
|
2876
|
+
eventScope,
|
|
2877
|
+
awaitDispatch: true
|
|
2878
|
+
} : {}
|
|
2466
2879
|
});
|
|
2467
2880
|
} catch (err) {
|
|
2468
2881
|
ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
|
|
@@ -2591,6 +3004,7 @@ function registerSlackMonitorEvents(params) {
|
|
|
2591
3004
|
ctx: params.ctx,
|
|
2592
3005
|
handleSlackMessage: params.handleSlackMessage
|
|
2593
3006
|
});
|
|
3007
|
+
if (params.ctx.installationIdentity.kind === "enterprise") return;
|
|
2594
3008
|
registerSlackReactionEvents({
|
|
2595
3009
|
ctx: params.ctx,
|
|
2596
3010
|
trackEvent: params.trackEvent
|
|
@@ -2609,6 +3023,7 @@ function registerSlackMonitorEvents(params) {
|
|
|
2609
3023
|
});
|
|
2610
3024
|
registerSlackHomeEvents({
|
|
2611
3025
|
ctx: params.ctx,
|
|
3026
|
+
slashCommandName: params.appHomeSlashCommandName,
|
|
2612
3027
|
trackEvent: params.trackEvent
|
|
2613
3028
|
});
|
|
2614
3029
|
registerSlackInteractionEvents({
|
|
@@ -2640,12 +3055,12 @@ const deliveredMessages = createPersistentDedupeCache({
|
|
|
2640
3055
|
}
|
|
2641
3056
|
}
|
|
2642
3057
|
});
|
|
2643
|
-
function makeKey(accountId, channelId, ts) {
|
|
2644
|
-
return `${accountId}:${channelId}:${ts}`;
|
|
3058
|
+
function makeKey(accountId, channelId, ts, teamId) {
|
|
3059
|
+
return `${accountId}:${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
|
|
2645
3060
|
}
|
|
2646
3061
|
async function hasSlackInboundMessageDelivery(params) {
|
|
2647
3062
|
if (!params.accountId || !params.channelId || !params.ts) return false;
|
|
2648
|
-
return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts));
|
|
3063
|
+
return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts, params.teamId));
|
|
2649
3064
|
}
|
|
2650
3065
|
async function recordSlackInboundMessageDeliveries(params) {
|
|
2651
3066
|
if (!params.accountId || params.messages.length === 0) return;
|
|
@@ -2653,7 +3068,7 @@ async function recordSlackInboundMessageDeliveries(params) {
|
|
|
2653
3068
|
const keys = /* @__PURE__ */ new Set();
|
|
2654
3069
|
for (const message of params.messages) {
|
|
2655
3070
|
if (!message.channel || !message.ts) continue;
|
|
2656
|
-
keys.add(makeKey(params.accountId, message.channel, message.ts));
|
|
3071
|
+
keys.add(makeKey(params.accountId, message.channel, message.ts, params.teamId));
|
|
2657
3072
|
}
|
|
2658
3073
|
await Promise.all(Array.from(keys, (key) => deliveredMessages.register(key, { deliveredAt }, { at: deliveredAt })));
|
|
2659
3074
|
}
|
|
@@ -2668,17 +3083,18 @@ function isSlackDirectMessageChannel(channelId) {
|
|
|
2668
3083
|
function isTopLevelSlackMessage(message) {
|
|
2669
3084
|
return !message.thread_ts && !message.parent_user_id;
|
|
2670
3085
|
}
|
|
2671
|
-
function buildTopLevelSlackConversationKey(message, accountId) {
|
|
3086
|
+
function buildTopLevelSlackConversationKey(message, accountId, teamId) {
|
|
2672
3087
|
if (!isTopLevelSlackMessage(message)) return null;
|
|
2673
3088
|
const senderId = resolveSlackSenderId(message);
|
|
2674
3089
|
if (!senderId) return null;
|
|
2675
|
-
return `slack:${accountId}:${message.channel}:${senderId}`;
|
|
3090
|
+
return `slack:${accountId}:${teamId ? `${teamId}:` : ""}${message.channel}:${senderId}`;
|
|
2676
3091
|
}
|
|
2677
|
-
function buildSlackDebounceKey(message, accountId) {
|
|
3092
|
+
function buildSlackDebounceKey(message, accountId, teamId) {
|
|
2678
3093
|
const senderId = resolveSlackSenderId(message);
|
|
2679
3094
|
if (!senderId) return null;
|
|
2680
3095
|
const messageTs = message.ts ?? message.event_ts;
|
|
2681
|
-
|
|
3096
|
+
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;
|
|
3097
|
+
return `slack:${accountId}:${teamId ? `${teamId}:` : ""}${threadKey}:${senderId}`;
|
|
2682
3098
|
}
|
|
2683
3099
|
//#endregion
|
|
2684
3100
|
//#region extensions/slack/src/monitor/thread-resolution.ts
|
|
@@ -2782,7 +3198,7 @@ function createSlackThreadTsResolver(params) {
|
|
|
2782
3198
|
}
|
|
2783
3199
|
//#endregion
|
|
2784
3200
|
//#region extensions/slack/src/monitor/message-handler.ts
|
|
2785
|
-
const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-
|
|
3201
|
+
const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-DofAvTtb.js"));
|
|
2786
3202
|
function createSlackDispatchCompletion() {
|
|
2787
3203
|
let resolve;
|
|
2788
3204
|
let reject;
|
|
@@ -2799,14 +3215,7 @@ const APP_MENTION_RETRY_TTL_MS = 6e4;
|
|
|
2799
3215
|
const RETRYABLE_FLUSH_MAX_ATTEMPTS = 3;
|
|
2800
3216
|
const RETRYABLE_FLUSH_RETRY_DELAY_MS = 1e3;
|
|
2801
3217
|
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
3218
|
function isRetryableSlackInboundError(error) {
|
|
2809
|
-
if (error instanceof SlackRetryableInboundError) return true;
|
|
2810
3219
|
return collectErrorGraphCandidates(error, (current) => [current.cause, current.error]).some((candidate) => REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE.test(formatErrorMessage(candidate)));
|
|
2811
3220
|
}
|
|
2812
3221
|
function shouldDebounceSlackMessage(message, cfg) {
|
|
@@ -2816,20 +3225,20 @@ function shouldDebounceSlackMessage(message, cfg) {
|
|
|
2816
3225
|
hasMedia: Boolean(message.files && message.files.length > 0)
|
|
2817
3226
|
});
|
|
2818
3227
|
}
|
|
2819
|
-
function buildSeenMessageKey(channelId, ts) {
|
|
3228
|
+
function buildSeenMessageKey(channelId, ts, teamId) {
|
|
2820
3229
|
if (!channelId || !ts) return null;
|
|
2821
|
-
return `${channelId}:${ts}`;
|
|
3230
|
+
return `${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
|
|
2822
3231
|
}
|
|
2823
3232
|
function createSlackMessageHandler(params) {
|
|
2824
|
-
const { ctx, account, trackEvent } = params;
|
|
3233
|
+
const { ctx, account, trackEvent, onPrepared } = params;
|
|
2825
3234
|
const { debounceMs, debouncer } = createChannelInboundDebouncer({
|
|
2826
3235
|
cfg: ctx.cfg,
|
|
2827
3236
|
channel: "slack",
|
|
2828
|
-
buildKey: (entry) => buildSlackDebounceKey(entry.message, ctx.accountId),
|
|
2829
|
-
shouldDebounce: (entry) => shouldDebounceSlackMessage(entry.message, ctx.cfg),
|
|
3237
|
+
buildKey: (entry) => buildSlackDebounceKey(entry.message, ctx.accountId, entry.opts.eventScope?.teamId),
|
|
3238
|
+
shouldDebounce: (entry) => !entry.opts.eventScope && shouldDebounceSlackMessage(entry.message, ctx.cfg),
|
|
2830
3239
|
onFlush: async (entries) => {
|
|
2831
3240
|
const retryEntries = (sourceError) => {
|
|
2832
|
-
if (!isRetryableSlackInboundError(sourceError)) return false;
|
|
3241
|
+
if (!isRetryableSlackInboundError(sourceError) || entries.some((entry) => entry.opts.eventScope)) return false;
|
|
2833
3242
|
const nextEntries = entries.map((entry) => {
|
|
2834
3243
|
if (entry.opts.dispatchCompletion) return null;
|
|
2835
3244
|
const retryAttempt = entry.opts.retryAttempt ?? 0;
|
|
@@ -2856,8 +3265,9 @@ function createSlackMessageHandler(params) {
|
|
|
2856
3265
|
await (async () => {
|
|
2857
3266
|
const last = entries.at(-1);
|
|
2858
3267
|
if (!last) return;
|
|
2859
|
-
const
|
|
2860
|
-
const
|
|
3268
|
+
const teamId = last.opts.eventScope?.teamId;
|
|
3269
|
+
const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId, teamId);
|
|
3270
|
+
const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId, teamId);
|
|
2861
3271
|
if (flushedKey && topLevelConversationKey) {
|
|
2862
3272
|
const pendingKeys = pendingTopLevelDebounceKeys.get(topLevelConversationKey);
|
|
2863
3273
|
if (pendingKeys) {
|
|
@@ -2871,7 +3281,7 @@ function createSlackMessageHandler(params) {
|
|
|
2871
3281
|
...last.message,
|
|
2872
3282
|
text: combinedText
|
|
2873
3283
|
};
|
|
2874
|
-
const seenMessageKey = buildSeenMessageKey(last.message.channel, last.message.ts);
|
|
3284
|
+
const seenMessageKey = buildSeenMessageKey(last.message.channel, last.message.ts, last.opts.eventScope?.teamId);
|
|
2875
3285
|
try {
|
|
2876
3286
|
const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
|
|
2877
3287
|
const { dispatchCompletion: _completion, awaitDispatch: _awaitDispatch, ...lastOpts } = last.opts;
|
|
@@ -2905,6 +3315,7 @@ function createSlackMessageHandler(params) {
|
|
|
2905
3315
|
}
|
|
2906
3316
|
})();
|
|
2907
3317
|
if (!prepared) return;
|
|
3318
|
+
onPrepared?.(prepared);
|
|
2908
3319
|
if (entries.length > 1) {
|
|
2909
3320
|
const ids = entries.map((entry) => entry.message.ts).filter(Boolean);
|
|
2910
3321
|
if (ids.length > 0) {
|
|
@@ -2917,20 +3328,22 @@ function createSlackMessageHandler(params) {
|
|
|
2917
3328
|
await dispatchPreparedSlackMessage(prepared);
|
|
2918
3329
|
await recordSlackInboundMessageDeliveries({
|
|
2919
3330
|
accountId: ctx.accountId,
|
|
3331
|
+
...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
|
|
2920
3332
|
messages: entries.map((entry) => entry.message)
|
|
2921
3333
|
});
|
|
2922
3334
|
} catch (error) {
|
|
2923
3335
|
if (!isRetryableSlackInboundError(error)) await recordSlackInboundMessageDeliveries({
|
|
2924
3336
|
accountId: ctx.accountId,
|
|
3337
|
+
...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
|
|
2925
3338
|
messages: entries.map((entry) => entry.message)
|
|
2926
3339
|
});
|
|
2927
3340
|
throw error;
|
|
2928
3341
|
}
|
|
2929
3342
|
} catch (error) {
|
|
2930
3343
|
if (isRetryableSlackInboundError(error)) for (const entry of entries) {
|
|
2931
|
-
const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts);
|
|
3344
|
+
const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts, entry.opts.eventScope?.teamId);
|
|
2932
3345
|
if (entrySeenKey) appMentionDispatchedKeys.delete(entrySeenKey);
|
|
2933
|
-
ctx.releaseSeenMessage(entry.message.channel, entry.message.ts);
|
|
3346
|
+
ctx.releaseSeenMessage(entry.message.channel, entry.message.ts, entry.opts.eventScope);
|
|
2934
3347
|
}
|
|
2935
3348
|
throw error;
|
|
2936
3349
|
}
|
|
@@ -2980,25 +3393,28 @@ function createSlackMessageHandler(params) {
|
|
|
2980
3393
|
async function enqueueSlackMessage(message, opts) {
|
|
2981
3394
|
if (opts.source === "message" && message.type !== "message") return;
|
|
2982
3395
|
if (opts.source === "message" && message.subtype && message.subtype !== "file_share" && message.subtype !== "bot_message" && message.subtype !== "thread_broadcast") return;
|
|
2983
|
-
|
|
3396
|
+
ctx.rememberSlackChannelType(message.channel, message.channel_type, opts.eventScope);
|
|
3397
|
+
const seenMessageKey = buildSeenMessageKey(message.channel, message.ts, opts.eventScope?.teamId);
|
|
2984
3398
|
if (seenMessageKey && await hasSlackInboundMessageDelivery({
|
|
2985
3399
|
accountId: ctx.accountId,
|
|
2986
3400
|
channelId: message.channel,
|
|
2987
|
-
ts: message.ts
|
|
3401
|
+
ts: message.ts,
|
|
3402
|
+
...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
|
|
2988
3403
|
})) return;
|
|
2989
|
-
const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts) : false;
|
|
3404
|
+
const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts, opts.eventScope) : false;
|
|
2990
3405
|
if (seenMessageKey && opts.source === "message" && !wasSeen) rememberAppMentionRetryKey(seenMessageKey);
|
|
2991
3406
|
if (seenMessageKey && wasSeen) {
|
|
2992
3407
|
if (opts.source !== "app_mention" || !consumeAppMentionRetryKey(seenMessageKey)) return;
|
|
2993
3408
|
}
|
|
2994
3409
|
trackEvent?.();
|
|
2995
|
-
const resolvedMessage = await threadTsResolver.resolve({
|
|
3410
|
+
const resolvedMessage = await (opts.eventScope ? createSlackThreadTsResolver({ client: opts.eventScope.client }) : threadTsResolver).resolve({
|
|
2996
3411
|
message,
|
|
2997
3412
|
source: opts.source
|
|
2998
3413
|
});
|
|
2999
|
-
const
|
|
3000
|
-
const
|
|
3001
|
-
const
|
|
3414
|
+
const teamId = opts.eventScope?.teamId;
|
|
3415
|
+
const debounceKey = buildSlackDebounceKey(resolvedMessage, ctx.accountId, teamId);
|
|
3416
|
+
const conversationKey = buildTopLevelSlackConversationKey(resolvedMessage, ctx.accountId, teamId);
|
|
3417
|
+
const canDebounce = !opts.eventScope && debounceMs > 0 && shouldDebounceSlackMessage(resolvedMessage, ctx.cfg);
|
|
3002
3418
|
if (!canDebounce && conversationKey) {
|
|
3003
3419
|
const pendingKeys = pendingTopLevelDebounceKeys.get(conversationKey);
|
|
3004
3420
|
if (pendingKeys && pendingKeys.size > 0) {
|
|
@@ -3029,6 +3445,203 @@ function createSlackMessageHandler(params) {
|
|
|
3029
3445
|
};
|
|
3030
3446
|
}
|
|
3031
3447
|
//#endregion
|
|
3448
|
+
//#region extensions/slack/src/monitor/presence-monitor.ts
|
|
3449
|
+
const SLACK_PRESENCE_GREETING_COOLDOWN_MS = 480 * 60 * 1e3;
|
|
3450
|
+
const SLACK_PRESENCE_POLL_INTERVAL_MS = 6e4;
|
|
3451
|
+
const SLACK_PRESENCE_AUTO_MAX_PARTICIPANTS = 8;
|
|
3452
|
+
const SLACK_PRESENCE_TARGET_TTL_MS = 1440 * 60 * 1e3;
|
|
3453
|
+
const SLACK_PRESENCE_MAX_POLLS_PER_INTERVAL = 45;
|
|
3454
|
+
const SLACK_PRESENCE_MAX_TARGETS = 2e3;
|
|
3455
|
+
function resolveMode(channelConfig, accountConfig) {
|
|
3456
|
+
return channelConfig?.mode ?? accountConfig?.mode ?? "off";
|
|
3457
|
+
}
|
|
3458
|
+
function hasSlackPresenceEventsEnabled(params) {
|
|
3459
|
+
if (resolveMode(void 0, params.account) !== "off") return true;
|
|
3460
|
+
return Object.values(params.channels ?? {}).some((entry) => resolveMode(entry?.presenceEvents, void 0) !== "off");
|
|
3461
|
+
}
|
|
3462
|
+
function isTargetEligible(target) {
|
|
3463
|
+
if (target.mode === "on") return true;
|
|
3464
|
+
if (target.autoEligibleKind === "channel") return false;
|
|
3465
|
+
return target.participants.size <= SLACK_PRESENCE_AUTO_MAX_PARTICIPANTS;
|
|
3466
|
+
}
|
|
3467
|
+
function formatSlackPresenceEvent(target, userId) {
|
|
3468
|
+
return [
|
|
3469
|
+
"Slack presence event:",
|
|
3470
|
+
`A human participant became active on Slack after being observed away: user_id=${JSON.stringify(userId)} channel_id=${JSON.stringify(target.channelId)}${target.threadId ? ` thread_ts=${JSON.stringify(target.threadId)}` : ""}.`,
|
|
3471
|
+
"Before greeting, retrieve relevant memory and wiki context for this immutable user_id, including a known timezone when available. Use their local time; if their timezone is unknown, do not guess.",
|
|
3472
|
+
"Send at most one short, natural greeting in this Slack conversation. Do not reveal private memory. If no greeting is appropriate, stay silent."
|
|
3473
|
+
].join("\n");
|
|
3474
|
+
}
|
|
3475
|
+
function resolveObservedTarget(params) {
|
|
3476
|
+
const { prepared } = params;
|
|
3477
|
+
const userId = prepared.message.user?.trim();
|
|
3478
|
+
if (!userId || prepared.message.bot_id || prepared.message.subtype === "bot_message") return null;
|
|
3479
|
+
const mode = resolveMode(prepared.channelConfig?.presenceEvents, params.accountConfig);
|
|
3480
|
+
if (mode === "off") return null;
|
|
3481
|
+
const channelId = prepared.message.channel;
|
|
3482
|
+
const rawThreadId = prepared.ctxPayload.MessageThreadId ?? prepared.ctxPayload.TransportThreadId ?? void 0;
|
|
3483
|
+
const threadId = rawThreadId === void 0 ? void 0 : String(rawThreadId);
|
|
3484
|
+
const channelType = prepared.message.channel_type;
|
|
3485
|
+
const autoEligibleKind = prepared.isDirectMessage ? "direct" : channelType === "mpim" ? "group" : threadId ? "thread" : "channel";
|
|
3486
|
+
if (mode === "auto" && autoEligibleKind === "channel") return null;
|
|
3487
|
+
return {
|
|
3488
|
+
key: `${channelId}${threadId ? `:thread:${threadId}` : ":top"}`,
|
|
3489
|
+
mode,
|
|
3490
|
+
channelId,
|
|
3491
|
+
threadId,
|
|
3492
|
+
to: prepared.isDirectMessage ? `user:${userId}` : `channel:${channelId}`,
|
|
3493
|
+
sessionKey: prepared.route.sessionKey,
|
|
3494
|
+
agentId: prepared.route.agentId,
|
|
3495
|
+
participants: /* @__PURE__ */ new Map([[userId, params.nowMs]]),
|
|
3496
|
+
lastActivityAtMs: params.nowMs,
|
|
3497
|
+
autoEligibleKind
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
function createSlackPresenceMonitor(params) {
|
|
3501
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3502
|
+
const presenceByUser = /* @__PURE__ */ new Map();
|
|
3503
|
+
const nowMs = params.nowMs ?? Date.now;
|
|
3504
|
+
const enqueue = params.enqueue ?? enqueueSystemEvent;
|
|
3505
|
+
const wake = params.wake ?? requestHeartbeat;
|
|
3506
|
+
let pollOffset = 0;
|
|
3507
|
+
let timer;
|
|
3508
|
+
let activePoll;
|
|
3509
|
+
let stopped = false;
|
|
3510
|
+
const pruneTargets = (now) => {
|
|
3511
|
+
for (const [key, target] of targets) if (now - target.lastActivityAtMs >= SLACK_PRESENCE_TARGET_TTL_MS) targets.delete(key);
|
|
3512
|
+
while (targets.size > SLACK_PRESENCE_MAX_TARGETS) {
|
|
3513
|
+
const oldestKey = targets.keys().next().value;
|
|
3514
|
+
if (typeof oldestKey !== "string") break;
|
|
3515
|
+
targets.delete(oldestKey);
|
|
3516
|
+
}
|
|
3517
|
+
const eligibleUsers = new Set(Array.from(targets.values()).filter(isTargetEligible).flatMap((target) => Array.from(target.participants.keys())));
|
|
3518
|
+
for (const userId of presenceByUser.keys()) if (!eligibleUsers.has(userId)) presenceByUser.delete(userId);
|
|
3519
|
+
};
|
|
3520
|
+
const observe = (prepared) => {
|
|
3521
|
+
const now = nowMs();
|
|
3522
|
+
pruneTargets(now);
|
|
3523
|
+
const observed = resolveObservedTarget({
|
|
3524
|
+
prepared,
|
|
3525
|
+
accountConfig: params.accountConfig,
|
|
3526
|
+
nowMs: now
|
|
3527
|
+
});
|
|
3528
|
+
if (!observed) return;
|
|
3529
|
+
const current = targets.get(observed.key);
|
|
3530
|
+
if (current) {
|
|
3531
|
+
current.mode = observed.mode;
|
|
3532
|
+
current.sessionKey = observed.sessionKey;
|
|
3533
|
+
current.agentId = observed.agentId;
|
|
3534
|
+
current.to = observed.to;
|
|
3535
|
+
current.lastActivityAtMs = now;
|
|
3536
|
+
for (const [participant, observedAt] of observed.participants) current.participants.set(participant, observedAt);
|
|
3537
|
+
targets.delete(observed.key);
|
|
3538
|
+
targets.set(observed.key, current);
|
|
3539
|
+
} else targets.set(observed.key, observed);
|
|
3540
|
+
pruneTargets(now);
|
|
3541
|
+
};
|
|
3542
|
+
const emitTransition = (userId, now) => {
|
|
3543
|
+
const target = Array.from(targets.values()).filter((candidate) => candidate.participants.has(userId) && isTargetEligible(candidate)).toSorted((a, b) => (b.participants.get(userId) ?? 0) - (a.participants.get(userId) ?? 0))[0];
|
|
3544
|
+
if (!target) return;
|
|
3545
|
+
const cooldownKey = `${params.accountId}:${userId}`;
|
|
3546
|
+
let reserved;
|
|
3547
|
+
try {
|
|
3548
|
+
reserved = params.cooldownStore.registerIfAbsent(cooldownKey, now, { ttlMs: SLACK_PRESENCE_GREETING_COOLDOWN_MS });
|
|
3549
|
+
} catch (err) {
|
|
3550
|
+
params.error?.(`slack presence cooldown persistence failed: ${String(err)}`);
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
if (!reserved) return;
|
|
3554
|
+
if (!enqueue(formatSlackPresenceEvent(target, userId), {
|
|
3555
|
+
sessionKey: target.sessionKey,
|
|
3556
|
+
contextKey: `slack:presence-active:${params.accountId}:${userId}`,
|
|
3557
|
+
deliveryContext: {
|
|
3558
|
+
channel: "slack",
|
|
3559
|
+
to: target.to,
|
|
3560
|
+
accountId: params.accountId,
|
|
3561
|
+
threadId: target.threadId
|
|
3562
|
+
}
|
|
3563
|
+
})) {
|
|
3564
|
+
params.cooldownStore.delete(cooldownKey);
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
wake({
|
|
3568
|
+
source: "notifications-event",
|
|
3569
|
+
intent: "immediate",
|
|
3570
|
+
reason: "wake",
|
|
3571
|
+
agentId: target.agentId,
|
|
3572
|
+
sessionKey: target.sessionKey,
|
|
3573
|
+
heartbeat: {
|
|
3574
|
+
target: "slack",
|
|
3575
|
+
to: target.to,
|
|
3576
|
+
accountId: params.accountId
|
|
3577
|
+
}
|
|
3578
|
+
});
|
|
3579
|
+
};
|
|
3580
|
+
const performPoll = async () => {
|
|
3581
|
+
const now = nowMs();
|
|
3582
|
+
pruneTargets(now);
|
|
3583
|
+
const candidates = Array.from(new Set(Array.from(targets.values()).filter(isTargetEligible).flatMap((target) => Array.from(target.participants.keys())))).toSorted();
|
|
3584
|
+
if (candidates.length === 0) return;
|
|
3585
|
+
const count = Math.min(candidates.length, SLACK_PRESENCE_MAX_POLLS_PER_INTERVAL);
|
|
3586
|
+
const selected = Array.from({ length: count }, (_, index) => candidates[(pollOffset + index) % candidates.length]).filter((userId) => Boolean(userId));
|
|
3587
|
+
pollOffset = (pollOffset + count) % candidates.length;
|
|
3588
|
+
for (const userId of selected) {
|
|
3589
|
+
if (stopped) return;
|
|
3590
|
+
try {
|
|
3591
|
+
const response = await params.client.getPresence({ user: userId });
|
|
3592
|
+
if (stopped) return;
|
|
3593
|
+
const next = response.presence === "active" || response.presence === "away" ? response.presence : void 0;
|
|
3594
|
+
if (!next) continue;
|
|
3595
|
+
const previous = presenceByUser.get(userId);
|
|
3596
|
+
presenceByUser.set(userId, next);
|
|
3597
|
+
if (previous === "away" && next === "active") emitTransition(userId, now);
|
|
3598
|
+
} catch (err) {
|
|
3599
|
+
if (stopped) return;
|
|
3600
|
+
params.error?.(`slack presence poll failed for user ${userId}: ${String(err)}`);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
const pollOnce = () => {
|
|
3605
|
+
if (stopped) return Promise.resolve();
|
|
3606
|
+
if (activePoll) return activePoll;
|
|
3607
|
+
const run = performPoll().finally(() => {
|
|
3608
|
+
if (activePoll === run) activePoll = void 0;
|
|
3609
|
+
});
|
|
3610
|
+
activePoll = run;
|
|
3611
|
+
return run;
|
|
3612
|
+
};
|
|
3613
|
+
return {
|
|
3614
|
+
observe,
|
|
3615
|
+
pollOnce,
|
|
3616
|
+
start: () => {
|
|
3617
|
+
if (timer) return;
|
|
3618
|
+
stopped = false;
|
|
3619
|
+
params.log?.(`slack presence polling enabled for account ${params.accountId}`);
|
|
3620
|
+
timer = setInterval(() => void pollOnce(), SLACK_PRESENCE_POLL_INTERVAL_MS);
|
|
3621
|
+
timer.unref?.();
|
|
3622
|
+
},
|
|
3623
|
+
stop: async () => {
|
|
3624
|
+
stopped = true;
|
|
3625
|
+
if (timer) {
|
|
3626
|
+
clearInterval(timer);
|
|
3627
|
+
timer = void 0;
|
|
3628
|
+
}
|
|
3629
|
+
await activePoll;
|
|
3630
|
+
}
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
//#endregion
|
|
3634
|
+
//#region extensions/slack/src/monitor/presence-cooldown-store.ts
|
|
3635
|
+
const SLACK_PRESENCE_COOLDOWN_MAX_ENTRIES = 25e3;
|
|
3636
|
+
function openSlackPresenceCooldownStore() {
|
|
3637
|
+
return getSlackRuntime().state.openSyncKeyedStore({
|
|
3638
|
+
namespace: "presence-greeting-cooldowns",
|
|
3639
|
+
maxEntries: SLACK_PRESENCE_COOLDOWN_MAX_ENTRIES,
|
|
3640
|
+
overflowPolicy: "reject-new",
|
|
3641
|
+
defaultTtlMs: SLACK_PRESENCE_GREETING_COOLDOWN_MS
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
//#endregion
|
|
3032
3645
|
//#region extensions/slack/src/monitor/reconnect-policy.ts
|
|
3033
3646
|
const SLACK_AUTH_ERROR_RE = /account_inactive|invalid_auth|token_revoked|token_expired|not_authed|org_login_required|team_access_not_granted|user_removed_from_team|team_disabled|missing_scope|cannot_find_service|invalid_token/i;
|
|
3034
3647
|
const NO_ERROR_DETAIL = "no error detail";
|
|
@@ -3217,13 +3830,15 @@ function resolveSlackBoltInterop(params) {
|
|
|
3217
3830
|
};
|
|
3218
3831
|
throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
|
|
3219
3832
|
}
|
|
3220
|
-
function publishSlackConnectedStatus(setStatus
|
|
3833
|
+
function publishSlackConnectedStatus(setStatus, identityHealth = {
|
|
3834
|
+
healthState: "healthy",
|
|
3835
|
+
lastError: null
|
|
3836
|
+
}) {
|
|
3221
3837
|
if (!setStatus) return;
|
|
3222
3838
|
setStatus({
|
|
3223
3839
|
connected: true,
|
|
3224
3840
|
lastConnectedAt: Date.now(),
|
|
3225
|
-
|
|
3226
|
-
lastError: null
|
|
3841
|
+
...identityHealth
|
|
3227
3842
|
});
|
|
3228
3843
|
}
|
|
3229
3844
|
function publishSlackDisconnectedStatus(setStatus, error) {
|
|
@@ -3389,8 +4004,11 @@ async function gracefulStopSlackApp(app) {
|
|
|
3389
4004
|
}
|
|
3390
4005
|
function formatSlackResolvedLabel(params) {
|
|
3391
4006
|
const extras = params.extra?.filter(Boolean) ?? [];
|
|
3392
|
-
const
|
|
3393
|
-
|
|
4007
|
+
const display = params.name ?? params.id;
|
|
4008
|
+
if (params.input === params.id && !params.name && extras.length === 0) return null;
|
|
4009
|
+
const details = [...params.input === params.id || display === params.id ? [] : [`id:${params.id}`], ...extras];
|
|
4010
|
+
const suffix = details.length > 0 ? ` (${details.join(", ")})` : "";
|
|
4011
|
+
return `${params.input}→${display}${suffix}`;
|
|
3394
4012
|
}
|
|
3395
4013
|
function formatSlackChannelResolved(entry) {
|
|
3396
4014
|
const id = entry.id ?? entry.input;
|
|
@@ -3511,6 +4129,25 @@ function createSlackExternalArgMenuStore() {
|
|
|
3511
4129
|
};
|
|
3512
4130
|
}
|
|
3513
4131
|
//#endregion
|
|
4132
|
+
//#region extensions/slack/src/monitor/response-url-budget.ts
|
|
4133
|
+
const SLACK_RESPONSE_URL_MAX_CALLS = 5;
|
|
4134
|
+
var SlackResponseAlreadyReportedError = class extends Error {};
|
|
4135
|
+
function isSlackResponseAlreadyReportedError(error) {
|
|
4136
|
+
return error instanceof SlackResponseAlreadyReportedError;
|
|
4137
|
+
}
|
|
4138
|
+
/** Count every response_url attempt, including requests Slack rejects. */
|
|
4139
|
+
function createSlackResponseUrlBudget(respond, maxCalls = SLACK_RESPONSE_URL_MAX_CALLS) {
|
|
4140
|
+
let remaining = maxCalls;
|
|
4141
|
+
return {
|
|
4142
|
+
remaining: () => remaining,
|
|
4143
|
+
respond: async (payload) => {
|
|
4144
|
+
if (remaining <= 0) throw new Error(`Slack response_url cannot be used more than ${String(maxCalls)} times.`);
|
|
4145
|
+
remaining -= 1;
|
|
4146
|
+
return await respond(payload);
|
|
4147
|
+
}
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
//#endregion
|
|
3514
4151
|
//#region extensions/slack/src/monitor/room-context.ts
|
|
3515
4152
|
function resolveSlackRoomContextHints(params) {
|
|
3516
4153
|
const untrustedChannelMetadata = params.isRoomish ? buildUntrustedChannelMetadata({
|
|
@@ -3541,7 +4178,7 @@ const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
|
|
|
3541
4178
|
const SLACK_HEADER_TEXT_MAX = 150;
|
|
3542
4179
|
const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = 47;
|
|
3543
4180
|
const loadSlashCommandsRuntime = createLazyRuntimeModule(() => import("./slash-commands.runtime-BTTMlmwy.js"));
|
|
3544
|
-
const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-
|
|
4181
|
+
const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-CSja9VuD.js"));
|
|
3545
4182
|
const loadSlackPluginCommandsRuntime = createLazyRuntimeModule(() => import("./slash-plugin-commands.runtime-Dosfcx3X.js"));
|
|
3546
4183
|
const loadSlashSkillCommandsRuntime = createLazyRuntimeModule(() => import("./slash-skill-commands.runtime-CD1J2ODY.js"));
|
|
3547
4184
|
function resolveSlackCommandMenuModelContext(params) {
|
|
@@ -3771,7 +4408,12 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
3771
4408
|
let supportsExternalArgMenus = typeof ctx.app.options === "function";
|
|
3772
4409
|
const slashCommand = resolveSlackSlashCommandConfig(ctx.slashCommand ?? account.config.slashCommand);
|
|
3773
4410
|
const handleSlashCommand = async (p) => {
|
|
3774
|
-
const { command, ack, respond, body, prompt, commandArgs, commandDefinition } = p;
|
|
4411
|
+
const { command, ack, respond: respondWithoutBudget, body, prompt, commandArgs, commandDefinition } = p;
|
|
4412
|
+
const responseBudget = p.responseTransport === "web-api" ? {
|
|
4413
|
+
respond: respondWithoutBudget,
|
|
4414
|
+
remaining: () => void 0
|
|
4415
|
+
} : createSlackResponseUrlBudget(respondWithoutBudget);
|
|
4416
|
+
const respond = responseBudget.respond;
|
|
3775
4417
|
const cfg = getRuntimeConfigSnapshot() ?? ctx.cfg;
|
|
3776
4418
|
try {
|
|
3777
4419
|
if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
@@ -3887,7 +4529,7 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
3887
4529
|
modeWhenAccessGroupsOff: "configured"
|
|
3888
4530
|
});
|
|
3889
4531
|
const senderGate = slashIngress.senderAccess.gate;
|
|
3890
|
-
if (
|
|
4532
|
+
if (isRoomish && senderGate?.allowed === false) {
|
|
3891
4533
|
await respond({
|
|
3892
4534
|
text: "You are not authorized to use this command here.",
|
|
3893
4535
|
response_type: "ephemeral"
|
|
@@ -4062,15 +4704,19 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4062
4704
|
cfg,
|
|
4063
4705
|
channel: "slack",
|
|
4064
4706
|
accountId: route.accountId
|
|
4065
|
-
})
|
|
4707
|
+
}),
|
|
4708
|
+
responseBudget
|
|
4066
4709
|
});
|
|
4067
4710
|
};
|
|
4711
|
+
const pendingSlashReplies = [];
|
|
4068
4712
|
const { counts } = await dispatchReplyWithDispatcher({
|
|
4069
4713
|
ctx: ctxPayload,
|
|
4070
4714
|
cfg,
|
|
4071
4715
|
dispatcherOptions: {
|
|
4072
4716
|
...replyPipeline,
|
|
4073
|
-
deliver: async (payload) =>
|
|
4717
|
+
deliver: async (payload) => {
|
|
4718
|
+
pendingSlashReplies.push(payload);
|
|
4719
|
+
},
|
|
4074
4720
|
onError: (err, info) => {
|
|
4075
4721
|
runtime.error?.(danger(`slack slash ${info.kind} reply failed: ${formatSlackError(err)}`));
|
|
4076
4722
|
}
|
|
@@ -4080,10 +4726,11 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4080
4726
|
onModelSelected
|
|
4081
4727
|
}
|
|
4082
4728
|
});
|
|
4083
|
-
if (
|
|
4729
|
+
if (pendingSlashReplies.length > 0) await deliverSlashPayloads(pendingSlashReplies);
|
|
4730
|
+
else if (counts.final + counts.tool + counts.block === 0) await deliverSlashPayloads([]);
|
|
4084
4731
|
} catch (err) {
|
|
4085
4732
|
runtime.error?.(danger(`slack slash handler failed: ${formatErrorMessage(err)}`));
|
|
4086
|
-
await respond({
|
|
4733
|
+
if (!isSlackResponseAlreadyReportedError(err) && responseBudget.remaining() !== 0) await respond({
|
|
4087
4734
|
text: "Sorry, something went wrong handling that command.",
|
|
4088
4735
|
response_type: "ephemeral"
|
|
4089
4736
|
});
|
|
@@ -4117,7 +4764,16 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4117
4764
|
nativeCommands.push(pluginCommand);
|
|
4118
4765
|
}
|
|
4119
4766
|
}
|
|
4120
|
-
if (
|
|
4767
|
+
if (slashCommand.enabled) ctx.app.command(buildSlackSlashCommandMatcher(slashCommand.name), async ({ command, ack, respond, body }) => {
|
|
4768
|
+
await handleSlashCommand({
|
|
4769
|
+
command,
|
|
4770
|
+
ack,
|
|
4771
|
+
respond,
|
|
4772
|
+
body,
|
|
4773
|
+
prompt: command.text?.trim() ?? ""
|
|
4774
|
+
});
|
|
4775
|
+
});
|
|
4776
|
+
else if (nativeCommands.length > 0) {
|
|
4121
4777
|
if (!slashCommandsRuntime) throw new Error("Missing commands runtime for native Slack commands.");
|
|
4122
4778
|
for (const command of nativeCommands) ctx.app.command(`/${command.name}`, async ({ command: cmd, ack, respond, body }) => {
|
|
4123
4779
|
const commandDefinition = slashCommandsRuntime.findCommandByNativeName(command.name, "slack");
|
|
@@ -4134,17 +4790,12 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4134
4790
|
commandDefinition: commandDefinition ?? void 0
|
|
4135
4791
|
});
|
|
4136
4792
|
});
|
|
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;
|
|
4793
|
+
} else logVerbose("slack: slash commands disabled");
|
|
4794
|
+
const registration = nativeCommands.length > 0 ? { mode: "native" } : slashCommand.enabled ? {
|
|
4795
|
+
mode: "single",
|
|
4796
|
+
name: slashCommand.name
|
|
4797
|
+
} : { mode: "disabled" };
|
|
4798
|
+
if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) return registration;
|
|
4148
4799
|
const registerArgOptions = () => {
|
|
4149
4800
|
const appWithOptions = ctx.app;
|
|
4150
4801
|
if (typeof appWithOptions.options !== "function") return;
|
|
@@ -4190,21 +4841,24 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4190
4841
|
}
|
|
4191
4842
|
const registerArgAction = (actionId) => {
|
|
4192
4843
|
ctx.app.action(actionId, async (args) => {
|
|
4193
|
-
const { ack, body
|
|
4844
|
+
const { ack, body } = args;
|
|
4845
|
+
const respond = args.respond;
|
|
4194
4846
|
const action = args.action;
|
|
4195
4847
|
await ack();
|
|
4196
4848
|
if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
4197
4849
|
runtime.log?.("slack: drop slash arg action payload (mismatched app/team)");
|
|
4198
4850
|
return;
|
|
4199
4851
|
}
|
|
4200
|
-
const respondFn = respond ?? (async (
|
|
4852
|
+
const respondFn = respond ?? (async (message) => {
|
|
4201
4853
|
if (!body.channel?.id || !body.user?.id) return;
|
|
4854
|
+
const payload = typeof message === "string" ? { text: message } : message;
|
|
4202
4855
|
await ctx.app.client.chat.postEphemeral({
|
|
4203
4856
|
token: ctx.botToken,
|
|
4204
4857
|
channel: body.channel.id,
|
|
4205
4858
|
user: body.user.id,
|
|
4206
|
-
text: payload.text,
|
|
4207
|
-
blocks: payload.blocks
|
|
4859
|
+
text: payload.text ?? "",
|
|
4860
|
+
...payload.blocks ? { blocks: payload.blocks } : {},
|
|
4861
|
+
...typeof payload.mrkdwn === "boolean" ? { mrkdwn: payload.mrkdwn } : {}
|
|
4208
4862
|
});
|
|
4209
4863
|
});
|
|
4210
4864
|
const parsed = parseSlackCommandArgValue(action?.value ?? action?.selected_option?.value);
|
|
@@ -4240,6 +4894,7 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4240
4894
|
command: commandPayload,
|
|
4241
4895
|
ack: async () => {},
|
|
4242
4896
|
respond: respondFn,
|
|
4897
|
+
responseTransport: respond ? "response-url" : "web-api",
|
|
4243
4898
|
body,
|
|
4244
4899
|
prompt,
|
|
4245
4900
|
commandArgs,
|
|
@@ -4248,6 +4903,7 @@ async function registerSlackMonitorSlashCommands(params) {
|
|
|
4248
4903
|
});
|
|
4249
4904
|
};
|
|
4250
4905
|
registerArgAction(SLACK_COMMAND_ARG_ACTION_LISTENER);
|
|
4906
|
+
return registration;
|
|
4251
4907
|
}
|
|
4252
4908
|
//#endregion
|
|
4253
4909
|
//#region extensions/slack/src/monitor/provider.ts
|
|
@@ -4262,7 +4918,7 @@ async function getSlackBoltInterop() {
|
|
|
4262
4918
|
}
|
|
4263
4919
|
return slackBoltInterop;
|
|
4264
4920
|
}
|
|
4265
|
-
const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-
|
|
4921
|
+
const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-DMmgtvg4.js"));
|
|
4266
4922
|
const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
4267
4923
|
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
|
|
4268
4924
|
function resolveStableSlackUserIdEntry(raw) {
|
|
@@ -4336,6 +4992,19 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4336
4992
|
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
|
4337
4993
|
const mainKey = normalizeMainKey(sessionCfg?.mainKey);
|
|
4338
4994
|
const slackMode = opts.mode ?? account.config.mode ?? "socket";
|
|
4995
|
+
const enterpriseOrgInstall = account.config.enterpriseOrgInstall === true;
|
|
4996
|
+
if (enterpriseOrgInstall && slackMode === "relay") throw new Error(`Slack Enterprise Grid org account "${account.accountId}" requires direct socket or HTTP delivery; relay mode is unsupported`);
|
|
4997
|
+
if (enterpriseOrgInstall && account.config.execApprovals?.enabled === true) throw new Error(`Slack Enterprise Grid org account "${account.accountId}" does not support Slack-native exec approvals`);
|
|
4998
|
+
if (enterpriseOrgInstall) {
|
|
4999
|
+
assertEnterpriseSlackPolicyConfig({
|
|
5000
|
+
config: account.config,
|
|
5001
|
+
accountId: account.accountId
|
|
5002
|
+
});
|
|
5003
|
+
assertNoEnterpriseSlackBindings({
|
|
5004
|
+
cfg,
|
|
5005
|
+
accountId: account.accountId
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
4339
5008
|
const slackWebhookPath = normalizeSlackWebhookPath(account.config.webhookPath);
|
|
4340
5009
|
const signingSecret = normalizeResolvedSecretInputString({
|
|
4341
5010
|
value: account.config.signingSecret,
|
|
@@ -4363,6 +5032,12 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4363
5032
|
cfg,
|
|
4364
5033
|
accountId: account.accountId
|
|
4365
5034
|
});
|
|
5035
|
+
if (enterpriseOrgInstall) assertEnterpriseSlackDmPolicy({
|
|
5036
|
+
accountId: account.accountId,
|
|
5037
|
+
dmEnabled,
|
|
5038
|
+
dmPolicy,
|
|
5039
|
+
allowFrom
|
|
5040
|
+
});
|
|
4366
5041
|
const groupDmEnabled = dmConfig?.groupEnabled ?? false;
|
|
4367
5042
|
const groupDmChannels = dmConfig?.groupChannels;
|
|
4368
5043
|
let channelsConfig = slackCfg.channels;
|
|
@@ -4432,34 +5107,41 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4432
5107
|
}) : () => {};
|
|
4433
5108
|
let botUserId = "";
|
|
4434
5109
|
let botId = "";
|
|
4435
|
-
let teamId = "";
|
|
4436
|
-
let apiAppId = "";
|
|
4437
5110
|
const expectedApiAppIdFromAppToken = slackMode === "socket" ? parseApiAppIdFromAppToken(appToken) : void 0;
|
|
4438
|
-
let authTestFailed = false;
|
|
4439
5111
|
let authTestError;
|
|
4440
5112
|
let authIdentityWarning;
|
|
5113
|
+
let authTestIdentity;
|
|
4441
5114
|
try {
|
|
4442
|
-
const auth = await
|
|
5115
|
+
const auth = await createSlackStartupAuthClient(botToken, clientOptions).auth.test();
|
|
4443
5116
|
const authUserId = normalizeOptionalString(auth.user_id) ?? "";
|
|
4444
5117
|
botId = normalizeOptionalString(auth.bot_id) ?? "";
|
|
4445
5118
|
botUserId = botId ? authUserId : "";
|
|
4446
|
-
|
|
4447
|
-
apiAppId = auth.api_app_id ?? "";
|
|
5119
|
+
authTestIdentity = auth;
|
|
4448
5120
|
authIdentityWarning = formatSlackBotTokenIdentityWarning({
|
|
4449
5121
|
auth,
|
|
4450
5122
|
accountId: account.accountId
|
|
4451
5123
|
});
|
|
4452
|
-
if (!authUserId)
|
|
4453
|
-
authTestFailed = true;
|
|
4454
|
-
authTestError = "auth.test returned no user_id";
|
|
4455
|
-
}
|
|
5124
|
+
if (!authUserId && !enterpriseOrgInstall) authTestError = "auth.test returned no user_id";
|
|
4456
5125
|
} catch (err) {
|
|
4457
|
-
authTestFailed = true;
|
|
4458
5126
|
authTestError = err instanceof Error ? err.message : String(err);
|
|
4459
5127
|
}
|
|
4460
|
-
|
|
5128
|
+
const installationIdentity = resolveSlackInstallationIdentity({
|
|
5129
|
+
enterpriseOrgInstall,
|
|
5130
|
+
auth: authTestError === void 0 ? authTestIdentity : void 0,
|
|
5131
|
+
authError: authTestError,
|
|
5132
|
+
transportApiAppId: expectedApiAppIdFromAppToken
|
|
5133
|
+
});
|
|
5134
|
+
const teamId = installationIdentity.kind === "workspace" ? installationIdentity.teamId : "";
|
|
5135
|
+
const apiAppId = installationIdentity.kind === "degraded" ? "" : installationIdentity.apiAppId ?? "";
|
|
5136
|
+
if (authTestError !== void 0) runtime.log?.(warn(`[${account.accountId}] slack auth.test failed at boot (${authTestError}); 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
5137
|
if (authIdentityWarning) runtime.log?.(warn(authIdentityWarning));
|
|
4462
|
-
|
|
5138
|
+
const identityHealth = resolveSlackIdentityHealth({
|
|
5139
|
+
installationIdentity,
|
|
5140
|
+
botUserId,
|
|
5141
|
+
authTestError,
|
|
5142
|
+
authIdentityWarning
|
|
5143
|
+
});
|
|
5144
|
+
if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) runtime.error?.(`slack token mismatch: bot token app_id=${apiAppId} but app token looks like app_id=${expectedApiAppIdFromAppToken}`);
|
|
4463
5145
|
const ctx = createSlackMonitorContext({
|
|
4464
5146
|
cfg,
|
|
4465
5147
|
accountId: account.accountId,
|
|
@@ -4471,6 +5153,7 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4471
5153
|
botId,
|
|
4472
5154
|
teamId,
|
|
4473
5155
|
apiAppId,
|
|
5156
|
+
installationIdentity,
|
|
4474
5157
|
historyLimit,
|
|
4475
5158
|
dmHistoryLimit,
|
|
4476
5159
|
sessionScope,
|
|
@@ -4504,12 +5187,26 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4504
5187
|
lastInboundAt: Date.now()
|
|
4505
5188
|
});
|
|
4506
5189
|
} : void 0;
|
|
5190
|
+
const presenceEventsEnabled = hasSlackPresenceEventsEnabled({
|
|
5191
|
+
account: slackCfg.presenceEvents,
|
|
5192
|
+
channels: slackCfg.channels
|
|
5193
|
+
});
|
|
5194
|
+
const presenceMonitor = installationIdentity.kind !== "enterprise" && presenceEventsEnabled ? createSlackPresenceMonitor({
|
|
5195
|
+
accountId: account.accountId,
|
|
5196
|
+
accountConfig: slackCfg.presenceEvents,
|
|
5197
|
+
client: app.client.users,
|
|
5198
|
+
cooldownStore: openSlackPresenceCooldownStore(),
|
|
5199
|
+
log: runtime.log,
|
|
5200
|
+
error: runtime.error
|
|
5201
|
+
}) : void 0;
|
|
5202
|
+
if (installationIdentity.kind === "enterprise" && presenceEventsEnabled) runtime.log?.(warn("slack presence events are unavailable for Enterprise Grid org installs"));
|
|
4507
5203
|
const handleSlackMessage = createSlackMessageHandler({
|
|
4508
5204
|
ctx,
|
|
4509
5205
|
account,
|
|
4510
|
-
trackEvent
|
|
5206
|
+
trackEvent,
|
|
5207
|
+
onPrepared: presenceMonitor?.observe
|
|
4511
5208
|
});
|
|
4512
|
-
if (isSlackAnyNativeApprovalClientEnabled({
|
|
5209
|
+
if (installationIdentity.kind !== "enterprise" && isSlackAnyNativeApprovalClientEnabled({
|
|
4513
5210
|
cfg,
|
|
4514
5211
|
accountId: account.accountId
|
|
4515
5212
|
})) registerChannelRuntimeContext({
|
|
@@ -4523,24 +5220,26 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4523
5220
|
},
|
|
4524
5221
|
abortSignal: opts.abortSignal
|
|
4525
5222
|
});
|
|
4526
|
-
|
|
5223
|
+
const commandRegistration = installationIdentity.kind === "enterprise" ? { mode: "disabled" } : await registerSlackMonitorSlashCommands({
|
|
4527
5224
|
ctx,
|
|
4528
5225
|
account,
|
|
4529
|
-
handleSlackMessage,
|
|
4530
5226
|
trackEvent
|
|
4531
5227
|
});
|
|
4532
|
-
|
|
5228
|
+
registerSlackMonitorEvents({
|
|
4533
5229
|
ctx,
|
|
4534
5230
|
account,
|
|
5231
|
+
handleSlackMessage,
|
|
5232
|
+
appHomeSlashCommandName: commandRegistration.mode === "single" ? commandRegistration.name : void 0,
|
|
4535
5233
|
trackEvent
|
|
4536
5234
|
});
|
|
5235
|
+
presenceMonitor?.start();
|
|
4537
5236
|
if (slackMode === "http" && slackHttpHandler) unregisterHttpHandler = registerSlackHttpHandler({
|
|
4538
5237
|
path: slackWebhookPath,
|
|
4539
5238
|
handler: slackHttpHandler,
|
|
4540
5239
|
log: runtime.log,
|
|
4541
5240
|
accountId: account.accountId
|
|
4542
5241
|
});
|
|
4543
|
-
if (resolveToken) (async () => {
|
|
5242
|
+
if (resolveToken && installationIdentity.kind !== "enterprise") (async () => {
|
|
4544
5243
|
if (opts.abortSignal?.aborted) return;
|
|
4545
5244
|
if (channelsConfig && Object.keys(channelsConfig).length > 0) try {
|
|
4546
5245
|
const entries = Object.keys(channelsConfig).filter((key) => key !== "*");
|
|
@@ -4559,7 +5258,8 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4559
5258
|
unresolved.push(entry.input);
|
|
4560
5259
|
continue;
|
|
4561
5260
|
}
|
|
4562
|
-
|
|
5261
|
+
const resolvedLabel = formatSlackChannelResolved(entry);
|
|
5262
|
+
if (resolvedLabel) mapping.push(resolvedLabel);
|
|
4563
5263
|
const existing = nextChannels[entry.id] ?? {};
|
|
4564
5264
|
nextChannels[entry.id] = {
|
|
4565
5265
|
...source,
|
|
@@ -4647,10 +5347,10 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4647
5347
|
abortSignal: opts.abortSignal,
|
|
4648
5348
|
onStarted: () => {
|
|
4649
5349
|
reconnectAttempts = 0;
|
|
4650
|
-
publishSlackConnectedStatus(opts.setStatus);
|
|
5350
|
+
publishSlackConnectedStatus(opts.setStatus, identityHealth);
|
|
4651
5351
|
if (!hasLoggedSocketConnected) {
|
|
4652
5352
|
hasLoggedSocketConnected = true;
|
|
4653
|
-
runtime.log?.("slack socket mode connected");
|
|
5353
|
+
runtime.log?.(identityHealth.healthState === "degraded" ? "slack socket mode connected (degraded identity)" : "slack socket mode connected");
|
|
4654
5354
|
}
|
|
4655
5355
|
}
|
|
4656
5356
|
});
|
|
@@ -4712,6 +5412,7 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4712
5412
|
});
|
|
4713
5413
|
}
|
|
4714
5414
|
} finally {
|
|
5415
|
+
await presenceMonitor?.stop();
|
|
4715
5416
|
if (slackMode === "relay") setSlackDefaultSendIdentity(account.accountId, void 0);
|
|
4716
5417
|
opts.abortSignal?.removeEventListener("abort", stopOnAbort);
|
|
4717
5418
|
unregisterSocketModeConnectionDiagnostics();
|
|
@@ -4721,4 +5422,4 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4721
5422
|
}
|
|
4722
5423
|
const resolveSlackRuntimeGroupPolicy = resolveOpenProviderRuntimeGroupPolicy$1;
|
|
4723
5424
|
//#endregion
|
|
4724
|
-
export {
|
|
5425
|
+
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 };
|