mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 09:16:46 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
9738e10895
commit
a34ff1b17b
6 changed files with 727 additions and 0 deletions
47
supabase/functions/_shared/telegram.ts
Normal file
47
supabase/functions/_shared/telegram.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Helpers compartilhados das Edge Functions Telegram
|
||||
|
||||
const TELEGRAM_API = "https://api.telegram.org";
|
||||
|
||||
export type TelegramApiResult<T = unknown> = {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
description?: string;
|
||||
error_code?: number;
|
||||
result?: T;
|
||||
parameters?: { retry_after?: number };
|
||||
};
|
||||
|
||||
export async function telegramApi<T = unknown>(
|
||||
token: string,
|
||||
method: string,
|
||||
body?: Record<string, unknown>,
|
||||
): Promise<TelegramApiResult<T>> {
|
||||
try {
|
||||
const res = await fetch(`${TELEGRAM_API}/bot${token}/${method}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return {
|
||||
ok: !!data.ok,
|
||||
status: res.status,
|
||||
description: data.description,
|
||||
error_code: data.error_code,
|
||||
result: data.result,
|
||||
parameters: data.parameters,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 0,
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Resposta padrão para o Telegram que NÃO deve gerar retry
|
||||
// (tudo que vem do webhook deve responder 200, mesmo em falha interna)
|
||||
export function telegramAck(): Response {
|
||||
return new Response("ok", { status: 200 });
|
||||
}
|
||||
124
supabase/functions/configure-telegram-webhook/index.ts
Normal file
124
supabase/functions/configure-telegram-webhook/index.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// configure-telegram-webhook
|
||||
// Gera secret aleatório, configura webhook no Telegram e marca telegram_webhook_configured=true.
|
||||
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from "../_shared/cors.ts";
|
||||
import { telegramApi } from "../_shared/telegram.ts";
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function randomHex(bytes: number): string {
|
||||
const buf = new Uint8Array(bytes);
|
||||
crypto.getRandomValues(buf);
|
||||
return Array.from(buf)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function getDecryptedSecret(
|
||||
admin: ReturnType<typeof createClient>,
|
||||
secretId: string,
|
||||
): Promise<string | null> {
|
||||
const { data, error } = await admin
|
||||
.rpc("vault_decrypt_secret", { secret_id: secretId })
|
||||
.single();
|
||||
if (!error && data) {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
return (data as any).decrypted_secret ?? (data as unknown as string);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const { data: agent, error: agentErr } = await admin
|
||||
.from("agent_instances")
|
||||
.select("id, uuid_tenant, telegram_bot_token_vault_id, status")
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (agentErr || !agent) {
|
||||
return jsonResponse({ error: "Agente não encontrado." }, 404);
|
||||
}
|
||||
if (!agent.telegram_bot_token_vault_id) {
|
||||
return jsonResponse(
|
||||
{ error: "Conecte um bot antes de configurar o webhook." },
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const token = await getDecryptedSecret(admin, agent.telegram_bot_token_vault_id);
|
||||
if (!token) {
|
||||
return jsonResponse(
|
||||
{ error: "Token corrompido. Desconecte e reconecte o bot." },
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
const webhookSecret = randomHex(32);
|
||||
const webhookUrl =
|
||||
`${supabaseUrl}/functions/v1/telegram-webhook?token=${agent.uuid_tenant}`;
|
||||
|
||||
const setRes = await telegramApi(token, "setWebhook", {
|
||||
url: webhookUrl,
|
||||
allowed_updates: ["message"],
|
||||
drop_pending_updates: true,
|
||||
secret_token: webhookSecret,
|
||||
});
|
||||
|
||||
if (!setRes.ok) {
|
||||
console.error("setWebhook failed", setRes);
|
||||
return jsonResponse(
|
||||
{ error: "Falha ao configurar webhook no Telegram." },
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
const { error: updErr } = await admin
|
||||
.from("agent_instances")
|
||||
.update({
|
||||
telegram_webhook_secret: webhookSecret,
|
||||
telegram_webhook_configured: true,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", agent.id);
|
||||
|
||||
if (updErr) {
|
||||
console.error("agent update error", updErr);
|
||||
return jsonResponse({ error: "Webhook configurado, mas falha ao salvar." }, 500);
|
||||
}
|
||||
|
||||
return jsonResponse({ success: true });
|
||||
} catch (err) {
|
||||
console.error("configure-telegram-webhook fatal", err);
|
||||
return jsonResponse(
|
||||
{ error: err instanceof Error ? err.message : "Erro inesperado" },
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
111
supabase/functions/disconnect-telegram/index.ts
Normal file
111
supabase/functions/disconnect-telegram/index.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// disconnect-telegram
|
||||
// Remove webhook no Telegram, deleta secret do Vault e limpa colunas em agent_instances.
|
||||
// Preserva telegram_messages_log (auditoria) e telegram_onboarding_completed.
|
||||
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from "../_shared/cors.ts";
|
||||
import { telegramApi } from "../_shared/telegram.ts";
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
async function getDecryptedSecret(
|
||||
admin: ReturnType<typeof createClient>,
|
||||
secretId: string,
|
||||
): Promise<string | null> {
|
||||
const { data, error } = await admin
|
||||
.rpc("vault_decrypt_secret", { secret_id: secretId })
|
||||
.single();
|
||||
if (!error && data) {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
return (data as any).decrypted_secret ?? (data as unknown as string);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const { data: agent, error: agentErr } = await admin
|
||||
.from("agent_instances")
|
||||
.select("id, telegram_bot_token_vault_id")
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (agentErr || !agent) {
|
||||
return jsonResponse({ error: "Agente não encontrado." }, 404);
|
||||
}
|
||||
|
||||
// 1) Tenta deletar webhook no Telegram (silent em falha)
|
||||
if (agent.telegram_bot_token_vault_id) {
|
||||
const token = await getDecryptedSecret(admin, agent.telegram_bot_token_vault_id);
|
||||
if (token) {
|
||||
try {
|
||||
await telegramApi(token, "deleteWebhook", { drop_pending_updates: true });
|
||||
} catch (e) {
|
||||
console.warn("deleteWebhook ignorado:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Remove secret do Vault via RPC
|
||||
try {
|
||||
await admin.rpc("vault_delete_secret", {
|
||||
secret_id: agent.telegram_bot_token_vault_id,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("vault_delete_secret ignorado:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Limpa colunas no agent_instances
|
||||
const { error: updErr } = await admin
|
||||
.from("agent_instances")
|
||||
.update({
|
||||
telegram_bot_token_vault_id: null,
|
||||
telegram_bot_username: null,
|
||||
telegram_webhook_configured: false,
|
||||
telegram_webhook_secret: null,
|
||||
telegram_first_message_received_at: null,
|
||||
telegram_connected_at: null,
|
||||
telegram_token_invalid: false,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", agent.id);
|
||||
|
||||
if (updErr) {
|
||||
console.error("agent update error", updErr);
|
||||
return jsonResponse({ error: "Falha ao desconectar." }, 500);
|
||||
}
|
||||
|
||||
return jsonResponse({ success: true });
|
||||
} catch (err) {
|
||||
console.error("disconnect-telegram fatal", err);
|
||||
return jsonResponse(
|
||||
{ error: err instanceof Error ? err.message : "Erro inesperado" },
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
212
supabase/functions/telegram-webhook/index.ts
Normal file
212
supabase/functions/telegram-webhook/index.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// telegram-webhook (PÚBLICA — verify_jwt = false)
|
||||
// Recebe updates do Telegram, valida 3 camadas (uuid_tenant, secret, rate limit),
|
||||
// registra mensagem em telegram_messages_log e responde com placeholder via sendMessage.
|
||||
//
|
||||
// TODO Fase 5: substituir resposta placeholder por proxy para container Hermes via SSH/API.
|
||||
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { telegramApi, telegramAck } from "../_shared/telegram.ts";
|
||||
|
||||
const RATE_LIMIT_MAX = 30;
|
||||
const RATE_LIMIT_WINDOW_MS = 60_000;
|
||||
|
||||
function firstName(fullName: string | null | undefined): string {
|
||||
if (!fullName) return "Mika";
|
||||
const parts = fullName.trim().split(/\s+/);
|
||||
return parts[0] || "Mika";
|
||||
}
|
||||
|
||||
async function getDecryptedSecret(
|
||||
admin: ReturnType<typeof createClient>,
|
||||
secretId: string,
|
||||
): Promise<string | null> {
|
||||
const { data, error } = await admin
|
||||
.rpc("vault_decrypt_secret", { secret_id: secretId })
|
||||
.single();
|
||||
if (!error && data) {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
return (data as any).decrypted_secret ?? (data as unknown as string);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// Sempre responde 200 para o Telegram não retentar — ack cedo em qualquer falha.
|
||||
try {
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
||||
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const admin = createClient(supabaseUrl, serviceKey);
|
||||
|
||||
const url = new URL(req.url);
|
||||
const uuidTenant = url.searchParams.get("token");
|
||||
if (!uuidTenant) return telegramAck();
|
||||
|
||||
const incomingSecret = req.headers.get("X-Telegram-Bot-Api-Secret-Token") ?? "";
|
||||
|
||||
// 1) Localiza agente + nome do usuário
|
||||
const { data: agent, error: agentErr } = await admin
|
||||
.from("agent_instances")
|
||||
.select(
|
||||
"id, user_id, status, telegram_webhook_secret, telegram_bot_token_vault_id, telegram_connected_at, profiles:profiles!agent_instances_user_id_fkey(full_name)",
|
||||
)
|
||||
.eq("uuid_tenant", uuidTenant)
|
||||
.maybeSingle();
|
||||
|
||||
if (agentErr || !agent) return telegramAck();
|
||||
if (agent.status === "suspended" || agent.status === "error") return telegramAck();
|
||||
|
||||
// 2) Valida secret_token (anti-spoofing)
|
||||
if (
|
||||
!agent.telegram_webhook_secret ||
|
||||
incomingSecret !== agent.telegram_webhook_secret
|
||||
) {
|
||||
console.warn("telegram-webhook: secret mismatch", { agent_id: agent.id });
|
||||
return new Response("unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
// 3) Rate limit por agent_instance (30 req/min)
|
||||
const now = new Date();
|
||||
const { data: bucket } = await admin
|
||||
.from("telegram_rate_limit_bucket")
|
||||
.select("*")
|
||||
.eq("agent_instance_id", agent.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (!bucket) {
|
||||
await admin.from("telegram_rate_limit_bucket").insert({
|
||||
agent_instance_id: agent.id,
|
||||
request_count: 1,
|
||||
window_start: now.toISOString(),
|
||||
updated_at: now.toISOString(),
|
||||
});
|
||||
} else {
|
||||
const windowStart = new Date(bucket.window_start as string);
|
||||
const elapsed = now.getTime() - windowStart.getTime();
|
||||
if (elapsed > RATE_LIMIT_WINDOW_MS) {
|
||||
await admin
|
||||
.from("telegram_rate_limit_bucket")
|
||||
.update({
|
||||
request_count: 1,
|
||||
window_start: now.toISOString(),
|
||||
updated_at: now.toISOString(),
|
||||
})
|
||||
.eq("agent_instance_id", agent.id);
|
||||
} else {
|
||||
const newCount = (bucket.request_count as number) + 1;
|
||||
if (newCount > RATE_LIMIT_MAX) {
|
||||
console.warn("telegram-webhook: rate limit hit", { agent_id: agent.id });
|
||||
return telegramAck(); // throttle silencioso
|
||||
}
|
||||
await admin
|
||||
.from("telegram_rate_limit_bucket")
|
||||
.update({
|
||||
request_count: newCount,
|
||||
updated_at: now.toISOString(),
|
||||
})
|
||||
.eq("agent_instance_id", agent.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Parse do payload
|
||||
const payload = await req.json().catch(() => null);
|
||||
if (!payload || !payload.message) return telegramAck();
|
||||
|
||||
const message = payload.message;
|
||||
const chatId = message.chat?.id as number | undefined;
|
||||
if (!chatId) return telegramAck();
|
||||
|
||||
const fromId = message.from?.id as number | null | undefined;
|
||||
const fromUsername = (message.from?.username as string | undefined) ?? null;
|
||||
const text = (message.text as string | undefined) ?? null;
|
||||
const entities = (message.entities as Array<{ type: string }> | undefined) ?? [];
|
||||
|
||||
let messageType: "text" | "command" | "other" = "other";
|
||||
if (entities.some((e) => e.type === "bot_command")) messageType = "command";
|
||||
else if (text) messageType = "text";
|
||||
|
||||
// 5) Detecta primeira mensagem da sessão atual
|
||||
let isFirstMessage = false;
|
||||
if (agent.telegram_connected_at) {
|
||||
const { count } = await admin
|
||||
.from("telegram_messages_log")
|
||||
.select("id", { count: "exact", head: true })
|
||||
.eq("agent_instance_id", agent.id)
|
||||
.eq("direction", "incoming")
|
||||
.gte("created_at", agent.telegram_connected_at as string);
|
||||
if ((count ?? 0) === 0) {
|
||||
isFirstMessage = true;
|
||||
await admin
|
||||
.from("agent_instances")
|
||||
.update({ telegram_first_message_received_at: now.toISOString() })
|
||||
.eq("id", agent.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 6) Insert da mensagem incoming
|
||||
await admin.from("telegram_messages_log").insert({
|
||||
agent_instance_id: agent.id,
|
||||
user_id: agent.user_id,
|
||||
telegram_chat_id: chatId,
|
||||
telegram_user_id: fromId ?? null,
|
||||
telegram_username: fromUsername,
|
||||
direction: "incoming",
|
||||
message_text: text,
|
||||
message_type: messageType,
|
||||
is_first_message: isFirstMessage,
|
||||
raw_payload: payload,
|
||||
});
|
||||
|
||||
// 7) Resposta placeholder via sendMessage
|
||||
// TODO Fase 5: substituir resposta placeholder por proxy para container Hermes via SSH/API.
|
||||
if (!agent.telegram_bot_token_vault_id) return telegramAck();
|
||||
const token = await getDecryptedSecret(admin, agent.telegram_bot_token_vault_id as string);
|
||||
if (!token) return telegramAck();
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const fullName = (agent as any).profiles?.full_name as string | null | undefined;
|
||||
const replyText =
|
||||
`Olá! Sou o Mika de ${firstName(fullName)}. Estou quase pronto para conversar com você de verdade — meu cérebro está sendo configurado pela DOMCO. Em breve vou responder de forma inteligente! Por enquanto, este é apenas um teste de conexão. ✨`;
|
||||
|
||||
let send = await telegramApi(token, "sendMessage", {
|
||||
chat_id: chatId,
|
||||
text: replyText,
|
||||
});
|
||||
|
||||
if (!send.ok && send.status === 401) {
|
||||
await admin
|
||||
.from("agent_instances")
|
||||
.update({ telegram_token_invalid: true, updated_at: new Date().toISOString() })
|
||||
.eq("id", agent.id);
|
||||
return telegramAck();
|
||||
}
|
||||
|
||||
if (!send.ok && send.status === 429) {
|
||||
const retryAfter = send.parameters?.retry_after ?? 1;
|
||||
await new Promise((r) => setTimeout(r, Math.min(retryAfter, 5) * 1000));
|
||||
send = await telegramApi(token, "sendMessage", {
|
||||
chat_id: chatId,
|
||||
text: replyText,
|
||||
});
|
||||
}
|
||||
|
||||
if (send.ok) {
|
||||
await admin.from("telegram_messages_log").insert({
|
||||
agent_instance_id: agent.id,
|
||||
user_id: agent.user_id,
|
||||
telegram_chat_id: chatId,
|
||||
direction: "outgoing",
|
||||
message_text: replyText,
|
||||
message_type: "text",
|
||||
is_first_message: false,
|
||||
raw_payload: send.result ?? null,
|
||||
});
|
||||
} else {
|
||||
console.error("sendMessage failed", send);
|
||||
}
|
||||
|
||||
return telegramAck();
|
||||
} catch (err) {
|
||||
console.error("telegram-webhook fatal", err);
|
||||
return telegramAck();
|
||||
}
|
||||
});
|
||||
221
supabase/functions/validate-telegram-bot/index.ts
Normal file
221
supabase/functions/validate-telegram-bot/index.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// validate-telegram-bot
|
||||
// Recebe { token } do usuário autenticado, valida no Telegram via getMe,
|
||||
// garante unicidade do bot, salva o token no Vault e atualiza agent_instances.
|
||||
|
||||
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 GetMeResult {
|
||||
id: number;
|
||||
is_bot: boolean;
|
||||
first_name: string;
|
||||
username: 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 body = await req.json().catch(() => ({}));
|
||||
const token = (body?.token ?? "").toString().trim();
|
||||
if (!token || !/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
|
||||
return jsonResponse(
|
||||
{ error: "Token inválido. Verifique se copiou corretamente do BotFather." },
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// 1) Valida no Telegram
|
||||
const me = await telegramApi<GetMeResult>(token, "getMe");
|
||||
if (!me.ok) {
|
||||
if (me.status === 401 || me.status === 404) {
|
||||
return jsonResponse(
|
||||
{ error: "Token inválido. Verifique se copiou corretamente do BotFather." },
|
||||
400,
|
||||
);
|
||||
}
|
||||
if (me.status === 429) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error:
|
||||
"O Telegram está limitando nossas requisições. Aguarde 1 minuto e tente novamente.",
|
||||
},
|
||||
429,
|
||||
);
|
||||
}
|
||||
return jsonResponse(
|
||||
{ error: me.description || "Falha ao validar token no Telegram." },
|
||||
502,
|
||||
);
|
||||
}
|
||||
|
||||
const bot = me.result!;
|
||||
const botUsername = bot.username;
|
||||
const botName = bot.first_name;
|
||||
const botId = bot.id;
|
||||
|
||||
// 2) Service-role client p/ banco e vault
|
||||
const admin = createClient(supabaseUrl, serviceKey);
|
||||
|
||||
// 3) Garante que o usuário tem agent_instance
|
||||
const { data: agent, error: agentErr } = await admin
|
||||
.from("agent_instances")
|
||||
.select("id, telegram_bot_token_vault_id, status")
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (agentErr) {
|
||||
console.error("agent lookup error", agentErr);
|
||||
return jsonResponse({ error: "Falha ao localizar agente." }, 500);
|
||||
}
|
||||
if (!agent) {
|
||||
return jsonResponse(
|
||||
{ error: "Seu agente ainda não está pronto. Aguarde o provisionamento." },
|
||||
409,
|
||||
);
|
||||
}
|
||||
if (agent.status === "suspended" || agent.status === "error") {
|
||||
return jsonResponse(
|
||||
{ error: "Agente suspenso. Regularize sua assinatura antes de conectar o Telegram." },
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
// 4) Verifica unicidade do bot username (em outro agent_instance)
|
||||
const { data: conflict } = await admin
|
||||
.from("agent_instances")
|
||||
.select("id")
|
||||
.eq("telegram_bot_username", botUsername)
|
||||
.neq("id", agent.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (conflict) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error:
|
||||
"Este bot já está conectado a outra conta Mika. Crie um novo bot no BotFather.",
|
||||
},
|
||||
409,
|
||||
);
|
||||
}
|
||||
|
||||
// 5) Se já houver um vault_id antigo, remove (reconexão)
|
||||
if (agent.telegram_bot_token_vault_id) {
|
||||
await admin.rpc("exec_sql_void", {}).catch(() => {});
|
||||
// tenta remover diretamente; ignora falha
|
||||
const { error: delErr } = await admin
|
||||
.from("vault.secrets" as unknown as never)
|
||||
.delete()
|
||||
.eq("id", agent.telegram_bot_token_vault_id);
|
||||
if (delErr) {
|
||||
console.warn("Vault old secret delete (ignorável):", delErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 6) Cria secret no Vault via RPC SQL
|
||||
const secretName = `telegram_bot_token_${userId}_${Math.floor(Date.now() / 1000)}`;
|
||||
const { data: vaultData, error: vaultErr } = await admin
|
||||
.rpc("vault_create_secret", {
|
||||
secret_value: token,
|
||||
secret_name: secretName,
|
||||
secret_description: `Telegram bot token for user ${userId}`,
|
||||
})
|
||||
.single();
|
||||
|
||||
let secretId: string | null = null;
|
||||
if (!vaultErr && vaultData) {
|
||||
// RPC pode retornar { secret_id } ou string
|
||||
// deno-lint-ignore no-explicit-any
|
||||
secretId = (vaultData as any).secret_id ?? (vaultData as unknown as string);
|
||||
} else {
|
||||
// Fallback: usa SQL direto via PostgREST (requer função vault.create_secret exposta).
|
||||
// Caso não exista a RPC, criamos via PostgREST raw SQL exec.
|
||||
const sqlRes = await fetch(`${supabaseUrl}/rest/v1/rpc/vault_create_secret`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: serviceKey,
|
||||
Authorization: `Bearer ${serviceKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secret_value: token,
|
||||
secret_name: secretName,
|
||||
secret_description: `Telegram bot token for user ${userId}`,
|
||||
}),
|
||||
});
|
||||
if (sqlRes.ok) {
|
||||
const out = await sqlRes.json().catch(() => null);
|
||||
secretId =
|
||||
(Array.isArray(out) ? out[0]?.secret_id ?? out[0] : out?.secret_id ?? out) ?? null;
|
||||
} else {
|
||||
console.error("vault_create_secret RPC failed", await sqlRes.text());
|
||||
}
|
||||
}
|
||||
|
||||
if (!secretId) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error:
|
||||
"Não foi possível salvar o token com segurança. Tente novamente em instantes.",
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
// 7) Atualiza agent_instances
|
||||
const { error: updErr } = await admin
|
||||
.from("agent_instances")
|
||||
.update({
|
||||
telegram_bot_token_vault_id: secretId,
|
||||
telegram_bot_username: botUsername,
|
||||
telegram_connected_at: new Date().toISOString(),
|
||||
telegram_token_invalid: false,
|
||||
telegram_webhook_configured: false,
|
||||
telegram_first_message_received_at: null,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", agent.id);
|
||||
|
||||
if (updErr) {
|
||||
console.error("agent update error", updErr);
|
||||
return jsonResponse({ error: "Falha ao salvar dados do bot." }, 500);
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
valid: true,
|
||||
bot_username: botUsername,
|
||||
bot_name: botName,
|
||||
bot_id: botId,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("validate-telegram-bot fatal", err);
|
||||
return jsonResponse(
|
||||
{ error: err instanceof Error ? err.message : "Erro inesperado" },
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue