tambahannya

This commit is contained in:
bipproduction
2025-10-23 12:10:50 +08:00
parent 367759c4cc
commit 80b6da80c5

View File

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