tambahannya
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { Badge, Button, Chip, Group, Pill, Stack } from "@mantine/core";
|
||||
import { Badge, Button, Chip, Group, Pill, Stack, Text } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import clientRoutes from "@/clientRoutes";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
export default function WajsLayout() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -41,6 +42,28 @@ export default function WajsLayout() {
|
||||
>
|
||||
Reconnect
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
modals.openConfirmModal({
|
||||
title: "Rescan QR",
|
||||
children: <Text>Are you sure you want to rescan QR?</Text>,
|
||||
confirmProps: { color: "red" },
|
||||
labels: {
|
||||
cancel: "Cancel",
|
||||
confirm: "Rescan QR",
|
||||
},
|
||||
onCancel: () => setLoading(false),
|
||||
onConfirm: () => {
|
||||
apiFetch.api.wa.restart.post();
|
||||
setLoading(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Rescan QR
|
||||
</Button>
|
||||
</Group>
|
||||
<Outlet />
|
||||
</Stack>
|
||||
|
||||
116
src/server/lib/mim_utils.ts
Normal file
116
src/server/lib/mim_utils.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// ✅ Tipe kategori utama MIME
|
||||
export type MimeCategory = "image" | "video" | "audio" | "document" | "archive" | "other";
|
||||
|
||||
// ✅ Struktur detail MIME
|
||||
export interface MimeDetail {
|
||||
type: string;
|
||||
category: MimeCategory;
|
||||
exampleMime: string[];
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
// ✅ Full list mimetype yang bisa dikembangkan
|
||||
export const MimeMap: Record<string, MimeDetail> = {
|
||||
image: {
|
||||
type: "image",
|
||||
category: "image",
|
||||
exampleMime: ["image/jpeg", "image/png", "image/gif"],
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "webp"]
|
||||
},
|
||||
video: {
|
||||
type: "video",
|
||||
category: "video",
|
||||
exampleMime: ["video/mp4", "video/mkv", "video/webm"],
|
||||
extensions: ["mp4", "mkv", "avi", "mov", "webm"]
|
||||
},
|
||||
audio: {
|
||||
type: "audio",
|
||||
category: "audio",
|
||||
exampleMime: ["audio/mpeg", "audio/wav", "audio/aac"],
|
||||
extensions: ["mp3", "wav", "aac", "ogg", "flac"]
|
||||
},
|
||||
document: {
|
||||
type: "application",
|
||||
category: "document",
|
||||
exampleMime: ["application/pdf", "application/msword"],
|
||||
extensions: ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt"]
|
||||
},
|
||||
archive: {
|
||||
type: "application",
|
||||
category: "archive",
|
||||
exampleMime: ["application/zip", "application/x-rar-compressed"],
|
||||
extensions: ["zip", "rar", "7z", "tar", "gz"]
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Ambil semua ekstensi valid
|
||||
const allExtensions = Object.values(MimeMap).flatMap(m => m.extensions);
|
||||
|
||||
// ✅ Type Guard untuk menghindari "never"
|
||||
export function isFileExtension(ext: string): ext is (typeof allExtensions)[number] {
|
||||
return allExtensions.includes(ext as any);
|
||||
}
|
||||
|
||||
// ✅ Class utama
|
||||
export class MimeType {
|
||||
private input: string;
|
||||
private ext?: string;
|
||||
private type?: string;
|
||||
|
||||
constructor(input: string) {
|
||||
this.input = input.toLowerCase().trim();
|
||||
this.parseInput();
|
||||
}
|
||||
|
||||
private parseInput() {
|
||||
if (this.input.includes("/")) {
|
||||
const [type, ext] = this.input.split("/");
|
||||
this.type = type;
|
||||
this.ext = ext;
|
||||
} else if (isFileExtension(this.input.replace(/^\./, ""))) {
|
||||
this.ext = this.input.replace(/^\./, "");
|
||||
this.type = Object.values(MimeMap).find(m => m.extensions.includes(this.ext!))?.type;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Dapatkan MIME Type lengkap: "image/png"
|
||||
getType(): string | undefined {
|
||||
if (this.type && this.ext) return `${this.type}/${this.ext}`;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ✅ Dapatkan ekstensi yang digunakan
|
||||
getExtension(): string | undefined {
|
||||
return this.ext;
|
||||
}
|
||||
|
||||
// ✅ Semua ekstensi dalam grup/type
|
||||
getExtensions(): string[] | undefined {
|
||||
const cat = this.getCategory();
|
||||
if (!cat) return;
|
||||
return MimeMap[cat]?.extensions;
|
||||
}
|
||||
|
||||
// ✅ Ambil kategori: "image", "video", dll.
|
||||
getCategory(): MimeCategory | undefined {
|
||||
if (this.type) {
|
||||
const found = Object.values(MimeMap).find(m => m.type === this.type);
|
||||
return found?.category;
|
||||
}
|
||||
if (this.ext) {
|
||||
const found = Object.values(MimeMap).find(m => m.extensions.includes(this.ext!));
|
||||
return found?.category;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ✅ Check cepat berdasarkan kategori
|
||||
isImage() { return this.getCategory() === "image"; }
|
||||
isVideo() { return this.getCategory() === "video"; }
|
||||
isAudio() { return this.getCategory() === "audio"; }
|
||||
isDocument() { return this.getCategory() === "document"; }
|
||||
isArchive() { return this.getCategory() === "archive"; }
|
||||
}
|
||||
|
||||
// ✅ Export default agar bisa: import MimeType from "./..."
|
||||
export default MimeType;
|
||||
@@ -6,6 +6,44 @@ import { v4 as uuid } from 'uuid';
|
||||
import { prisma } from '../prisma';
|
||||
import { getValueByPath } from '../get_value_by_path';
|
||||
import "colors"
|
||||
import { logger } from '../logger';
|
||||
import _ from 'lodash';
|
||||
import MimeType from '../mim_utils';
|
||||
import sharp from "sharp";
|
||||
|
||||
interface Base64ImageResult {
|
||||
fileName: string;
|
||||
base64: string;
|
||||
sizeBeforeKB: number;
|
||||
sizeAfterKB: number;
|
||||
}
|
||||
|
||||
export async function convertImageToPngBase64(
|
||||
inputPath: string,
|
||||
): Promise<Base64ImageResult> {
|
||||
// Baca buffer asli
|
||||
const originalBuffer = await fs.readFile(inputPath);
|
||||
const sizeBeforeKB = originalBuffer.length / 1024;
|
||||
|
||||
// Konversi & kompres ke PNG
|
||||
const optimizedBuffer = await sharp(originalBuffer)
|
||||
.png({ compressionLevel: 9 })
|
||||
.toBuffer();
|
||||
|
||||
const sizeAfterKB = optimizedBuffer.length / 1024;
|
||||
|
||||
// Convert ke base64
|
||||
const base64 = `data:image/png;base64,${optimizedBuffer.toString("base64")}`;
|
||||
|
||||
return {
|
||||
fileName: inputPath.split("/").pop() || "image.png",
|
||||
base64,
|
||||
sizeBeforeKB,
|
||||
sizeAfterKB,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
const MEDIA_DIR = path.join(process.cwd(), 'downloads');
|
||||
await ensureDir(MEDIA_DIR);
|
||||
@@ -27,7 +65,7 @@ type DataMessage = {
|
||||
type: WAWebJS.MessageTypes;
|
||||
to: string;
|
||||
deviceType: string;
|
||||
media: any[] | null;
|
||||
media: Record<string, any>;
|
||||
notifyName: string;
|
||||
}
|
||||
|
||||
@@ -180,17 +218,6 @@ async function startClient() {
|
||||
}
|
||||
}
|
||||
|
||||
function detectFileCategory(mime: string) {
|
||||
if (mime.startsWith("image/")) return "image";
|
||||
if (mime.startsWith("audio/")) return "audio";
|
||||
if (mime.startsWith("video/")) return "video";
|
||||
if (mime === "application/pdf") return "pdf";
|
||||
if (mime.includes("spreadsheet") || mime.includes("excel")) return "excel";
|
||||
if (mime.includes("word")) return "document";
|
||||
if (mime.includes("presentation") || mime.includes("powerpoint")) return "presentation";
|
||||
return "file";
|
||||
}
|
||||
|
||||
// === HANDLER PESAN MASUK ===
|
||||
async function handleIncomingMessage(msg: WAWebJS.Message) {
|
||||
const chat = await msg.getChat();
|
||||
@@ -205,6 +232,18 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("kirim ke webhook")
|
||||
const res = await fetch("https://n8n.wibudev.com/webhook/dc164759-b7ba-47d5-b5d8-ffd9d5840090", {
|
||||
body: JSON.stringify(msg),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
const json = await res.text();
|
||||
console.log(json);
|
||||
|
||||
try {
|
||||
const notifyName = (msg as any)._data.notifyName;
|
||||
|
||||
@@ -217,107 +256,118 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
|
||||
type: msg.type,
|
||||
to: msg.to,
|
||||
deviceType: msg.deviceType,
|
||||
media: null,
|
||||
media: {},
|
||||
notifyName,
|
||||
};
|
||||
|
||||
// === HANDLE MEDIA ===
|
||||
if (msg.hasMedia) {
|
||||
const media = await msg.downloadMedia();
|
||||
|
||||
// Pastikan formatnya data:<mimetype>;base64,<data>
|
||||
const mime = media.mimetype || 'application/octet-stream';
|
||||
const prefixedBase64 = `data:${mime};base64,${media.data}`;
|
||||
|
||||
dataMessage.media = [{
|
||||
type: "file:full",
|
||||
data: prefixedBase64,
|
||||
mime: mime,
|
||||
name: media.filename || `${uuid()}.${mime.split('/')[1] || 'bin'}`
|
||||
}];
|
||||
|
||||
// await fs.writeFile(path.join(MEDIA_DIR, dataMessage.media[0].name), Buffer.from(media.data, 'base64'));
|
||||
|
||||
}
|
||||
|
||||
// === KIRIM KE WEBHOOK ===
|
||||
try {
|
||||
const webhooks = await prisma.webHook.findMany({ where: { enabled: true } });
|
||||
if (!webhooks.length) {
|
||||
log('🚫 Tidak ada webhook yang aktif');
|
||||
return;
|
||||
}
|
||||
// try {
|
||||
// const webhooks = await prisma.webHook.findMany({ where: { enabled: true } });
|
||||
// if (!webhooks.length) {
|
||||
// log('🚫 Tidak ada webhook yang aktif');
|
||||
// return;
|
||||
// }
|
||||
|
||||
await Promise.allSettled(
|
||||
webhooks.map(async (hook) => {
|
||||
try {
|
||||
log(`🌐 Mengirim webhook ke ${hook.url}`);
|
||||
|
||||
let body = payloadConverter({
|
||||
payload: hook.payload ?? JSON.stringify(dataMessage),
|
||||
data: dataMessage,
|
||||
});
|
||||
|
||||
if (dataMessage.hasMedia) {
|
||||
const bodyMedia = JSON.parse(body);
|
||||
bodyMedia.question = msg.body ?? dataMessage.media?.[0].mime;
|
||||
bodyMedia.uploads = dataMessage.media;
|
||||
body = JSON.stringify(bodyMedia);
|
||||
}
|
||||
// // await Promise.allSettled(
|
||||
// // webhooks.map(async (hook) => {
|
||||
// // try {
|
||||
// // log(`🌐 Mengirim webhook ke ${hook.url}`);
|
||||
|
||||
// await fs.writeFile(path.join(process.cwd(), 'webhook.json'), body);
|
||||
// // let res: Response = {} as Response;
|
||||
// // if (!dataMessage.hasMedia) {
|
||||
// // logger.info(`[SEND NO MEDIA] ${hook.url}`);
|
||||
// // res = await fetch(hook.url, {
|
||||
// // method: hook.method,
|
||||
// // headers: {
|
||||
// // "Content-Type": "application/json",
|
||||
// // Authorization: `Bearer ${hook.apiToken}`,
|
||||
// // },
|
||||
// // body: JSON.stringify({
|
||||
// // question: msg.body,
|
||||
// // overrideConfig: {
|
||||
// // sessionId: `${_.kebabCase(dataMessage.fromNumber)}_x_${dataMessage.fromNumber}`,
|
||||
// // vars: { userName: _.kebabCase(dataMessage.fromNumber), userPhone: dataMessage.fromNumber },
|
||||
// // }
|
||||
// // }),
|
||||
// // });
|
||||
// // }
|
||||
|
||||
const res = await fetch(hook.url, {
|
||||
method: hook.method,
|
||||
headers: {
|
||||
...(JSON.parse(hook.headers ?? '{}') as Record<string, string>),
|
||||
...(hook.apiToken ? { Authorization: `Bearer ${hook.apiToken}` } : {}),
|
||||
},
|
||||
body,
|
||||
});
|
||||
// // if (dataMessage.hasMedia) {
|
||||
// // logger.info(`[SEND MEDIA] ${hook.url}`);
|
||||
// // const media = await msg.downloadMedia();
|
||||
|
||||
const responseText = await res.text();
|
||||
// // const mimeMessage = media.mimetype || 'application/octet-stream';
|
||||
// // const typeMime = new MimeType(mimeMessage);
|
||||
|
||||
if (!res.ok) {
|
||||
log(`⚠️ Webhook ${hook.url} gagal: ${res.status}`);
|
||||
log(responseText);
|
||||
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR01]");
|
||||
return;
|
||||
}
|
||||
// // const prefixedBase64 = `data:${mimeMessage};base64,${media.data}`;
|
||||
|
||||
const responseJson = JSON.parse(responseText);
|
||||
// // dataMessage.media = {
|
||||
// // type: typeMime.getCategory() === "image" ? "file" : "file:full",
|
||||
// // data: prefixedBase64,
|
||||
// // mime: mimeMessage,
|
||||
// // name: media.filename || `${uuid()}.${typeMime.getExtension()}`
|
||||
// // };
|
||||
|
||||
if (hook.replay) {
|
||||
try {
|
||||
const textResponseRaw = hook.replayKey
|
||||
? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
|
||||
: JSON.stringify(responseJson, null, 2);
|
||||
// // res = await fetch(hook.url, {
|
||||
// // method: hook.method,
|
||||
// // headers: {
|
||||
// // "Content-Type": "application/json",
|
||||
// // Authorization: `Bearer ${hook.apiToken}`,
|
||||
// // },
|
||||
// // body: JSON.stringify({
|
||||
// // question: msg.body || dataMessage.media.mime,
|
||||
// // overrideConfig: {
|
||||
// // sessionId: `${_.kebabCase(dataMessage.fromNumber)}_x_${dataMessage.fromNumber}`,
|
||||
// // vars: { userName: _.kebabCase(dataMessage.fromNumber), userPhone: dataMessage.fromNumber },
|
||||
// // },
|
||||
// // uploads: [dataMessage.media],
|
||||
// // }),
|
||||
// // });
|
||||
// // }
|
||||
|
||||
const typingDelay = Math.min(5000, Math.max(1500, textResponseRaw.length * 20));
|
||||
await new Promise((r) => setTimeout(r, typingDelay));
|
||||
// // const responseText = await res.text();
|
||||
|
||||
await chat.clearState();
|
||||
// send message
|
||||
await chat.sendMessage(textResponseRaw);
|
||||
|
||||
log(`💬 Balasan dikirim ke ${msg.from} setelah mengetik selama ${typingDelay}ms`);
|
||||
} catch (err) {
|
||||
log('⚠️ Gagal menampilkan status mengetik:', err);
|
||||
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR03]");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log(`❌ Gagal kirim ke ${hook.url}:`, err);
|
||||
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR04]");
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
log('❌ Error mengirim webhook:', error);
|
||||
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR05]");
|
||||
}
|
||||
// // if (!res.ok) {
|
||||
// // log(`⚠️ Webhook ${hook.url} gagal: ${res.status}`);
|
||||
// // logger.error(`[REPLY] Response: ${responseText}`);
|
||||
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR01]");
|
||||
// // return;
|
||||
// // }
|
||||
|
||||
// // const responseJson = JSON.parse(responseText);
|
||||
// // logger.info(`[REPLY] Response: ${responseJson.text}`);
|
||||
|
||||
// // if (hook.replay) {
|
||||
// // try {
|
||||
// // const textResponseRaw = hook.replayKey
|
||||
// // ? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
|
||||
// // : JSON.stringify(responseJson, null, 2);
|
||||
|
||||
// // await chat.clearState();
|
||||
// // // send message
|
||||
// // await chat.sendMessage(textResponseRaw);
|
||||
|
||||
// // logger.info(`💬 Balasan dikirim ke ${msg.from}`);
|
||||
// // } catch (err) {
|
||||
// // logger.error(`⚠️ Gagal menampilkan status mengetik: ${err}`);
|
||||
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR03]");
|
||||
// // }
|
||||
// // }
|
||||
// // } catch (err) {
|
||||
// // logger.error(`❌ Gagal kirim ke ${hook.url}: ${err}`);
|
||||
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR04]");
|
||||
// // }
|
||||
// // })
|
||||
// // );
|
||||
// } catch (error) {
|
||||
// logger.error(`❌ Error mengirim webhook [ERR05]: ${error}`);
|
||||
// await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR05]");
|
||||
// }
|
||||
} catch (err) {
|
||||
log('❌ Error handling pesan:', err);
|
||||
logger.error(`❌ Error handling pesan [ERR06]: ${err}`);
|
||||
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR06]");
|
||||
} finally {
|
||||
await chat.clearState();
|
||||
@@ -325,46 +375,6 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
|
||||
}
|
||||
|
||||
|
||||
function payloadConverter({ payload, data }: { payload: string; data: DataMessage }) {
|
||||
try {
|
||||
const map: Record<string, any> = {
|
||||
'data.from': data.from,
|
||||
'data.fromNumber': data.fromNumber,
|
||||
'data.fromMe': data.fromMe,
|
||||
'data.body': data.body,
|
||||
'data.hasMedia': data.hasMedia,
|
||||
'data.type': data.type,
|
||||
'data.to': data.to,
|
||||
'data.deviceType': data.deviceType,
|
||||
'data.notifyName': data.notifyName,
|
||||
'data.media': data.media
|
||||
};
|
||||
|
||||
let result = payload;
|
||||
|
||||
for (const [key, value] of Object.entries(map)) {
|
||||
let safeValue: string;
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
safeValue = '';
|
||||
} else if (typeof value === 'object') {
|
||||
// Perbaikan di sini — objek seperti media dikonversi ke JSON string
|
||||
safeValue = JSON.stringify(value);
|
||||
} else {
|
||||
safeValue = String(value);
|
||||
}
|
||||
|
||||
result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), safeValue);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error("⚠️ payloadConverter error:", err);
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === CLEANUP SAAT EXIT ===
|
||||
process.on('SIGINT', async () => {
|
||||
log('🛑 SIGINT diterima, menutup client...');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Elysia from "elysia";
|
||||
import Elysia, { t } from "elysia";
|
||||
import { startClient, getState } from "../lib/wa/wa_service";
|
||||
import _ from "lodash";
|
||||
|
||||
@@ -48,5 +48,36 @@ const WaRoute = new Elysia({
|
||||
state: _.omit(state, "client"),
|
||||
};
|
||||
})
|
||||
.post("send-text", async ({body}) => {
|
||||
const state = getState();
|
||||
if (!state.ready) {
|
||||
return {
|
||||
message: "WhatsApp route not ready",
|
||||
};
|
||||
}
|
||||
|
||||
const client = state.client;
|
||||
if (!client) {
|
||||
return {
|
||||
message: "WhatsApp client not ready",
|
||||
};
|
||||
}
|
||||
|
||||
const chat = await client.getChatById(`${body.number}@c.us`);
|
||||
await chat.sendMessage(body.text);
|
||||
|
||||
return {
|
||||
message: "WhatsApp route ready",
|
||||
};
|
||||
},{
|
||||
body: t.Object({
|
||||
number: t.String(),
|
||||
text: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
description: "Send text to WhatsApp",
|
||||
tags: ["WhatsApp"],
|
||||
}
|
||||
})
|
||||
|
||||
export default WaRoute;
|
||||
|
||||
Reference in New Issue
Block a user