fix(telegram): preserve inbound quote context and avoid QUOTE_TEXT_INVALID

This commit is contained in:
Denis Rybnikov
2026-02-08 23:48:14 +01:00
committed by Ayaan Zaidi
parent 727a390d13
commit a4b38ce886
4 changed files with 60 additions and 26 deletions

View File

@@ -967,6 +967,42 @@ describe("createTelegramBot", () => {
expect(payload.ReplyToSender).toBe("unknown sender"); expect(payload.ReplyToSender).toBe("unknown sender");
}); });
it("uses external_reply quote text for partial replies", async () => {
onSpy.mockReset();
sendMessageSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
replySpy.mockReset();
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({
message: {
chat: { id: 7, type: "private" },
text: "Sure, see below",
date: 1736380800,
external_reply: {
message_id: 9002,
text: "Can you summarize this?",
from: { first_name: "Ada" },
quote: {
text: "summarize this",
},
},
},
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expect(payload.Body).toContain("[Quoting Ada id:9002]");
expect(payload.Body).toContain('"summarize this"');
expect(payload.ReplyToId).toBe("9002");
expect(payload.ReplyToBody).toBe("summarize this");
expect(payload.ReplyToSender).toBe("Ada");
});
it("sends replies without native reply threading", async () => { it("sends replies without native reply threading", async () => {
onSpy.mockReset(); onSpy.mockReset();
sendMessageSpy.mockReset(); sendMessageSpy.mockReset();

View File

@@ -194,7 +194,7 @@ describe("deliverReplies", () => {
); );
}); });
it("uses reply_parameters when quote text is provided", async () => { it("uses reply_to_message_id when quote text is provided", async () => {
const runtime = { error: vi.fn(), log: vi.fn() }; const runtime = { error: vi.fn(), log: vi.fn() };
const sendMessage = vi.fn().mockResolvedValue({ const sendMessage = vi.fn().mockResolvedValue({
message_id: 10, message_id: 10,
@@ -217,10 +217,14 @@ describe("deliverReplies", () => {
"123", "123",
expect.any(String), expect.any(String),
expect.objectContaining({ expect.objectContaining({
reply_parameters: { reply_to_message_id: 500,
message_id: 500, }),
quote: "quoted text", );
}, expect(sendMessage).toHaveBeenCalledWith(
"123",
expect.any(String),
expect.not.objectContaining({
reply_parameters: expect.anything(),
}), }),
); );
}); });

View File

@@ -484,16 +484,8 @@ function buildTelegramSendParams(opts?: {
}): Record<string, unknown> { }): Record<string, unknown> {
const threadParams = buildTelegramThreadParams(opts?.thread); const threadParams = buildTelegramThreadParams(opts?.thread);
const params: Record<string, unknown> = {}; const params: Record<string, unknown> = {};
const quoteText = opts?.replyQuoteText?.trim();
if (opts?.replyToMessageId) { if (opts?.replyToMessageId) {
if (quoteText) { params.reply_to_message_id = opts.replyToMessageId;
params.reply_parameters = {
message_id: Math.trunc(opts.replyToMessageId),
quote: quoteText,
};
} else {
params.reply_to_message_id = opts.replyToMessageId;
}
} }
if (threadParams) { if (threadParams) {
params.message_thread_id = threadParams.message_thread_id; params.message_thread_id = threadParams.message_thread_id;

View File

@@ -226,31 +226,33 @@ export type TelegramReplyTarget = {
export function describeReplyTarget(msg: Message): TelegramReplyTarget | null { export function describeReplyTarget(msg: Message): TelegramReplyTarget | null {
const reply = msg.reply_to_message; const reply = msg.reply_to_message;
const quote = msg.quote; const externalReply = msg.external_reply;
const quoteText = msg.quote?.text ?? reply?.quote?.text ?? externalReply?.quote?.text;
let body = ""; let body = "";
let kind: TelegramReplyTarget["kind"] = "reply"; let kind: TelegramReplyTarget["kind"] = "reply";
if (quote?.text) { if (typeof quoteText === "string") {
body = quote.text.trim(); body = quoteText.trim();
if (body) { if (body) {
kind = "quote"; kind = "quote";
} }
} }
if (!body && reply) { const replyLike = reply ?? externalReply;
const replyBody = (reply.text ?? reply.caption ?? "").trim(); if (!body && replyLike) {
const replyBody = (replyLike.text ?? replyLike.caption ?? "").trim();
body = replyBody; body = replyBody;
if (!body) { if (!body) {
if (reply.photo) { if (replyLike.photo) {
body = "<media:image>"; body = "<media:image>";
} else if (reply.video) { } else if (replyLike.video) {
body = "<media:video>"; body = "<media:video>";
} else if (reply.audio || reply.voice) { } else if (replyLike.audio || replyLike.voice) {
body = "<media:audio>"; body = "<media:audio>";
} else if (reply.document) { } else if (replyLike.document) {
body = "<media:document>"; body = "<media:document>";
} else { } else {
const locationData = extractTelegramLocation(reply); const locationData = extractTelegramLocation(replyLike);
if (locationData) { if (locationData) {
body = formatLocationText(locationData); body = formatLocationText(locationData);
} }
@@ -260,11 +262,11 @@ export function describeReplyTarget(msg: Message): TelegramReplyTarget | null {
if (!body) { if (!body) {
return null; return null;
} }
const sender = reply ? buildSenderName(reply) : undefined; const sender = replyLike ? buildSenderName(replyLike) : undefined;
const senderLabel = sender ?? "unknown sender"; const senderLabel = sender ?? "unknown sender";
return { return {
id: reply?.message_id ? String(reply.message_id) : undefined, id: replyLike?.message_id ? String(replyLike.message_id) : undefined,
sender: senderLabel, sender: senderLabel,
body, body,
kind, kind,