feat: add slack multi-account routing
This commit is contained in:
@@ -17,6 +17,16 @@ export type ResolvedSlackAccount = {
|
||||
botTokenSource: SlackTokenSource;
|
||||
appTokenSource: SlackTokenSource;
|
||||
config: SlackAccountConfig;
|
||||
groupPolicy?: SlackAccountConfig["groupPolicy"];
|
||||
textChunkLimit?: SlackAccountConfig["textChunkLimit"];
|
||||
mediaMaxMb?: SlackAccountConfig["mediaMaxMb"];
|
||||
reactionNotifications?: SlackAccountConfig["reactionNotifications"];
|
||||
reactionAllowlist?: SlackAccountConfig["reactionAllowlist"];
|
||||
replyToMode?: SlackAccountConfig["replyToMode"];
|
||||
actions?: SlackAccountConfig["actions"];
|
||||
slashCommand?: SlackAccountConfig["slashCommand"];
|
||||
dm?: SlackAccountConfig["dm"];
|
||||
channels?: SlackAccountConfig["channels"];
|
||||
};
|
||||
|
||||
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
@@ -66,31 +76,26 @@ export function resolveSlackAccount(params: {
|
||||
const accountEnabled = merged.enabled !== false;
|
||||
const enabled = baseEnabled && accountEnabled;
|
||||
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
|
||||
const botToken = resolveSlackBotToken(
|
||||
merged.botToken ??
|
||||
(allowEnv ? process.env.SLACK_BOT_TOKEN : undefined) ??
|
||||
(allowEnv ? params.cfg.slack?.botToken : undefined),
|
||||
);
|
||||
const appToken = resolveSlackAppToken(
|
||||
merged.appToken ??
|
||||
(allowEnv ? process.env.SLACK_APP_TOKEN : undefined) ??
|
||||
(allowEnv ? params.cfg.slack?.appToken : undefined),
|
||||
);
|
||||
const botTokenSource: SlackTokenSource = merged.botToken
|
||||
const envBot = allowEnv
|
||||
? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN)
|
||||
: undefined;
|
||||
const envApp = allowEnv
|
||||
? resolveSlackAppToken(process.env.SLACK_APP_TOKEN)
|
||||
: undefined;
|
||||
const configBot = resolveSlackBotToken(merged.botToken);
|
||||
const configApp = resolveSlackAppToken(merged.appToken);
|
||||
const botToken = configBot ?? envBot;
|
||||
const appToken = configApp ?? envApp;
|
||||
const botTokenSource: SlackTokenSource = configBot
|
||||
? "config"
|
||||
: allowEnv && process.env.SLACK_BOT_TOKEN
|
||||
: envBot
|
||||
? "env"
|
||||
: allowEnv && params.cfg.slack?.botToken
|
||||
? "config"
|
||||
: "none";
|
||||
const appTokenSource: SlackTokenSource = merged.appToken
|
||||
: "none";
|
||||
const appTokenSource: SlackTokenSource = configApp
|
||||
? "config"
|
||||
: allowEnv && process.env.SLACK_APP_TOKEN
|
||||
: envApp
|
||||
? "env"
|
||||
: allowEnv && params.cfg.slack?.appToken
|
||||
? "config"
|
||||
: "none";
|
||||
: "none";
|
||||
|
||||
return {
|
||||
accountId,
|
||||
@@ -101,6 +106,16 @@ export function resolveSlackAccount(params: {
|
||||
botTokenSource,
|
||||
appTokenSource,
|
||||
config: merged,
|
||||
groupPolicy: merged.groupPolicy,
|
||||
textChunkLimit: merged.textChunkLimit,
|
||||
mediaMaxMb: merged.mediaMaxMb,
|
||||
reactionNotifications: merged.reactionNotifications,
|
||||
reactionAllowlist: merged.reactionAllowlist,
|
||||
replyToMode: merged.replyToMode,
|
||||
actions: merged.actions,
|
||||
slashCommand: merged.slashCommand,
|
||||
dm: merged.dm,
|
||||
channels: merged.channels,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { WebClient } from "@slack/web-api";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import { sendMessageSlack } from "./send.js";
|
||||
import { resolveSlackBotToken } from "./token.js";
|
||||
|
||||
export type SlackActionClientOpts = {
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
client?: WebClient;
|
||||
};
|
||||
@@ -28,12 +31,16 @@ export type SlackPin = {
|
||||
file?: { id?: string; name?: string };
|
||||
};
|
||||
|
||||
function resolveToken(explicit?: string) {
|
||||
const cfgToken = loadConfig().slack?.botToken;
|
||||
const token = resolveSlackBotToken(
|
||||
explicit ?? process.env.SLACK_BOT_TOKEN ?? cfgToken ?? undefined,
|
||||
);
|
||||
function resolveToken(explicit?: string, accountId?: string) {
|
||||
const cfg = loadConfig();
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
const token = resolveSlackBotToken(explicit ?? account.botToken ?? undefined);
|
||||
if (!token) {
|
||||
logVerbose(
|
||||
`slack actions: missing bot token for account=${account.accountId} explicit=${Boolean(
|
||||
explicit,
|
||||
)} source=${account.botTokenSource ?? "unknown"}`,
|
||||
);
|
||||
throw new Error(
|
||||
"SLACK_BOT_TOKEN or slack.botToken is required for Slack actions",
|
||||
);
|
||||
@@ -50,7 +57,7 @@ function normalizeEmoji(raw: string) {
|
||||
}
|
||||
|
||||
async function getClient(opts: SlackActionClientOpts = {}) {
|
||||
const token = resolveToken(opts.token);
|
||||
const token = resolveToken(opts.token, opts.accountId);
|
||||
return opts.client ?? new WebClient(token);
|
||||
}
|
||||
|
||||
@@ -141,6 +148,7 @@ export async function sendSlackMessage(
|
||||
opts: SlackActionClientOpts & { mediaUrl?: string } = {},
|
||||
) {
|
||||
return await sendMessageSlack(to, content, {
|
||||
accountId: opts.accountId,
|
||||
token: opts.token,
|
||||
mediaUrl: opts.mediaUrl,
|
||||
client: opts.client,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
export {
|
||||
listEnabledSlackAccounts,
|
||||
listSlackAccountIds,
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
} from "./accounts.js";
|
||||
export {
|
||||
deleteSlackMessage,
|
||||
editSlackMessage,
|
||||
|
||||
@@ -80,6 +80,7 @@ type SlackMessageEvent = {
|
||||
user?: string;
|
||||
bot_id?: string;
|
||||
subtype?: string;
|
||||
username?: string;
|
||||
text?: string;
|
||||
ts?: string;
|
||||
thread_ts?: string;
|
||||
@@ -93,6 +94,7 @@ type SlackAppMentionEvent = {
|
||||
type: "app_mention";
|
||||
user?: string;
|
||||
bot_id?: string;
|
||||
username?: string;
|
||||
text?: string;
|
||||
ts?: string;
|
||||
thread_ts?: string;
|
||||
@@ -170,6 +172,7 @@ type SlackThreadBroadcastEvent = {
|
||||
type SlackChannelConfigResolved = {
|
||||
allowed: boolean;
|
||||
requireMention: boolean;
|
||||
allowBots?: boolean;
|
||||
users?: Array<string | number>;
|
||||
skills?: string[];
|
||||
systemPrompt?: string;
|
||||
@@ -294,6 +297,7 @@ function resolveSlackChannelConfig(params: {
|
||||
enabled?: boolean;
|
||||
allow?: boolean;
|
||||
requireMention?: boolean;
|
||||
allowBots?: boolean;
|
||||
users?: Array<string | number>;
|
||||
skills?: string[];
|
||||
systemPrompt?: string;
|
||||
@@ -317,6 +321,7 @@ function resolveSlackChannelConfig(params: {
|
||||
enabled?: boolean;
|
||||
allow?: boolean;
|
||||
requireMention?: boolean;
|
||||
allowBots?: boolean;
|
||||
users?: Array<string | number>;
|
||||
skills?: string[];
|
||||
systemPrompt?: string;
|
||||
@@ -349,13 +354,14 @@ function resolveSlackChannelConfig(params: {
|
||||
const requireMention =
|
||||
firstDefined(resolved.requireMention, fallback?.requireMention, true) ??
|
||||
true;
|
||||
const allowBots = firstDefined(resolved.allowBots, fallback?.allowBots);
|
||||
const users = firstDefined(resolved.users, fallback?.users);
|
||||
const skills = firstDefined(resolved.skills, fallback?.skills);
|
||||
const systemPrompt = firstDefined(
|
||||
resolved.systemPrompt,
|
||||
fallback?.systemPrompt,
|
||||
);
|
||||
return { allowed, requireMention, users, skills, systemPrompt };
|
||||
return { allowed, requireMention, allowBots, users, skills, systemPrompt };
|
||||
}
|
||||
|
||||
async function resolveSlackMedia(params: {
|
||||
@@ -706,15 +712,14 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
opts: { source: "message" | "app_mention"; wasMentioned?: boolean },
|
||||
) => {
|
||||
if (opts.source === "message" && message.type !== "message") return;
|
||||
if (message.bot_id) return;
|
||||
if (
|
||||
opts.source === "message" &&
|
||||
message.subtype &&
|
||||
message.subtype !== "file_share"
|
||||
message.subtype !== "file_share" &&
|
||||
message.subtype !== "bot_message"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!message.user) return;
|
||||
if (markMessageSeen(message.channel, message.ts)) return;
|
||||
|
||||
let channelInfo: {
|
||||
@@ -735,6 +740,40 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const isRoom =
|
||||
resolvedChannelType === "channel" || resolvedChannelType === "group";
|
||||
|
||||
const channelConfig = isRoom
|
||||
? resolveSlackChannelConfig({
|
||||
channelId: message.channel,
|
||||
channelName,
|
||||
channels: channelsConfig,
|
||||
})
|
||||
: null;
|
||||
|
||||
const allowBots =
|
||||
channelConfig?.allowBots ??
|
||||
account.config?.allowBots ??
|
||||
cfg.slack?.allowBots ??
|
||||
false;
|
||||
const isBotMessage = Boolean(message.bot_id);
|
||||
if (isBotMessage) {
|
||||
if (message.user && botUserId && message.user === botUserId) return;
|
||||
if (!allowBots) {
|
||||
logVerbose(
|
||||
`slack: drop bot message ${message.bot_id ?? "unknown"} (allowBots=false)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isDirectMessage && !message.user) {
|
||||
logVerbose("slack: drop dm message (missing user id)");
|
||||
return;
|
||||
}
|
||||
const senderId =
|
||||
message.user ?? (isBotMessage ? message.bot_id : undefined);
|
||||
if (!senderId) {
|
||||
logVerbose("slack: drop message (missing sender id)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!isChannelAllowed({
|
||||
channelId: message.channel,
|
||||
@@ -756,6 +795,11 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom);
|
||||
|
||||
if (isDirectMessage) {
|
||||
const directUserId = message.user;
|
||||
if (!directUserId) {
|
||||
logVerbose("slack: drop dm message (missing user id)");
|
||||
return;
|
||||
}
|
||||
if (!dmEnabled || dmPolicy === "disabled") {
|
||||
logVerbose("slack: drop dm (dms disabled)");
|
||||
return;
|
||||
@@ -763,20 +807,20 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
if (dmPolicy !== "open") {
|
||||
const permitted = allowListMatches({
|
||||
allowList: effectiveAllowFromLower,
|
||||
id: message.user,
|
||||
id: directUserId,
|
||||
});
|
||||
if (!permitted) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const sender = await resolveUserName(message.user);
|
||||
const sender = await resolveUserName(directUserId);
|
||||
const senderName = sender?.name ?? undefined;
|
||||
const { code, created } = await upsertProviderPairingRequest({
|
||||
provider: "slack",
|
||||
id: message.user,
|
||||
id: directUserId,
|
||||
meta: { name: senderName },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(
|
||||
`slack pairing request sender=${message.user} name=${senderName ?? "unknown"}`,
|
||||
`slack pairing request sender=${directUserId} name=${senderName ?? "unknown"}`,
|
||||
);
|
||||
try {
|
||||
await sendMessageSlack(
|
||||
@@ -811,31 +855,28 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const channelConfig = isRoom
|
||||
? resolveSlackChannelConfig({
|
||||
channelId: message.channel,
|
||||
channelName,
|
||||
channels: channelsConfig,
|
||||
})
|
||||
: null;
|
||||
|
||||
const wasMentioned =
|
||||
opts.wasMentioned ??
|
||||
(!isDirectMessage &&
|
||||
(Boolean(botUserId && message.text?.includes(`<@${botUserId}>`)) ||
|
||||
matchesMentionPatterns(message.text ?? "", mentionRegexes)));
|
||||
const sender = await resolveUserName(message.user);
|
||||
const senderName = sender?.name ?? message.user;
|
||||
const sender = message.user ? await resolveUserName(message.user) : null;
|
||||
const senderName =
|
||||
sender?.name ??
|
||||
message.username?.trim() ??
|
||||
message.user ??
|
||||
message.bot_id ??
|
||||
"unknown";
|
||||
const channelUserAuthorized = isRoom
|
||||
? resolveSlackUserAllowed({
|
||||
allowList: channelConfig?.users,
|
||||
userId: message.user,
|
||||
userId: senderId,
|
||||
userName: senderName,
|
||||
})
|
||||
: true;
|
||||
if (isRoom && !channelUserAuthorized) {
|
||||
logVerbose(
|
||||
`Blocked unauthorized slack sender ${message.user} (not in channel users)`,
|
||||
`Blocked unauthorized slack sender ${senderId} (not in channel users)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -844,7 +885,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
(allowList.length === 0 ||
|
||||
allowListMatches({
|
||||
allowList,
|
||||
id: message.user,
|
||||
id: senderId,
|
||||
name: senderName,
|
||||
})) &&
|
||||
channelUserAuthorized;
|
||||
@@ -1010,7 +1051,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
|
||||
SenderName: senderName,
|
||||
SenderId: message.user,
|
||||
SenderId: senderId,
|
||||
Provider: "slack" as const,
|
||||
Surface: "slack" as const,
|
||||
MessageSid: message.ts,
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
resolveTextChunkLimit,
|
||||
} from "../auto-reply/chunk.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { loadWebMedia } from "../web/media.js";
|
||||
import type { SlackTokenSource } from "./accounts.js";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import { resolveSlackBotToken } from "./token.js";
|
||||
|
||||
@@ -38,11 +40,17 @@ function resolveToken(params: {
|
||||
explicit?: string;
|
||||
accountId: string;
|
||||
fallbackToken?: string;
|
||||
fallbackSource?: SlackTokenSource;
|
||||
}) {
|
||||
const explicit = resolveSlackBotToken(params.explicit);
|
||||
if (explicit) return explicit;
|
||||
const fallback = resolveSlackBotToken(params.fallbackToken);
|
||||
if (!fallback) {
|
||||
logVerbose(
|
||||
`slack send: missing bot token for account=${params.accountId} explicit=${Boolean(
|
||||
params.explicit,
|
||||
)} source=${params.fallbackSource ?? "unknown"}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Slack bot token missing for account "${params.accountId}" (set slack.accounts.${params.accountId}.botToken or SLACK_BOT_TOKEN for default).`,
|
||||
);
|
||||
@@ -154,6 +162,7 @@ export async function sendMessageSlack(
|
||||
explicit: opts.token,
|
||||
accountId: account.accountId,
|
||||
fallbackToken: account.botToken,
|
||||
fallbackSource: account.botTokenSource,
|
||||
});
|
||||
const client = opts.client ?? new WebClient(token);
|
||||
const recipient = parseRecipient(to);
|
||||
|
||||
Reference in New Issue
Block a user