fix: prefer config tokens over env for discord/telegram

This commit is contained in:
Peter Steinberger
2026-01-16 23:12:50 +00:00
parent bf72a126d1
commit 106e308953
7 changed files with 73 additions and 13 deletions

View File

@@ -11,6 +11,7 @@
### Breaking ### Breaking
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702) - **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:** 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 ### Changes
- CLI: set process titles to `clawdbot-<command>` for clearer process listings. - CLI: set process titles to `clawdbot-<command>` for clearer process listings.

View File

@@ -13,6 +13,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
2) Set the token for Clawdbot: 2) Set the token for Clawdbot:
- Env: `DISCORD_BOT_TOKEN=...` - Env: `DISCORD_BOT_TOKEN=...`
- Or config: `channels.discord.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. 3) Invite the bot to your server with message permissions.
4) Start the gateway. 4) Start the gateway.
5) DM access is pairing by default; approve the pairing code on first contact. 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. 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. 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`). 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`. 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` (a config block is optional). - If you prefer env vars, set `DISCORD_BOT_TOKEN` (and omit config).
5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected. 5. Direct chats: use `user:<id>` (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:<channelId>` for delivery. Mentions are required by default and can be set per guild or per channel. 6. Guild channels: use `channel:<channelId>` 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 <code>`. 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 <code>`.

View File

@@ -13,6 +13,7 @@ Status: production-ready for bot DMs + groups via grammY. Long-polling by defaul
2) Set the token: 2) Set the token:
- Env: `TELEGRAM_BOT_TOKEN=...` - Env: `TELEGRAM_BOT_TOKEN=...`
- Or config: `channels.telegram.botToken: "..."`. - Or config: `channels.telegram.botToken: "..."`.
- If both are set, config wins; env is fallback.
3) Start the gateway. 3) Start the gateway.
4) DM access is pairing by default; approve the pairing code on first contact. 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. 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. 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. 5) For groups: add the bot, decide privacy/admin behavior (below), then set `channels.telegram.groups` to control mention gating + allowlists.

47
src/discord/token.test.ts Normal file
View File

@@ -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");
});
});

View File

@@ -29,13 +29,13 @@ export function resolveDiscordToken(
if (accountToken) return { token: accountToken, source: "config" }; if (accountToken) return { token: accountToken, source: "config" };
const allowEnv = accountId === DEFAULT_ACCOUNT_ID; const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
const configToken = allowEnv ? normalizeDiscordToken(discordCfg?.token ?? undefined) : undefined;
if (configToken) return { token: configToken, source: "config" };
const envToken = allowEnv const envToken = allowEnv
? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN) ? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN)
: undefined; : undefined;
if (envToken) return { token: envToken, source: "env" }; 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" }; return { token: "", source: "none" };
} }

View File

@@ -16,12 +16,22 @@ describe("resolveTelegramToken", () => {
vi.unstubAllEnvs(); vi.unstubAllEnvs();
}); });
it("prefers env token over config", () => { it("prefers config token over env", () => {
vi.stubEnv("TELEGRAM_BOT_TOKEN", "env-token"); vi.stubEnv("TELEGRAM_BOT_TOKEN", "env-token");
const cfg = { const cfg = {
channels: { telegram: { botToken: "cfg-token" } }, channels: { telegram: { botToken: "cfg-token" } },
} as ClawdbotConfig; } as ClawdbotConfig;
const res = resolveTelegramToken(cfg); 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.token).toBe("env-token");
expect(res.source).toBe("env"); expect(res.source).toBe("env");
}); });

View File

@@ -54,11 +54,6 @@ export function resolveTelegramToken(
} }
const allowEnv = accountId === DEFAULT_ACCOUNT_ID; 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(); const tokenFile = telegramCfg?.tokenFile?.trim();
if (tokenFile && allowEnv) { if (tokenFile && allowEnv) {
if (!fs.existsSync(tokenFile)) { if (!fs.existsSync(tokenFile)) {
@@ -81,5 +76,10 @@ export function resolveTelegramToken(
return { token: configToken, source: "config" }; 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" }; return { token: "", source: "none" };
} }