Merge pull request #821 from gumadeiras/fix-bindings-telegram-webhook

Telegram: fix webhook multi-account routing (respect bindings.accountId)
This commit is contained in:
Peter Steinberger
2026-01-13 03:41:17 +00:00
committed by GitHub
5 changed files with 81 additions and 4 deletions

View File

@@ -15,6 +15,7 @@
- Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm)
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake)
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
- Telegram: respect account-scoped bindings when webhook mode is enabled. (#821 — thanks @gumadeiras)
- Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus)
- Browser tools: treat explicit `maxChars: 0` as unlimited while keeping the default limit only when omitted. (#796 — thanks @gabriel-trigo)
- Tools: allow Claude/Gemini tool param aliases (`file_path`, `old_string`, `new_string`) while enforcing required params at runtime. (#793 — thanks @hsrvc)

View File

@@ -887,6 +887,53 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("routes DMs by telegram accountId binding", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
accounts: {
opie: {
botToken: "tok-opie",
dmPolicy: "open",
},
},
},
bindings: [
{
agentId: "opie",
match: { provider: "telegram", accountId: "opie" },
},
],
});
createTelegramBot({ token: "tok", accountId: "opie" });
const handler = getOnHandler("message") as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: { id: 123, type: "private" },
from: { id: 999, username: "testuser" },
text: "hello",
date: 1736380800,
message_id: 42,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expect(payload.AccountId).toBe("opie");
expect(payload.SessionKey).toBe("agent:opie:main");
});
it("allows per-group requireMention override", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<

View File

@@ -96,6 +96,8 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
if (opts.useWebhook) {
await startTelegramWebhook({
token,
accountId: account.accountId,
config: cfg,
path: opts.webhookPath,
port: opts.webhookPort,
secret: opts.webhookSecret,

View File

@@ -14,25 +14,37 @@ const handlerSpy = vi.fn(
const setWebhookSpy = vi.fn();
const stopSpy = vi.fn();
const createTelegramBotSpy = vi.fn(() => ({
api: { setWebhook: setWebhookSpy },
stop: stopSpy,
}));
vi.mock("grammy", () => ({
webhookCallback: () => handlerSpy,
}));
vi.mock("./bot.js", () => ({
createTelegramBot: () => ({
api: { setWebhook: setWebhookSpy },
stop: stopSpy,
}),
createTelegramBot: (...args: unknown[]) => createTelegramBotSpy(...args),
}));
describe("startTelegramWebhook", () => {
it("starts server, registers webhook, and serves health", async () => {
createTelegramBotSpy.mockClear();
const abort = new AbortController();
const cfg = { bindings: [] };
const { server } = await startTelegramWebhook({
token: "tok",
accountId: "opie",
config: cfg,
port: 0, // random free port
abortSignal: abort.signal,
});
expect(createTelegramBotSpy).toHaveBeenCalledWith(
expect.objectContaining({
accountId: "opie",
config: expect.objectContaining({ bindings: [] }),
}),
);
const address = server.address();
if (!address || typeof address === "string") throw new Error("no address");
const url = `http://127.0.0.1:${address.port}`;
@@ -46,13 +58,23 @@ describe("startTelegramWebhook", () => {
it("invokes webhook handler on matching path", async () => {
handlerSpy.mockClear();
createTelegramBotSpy.mockClear();
const abort = new AbortController();
const cfg = { bindings: [] };
const { server } = await startTelegramWebhook({
token: "tok",
accountId: "opie",
config: cfg,
port: 0,
abortSignal: abort.signal,
path: "/hook",
});
expect(createTelegramBotSpy).toHaveBeenCalledWith(
expect.objectContaining({
accountId: "opie",
config: expect.objectContaining({ bindings: [] }),
}),
);
const addr = server.address();
if (!addr || typeof addr === "string") throw new Error("no addr");
await fetch(`http://127.0.0.1:${addr.port}/hook`, { method: "POST" });

View File

@@ -1,6 +1,7 @@
import { createServer } from "node:http";
import { webhookCallback } from "grammy";
import type { ClawdbotConfig } from "../config/config.js";
import { formatErrorMessage } from "../infra/errors.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
@@ -8,6 +9,8 @@ import { createTelegramBot } from "./bot.js";
export async function startTelegramWebhook(opts: {
token: string;
accountId?: string;
config?: ClawdbotConfig;
path?: string;
port?: number;
host?: string;
@@ -27,6 +30,8 @@ export async function startTelegramWebhook(opts: {
token: opts.token,
runtime,
proxyFetch: opts.fetch,
config: opts.config,
accountId: opts.accountId,
});
const handler = webhookCallback(bot, "http", {
secretToken: opts.secret,