mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 10:16:43 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
676a424259
commit
a8ed0f79e8
2 changed files with 314 additions and 0 deletions
134
supabase/functions/create-managed-bot/index.ts
Normal file
134
supabase/functions/create-managed-bot/index.ts
Normal file
|
|
@ -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<typeof createClient>,
|
||||
username: string,
|
||||
): Promise<boolean> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
});
|
||||
180
supabase/functions/managed-bot-webhook/index.ts
Normal file
180
supabase/functions/managed-bot-webhook/index.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
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();
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue