import { App } from "@slack/bolt"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../auto-reply/reply/history.js"; import { loadConfig } from "../../config/config.js"; import type { SessionScope } from "../../config/sessions.js"; import type { DmPolicy, GroupPolicy } from "../../config/types.js"; import { normalizeMainKey } from "../../routing/session-key.js"; import type { RuntimeEnv } from "../../runtime.js"; import { resolveSlackAccount } from "../accounts.js"; import { resolveSlackAppToken, resolveSlackBotToken } from "../token.js"; import { resolveSlackSlashCommandConfig } from "./commands.js"; import { createSlackMonitorContext } from "./context.js"; import { registerSlackMonitorEvents } from "./events.js"; import { createSlackMessageHandler } from "./message-handler.js"; import { registerSlackMonitorSlashCommands } from "./slash.js"; import type { MonitorSlackOpts } from "./types.js"; export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const cfg = opts.config ?? loadConfig(); const account = resolveSlackAccount({ cfg, accountId: opts.accountId, }); const historyLimit = Math.max( 0, account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT, ); const sessionCfg = cfg.session; const sessionScope: SessionScope = sessionCfg?.scope ?? "per-sender"; const mainKey = normalizeMainKey(sessionCfg?.mainKey); const botToken = resolveSlackBotToken(opts.botToken ?? account.botToken); const appToken = resolveSlackAppToken(opts.appToken ?? account.appToken); if (!botToken || !appToken) { throw new Error( `Slack bot + app tokens missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken/appToken or SLACK_BOT_TOKEN/SLACK_APP_TOKEN for default).`, ); } const runtime: RuntimeEnv = opts.runtime ?? { log: console.log, error: console.error, exit: (code: number): never => { throw new Error(`exit ${code}`); }, }; const slackCfg = account.config; const dmConfig = slackCfg.dm; const dmEnabled = dmConfig?.enabled ?? true; const dmPolicy = (dmConfig?.policy ?? "pairing") as DmPolicy; const allowFrom = dmConfig?.allowFrom; const groupDmEnabled = dmConfig?.groupEnabled ?? false; const groupDmChannels = dmConfig?.groupChannels; const channelsConfig = slackCfg.channels; const groupPolicy = (slackCfg.groupPolicy ?? "open") as GroupPolicy; const useAccessGroups = cfg.commands?.useAccessGroups !== false; const reactionMode = slackCfg.reactionNotifications ?? "own"; const reactionAllowlist = slackCfg.reactionAllowlist ?? []; const replyToMode = slackCfg.replyToMode ?? "off"; const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand); const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId); const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions"; const mediaMaxBytes = (opts.mediaMaxMb ?? slackCfg.mediaMaxMb ?? 20) * 1024 * 1024; const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; const app = new App({ token: botToken, appToken, socketMode: true, }); let botUserId = ""; let teamId = ""; try { const auth = await app.client.auth.test({ token: botToken }); botUserId = auth.user_id ?? ""; teamId = auth.team_id ?? ""; } catch { // auth test failing is non-fatal; message handler falls back to regex mentions. } const ctx = createSlackMonitorContext({ cfg, accountId: account.accountId, botToken, app, runtime, botUserId, teamId, historyLimit, sessionScope, mainKey, dmEnabled, dmPolicy, allowFrom, groupDmEnabled, groupDmChannels, channelsConfig, groupPolicy, useAccessGroups, reactionMode, reactionAllowlist, replyToMode, slashCommand, textLimit, ackReactionScope, mediaMaxBytes, removeAckAfterReply, }); const handleSlackMessage = createSlackMessageHandler({ ctx, account }); registerSlackMonitorEvents({ ctx, account, handleSlackMessage }); registerSlackMonitorSlashCommands({ ctx, account }); const stopOnAbort = () => { if (opts.abortSignal?.aborted) void app.stop(); }; opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true }); try { await app.start(); runtime.log?.("slack socket mode connected"); if (opts.abortSignal?.aborted) return; await new Promise((resolve) => { opts.abortSignal?.addEventListener("abort", () => resolve(), { once: true, }); }); } finally { opts.abortSignal?.removeEventListener("abort", stopOnAbort); await app.stop().catch(() => undefined); } }