mika-agent-assist/supabase/functions/telegram-webhook/index.ts
gpt-engineer-app[bot] 8a7adff80b Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
2026-05-22 21:00:49 +00:00

147 lines
5.2 KiB
TypeScript

// telegram-webhook (PÚBLICA — verify_jwt = false)
// Recebe updates legados do Telegram, valida 3 camadas (uuid_tenant, secret, rate limit)
// e registra mensagem em telegram_messages_log. O Hermes responde via polling/runtime;
// esta função nunca deve enviar fallback para o usuário final.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
import { telegramAck } from "../_shared/telegram.ts";
const RATE_LIMIT_MAX = 30;
const RATE_LIMIT_WINDOW_MS = 60_000;
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_connected_at",
)
.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,
});
console.log("telegram-webhook: update logged; runtime/polling owns the reply", {
agent_id: agent.id,
chat_id: chatId,
});
return telegramAck();
} catch (err) {
console.error("telegram-webhook fatal", err);
return telegramAck();
}
});