supersam/supabase/functions/chatbot-webhook/index.ts

142 lines
3.9 KiB
TypeScript

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<ProviderName>(["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<Record<string, unknown>>(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,
);
}
});