mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 04:17:42 +00:00
feat(telegram): inline keyboard buttons (#491)
Co-authored-by: Azade <azade@hey.com>
This commit is contained in:
@@ -85,9 +85,10 @@ describe("telegram inbound media", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = onSpy.mock.calls[0]?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, "fetch" as never)
|
||||
@@ -153,9 +154,10 @@ describe("telegram inbound media", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = onSpy.mock.calls[0]?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -199,9 +201,10 @@ describe("telegram inbound media", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = onSpy.mock.calls[0]?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -263,9 +266,10 @@ describe("telegram media groups", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -323,9 +327,10 @@ describe("telegram media groups", () => {
|
||||
} as Response);
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -374,9 +379,10 @@ describe("telegram location parsing", () => {
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0]?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -415,9 +421,10 @@ describe("telegram location parsing", () => {
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0]?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
|
||||
@@ -46,6 +46,7 @@ const onSpy = vi.fn();
|
||||
const stopSpy = vi.fn();
|
||||
const commandSpy = vi.fn();
|
||||
const botCtorSpy = vi.fn();
|
||||
const answerCallbackQuerySpy = vi.fn(async () => undefined);
|
||||
const sendChatActionSpy = vi.fn();
|
||||
const setMessageReactionSpy = vi.fn(async () => undefined);
|
||||
const setMyCommandsSpy = vi.fn(async () => undefined);
|
||||
@@ -54,6 +55,7 @@ const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 }));
|
||||
const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 }));
|
||||
type ApiStub = {
|
||||
config: { use: (arg: unknown) => void };
|
||||
answerCallbackQuery: typeof answerCallbackQuerySpy;
|
||||
sendChatAction: typeof sendChatActionSpy;
|
||||
setMessageReaction: typeof setMessageReactionSpy;
|
||||
setMyCommands: typeof setMyCommandsSpy;
|
||||
@@ -63,6 +65,7 @@ type ApiStub = {
|
||||
};
|
||||
const apiStub: ApiStub = {
|
||||
config: { use: useSpy },
|
||||
answerCallbackQuery: answerCallbackQuerySpy,
|
||||
sendChatAction: sendChatActionSpy,
|
||||
setMessageReaction: setMessageReactionSpy,
|
||||
setMyCommands: setMyCommandsSpy,
|
||||
@@ -113,6 +116,12 @@ vi.mock("../auto-reply/reply.js", () => {
|
||||
return { getReplyFromConfig: replySpy, __replySpy: replySpy };
|
||||
});
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
describe("createTelegramBot", () => {
|
||||
beforeEach(() => {
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -122,6 +131,7 @@ describe("createTelegramBot", () => {
|
||||
sendAnimationSpy.mockReset();
|
||||
sendPhotoSpy.mockReset();
|
||||
setMessageReactionSpy.mockReset();
|
||||
answerCallbackQuerySpy.mockReset();
|
||||
setMyCommandsSpy.mockReset();
|
||||
middlewareUseSpy.mockReset();
|
||||
sequentializeSpy.mockReset();
|
||||
@@ -206,6 +216,40 @@ describe("createTelegramBot", () => {
|
||||
).toBe("telegram:555");
|
||||
});
|
||||
|
||||
it("routes callback_query payloads as messages and answers callbacks", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const callbackHandler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "callback_query",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(callbackHandler).toBeDefined();
|
||||
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-1",
|
||||
data: "cmd:option_a",
|
||||
from: { id: 9, first_name: "Ada", username: "ada_bot" },
|
||||
message: {
|
||||
chat: { id: 1234, type: "private" },
|
||||
date: 1736380800,
|
||||
message_id: 10,
|
||||
},
|
||||
},
|
||||
me: { username: "clawdbot_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.Body).toContain("cmd:option_a");
|
||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-1");
|
||||
});
|
||||
|
||||
it("wraps inbound message with Telegram envelope", async () => {
|
||||
const originalTz = process.env.TZ;
|
||||
process.env.TZ = "Europe/Vienna";
|
||||
@@ -219,7 +263,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
expect(onSpy).toHaveBeenCalledWith("message", expect.any(Function));
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -266,7 +310,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -308,7 +352,7 @@ describe("createTelegramBot", () => {
|
||||
.mockResolvedValueOnce({ code: "PAIRME12", created: false });
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -339,7 +383,7 @@ describe("createTelegramBot", () => {
|
||||
sendChatActionSpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
await handler({
|
||||
@@ -365,7 +409,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -401,7 +445,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -447,7 +491,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -491,7 +535,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -523,7 +567,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -553,7 +597,7 @@ describe("createTelegramBot", () => {
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -591,7 +635,7 @@ describe("createTelegramBot", () => {
|
||||
replySpy.mockResolvedValue({ text: "a".repeat(4500) });
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
await handler({
|
||||
@@ -624,7 +668,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok", replyToMode: "first" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
await handler({
|
||||
@@ -663,7 +707,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
await handler({
|
||||
@@ -694,7 +738,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok", replyToMode: "all" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
await handler({
|
||||
@@ -729,7 +773,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -757,7 +801,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -806,7 +850,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -839,7 +883,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -877,7 +921,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -913,7 +957,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -941,7 +985,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -976,7 +1020,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1015,7 +1059,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1048,7 +1092,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1081,7 +1125,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1114,7 +1158,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1147,7 +1191,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1180,7 +1224,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1212,7 +1256,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1245,7 +1289,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1277,7 +1321,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1308,7 +1352,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1339,7 +1383,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1372,7 +1416,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1404,7 +1448,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1437,7 +1481,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1471,7 +1515,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1504,7 +1548,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1537,7 +1581,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1568,7 +1612,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1629,7 +1673,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1673,7 +1717,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
|
||||
@@ -307,6 +307,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
primaryCtx: TelegramContext,
|
||||
allMedia: Array<{ path: string; contentType?: string }>,
|
||||
storeAllowFrom: string[],
|
||||
options?: { forceWasMentioned?: boolean },
|
||||
) => {
|
||||
const msg = primaryCtx.message;
|
||||
recordProviderActivity({
|
||||
@@ -468,9 +469,11 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
senderId,
|
||||
senderUsername,
|
||||
});
|
||||
const wasMentioned =
|
||||
const computedWasMentioned =
|
||||
(Boolean(botUsername) && hasBotMention(msg, botUsername)) ||
|
||||
matchesMentionPatterns(msg.text ?? msg.caption ?? "", mentionRegexes);
|
||||
const wasMentioned =
|
||||
options?.forceWasMentioned === true ? true : computedWasMentioned;
|
||||
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some(
|
||||
(ent) => ent.type === "mention",
|
||||
);
|
||||
@@ -991,6 +994,40 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
bot.on("callback_query", async (ctx) => {
|
||||
const callback = ctx.callbackQuery;
|
||||
if (!callback) return;
|
||||
try {
|
||||
const data = (callback.data ?? "").trim();
|
||||
const callbackMessage = callback.message;
|
||||
if (!data || !callbackMessage) return;
|
||||
|
||||
const syntheticMessage: TelegramMessage = {
|
||||
...callbackMessage,
|
||||
from: callback.from,
|
||||
text: data,
|
||||
caption: undefined,
|
||||
caption_entities: undefined,
|
||||
entities: undefined,
|
||||
};
|
||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||
const getFile =
|
||||
typeof ctx.getFile === "function"
|
||||
? ctx.getFile.bind(ctx)
|
||||
: async () => ({});
|
||||
await processMessage(
|
||||
{ message: syntheticMessage, me: ctx.me, getFile },
|
||||
[],
|
||||
storeAllowFrom,
|
||||
{ forceWasMentioned: true },
|
||||
);
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`callback handler failed: ${String(err)}`));
|
||||
} finally {
|
||||
await bot.api.answerCallbackQuery(callback.id).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
bot.on("message", async (ctx) => {
|
||||
try {
|
||||
const msg = ctx.message;
|
||||
|
||||
@@ -29,7 +29,51 @@ vi.mock("grammy", () => ({
|
||||
InputFile: class {},
|
||||
}));
|
||||
|
||||
import { reactMessageTelegram, sendMessageTelegram } from "./send.js";
|
||||
import {
|
||||
buildInlineKeyboard,
|
||||
reactMessageTelegram,
|
||||
sendMessageTelegram,
|
||||
} from "./send.js";
|
||||
|
||||
describe("buildInlineKeyboard", () => {
|
||||
it("returns undefined for empty input", () => {
|
||||
expect(buildInlineKeyboard()).toBeUndefined();
|
||||
expect(buildInlineKeyboard([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("builds inline keyboards for valid input", () => {
|
||||
const result = buildInlineKeyboard([
|
||||
[{ text: "Option A", callback_data: "cmd:a" }],
|
||||
[
|
||||
{ text: "Option B", callback_data: "cmd:b" },
|
||||
{ text: "Option C", callback_data: "cmd:c" },
|
||||
],
|
||||
]);
|
||||
expect(result).toEqual({
|
||||
inline_keyboard: [
|
||||
[{ text: "Option A", callback_data: "cmd:a" }],
|
||||
[
|
||||
{ text: "Option B", callback_data: "cmd:b" },
|
||||
{ text: "Option C", callback_data: "cmd:c" },
|
||||
],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("filters invalid buttons and empty rows", () => {
|
||||
const result = buildInlineKeyboard([
|
||||
[
|
||||
{ text: "", callback_data: "cmd:skip" },
|
||||
{ text: "Ok", callback_data: "cmd:ok" },
|
||||
],
|
||||
[{ text: "Missing data", callback_data: "" }],
|
||||
[],
|
||||
]);
|
||||
expect(result).toEqual({
|
||||
inline_keyboard: [[{ text: "Ok", callback_data: "cmd:ok" }]],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessageTelegram", () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { ReactionType, ReactionTypeEmoji } from "@grammyjs/types";
|
||||
import type {
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
ReactionType,
|
||||
ReactionTypeEmoji,
|
||||
} from "@grammyjs/types";
|
||||
import { type ApiClientOptions, Bot, InputFile } from "grammy";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
@@ -30,6 +35,8 @@ type TelegramSendOpts = {
|
||||
replyToMessageId?: number;
|
||||
/** Forum topic thread ID (for forum supergroups) */
|
||||
messageThreadId?: number;
|
||||
/** Inline keyboard buttons (reply markup). */
|
||||
buttons?: Array<Array<{ text: string; callback_data: string }>>;
|
||||
};
|
||||
|
||||
type TelegramSendResult = {
|
||||
@@ -103,6 +110,26 @@ function normalizeMessageId(raw: string | number): number {
|
||||
throw new Error("Message id is required for Telegram reactions");
|
||||
}
|
||||
|
||||
export function buildInlineKeyboard(
|
||||
buttons?: TelegramSendOpts["buttons"],
|
||||
): InlineKeyboardMarkup | undefined {
|
||||
if (!buttons?.length) return undefined;
|
||||
const rows = buttons
|
||||
.map((row) =>
|
||||
row
|
||||
.filter((button) => button?.text && button?.callback_data)
|
||||
.map(
|
||||
(button): InlineKeyboardButton => ({
|
||||
text: button.text,
|
||||
callback_data: button.callback_data,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.filter((row) => row.length > 0);
|
||||
if (rows.length === 0) return undefined;
|
||||
return { inline_keyboard: rows };
|
||||
}
|
||||
|
||||
export async function sendMessageTelegram(
|
||||
to: string,
|
||||
text: string,
|
||||
@@ -124,6 +151,7 @@ export async function sendMessageTelegram(
|
||||
: undefined;
|
||||
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||
const mediaUrl = opts.mediaUrl?.trim();
|
||||
const replyMarkup = buildInlineKeyboard(opts.buttons);
|
||||
|
||||
// Build optional params for forum topics and reply threading.
|
||||
// Only include these if actually provided to keep API calls clean.
|
||||
@@ -171,8 +199,15 @@ export async function sendMessageTelegram(
|
||||
const file = new InputFile(media.buffer, fileName);
|
||||
const caption = text?.trim() || undefined;
|
||||
const mediaParams = hasThreadParams
|
||||
? { caption, ...threadParams }
|
||||
: { caption };
|
||||
? {
|
||||
caption,
|
||||
...threadParams,
|
||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
}
|
||||
: {
|
||||
caption,
|
||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
};
|
||||
let result:
|
||||
| Awaited<ReturnType<typeof api.sendPhoto>>
|
||||
| Awaited<ReturnType<typeof api.sendVideo>>
|
||||
@@ -240,8 +275,15 @@ export async function sendMessageTelegram(
|
||||
}
|
||||
const htmlText = markdownToTelegramHtml(text);
|
||||
const textParams = hasThreadParams
|
||||
? { parse_mode: "HTML" as const, ...threadParams }
|
||||
: { parse_mode: "HTML" as const };
|
||||
? {
|
||||
parse_mode: "HTML" as const,
|
||||
...threadParams,
|
||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
}
|
||||
: {
|
||||
parse_mode: "HTML" as const,
|
||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
};
|
||||
const res = await request(
|
||||
() => api.sendMessage(chatId, htmlText, textParams),
|
||||
"message",
|
||||
@@ -255,10 +297,17 @@ export async function sendMessageTelegram(
|
||||
`telegram HTML parse failed, retrying as plain text: ${errText}`,
|
||||
);
|
||||
}
|
||||
const plainParams =
|
||||
hasThreadParams || replyMarkup
|
||||
? {
|
||||
...threadParams,
|
||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
}
|
||||
: undefined;
|
||||
return await request(
|
||||
() =>
|
||||
hasThreadParams
|
||||
? api.sendMessage(chatId, text, threadParams)
|
||||
plainParams
|
||||
? api.sendMessage(chatId, text, plainParams)
|
||||
: api.sendMessage(chatId, text),
|
||||
"message-plain",
|
||||
).catch((err2) => {
|
||||
|
||||
Reference in New Issue
Block a user