// admin-recover-telegram-owner // Operação ADMIN one-shot para corrigir agentes provisionados ANTES do // fluxo de captura automática. Faz: // 1. Suspende o container Hermes (libera getUpdates do bot) // 2. Long-polling getUpdates por até 90s buscando chat privado humano // 3. Persiste telegram_user_chat_id em agent_instances // 4. Atualiza Railway TELEGRAM_ALLOWED_USERS / TELEGRAM_HOME_CHANNEL // 5. Retoma o container (resume) → o redeploy aplica as novas env vars // // O admin chama esta função e simultaneamente pede ao cliente para enviar // /start no Telegram ao bot dele. A primeira mensagem privada captura o id. import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { corsHeaders } from "../_shared/cors.ts"; import { telegramApi } from "../_shared/telegram.ts"; import { deployRailwayService, getServiceContext, setHermesSuspended, upsertRailwayVariableCollection, } from "../_shared/railway.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY")!; const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN"); interface TelegramUpdate { update_id: number; message?: { chat?: { id: number; type?: string }; from?: { id: number; is_bot?: boolean; username?: string; first_name?: string }; }; } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); if (!RAILWAY_API_TOKEN) { return jsonResponse({ error: "RAILWAY_API_TOKEN não configurado" }, 500); } // Auth admin const authHeader = req.headers.get("Authorization") ?? ""; const jwt = authHeader.replace(/^Bearer\s+/i, ""); if (!jwt) return jsonResponse({ error: "missing authorization" }, 401); const userClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { global: { headers: { Authorization: `Bearer ${jwt}` } }, }); const { data: userData, error: userErr } = await userClient.auth.getUser(); if (userErr || !userData?.user) { return jsonResponse({ error: "invalid token" }, 401); } const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); const { data: isAdmin } = await admin.rpc("has_role", { _user_id: userData.user.id, _role: "admin", }); if (!isAdmin) return jsonResponse({ error: "admin role required" }, 403); const body = await req.json().catch(() => ({})); const agentId = (body?.agent_instance_id ?? "").toString(); if (!agentId) return jsonResponse({ error: "agent_instance_id required" }, 400); // Carrega agent const { data: agent } = await admin .from("agent_instances") .select( "id, status, telegram_bot_token_vault_id, telegram_bot_username, telegram_user_chat_id, railway_service_id, vps_pool_id", ) .eq("id", agentId) .maybeSingle(); if (!agent) return jsonResponse({ error: "agent não encontrado" }, 404); if (!agent.railway_service_id) { return jsonResponse({ error: "agente sem railway_service_id" }, 409); } if (!agent.telegram_bot_token_vault_id) { return jsonResponse({ error: "agente sem telegram bot token" }, 409); } // Resolve project/environment let projectId: string | null = null; let environmentId: string | null = null; if (agent.vps_pool_id) { const { data: pool } = await admin .from("vps_pool") .select("railway_project_id, railway_environment_id") .eq("id", agent.vps_pool_id) .maybeSingle(); projectId = pool?.railway_project_id ?? null; environmentId = pool?.railway_environment_id ?? null; } if (!projectId || !environmentId) { const ctx = await getServiceContext({ token: RAILWAY_API_TOKEN, serviceId: agent.railway_service_id, }); projectId = projectId ?? ctx.projectId; environmentId = environmentId ?? ctx.environmentId; } if (!projectId || !environmentId) { return jsonResponse({ error: "não foi possível resolver railway project/env" }, 500); } // Decifra token const { data: secret } = await admin.rpc("vault_decrypt_secret", { secret_id: agent.telegram_bot_token_vault_id, }); // deno-lint-ignore no-explicit-any const token: string = (secret?.[0] as any)?.decrypted_secret ?? ""; if (!token) return jsonResponse({ error: "falha ao decifrar token" }, 500); // 1) Suspende Hermes (libera getUpdates) — best effort try { await setHermesSuspended({ token: RAILWAY_API_TOKEN, serviceId: agent.railway_service_id, environmentId, projectId, suspend: true, }); console.log(`[recover] Hermes suspenso, aguardando 25s para descer`); } catch (e) { console.warn("[recover] suspendHermes falhou:", e); } // Espera o redeploy de suspend efetivar (Hermes para de consumir updates) await new Promise((r) => setTimeout(r, 25_000)); // Deleta webhook e dropa pending para garantir polling fresco await telegramApi(token, "deleteWebhook", { drop_pending_updates: false }); // 2) Long-poll getUpdates — total até ~75s let ownerChatId: number | null = null; let ownerFirstName: string | null = null; let lastOffset = 0; const deadline = Date.now() + 75_000; while (Date.now() < deadline && !ownerChatId) { const remainingSec = Math.max(2, Math.floor((deadline - Date.now()) / 1000)); const timeout = Math.min(20, remainingSec); const upd = await telegramApi(token, "getUpdates", { timeout, offset: lastOffset, allowed_updates: ["message"], }); if (!upd.ok) { console.warn("[recover] getUpdates falhou:", upd); await new Promise((r) => setTimeout(r, 1500)); continue; } const updates = upd.result ?? []; for (const u of updates) { if (u.update_id >= lastOffset) lastOffset = u.update_id + 1; const m = u.message; if (!m?.chat || !m.from || m.from.is_bot) continue; if (m.chat.type && m.chat.type !== "private") continue; ownerChatId = m.chat.id; ownerFirstName = m.from.first_name ?? null; break; } } if (!ownerChatId) { // Retoma Hermes mesmo sem capturar try { await setHermesSuspended({ token: RAILWAY_API_TOKEN, serviceId: agent.railway_service_id, environmentId, projectId, suspend: false, }); } catch { /* ignore */ } return jsonResponse({ found: false, hint: "Cliente não enviou mensagem na janela de 75s. Tente novamente.", }, 408); } // 3) Persiste chat_id no DB await admin .from("agent_instances") .update({ telegram_user_chat_id: ownerChatId, telegram_first_message_received_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) .eq("id", agent.id); // 4) Mensagem de confirmação ao cliente await telegramApi(token, "sendMessage", { chat_id: ownerChatId, text: `✅ Tudo certo${ownerFirstName ? `, ${ownerFirstName}` : ""}! Estou finalizando minha ativação. Em alguns instantes começo a conversar com você de verdade. ✨`, }); // 5) Atualiza Railway env vars TELEGRAM_ALLOWED_USERS / HOME_CHANNEL + retira suspend const chatIdStr = String(ownerChatId); await upsertRailwayVariableCollection({ token: RAILWAY_API_TOKEN, serviceId: agent.railway_service_id, environmentId, projectId, variables: { TELEGRAM_ALLOWED_USERS: chatIdStr, TELEGRAM_HOME_CHANNEL: chatIdStr, HERMES_SUSPENDED: "", }, skipDeploys: true, }); await deployRailwayService({ token: RAILWAY_API_TOKEN, serviceId: agent.railway_service_id, environmentId, }); // Volta status para active se estava suspended await admin .from("agent_instances") .update({ status: "active", updated_at: new Date().toISOString() }) .eq("id", agent.id) .in("status", ["suspended", "active", "provisioning"]); return jsonResponse({ found: true, chat_id: ownerChatId, first_name: ownerFirstName, redeployed: true, }); });