Slack: add some fixes and connect it all up

This commit is contained in:
Shadow
2026-01-04 01:53:15 -06:00
parent 02d7e286ea
commit 8c38a7fee8
45 changed files with 1568 additions and 89 deletions

View File

@@ -15,6 +15,7 @@ export type ProviderKind =
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
@@ -47,6 +48,7 @@ type ReloadAction =
| "restart-provider:whatsapp"
| "restart-provider:telegram"
| "restart-provider:discord"
| "restart-provider:slack"
| "restart-provider:signal"
| "restart-provider:imessage";
@@ -70,6 +72,7 @@ const RELOAD_RULES: ReloadRule[] = [
{ prefix: "web", kind: "hot", actions: ["restart-provider:whatsapp"] },
{ prefix: "telegram", kind: "hot", actions: ["restart-provider:telegram"] },
{ prefix: "discord", kind: "hot", actions: ["restart-provider:discord"] },
{ prefix: "slack", kind: "hot", actions: ["restart-provider:slack"] },
{ prefix: "signal", kind: "hot", actions: ["restart-provider:signal"] },
{ prefix: "imessage", kind: "hot", actions: ["restart-provider:imessage"] },
{ prefix: "identity", kind: "none" },
@@ -200,6 +203,9 @@ export function buildGatewayReloadPlan(
case "restart-provider:discord":
plan.restartProviders.add("discord");
break;
case "restart-provider:slack":
plan.restartProviders.add("slack");
break;
case "restart-provider:signal":
plan.restartProviders.add("signal");
break;

View File

@@ -23,6 +23,7 @@ export type HookMappingResolved = {
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to?: string;
@@ -61,6 +62,7 @@ export type HookAction =
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to?: string;
@@ -99,7 +101,14 @@ type HookTransformResult = Partial<{
name: string;
sessionKey: string;
deliver: boolean;
channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
channel:
| "last"
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to: string;
thinking: string;
timeoutSeconds: number;

View File

@@ -635,6 +635,7 @@ export const CronPayloadSchema = Type.Union([
Type.Literal("whatsapp"),
Type.Literal("telegram"),
Type.Literal("discord"),
Type.Literal("slack"),
]),
),
to: Type.Optional(Type.String()),

View File

@@ -37,6 +37,7 @@ type HookDispatchers = {
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to?: string;

View File

@@ -8,6 +8,11 @@ import { type DiscordProbe, probeDiscord } from "../../discord/probe.js";
import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js";
import { webAuthExists } from "../../providers/web/index.js";
import { probeSignal, type SignalProbe } from "../../signal/probe.js";
import { probeSlack, type SlackProbe } from "../../slack/probe.js";
import {
resolveSlackAppToken,
resolveSlackBotToken,
} from "../../slack/token.js";
import { probeTelegram, type TelegramProbe } from "../../telegram/probe.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import { getWebAuthAgeMs, readWebSelfId } from "../../web/session.js";
@@ -74,6 +79,41 @@ export const providersHandlers: GatewayRequestHandlers = {
discordLastProbeAt = Date.now();
}
const slackCfg = cfg.slack;
const slackEnabled = slackCfg?.enabled !== false;
const slackBotEnvToken = slackEnabled
? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN)
: undefined;
const slackBotConfigToken = slackEnabled
? resolveSlackBotToken(slackCfg?.botToken)
: undefined;
const slackBotToken = slackBotEnvToken ?? slackBotConfigToken ?? "";
const slackBotTokenSource = slackBotEnvToken
? "env"
: slackBotConfigToken
? "config"
: "none";
const slackAppEnvToken = slackEnabled
? resolveSlackAppToken(process.env.SLACK_APP_TOKEN)
: undefined;
const slackAppConfigToken = slackEnabled
? resolveSlackAppToken(slackCfg?.appToken)
: undefined;
const slackAppToken = slackAppEnvToken ?? slackAppConfigToken ?? "";
const slackAppTokenSource = slackAppEnvToken
? "env"
: slackAppConfigToken
? "config"
: "none";
const slackConfigured =
slackEnabled && Boolean(slackBotToken) && Boolean(slackAppToken);
let slackProbe: SlackProbe | undefined;
let slackLastProbeAt: number | null = null;
if (probe && slackConfigured) {
slackProbe = await probeSlack(slackBotToken, timeoutMs);
slackLastProbeAt = Date.now();
}
const signalCfg = cfg.signal;
const signalEnabled = signalCfg?.enabled !== false;
const signalHost = signalCfg?.httpHost?.trim() || "127.0.0.1";
@@ -152,6 +192,17 @@ export const providersHandlers: GatewayRequestHandlers = {
probe: discordProbe,
lastProbeAt: discordLastProbeAt,
},
slack: {
configured: slackConfigured,
botTokenSource: slackBotTokenSource,
appTokenSource: slackAppTokenSource,
running: runtime.slack.running,
lastStartAt: runtime.slack.lastStartAt ?? null,
lastStopAt: runtime.slack.lastStopAt ?? null,
lastError: runtime.slack.lastError ?? null,
probe: slackProbe,
lastProbeAt: slackLastProbeAt,
},
signal: {
configured: signalConfigured,
baseUrl: signalBaseUrl,

View File

@@ -3,6 +3,7 @@ import { sendMessageDiscord } from "../../discord/index.js";
import { shouldLogVerbose } from "../../globals.js";
import { sendMessageIMessage } from "../../imessage/index.js";
import { sendMessageSignal } from "../../signal/index.js";
import { sendMessageSlack } from "../../slack/send.js";
import { sendMessageTelegram } from "../../telegram/send.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import { sendMessageWhatsApp } from "../../web/outbound.js";
@@ -87,6 +88,22 @@ export const sendHandlers: GatewayRequestHandlers = {
payload,
});
respond(true, payload, undefined, { provider });
} else if (provider === "slack") {
const result = await sendMessageSlack(to, message, {
mediaUrl: request.mediaUrl,
});
const payload = {
runId: idem,
messageId: result.messageId,
channelId: result.channelId,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else if (provider === "signal") {
const cfg = loadConfig();
const host = cfg.signal?.httpHost?.trim() || "127.0.0.1";

View File

@@ -7,6 +7,11 @@ import type { createSubsystemLogger } from "../logging.js";
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
import type { RuntimeEnv } from "../runtime.js";
import { monitorSignalProvider } from "../signal/index.js";
import {
monitorSlackProvider,
resolveSlackAppToken,
resolveSlackBotToken,
} from "../slack/index.js";
import { monitorTelegramProvider } from "../telegram/monitor.js";
import { probeTelegram } from "../telegram/probe.js";
import { resolveTelegramToken } from "../telegram/token.js";
@@ -29,6 +34,13 @@ export type DiscordRuntimeStatus = {
lastError?: string | null;
};
export type SlackRuntimeStatus = {
running: boolean;
lastStartAt?: number | null;
lastStopAt?: number | null;
lastError?: string | null;
};
export type SignalRuntimeStatus = {
running: boolean;
lastStartAt?: number | null;
@@ -50,6 +62,7 @@ export type ProviderRuntimeSnapshot = {
whatsapp: WebProviderStatus;
telegram: TelegramRuntimeStatus;
discord: DiscordRuntimeStatus;
slack: SlackRuntimeStatus;
signal: SignalRuntimeStatus;
imessage: IMessageRuntimeStatus;
};
@@ -61,11 +74,13 @@ type ProviderManagerOptions = {
logWhatsApp: SubsystemLogger;
logTelegram: SubsystemLogger;
logDiscord: SubsystemLogger;
logSlack: SubsystemLogger;
logSignal: SubsystemLogger;
logIMessage: SubsystemLogger;
whatsappRuntimeEnv: RuntimeEnv;
telegramRuntimeEnv: RuntimeEnv;
discordRuntimeEnv: RuntimeEnv;
slackRuntimeEnv: RuntimeEnv;
signalRuntimeEnv: RuntimeEnv;
imessageRuntimeEnv: RuntimeEnv;
};
@@ -79,6 +94,8 @@ export type ProviderManager = {
stopTelegramProvider: () => Promise<void>;
startDiscordProvider: () => Promise<void>;
stopDiscordProvider: () => Promise<void>;
startSlackProvider: () => Promise<void>;
stopSlackProvider: () => Promise<void>;
startSignalProvider: () => Promise<void>;
stopSignalProvider: () => Promise<void>;
startIMessageProvider: () => Promise<void>;
@@ -94,11 +111,13 @@ export function createProviderManager(
logWhatsApp,
logTelegram,
logDiscord,
logSlack,
logSignal,
logIMessage,
whatsappRuntimeEnv,
telegramRuntimeEnv,
discordRuntimeEnv,
slackRuntimeEnv,
signalRuntimeEnv,
imessageRuntimeEnv,
} = opts;
@@ -106,11 +125,13 @@ export function createProviderManager(
let whatsappAbort: AbortController | null = null;
let telegramAbort: AbortController | null = null;
let discordAbort: AbortController | null = null;
let slackAbort: AbortController | null = null;
let signalAbort: AbortController | null = null;
let imessageAbort: AbortController | null = null;
let whatsappTask: Promise<unknown> | null = null;
let telegramTask: Promise<unknown> | null = null;
let discordTask: Promise<unknown> | null = null;
let slackTask: Promise<unknown> | null = null;
let signalTask: Promise<unknown> | null = null;
let imessageTask: Promise<unknown> | null = null;
@@ -137,6 +158,12 @@ export function createProviderManager(
lastStopAt: null,
lastError: null,
};
let slackRuntime: SlackRuntimeStatus = {
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
};
let signalRuntime: SignalRuntimeStatus = {
running: false,
lastStartAt: null,
@@ -432,6 +459,93 @@ export function createProviderManager(
};
};
const startSlackProvider = async () => {
if (slackTask) return;
const cfg = loadConfig();
if (cfg.slack?.enabled === false) {
slackRuntime = {
...slackRuntime,
running: false,
lastError: "disabled",
};
if (shouldLogVerbose()) {
logSlack.debug("slack provider disabled (slack.enabled=false)");
}
return;
}
const botToken = resolveSlackBotToken(
process.env.SLACK_BOT_TOKEN ?? cfg.slack?.botToken ?? undefined,
);
const appToken = resolveSlackAppToken(
process.env.SLACK_APP_TOKEN ?? cfg.slack?.appToken ?? undefined,
);
if (!botToken || !appToken) {
slackRuntime = {
...slackRuntime,
running: false,
lastError: "not configured",
};
if (shouldLogVerbose()) {
logSlack.debug(
"slack provider not configured (missing SLACK_BOT_TOKEN/SLACK_APP_TOKEN)",
);
}
return;
}
logSlack.info(
`starting provider${cfg.slack ? "" : " (no slack config; tokens via env)"}`,
);
slackAbort = new AbortController();
slackRuntime = {
...slackRuntime,
running: true,
lastStartAt: Date.now(),
lastError: null,
};
const task = monitorSlackProvider({
botToken,
appToken,
runtime: slackRuntimeEnv,
abortSignal: slackAbort.signal,
mediaMaxMb: cfg.slack?.mediaMaxMb,
slashCommand: cfg.slack?.slashCommand,
})
.catch((err) => {
slackRuntime = {
...slackRuntime,
lastError: formatError(err),
};
logSlack.error(`provider exited: ${formatError(err)}`);
})
.finally(() => {
slackAbort = null;
slackTask = null;
slackRuntime = {
...slackRuntime,
running: false,
lastStopAt: Date.now(),
};
});
slackTask = task;
};
const stopSlackProvider = async () => {
if (!slackAbort && !slackTask) return;
slackAbort?.abort();
try {
await slackTask;
} catch {
// ignore
}
slackAbort = null;
slackTask = null;
slackRuntime = {
...slackRuntime,
running: false,
lastStopAt: Date.now(),
};
};
const startSignalProvider = async () => {
if (signalTask) return;
const cfg = loadConfig();
@@ -634,6 +748,7 @@ export function createProviderManager(
const startProviders = async () => {
await startWhatsAppProvider();
await startDiscordProvider();
await startSlackProvider();
await startTelegramProvider();
await startSignalProvider();
await startIMessageProvider();
@@ -652,6 +767,7 @@ export function createProviderManager(
whatsapp: { ...whatsappRuntime },
telegram: { ...telegramRuntime },
discord: { ...discordRuntime },
slack: { ...slackRuntime },
signal: { ...signalRuntime },
imessage: { ...imessageRuntime },
});
@@ -665,6 +781,8 @@ export function createProviderManager(
stopTelegramProvider,
startDiscordProvider,
stopDiscordProvider,
startSlackProvider,
stopSlackProvider,
startSignalProvider,
stopSignalProvider,
startIMessageProvider,

View File

@@ -55,6 +55,12 @@ const hoisted = vi.hoisted(() => {
lastStopAt: null,
lastError: null,
},
slack: {
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
signal: {
running: false,
lastStartAt: null,
@@ -78,6 +84,8 @@ const hoisted = vi.hoisted(() => {
stopTelegramProvider: vi.fn(async () => {}),
startDiscordProvider: vi.fn(async () => {}),
stopDiscordProvider: vi.fn(async () => {}),
startSlackProvider: vi.fn(async () => {}),
stopSlackProvider: vi.fn(async () => {}),
startSignalProvider: vi.fn(async () => {}),
stopSignalProvider: vi.fn(async () => {}),
startIMessageProvider: vi.fn(async () => {}),

View File

@@ -148,12 +148,14 @@ const logWsControl = log.child("ws");
const logWhatsApp = logProviders.child("whatsapp");
const logTelegram = logProviders.child("telegram");
const logDiscord = logProviders.child("discord");
const logSlack = logProviders.child("slack");
const logSignal = logProviders.child("signal");
const logIMessage = logProviders.child("imessage");
const canvasRuntime = runtimeForLogger(logCanvas);
const whatsappRuntimeEnv = runtimeForLogger(logWhatsApp);
const telegramRuntimeEnv = runtimeForLogger(logTelegram);
const discordRuntimeEnv = runtimeForLogger(logDiscord);
const slackRuntimeEnv = runtimeForLogger(logSlack);
const signalRuntimeEnv = runtimeForLogger(logSignal);
const imessageRuntimeEnv = runtimeForLogger(logIMessage);
@@ -478,6 +480,7 @@ export async function startGatewayServer(
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to?: string;
@@ -722,11 +725,13 @@ export async function startGatewayServer(
logWhatsApp,
logTelegram,
logDiscord,
logSlack,
logSignal,
logIMessage,
whatsappRuntimeEnv,
telegramRuntimeEnv,
discordRuntimeEnv,
slackRuntimeEnv,
signalRuntimeEnv,
imessageRuntimeEnv,
});
@@ -736,11 +741,13 @@ export async function startGatewayServer(
startWhatsAppProvider,
startTelegramProvider,
startDiscordProvider,
startSlackProvider,
startSignalProvider,
startIMessageProvider,
stopWhatsAppProvider,
stopTelegramProvider,
stopDiscordProvider,
stopSlackProvider,
stopSignalProvider,
stopIMessageProvider,
markWhatsAppLoggedOut,
@@ -1593,7 +1600,7 @@ export async function startGatewayServer(
}
}
// Launch configured providers (WhatsApp Web, Discord, Telegram) so gateway replies via the
// Launch configured providers (WhatsApp Web, Discord, Slack, Telegram) so gateway replies via the
// surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS.
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
try {
@@ -1703,6 +1710,9 @@ export async function startGatewayServer(
startDiscordProvider,
);
}
if (plan.restartProviders.has("slack")) {
await restartProvider("slack", stopSlackProvider, startSlackProvider);
}
if (plan.restartProviders.has("signal")) {
await restartProvider(
"signal",
@@ -1806,6 +1816,7 @@ export async function startGatewayServer(
await stopWhatsAppProvider();
await stopTelegramProvider();
await stopDiscordProvider();
await stopSlackProvider();
await stopSignalProvider();
await stopIMessageProvider();
await stopGmailWatcher();