From a8ed0f79e8f010d89ce6c513002d6661830934be Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:24:43 +0000 Subject: [PATCH] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../functions/create-managed-bot/index.ts | 134 +++++++++++++ .../functions/managed-bot-webhook/index.ts | 180 ++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 supabase/functions/create-managed-bot/index.ts create mode 100644 supabase/functions/managed-bot-webhook/index.ts diff --git a/supabase/functions/create-managed-bot/index.ts b/supabase/functions/create-managed-bot/index.ts new file mode 100644 index 0000000..a281e75 --- /dev/null +++ b/supabase/functions/create-managed-bot/index.ts @@ -0,0 +1,134 @@ +// create-managed-bot +// Recebe { agent_instance_id?, agent_name } do usuário autenticado. +// Gera username sugerido, marca o agent_instance como managed_bot_pending +// e retorna a URL de deep-link para o @mika_managerbot criar o bot em 1 toque. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { corsHeaders } from "../_shared/cors.ts"; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} + +function generateUsername(name: string): string { + const base = (name || "mika") + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") // remove acentos + .replace(/[^a-z0-9]/g, "") // só letras e números + .substring(0, 28); + const safe = base.length >= 3 ? base : `mika${base}`; + return `${safe.substring(0, 28)}bot`; +} + +async function usernameAvailable( + admin: ReturnType, + username: string, +): Promise { + const { data } = await admin + .from("agent_instances") + .select("id") + .or(`telegram_bot_username.eq.${username},managed_bot_suggested_username.eq.${username}`) + .limit(1) + .maybeSingle(); + return !data; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const supabaseUrl = Deno.env.get("SUPABASE_URL")!; + const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!; + const managerUsername = + Deno.env.get("TELEGRAM_MANAGER_BOT_USERNAME") || "mika_managerbot"; + + const authHeader = req.headers.get("Authorization") ?? ""; + const userClient = createClient(supabaseUrl, anonKey, { + global: { headers: { Authorization: authHeader } }, + }); + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData.user) { + return jsonResponse({ error: "Não autenticado" }, 401); + } + const userId = userData.user.id; + + const body = await req.json().catch(() => ({})); + const agentName = (body?.agent_name ?? "").toString().trim(); + const explicitAgentId = (body?.agent_instance_id ?? "").toString().trim(); + + if (!agentName || agentName.length < 2) { + return jsonResponse({ error: "Nome do agente inválido." }, 400); + } + + const admin = createClient(supabaseUrl, serviceKey); + + // Localiza agent_instance do usuário + const query = admin + .from("agent_instances") + .select("id, user_id, agent_name, managed_bot_suggested_username") + .eq("user_id", userId); + const { data: agent, error: agentErr } = explicitAgentId + ? await query.eq("id", explicitAgentId).maybeSingle() + : await query.maybeSingle(); + + if (agentErr) { + console.error("agent lookup error", agentErr); + return jsonResponse({ error: "Falha ao localizar agente." }, 500); + } + if (!agent) { + return jsonResponse( + { error: "Agente não encontrado. Aguarde o provisionamento." }, + 404, + ); + } + + // Gera username único (até 5 tentativas com sufixo numérico) + let suggestedUsername = generateUsername(agentName); + let attempts = 0; + while (attempts < 5 && !(await usernameAvailable(admin, suggestedUsername))) { + attempts++; + const suffix = Math.floor(Math.random() * 9000 + 1000); + const baseNoBot = suggestedUsername.replace(/bot$/, ""); + suggestedUsername = `${baseNoBot.substring(0, 24)}${suffix}bot`; + } + + // Persiste estado pendente + const { error: updErr } = await admin + .from("agent_instances") + .update({ + managed_bot_pending: true, + managed_bot_suggested_username: suggestedUsername, + agent_name: agent.agent_name || agentName, + updated_at: new Date().toISOString(), + }) + .eq("id", agent.id); + + if (updErr) { + console.error("agent update error", updErr); + return jsonResponse({ error: "Falha ao iniciar criação do bot." }, 500); + } + + const url = + `https://t.me/newbot/${managerUsername}/${suggestedUsername}` + + `?name=${encodeURIComponent(agentName)}`; + + return jsonResponse({ + url, + suggested_username: suggestedUsername, + manager_username: managerUsername, + }); + } catch (err) { + console.error("create-managed-bot fatal", err); + return jsonResponse( + { error: err instanceof Error ? err.message : "Erro inesperado" }, + 500, + ); + } +}); diff --git a/supabase/functions/managed-bot-webhook/index.ts b/supabase/functions/managed-bot-webhook/index.ts new file mode 100644 index 0000000..195727f --- /dev/null +++ b/supabase/functions/managed-bot-webhook/index.ts @@ -0,0 +1,180 @@ +// managed-bot-webhook +// Recebe updates do @mika_managerbot. Quando o update tem o campo "managed_bot", +// busca o token do novo bot via getManagedBotToken, salva no Vault, atualiza +// o agent_instance correspondente e dispara provisionamento automático. +// +// Setup do webhook: GET ?setup=true configura o webhook do manager bot. +// +// IMPORTANTE: Esta função usa endpoints experimentais do BotFather Bot +// Management Mode que NÃO são parte da Bot API pública oficial. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; + +function ack(): Response { + // Sempre 200 — Telegram não deve retentar. + return new Response("ok", { status: 200 }); +} + +function json(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +Deno.serve(async (req) => { + const url = new URL(req.url); + const supabaseUrl = Deno.env.get("SUPABASE_URL")!; + const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + const managerToken = Deno.env.get("TELEGRAM_MANAGER_BOT_TOKEN"); + + // Setup: configura webhook + if (url.searchParams.get("setup") === "true") { + if (!managerToken) { + return json({ error: "TELEGRAM_MANAGER_BOT_TOKEN não configurado" }, 400); + } + const webhookUrl = `${supabaseUrl}/functions/v1/managed-bot-webhook`; + const res = await fetch( + `https://api.telegram.org/bot${managerToken}/setWebhook`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: webhookUrl, + allowed_updates: ["message", "managed_bot"], + }), + }, + ); + const data = await res.json().catch(() => ({})); + return json({ webhook_set: webhookUrl, telegram_response: data }); + } + + if (req.method !== "POST") { + return ack(); + } + + if (!managerToken) { + console.error("TELEGRAM_MANAGER_BOT_TOKEN ausente"); + return ack(); + } + + let update: Record; + try { + update = await req.json(); + } catch { + return ack(); + } + + // Só processamos eventos de managed_bot + // deno-lint-ignore no-explicit-any + const managed = (update as any).managed_bot; + if (!managed || !managed.bot) { + return ack(); + } + + try { + const admin = createClient(supabaseUrl, serviceKey); + + const botId = managed.bot.id; + const botUsername: string = managed.bot.username; + // deno-lint-ignore no-explicit-any + const tgUser = (managed as any).user ?? {}; + + // 1) Busca token do bot recém-criado + const tokenRes = await fetch( + `https://api.telegram.org/bot${managerToken}/getManagedBotToken`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bot_id: botId }), + }, + ); + const tokenJson = await tokenRes.json().catch(() => ({})); + // deno-lint-ignore no-explicit-any + const newBotToken: string | undefined = (tokenJson as any)?.result?.token; + if (!newBotToken) { + console.error("getManagedBotToken falhou", tokenJson); + return ack(); + } + + // 2) Localiza agent_instance pendente correspondente + const filter = tgUser?.id + ? `telegram_user_chat_id.eq.${tgUser.id},managed_bot_suggested_username.eq.${botUsername}` + : `managed_bot_suggested_username.eq.${botUsername}`; + + const { data: agentInstance, error: lookupErr } = await admin + .from("agent_instances") + .select("id, user_id") + .eq("managed_bot_pending", true) + .or(filter) + .order("created_at", { ascending: false }) + .limit(1) + .maybeSingle(); + + if (lookupErr || !agentInstance) { + console.error( + "Nenhum agent_instance pendente encontrado para managed bot", + { botUsername, tgUserId: tgUser?.id, lookupErr }, + ); + return ack(); + } + + // 3) Salva token no Vault (reutiliza vault_create_secret) + const secretName = `telegram_bot_token_${agentInstance.id}_${Math.floor( + Date.now() / 1000, + )}`; + const { data: vaultData, error: vaultErr } = await admin + .rpc("vault_create_secret", { + secret_value: newBotToken, + secret_name: secretName, + secret_description: `Managed bot token for agent ${agentInstance.id}`, + }) + .single(); + + if (vaultErr || !vaultData) { + console.error("vault_create_secret falhou", vaultErr); + return ack(); + } + // deno-lint-ignore no-explicit-any + const secretId = (vaultData as any).secret_id ?? vaultData; + + // 4) Atualiza agent_instance + const { error: updErr } = await admin + .from("agent_instances") + .update({ + telegram_bot_token_vault_id: secretId, + telegram_bot_username: botUsername, + telegram_user_chat_id: tgUser?.id ? Number(tgUser.id) : null, + telegram_connected_at: new Date().toISOString(), + telegram_onboarding_completed: true, + telegram_token_invalid: false, + managed_bot_pending: false, + onboarding_completed: true, + updated_at: new Date().toISOString(), + }) + .eq("id", agentInstance.id); + + if (updErr) { + console.error("agent update falhou", updErr); + return ack(); + } + + // 5) Dispara provisionamento (fire-and-forget) + fetch(`${supabaseUrl}/functions/v1/provision-agent`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${serviceKey}`, + }, + body: JSON.stringify({ + agent_instance_id: agentInstance.id, + user_id: agentInstance.user_id, + }), + }).catch((err) => console.error("Auto-provision error:", err)); + + return ack(); + } catch (err) { + console.error("managed-bot-webhook fatal", err); + return ack(); + } +});