feat(telegram): inline keyboard buttons (#491)

Co-authored-by: Azade <azade@hey.com>
This commit is contained in:
Peter Steinberger
2026-01-09 20:46:11 +01:00
parent 46f0a08878
commit 6d378ee608
19 changed files with 894 additions and 98 deletions

View File

@@ -1,6 +1,7 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ClawdbotConfig } from "../../config/config.js";
import { resolveProviderCapabilities } from "../../config/provider-capabilities.js";
import {
reactMessageTelegram,
sendMessageTelegram,
@@ -15,11 +16,74 @@ import {
readStringParam,
} from "./common.js";
type TelegramButton = {
text: string;
callback_data: string;
};
function hasInlineButtonsCapability(params: {
cfg: ClawdbotConfig;
accountId?: string | undefined;
}): boolean {
const caps =
resolveProviderCapabilities({
cfg: params.cfg,
provider: "telegram",
accountId: params.accountId,
}) ?? [];
return caps.some((cap) => cap.toLowerCase() === "inlinebuttons");
}
export function readTelegramButtons(
params: Record<string, unknown>,
): TelegramButton[][] | undefined {
const raw = params.buttons;
if (raw == null) return undefined;
if (!Array.isArray(raw)) {
throw new Error("buttons must be an array of button rows");
}
const rows = raw.map((row, rowIndex) => {
if (!Array.isArray(row)) {
throw new Error(`buttons[${rowIndex}] must be an array`);
}
return row.map((button, buttonIndex) => {
if (!button || typeof button !== "object") {
throw new Error(
`buttons[${rowIndex}][${buttonIndex}] must be an object`,
);
}
const text =
typeof (button as { text?: unknown }).text === "string"
? (button as { text: string }).text.trim()
: "";
const callbackData =
typeof (button as { callback_data?: unknown }).callback_data ===
"string"
? (button as { callback_data: string }).callback_data.trim()
: "";
if (!text || !callbackData) {
throw new Error(
`buttons[${rowIndex}][${buttonIndex}] requires text and callback_data`,
);
}
if (callbackData.length > 64) {
throw new Error(
`buttons[${rowIndex}][${buttonIndex}] callback_data too long (max 64 chars)`,
);
}
return { text, callback_data: callbackData };
});
});
const filtered = rows.filter((row) => row.length > 0);
return filtered.length > 0 ? filtered : undefined;
}
export async function handleTelegramAction(
params: Record<string, unknown>,
cfg: ClawdbotConfig,
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const accountId = readStringParam(params, "accountId");
const isActionEnabled = createActionGate(cfg.telegram?.actions);
if (action === "react") {
@@ -36,7 +100,7 @@ export async function handleTelegramAction(
const { emoji, remove, isEmpty } = readReactionParams(params, {
removeErrorMessage: "Emoji is required to remove a Telegram reaction.",
});
const token = resolveTelegramToken(cfg).token;
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
@@ -45,6 +109,7 @@ export async function handleTelegramAction(
await reactMessageTelegram(chatId ?? "", messageId ?? 0, emoji ?? "", {
token,
remove,
accountId: accountId ?? undefined,
});
if (!remove && !isEmpty) {
return jsonResult({ ok: true, added: emoji });
@@ -59,6 +124,15 @@ export async function handleTelegramAction(
const to = readStringParam(params, "to", { required: true });
const content = readStringParam(params, "content", { required: true });
const mediaUrl = readStringParam(params, "mediaUrl");
const buttons = readTelegramButtons(params);
if (
buttons &&
!hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })
) {
throw new Error(
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to telegram.capabilities (or telegram.accounts.<id>.capabilities).',
);
}
// Optional threading parameters for forum topics and reply chains
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
integer: true,
@@ -66,7 +140,7 @@ export async function handleTelegramAction(
const messageThreadId = readNumberParam(params, "messageThreadId", {
integer: true,
});
const token = resolveTelegramToken(cfg).token;
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
@@ -74,7 +148,9 @@ export async function handleTelegramAction(
}
const result = await sendMessageTelegram(to, content, {
token,
accountId: accountId ?? undefined,
mediaUrl: mediaUrl || undefined,
buttons,
replyToMessageId: replyToMessageId ?? undefined,
messageThreadId: messageThreadId ?? undefined,
});