Files
Moltbot/src/channels/plugins/outbound/slack.ts
2026-02-17 13:36:48 +09:00

131 lines
3.7 KiB
TypeScript

import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { sendMessageSlack, type SlackSendIdentity } from "../../../slack/send.js";
import type { ChannelOutboundAdapter } from "../types.js";
function resolveSlackSendIdentity(identity?: OutboundIdentity): SlackSendIdentity | undefined {
if (!identity) {
return undefined;
}
const username = identity.name?.trim() || undefined;
const iconUrl = identity.avatarUrl?.trim() || undefined;
const rawEmoji = identity.emoji?.trim();
const iconEmoji = !iconUrl && rawEmoji && /^:[^:\s]+:$/.test(rawEmoji) ? rawEmoji : undefined;
if (!username && !iconUrl && !iconEmoji) {
return undefined;
}
return { username, iconUrl, iconEmoji };
}
async function applySlackMessageSendingHooks(params: {
to: string;
text: string;
threadTs?: string;
accountId?: string;
mediaUrl?: string;
}): Promise<{ cancelled: boolean; text: string }> {
const hookRunner = getGlobalHookRunner();
if (!hookRunner?.hasHooks("message_sending")) {
return { cancelled: false, text: params.text };
}
const hookResult = await hookRunner.runMessageSending(
{
to: params.to,
content: params.text,
metadata: {
threadTs: params.threadTs,
channelId: params.to,
...(params.mediaUrl ? { mediaUrl: params.mediaUrl } : {}),
},
},
{ channelId: "slack", accountId: params.accountId ?? undefined },
);
if (hookResult?.cancel) {
return { cancelled: true, text: params.text };
}
return { cancelled: false, text: hookResult?.content ?? params.text };
}
async function sendSlackOutboundMessage(params: {
to: string;
text: string;
mediaUrl?: string;
mediaLocalRoots?: readonly string[];
accountId?: string | null;
deps?: { sendSlack?: typeof sendMessageSlack } | null;
replyToId?: string | null;
threadId?: string | number | null;
identity?: OutboundIdentity;
}) {
const send = params.deps?.sendSlack ?? sendMessageSlack;
// Use threadId fallback so routed tool notifications stay in the Slack thread.
const threadTs =
params.replyToId ?? (params.threadId != null ? String(params.threadId) : undefined);
const hookResult = await applySlackMessageSendingHooks({
to: params.to,
text: params.text,
threadTs,
mediaUrl: params.mediaUrl,
accountId: params.accountId ?? undefined,
});
if (hookResult.cancelled) {
return {
channel: "slack" as const,
messageId: "cancelled-by-hook",
channelId: params.to,
meta: { cancelled: true },
};
}
const slackIdentity = resolveSlackSendIdentity(params.identity);
const result = await send(params.to, hookResult.text, {
threadTs,
accountId: params.accountId ?? undefined,
...(params.mediaUrl
? { mediaUrl: params.mediaUrl, mediaLocalRoots: params.mediaLocalRoots }
: {}),
...(slackIdentity ? { identity: slackIdentity } : {}),
});
return { channel: "slack" as const, ...result };
}
export const slackOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct",
chunker: null,
textChunkLimit: 4000,
sendText: async ({ to, text, accountId, deps, replyToId, threadId, identity }) => {
return await sendSlackOutboundMessage({
to,
text,
accountId,
deps,
replyToId,
threadId,
identity,
});
},
sendMedia: async ({
to,
text,
mediaUrl,
mediaLocalRoots,
accountId,
deps,
replyToId,
threadId,
identity,
}) => {
return await sendSlackOutboundMessage({
to,
text,
mediaUrl,
mediaLocalRoots,
accountId,
deps,
replyToId,
threadId,
identity,
});
},
};