From a4f88e61d7e201d8bd63b63b68d12a53aea6a492 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:44:34 +0000 Subject: [PATCH 1/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../functions/capture-telegram-owner/index.ts | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 supabase/functions/capture-telegram-owner/index.ts diff --git a/supabase/functions/capture-telegram-owner/index.ts b/supabase/functions/capture-telegram-owner/index.ts new file mode 100644 index 0000000..02d14fa --- /dev/null +++ b/supabase/functions/capture-telegram-owner/index.ts @@ -0,0 +1,183 @@ +// capture-telegram-owner +// Após o cliente conectar o bot (validate-telegram-bot), pedimos a ele que +// envie /start ao próprio bot. Esta função faz polling no getUpdates do +// bot do cliente para descobrir o chat_id do dono. Quando encontra: +// 1) grava telegram_user_chat_id em agent_instances +// 2) envia mensagem de confirmação ao usuário +// 3) limpa o offset (markAsRead) chamando getUpdates com offset alto +// +// É chamada repetidamente pelo frontend (poll a cada 2s) até retornar +// { found: true } ou o usuário desistir. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { telegramApi } from "../_shared/telegram.ts"; + +interface TelegramUpdate { + update_id: number; + message?: { + chat?: { id: number; type?: string }; + from?: { id: number; is_bot?: boolean; username?: string; first_name?: string }; + text?: 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 }); + } + + 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 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 admin = createClient(supabaseUrl, serviceKey); + + // 1) Carrega agent + token do Vault + const { data: agent, error: agentErr } = 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("user_id", userId) + .maybeSingle(); + + if (agentErr || !agent) { + return jsonResponse({ error: "Agente não encontrado." }, 404); + } + + // Já capturado anteriormente — short-circuit + if (agent.telegram_user_chat_id) { + return jsonResponse({ + found: true, + chat_id: Number(agent.telegram_user_chat_id), + bot_username: agent.telegram_bot_username, + already_captured: true, + }); + } + + if (!agent.telegram_bot_token_vault_id) { + return jsonResponse( + { error: "Bot ainda não conectado. Volte e cole o token do BotFather." }, + 409, + ); + } + + 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); + } + + // 2) Garante que o webhook está deletado (senão getUpdates falha) + await telegramApi(token, "deleteWebhook", { drop_pending_updates: false }); + + // 3) Faz getUpdates com timeout curto (long polling 8s) — pega TODAS as mensagens recentes + const updRes = await telegramApi(token, "getUpdates", { + timeout: 8, + allowed_updates: ["message"], + }); + + if (!updRes.ok) { + console.error("getUpdates failed", updRes); + return jsonResponse( + { error: updRes.description || "Falha ao consultar Telegram." }, + 502, + ); + } + + const updates = updRes.result ?? []; + + // 4) Procura a primeira mensagem privada de um humano + let ownerChatId: number | null = null; + let ownerUsername: string | null = null; + let ownerFirstName: string | null = null; + let highestUpdateId = 0; + + for (const u of updates) { + if (u.update_id > highestUpdateId) highestUpdateId = u.update_id; + const msg = u.message; + if (!msg) continue; + const chat = msg.chat; + const from = msg.from; + if (!chat || !from || from.is_bot) continue; + // Só aceita chat privado (chat.id === from.id em DMs) + if (chat.type && chat.type !== "private") continue; + + ownerChatId = chat.id; + ownerUsername = from.username ?? null; + ownerFirstName = from.first_name ?? null; + break; + } + + // 5) Avança o offset para "consumir" os updates lidos (mesmo se não achou, + // limpa lixo antigo). Usar offset = highest+1. + if (highestUpdateId > 0) { + await telegramApi(token, "getUpdates", { + offset: highestUpdateId + 1, + timeout: 0, + limit: 1, + }); + } + + if (!ownerChatId) { + return jsonResponse({ + found: false, + bot_username: agent.telegram_bot_username, + hint: "Envie /start no Telegram ao seu bot para identificá-lo.", + }); + } + + // 6) Persiste chat_id + 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); + + // 7) Mensagem de confirmação imediata (é o último "ping" antes do Hermes assumir) + await telegramApi(token, "sendMessage", { + chat_id: ownerChatId, + text: + `✅ Tudo certo${ownerFirstName ? `, ${ownerFirstName}` : ""}! Seu agente está sendo ativado e em alguns instantes começa a conversar com você por aqui. ✨`, + }); + + return jsonResponse({ + found: true, + chat_id: ownerChatId, + username: ownerUsername, + first_name: ownerFirstName, + bot_username: agent.telegram_bot_username, + }); + } catch (err) { + console.error("capture-telegram-owner fatal", err); + return jsonResponse( + { error: err instanceof Error ? err.message : "Erro inesperado" }, + 500, + ); + } +}); From 86cb3b08f52a704f4cec641ef0dd03c78bb57e83 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:44:57 +0000 Subject: [PATCH 2/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/provision-agent/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/supabase/functions/provision-agent/index.ts b/supabase/functions/provision-agent/index.ts index 47261a1..ddc7e54 100644 --- a/supabase/functions/provision-agent/index.ts +++ b/supabase/functions/provision-agent/index.ts @@ -480,6 +480,15 @@ async function handleUpdateExistingService( HERMES_TTS_PROVIDER: ttsProvider, }; + // Re-aplica TELEGRAM_ALLOWED_USERS / HOME_CHANNEL se já capturamos chat_id do dono + // (importante para corrigir agentes que foram provisionados sem chat_id e tinham + // que pedir pairing manual). + if (agent.telegram_user_chat_id) { + const chatIdStr = String(agent.telegram_user_chat_id); + variables.TELEGRAM_ALLOWED_USERS = chatIdStr; + variables.TELEGRAM_HOME_CHANNEL = chatIdStr; + } + try { await upsertRailwayVariableCollection({ token: RAILWAY_API_TOKEN!, From d5ea48719000fcc96a6fdb8705b979ea0d4f2347 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:45:21 +0000 Subject: [PATCH 3/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/capture-telegram-owner/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/supabase/functions/capture-telegram-owner/index.ts b/supabase/functions/capture-telegram-owner/index.ts index 02d14fa..07d531e 100644 --- a/supabase/functions/capture-telegram-owner/index.ts +++ b/supabase/functions/capture-telegram-owner/index.ts @@ -5,6 +5,8 @@ // 1) grava telegram_user_chat_id em agent_instances // 2) envia mensagem de confirmação ao usuário // 3) limpa o offset (markAsRead) chamando getUpdates com offset alto +// 4) se o agente já está provisionado no Railway, atualiza as env vars +// TELEGRAM_ALLOWED_USERS / TELEGRAM_HOME_CHANNEL e dispara redeploy // // É chamada repetidamente pelo frontend (poll a cada 2s) até retornar // { found: true } ou o usuário desistir. @@ -12,6 +14,13 @@ 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, + upsertRailwayVariableCollection, +} from "../_shared/railway.ts"; + +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN"); interface TelegramUpdate { update_id: number; From 8056aaab35d81db5b0a01a2f8e847a7100c210c5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:45:50 +0000 Subject: [PATCH 4/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../functions/capture-telegram-owner/index.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/supabase/functions/capture-telegram-owner/index.ts b/supabase/functions/capture-telegram-owner/index.ts index 07d531e..54ef769 100644 --- a/supabase/functions/capture-telegram-owner/index.ts +++ b/supabase/functions/capture-telegram-owner/index.ts @@ -175,12 +175,67 @@ Deno.serve(async (req) => { `✅ Tudo certo${ownerFirstName ? `, ${ownerFirstName}` : ""}! Seu agente está sendo ativado e em alguns instantes começa a conversar com você por aqui. ✨`, }); + // 8) Se já existe serviço Railway, atualiza TELEGRAM_ALLOWED_USERS / HOME_CHANNEL e redeploy + let redeployed = false; + if (agent.railway_service_id && RAILWAY_API_TOKEN) { + try { + 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) { + 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, + }, + skipDeploys: true, + }); + await deployRailwayService({ + token: RAILWAY_API_TOKEN, + serviceId: agent.railway_service_id, + environmentId, + }); + redeployed = true; + console.log( + `[capture-telegram-owner] Railway redeploy disparado para serviço ${agent.railway_service_id}`, + ); + } + } catch (e) { + console.error( + "[capture-telegram-owner] falha ao atualizar Railway:", + e instanceof Error ? e.message : String(e), + ); + } + } + return jsonResponse({ found: true, chat_id: ownerChatId, username: ownerUsername, first_name: ownerFirstName, bot_username: agent.telegram_bot_username, + redeployed, }); } catch (err) { console.error("capture-telegram-owner fatal", err); From c0106e1b9e030d6f2626c62b331e6906d20d009c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:46:35 +0000 Subject: [PATCH 5/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../mika/telegram/BotFatherWizard.tsx | 70 +++++++++++++++---- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/src/components/mika/telegram/BotFatherWizard.tsx b/src/components/mika/telegram/BotFatherWizard.tsx index 6071135..7f5dbaf 100644 --- a/src/components/mika/telegram/BotFatherWizard.tsx +++ b/src/components/mika/telegram/BotFatherWizard.tsx @@ -1,14 +1,14 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { ArrowRight, Check, Copy, - ExternalLink, Loader2, AlertCircle, + MessageCircle, } from "lucide-react"; import { toast } from "sonner"; @@ -30,16 +30,24 @@ interface Props { onSkip: () => void; } +type Phase = "configure" | "awaiting_start" | "captured"; + export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Props) { + const [phase, setPhase] = useState("configure"); const [step1Done, setStep1Done] = useState(false); const [step2Done, setStep2Done] = useState(false); const [step3Done, setStep3Done] = useState(false); const [token, setToken] = useState(""); const [submitting, setSubmitting] = useState(false); const [errorMsg, setErrorMsg] = useState(null); + const [validatedBot, setValidatedBot] = useState<{ + bot_username: string; + bot_name: string; + bot_id: number; + } | null>(null); + const pollRef = useRef(null); const suggestedUsername = useMemo(() => { - // base do agent_name; cai pro firstName se não der const base = sanitizeForUsername(agentName).replace(/^mikade/, "mika"); if (base.length >= 5) { const trimmed = base.slice(0, 28); @@ -55,16 +63,6 @@ export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Pr setStep1Done(true); } - async function copyToClipboard(value: string, onDone: () => void) { - try { - await navigator.clipboard.writeText(value); - toast.success("Copiado!"); - onDone(); - } catch { - toast.error("Não foi possível copiar."); - } - } - async function handleActivate() { if (!tokenValid) return; setSubmitting(true); @@ -82,9 +80,53 @@ export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Pr ); return; } - onActivated(data); + setSubmitting(false); + setValidatedBot(data); + setPhase("awaiting_start"); } + function handleOpenMyBot() { + if (!validatedBot?.bot_username) return; + window.open( + `https://t.me/${validatedBot.bot_username}`, + "_blank", + "noopener,noreferrer", + ); + } + + // Polling: enquanto phase === awaiting_start, chama capture-telegram-owner a cada 2.5s + useEffect(() => { + if (phase !== "awaiting_start") return; + let cancelled = false; + + async function tick() { + if (cancelled) return; + const { data, error } = await invokeFunction<{ + found: boolean; + chat_id?: number; + first_name?: string; + bot_username?: string; + }>("capture-telegram-owner", {}); + if (cancelled) return; + if (error) { + console.warn("capture-telegram-owner error", error); + return; + } + if (data?.found && validatedBot) { + setPhase("captured"); + setTimeout(() => onActivated(validatedBot), 1400); + } + } + + tick(); + pollRef.current = window.setInterval(tick, 2500); + return () => { + cancelled = true; + if (pollRef.current) window.clearInterval(pollRef.current); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [phase, validatedBot]); + return (
{/* COLUNA ESQUERDA — passos */} From d33261f798fd9498f94fcc44f4acf44f88654cb6 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:46:49 +0000 Subject: [PATCH 6/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/telegram/BotFatherWizard.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/mika/telegram/BotFatherWizard.tsx b/src/components/mika/telegram/BotFatherWizard.tsx index 7f5dbaf..3e7131c 100644 --- a/src/components/mika/telegram/BotFatherWizard.tsx +++ b/src/components/mika/telegram/BotFatherWizard.tsx @@ -127,6 +127,17 @@ export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Pr // eslint-disable-next-line react-hooks/exhaustive-deps }, [phase, validatedBot]); + if (phase === "awaiting_start" || phase === "captured") { + return ( + + ); + } + return (
{/* COLUNA ESQUERDA — passos */} From 0da092feed219598c222cffaa1812d547d7c5733 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:47:08 +0000 Subject: [PATCH 7/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../mika/telegram/BotFatherWizard.tsx | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/components/mika/telegram/BotFatherWizard.tsx b/src/components/mika/telegram/BotFatherWizard.tsx index 3e7131c..28dff5c 100644 --- a/src/components/mika/telegram/BotFatherWizard.tsx +++ b/src/components/mika/telegram/BotFatherWizard.tsx @@ -500,3 +500,95 @@ function ChatPreview({
); } + +function AwaitingStartPanel({ + botUsername, + botName, + captured, + onOpenBot, +}: { + botUsername: string; + botName: string; + captured: boolean; + onOpenBot: () => void; +}) { + return ( +
+ + + {captured ? ( + +
+ +
+

+ Conectado! 🎉 +

+

+ Identificamos você no Telegram. Estamos finalizando a ativação + do seu agente — em alguns instantes ele começa a responder. +

+
+ ) : ( + +
+ +
+
+

+ Última etapa: diga "oi" pro {botName} +

+

+ Abra seu bot no Telegram e envie qualquer mensagem (pode + ser /start). + Assim a gente sabe que é você e libera o acesso exclusivo. +

+
+ + + +
+ + Aguardando sua primeira mensagem… +
+ +
+

+ Por que isso? +

+

+ Seu agente responde só pra você. Ao enviar a primeira + mensagem, capturamos seu ID do Telegram e bloqueamos o bot + para qualquer outra pessoa — segurança total. +

+
+
+ )} +
+
+
+ ); +} From f1c36d2279c3b96a8ac3d8ff9e518f6efbc23e7e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:49:46 +0000 Subject: [PATCH 8/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../admin-recover-telegram-owner/index.ts | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 supabase/functions/admin-recover-telegram-owner/index.ts diff --git a/supabase/functions/admin-recover-telegram-owner/index.ts b/supabase/functions/admin-recover-telegram-owner/index.ts new file mode 100644 index 0000000..3e7b510 --- /dev/null +++ b/supabase/functions/admin-recover-telegram-owner/index.ts @@ -0,0 +1,241 @@ +// 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, + suspended: 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, + suspended: 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, + }); +}); From ff18aeb1e1e1d99f3b8bb6bb5fe47c89044872d5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:50:00 +0000 Subject: [PATCH 9/9] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/admin-recover-telegram-owner/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supabase/functions/admin-recover-telegram-owner/index.ts b/supabase/functions/admin-recover-telegram-owner/index.ts index 3e7b510..d6dec97 100644 --- a/supabase/functions/admin-recover-telegram-owner/index.ts +++ b/supabase/functions/admin-recover-telegram-owner/index.ts @@ -127,7 +127,7 @@ Deno.serve(async (req) => { serviceId: agent.railway_service_id, environmentId, projectId, - suspended: true, + suspend: true, }); console.log(`[recover] Hermes suspenso, aguardando 25s para descer`); } catch (e) { @@ -178,7 +178,7 @@ Deno.serve(async (req) => { serviceId: agent.railway_service_id, environmentId, projectId, - suspended: false, + suspend: false, }); } catch { /* ignore */ } return jsonResponse({