// Telegram tool catalog. Shared types + helpers live in ./types.ts. import { z } from "zod"; import { tool, type Tool } from "./types.ts"; export { createTgClient, toolToGeminiFunction, type ToolCtx, type Tool, type TgClient } from "./types.ts"; // ---- Shared sub-schemas ---- const chatField = z.union([z.number().int(), z.string().min(1)]) .describe("Telegram chat id (integer) OR a public @username string."); const ParseMode = z.enum(["HTML", "MarkdownV2", "Markdown"]).optional(); const ButtonSchema = z.object({ text: z.string().min(1).max(64), url: z.string().url().optional(), callback_data: z.string().max(64).optional(), copy_text: z.string().max(256).optional(), switch_inline_query: z.string().optional(), switch_inline_query_current_chat: z.string().optional(), }); function buildKeyboard(rows: z.infer[][] | undefined) { if (!rows || rows.length === 0) return undefined; return { inline_keyboard: rows.map((row) => row.map((b) => { const out: Record = { text: b.text }; if (b.url) out.url = b.url; if (b.callback_data) out.callback_data = b.callback_data; if (b.copy_text) out.copy_text = { text: b.copy_text }; if (b.switch_inline_query !== undefined) out.switch_inline_query = b.switch_inline_query; if (b.switch_inline_query_current_chat !== undefined) out.switch_inline_query_current_chat = b.switch_inline_query_current_chat; return out; }), ), }; } // ---- The tool catalog ---- export const telegramTools: Tool[] = [ // ---- discovery ---- tool({ name: "tg_me", title: "Bot identity", description: "Return basic info about the bot — id, username, name, capability flags.", parameters: {}, handler: async (_a, { tg }) => tg.call("getMe"), }), tool({ name: "tg_resolve_chat", title: "Resolve a user to a DM chat id", description: "Look up a user in users.db and return their telegram_id (== their DM chat_id). Provide exactly one of telegram_id, minecraft_name, telegram_username.", parameters: { telegram_id: z.number().int().optional(), minecraft_name: z.string().optional(), telegram_username: z.string().optional(), }, handler: async ({ telegram_id, minecraft_name, telegram_username }, { db }) => { const n = [telegram_id, minecraft_name, telegram_username].filter((v) => v !== undefined).length; if (n !== 1) throw new Error("Provide exactly one of telegram_id, minecraft_name, telegram_username."); const sql = "SELECT telegram_id, name, role, minecraft_name, username, status FROM users WHERE " + (telegram_id !== undefined ? "telegram_id = ?" : minecraft_name !== undefined ? "minecraft_name = ?" : "username = ?"); const arg = telegram_id ?? minecraft_name ?? telegram_username!.replace(/^@/, ""); const row = db.prepare(sql).get(arg) as { telegram_id: number } | undefined; if (!row) throw new Error("Not found in users table."); return { chat_id: row.telegram_id, user: row }; }, }), tool({ name: "tg_known_chats", title: "Known chats", description: "List chat_ids the bot has ever sent or received a message in — find group chat_ids here.", parameters: {}, handler: async (_a, { db }) => ({ chats: db.prepare( "SELECT chat_id, COUNT(*) AS n, MAX(created_at) AS last_at FROM messages GROUP BY chat_id ORDER BY last_at DESC", ).all(), }), }), tool({ name: "tg_get_chat", title: "Get chat info", description: "Telegram getChat — full info about a chat (type, title, description, photo, member count for groups).", parameters: { chat: chatField }, handler: async ({ chat }, { tg }) => tg.call("getChat", { chat_id: chat }), }), tool({ name: "tg_get_chat_admins", title: "List chat admins", description: "Telegram getChatAdministrators — list admin members of a group/supergroup/channel.", parameters: { chat: chatField }, handler: async ({ chat }, { tg }) => tg.call("getChatAdministrators", { chat_id: chat }), }), // ---- messaging ---- tool({ name: "tg_send_message", title: "Send a text message", description: "sendMessage. Supports HTML/Markdown, quote-replies, silent sends, link-preview suppression, and inline keyboards.", parameters: { chat: chatField, text: z.string().min(1).max(4096), parse_mode: ParseMode, reply_to_message_id: z.number().int().optional(), disable_notification: z.boolean().optional(), disable_link_preview: z.boolean().optional(), protect_content: z.boolean().optional(), buttons: z.array(z.array(ButtonSchema)).optional(), message_thread_id: z.number().int().optional(), }, handler: async (a, { tg }) => { const p: Record = { chat_id: a.chat, text: a.text, parse_mode: a.parse_mode, disable_notification: a.disable_notification, protect_content: a.protect_content, message_thread_id: a.message_thread_id, }; if (a.reply_to_message_id !== undefined) p.reply_parameters = { message_id: a.reply_to_message_id }; if (a.disable_link_preview) p.link_preview_options = { is_disabled: true }; const kb = buildKeyboard(a.buttons); if (kb) p.reply_markup = kb; return tg.call("sendMessage", p); }, }), tool({ name: "tg_edit_message", title: "Edit a message", description: "editMessageText — replace text + keyboard of a previously-sent message.", parameters: { chat: chatField, message_id: z.number().int(), text: z.string().min(1).max(4096), parse_mode: ParseMode, disable_link_preview: z.boolean().optional(), buttons: z.array(z.array(ButtonSchema)).optional(), }, handler: async (a, { tg }) => { const p: Record = { chat_id: a.chat, message_id: a.message_id, text: a.text, parse_mode: a.parse_mode }; if (a.disable_link_preview) p.link_preview_options = { is_disabled: true }; const kb = buildKeyboard(a.buttons); if (kb) p.reply_markup = kb; return tg.call("editMessageText", p); }, }), tool({ name: "tg_delete_message", title: "Delete a message", description: "deleteMessage. Bots can always delete their own messages; others require admin rights.", parameters: { chat: chatField, message_id: z.number().int() }, handler: async ({ chat, message_id }, { tg }) => ({ deleted: await tg.call("deleteMessage", { chat_id: chat, message_id }) }), }), tool({ name: "tg_pin_message", title: "Pin a message", description: "pinChatMessage. Needs admin rights in groups.", parameters: { chat: chatField, message_id: z.number().int(), disable_notification: z.boolean().optional(), }, handler: async ({ chat, message_id, disable_notification }, { tg }) => ({ pinned: await tg.call("pinChatMessage", { chat_id: chat, message_id, disable_notification }) }), }), tool({ name: "tg_unpin_message", title: "Unpin a message", description: "unpinChatMessage. Omit message_id to unpin the most recent pin.", parameters: { chat: chatField, message_id: z.number().int().optional() }, handler: async ({ chat, message_id }, { tg }) => ({ unpinned: await tg.call("unpinChatMessage", { chat_id: chat, message_id }) }), }), tool({ name: "tg_forward_message", title: "Forward a message", description: "forwardMessage — relay a message between chats, preserving original author attribution.", parameters: { from_chat: chatField, to_chat: chatField, message_id: z.number().int(), disable_notification: z.boolean().optional(), }, handler: async ({ from_chat, to_chat, message_id, disable_notification }, { tg }) => tg.call("forwardMessage", { from_chat_id: from_chat, chat_id: to_chat, message_id, disable_notification }), }), // ---- interactivity ---- tool({ name: "tg_chat_action", title: "Show a busy indicator", description: "sendChatAction — show 'typing…' or similar for ~5s. Re-call to extend.", parameters: { chat: chatField, action: z.enum([ "typing", "upload_photo", "record_video", "upload_video", "record_voice", "upload_voice", "upload_document", "choose_sticker", "find_location", "record_video_note", "upload_video_note", ]), message_thread_id: z.number().int().optional(), }, handler: async ({ chat, action, message_thread_id }, { tg }) => ({ shown: await tg.call("sendChatAction", { chat_id: chat, action, message_thread_id }) }), }), tool({ name: "tg_react", title: "React to a message", description: "setMessageReaction — attach emoji reactions or clear them by passing emojis=[].", parameters: { chat: chatField, message_id: z.number().int(), emojis: z.array(z.string().min(1).max(8)).max(11) .describe("Emoji to apply. Pass [] to clear all reactions."), is_big: z.boolean().optional(), }, handler: async ({ chat, message_id, emojis, is_big }, { tg }) => { const reaction = emojis.map((e) => ({ type: "emoji" as const, emoji: e })); return { reacted: await tg.call("setMessageReaction", { chat_id: chat, message_id, reaction, is_big }) }; }, }), tool({ name: "tg_send_poll", title: "Send a poll", description: "sendPoll — regular or quiz-style. For quiz, set type='quiz' and correct_option_id (0-based).", parameters: { chat: chatField, question: z.string().min(1).max(300), options: z.array(z.string().min(1).max(100)).min(2).max(12), type: z.enum(["regular", "quiz"]).default("regular"), is_anonymous: z.boolean().default(true), allows_multiple_answers: z.boolean().optional(), correct_option_id: z.number().int().min(0).optional(), explanation: z.string().max(200).optional(), open_period: z.number().int().min(5).max(600).optional(), }, handler: async (a, { tg }) => tg.call("sendPoll", { chat_id: a.chat, question: a.question, options: a.options.map((text) => ({ text })), type: a.type, is_anonymous: a.is_anonymous, allows_multiple_answers: a.allows_multiple_answers, correct_option_id: a.correct_option_id, explanation: a.explanation, open_period: a.open_period, }), }), tool({ name: "tg_send_dice", title: "Send a dice / slot / dart", description: "sendDice — animated random emoji. Telegram decides the outcome; check result.dice.value.", parameters: { chat: chatField, emoji: z.enum(["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"]).default("🎲"), }, handler: async ({ chat, emoji }, { tg }) => tg.call("sendDice", { chat_id: chat, emoji }), }), // ---- rich content ---- tool({ name: "tg_send_photo", title: "Send a photo", description: "sendPhoto — pass an HTTPS URL Telegram can fetch, or a previously-uploaded file_id.", parameters: { chat: chatField, photo: z.string().min(1).describe("HTTPS URL or Telegram file_id."), caption: z.string().max(1024).optional(), parse_mode: ParseMode, has_spoiler: z.boolean().optional(), disable_notification: z.boolean().optional(), reply_to_message_id: z.number().int().optional(), buttons: z.array(z.array(ButtonSchema)).optional(), }, handler: async (a, { tg }) => { const p: Record = { chat_id: a.chat, photo: a.photo, caption: a.caption, parse_mode: a.parse_mode, has_spoiler: a.has_spoiler, disable_notification: a.disable_notification, }; if (a.reply_to_message_id !== undefined) p.reply_parameters = { message_id: a.reply_to_message_id }; const kb = buildKeyboard(a.buttons); if (kb) p.reply_markup = kb; return tg.call("sendPhoto", p); }, }), tool({ name: "tg_send_sticker", title: "Send a sticker", description: "sendSticker — pass a sticker file_id (see tg_stickers_known).", parameters: { chat: chatField, file_id: z.string().min(1), reply_to_message_id: z.number().int().optional(), disable_notification: z.boolean().optional(), }, handler: async ({ chat, file_id, reply_to_message_id, disable_notification }, { tg }) => { const p: Record = { chat_id: chat, sticker: file_id, disable_notification }; if (reply_to_message_id !== undefined) p.reply_parameters = { message_id: reply_to_message_id }; return tg.call("sendSticker", p); }, }), tool({ name: "tg_stickers_known", title: "List bot-bound stickers", description: "Return event→file_id mappings the bot stored via /setsticker.", parameters: {}, handler: async (_a, { db }) => { const rows = db.prepare("SELECT event, file_id FROM stickers ORDER BY event").all(); return { count: rows.length, stickers: rows }; }, }), // ---- forum topics ---- tool({ name: "tg_create_topic", title: "Create a forum topic", description: "createForumTopic — only in forum-mode supergroups. Returns message_thread_id.", parameters: { chat: chatField, name: z.string().min(1).max(128), icon_color: z.number().int().optional(), }, handler: async ({ chat, name, icon_color }, { tg }) => tg.call("createForumTopic", { chat_id: chat, name, icon_color }), }), tool({ name: "tg_close_topic", title: "Close a forum topic", description: "closeForumTopic.", parameters: { chat: chatField, message_thread_id: z.number().int() }, handler: async ({ chat, message_thread_id }, { tg }) => ({ closed: await tg.call("closeForumTopic", { chat_id: chat, message_thread_id }) }), }), tool({ name: "tg_reopen_topic", title: "Reopen a forum topic", description: "reopenForumTopic.", parameters: { chat: chatField, message_thread_id: z.number().int() }, handler: async ({ chat, message_thread_id }, { tg }) => ({ reopened: await tg.call("reopenForumTopic", { chat_id: chat, message_thread_id }) }), }), // ---- bot config ---- tool({ name: "tg_set_chat_title", title: "Set a chat title", description: "setChatTitle — requires bot admin rights.", parameters: { chat: chatField, title: z.string().min(1).max(128) }, handler: async ({ chat, title }, { tg }) => ({ updated: await tg.call("setChatTitle", { chat_id: chat, title }) }), }), tool({ name: "tg_set_my_commands", title: "Set the bot's slash-command menu", description: "setMyCommands. Scope=default for the global menu; pass `chat` for a single chat.", parameters: { commands: z.array(z.object({ command: z.string().min(1).max(32), description: z.string().min(1).max(256), })).min(0).max(100), scope: z.enum([ "default", "all_private_chats", "all_group_chats", "all_chat_administrators", ]).optional(), chat: chatField.optional(), language_code: z.string().optional(), }, handler: async ({ commands, scope, chat, language_code }, { tg }) => { const p: Record = { commands, language_code }; if (chat !== undefined) p.scope = { type: scope === "all_chat_administrators" ? "chat_administrators" : "chat", chat_id: chat }; else if (scope) p.scope = { type: scope }; return { updated: await tg.call("setMyCommands", p) }; }, }), ];