fix(auto-reply): acknowledge reset triggers

This commit is contained in:
Peter Steinberger
2025-12-10 15:55:20 +00:00
parent 8f456ea73b
commit 51d77aea2e
5 changed files with 108 additions and 82 deletions

View File

@@ -18,6 +18,9 @@ import { saveMediaBuffer } from "../media/store.js";
import type { RuntimeEnv } from "../runtime.js";
import { loadWebMedia } from "../web/media.js";
const PARSE_ERR_RE =
/can't parse entities|parse entities|find end of the entity/i;
type TelegramMessage = Message.CommonMessage;
type TelegramContext = {
@@ -203,7 +206,7 @@ async function deliverReplies(params: {
: [];
if (mediaList.length === 0) {
for (const chunk of chunkText(reply.text || "", 4000)) {
await bot.api.sendMessage(chatId, chunk, { parse_mode: "Markdown" });
await sendTelegramText(bot, chatId, chunk, runtime);
}
continue;
}
@@ -303,3 +306,25 @@ async function resolveMedia(
else if (msg.audio || msg.voice) placeholder = "<media:audio>";
return { path: saved.path, contentType: saved.contentType, placeholder };
}
async function sendTelegramText(
bot: Bot,
chatId: string,
text: string,
runtime: RuntimeEnv,
) {
try {
await bot.api.sendMessage(chatId, text, { parse_mode: "Markdown" });
} catch (err) {
if (PARSE_ERR_RE.test(String(err ?? ""))) {
runtime.log?.(
`telegram markdown parse failed; retrying without formatting: ${String(
err,
)}`,
);
await bot.api.sendMessage(chatId, text);
return;
}
throw err;
}
}

View File

@@ -1,87 +1,33 @@
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { sendMessageTelegram } from "./send.js";
const originalEnv = process.env.TELEGRAM_BOT_TOKEN;
const loadWebMediaMock = vi.fn();
const apiMock = {
sendMessage: vi.fn(),
sendPhoto: vi.fn(),
sendVideo: vi.fn(),
sendAudio: vi.fn(),
sendDocument: vi.fn(),
};
vi.mock("grammy", async (orig) => {
const actual = await orig();
return {
...actual,
Bot: vi.fn().mockImplementation(() => ({ api: apiMock })),
InputFile: actual.InputFile,
};
});
vi.mock("../web/media.js", () => ({
loadWebMedia: (...args: unknown[]) => loadWebMediaMock(...args),
}));
describe("sendMessageTelegram", () => {
beforeEach(() => {
vi.resetAllMocks();
process.env.TELEGRAM_BOT_TOKEN = "token123";
});
afterAll(() => {
process.env.TELEGRAM_BOT_TOKEN = originalEnv;
});
it("sends text and returns ids", async () => {
apiMock.sendMessage.mockResolvedValueOnce({
message_id: 42,
chat: { id: 999 },
});
const res = await sendMessageTelegram("12345", "hello", {
verbose: false,
api: apiMock as never,
});
expect(res).toEqual({ messageId: "42", chatId: "999" });
expect(apiMock.sendMessage).toHaveBeenCalled();
});
it("throws when token missing", async () => {
process.env.TELEGRAM_BOT_TOKEN = "";
await expect(sendMessageTelegram("1", "hi")).rejects.toThrow(
/TELEGRAM_BOT_TOKEN/,
it("falls back to plain text when Telegram rejects Markdown", async () => {
const chatId = "123";
const parseErr = new Error(
"400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 9",
);
});
const sendMessage = vi
.fn()
.mockRejectedValueOnce(parseErr)
.mockResolvedValueOnce({
message_id: 42,
chat: { id: chatId },
});
const api = { sendMessage } as unknown as { sendMessage: typeof sendMessage };
it("throws on api error", async () => {
apiMock.sendMessage.mockRejectedValueOnce(new Error("bad token"));
const res = await sendMessageTelegram(chatId, "_oops_", {
token: "tok",
api,
verbose: true,
});
await expect(
sendMessageTelegram("1", "hi", { api: apiMock as never }),
).rejects.toThrow(/bad token/i);
});
it("sends media via appropriate method", async () => {
loadWebMediaMock.mockResolvedValueOnce({
buffer: Buffer.from([1, 2, 3]),
contentType: "image/jpeg",
kind: "image",
fileName: "pic.jpg",
expect(sendMessage).toHaveBeenNthCalledWith(1, chatId, "_oops_", {
parse_mode: "Markdown",
});
apiMock.sendPhoto.mockResolvedValueOnce({
message_id: 99,
chat: { id: 123 },
});
const res = await sendMessageTelegram("123", "hello", {
mediaUrl: "http://example.com/pic.jpg",
api: apiMock as never,
});
expect(res).toEqual({ messageId: "99", chatId: "123" });
expect(loadWebMediaMock).toHaveBeenCalled();
expect(apiMock.sendPhoto).toHaveBeenCalled();
expect(sendMessage).toHaveBeenNthCalledWith(2, chatId, "_oops_");
expect(res.chatId).toBe(chatId);
expect(res.messageId).toBe("42");
});
});

View File

@@ -17,6 +17,9 @@ type TelegramSendResult = {
chatId: string;
};
const PARSE_ERR_RE =
/can't parse entities|parse entities|find end of the entity/i;
function resolveToken(explicit?: string): string {
const token = explicit ?? process.env.TELEGRAM_BOT_TOKEN;
if (!token) {
@@ -116,7 +119,22 @@ export async function sendMessageTelegram(
const res = await sendWithRetry(
() => api.sendMessage(chatId, text, { parse_mode: "Markdown" }),
"message",
);
).catch(async (err) => {
// Telegram rejects malformed Markdown (e.g., unbalanced '_' or '*').
// When that happens, fall back to plain text so the message still delivers.
if (PARSE_ERR_RE.test(String(err ?? ""))) {
if (opts.verbose) {
console.warn(
`telegram markdown parse failed, retrying as plain text: ${String(err)}`,
);
}
return await sendWithRetry(
() => api.sendMessage(chatId, text),
"message-plain",
);
}
throw err;
});
const messageId = String(res?.message_id ?? "unknown");
return { messageId, chatId: String(res?.chat?.id ?? chatId) };
}