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() 
}
)