tambahannya
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user