From ecb91bbb1a1f9b31ac9dc6afbd8bfffdee722889 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 3800fea96287e225694466c548ee077ec91b5816 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 bab7eeaf919204df9f72c2c0340a4b1c8760c2d5 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)