fix(telegram): skip failed photo downloads in media group instead of dropping entire group

This commit is contained in:
Marcus Castro
2026-02-19 00:51:29 -03:00
committed by Ayaan Zaidi
parent 9363c320d8
commit 81fe06c058
2 changed files with 121 additions and 1 deletions

View File

@@ -270,7 +270,15 @@ export const registerTelegramHandlers = ({
const allMedia: TelegramMediaRef[] = [];
for (const { ctx } of entry.messages) {
const media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
let media;
try {
media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
} catch (mediaErr) {
runtime.log?.(
warn(`media group: skipping photo that failed to fetch: ${String(mediaErr)}`),
);
continue;
}
if (media) {
allMedia.push({
path: media.path,

View File

@@ -1874,4 +1874,116 @@ describe("createTelegramBot", () => {
expect(replySpy).not.toHaveBeenCalled();
fetchSpy.mockRestore();
});
it("processes remaining media group photos when one photo download fails", async () => {
onSpy.mockReset();
replySpy.mockReset();
loadConfig.mockReturnValue({
channels: {
telegram: {
groupPolicy: "open",
groups: {
"-100777111222": {
enabled: true,
requireMention: false,
},
},
},
},
});
let fetchCallIndex = 0;
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
fetchCallIndex++;
if (fetchCallIndex === 2) {
throw new Error("MediaFetchError: Failed to fetch media");
}
return 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", testTimings: TELEGRAM_TEST_TIMINGS });
const handler = getOnHandler("channel_post") as (
ctx: Record<string, unknown>,
) => Promise<void>;
const first = handler({
channelPost: {
chat: { id: -100777111222, type: "channel", title: "Wake Channel" },
message_id: 401,
caption: "partial album",
date: 1736380800,
media_group_id: "partial-album-1",
photo: [{ file_id: "p1" }],
},
me: { username: "openclaw_bot" },
getFile: async () => ({ file_path: "photos/p1.jpg" }),
});
const second = handler({
channelPost: {
chat: { id: -100777111222, type: "channel", title: "Wake Channel" },
message_id: 402,
date: 1736380801,
media_group_id: "partial-album-1",
photo: [{ file_id: "p2" }],
},
me: { username: "openclaw_bot" },
getFile: async () => ({ file_path: "photos/p2.jpg" }),
});
await Promise.all([first, second]);
expect(replySpy).not.toHaveBeenCalled();
const flushTimerCall = [...setTimeoutSpy.mock.calls]
.toReversed()
.find((call) => call[1] === TELEGRAM_TEST_TIMINGS.mediaGroupFlushMs);
const flushTimer = flushTimerCall?.[0] as (() => unknown) | undefined;
expect(flushTimer).toBeTypeOf("function");
await flushTimer?.();
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0]?.[0] as { Body?: string; MediaPaths?: string[] };
expect(payload.Body).toContain("partial album");
expect(payload.MediaPaths).toHaveLength(1);
} finally {
setTimeoutSpy.mockRestore();
fetchSpy.mockRestore();
}
});
it("dedupes duplicate message updates by update_id", async () => {
onSpy.mockReset();
replySpy.mockReset();
loadConfig.mockReturnValue({
channels: {
telegram: { dmPolicy: "open", allowFrom: ["*"] },
},
});
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
const ctx = {
update: { update_id: 111 },
message: {
chat: { id: 123, type: "private" },
from: { id: 456, username: "testuser" },
text: "hello",
date: 1736380800,
message_id: 42,
},
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
};
await handler(ctx);
await handler(ctx);
expect(replySpy).toHaveBeenCalledTimes(1);
});
});