INSTALL NODE js
sudo apt remove nodejs npm -y
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
check
node -v
npm -v
mkdir wa-api-server
mkdir sessions uploads
cd wa-api-server
npm init -y
sudo apt update
sudo apt install git -y
##npm install @whiskeysockets/baileys express qrcode-terminal multer node-fetch
sudo apt install -y build-essential
npm install express qrcode axios multer @whiskeysockets/baileys@7.0.0-rc.2
npm install mysql2
file server.js
import express from "express"
import axios from "axios"
import fs from "fs"
import path from "path"
import multer from "multer"
import QRCode from "qrcode"
import https from "https" // ⬅️ WAJIB kalau pakai ES Module
import {
default as makeWASocket,
useMultiFileAuthState,
fetchLatestBaileysVersion,
DisconnectReason
} from "@whiskeysockets/baileys"
import { downloadMediaMessage } from "@whiskeysockets/baileys"
const upload = multer({ dest: "uploads/" })
import mysql from "mysql2/promise" // ✅ ganti pakai import, bukan require
const db = mysql.createPool({
host: "localhost",
user: "nokit",
password: "tyughjbnm123",
database: "api-wa",
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
})
console.log("✅ Connected to MySQL")
const app = express()
app.use(express.json())
const PORT = 3000
const qrCodes = {}
// ================== GLOBAL STATE ==================
// { sessionId: { sock, status, user } }
// { sessionId: qr }
// ================== START SESSION ==================
const sessions = {}
async function startSession(sessionId) {
// 🔥 stop socket lama jika ada
if (sessions[sessionId]?.sock) {
try {
await sessions[sessionId].sock.end()
} catch {}
delete sessions[sessionId]
}
const sessionPath = `./sessions/${sessionId}`
const { state, saveCreds } = await useMultiFileAuthState(sessionPath)
const { version } = await fetchLatestBaileysVersion()
const sock = makeWASocket({
version,
auth: state,
printQRInTerminal: false,
browser: ["Chrome", "Linux", "1.0"]
})
sessions[sessionId] = {
sock,
status: "CONNECTING",
user: null
}
sock.ev.on("connection.update", async (update) => {
const { connection, qr, lastDisconnect } = update
const code = lastDisconnect?.error?.output?.statusCode
if (qr) {
qrCodes[sessionId] = qr
sessions[sessionId].status = "QR"
console.log(`📲 QR READY ${sessionId}`)
}
if (connection === "open") {
sessions[sessionId].status = "CONNECTED"
sessions[sessionId].user = sock.user
delete qrCodes[sessionId]
console.log(`✅ CONNECTED ${sessionId}`)
}
if (connection === "close") {
console.log(`❌ CLOSED ${sessionId} code=${code}`)
// 🚨 LOGGED OUT → HAPUS TOTAL
if (code === DisconnectReason.loggedOut) {
console.log(`🧹 LOGGED OUT ${sessionId}`)
delete sessions[sessionId]
delete qrCodes[sessionId]
const sessionPath = `./sessions/${sessionId}`
if (fs.existsSync(sessionPath)) {
fs.rmSync(sessionPath, { recursive: true, force: true })
}
return
}
// ✅ CODE 515 = NORMAL PAIRING RESTART
if (code === 515) {
console.log(`🔁 PAIRING RESTART ${sessionId}`)
setTimeout(() => startSession(sessionId), 1000)
return
}
// 🔄 reconnect biasa (kalau sudah pernah connect)
if (sessions[sessionId]?.status === "CONNECTED") {
console.log(`🔄 RECONNECT ${sessionId}`)
setTimeout(() => startSession(sessionId), 2000)
}
}
})
//// ke atas session conect dll ///
// === LISTENER PESAN MASUK ===
// === SAVE MEDIA FUNCTION ===
async function saveMediaMessage(msg, sessionId) {
const type = Object.keys(msg.message)[0]
const mediaMessage = msg.message[type]
const buffer = await downloadMediaMessage(msg, "buffer", {})
const mimeType = mediaMessage.mimetype
const fileName = `${Date.now()}-${sessionId}.${mimeType.split("/")[1]}`
const filePath = path.join("media", sessionId, fileName)
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, buffer)
return { fileName, filePath, mimeType }
}
sock.ev.on("messages.upsert", async (m) => {
const msg = m.messages[0]
if (!msg.message) return // cuma skip kalau memang kosong
const processedMessages = new Set()
function markProcessed(msgId) {
processedMessages.add(msgId)
// kalau sudah terlalu banyak, hapus sebagian biar nggak makan memori
if (processedMessages.size > 10000) {
const first = processedMessages.values().next().value
processedMessages.delete(first)
}
}
const msgId = msg.key.id
if (processedMessages.has(msgId)) return // sudah diproses
processedMessages.add(msgId)
const fromMe = msg.key.fromMe ? 1 : 0
const from = msg.key.remoteJid
const pushName = msg.pushName || null // Nama WA pengirim
const isGroup = from.endsWith("@g.us")
const groupId = isGroup ? from : null
const sender = isGroup ? msg.key.participant : from
let groupName = null
let text = msg.message.conversation
|| msg.message.extendedTextMessage?.text
|| null
let mediaInfo = null
if (isGroup) {
try {
const metadata = await sock.groupMetadata(groupId)
groupName = metadata.subject
} catch (err) {
console.error("Gagal ambil nama group:", err.message)
}
}
/*
if (remoteJid.endsWith("@g.us")) {
groupId = remoteJid
senderx = msg.key.participant || "unknown@s.whatsapp.net"
const sender = sender.split("@")[0]
// ambil metadata grup
try {
const metadata = await sock.groupMetadata(groupId)
groupName = metadata.subject
} catch (err) {
console.error("❌ Gagal ambil metadata grup:", err)
}
console.log(`📩 Grup: ${groupName} (${groupId}) | Pengirim: ${sender} | Pesan: ${text}`)
} else {
// private chat
sender = remoteJid
console.log(`📩 Private: ${sender} | Pesan: ${text}`)
}
*/
// kalau ada media
if (
msg.message.imageMessage ||
msg.message.videoMessage ||
msg.message.documentMessage ||
msg.message.audioMessage ||
msg.message.stickerMessage
) {
//mediaInfo = await saveMediaMessage(msg, sessionId)
let mediaInfo = null
try {
mediaInfo = await saveMediaMessage(msg, sessionId)
} catch (err) {
console.log("⚠️ Media gagal di-download, skip")
}
if (!text) {
if (msg.message.imageMessage) text = msg.message.imageMessage.caption || "[Gambar]"
else if (msg.message.videoMessage) text = msg.message.videoMessage.caption || "[Video]"
else if (msg.message.documentMessage) text = msg.message.documentMessage.fileName || "[Dokumen]"
else if (msg.message.audioMessage) text = "[Audio]"
else if (msg.message.stickerMessage) text = "[Stiker]"
}
}
console.log(`📩 [${sessionId}] Pesan dari ${from}:`, text)
// simpan ke database
try {
await db.query(
"INSERT INTO messages (session_id, group_id,group_name,sender, sender_name, message, direction, file_name, file_path, mime_type) VALUES (?,?,?,?,?,?, ?, ?, ?, ?)",
[
sessionId,
groupId,
groupName,
sender,
pushName,
text,
fromMe ? "outgoing" : "incoming",
mediaInfo?.fileName || null,
mediaInfo?.filePath || null,
mediaInfo?.mimeType || null,
]
)
console.log(`💾 Pesan tersimpan di DB untuk session ${sessionId}`)
} catch (err) {
console.error("DB insert error:", err.message)
}
//untuk menyimpan status
if (msg.key.remoteJid !== "status@broadcast") return
const sender1 = msg.key.participant?.split("@")[0] || "unknown"
let type = "other"
let content = null
let mediaPath = null
try {
if (msg.message?.extendedTextMessage) {
type = "text"
content = msg.message.extendedTextMessage.text
}
else if (msg.message?.imageMessage) {
type = "image"
const buffer = await downloadMediaMessage(
msg,
"buffer",
{},
{ logger: console }
)
mediaPath = `./media/status/${Date.now()}_${sender1}.jpg`
fs.writeFileSync(mediaPath, buffer)
}
else if (msg.message?.videoMessage) {
type = "video"
const buffer = await downloadMediaMessage(
msg,
"buffer",
{},
{ logger: console }
)
mediaPath = `./media/status/${Date.now()}_${sender1}.mp4`
fs.writeFileSync(mediaPath, buffer)
}
else if (msg.message?.audioMessage) {
type = "audio"
const buffer = await downloadMediaMessage(
msg,
"buffer",
{},
{ logger: console }
)
mediaPath = `./media/status/${Date.now()}_${sender1}.ogg`
fs.writeFileSync(mediaPath, buffer)
}
// simpan ke database
await db.query(
"INSERT INTO statuses (sender, type, content, media_path) VALUES (?, ?, ?, ?)",
[sender1, type, content, mediaPath]
)
} catch (err) {
console.error("❌ Gagal simpan status:", err)
}
try {
const [rows] = await db.query("SELECT webhook_url FROM devices WHERE session_id = ?", [sessionId])
const webhookUrl = rows[0]?.webhook_url
if (webhookUrl) {
await axios.post(webhookUrl, {
sessionId,
from,
text,
raw: msg
})
}
} catch (err) {
console.error("Webhook error:", err.message)
}
})
// === SAVE CREDS ===
sock.ev.on("creds.update", saveCreds)
return sock
}
export { startSession, sessions, qrCodes }
// ================== API ==================
app.post("/login", async (req, res) => {
const { sessionId } = req.body
if (!sessionId) return res.status(400).json({ error: "sessionId required" })
if (!sessions[sessionId]) {
await startSession(sessionId)
}
res.json({
status: "OK",
message: `Scan QR di /qr/${sessionId}`
})
})
app.get("/qr/:sessionId", async (req, res) => {
const qr = qrCodes[req.params.sessionId]
if (!qr) return res.send("QR belum tersedia / sudah login")
const img = await QRCode.toDataURL(qr)
res.send(`
<html>
<body style="text-align:center">
<h2>Scan QR</h2>
<img src="${img}" />
</body>
</html>
`)
})
app.get('/sessions', (req, res) => {
const activeSessions = Object.keys(sessions).map(sessionId => {
return {
sessionId,
status: sessions[sessionId].status || 'unknown',
user: sessions[sessionId].user || null
}
});
res.json(activeSessions);
});
// ✅ Endpoint cek status session
app.get("/status/:sessionId", (req, res) => {
const { sessionId } = req.params;
const session = sessions[sessionId];
if (!session) {
return res.status(404).json({ status: "NOT_FOUND", message: "Session tidak ditemukan" });
}
if (session.status === "CONNECTED") {
return res.json({ status: "CONNECTED", message: "WhatsApp sudah login" });
}
if (session.status === "QR") {
return res.json({ status: "QR", message: "Menunggu scan QR" });
}
if (session.status === "DISCONNECTED") {
return res.json({ status: "DISCONNECTED", message: "WhatsApp terputus, butuh login ulang" });
}
// default
return res.json({ status: session.status || "UNKNOWN" });
});
// ===== API: KIRIM PESAN TEXT =====
app.post("/send-text", async (req, res) => {
const { sessionId, number, message } = req.body
const session = sessions[sessionId]
if (!session || session.status !== "CONNECTED") {
return res.status(400).json({
error: "Session belum CONNECTED"
})
}
const jid = number.includes("@s.whatsapp.net")
? number
: number + "@s.whatsapp.net"
await session.sock.sendMessage(jid, { text: message })
res.json({ status: "success" })
})
// ===== API: KIRIM MEDIA (UPLOAD) =====
app.post("/send-media", upload.single("file"), async (req, res) => {
try {
const { sessionId, number, caption } = req.body
const sock = sessions[sessionId]
if (!sock) return res.status(400).json({ error: "Session tidak ditemukan" })
const file = req.file
const filePath = path.resolve(file.path)
const jid = number.includes("@s.whatsapp.net") ? number : number + "@s.whatsapp.net"
let media = {}
const mime = file.mimetype
if (mime.startsWith("image")) media = { image: fs.readFileSync(filePath), caption }
else if (mime.startsWith("video")) media = { video: fs.readFileSync(filePath), caption }
else media = { document: fs.readFileSync(filePath), fileName: file.originalname, caption }
await sock.sendMessage(jid, media)
fs.unlinkSync(filePath)
res.json({ status: "success", message: "Media sent" })
} catch (err) {
console.error(err)
res.status(500).json({ error: err.toString() })
}
})
// ===== API: KIRIM MEDIA DARI URL =====
app.post("/send-url", async (req, res) => {
try {
const { sessionId, number, url, caption } = req.body
const session = sessions[sessionId]
if (!session || !session.sock) {
return res.status(400).json({ error: "Session tidak ditemukan / belum connect" })
}
const sock = session.sock // ✅ AMBIL SOCKET ASLI
const jid = number.includes("@s.whatsapp.net")
? number
: number + "@s.whatsapp.net"
const response = await axios.get(url, {
responseType: "arraybuffer",
timeout: 20000,
maxContentLength: 20 * 1024 * 1024,
httpsAgent: new https.Agent({ rejectUnauthorized: false })
})
const buffer = Buffer.from(response.data)
const contentType = response.headers["content-type"] || ""
let media
if (contentType.startsWith("image")) {
media = { image: buffer, caption }
} else if (contentType.startsWith("video")) {
media = { video: buffer, caption }
} else {
const fileName = path.basename(url.split("?")[0]) || "file"
media = { document: buffer, fileName, mimetype: contentType, caption }
}
await sock.sendMessage(jid, media)
res.json({ status: "success", message: "Media sent" })
} catch (err) {
console.error("SEND URL ERROR:", err)
res.status(500).json({ error: err.message })
}
})
async function loadAllSessions() {
if (!fs.existsSync("./sessions")) return
const sessionIds = fs.readdirSync("./sessions")
for (const sessionId of sessionIds) {
console.log(`♻ Auto reconnect session: ${sessionId}`)
await startSession(sessionId)
}
}
// ================== RUN ==================
app.listen(PORT, async () => {
console.log(`🚀 WA Multi-session running http://localhost:${PORT}`)
await loadAllSessions()
}
)