Pesan di engine-whatsapp mengalir melalui dua pipeline: inbound (pesan masuk dari WhatsApp) dan outbound (pesan keluar dari backend Dazo). Keduanya berakhir di webhook + Socket.IO.
Inbound — pesan masuk
Event messages.upsert
Baileys emit messages.upsert saat ada pesan masuk. Handler di src/service/whatsappService.ts:219-226:
sock.ev.on('messages.upsert', async (m) => {
try {
if (m.type !== 'notify') return; // hanya proses type 'notify'
await IncomingMessageService.handle(sock, m.messages, instanceId);
} catch (error) {
console.error(`Error processing message:`, error);
}
});IncomingMessageService.handle()
src/service/incomingMessageService.ts:22-35 — loop setiap pesan:
- Skip
status@broadcast - Skip
@newsletter - Panggil
processMessage(sock, msg, instanceId)
processMessage()
src/service/incomingMessageService.ts:37-142 — pipeline lengkap:
1. Get content type (getContentType)
2. Skip protocolMessage, senderKeyDistributionMessage
3. Normalisasi @lid → remoteJidAlt
4. jidNormalizedUser() → from
5. Ambil groupMetadata bila @g.us
6. Tentukan chatName (grup: subject; personal: pushName)
7. handleMedia() — unduh media ke public/files/
8. extractContent() — teks/caption/koordinat/contextInfo
9. ChatHistoryService.manageChatHistory() — buat chat_histories bila belum ada
10. Ambil profilePictureUrl (fallback ui-avatars.com)
11. Tentukan participantId
12. Bentuk payload dengan report: false
13. sendWebhook(payload, instanceId, msg.key.id)
14. emit message_upsert ke room instanceIdSkip rules
Pesan dari sumber berikut dilewati — tidak diproses sama sekali:
| Sumber | Skip di |
|---|---|
status@broadcast | incomingMessageService.ts:28 |
@newsletter | incomingMessageService.ts:29 |
protocolMessage | incomingMessageService.ts:65 |
senderKeyDistributionMessage | incomingMessageService.ts:65 |
@lid normalisasi
WhatsApp kadang mengirim JID dengan suffix @lid (internal). Kode menggantinya dengan remoteJidAlt bila tersedia:
// incomingMessageService.ts:41
if (/@lid/.test(msg.key.remoteJid!)) {
if (msg.key.remoteJidAlt) {
msg.key.remoteJid = msg.key.remoteJidAlt;
}
}
const from = jidNormalizedUser(msg.key.remoteJid!);Ada di dua tempat — incomingMessageService.ts dan whatsappService.ts (handler messages.update). Bila mengubah satu, periksa yang lain.
Media download — handleMedia()
src/utils/helper.ts:135-199 — unduh media (image/video/document/audio/sticker) atau simpan thumbnail lokasi:
| Tipe | Aksi |
|---|---|
imageMessage, videoMessage, documentMessage, audioMessage, stickerMessage | downloadContentFromMessage → buffer → tulis ke public/files/<instanceId>_<uuid>.<ext> |
locationMessage | Jika ada jpegThumbnail → tulis buffer ke public/files/<instanceId>_loc_<uuid>.jpeg |
| Lainnya | Skip (return undefined) |
URL publik dibentuk: {APP_URL}/files/<filename>. URL ini masuk ke field text di payload webhook.
Ekstensi file via getExtension():
| Tipe | Ekstensi |
|---|---|
audioMessage | mp3 |
stickerMessage | webp |
| Lainnya | dari mimetype (split /, ambil kedua bagian) |
extractContent()
src/utils/helper.ts:204-272 — ekstrak isi pesan berdasarkan tipe:
| Tipe Baileys | typeMsg | Field diambil |
|---|---|---|
conversation | text | conversation |
extendedTextMessage | extendedtext | text |
imageMessage | image | caption, text = mediaUrl |
videoMessage | video | caption, text = mediaUrl |
documentMessage | document | caption, text = mediaUrl |
audioMessage | audio | text = mediaUrl |
stickerMessage | document | text = mediaUrl |
locationMessage | location | latitude, longitude, name, address, text = thumbnailUrl |
quoteMsg = contextInfo di-JSON-stringify (dari extendedTextMessage.contextInfo atau field contextInfo di tipe lain).
ChatHistoryService.manageChatHistory()
src/service/chatHistory/chatHistoryService.ts:13-104:
- Cari
devicesbyid: instanceId→ dapatstore_id - Jika device tidak ada → return
- Cari
chat_historiesdengan filter:store_id,instance,nohp(format+628...),end_date: null,status: { $ne: 'close' } - Jika tidak ada → buat baru:
id: uuid,status: 'open',chat_status: 'new',chat_start: messageId - Jika ada → skip
Foto profil
if (!isFromMe) {
try {
ppUrl = await sock.profilePictureUrl(from, 'image');
} catch {
ppUrl = 'https://ui-avatars.com/api/?name=' + encodeURIComponent(chatName);
}
}Foto profil gagal → fallback ui-avatars.com dengan nama chat.
Payload webhook (report: false)
src/service/incomingMessageService.ts:109-132:
{
"result": true,
"report": false,
"type": "text", // text | extendedtext | image | video | document | audio | location
"id": "3EB0...", // message id
"fromMe": false,
"number": "628123456789@s.whatsapp.net",
"name": "Nama Chat / Nama Grup",
"text": "isi pesan atau URL media",
"caption": "",
"quoteMsg": "{...}", // contextInfo JSON string
"url": "http://localhost:5002/files/<instance>_<uuid>.jpeg",
"latitude": "undefined",
"longitude": "undefined",
"locationName": "",
"locationAddress": "",
"profilePic": "https://...",
"participantNumber": "628...@s.whatsapp.net",
"participantName": "Push Name",
"timestamp": 1700000000,
"status": "pending",
"instanceID": "0c1a968c-...",
"raw_message": "{...}"
}sendWebhook()
src/utils/helper.ts:17-93 — kirim webhook + emit socket:
- Skip jika
WEBHOOK_URLmengandungwebhook-test(mode dummy) POST {WEBHOOK_URL}/callback_messagedengan payload- Jika backend balas
{ result: false }→ log warning, tidak emit socket - Jika sukses → emit socket event:
report: true→message_status_updatereport: false→getmessage+getmessage_bubble
Socket emit — getmessage
src/utils/helper.ts:71-87:
SocketService.getInstance().emitTo(instanceId, 'getmessage', {
id: numenc, // AES-256-CBC(instanceId + userNumber)
type: 'getmessage',
id_device: userNumber,
instance: instanceId,
text: messageId,
data: responseData, // data dari response backend
});
SocketService.getInstance().emitTo(instanceId, 'getmessage_bubble', { ... }); // identiknumenc = encrypt(instanceId + userNumber, AES-256-CBC, secret, iv). Lihat Security.
Outbound — pesan keluar
Backend Dazo memanggil REST API:
| Endpoint | Fungsi |
|---|---|
POST /api/send-message | Kirim teks |
POST /api/send-media | Kirim image/video/document/audio/link |
POST /api/send-location | Kirim lokasi |
POST /api/send-broadcast | Kirim ke banyak nomor |
sendTextMessage()
src/service/whatsappService.ts:425-487:
getSocket(instanceId)— returnnulljika tidakCONNECTED→ throwformatNumber(number)— normalisasi JIDonWhatsAppvalidation (untuk personal, bukan grup) — throw jika tidak terdaftarpresenceSubscribe→sendPresenceUpdate('composing')→ jeda 1 detik →sendPresenceUpdate('paused')(anti-ban simulation)sock.sendMessage(formattedNumber, { text: message }, options)—options.quotedjika adaquoted_raw_messageemitStatusToSocket()— emitsendmessage+sendmessage_bubble
Anti-ban presence
Pengiriman pesan mensimulasikan presence (typing indicator):
| Tahap | Presence | Jeda |
|---|---|---|
| 1 | composing (untuk teks) / recording (untuk audio PTT) | 1 detik (teks) / random 1-2 detik (media) |
| 2 | paused | — |
Untuk broadcast >1 nomor, ada jeda acak 3-5 detik antar pengiriman:
// whatsappService.ts:717-719
if (numbers.length > 1) {
const delayTime = Math.floor(Math.random() * 2000) + 3000; // 3000-5000ms
await new Promise((resolve) => setTimeout(resolve, delayTime));
}onWhatsApp validation
Untuk nomor personal (bukan grup), engine validasi nomor terdaftar di WhatsApp:
if (!formattedNumber.endsWith('@g.us')) {
const presence = await sock.onWhatsApp(formattedNumber);
if (!presence || presence.length === 0 || !presence[0].exists) {
throw new Error(`Nomor tujuan belum terdaftar Whatsapp`);
}
}sendMediaMessage()
src/service/whatsappService.ts:492-620:
getSocket+formatNumber+onWhatsApp(personal only)- Bentuk
messagePayloadper tipe:
type | Payload |
|---|---|
image | { image: { url }, caption } |
video | { video: { url }, caption } |
document | { document: { url }, mimetype: 'application/pdf', fileName: filename || 'file.pdf', caption } |
audio | { audio: { url }, mimetype: 'audio/mp4', ptt } |
link | getLinkPreview(url) → { text: url, linkPreview: { title, description, ... } } |
- Validasi URL via
isMediaUrlValid(HTTP HEAD, timeout 5 detik) — untukimage/video/document/audio(bukanlink) - Presence:
recordingjika audio PTT, selainnyacomposing sock.sendMessagedenganoptions.quotedjika adaemitStatusToSocket
emitStatusToSocket()
src/service/whatsappService.ts:847-885 — emit sendmessage + sendmessage_bubble ke room instanceId:
const socketPayload = {
id: number,
type: 'sendmessage',
number: 0,
instance: instanceId,
text: pesanId || '',
status: status, // 'sent' | 'error'
role: role || 'user',
data: dataSource,
timestamp: new Date().toISOString(),
};
SocketService.getInstance().emitTo(instanceId, 'sendmessage', socketPayload);
SocketService.getInstance().emitTo(instanceId, 'sendmessage_bubble', socketPayload);Status ACK — messages.update
Baileys emit messages.update saat status ACK pesan berubah. Handler di src/service/whatsappService.ts:231-309:
Status mapping
| Baileys status | String |
|---|---|
1 | pending |
2 | sent |
3 | delivered |
4 | viewed |
5 | played |
Dedupe
const processedUpdates = new Set<string>(); // per sesi, tidak pernah dibersihkan
const dedupeKey = `${update.key.id}-${update.update.status}`;
if (processedUpdates.has(dedupeKey)) continue;
processedUpdates.add(dedupeKey);temp_id lookup
Engine mencari temp_id di chat_inbox berdasarkan pesan_id + instance:
const inbox = await ChatInboxModel.findOne({
pesan_id: update.key.id,
instance: instanceId,
});
const payload = {
result: true,
report: true,
number: from,
temp_id: inbox?.temp_id ?? null,
id: update.key.id,
status: statusStr,
instanceID: instanceId,
};
await sendWebhook(payload, instanceId, update.key.id!);Webhook report: true
Payload update status:
{
"result": true,
"report": true,
"number": "628123456789@s.whatsapp.net",
"temp_id": "temp-abc",
"id": "3EB0...",
"status": "delivered",
"instanceID": "0c1a968c-..."
}Jika backend balas { result: false }, engine tidak emit message_status_update ke Socket.IO.
Broadcast
src/service/whatsappService.ts:685-776 — kirim ke banyak nomor:
- Parse
number(array atau string dipisah koma) - Loop setiap nomor:
- Jeda acak 3-5 detik jika >1 nomor
- Panggil
sendTextMessage(throw jika gagal) - Tangkap error per nomor → push ke
results
- Return:
- 1 nomor → objek tunggal
-
1 nomor → rekap
{ result, count, success, failed, message, details }
Langkah berikutnya
- Detail event Socket.IO? Baca Realtime Events.
- Cara sesi dibuat? Baca Session Lifecycle.
- Detail endpoint kirim pesan? Baca Messaging API.