diff --git a/CHANGELOG.md b/CHANGELOG.md index a8deecc88..3151e77f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) 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..a85a7feb2 100644 --- a/src/telegram/webhook.test.ts +++ b/src/telegram/webhook.test.ts @@ -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" }); 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,