Files
openclaw/src/slack/monitor/message-handler/prepare.ts
2026-03-02 22:30:21 +00:00

803 lines
26 KiB
TypeScript

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);
// D-prefixed channels are always direct messages. Skip channel lookups in
// that common path to avoid an unnecessary API round-trip.
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,
})
: 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;
// Keep true thread replies thread-scoped, but preserve channel-level sessions
// for top-level room turns when replyToMode is off.
// For DMs, preserve existing auto-thread behavior when replyToMode="all".
const autoThreadId =
!isThreadReply && replyToMode === "all" && threadContext.messageTs
? threadContext.messageTs
: undefined;
const roomThreadId =
isThreadReply && threadTs
? threadTs
: replyToMode === "off"
? undefined
: threadContext.messageTs;
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",
});
// Strip Slack mentions (<@U123>) before command detection so "@Labrador /new" is recognized
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;
// Allow "control commands" to bypass mention gating if sender is authorized.
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,
});
// Use direct media (including forwarded attachment media) if available, else thread starter media
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,
// Preserve thread context for routed tool notifications.
MessageThreadId: threadContext.messageThreadId,
ParentSessionKey: threadKeys.parentSessionKey,
// Only include thread starter body for NEW sessions (existing sessions already have it in their transcript)
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,
}) 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,
};
}