import { channelFromProvider, createServiceClient, json, normalizeIncomingEvent, orderUpdateByAction, type ProviderName, } from "../_shared/chatbot.ts"; import { getClientIp, getCorsHeaders, hashText, readJsonBody, requireRateLimit, verifyInternalRequest, } from "../_shared/security.ts"; const MAX_BODY_BYTES = 64 * 1024; const allowedProviders = new Set(["telegram", "vk", "messenger_max"]); Deno.serve(async (request) => { if (request.method === "OPTIONS") { const corsHeaders = getCorsHeaders(request, "webhook"); return corsHeaders ? new Response("ok", { headers: corsHeaders }) : json({ error: "Origin not allowed" }, 403); } if (request.method !== "POST") { return json({ error: "Method not allowed" }, 405); } const corsHeaders = getCorsHeaders(request, "webhook") || {}; try { const url = new URL(request.url); const provider = url.searchParams.get("provider") as ProviderName | null; if (!provider || !allowedProviders.has(provider)) { return json({ error: "provider is required" }, 400); } const { body, rawBody } = await readJsonBody>(request, { maxBytes: MAX_BODY_BYTES, }); await verifyInternalRequest(request, rawBody, { rawBody, secretEnvNames: [ `CHATBOT_WEBHOOK_SECRET_${provider.toUpperCase()}`, "CHATBOT_WEBHOOK_SECRET", ], tokenEnvNames: [ `CHATBOT_WEBHOOK_TOKEN_${provider.toUpperCase()}`, "CHATBOT_WEBHOOK_TOKEN", ], }); const event = normalizeIncomingEvent(provider, body); if (!event.orderId) { return json({ error: "order_id is required" }, 400); } const supabase = createServiceClient(); const rateKey = event.externalMessageId || (await hashText(`${provider}:${getClientIp(request)}:${event.text}`)); await requireRateLimit(supabase, { scope: `webhook-${provider}`, key: rateKey, maxCount: 60, windowSeconds: 60, blockSeconds: 300, }); const orderUpdate = orderUpdateByAction(event.action); const messagePayload = { order_id: event.orderId, sender_name: "chatbot-webhook", sender_type: event.senderType, channel: channelFromProvider(event.provider), text: event.text || `Inbound ${event.provider} event`, external_message_id: event.externalMessageId, payload: event.payload, }; const { error: messageError } = await supabase.from("chat_messages").insert(messagePayload); if (messageError && messageError.code !== "23505") { throw messageError; } if (orderUpdate) { const { data: currentOrder, error: orderError } = await supabase .from("orders") .select("id, status, delivery_agreement_status") .eq("id", event.orderId) .single(); if (orderError) { throw orderError; } const { error: updateError } = await supabase .from("orders") .update({ status: orderUpdate.status, delivery_agreement_status: orderUpdate.deliveryAgreementStatus, }) .eq("id", event.orderId); if (updateError) { throw updateError; } const { error: historyError } = await supabase.from("order_history").insert({ order_id: event.orderId, action: `Webhook ${provider}: ${event.action}`, old_status: currentOrder.status, new_status: orderUpdate.status, metadata: { ...event.payload, old_delivery_agreement_status: currentOrder.delivery_agreement_status, new_delivery_agreement_status: orderUpdate.deliveryAgreementStatus, }, }); if (historyError) { throw historyError; } } return new Response(JSON.stringify({ ok: true }), { headers: corsHeaders, }); } catch (error) { return json( { ok: false, error: error instanceof Error ? error.message : "Unexpected error", }, 500, ); } });