tambahannya

This commit is contained in:
bipproduction
2025-10-20 07:54:09 +08:00
parent 06478a025f
commit ea1937da6d
21 changed files with 4857 additions and 540 deletions

View File

@@ -5,11 +5,19 @@ import path from 'path';
import { v4 as uuid } from 'uuid';
import { prisma } from '../prisma';
import { getValueByPath } from '../get_value_by_path';
import "colors"
// === KONFIGURASI UTAMA ===
const MEDIA_DIR = path.join(process.cwd(), 'downloads');
await ensureDir(MEDIA_DIR);
async function ensureDir(dir: string) {
try {
await fs.access(dir);
} catch {
await fs.mkdir(dir, { recursive: true });
}
}
type DataMessage = {
from: string;
fromNumber: string;
@@ -19,12 +27,7 @@ type DataMessage = {
type: WAWebJS.MessageTypes;
to: string;
deviceType: string;
media: {
data: WAWebJS.MessageMedia["data"];
mimetype: WAWebJS.MessageMedia["mimetype"];
filename: WAWebJS.MessageMedia["filename"];
filesize: WAWebJS.MessageMedia["filesize"];
};
media: any[] | null;
notifyName: string;
}
@@ -60,13 +63,6 @@ function log(...args: any[]) {
console.log(`[${new Date().toISOString()}]`, ...args);
}
async function ensureDir(dir: string) {
try {
await fs.access(dir);
} catch {
await fs.mkdir(dir, { recursive: true });
}
}
async function safeRm(path: string) {
try {
@@ -95,6 +91,8 @@ async function destroyClient() {
}
}
let connectedAt: number | null = null;
// === PEMBUATAN CLIENT ===
async function startClient() {
if (state.isStarting || state.isReconnecting) {
@@ -131,6 +129,7 @@ async function startClient() {
});
client.on('ready', () => {
connectedAt = Date.now();
log('✅ WhatsApp client siap digunakan!');
state.ready = true;
state.isReconnecting = false;
@@ -177,17 +176,32 @@ 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();
await chat.sendStateTyping();
log(`💬 Pesan dari ${msg.from}: ${msg.body || '[MEDIA]'}`);
if (!connectedAt) return;
if (msg.timestamp * 1000 < connectedAt) return;
if (msg.from.endsWith('@g.us') || msg.isStatus || msg.from === 'status@broadcast') {
log(`🚫 Pesan dari grup/status diabaikan (${msg.from})`);
return;
}
try {
const body = msg.body?.toLowerCase().trim() || '';
const notifyName = (msg as any)._data.notifyName;
const dataMessage: DataMessage = {
@@ -199,32 +213,32 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
type: msg.type,
to: msg.to,
deviceType: msg.deviceType,
media: {
data: null as unknown as WAWebJS.MessageMedia['data'],
mimetype: null as unknown as WAWebJS.MessageMedia['mimetype'],
filename: null as unknown as WAWebJS.MessageMedia['filename'],
filesize: null as unknown as WAWebJS.MessageMedia['filesize'],
},
media: null,
notifyName,
};
// Media handler
// === HANDLE MEDIA ===
if (msg.hasMedia) {
const media = await msg.downloadMedia();
dataMessage.media = {
data: media.data,
mimetype: media.mimetype,
filename: media.filename,
filesize: media.filesize
};
// 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'));
}
// to web hook
// === KIRIM KE WEBHOOK ===
try {
const webhooks = await prisma.webHook.findMany({ where: { enabled: true } });
if (!webhooks.length) {
log('🚫 Tidak ada webhook yang aktif');
return;
@@ -233,8 +247,22 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
await Promise.allSettled(
webhooks.map(async (hook) => {
try {
console.log("send webhook " + hook.url);
const body = payloadConverter({ payload: hook.payload ?? JSON.stringify(dataMessage), data: dataMessage });
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 fs.writeFile(path.join(process.cwd(), 'webhook.json'), body);
const res = await fetch(hook.url, {
method: hook.method,
headers: {
@@ -244,56 +272,58 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
body,
});
if (!res.ok) log(`⚠️ Webhook ${hook.url} gagal: ${res.status}`);
const responseJson = await res.json();
const responseText = await res.text();
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 responseJson = JSON.parse(responseText);
if (hook.replay) {
try {
// === Simulasikan sedang mengetik ===
const chat = await msg.getChat();
await chat.sendStateTyping(); // tampilkan status 'sedang mengetik...'
// Durasi delay tergantung panjang teks (lebih panjang = lebih lama)
const textResponseRaw = hook.replayKey
? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
: JSON.stringify(responseJson, null, 2);
const typingDelay = Math.min(5000, Math.max(1500, textResponseRaw.length * 20)); // 1.55 detik
await new Promise((resolve) => setTimeout(resolve, typingDelay));
const typingDelay = Math.min(5000, Math.max(1500, textResponseRaw.length * 20));
await new Promise((r) => setTimeout(r, typingDelay));
// Setelah delay, hentikan typing indicator
await chat.clearState(); // hilangkan status "mengetik..."
// Kirim balasan ke pengirim
await msg.reply(textResponseRaw);
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(hook.replayKey
? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
: JSON.stringify(responseJson, null, 2)
);
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) {
console.log(error);
log('❌ Error mengirim webhook:', error);
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR05]");
}
} catch (err) {
log('❌ Error handling pesan:', err);
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR06]");
} finally {
await chat.clearState();
}
}
function payloadConverter({ payload, data }: { payload: string; data: DataMessage }) {
try {
const map: Record<string, string | number | boolean | null> = {
const map: Record<string, any> = {
'data.from': data.from,
'data.fromNumber': data.fromNumber,
'data.fromMe': data.fromMe,
@@ -303,19 +333,30 @@ function payloadConverter({ payload, data }: { payload: string; data: DataMessag
'data.to': data.to,
'data.deviceType': data.deviceType,
'data.notifyName': data.notifyName,
'data.media.data': data.media?.data ?? null,
'data.media.mimetype': data.media?.mimetype ?? null,
'data.media.filename': data.media?.filename ?? null,
'data.media.filesize': data.media?.filesize ?? 0,
'data.media': data.media
};
let result = payload;
for (const [key, value] of Object.entries(map)) {
result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), String(value ?? ''));
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 {
return JSON.stringify(data, null, 2);
} catch (err) {
console.error("⚠️ payloadConverter error:", err);
return JSON.stringify(data);
}
}

View File

@@ -0,0 +1,343 @@
import WAWebJS, { Client, LocalAuth, MessageMedia } from 'whatsapp-web.js';
import qrcode from 'qrcode-terminal';
import fs from 'fs/promises';
import path from 'path';
import { v4 as uuid } from 'uuid';
import { prisma } from '../prisma';
import { getValueByPath } from '../get_value_by_path';
// === KONFIGURASI UTAMA ===
const MEDIA_DIR = path.join(process.cwd(), 'downloads');
await ensureDir(MEDIA_DIR);
type DataMessage = {
from: string;
fromNumber: string;
fromMe: boolean;
body: string;
hasMedia: boolean;
type: WAWebJS.MessageTypes;
to: string;
deviceType: string;
media: {
data: WAWebJS.MessageMedia["data"];
mimetype: WAWebJS.MessageMedia["mimetype"];
filename: WAWebJS.MessageMedia["filename"];
filesize: WAWebJS.MessageMedia["filesize"];
};
notifyName: string;
}
// === STATE GLOBAL ===
const state = {
client: null as Client | null,
reconnectTimeout: null as NodeJS.Timeout | null,
isReconnecting: false,
isStarting: false,
qr: null as string | null,
ready: false,
async restart() {
log('🔄 Restart manual diminta...');
await destroyClient();
await startClient();
},
async forceStart() {
log('⚠️ Force start — menghapus cache dan session auth...');
await destroyClient();
await safeRm("./.wwebjs_auth");
await safeRm("./wwebjs_cache");
await startClient();
},
async stop() {
log('🛑 Stop manual diminta...');
await destroyClient();
},
};
// === UTIL ===
function log(...args: any[]) {
console.log(`[${new Date().toISOString()}]`, ...args);
}
async function ensureDir(dir: string) {
try {
await fs.access(dir);
} catch {
await fs.mkdir(dir, { recursive: true });
}
}
async function safeRm(path: string) {
try {
await fs.rm(path, { recursive: true, force: true });
} catch (err) {
log(`⚠️ Gagal hapus ${path}:`, err);
}
}
// === CLEANUP CLIENT ===
async function destroyClient() {
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
if (state.client) {
try {
state.client.removeAllListeners();
await state.client.destroy();
log('🧹 Client lama dihentikan & listener dibersihkan');
} catch (err) {
log('⚠️ Gagal destroy client:', err);
}
state.client = null;
state.ready = false;
}
}
// === PEMBUATAN CLIENT ===
async function startClient() {
if (state.isStarting || state.isReconnecting) {
log('⏳ startClient diabaikan — proses sedang berjalan...');
return;
}
state.isStarting = true;
await destroyClient();
log('🚀 Memulai WhatsApp client...');
const client = new Client({
authStrategy: new LocalAuth({
dataPath: path.join(process.cwd(), '.wwebjs_auth'),
}),
puppeteer: {
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
],
},
});
state.client = client;
// === EVENT LISTENERS ===
client.on('qr', (qr) => {
state.qr = qr;
qrcode.generate(qr, { small: true });
log('🔑 QR code baru diterbitkan');
});
client.on('ready', () => {
log('✅ WhatsApp client siap digunakan!');
state.ready = true;
state.isReconnecting = false;
state.isStarting = false;
state.qr = null;
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
});
client.on('auth_failure', (msg) => {
log('❌ Autentikasi gagal:', msg);
state.ready = false;
});
client.on('disconnected', async (reason) => {
log('⚠️ Client terputus:', reason);
state.ready = false;
if (state.reconnectTimeout) clearTimeout(state.reconnectTimeout);
log('⏳ Mencoba reconnect dalam 5 detik...');
state.reconnectTimeout = setTimeout(async () => {
state.isReconnecting = false;
await startClient();
}, 5000);
});
client.on('message', handleIncomingMessage);
// === INISIALISASI ===
try {
await client.initialize();
} catch (err) {
log('❌ Gagal inisialisasi client:', err);
log('⏳ Mencoba reconnect dalam 10 detik...');
state.reconnectTimeout = setTimeout(async () => {
state.isReconnecting = false;
await startClient();
}, 10000);
} finally {
state.isStarting = false;
}
}
// === HANDLER PESAN MASUK ===
async function handleIncomingMessage(msg: WAWebJS.Message) {
log(`💬 Pesan dari ${msg.from}: ${msg.body || '[MEDIA]'}`);
if (msg.from.endsWith('@g.us') || msg.isStatus || msg.from === 'status@broadcast') {
log(`🚫 Pesan dari grup/status diabaikan (${msg.from})`);
return;
}
try {
const body = msg.body?.toLowerCase().trim() || '';
const notifyName = (msg as any)._data.notifyName;
const dataMessage: DataMessage = {
from: msg.from,
fromNumber: msg.from.split('@')[0] || '',
fromMe: msg.fromMe,
body: msg.body,
hasMedia: msg.hasMedia,
type: msg.type,
to: msg.to,
deviceType: msg.deviceType,
media: {
data: null as unknown as WAWebJS.MessageMedia['data'],
mimetype: null as unknown as WAWebJS.MessageMedia['mimetype'],
filename: null as unknown as WAWebJS.MessageMedia['filename'],
filesize: null as unknown as WAWebJS.MessageMedia['filesize'],
},
notifyName,
};
// Media handler
if (msg.hasMedia) {
const media = await msg.downloadMedia();
dataMessage.media = {
data: media.data,
mimetype: media.mimetype,
filename: media.filename,
filesize: media.filesize
};
}
// to web hook
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 {
console.log("send webhook " + hook.url);
const body = payloadConverter({ payload: hook.payload ?? JSON.stringify(dataMessage), data: dataMessage });
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 (!res.ok) log(`⚠️ Webhook ${hook.url} gagal: ${res.status}`);
const responseJson = await res.json();
if (hook.replay) {
try {
// === Simulasikan sedang mengetik ===
const chat = await msg.getChat();
await chat.sendStateTyping(); // tampilkan status 'sedang mengetik...'
// Durasi delay tergantung panjang teks (lebih panjang = lebih lama)
const textResponseRaw = hook.replayKey
? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
: JSON.stringify(responseJson, null, 2);
const typingDelay = Math.min(5000, Math.max(1500, textResponseRaw.length * 20)); // 1.55 detik
await new Promise((resolve) => setTimeout(resolve, typingDelay));
// Setelah delay, hentikan typing indicator
await chat.clearState(); // hilangkan status "mengetik..."
// Kirim balasan ke pengirim
await msg.reply(textResponseRaw);
log(`💬 Balasan dikirim ke ${msg.from} setelah mengetik selama ${typingDelay}ms`);
} catch (err) {
log('⚠️ Gagal menampilkan status mengetik:', err);
await msg.reply(hook.replayKey
? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
: JSON.stringify(responseJson, null, 2)
);
}
}
} catch (err) {
log(`❌ Gagal kirim ke ${hook.url}:`, err);
}
})
);
} catch (error) {
console.log(error);
}
} catch (err) {
log('❌ Error handling pesan:', err);
}
}
function payloadConverter({ payload, data }: { payload: string; data: DataMessage }) {
try {
const map: Record<string, string | number | boolean | null> = {
'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': data.media?.data ?? null,
'data.media.mimetype': data.media?.mimetype ?? null,
'data.media.filename': data.media?.filename ?? null,
'data.media.filesize': data.media?.filesize ?? 0,
};
let result = payload;
for (const [key, value] of Object.entries(map)) {
result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), String(value ?? ''));
}
return result;
} catch {
return JSON.stringify(data, null, 2);
}
}
// === CLEANUP SAAT EXIT ===
process.on('SIGINT', async () => {
log('🛑 SIGINT diterima, menutup client...');
await destroyClient();
process.exit(0);
});
process.on('SIGTERM', async () => {
log('🛑 SIGTERM diterima, menutup client...');
await destroyClient();
process.exit(0);
});
const getState = () => state;
export { startClient, destroyClient, getState };
if (import.meta.main) {
await startClient();
}