import { Bot, webhookCallback } from "grammy"; import { addSubscriber, removeSubscriber, updateSubscriberTypes, updateSubscriberComponents, getSubscriber, } from "./kv-store.js"; import { fetchComponentByName, escapeHtml } from "./status-fetcher.js"; import { registerInfoCommands } from "./bot-info-commands.js"; /** * Extract chatId and threadId from grammY context */ function getChatTarget(ctx) { return { chatId: ctx.chat.id, threadId: ctx.message?.message_thread_id ?? null, }; } /** * Handle incoming Telegram webhook via grammY */ export async function handleTelegramWebhook(c) { const bot = new Bot(c.env.BOT_TOKEN); const kv = c.env.claude_status; bot.command("start", async (ctx) => { const { chatId, threadId } = getChatTarget(ctx); await addSubscriber(kv, chatId, threadId); await ctx.reply( "Subscribed to Claude status updates (incidents + components).\n" + "Use /subscribe to change preferences.\n" + "Use /stop to unsubscribe.", { parse_mode: "HTML" } ); }); bot.command("stop", async (ctx) => { const { chatId, threadId } = getChatTarget(ctx); await removeSubscriber(kv, chatId, threadId); await ctx.reply("Unsubscribed from Claude status updates. Use /start to resubscribe.", { parse_mode: "HTML", }); }); bot.command("subscribe", async (ctx) => { const { chatId, threadId } = getChatTarget(ctx); const args = ctx.match?.trim().toLowerCase().split(/\s+/) || []; const arg = args[0]; // Handle "/subscribe component " or "/subscribe component all" if (arg === "component" && args.length > 1) { const componentArg = args.slice(1).join(" "); const sub = await getSubscriber(kv, chatId, threadId); if (!sub) { await ctx.reply("Not subscribed yet. Use /start first.", { parse_mode: "HTML" }); return; } if (componentArg === "all") { await updateSubscriberComponents(kv, chatId, threadId, []); await ctx.reply("Component filter cleared — receiving all component updates.", { parse_mode: "HTML", }); return; } // Validate component name against live API const component = await fetchComponentByName(componentArg); if (!component) { await ctx.reply(`Component "${escapeHtml(componentArg)}" not found.`, { parse_mode: "HTML", }); return; } // Add to component filter (deduplicate) const components = sub.components || []; if (!components.some((c) => c.toLowerCase() === component.name.toLowerCase())) { components.push(component.name); } await updateSubscriberComponents(kv, chatId, threadId, components); // Ensure "component" is in types if (!sub.types.includes("component")) { sub.types.push("component"); await updateSubscriberTypes(kv, chatId, threadId, sub.types); } await ctx.reply( `Subscribed to component: ${escapeHtml(component.name)}\n` + `Active filters: ${components.join(", ")}`, { parse_mode: "HTML" } ); return; } const validTypes = { incident: ["incident"], component: ["component"], all: ["incident", "component"], }; if (!arg || !validTypes[arg]) { const sub = await getSubscriber(kv, chatId, threadId); const current = sub?.types?.join(", ") || "none (use /start first)"; const compFilter = sub?.components?.length ? sub.components.join(", ") : "all"; await ctx.reply( "Usage: /subscribe <type> [component]\n\n" + "Types: incident, component, all\n" + "Component filter: /subscribe component api\n" + "Clear filter: /subscribe component all\n" + `\nCurrent types: ${current}\n` + `Components: ${compFilter}`, { parse_mode: "HTML" } ); return; } const updated = await updateSubscriberTypes(kv, chatId, threadId, validTypes[arg]); if (!updated) { await ctx.reply("Not subscribed yet. Use /start first.", { parse_mode: "HTML" }); return; } await ctx.reply(`Subscription updated: ${validTypes[arg].join(", ")}`, { parse_mode: "HTML", }); }); // Info commands: /help, /status, /history, /uptime registerInfoCommands(bot); bot.on("message", async (ctx) => { await ctx.reply( "Claude Status Bot\n\n" + "/help — Detailed command guide\n" + "/start — Subscribe to notifications\n" + "/stop — Unsubscribe\n" + "/status — Current system status\n" + "/subscribe — Notification preferences\n" + "/history — Recent incidents\n" + "/uptime — Component health overview", { parse_mode: "HTML" } ); }); const handler = webhookCallback(bot, "cloudflare-mod"); return handler(c.req.raw); }