mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 13:01:41 +00:00
fix(telegram): include replied media files in reply context (#28488)
* fix(telegram): include replied media files in reply context * fix(telegram): keep reply media fields nullable * perf(telegram): defer reply-media fetch to debounce flush * fix(telegram): gate and preserve reply media attachments * fix(telegram): preserve cached-sticker reply media context * fix: update changelog for telegram reply-media context fixes (#28488) (thanks @obviyus)
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
commandSpy,
|
||||
editMessageTextSpy,
|
||||
enqueueSystemEventSpy,
|
||||
getFileSpy,
|
||||
getLoadConfigMock,
|
||||
getReadChannelAllowFromStoreMock,
|
||||
getOnHandler,
|
||||
@@ -404,6 +405,189 @@ describe("createTelegramBot", () => {
|
||||
expect(payload.ReplyToSender).toBe("Ada");
|
||||
});
|
||||
|
||||
it("includes replied image media in inbound context for text replies", async () => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
getFileSpy.mockClear();
|
||||
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async () =>
|
||||
new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/png" },
|
||||
}),
|
||||
);
|
||||
try {
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 7, type: "private" },
|
||||
text: "what is in this image?",
|
||||
date: 1736380800,
|
||||
reply_to_message: {
|
||||
message_id: 9001,
|
||||
photo: [{ file_id: "reply-photo-1" }],
|
||||
from: { first_name: "Ada" },
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0] as {
|
||||
MediaPath?: string;
|
||||
MediaPaths?: string[];
|
||||
ReplyToBody?: string;
|
||||
};
|
||||
expect(payload.ReplyToBody).toBe("<media:image>");
|
||||
expect(payload.MediaPaths).toHaveLength(1);
|
||||
expect(payload.MediaPath).toBe(payload.MediaPaths?.[0]);
|
||||
expect(getFileSpy).toHaveBeenCalledWith("reply-photo-1");
|
||||
} finally {
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not fetch reply media for unauthorized DM replies", async () => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
getFileSpy.mockClear();
|
||||
sendMessageSpy.mockClear();
|
||||
readChannelAllowFromStore.mockResolvedValue([]);
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 7, type: "private" },
|
||||
text: "hey",
|
||||
date: 1736380800,
|
||||
from: { id: 999, first_name: "Eve" },
|
||||
reply_to_message: {
|
||||
message_id: 9001,
|
||||
photo: [{ file_id: "reply-photo-1" }],
|
||||
from: { first_name: "Ada" },
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
|
||||
expect(getFileSpy).not.toHaveBeenCalled();
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("defers reply media download until debounce flush", async () => {
|
||||
const DEBOUNCE_MS = 4321;
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
getFileSpy.mockClear();
|
||||
loadConfig.mockReturnValue({
|
||||
agents: {
|
||||
defaults: {
|
||||
envelopeTimezone: "utc",
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
inbound: {
|
||||
debounceMs: DEBOUNCE_MS,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async () =>
|
||||
new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/png" },
|
||||
}),
|
||||
);
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
try {
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 7, type: "private" },
|
||||
text: "first",
|
||||
date: 1736380800,
|
||||
message_id: 101,
|
||||
from: { id: 42, first_name: "Ada" },
|
||||
reply_to_message: {
|
||||
message_id: 9001,
|
||||
photo: [{ file_id: "reply-photo-1" }],
|
||||
from: { first_name: "Ada" },
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 7, type: "private" },
|
||||
text: "second",
|
||||
date: 1736380801,
|
||||
message_id: 102,
|
||||
from: { id: 42, first_name: "Ada" },
|
||||
reply_to_message: {
|
||||
message_id: 9001,
|
||||
photo: [{ file_id: "reply-photo-1" }],
|
||||
from: { first_name: "Ada" },
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
expect(getFileSpy).not.toHaveBeenCalled();
|
||||
|
||||
const flushTimerCallIndex = setTimeoutSpy.mock.calls.findLastIndex(
|
||||
(call) => call[1] === DEBOUNCE_MS,
|
||||
);
|
||||
const flushTimer =
|
||||
flushTimerCallIndex >= 0
|
||||
? (setTimeoutSpy.mock.calls[flushTimerCallIndex]?.[0] as (() => unknown) | undefined)
|
||||
: undefined;
|
||||
if (flushTimerCallIndex >= 0) {
|
||||
clearTimeout(
|
||||
setTimeoutSpy.mock.results[flushTimerCallIndex]?.value as ReturnType<typeof setTimeout>,
|
||||
);
|
||||
}
|
||||
expect(flushTimer).toBeTypeOf("function");
|
||||
await flushTimer?.();
|
||||
await vi.waitFor(() => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(getFileSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getFileSpy).toHaveBeenCalledWith("reply-photo-1");
|
||||
} finally {
|
||||
setTimeoutSpy.mockRestore();
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("handles quote-only replies without reply metadata", async () => {
|
||||
onSpy.mockClear();
|
||||
sendMessageSpy.mockClear();
|
||||
|
||||
Reference in New Issue
Block a user