diff --git a/CHANGELOG.md b/CHANGELOG.md index 595e0b9fd..978155aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Breaking - **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702) - **BREAKING:** Microsoft Teams is now a plugin; install `@clawdbot/msteams` via `clawdbot plugins install @clawdbot/msteams`. +- **BREAKING:** Discord/Telegram channel tokens now prefer config over env (env is fallback only). ### Changes - CLI: set process titles to `clawdbot-` for clearer process listings. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 3ded9f374..90052d403 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -13,6 +13,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa 2) Set the token for Clawdbot: - Env: `DISCORD_BOT_TOKEN=...` - Or config: `channels.discord.token: "..."`. + - If both are set, config wins; env is fallback. 3) Invite the bot to your server with message permissions. 4) Start the gateway. 5) DM access is pairing by default; approve the pairing code on first contact. @@ -39,8 +40,8 @@ Minimal config: 1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token. 2. Invite the bot to your server with the permissions required to read/send messages where you want to use it. 3. Configure Clawdbot with `DISCORD_BOT_TOKEN` (or `channels.discord.token` in `~/.clawdbot/clawdbot.json`). -4. Run the gateway; it auto-starts the Discord channel when a token is available (env or config) and `channels.discord.enabled` is not `false`. - - If you prefer env vars, set `DISCORD_BOT_TOKEN` (a config block is optional). +4. Run the gateway; it auto-starts the Discord channel when a token is available (config first, env fallback) and `channels.discord.enabled` is not `false`. + - If you prefer env vars, set `DISCORD_BOT_TOKEN` (and omit config). 5. Direct chats: use `user:` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected. 6. Guild channels: use `channel:` for delivery. Mentions are required by default and can be set per guild or per channel. 7. Direct chats: secure by default via `channels.discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `clawdbot pairing approve discord `. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index e50285aac..a7064d1e7 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -13,6 +13,7 @@ Status: production-ready for bot DMs + groups via grammY. Long-polling by defaul 2) Set the token: - Env: `TELEGRAM_BOT_TOKEN=...` - Or config: `channels.telegram.botToken: "..."`. + - If both are set, config wins; env is fallback. 3) Start the gateway. 4) DM access is pairing by default; approve the pairing code on first contact. @@ -60,11 +61,11 @@ Example: } ``` -Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account). +Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account; used only when config is missing). Multi-account support: use `channels.telegram.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. -3) Start the gateway. Telegram starts when a token is resolved (env or config). +3) Start the gateway. Telegram starts when a token is resolved (config first, env fallback). 4) DM access defaults to pairing. Approve the code when the bot is first contacted. 5) For groups: add the bot, decide privacy/admin behavior (below), then set `channels.telegram.groups` to control mention gating + allowlists. diff --git a/src/discord/token.test.ts b/src/discord/token.test.ts new file mode 100644 index 000000000..40968b2ec --- /dev/null +++ b/src/discord/token.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { ClawdbotConfig } from "../config/config.js"; +import { resolveDiscordToken } from "./token.js"; + +describe("resolveDiscordToken", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("prefers config token over env", () => { + vi.stubEnv("DISCORD_BOT_TOKEN", "env-token"); + const cfg = { + channels: { discord: { token: "cfg-token" } }, + } as ClawdbotConfig; + const res = resolveDiscordToken(cfg); + expect(res.token).toBe("cfg-token"); + expect(res.source).toBe("config"); + }); + + it("uses env token when config is missing", () => { + vi.stubEnv("DISCORD_BOT_TOKEN", "env-token"); + const cfg = { + channels: { discord: {} }, + } as ClawdbotConfig; + const res = resolveDiscordToken(cfg); + expect(res.token).toBe("env-token"); + expect(res.source).toBe("env"); + }); + + it("prefers account token for non-default accounts", () => { + vi.stubEnv("DISCORD_BOT_TOKEN", "env-token"); + const cfg = { + channels: { + discord: { + token: "base-token", + accounts: { + work: { token: "acct-token" }, + }, + }, + }, + } as ClawdbotConfig; + const res = resolveDiscordToken(cfg, { accountId: "work" }); + expect(res.token).toBe("acct-token"); + expect(res.source).toBe("config"); + }); +}); diff --git a/src/discord/token.ts b/src/discord/token.ts index d7e013f87..752e98ff8 100644 --- a/src/discord/token.ts +++ b/src/discord/token.ts @@ -29,13 +29,13 @@ export function resolveDiscordToken( if (accountToken) return { token: accountToken, source: "config" }; const allowEnv = accountId === DEFAULT_ACCOUNT_ID; + const configToken = allowEnv ? normalizeDiscordToken(discordCfg?.token ?? undefined) : undefined; + if (configToken) return { token: configToken, source: "config" }; + const envToken = allowEnv ? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN) : undefined; if (envToken) return { token: envToken, source: "env" }; - const configToken = allowEnv ? normalizeDiscordToken(discordCfg?.token ?? undefined) : undefined; - if (configToken) return { token: configToken, source: "config" }; - return { token: "", source: "none" }; } diff --git a/src/telegram/token.test.ts b/src/telegram/token.test.ts index 41252eb49..13c46172f 100644 --- a/src/telegram/token.test.ts +++ b/src/telegram/token.test.ts @@ -16,12 +16,22 @@ describe("resolveTelegramToken", () => { vi.unstubAllEnvs(); }); - it("prefers env token over config", () => { + it("prefers config token over env", () => { vi.stubEnv("TELEGRAM_BOT_TOKEN", "env-token"); const cfg = { channels: { telegram: { botToken: "cfg-token" } }, } as ClawdbotConfig; const res = resolveTelegramToken(cfg); + expect(res.token).toBe("cfg-token"); + expect(res.source).toBe("config"); + }); + + it("uses env token when config is missing", () => { + vi.stubEnv("TELEGRAM_BOT_TOKEN", "env-token"); + const cfg = { + channels: { telegram: {} }, + } as ClawdbotConfig; + const res = resolveTelegramToken(cfg); expect(res.token).toBe("env-token"); expect(res.source).toBe("env"); }); diff --git a/src/telegram/token.ts b/src/telegram/token.ts index 1804cdef9..33103584a 100644 --- a/src/telegram/token.ts +++ b/src/telegram/token.ts @@ -54,11 +54,6 @@ export function resolveTelegramToken( } const allowEnv = accountId === DEFAULT_ACCOUNT_ID; - const envToken = allowEnv ? (opts.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim() : ""; - if (envToken) { - return { token: envToken, source: "env" }; - } - const tokenFile = telegramCfg?.tokenFile?.trim(); if (tokenFile && allowEnv) { if (!fs.existsSync(tokenFile)) { @@ -81,5 +76,10 @@ export function resolveTelegramToken( return { token: configToken, source: "config" }; } + const envToken = allowEnv ? (opts.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim() : ""; + if (envToken) { + return { token: envToken, source: "env" }; + } + return { token: "", source: "none" }; }