D
Engineering

Conventions

Pola dominan adalah class statis service + controller tanpa DI. Single source route di itemRoutes.ts. ES modules tanpa ekstensi .js. formatNumber untuk JID. sendSuccess/sendError helper. Sesi in-memory Map.

engine-whatsapp memakai class statis service + controller pattern. Semua service dan controller berupa class dengan method static — tidak ada dependency injection, tidak ada instance. SocketService adalah pengecualian: singleton via getInstance().

Struktur direktori

text
src/
├── app.ts                      # Entry: Express + HTTP + Socket.IO + Mongo + restore sesi
├── config/
│   ├── config.ts               # Loader env var
│   └── database.ts             # Koneksi MongoDB
├── controller/                 # Validasi request + bentuk response. Tanpa logika Baileys
│   ├── SessionController.ts
│   ├── MessageController.ts
│   ├── GroupController.ts
│   └── NotificationController.ts
├── middleware/
│   └── errorHandler.ts         # Global error handler
├── model/                      # 4 skema Mongoose
│   ├── devices.model.ts
│   ├── admin_devices.model.ts
│   ├── chatInbox.model.ts
│   └── chatHistory.model.ts
├── routes/itemRoutes.ts        # SATU-SATUNYA tempat definisi route (prefix /api)
├── service/
│   ├── whatsappService.ts             # ~886 baris. Inti: sesi, kirim, event listener
│   ├── incomingMessageService.ts      # Pipeline pesan masuk
│   ├── socketService.ts               # Singleton Socket.IO
│   ├── notificationService.ts         # Relay notif + Facebook Pixel
│   ├── chatHistory/chatHistoryService.ts
│   └── devices/deviceServices.ts
└── utils/
    ├── helper.ts                      # sendWebhook, formatNumber, handleMedia, extractContent, enkripsi
    └── responseHandler.ts             # sendSuccess / sendError

Alur lapisan: routes → controller → service → (model | Baileys | webhook | socket).

whatsappService.ts adalah pusat gravitasi repo. Perubahan di sana berdampak luas — baca dulu keseluruhan fungsi terkait sebelum mengedit.

Class statis — tanpa DI

Semua service dan controller memakai class dengan method static:

ts
// controller
export class SessionController {
  static async create(req: Request, res: Response) { ... }
}

// service
export class WhatsAppService {
  private static sessions = new Map<string, SessionData>();
  static async startSession(instanceId: string): Promise<WASocket> { ... }
}

Tidak ada constructor injection, tidak ada instance. Pemanggilan langsung: WhatsAppService.startSession(instance), SessionController.create(req, res).

Pengecualian — SocketService singleton

ts
export class SocketService {
  private static instance: SocketService;
  private io: SocketIOServer | null = null;

  public static getInstance(): SocketService {
    if (!SocketService.instance) {
      SocketService.instance = new SocketService();
    }
    return SocketService.instance;
  }
}

SocketService perlu instance karena io (Socket.IO server) hanya boleh diinisialisasi sekali. Akses via SocketService.getInstance().

ES modules — tanpa ekstensi .js

package.json memakai "type": "module". Jangan menulis ekstensi .js pada import di src/ — TypeScript yang menambahkannya saat build:

ts
import config from '../config/config';        // benar
import config from '../config/config.js';     // salah untuk source

Path alias tidak konsisten — jangan ditiru

tsconfig.json memetakan * → src/*, sehingga import { x } from 'utils/helper' valid. Beberapa file memakai bentuk ini (MessageController.ts:4, whatsappService.ts:23), sisanya memakai relatif.

Gunakan import relatif untuk kode baru — itu bentuk mayoritas dan tidak bergantung pada resolve-tspaths.

Route — single source

src/routes/itemRoutes.ts adalah satu-satunya tempat definisi route. Route baru → daftarkan di sini, tidak di tempat lain:

ts
const router = Router();

// Session
router.post('/new', SessionController.create);
router.get('/qrcode', SessionController.getQR);
router.get('/logout', SessionController.logout);
router.get('/device-info', SessionController.status);

// Messaging
router.post('/send-message', MessageController.sendText);
// ...

export default router;

Di-mount di app.ts:23:

ts
app.use('/api', itemRoutes);
app.use('/files', express.static(path.join(import.meta.dirname, '../public/files')));

Format response — sendSuccess / sendError

Selalu lewat helper, jangan res.json() langsung:

ts
import { sendError, sendSuccess } from '../utils/responseHandler';

sendSuccess(res, res.statusCode, data, 'Pesan sukses');
sendError(res, 400, 'Pesan error');

Shape response standar:

jsonc
// Sukses
{ "success": true,  "message": "...", "data": { } }

// Gagal
{ "success": false, "message": "...", "error": { } }  // `error` disembunyikan saat APP_ENV=production

Penanganan error di controller

Pola yang dipakai: try/catch di setiap method, lalu sendError. Perhatikan bahwa banyak controller memakai res.statusCode (bukan angka literal) sebagai status error:

ts
// Pattern yang ada (inkonsisten — res.statusCode = 200 jika belum diset)
catch (error: any) {
  return sendError(res, res.statusCode, `Gagal: ${error.message}`);
}

instanceId — identitas konsisten

UUID yang mengidentifikasi satu sesi WhatsApp. Dipakai serentak sebagai:

  • nama folder kredensial → session/auth/<instanceId>
  • key pada WhatsAppService.sessions (Map in-memory)
  • nama room Socket.IO
  • kolom id di koleksi devices / admin_devices

Konsistensi keempatnya wajib dijaga. Jangan mengubah salah satu tanpa memperbarui yang lain.

Format nomor — formatNumber()

Selalu lewat formatNumber() di utils/helper.ts. Jangan menyusun JID manual:

ts
export function formatNumber(number: string) {
  // 1. Sudah berakhiran @s.whatsapp.net atau @g.us → biarkan
  if (number.endsWith('@s.whatsapp.net') || number.endsWith('@g.us')) return number;

  // 2. Mengandung '-' atau panjang > 15 digit → grup
  const isGroup = number.includes('-') || number.length > 15;
  if (isGroup) return number + '@g.us';

  // 3. Default: personal → @s.whatsapp.net
  return number.replace(/\D/g, '') + '@s.whatsapp.net';
}

Deteksi grup memakai heuristik: mengandung - atau panjang > 15 digit → @g.us.

Sesi bersifat in-memory

ts
private static sessions = new Map<string, SessionData>();

Konsekuensi yang harus diingat:

  • Tidak bisa PM2 cluster mode atau scaling horizontal tanpa mengubah arsitektur.
  • Kredensial persisten di filesystem (session/auth/<instanceId>), bukan di MongoDB.
  • Restart = semua sesi dimuat ulang dari disk (dengan jeda 2 detik antar sesi).

@lid normalisasi

WhatsApp mengirim JID @lid pada kasus tertentu. Kode menggantinya dengan remoteJidAlt. Ada di dua tempat — whatsappService.ts (messages.update) dan incomingMessageService.ts (processMessage). Bila mengubah satu, periksa yang lain:

ts
// incomingMessageService.ts:41
if (/@lid/.test(msg.key.remoteJid!)) {
  if (msg.key.remoteJidAlt) {
    msg.key.remoteJid = msg.key.remoteJidAlt;
  }
}

// whatsappService.ts:273 (messages.update)
if (/@lid/.test(remoteJid)) {
  if (update.key.remoteJidAlt) {
    update.key.remoteJid = update.key.remoteJidAlt;
  }
  continue;
}

Event socket — pola _bubble

Beberapa event dikirim ganda dengan payload identik untuk konsumen frontend berbeda:

EventPair
getmessagegetmessage_bubble
sendmessagesendmessage_bubble
updatechatlistupdatechatlist_bubble

Selalu emit lewat SocketService.getInstance().emitTo(room, event, data) — jangan pakai this.io.emit() langsung (kecuali updateTeamchat yang global).

Logging — Pino

Log terstruktur via Pino, destination ./wa-logs.txt + stdout:

ts
const mainLogger = pino({
  timestamp: () => `,"time":"${new Date().toJSON()}"`,
  level: config.level_log,   // dari .env
}, pino.destination('./wa-logs.txt'));

Child logger per sesi, tagged {session: "<instanceId>"}:

ts
const deviceLogger = mainLogger.child({ session: instanceId });

Level dari LEVEL_LOG (trace/debug/info/warn/error).

Ada juga console.log debug tercecer di beberapa file (whatsappService.ts, chatHistoryService.ts) — sebagian dikomentari. Jangan menambahkan tanpa koordinasi.

ts
// src/service/myFeature.ts
import MyModel from '../model/myModel';

export class MyFeatureService {
  static async doSomething(instanceId: string) {
    return await MyModel.findOne({ id: instanceId });
  }
}

Daftarkan di controller atau service yang relevan. Route baru → src/routes/itemRoutes.ts.

Anti-pola yang sering muncul

  1. res.statusCode sebagai status error — bisa 200 untuk error. Gunakan eksplisit.
  2. Kredensial enkripsi hard-coded di utils/helper.ts — lihat Security.
  3. Import path alias utils/helper — gunakan relatif untuk kode baru.
  4. console.log debug tercecer — sebagian dikomentari, jangan tambah.
  5. Sesi in-memory — tidak bisa cluster mode. Lihat Deployment.

Langkah berikutnya