Server Socket.IO berjalan pada port yang sama dengan HTTP (PORT, default 5002), CORS terbuka (*). Diinisialisasi di src/app.ts:32 via SocketService.getInstance().init(server).
Singleton — SocketService
src/service/socketService.ts — singleton via getInstance(). io (Socket.IO server) hanya boleh diinisialisasi sekali:
public init(httpServer: HttpServer) {
this.io = new SocketIOServer(httpServer, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
this.io.on('connection', (socket: Socket) => {
// handler client events
});
}Rooms
| Room | Anggota | Event yang diterima |
|---|---|---|
instanceId (per device) | Frontend yang join_instance | qrcode, device_status, message_upsert, getmessage(+_bubble), sendmessage(+_bubble), message_status_update, updatechatlist(+_bubble) |
store_id (per toko) | Frontend yang join_store | pushnotif |
| global (semua client) | Semua yang connect | updateTeamchat |
Event dari Client
| Event | Payload | Fungsi |
|---|---|---|
join_instance | instanceId (string) | Masuk room device. Server langsung membalas device_status berisi status saat itu |
join_store | storeId (string) | Masuk room store (untuk menerima pushnotif) |
leave_instance | instanceId (string) | Keluar dari room device |
join_instance handler
socket.on('join_instance', (instanceId: string) => {
const roomId = String(instanceId);
socket.join(roomId);
const currentStatus = WhatsAppService.getSessionStatus(roomId);
if (currentStatus) {
socket.emit('device_status', currentStatus); // langsung balas status
}
});Client yang join langsung dapat status device terkini — berguna untuk menampilkan indikator connected/disconnected di UI.
Event dari Server
| Event | Room | Kapan | Isi |
|---|---|---|---|
qrcode | instanceId | QR baru tersedia | { id, qr (base64), message } |
device_status | instanceId | Koneksi berubah / saat join | { id, status: "open"|"close", message, isConnected, isActivated, user } |
message_upsert | instanceId | Pesan masuk diterima | { data: <payload pesan masuk> } |
getmessage | instanceId | Setelah webhook pesan masuk sukses | { id (terenkripsi), type, id_device, instance, text, data } |
getmessage_bubble | instanceId | Sama seperti getmessage | idem |
message_status_update | instanceId | Status ACK pesan berubah | { id (terenkripsi), instance, pesan_id, temp_id, number, status_ack } |
sendmessage | instanceId | Setelah kirim pesan | { id, type, number, instance, text, status, role, data, timestamp } |
sendmessage_bubble | instanceId | Sama seperti sendmessage | idem |
updatechatlist | instanceId | Relay /api/update-chatlist | payload chatlist |
updatechatlist_bubble | instanceId | idem | idem |
pushnotif | store_id | Relay /api/push-notif | payload notifikasi |
updateTeamchat | global | Relay /api/update-teamchat | payload team chat |
qrcode
Dipancarkan saat Baileys memberikan QR baru:
// whatsappService.ts:133-137
SocketService.getInstance().emitTo(instanceId, 'qrcode', {
id: instanceId,
qr: qrBase64, // data:image/png;base64,...
message: 'Silahkan scan QR Code',
});Frontend render QR dari qr (base64 data URL).
device_status
Dipancarkan saat koneksi berubah (open/close) dan saat client join_instance:
// Open
{
"id": "0c1a968c-...",
"status": "open",
"isConnected": true,
"isActivated": true,
"message": "Device Terhubung",
"user": { "name": "Nama Profil WA", "id": "628123456789" }
}
// Close
{
"id": "0c1a968c-...",
"status": "close",
"isConnected": false,
"isActivated": false,
"message": "Koneksi Terputus",
"user": null
}message_upsert
Dipancarkan di processMessage sebelum webhook (async, tidak menunggu hasil webhook):
// incomingMessageService.ts:139-141
SocketService.getInstance().emitTo(instanceId, 'message_upsert', {
data: payload // payload lengkap pesan masuk
});getmessage / getmessage_bubble
Dipancarkan di sendWebhook setelah webhook sukses:
// 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', { ... });id = encrypt(instanceId + userNumber) — AES-256-CBC. Lihat Security.
sendmessage / sendmessage_bubble
Dipancarkan setelah kirim pesan (outbound) via emitStatusToSocket:
// whatsappService.ts:860-870
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(),
};message_status_update
Dipancarkan di sendWebhook setelah webhook status ACK sukses:
// utils/helper.ts:56-68
SocketService.getInstance().emitTo(instanceId, 'message_status_update', {
id: numenc, // terenkripsi
instance: instanceId,
pesan_id: payload.id,
temp_id: payload.temp_id ?? null,
number: userNumber,
status_ack: payload.status, // sent/delivered/viewed/played
});pushnotif
Dipancarkan ke room store_id via NotificationService.handlePushNotification:
// notificationService.ts:82
SocketService.getInstance().emitTo(payload.store_id, 'pushnotif', socketPayload);Payload:
{
"id": "...",
"store_id": "...",
"type": "...",
"roles": ["admin", "cs"], // di-split dari string comma-separated
"team_ids": ["1", "2", "3"], // di-split dari string comma-separated
"created_at": "...",
"notification": { ... }, // di-parse dari JSON string jika perlu
"readers": [],
"data": { ... } // di-parse dari JSON string jika perlu
}updatechatlist / updatechatlist_bubble
Dipancarkan ke room instance:
// notificationService.ts:112-113
SocketService.getInstance().emitTo(payload.instance, 'updatechatlist', socketPayload);
SocketService.getInstance().emitTo(payload.instance, 'updatechatlist_bubble', socketPayload);updateTeamchat
Satu-satunya event global (.emit, bukan .emitTo):
// notificationService.ts:138
SocketService.getInstance().emit('updateTeamchat', socketPayload);Semua client yang connect menerima event ini — tidak perlu join room.
Pola _bubble
Beberapa event dikirim ganda dengan payload identik:
| Event | Pair |
|---|---|
getmessage | getmessage_bubble |
sendmessage | sendmessage_bubble |
updatechatlist | updatechatlist_bubble |
API helper
emitTo(room, event, data)
public emitTo(instanceId: string, eventName: string, data: any) {
if (this.io) {
this.io.to(instanceId).emit(eventName, data);
}
}Kirim ke room tertentu. Dipakai mayoritas event.
emit(event, data) — global
public emit(event: string, data: any) {
if (this.io) {
this.io.emit(event, data);
}
}Kirim ke semua client. Hanya dipakai updateTeamchat.
Contoh client
const socket = io("http://localhost:5002");
// Join room device
socket.emit("join_instance", "0c1a968c-7918-4c29-a524-0ad7c445ee7f");
// Listen QR
socket.on("qrcode", ({ qr, message }) => {
console.log(message);
renderQR(qr); // qr = data:image/png;base64,...
});
// Listen status
socket.on("device_status", (s) => {
console.log(s.status); // "open" | "close"
});
// Listen pesan masuk
socket.on("message_upsert", (m) => {
console.log("Pesan masuk:", m.data.text);
});
// Listen pesan masuk (setelah webhook sukses)
socket.on("getmessage", (m) => {
console.log("Pesan dari:", m.id_device);
});
// Join room store untuk pushnotif
socket.emit("join_store", "store-123");
socket.on("pushnotif", (n) => {
console.log("Notif:", n.type);
});Keamanan
Langkah berikutnya
- Cara pesan masuk? Baca Message Flow.
- Cara sesi dibuat? Baca Session Lifecycle.
- Detail endpoint relay notif? Baca Notification API.