| import { resolveAckReaction } from "../../../agents/identity.js"; |
| import { hasControlCommand } from "../../../auto-reply/command-detection.js"; |
| import { shouldHandleTextCommands } from "../../../auto-reply/commands-registry.js"; |
| import { |
| formatInboundEnvelope, |
| resolveEnvelopeFormatOptions, |
| } from "../../../auto-reply/envelope.js"; |
| import { |
| buildPendingHistoryContextFromMap, |
| recordPendingHistoryEntryIfEnabled, |
| } from "../../../auto-reply/reply/history.js"; |
| import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js"; |
| import { |
| buildMentionRegexes, |
| matchesMentionWithExplicit, |
| } from "../../../auto-reply/reply/mentions.js"; |
| import type { FinalizedMsgContext } from "../../../auto-reply/templating.js"; |
| import { |
| shouldAckReaction as shouldAckReactionGate, |
| type AckReactionScope, |
| } from "../../../channels/ack-reactions.js"; |
| import { resolveControlCommandGate } from "../../../channels/command-gating.js"; |
| import { resolveConversationLabel } from "../../../channels/conversation-label.js"; |
| import { logInboundDrop } from "../../../channels/logging.js"; |
| import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js"; |
| import { recordInboundSession } from "../../../channels/session.js"; |
| import { readSessionUpdatedAt, resolveStorePath } from "../../../config/sessions.js"; |
| import { logVerbose, shouldLogVerbose } from "../../../globals.js"; |
| import { enqueueSystemEvent } from "../../../infra/system-events.js"; |
| import { resolveAgentRoute } from "../../../routing/resolve-route.js"; |
| import { resolveThreadSessionKeys } from "../../../routing/session-key.js"; |
| import { resolvePinnedMainDmOwnerFromAllowlist } from "../../../security/dm-policy-shared.js"; |
| import { resolveSlackReplyToMode, type ResolvedSlackAccount } from "../../accounts.js"; |
| import { reactSlackMessage } from "../../actions.js"; |
| import { sendMessageSlack } from "../../send.js"; |
| import { hasSlackThreadParticipation } from "../../sent-thread-cache.js"; |
| import { resolveSlackThreadContext } from "../../threading.js"; |
| import type { SlackMessageEvent } from "../../types.js"; |
| import { |
| normalizeSlackAllowOwnerEntry, |
| resolveSlackAllowListMatch, |
| resolveSlackUserAllowed, |
| } from "../allow-list.js"; |
| import { resolveSlackEffectiveAllowFrom } from "../auth.js"; |
| import { resolveSlackChannelConfig } from "../channel-config.js"; |
| import { stripSlackMentionsForCommandDetection } from "../commands.js"; |
| import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js"; |
| import { authorizeSlackDirectMessage } from "../dm-auth.js"; |
| import { resolveSlackThreadStarter } from "../media.js"; |
| import { resolveSlackRoomContextHints } from "../room-context.js"; |
| import { resolveSlackMessageContent } from "./prepare-content.js"; |
| import { resolveSlackThreadContextData } from "./prepare-thread-context.js"; |
| import type { PreparedSlackMessage } from "./types.js"; |
|
|
| const mentionRegexCache = new WeakMap<SlackMonitorContext, Map<string, RegExp[]>>(); |
|
|
| function resolveCachedMentionRegexes( |
| ctx: SlackMonitorContext, |
| agentId: string | undefined, |
| ): RegExp[] { |
| const key = agentId?.trim() || "__default__"; |
| let byAgent = mentionRegexCache.get(ctx); |
| if (!byAgent) { |
| byAgent = new Map<string, RegExp[]>(); |
| mentionRegexCache.set(ctx, byAgent); |
| } |
| const cached = byAgent.get(key); |
| if (cached) { |
| return cached; |
| } |
| const built = buildMentionRegexes(ctx.cfg, agentId); |
| byAgent.set(key, built); |
| return built; |
| } |
|
|
| type SlackConversationContext = { |
| channelInfo: { |
| name?: string; |
| type?: SlackMessageEvent["channel_type"]; |
| topic?: string; |
| purpose?: string; |
| }; |
| channelName?: string; |
| resolvedChannelType: ReturnType<typeof normalizeSlackChannelType>; |
| isDirectMessage: boolean; |
| isGroupDm: boolean; |
| isRoom: boolean; |
| isRoomish: boolean; |
| channelConfig: ReturnType<typeof resolveSlackChannelConfig> | null; |
| allowBots: boolean; |
| isBotMessage: boolean; |
| }; |
|
|
| type SlackAuthorizationContext = { |
| senderId: string; |
| allowFromLower: string[]; |
| }; |
|
|
| type SlackRoutingContext = { |
| route: ReturnType<typeof resolveAgentRoute>; |
| chatType: "direct" | "group" | "channel"; |
| replyToMode: ReturnType<typeof resolveSlackReplyToMode>; |
| threadContext: ReturnType<typeof resolveSlackThreadContext>; |
| threadTs: string | undefined; |
| isThreadReply: boolean; |
| threadKeys: ReturnType<typeof resolveThreadSessionKeys>; |
| sessionKey: string; |
| historyKey: string; |
| }; |
|
|
| async function resolveSlackConversationContext(params: { |
| ctx: SlackMonitorContext; |
| account: ResolvedSlackAccount; |
| message: SlackMessageEvent; |
| }): Promise<SlackConversationContext> { |
| const { ctx, account, message } = params; |
| const cfg = ctx.cfg; |
|
|
| let channelInfo: { |
| name?: string; |
| type?: SlackMessageEvent["channel_type"]; |
| topic?: string; |
| purpose?: string; |
| } = {}; |
| let resolvedChannelType = normalizeSlackChannelType(message.channel_type, message.channel); |
| |
| |
| if (resolvedChannelType !== "im" && (!message.channel_type || message.channel_type !== "im")) { |
| channelInfo = await ctx.resolveChannelName(message.channel); |
| resolvedChannelType = normalizeSlackChannelType( |
| message.channel_type ?? channelInfo.type, |
| message.channel, |
| ); |
| } |
| const channelName = channelInfo?.name; |
| const isDirectMessage = resolvedChannelType === "im"; |
| const isGroupDm = resolvedChannelType === "mpim"; |
| const isRoom = resolvedChannelType === "channel" || resolvedChannelType === "group"; |
| const isRoomish = isRoom || isGroupDm; |
| const channelConfig = isRoom |
| ? resolveSlackChannelConfig({ |
| channelId: message.channel, |
| channelName, |
| channels: ctx.channelsConfig, |
| channelKeys: ctx.channelsConfigKeys, |
| defaultRequireMention: ctx.defaultRequireMention, |
| allowNameMatching: ctx.allowNameMatching, |
| }) |
| : null; |
| const allowBots = |
| channelConfig?.allowBots ?? |
| account.config?.allowBots ?? |
| cfg.channels?.slack?.allowBots ?? |
| false; |
|
|
| return { |
| channelInfo, |
| channelName, |
| resolvedChannelType, |
| isDirectMessage, |
| isGroupDm, |
| isRoom, |
| isRoomish, |
| channelConfig, |
| allowBots, |
| isBotMessage: Boolean(message.bot_id), |
| }; |
| } |
|
|
| async function authorizeSlackInboundMessage(params: { |
| ctx: SlackMonitorContext; |
| account: ResolvedSlackAccount; |
| message: SlackMessageEvent; |
| conversation: SlackConversationContext; |
| }): Promise<SlackAuthorizationContext | null> { |
| const { ctx, account, message, conversation } = params; |
| const { isDirectMessage, channelName, resolvedChannelType, isBotMessage, allowBots } = |
| conversation; |
|
|
| if (isBotMessage) { |
| if (message.user && ctx.botUserId && message.user === ctx.botUserId) { |
| return null; |
| } |
| if (!allowBots) { |
| logVerbose(`slack: drop bot message ${message.bot_id ?? "unknown"} (allowBots=false)`); |
| return null; |
| } |
| } |
|
|
| if (isDirectMessage && !message.user) { |
| logVerbose("slack: drop dm message (missing user id)"); |
| return null; |
| } |
|
|
| const senderId = message.user ?? (isBotMessage ? message.bot_id : undefined); |
| if (!senderId) { |
| logVerbose("slack: drop message (missing sender id)"); |
| return null; |
| } |
|
|
| if ( |
| !ctx.isChannelAllowed({ |
| channelId: message.channel, |
| channelName, |
| channelType: resolvedChannelType, |
| }) |
| ) { |
| logVerbose("slack: drop message (channel not allowed)"); |
| return null; |
| } |
|
|
| const { allowFromLower } = await resolveSlackEffectiveAllowFrom(ctx, { |
| includePairingStore: isDirectMessage, |
| }); |
|
|
| if (isDirectMessage) { |
| const directUserId = message.user; |
| if (!directUserId) { |
| logVerbose("slack: drop dm message (missing user id)"); |
| return null; |
| } |
| const allowed = await authorizeSlackDirectMessage({ |
| ctx, |
| accountId: account.accountId, |
| senderId: directUserId, |
| allowFromLower, |
| resolveSenderName: ctx.resolveUserName, |
| sendPairingReply: async (text) => { |
| await sendMessageSlack(message.channel, text, { |
| token: ctx.botToken, |
| client: ctx.app.client, |
| accountId: account.accountId, |
| }); |
| }, |
| onDisabled: () => { |
| logVerbose("slack: drop dm (dms disabled)"); |
| }, |
| onUnauthorized: ({ allowMatchMeta }) => { |
| logVerbose( |
| `Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`, |
| ); |
| }, |
| log: logVerbose, |
| }); |
| if (!allowed) { |
| return null; |
| } |
| } |
|
|
| return { |
| senderId, |
| allowFromLower, |
| }; |
| } |
|
|
| function resolveSlackRoutingContext(params: { |
| ctx: SlackMonitorContext; |
| account: ResolvedSlackAccount; |
| message: SlackMessageEvent; |
| isDirectMessage: boolean; |
| isGroupDm: boolean; |
| isRoom: boolean; |
| isRoomish: boolean; |
| }): SlackRoutingContext { |
| const { ctx, account, message, isDirectMessage, isGroupDm, isRoom, isRoomish } = params; |
| const route = resolveAgentRoute({ |
| cfg: ctx.cfg, |
| channel: "slack", |
| accountId: account.accountId, |
| teamId: ctx.teamId || undefined, |
| peer: { |
| kind: isDirectMessage ? "direct" : isRoom ? "channel" : "group", |
| id: isDirectMessage ? (message.user ?? "unknown") : message.channel, |
| }, |
| }); |
|
|
| const chatType = isDirectMessage ? "direct" : isGroupDm ? "group" : "channel"; |
| const replyToMode = resolveSlackReplyToMode(account, chatType); |
| const threadContext = resolveSlackThreadContext({ message, replyToMode }); |
| const threadTs = threadContext.incomingThreadTs; |
| const isThreadReply = threadContext.isThreadReply; |
| |
| |
| |
| const autoThreadId = |
| !isThreadReply && replyToMode === "all" && threadContext.messageTs |
| ? threadContext.messageTs |
| : undefined; |
| |
| |
| |
| |
| |
| const roomThreadId = isThreadReply && threadTs ? threadTs : undefined; |
| const canonicalThreadId = isRoomish ? roomThreadId : isThreadReply ? threadTs : autoThreadId; |
| const threadKeys = resolveThreadSessionKeys({ |
| baseSessionKey: route.sessionKey, |
| threadId: canonicalThreadId, |
| parentSessionKey: canonicalThreadId && ctx.threadInheritParent ? route.sessionKey : undefined, |
| }); |
| const sessionKey = threadKeys.sessionKey; |
| const historyKey = |
| isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel; |
|
|
| return { |
| route, |
| chatType, |
| replyToMode, |
| threadContext, |
| threadTs, |
| isThreadReply, |
| threadKeys, |
| sessionKey, |
| historyKey, |
| }; |
| } |
|
|
| export async function prepareSlackMessage(params: { |
| ctx: SlackMonitorContext; |
| account: ResolvedSlackAccount; |
| message: SlackMessageEvent; |
| opts: { source: "message" | "app_mention"; wasMentioned?: boolean }; |
| }): Promise<PreparedSlackMessage | null> { |
| const { ctx, account, message, opts } = params; |
| const cfg = ctx.cfg; |
| const conversation = await resolveSlackConversationContext({ ctx, account, message }); |
| const { |
| channelInfo, |
| channelName, |
| isDirectMessage, |
| isGroupDm, |
| isRoom, |
| isRoomish, |
| channelConfig, |
| isBotMessage, |
| } = conversation; |
| const authorization = await authorizeSlackInboundMessage({ |
| ctx, |
| account, |
| message, |
| conversation, |
| }); |
| if (!authorization) { |
| return null; |
| } |
| const { senderId, allowFromLower } = authorization; |
| const routing = resolveSlackRoutingContext({ |
| ctx, |
| account, |
| message, |
| isDirectMessage, |
| isGroupDm, |
| isRoom, |
| isRoomish, |
| }); |
| const { |
| route, |
| replyToMode, |
| threadContext, |
| threadTs, |
| isThreadReply, |
| threadKeys, |
| sessionKey, |
| historyKey, |
| } = routing; |
|
|
| const mentionRegexes = resolveCachedMentionRegexes(ctx, route.agentId); |
| const hasAnyMention = /<@[^>]+>/.test(message.text ?? ""); |
| const explicitlyMentioned = Boolean( |
| ctx.botUserId && message.text?.includes(`<@${ctx.botUserId}>`), |
| ); |
| const wasMentioned = |
| opts.wasMentioned ?? |
| (!isDirectMessage && |
| matchesMentionWithExplicit({ |
| text: message.text ?? "", |
| mentionRegexes, |
| explicit: { |
| hasAnyMention, |
| isExplicitlyMentioned: explicitlyMentioned, |
| canResolveExplicit: Boolean(ctx.botUserId), |
| }, |
| })); |
| const implicitMention = Boolean( |
| !isDirectMessage && |
| ctx.botUserId && |
| message.thread_ts && |
| (message.parent_user_id === ctx.botUserId || |
| hasSlackThreadParticipation(account.accountId, message.channel, message.thread_ts)), |
| ); |
|
|
| let resolvedSenderName = message.username?.trim() || undefined; |
| const resolveSenderName = async (): Promise<string> => { |
| if (resolvedSenderName) { |
| return resolvedSenderName; |
| } |
| if (message.user) { |
| const sender = await ctx.resolveUserName(message.user); |
| const normalized = sender?.name?.trim(); |
| if (normalized) { |
| resolvedSenderName = normalized; |
| return resolvedSenderName; |
| } |
| } |
| resolvedSenderName = message.user ?? message.bot_id ?? "unknown"; |
| return resolvedSenderName; |
| }; |
| const senderNameForAuth = ctx.allowNameMatching ? await resolveSenderName() : undefined; |
|
|
| const channelUserAuthorized = isRoom |
| ? resolveSlackUserAllowed({ |
| allowList: channelConfig?.users, |
| userId: senderId, |
| userName: senderNameForAuth, |
| allowNameMatching: ctx.allowNameMatching, |
| }) |
| : true; |
| if (isRoom && !channelUserAuthorized) { |
| logVerbose(`Blocked unauthorized slack sender ${senderId} (not in channel users)`); |
| return null; |
| } |
|
|
| const allowTextCommands = shouldHandleTextCommands({ |
| cfg, |
| surface: "slack", |
| }); |
| |
| const textForCommandDetection = stripSlackMentionsForCommandDetection(message.text ?? ""); |
| const hasControlCommandInMessage = hasControlCommand(textForCommandDetection, cfg); |
|
|
| const ownerAuthorized = resolveSlackAllowListMatch({ |
| allowList: allowFromLower, |
| id: senderId, |
| name: senderNameForAuth, |
| allowNameMatching: ctx.allowNameMatching, |
| }).allowed; |
| const channelUsersAllowlistConfigured = |
| isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0; |
| const channelCommandAuthorized = |
| isRoom && channelUsersAllowlistConfigured |
| ? resolveSlackUserAllowed({ |
| allowList: channelConfig?.users, |
| userId: senderId, |
| userName: senderNameForAuth, |
| allowNameMatching: ctx.allowNameMatching, |
| }) |
| : false; |
| const commandGate = resolveControlCommandGate({ |
| useAccessGroups: ctx.useAccessGroups, |
| authorizers: [ |
| { configured: allowFromLower.length > 0, allowed: ownerAuthorized }, |
| { |
| configured: channelUsersAllowlistConfigured, |
| allowed: channelCommandAuthorized, |
| }, |
| ], |
| allowTextCommands, |
| hasControlCommand: hasControlCommandInMessage, |
| }); |
| const commandAuthorized = commandGate.commandAuthorized; |
|
|
| if (isRoomish && commandGate.shouldBlock) { |
| logInboundDrop({ |
| log: logVerbose, |
| channel: "slack", |
| reason: "control command (unauthorized)", |
| target: senderId, |
| }); |
| return null; |
| } |
|
|
| const shouldRequireMention = isRoom |
| ? (channelConfig?.requireMention ?? ctx.defaultRequireMention) |
| : false; |
|
|
| |
| const canDetectMention = Boolean(ctx.botUserId) || mentionRegexes.length > 0; |
| const mentionGate = resolveMentionGatingWithBypass({ |
| isGroup: isRoom, |
| requireMention: Boolean(shouldRequireMention), |
| canDetectMention, |
| wasMentioned, |
| implicitMention, |
| hasAnyMention, |
| allowTextCommands, |
| hasControlCommand: hasControlCommandInMessage, |
| commandAuthorized, |
| }); |
| const effectiveWasMentioned = mentionGate.effectiveWasMentioned; |
| if (isRoom && shouldRequireMention && mentionGate.shouldSkip) { |
| ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping channel message"); |
| const pendingText = (message.text ?? "").trim(); |
| const fallbackFile = message.files?.[0]?.name |
| ? `[Slack file: ${message.files[0].name}]` |
| : message.files?.length |
| ? "[Slack file]" |
| : ""; |
| const pendingBody = pendingText || fallbackFile; |
| recordPendingHistoryEntryIfEnabled({ |
| historyMap: ctx.channelHistories, |
| historyKey, |
| limit: ctx.historyLimit, |
| entry: pendingBody |
| ? { |
| sender: await resolveSenderName(), |
| body: pendingBody, |
| timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, |
| messageId: message.ts, |
| } |
| : null, |
| }); |
| return null; |
| } |
|
|
| const threadStarter = |
| isThreadReply && threadTs |
| ? await resolveSlackThreadStarter({ |
| channelId: message.channel, |
| threadTs, |
| client: ctx.app.client, |
| }) |
| : null; |
| const resolvedMessageContent = await resolveSlackMessageContent({ |
| message, |
| isThreadReply, |
| threadStarter, |
| isBotMessage, |
| botToken: ctx.botToken, |
| mediaMaxBytes: ctx.mediaMaxBytes, |
| }); |
| if (!resolvedMessageContent) { |
| return null; |
| } |
| const { rawBody, effectiveDirectMedia } = resolvedMessageContent; |
|
|
| const ackReaction = resolveAckReaction(cfg, route.agentId, { |
| channel: "slack", |
| accountId: account.accountId, |
| }); |
| const ackReactionValue = ackReaction ?? ""; |
|
|
| const shouldAckReaction = () => |
| Boolean( |
| ackReaction && |
| shouldAckReactionGate({ |
| scope: ctx.ackReactionScope as AckReactionScope | undefined, |
| isDirect: isDirectMessage, |
| isGroup: isRoomish, |
| isMentionableGroup: isRoom, |
| requireMention: Boolean(shouldRequireMention), |
| canDetectMention, |
| effectiveWasMentioned, |
| shouldBypassMention: mentionGate.shouldBypassMention, |
| }), |
| ); |
|
|
| const ackReactionMessageTs = message.ts; |
| const ackReactionPromise = |
| shouldAckReaction() && ackReactionMessageTs && ackReactionValue |
| ? reactSlackMessage(message.channel, ackReactionMessageTs, ackReactionValue, { |
| token: ctx.botToken, |
| client: ctx.app.client, |
| }).then( |
| () => true, |
| (err) => { |
| logVerbose(`slack react failed for channel ${message.channel}: ${String(err)}`); |
| return false; |
| }, |
| ) |
| : null; |
|
|
| const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`; |
| const senderName = await resolveSenderName(); |
| const preview = rawBody.replace(/\s+/g, " ").slice(0, 160); |
| const inboundLabel = isDirectMessage |
| ? `Slack DM from ${senderName}` |
| : `Slack message in ${roomLabel} from ${senderName}`; |
| const slackFrom = isDirectMessage |
| ? `slack:${message.user}` |
| : isRoom |
| ? `slack:channel:${message.channel}` |
| : `slack:group:${message.channel}`; |
|
|
| enqueueSystemEvent(`${inboundLabel}: ${preview}`, { |
| sessionKey, |
| contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`, |
| }); |
|
|
| const envelopeFrom = |
| resolveConversationLabel({ |
| ChatType: isDirectMessage ? "direct" : "channel", |
| SenderName: senderName, |
| GroupSubject: isRoomish ? roomLabel : undefined, |
| From: slackFrom, |
| }) ?? (isDirectMessage ? senderName : roomLabel); |
| const threadInfo = |
| isThreadReply && threadTs |
| ? ` thread_ts: ${threadTs}${message.parent_user_id ? ` parent_user_id: ${message.parent_user_id}` : ""}` |
| : ""; |
| const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}${threadInfo}]`; |
| const storePath = resolveStorePath(ctx.cfg.session?.store, { |
| agentId: route.agentId, |
| }); |
| const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg); |
| const previousTimestamp = readSessionUpdatedAt({ |
| storePath, |
| sessionKey, |
| }); |
| const body = formatInboundEnvelope({ |
| channel: "Slack", |
| from: envelopeFrom, |
| timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, |
| body: textWithId, |
| chatType: isDirectMessage ? "direct" : "channel", |
| sender: { name: senderName, id: senderId }, |
| previousTimestamp, |
| envelope: envelopeOptions, |
| }); |
|
|
| let combinedBody = body; |
| if (isRoomish && ctx.historyLimit > 0) { |
| combinedBody = buildPendingHistoryContextFromMap({ |
| historyMap: ctx.channelHistories, |
| historyKey, |
| limit: ctx.historyLimit, |
| currentMessage: combinedBody, |
| formatEntry: (entry) => |
| formatInboundEnvelope({ |
| channel: "Slack", |
| from: roomLabel, |
| timestamp: entry.timestamp, |
| body: `${entry.body}${ |
| entry.messageId ? ` [id:${entry.messageId} channel:${message.channel}]` : "" |
| }`, |
| chatType: "channel", |
| senderLabel: entry.sender, |
| envelope: envelopeOptions, |
| }), |
| }); |
| } |
|
|
| const slackTo = isDirectMessage ? `user:${message.user}` : `channel:${message.channel}`; |
|
|
| const { untrustedChannelMetadata, groupSystemPrompt } = resolveSlackRoomContextHints({ |
| isRoomish, |
| channelInfo, |
| channelConfig, |
| }); |
|
|
| const { |
| threadStarterBody, |
| threadHistoryBody, |
| threadSessionPreviousTimestamp, |
| threadLabel, |
| threadStarterMedia, |
| } = await resolveSlackThreadContextData({ |
| ctx, |
| account, |
| message, |
| isThreadReply, |
| threadTs, |
| threadStarter, |
| roomLabel, |
| storePath, |
| sessionKey, |
| envelopeOptions, |
| effectiveDirectMedia, |
| }); |
|
|
| |
| const effectiveMedia = effectiveDirectMedia ?? threadStarterMedia; |
| const firstMedia = effectiveMedia?.[0]; |
|
|
| const inboundHistory = |
| isRoomish && ctx.historyLimit > 0 |
| ? (ctx.channelHistories.get(historyKey) ?? []).map((entry) => ({ |
| sender: entry.sender, |
| body: entry.body, |
| timestamp: entry.timestamp, |
| })) |
| : undefined; |
| const commandBody = textForCommandDetection.trim(); |
|
|
| const ctxPayload = finalizeInboundContext({ |
| Body: combinedBody, |
| BodyForAgent: rawBody, |
| InboundHistory: inboundHistory, |
| RawBody: rawBody, |
| CommandBody: commandBody, |
| BodyForCommands: commandBody, |
| From: slackFrom, |
| To: slackTo, |
| SessionKey: sessionKey, |
| AccountId: route.accountId, |
| ChatType: isDirectMessage ? "direct" : "channel", |
| ConversationLabel: envelopeFrom, |
| GroupSubject: isRoomish ? roomLabel : undefined, |
| GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined, |
| UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined, |
| SenderName: senderName, |
| SenderId: senderId, |
| Provider: "slack" as const, |
| Surface: "slack" as const, |
| MessageSid: message.ts, |
| ReplyToId: threadContext.replyToId, |
| |
| MessageThreadId: threadContext.messageThreadId, |
| ParentSessionKey: threadKeys.parentSessionKey, |
| |
| ThreadStarterBody: !threadSessionPreviousTimestamp ? threadStarterBody : undefined, |
| ThreadHistoryBody: threadHistoryBody, |
| IsFirstThreadTurn: |
| isThreadReply && threadTs && !threadSessionPreviousTimestamp ? true : undefined, |
| ThreadLabel: threadLabel, |
| Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, |
| WasMentioned: isRoomish ? effectiveWasMentioned : undefined, |
| MediaPath: firstMedia?.path, |
| MediaType: firstMedia?.contentType, |
| MediaUrl: firstMedia?.path, |
| MediaPaths: |
| effectiveMedia && effectiveMedia.length > 0 ? effectiveMedia.map((m) => m.path) : undefined, |
| MediaUrls: |
| effectiveMedia && effectiveMedia.length > 0 ? effectiveMedia.map((m) => m.path) : undefined, |
| MediaTypes: |
| effectiveMedia && effectiveMedia.length > 0 |
| ? effectiveMedia.map((m) => m.contentType ?? "") |
| : undefined, |
| CommandAuthorized: commandAuthorized, |
| OriginatingChannel: "slack" as const, |
| OriginatingTo: slackTo, |
| NativeChannelId: message.channel, |
| }) satisfies FinalizedMsgContext; |
| const pinnedMainDmOwner = isDirectMessage |
| ? resolvePinnedMainDmOwnerFromAllowlist({ |
| dmScope: cfg.session?.dmScope, |
| allowFrom: ctx.allowFrom, |
| normalizeEntry: normalizeSlackAllowOwnerEntry, |
| }) |
| : null; |
|
|
| await recordInboundSession({ |
| storePath, |
| sessionKey, |
| ctx: ctxPayload, |
| updateLastRoute: isDirectMessage |
| ? { |
| sessionKey: route.mainSessionKey, |
| channel: "slack", |
| to: `user:${message.user}`, |
| accountId: route.accountId, |
| threadId: threadContext.messageThreadId, |
| mainDmOwnerPin: |
| pinnedMainDmOwner && message.user |
| ? { |
| ownerRecipient: pinnedMainDmOwner, |
| senderRecipient: message.user.toLowerCase(), |
| onSkip: ({ ownerRecipient, senderRecipient }) => { |
| logVerbose( |
| `slack: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`, |
| ); |
| }, |
| } |
| : undefined, |
| } |
| : undefined, |
| onRecordError: (err) => { |
| ctx.logger.warn( |
| { |
| error: String(err), |
| storePath, |
| sessionKey, |
| }, |
| "failed updating session meta", |
| ); |
| }, |
| }); |
|
|
| const replyTarget = ctxPayload.To ?? undefined; |
| if (!replyTarget) { |
| return null; |
| } |
|
|
| if (shouldLogVerbose()) { |
| logVerbose(`slack inbound: channel=${message.channel} from=${slackFrom} preview="${preview}"`); |
| } |
|
|
| return { |
| ctx, |
| account, |
| message, |
| route, |
| channelConfig, |
| replyTarget, |
| ctxPayload, |
| replyToMode, |
| isDirectMessage, |
| isRoomish, |
| historyKey, |
| preview, |
| ackReactionMessageTs, |
| ackReactionValue, |
| ackReactionPromise, |
| }; |
| } |
|
|