WA Sender - Service Otomatisasi Pesan WhatsApp

Deskripsi Proyek: Engine WhatsApp Otomatis
Proyek ini adalah sebuah backend service (engine) yang dibangun menggunakan **Node.js** dan **Express** untuk mengirim pesan dan file WhatsApp secara otomatis. Engine ini dirancang dengan fokus pada stabilitas dan keamanan, menggunakan standar modern seperti **ES Modules**. Untuk meminimalisir risiko pemblokiran oleh WhatsApp, engine ini tidak mengirim pesan secara langsung, melainkan menggunakan **sistem antrian (queue) sederhana dengan jeda waktu acak** antar pengiriman untuk mensimulasikan perilaku manusia. Selain itu, semua endpoint API diamankan dengan **API Key** untuk mencegah akses yang tidak sah.
Fitur Utama
- Menggunakan ES Modules (import/export) untuk kode yang modern.
- Endpoint `POST /api/send-message` untuk mengirim pesan teks.
- Endpoint `POST /api/send-file` untuk mengirim file dari direktori server.
- Keamanan endpoint dengan validasi API Key melalui header `x-api-key`.
- Sistem Antrian (Queue) untuk memproses pengiriman satu per satu.
- Jeda waktu acak (2-7 detik) antar pesan untuk mengurangi risiko di-ban.
- Penyimpanan Sesi (LocalAuth) sehingga tidak perlu scan QR code berulang kali.
{
"name": "wa-engine",
"version": "1.0.0",
"description": "Engine WhatsApp otomatis dengan keamanan dan antrian anti-ban",
"main": "app.js",
"type": "module",
"scripts": {
"start": "node app.js"
},
"keywords": [
"whatsapp",
"nodejs",
"express",
"api"
],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^16.3.1",
"express": "^4.18.2",
"qrcode-terminal": "^0.12.0",
"whatsapp-web.js": "^1.23.0"
}
}# Port untuk server Express
PORT=3000
# Kunci API rahasia untuk mengamankan endpoint
API_KEY=GANTI_DENGAN_KUNCI_RAHASIA_ANDAimport wweb from 'whatsapp-web.js';
const { Client, LocalAuth, MessageMedia } = wweb;
import qrcode from 'qrcode-terminal';
console.log("Mempersiapkan client WhatsApp...");
const client = new Client({
authStrategy: new LocalAuth(),
puppeteer: {
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
}
});
client.on('qr', (qr) => {
console.log('QR CODE DITERIMA, SILAKAN SCAN:');
qrcode.generate(qr, { small: true });
});
client.on('ready', () => {
console.log('Client WhatsApp sudah siap!');
});
client.on('auth_failure', msg => {
console.error('AUTENTIKASI GAGAL', msg);
});
client.initialize();
export { client, MessageMedia };import { client, MessageMedia } from './whatsapp.js';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const messageQueue = [];
let isProcessing = false;
const processQueue = async () => {
if (isProcessing || messageQueue.length === 0) return;
isProcessing = true;
const job = messageQueue.shift();
console.log(`[Queue] Memproses pekerjaan untuk ${job.data.chatId}...`);
try {
if (job.type === 'message') {
await client.sendMessage(job.data.chatId, job.data.message);
} else if (job.type === 'file') {
const filePath = path.join(__dirname, 'public', 'files', job.data.filename);
const media = MessageMedia.fromFilePath(filePath);
await client.sendMessage(job.data.chatId, media, { caption: job.data.caption });
}
console.log(`[Queue] Pekerjaan untuk ${job.data.chatId} berhasil.`);
} catch (error) {
console.error(`[Queue] Gagal memproses:`, error.message);
}
const randomDelay = Math.floor(Math.random() * (7000 - 2000 + 1)) + 2000;
console.log(`[Queue] Menunggu ${randomDelay / 1000} detik...`);
setTimeout(() => {
isProcessing = false;
processQueue();
}, randomDelay);
};
export const addToQueue = (job) => {
messageQueue.push(job);
console.log(`[Queue] Pekerjaan baru ditambahkan. Total antrian: ${messageQueue.length}`);
processQueue();
};export const apiKeyAuth = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ status: 'error', message: 'Akses ditolak: API Key tidak ada' });
}
if (apiKey !== process.env.API_KEY) {
return res.status(403).json({ status: 'error', message: 'Akses ditolak: API Key tidak valid' });
}
next();
};import { client } from '../whatsapp.js';
import { addToQueue } from '../queue.js';
const formatNumber = (number) => {
if (number.startsWith('0')) {
return `62${number.slice(1)}@c.us`;
} else if (number.startsWith('62')) {
return `${number}@c.us`;
}
return `${number}@c.us`;
}
export const sendMessage = async (req, res) => {
const { number, message } = req.body;
if (!number || !message) {
return res.status(400).json({ status: 'error', message: 'Nomor dan pesan wajib diisi' });
}
const chatId = formatNumber(number);
try {
const userExists = await client.isRegisteredUser(chatId);
if (!userExists) {
return res.status(404).json({ status: 'error', message: 'Nomor tidak terdaftar di WhatsApp' });
}
addToQueue({ type: 'message', data: { chatId, message } });
res.status(202).json({ status: 'success', message: 'Pesan telah dimasukkan ke dalam antrian untuk dikirim.' });
} catch (error) {
console.error(error);
res.status(500).json({ status: 'error', message: 'Gagal memvalidasi nomor', error: error.message });
}
};
export const sendFile = async (req, res) => {
const { number, caption, filename } = req.body;
if (!number || !filename) {
return res.status(400).json({ status: 'error', message: 'Nomor dan nama file wajib diisi' });
}
const chatId = formatNumber(number);
try {
const userExists = await client.isRegisteredUser(chatId);
if (!userExists) {
return res.status(404).json({ status: 'error', message: 'Nomor tidak terdaftar di WhatsApp' });
}
addToQueue({ type: 'file', data: { chatId, caption: caption || '', filename } });
res.status(202).json({ status: 'success', message: 'File telah dimasukkan ke dalam antrian untuk dikirim.' });
} catch (error) {
console.error(error);
res.status(500).json({ status: 'error', message: 'Gagal memvalidasi nomor', error: error.message });
}
};import express from 'express';
import { sendMessage, sendFile } from '../controllers/messageController.js';
const router = express.Router();
router.post('/send-message', sendMessage);
router.post('/send-file', sendFile);
export default router;import 'dotenv/config';
import express from 'express';
import './whatsapp.js';
import messageRoutes from './routes/messageRoutes.js';
import { apiKeyAuth } from './middleware/auth.js';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => {
res.send('<h1>WhatsApp Engine API (v2)</h1><p>Status: Running</p>');
});
// Semua rute di bawah /api akan dilindungi oleh middleware apiKeyAuth
app.use('/api', apiKeyAuth, messageRoutes);
app.listen(PORT, () => {
console.log(`Server berjalan di port ${PORT}`);
console.log('Tunggu client WhatsApp siap sebelum mengirim pesan...');
});# Definisikan variabel untuk penggunaan berulang
@hostname = http://localhost:3000
@api_path = /api
@target_number = 6281234567890
@api_key = GANTI_DENGAN_KUNCI_RAHASIA_ANDA
### Kirim Pesan Teks
POST {{hostname}}{{api_path}}/send-message
Content-Type: application/json
x-api-key: {{api_key}}
{
"number": "{{target_number}}",
"message": "Halo! Ini pesan dari engine yang aman. 😎"
}
### Kirim File (pastikan file ada di folder public/files)
POST {{hostname}}{{api_path}}/send-file
Content-Type: application/json
x-api-key: {{api_key}}
{
"number": "{{target_number}}",
"caption": "Ini adalah file yang dikirim dengan jeda acak.",
"filename": "contoh-gambar.jpg"
}Tips Keamanan & Praktik Terbaik
Hasil Eksperimen
Eksperimen berjalan stabil dengan penerapan jeda acak antar pesan, terbukti efektif mencegah blokir nomor. Sistem berhasil diuji di lingkungan produksi (VPS) menggunakan PM2, menunjukkan kinerja yang andal dan efisien. Seluruh fungsi, termasuk pengiriman teks dan file, berjalan tanpa error. Sesi login tetap terjaga bahkan setelah server restart, menghilangkan kebutuhan untuk scan QR berulang. Engine ini terbukti menjadi solusi yang solid untuk otomatisasi skala kecil hingga menengah.