feat(telegram): add channel_post support for bot-to-bot communication (#17857)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 27a343cd4d
Co-authored-by: theSamPadilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
Sam Padilla
2026-02-17 03:14:18 -06:00
committed by GitHub
parent 5db95cd8d5
commit 32d12fcae9
4 changed files with 488 additions and 142 deletions

View File

@@ -45,6 +45,14 @@ const mockMessage = (message: Pick<Message, "chat"> & Partial<Message>): Message
date: 0,
...message,
}) as Message;
const TELEGRAM_TEST_TIMINGS = {
mediaGroupFlushMs: 20,
textFragmentGapMs: 30,
} as const;
const sleep = async (ms: number) => {
await new Promise<void>((resolve) => setTimeout(resolve, ms));
};
describe("createTelegramBot", () => {
beforeEach(() => {
@@ -1864,6 +1872,168 @@ describe("createTelegramBot", () => {
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
expect(sendMessageSpy.mock.calls[0]?.[1]).toContain("final reply");
});
it("buffers channel_post media groups and processes them together", async () => {
onSpy.mockReset();
replySpy.mockReset();
loadConfig.mockReturnValue({
channels: {
telegram: {
groupPolicy: "open",
groups: {
"-100777111222": {
enabled: true,
requireMention: false,
},
},
},
},
});
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
async () =>
new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), {
status: 200,
headers: { "content-type": "image/png" },
}),
);
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: 201,
caption: "album caption",
date: 1736380800,
media_group_id: "channel-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: 202,
date: 1736380801,
media_group_id: "channel-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();
await sleep(TELEGRAM_TEST_TIMINGS.mediaGroupFlushMs + 80);
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0]?.[0] as { Body?: string; MediaPaths?: string[] };
expect(payload.Body).toContain("album caption");
expect(payload.MediaPaths).toHaveLength(2);
fetchSpy.mockRestore();
});
it("coalesces channel_post near-limit text fragments into one message", async () => {
onSpy.mockReset();
replySpy.mockReset();
loadConfig.mockReturnValue({
channels: {
telegram: {
groupPolicy: "open",
groups: {
"-100777111222": {
enabled: true,
requireMention: false,
},
},
},
},
});
createTelegramBot({ token: "tok", testTimings: TELEGRAM_TEST_TIMINGS });
const handler = getOnHandler("channel_post") as (ctx: Record<string, unknown>) => Promise<void>;
const part1 = "A".repeat(4050);
const part2 = "B".repeat(50);
await handler({
channelPost: {
chat: { id: -100777111222, type: "channel", title: "Wake Channel" },
message_id: 301,
date: 1736380800,
text: part1,
},
me: { username: "openclaw_bot" },
getFile: async () => ({}),
});
await handler({
channelPost: {
chat: { id: -100777111222, type: "channel", title: "Wake Channel" },
message_id: 302,
date: 1736380801,
text: part2,
},
me: { username: "openclaw_bot" },
getFile: async () => ({}),
});
expect(replySpy).not.toHaveBeenCalled();
await sleep(TELEGRAM_TEST_TIMINGS.textFragmentGapMs + 100);
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0]?.[0] as { RawBody?: string };
expect(payload.RawBody).toContain(part1.slice(0, 32));
expect(payload.RawBody).toContain(part2.slice(0, 32));
});
it("drops oversized channel_post media instead of dispatching a placeholder message", async () => {
onSpy.mockReset();
replySpy.mockReset();
loadConfig.mockReturnValue({
channels: {
telegram: {
groupPolicy: "open",
groups: {
"-100777111222": {
enabled: true,
requireMention: false,
},
},
},
},
});
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
async () =>
new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), {
status: 200,
headers: { "content-type": "image/jpeg" },
}),
);
createTelegramBot({ token: "tok", mediaMaxMb: 0 });
const handler = getOnHandler("channel_post") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({
channelPost: {
chat: { id: -100777111222, type: "channel", title: "Wake Channel" },
message_id: 401,
date: 1736380800,
photo: [{ file_id: "oversized" }],
},
me: { username: "openclaw_bot" },
getFile: async () => ({ file_path: "photos/oversized.jpg" }),
});
expect(replySpy).not.toHaveBeenCalled();
fetchSpy.mockRestore();
});
it("dedupes duplicate message updates by update_id", async () => {
onSpy.mockReset();
replySpy.mockReset();