diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index c578f8fc4..aeafdae3c 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -1,10 +1,19 @@ import { describe, expect, it, vi } from "vitest"; +import * as replyModule from "../auto-reply/reply.js"; +import { createTelegramBot } from "./bot.js"; const useSpy = vi.fn(); const onSpy = vi.fn(); const stopSpy = vi.fn(); -type ApiStub = { config: { use: (arg: unknown) => void } }; -const apiStub: ApiStub = { config: { use: useSpy } }; +const sendChatActionSpy = vi.fn(); +type ApiStub = { + config: { use: (arg: unknown) => void }; + sendChatAction: typeof sendChatActionSpy; +}; +const apiStub: ApiStub = { + config: { use: useSpy }, + sendChatAction: sendChatActionSpy, +}; vi.mock("grammy", () => ({ Bot: class { @@ -24,13 +33,13 @@ vi.mock("@grammyjs/transformer-throttler", () => ({ })); vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(); + const replySpy = vi.fn(async (_ctx, opts) => { + await opts?.onReplyStart?.(); + return undefined; + }); return { getReplyFromConfig: replySpy, __replySpy: replySpy }; }); -import { createTelegramBot } from "./bot.js"; -import * as replyModule from "../auto-reply/reply.js"; - describe("createTelegramBot", () => { it("installs grammY throttler", () => { createTelegramBot({ token: "tok" }); @@ -47,7 +56,9 @@ describe("createTelegramBot", () => { createTelegramBot({ token: "tok" }); expect(onSpy).toHaveBeenCalledWith("message", expect.any(Function)); - const handler = onSpy.mock.calls[0][1] as (ctx: any) => Promise; + const handler = onSpy.mock.calls[0][1] as ( + ctx: Record, + ) => Promise; const message = { chat: { id: 1234, type: "private" }, @@ -72,4 +83,21 @@ describe("createTelegramBot", () => { ); expect(payload.Body).toContain("hello world"); }); + + it("triggers typing cue via onReplyStart", async () => { + onSpy.mockReset(); + sendChatActionSpy.mockReset(); + + createTelegramBot({ token: "tok" }); + const handler = onSpy.mock.calls[0][1] as ( + ctx: Record, + ) => Promise; + await handler({ + message: { chat: { id: 42, type: "private" }, text: "hi" }, + me: { username: "clawdis_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(sendChatActionSpy).toHaveBeenCalledWith(42, "typing"); + }); }); diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index 3bc0b0394..19a94fb08 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -1,13 +1,14 @@ // @ts-nocheck import { Buffer } from "node:buffer"; + import { apiThrottler } from "@grammyjs/transformer-throttler"; import type { ApiClientOptions, Message } from "grammy"; import { Bot, InputFile, webhookCallback } from "grammy"; import { chunkText } from "../auto-reply/chunk.js"; +import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { getReplyFromConfig } from "../auto-reply/reply.js"; import type { ReplyPayload } from "../auto-reply/types.js"; -import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { loadConfig } from "../config/config.js"; import { danger, logVerbose } from "../globals.js"; import { getChildLogger } from "../logging.js"; @@ -70,6 +71,16 @@ export function createTelegramBot(opts: TelegramBotOptions) { const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup"; + const sendTyping = async () => { + try { + await bot.api.sendChatAction(chatId, "typing"); + } catch (err) { + logVerbose( + `telegram typing cue failed for chat ${chatId}: ${String(err)}`, + ); + } + }; + // allowFrom for direct chats if (!isGroup && Array.isArray(allowFrom) && allowFrom.length > 0) { const candidate = String(chatId); @@ -99,7 +110,12 @@ export function createTelegramBot(opts: TelegramBotOptions) { } const media = await resolveMedia(ctx, mediaMaxBytes); - const rawBody = (msg.text ?? msg.caption ?? media?.placeholder ?? "").trim(); + const rawBody = ( + msg.text ?? + msg.caption ?? + media?.placeholder ?? + "" + ).trim(); if (!rawBody) return; const body = formatAgentEnvelope({ @@ -126,7 +142,11 @@ export function createTelegramBot(opts: TelegramBotOptions) { MediaUrl: media?.path, }; - const replyResult = await getReplyFromConfig(ctxPayload, {}, cfg); + const replyResult = await getReplyFromConfig( + ctxPayload, + { onReplyStart: sendTyping }, + cfg, + ); const replies = replyResult ? Array.isArray(replyResult) ? replyResult