mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 09:36:43 +00:00
241 lines
8.1 KiB
TypeScript
241 lines
8.1 KiB
TypeScript
// 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<TelegramUpdate[]>(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,
|
|
});
|
|
});
|