diff --git a/src/server/routes/wa_hook_route.ts b/src/server/routes/wa_hook_route.ts index 573c885..17cd87c 100644 --- a/src/server/routes/wa_hook_route.ts +++ b/src/server/routes/wa_hook_route.ts @@ -4,244 +4,236 @@ import type { WAHookMessage } from "types/wa_messages"; import _ from "lodash"; import { logger } from "../lib/logger"; import { - WhatsAppClient, - WhatsAppMessageType, - type ProcessedIncomingMessage, + WhatsAppClient, + WhatsAppMessageType, + type ProcessedIncomingMessage, } from "whatsapp-client-sdk"; const client = new WhatsAppClient({ - accessToken: process.env.WA_TOKEN!, - phoneNumberId: process.env.WA_PHONE_NUMBER_ID!, - webhookVerifyToken: process.env.WA_WEBHOOK_TOKEN!, + accessToken: process.env.WA_TOKEN!, + phoneNumberId: process.env.WA_PHONE_NUMBER_ID!, + webhookVerifyToken: process.env.WA_WEBHOOK_TOKEN!, }); async function fetchWithTimeout( - input: RequestInfo, - init: RequestInit, - timeoutMs = 120_000 + input: RequestInfo, + init: RequestInit, + timeoutMs = 120_000 ) { - const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), timeoutMs); - try { - return await fetch(input, { ...init, signal: controller.signal }); - } finally { - clearTimeout(id); - } + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(input, { ...init, signal: controller.signal }); + } finally { + clearTimeout(id); + } } const FLOW_ID = "1"; async function flowAi({ - message, - question, - name, - number, + message }: { - message: ProcessedIncomingMessage; - question: string; - name: string; - number: string; + message: ProcessedIncomingMessage; }) { - const flow = await prisma.chatFlows.findUnique({ - where: { id: FLOW_ID }, - }); + const flow = await prisma.chatFlows.findUnique({ + where: { id: FLOW_ID }, + }); - if (!flow) { - logger.info("[POST] no flow found"); - return; - } - - if (flow.defaultFlow && flow.active) { - logger.info("[POST] flow found"); - - await client.markMessageAsRead(message.id); - await client.sendTypingIndicator(message.from); - - const { flowUrl, flowToken } = flow; - - try { - const response = await fetchWithTimeout( - `${flowUrl}/prediction/${flow.defaultFlow}`, - { - headers: { - Authorization: `Bearer ${flowToken}`, - "Content-Type": "application/json", - }, - method: "POST", - body: JSON.stringify({ - question, - overrideConfig: { - sessionId: `${_.kebabCase(name)}_x_${number}`, - vars: { userName: _.kebabCase(name), userPhone: number }, - }, - }), - } - ); - - const responseText = await response.text(); - - try { - const result = JSON.parse(responseText); - await prisma.waHook.create({ - data: { - data: JSON.stringify({ - question, - name, - number, - answer: result.text, - flowId: flow.defaultFlow, - }), - }, - }); - - if (flow.waPhoneNumberId && flow.waToken && flow.active) { - await client.sendText(number, result.text); - } - } catch (error) { - logger.error(`[POST] Error parsing AI response ${error}`); - logger.error(responseText); - } - } catch (error) { - logger.error(`[POST] Error calling flow API ${error}`); + if (!flow) { + logger.info("[POST] no flow found"); + return; + } + + if (flow.defaultFlow && flow.active) { + logger.info("[POST] flow found"); + + await client.markMessageAsRead(message.id); + await client.sendTypingIndicatorWithDuration(message.from, 5000); + + const { flowUrl, flowToken } = flow; + + try { + const response = await fetchWithTimeout( + `${flowUrl}/prediction/${flow.defaultFlow}`, + { + headers: { + Authorization: `Bearer ${flowToken}`, + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + question: message.text, + overrideConfig: { + sessionId: `${_.kebabCase(message.contact?.name)}_x_${message.from}`, + vars: { userName: _.kebabCase(message.contact?.name), userPhone: message.from }, + }, + }), + } + ); + + const responseText = await response.text(); + + try { + const result = JSON.parse(responseText); + await prisma.waHook.create({ + data: { + data: JSON.stringify({ + question: message.text, + name: message.contact?.name, + number: message.from, + answer: result.text, + flowId: flow.defaultFlow, + }), + }, + }); + + if (flow.waPhoneNumberId && flow.waToken && flow.active) { + await new Promise(resolve => setTimeout(resolve, 3000)); + await client.sendText(message.from, result.text); + } + } catch (error) { + logger.error(`[POST] Error parsing AI response ${error}`); + logger.error(responseText); + } + } catch (error) { + logger.error(`[POST] Error calling flow API ${error}`); + } } - } } const WaHookRoute = new Elysia({ - prefix: "/wa-hook", - tags: ["WhatsApp Hook"], + prefix: "/wa-hook", + tags: ["WhatsApp Hook"], }) - // ✅ Handle verifikasi Webhook (GET) - .get( - "/hook", - async (ctx) => { - const { query, set } = ctx; - const mode = query["hub.mode"]; - const challenge = query["hub.challenge"]; - const verifyToken = query["hub.verify_token"]; + // ✅ Handle verifikasi Webhook (GET) + .get( + "/hook", + async (ctx) => { + const { query, set } = ctx; + const mode = query["hub.mode"]; + const challenge = query["hub.challenge"]; + const verifyToken = query["hub.verify_token"]; - const getToken = await prisma.apiKey.findUnique({ - where: { key: verifyToken }, - }); + const getToken = await prisma.apiKey.findUnique({ + where: { key: verifyToken }, + }); - if (!getToken) { - set.status = 403; - return "Verification failed [ERR01]"; - } + if (!getToken) { + set.status = 403; + return "Verification failed [ERR01]"; + } - if (mode === "subscribe") { - set.status = 200; - return challenge; - } + if (mode === "subscribe") { + set.status = 200; + return challenge; + } - set.status = 403; - return "Verification failed [ERR02]"; - }, - { - query: t.Object({ - ["hub.mode"]: t.Optional(t.String()), - ["hub.verify_token"]: t.Optional(t.String()), - ["hub.challenge"]: t.Optional(t.String()), - }), - detail: { - summary: "Webhook Verification", - description: "Verifikasi dari WhatsApp API", - }, - } - ) - - // ✅ Handle incoming message (POST) - .post( - "/hook", - async ({ body }) => { - const webhook = client.parseWebhook(body); - - if (webhook[0]?.type === WhatsAppMessageType.TEXT) { - const messageQuestion = webhook[0]?.text; - const from = webhook[0]?.from; - const name = webhook[0].contact?.name; - - if (messageQuestion && from) { - logger.info( - `[POST] Message: ${JSON.stringify({ message: messageQuestion, from, name })}` - ); - // gunakan void agar tidak ada warning “unawaited promise” - void flowAi({ - message: webhook[0], - question: messageQuestion, - name: name || "default_name", - number: from, - }); + set.status = 403; + return "Verification failed [ERR02]"; + }, + { + query: t.Object({ + ["hub.mode"]: t.Optional(t.String()), + ["hub.verify_token"]: t.Optional(t.String()), + ["hub.challenge"]: t.Optional(t.String()), + }), + detail: { + summary: "Webhook Verification", + description: "Verifikasi dari WhatsApp API", + }, } - } + ) - return { - success: true, - message: "WhatsApp Hook received", - }; - }, - { - body: t.Any(), - detail: { - summary: "Receive WhatsApp Messages", - description: "Menerima pesan dari WhatsApp Webhook", - }, - } - ) + // ✅ Handle incoming message (POST) + .post( + "/hook", + async ({ body }) => { + const webhook = client.parseWebhook(body); - // ✅ List WhatsApp Hook - .get( - "/list", - async ({ query }) => { - const limit = query.limit ?? 10; - const page = query.page ?? 1; + if (webhook[0]?.type === WhatsAppMessageType.TEXT) { + const messageQuestion = webhook[0]?.text; + const from = webhook[0]?.from; + const name = webhook[0].contact?.name; - const list = await prisma.waHook.findMany({ - take: limit, - skip: (page - 1) * limit, - orderBy: { createdAt: "desc" }, - }); + if (messageQuestion && from) { + logger.info( + `[POST] Message: ${JSON.stringify({ message: messageQuestion, from, name })}` + ); + // gunakan void agar tidak ada warning “unawaited promise” + void flowAi({ + message: webhook[0], + }); + } + } - const count = await prisma.waHook.count(); - const result = list.map((item) => ({ - id: item.id, - data: item.data as WAHookMessage, - createdAt: item.createdAt, - })); + return { + success: true, + message: "WhatsApp Hook received", + }; + }, + { + body: t.Any(), + detail: { + summary: "Receive WhatsApp Messages", + description: "Menerima pesan dari WhatsApp Webhook", + }, + } + ) - return { - list: result, - count: Math.ceil(count / limit), - }; - }, - { - query: t.Object({ - page: t.Optional(t.Number({ minimum: 1, default: 1 })), - limit: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10 })), - }), - detail: { - summary: "List WhatsApp Hook", - description: "List semua WhatsApp Hook", - }, - } - ) + // ✅ List WhatsApp Hook + .get( + "/list", + async ({ query }) => { + const limit = query.limit ?? 10; + const page = query.page ?? 1; - // ✅ Reset WhatsApp Hook - .post( - "/reset", - async () => { - await prisma.waHook.deleteMany(); - return { - success: true, - message: "WhatsApp Hook reset", - }; - }, - { - detail: { - summary: "Reset WhatsApp Hook", - description: "Reset semua WhatsApp Hook", - }, - } - ); + const list = await prisma.waHook.findMany({ + take: limit, + skip: (page - 1) * limit, + orderBy: { createdAt: "desc" }, + }); + + const count = await prisma.waHook.count(); + const result = list.map((item) => ({ + id: item.id, + data: item.data as WAHookMessage, + createdAt: item.createdAt, + })); + + return { + list: result, + count: Math.ceil(count / limit), + }; + }, + { + query: t.Object({ + page: t.Optional(t.Number({ minimum: 1, default: 1 })), + limit: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10 })), + }), + detail: { + summary: "List WhatsApp Hook", + description: "List semua WhatsApp Hook", + }, + } + ) + + // ✅ Reset WhatsApp Hook + .post( + "/reset", + async () => { + await prisma.waHook.deleteMany(); + return { + success: true, + message: "WhatsApp Hook reset", + }; + }, + { + detail: { + summary: "Reset WhatsApp Hook", + description: "Reset semua WhatsApp Hook", + }, + } + ); export default WaHookRoute;