diff --git a/src/server/routes/wa_hook_route.ts b/src/server/routes/wa_hook_route.ts index e672d6d..573c885 100644 --- a/src/server/routes/wa_hook_route.ts +++ b/src/server/routes/wa_hook_route.ts @@ -3,203 +3,245 @@ import { prisma } from "../lib/prisma"; import type { WAHookMessage } from "types/wa_messages"; import _ from "lodash"; import { logger } from "../lib/logger"; - - -import { WhatsAppClient, WhatsAppMessageType } from 'whatsapp-client-sdk'; +import { + 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) { - const controller = new AbortController() - const id = setTimeout(() => controller.abort(), timeoutMs) - try { - return await fetch(input, { ...init, signal: controller.signal }) - } finally { - clearTimeout(id) - } +async function fetchWithTimeout( + 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); + } } -async function flowAi({ question, name, number }: { question: string, name: string, number: string }) { +const FLOW_ID = "1"; - const flow = await prisma.chatFlows.findUnique({ - where: { - id: "1", - }, - }) +async function flowAi({ + message, + question, + name, + number, +}: { + message: ProcessedIncomingMessage; + question: string; + name: string; + number: string; +}) { + const flow = await prisma.chatFlows.findUnique({ + where: { id: FLOW_ID }, + }); - if (!flow) { - logger.info("[POST] no flow found") - } + if (!flow) { + logger.info("[POST] no flow found"); + return; + } - if (flow?.defaultFlow && flow.active) { - const { flowUrl, flowToken } = flow - const response = await fetchWithTimeout(`${flowUrl}/prediction/${flow.defaultFlow}`, { - headers: { - Authorization: `Bearer ${flowToken}`, - 'Content-Type': 'application/json', + 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 }, }, - 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) - const create = await prisma.waHook.create({ - data: { - data: JSON.stringify({ - question, - name, - number, - answer: result.text, - flowId: flow.defaultFlow, - }), - }, - }); - - if (flow?.waPhoneNumberId && flow?.waToken && flow.active) { - client.sendText(number, result.text) - } - - } catch (error) { - console.log(error) - console.log(responseText) + }), } - } + ); + 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}`); + } + } } 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, - } - }); - - console.log(getToken); - - if (!getToken) { - set.status = 403; - return "Verification failed [ERR01]"; - } - - if (mode === "subscribe") { - set.status = 200; - return challenge; - } + const getToken = await prisma.apiKey.findUnique({ + where: { key: verifyToken }, + }); + if (!getToken) { 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 "Verification failed [ERR01]"; + } + + 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, + }); } - }) + } - // ✅ Handle incoming message (POST) - .post("/hook", async ({ body }) => { + return { + success: true, + message: "WhatsApp Hook received", + }; + }, + { + body: t.Any(), + detail: { + summary: "Receive WhatsApp Messages", + description: "Menerima pesan dari WhatsApp Webhook", + }, + } + ) - 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 message = 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 (message && from) { - logger.info(`[POST] Message: ${JSON.stringify({ - message, - from, - name - })}`) - flowAi({ - question: message, - name: name || "default_name", - number: from, - }) - } - } + 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" - } - }) - .get("/list", async ({ query }) => { - const list = await prisma.waHook.findMany({ - take: query.limit, - skip: ((query.page || 1) - 1) * (query.limit || 10), - orderBy: { - createdAt: "desc", - }, - }); + 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", + }, + } + ) - 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 / (query.limit || 10)), - }; - }, { - 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", - } - }) - .post("/reset", async () => { - await prisma.waHook.deleteMany() - return { - success: true, - message: "WhatsApp Hook reset" - }; - }, { - detail: { - summary: "Reset WhatsApp Hook", - description: "Reset 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;