From 42e2a9bfe41f620bb37a2f106e5a65e39afbdc4e Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 12 Jan 2026 21:59:28 -0500 Subject: [PATCH 1/3] Add accountId and config support to Telegram webhook The Telegram webhook and monitor now accept and pass through accountId and config parameters, enabling routing and configuration per Telegram account. Tests have been updated to verify correct bot instantiation and DM routing based on accountId bindings. --- src/telegram/bot.test.ts | 47 ++++++++++++++++++++++++++++++++++++ src/telegram/monitor.ts | 2 ++ src/telegram/webhook.test.ts | 19 ++++++++++++--- src/telegram/webhook.ts | 5 ++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 65caf1ebf..15d095459 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -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, + ) => Promise; + + 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< diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index 6ae8e45bb..7ed3f2faf 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -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, diff --git a/src/telegram/webhook.test.ts b/src/telegram/webhook.test.ts index 657cab274..ba5aa83cc 100644 --- a/src/telegram/webhook.test.ts +++ b/src/telegram/webhook.test.ts @@ -14,25 +14,33 @@ 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 { server } = await startTelegramWebhook({ token: "tok", + accountId: "opie", + config: { bindings: [] }, port: 0, // random free port abortSignal: abort.signal, }); + expect(createTelegramBotSpy).toHaveBeenCalledWith( + expect.objectContaining({ accountId: "opie" }), + ); const address = server.address(); if (!address || typeof address === "string") throw new Error("no address"); const url = `http://127.0.0.1:${address.port}`; @@ -46,9 +54,12 @@ describe("startTelegramWebhook", () => { it("invokes webhook handler on matching path", async () => { handlerSpy.mockClear(); + createTelegramBotSpy.mockClear(); const abort = new AbortController(); const { server } = await startTelegramWebhook({ token: "tok", + accountId: "opie", + config: { bindings: [] }, port: 0, abortSignal: abort.signal, path: "/hook", diff --git a/src/telegram/webhook.ts b/src/telegram/webhook.ts index b0b26b6c7..479f98466 100644 --- a/src/telegram/webhook.ts +++ b/src/telegram/webhook.ts @@ -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, From 97b6a83137b3ae32e768c559f947ca8173e5903a Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 12 Jan 2026 22:16:46 -0500 Subject: [PATCH 2/3] Improve webhook test config verification Refactors tests to use a shared config object and adds stricter assertions to verify that the config is passed correctly to createTelegramBot. Ensures the bindings property is checked in the test expectations. --- src/telegram/webhook.test.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/telegram/webhook.test.ts b/src/telegram/webhook.test.ts index ba5aa83cc..a85a7feb2 100644 --- a/src/telegram/webhook.test.ts +++ b/src/telegram/webhook.test.ts @@ -31,15 +31,19 @@ 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: { bindings: [] }, + config: cfg, port: 0, // random free port abortSignal: abort.signal, }); expect(createTelegramBotSpy).toHaveBeenCalledWith( - expect.objectContaining({ accountId: "opie" }), + expect.objectContaining({ + accountId: "opie", + config: expect.objectContaining({ bindings: [] }), + }), ); const address = server.address(); if (!address || typeof address === "string") throw new Error("no address"); @@ -56,14 +60,21 @@ describe("startTelegramWebhook", () => { handlerSpy.mockClear(); createTelegramBotSpy.mockClear(); const abort = new AbortController(); + const cfg = { bindings: [] }; const { server } = await startTelegramWebhook({ token: "tok", accountId: "opie", - config: { bindings: [] }, + 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" }); From a81e3f6d337545fbcfd8cc3d6bf29afa989bab36 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 13 Jan 2026 03:40:30 +0000 Subject: [PATCH 3/3] fix: respect telegram webhook bindings (#821) (thanks @gumadeiras) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b84b1a2..3ea8ee5be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,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)