diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..3a7da54
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,20 @@
+# Database Configuration
+DB_HOST=localhost
+DB_NAME=otp_db
+DB_USER=otp_user
+DB_PASS=STRONG_PASSWORD
+
+# Redis Configuration
+REDIS_HOST=127.0.0.1
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=1
+
+# WhatsApp Gateway Configuration
+WHATSAPP_GATEWAY_URL=http://localhost:3732
+WHATSAPP_WEBHOOK_SECRET=flash_call_otp_webhook_secret_key
+WHATSAPP_SESSION_KEY=flash_call_otp
+
+# Application Security Keys
+APP_KEY=f3a9e7c1b8d5f2a4c6e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1
+DEVICE_KEY=er4er4
diff --git a/backend/.htaccess b/backend/.htaccess
deleted file mode 100644
index a3cbe6c..0000000
--- a/backend/.htaccess
+++ /dev/null
@@ -1,27 +0,0 @@
-# Protect includes directory
-
- RewriteEngine On
-
- # Block direct access to includes/
- RewriteRule ^includes/ - [F,L]
-
- # Block access to config files
- RewriteRule ^config\.php$ - [F,L]
-
- # Block access to hidden files
- RewriteRule (^|/)\. - [F,L]
-
- # Block access to SQL files
- RewriteRule \.sql$ - [F,L]
-
- # Block access to log files
- RewriteRule \.log$ - [F,L]
-
-
-# Disable directory listing
-Options -Indexes
-
-# Prevent script execution in includes
-
- php_flag engine off
-
diff --git a/backend/api/request-otp.php b/backend/api/request-otp.php
index d1c43b1..fd9f31d 100644
--- a/backend/api/request-otp.php
+++ b/backend/api/request-otp.php
@@ -102,18 +102,23 @@ $otpCode = str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
// Determine delivery method
$method = 'flash_call'; // Default fallback
-$whatsappAvailable = false;
+$methodInput = isset($input['method']) ? strtolower(trim($input['method'])) : null;
-try {
- $whatsappAvailable = WhatsAppClient::isAvailable($phone);
-} catch (\Throwable $e) {
- error_log('WhatsApp check failed: ' . $e->getMessage());
-}
-
-if ($whatsappAvailable) {
- $method = 'whatsapp';
+if ($methodInput && in_array($methodInput, ['flash_call', 'sms', 'whatsapp', 'telegram'], true)) {
+ $method = $methodInput;
} else {
- $method = ($deviceType === 'ios') ? 'sms' : 'flash_call';
+ $whatsappAvailable = false;
+ try {
+ $whatsappAvailable = WhatsAppClient::isAvailable($phone);
+ } catch (\Throwable $e) {
+ error_log('WhatsApp check failed: ' . $e->getMessage());
+ }
+
+ if ($whatsappAvailable) {
+ $method = 'whatsapp';
+ } else {
+ $method = ($deviceType === 'ios') ? 'sms' : 'flash_call';
+ }
}
$db = Database::getInstance();
diff --git a/backend/composer.json b/backend/composer.json
new file mode 100644
index 0000000..8440648
--- /dev/null
+++ b/backend/composer.json
@@ -0,0 +1,7 @@
+{
+ "name": "intaleq/flash-call-otp",
+ "description": "Flash Call OTP Backend",
+ "require": {
+ "vlucas/phpdotenv": "^5.5"
+ }
+}
diff --git a/whatsapp-gateway/.env.example b/whatsapp-gateway/.env.example
new file mode 100644
index 0000000..669c2f9
--- /dev/null
+++ b/whatsapp-gateway/.env.example
@@ -0,0 +1,2 @@
+PORT=3732
+WEBHOOK_SECRET=flash_call_otp_webhook_secret_key
diff --git a/whatsapp-gateway/baileys-client.js b/whatsapp-gateway/baileys-client.js
new file mode 100644
index 0000000..e60190d
--- /dev/null
+++ b/whatsapp-gateway/baileys-client.js
@@ -0,0 +1,469 @@
+const baileys = require('@whiskeysockets/baileys');
+const makeWASocket = baileys.default || baileys.makeWASocket || baileys;
+const { useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, downloadMediaMessage, makeCacheableSignalKeyStore } = baileys;
+const pino = require('pino');
+const NodeCache = require('node-cache');
+const axios = require('axios');
+const fs = require('fs');
+const path = require('path');
+
+const sessions = new Map(); // Store active sockets in memory
+const retryCounters = new Map(); // Track reconnection attempts per session
+const recentMessages = new Map(); // Cache of recent messages in memory to serve getMessage callback
+const phoneToLid = new Map(); // Map phone numbers to LID JIDs for correct E2EE routing
+
+// Global retry counter cache — persists across socket reconnects
+// This is CRITICAL for E2EE: tracks message retry attempts so Baileys can
+// re-encrypt messages when a recipient's device requests a retry
+const msgRetryCounterCache = new NodeCache({ stdTTL: 600, checkperiod: 120 });
+
+const MAX_RETRIES = 5; // Maximum reconnection attempts before giving up
+
+// Local folder for saving auth keys
+const SESSIONS_DIR = path.join(__dirname, 'sessions');
+if (!fs.existsSync(SESSIONS_DIR)) {
+ fs.mkdirSync(SESSIONS_DIR, { recursive: true });
+}
+
+async function sendWebhook(webhook_url, payload) {
+ try {
+ console.log(`[Webhook] Sending to ${webhook_url} | state=${payload.state}`);
+ const response = await axios.post(webhook_url, payload, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Webhook-Secret': process.env.WEBHOOK_SECRET || ''
+ },
+ timeout: 10000
+ });
+ console.log(`[Webhook] ✅ Success | HTTP ${response.status}`);
+ } catch (err) {
+ if (err.response) {
+ console.error(`[Webhook] ❌ HTTP ${err.response.status} | ${JSON.stringify(err.response.data)}`);
+ } else {
+ console.error(`[Webhook] ❌ Network Error: ${err.message}`);
+ }
+ }
+}
+
+async function startSession(session_key, webhook_url) {
+ // Return existing socket if it's already active
+ if (sessions.has(session_key)) {
+ console.log(`[Session] ${session_key} already active, reusing`);
+ return sessions.get(session_key);
+ }
+
+ console.log(`[Session] Starting ${session_key} → webhook: ${webhook_url}`);
+
+ const sessionFolder = path.join(SESSIONS_DIR, session_key);
+ const { state, saveCreds } = await useMultiFileAuthState(sessionFolder);
+
+ // Fetch the latest WhatsApp Web version to avoid 405 rejection
+ let version;
+ try {
+ const versionInfo = await fetchLatestBaileysVersion();
+ version = versionInfo.version;
+ console.log(`[Baileys] Using WA version: ${version}`);
+ } catch (e) {
+ console.warn(`[Baileys] Could not fetch version, using default`);
+ }
+
+ const logger = pino({ level: 'silent' });
+
+ const socketConfig = {
+ auth: {
+ creds: state.creds,
+ // Wrap keys with makeCacheableSignalKeyStore for fast in-memory
+ // Signal key access — this prevents E2EE key lookup failures
+ // that cause "Waiting for this message" on recipient devices
+ keys: makeCacheableSignalKeyStore(state.keys, logger),
+ },
+ printQRInTerminal: false,
+ logger: logger,
+ browser: ['Nabeh Gateway', 'Chrome', '120.0.0'],
+ // Message retry counter cache — tracks how many times each message
+ // retry has been attempted, preventing infinite retry loops
+ msgRetryCounterCache,
+ getMessage: async (key) => {
+ if (recentMessages.has(key.id)) {
+ return recentMessages.get(key.id);
+ }
+ return undefined;
+ }
+ };
+ if (version) socketConfig.version = version;
+
+ const sock = makeWASocket(socketConfig);
+ console.log(`[Session] Socket created for ${session_key}`);
+
+ sessions.set(session_key, sock);
+
+ sock.ev.on('creds.update', saveCreds);
+
+ // Listen for incoming messages
+ sock.ev.on('messages.upsert', async (m) => {
+ console.log(`[Upsert] Event received type=${m.type} messagesCount=${m.messages?.length}`);
+ if (m.messages && m.messages.length > 0) {
+ console.log(`[Upsert] First message keys:`, Object.keys(m.messages[0]));
+ console.log(`[Upsert] First message key:`, JSON.stringify(m.messages[0].key));
+ console.log(`[Upsert] First message structure:`, JSON.stringify(m.messages[0].message));
+
+ // Cache all incoming messages to serve E2EE retries
+ for (const msg of m.messages) {
+ if (msg.key && msg.key.id && msg.message) {
+ recentMessages.set(msg.key.id, msg.message);
+ if (recentMessages.size > 2000) {
+ const firstKey = recentMessages.keys().next().value;
+ recentMessages.delete(firstKey);
+ }
+ }
+ }
+ }
+
+ if (m.type !== 'notify') return;
+
+ for (const msg of m.messages) {
+ // Ignore messages sent by ourselves
+ if (msg.key.fromMe) continue;
+
+ const remoteJid = msg.key.remoteJid;
+ if (!remoteJid) continue;
+
+ // Only process individual chats (ignore groups and broadcasts)
+ const isGroup = remoteJid.endsWith('@g.us');
+ const isBroadcast = remoteJid.endsWith('@broadcast');
+ if (isGroup || isBroadcast) continue;
+
+ // Extract text body
+ const body = msg.message?.conversation ||
+ msg.message?.extendedTextMessage?.text ||
+ msg.message?.imageMessage?.caption ||
+ msg.message?.videoMessage?.caption || '';
+
+ const isAudio = !!msg.message?.audioMessage;
+ const isImage = !!msg.message?.imageMessage;
+
+ // Only process messages that have text content OR are audio/image messages
+ if (!body && !isAudio && !isImage) continue;
+
+ let audioBase64 = null;
+ let audioMimeType = null;
+ let imageBase64 = null;
+ let imageMimeType = null;
+
+ if (isAudio) {
+ try {
+ console.log(`[Baileys] Downloading audio message for ${remoteJid}`);
+ const buffer = await downloadMediaMessage(
+ msg,
+ 'buffer',
+ {},
+ {
+ logger: pino({ level: 'silent' }),
+ rekey: true
+ }
+ );
+ audioBase64 = buffer.toString('base64');
+ audioMimeType = msg.message.audioMessage.mimetype || 'audio/ogg';
+ } catch (e) {
+ console.error('[Baileys] Failed to download audio message:', e.message);
+ continue; // Skip if audio download fails to prevent empty processing
+ }
+ } else if (isImage) {
+ try {
+ console.log(`[Baileys] Downloading image message for ${remoteJid}`);
+ const buffer = await downloadMediaMessage(
+ msg,
+ 'buffer',
+ {},
+ {
+ logger: pino({ level: 'silent' }),
+ rekey: true
+ }
+ );
+ imageBase64 = buffer.toString('base64');
+ imageMimeType = msg.message.imageMessage.mimetype || 'image/jpeg';
+ } catch (e) {
+ console.error('[Baileys] Failed to download image message:', e.message);
+ continue; // Skip if image download fails
+ }
+ }
+
+ // Extract sender phone number (handle LID privacy scheme)
+ let senderPhone = '';
+ if (msg.key.senderPn && msg.key.senderPn.endsWith('@s.whatsapp.net')) {
+ senderPhone = msg.key.senderPn.split('@')[0];
+ // CRITICAL: Map this phone number to the LID JID for reply routing.
+ // Signal E2EE sessions are bound to the LID, so replies MUST go
+ // to the LID JID, not the phone JID, to prevent session conflicts.
+ if (remoteJid.endsWith('@lid')) {
+ phoneToLid.set(senderPhone, remoteJid);
+ console.log(`[LID] Mapped ${senderPhone} → ${remoteJid}`);
+ }
+ } else if (remoteJid.endsWith('@s.whatsapp.net')) {
+ senderPhone = remoteJid.split('@')[0];
+ } else if (remoteJid.endsWith('@lid')) {
+ senderPhone = remoteJid.split('@')[0];
+ }
+
+ if (!senderPhone) continue;
+
+ const senderName = msg.pushName || '';
+
+ if (isAudio) {
+ console.log(`[Message] Received audio voice note from ${senderPhone} (JID: ${remoteJid})`);
+ } else if (isImage) {
+ console.log(`[Message] Received image from ${senderPhone} (JID: ${remoteJid})`);
+ } else {
+ console.log(`[Message] Received from ${senderPhone} (JID: ${remoteJid}): ${body}`);
+ }
+
+ await sendWebhook(webhook_url, {
+ session_key,
+ state: 'message_received',
+ message: {
+ id: msg.key.id,
+ phone: senderPhone,
+ name: senderName,
+ body: body,
+ audio: audioBase64,
+ mimeType: audioMimeType,
+ duration: msg.message?.audioMessage?.seconds ? Number(msg.message.audioMessage.seconds) : null,
+ image: imageBase64,
+ imageMimeType: imageMimeType,
+ timestamp: msg.messageTimestamp
+ }
+ });
+ }
+ });
+
+ sock.ev.on('connection.update', async (update) => {
+ const { connection, lastDisconnect, qr } = update;
+
+ if (qr) {
+ console.log(`[QR] Generated for ${session_key}`);
+ // Reset retry counter on QR generation (session is alive)
+ retryCounters.set(session_key, 0);
+ await sendWebhook(webhook_url, {
+ session_key,
+ state: 'waiting_qr',
+ qr_code: qr
+ });
+ }
+
+ if (connection === 'close') {
+ const statusCode = lastDisconnect?.error?.output?.statusCode;
+ const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
+
+ if (!shouldReconnect) {
+ console.log(`[Connection] ${session_key} permanently closed (logged out). Cleaning up.`);
+ sessions.delete(session_key);
+ await cleanupSession(session_key);
+ await sendWebhook(webhook_url, {
+ session_key,
+ state: 'disconnected'
+ });
+ } else {
+ const retries = (retryCounters.get(session_key) || 0) + 1;
+ retryCounters.set(session_key, retries);
+
+ sessions.delete(session_key);
+ let delay;
+ if (retries <= MAX_RETRIES) {
+ delay = Math.min(retries * 3000, 15000); // 3s, 6s, 9s, 12s, 15s
+ console.log(`[Connection] Reconnecting ${session_key} (quick retry ${retries}/${MAX_RETRIES}) in ${delay}ms...`);
+ } else {
+ delay = 60000; // 60s
+ console.log(`[Connection] Reconnecting ${session_key} (long-term retry ${retries}) in 60s...`);
+ }
+ setTimeout(() => startSession(session_key, webhook_url), delay);
+ }
+ } else if (connection === 'open') {
+ console.log(`[Connection] ✅ ${session_key} connected successfully!`);
+ retryCounters.set(session_key, 0); // Reset on successful connection
+
+ // Parse phone number from the JID (e.g. 9665XXXXXXX@s.whatsapp.net)
+ const phone = sock.user.id.split(':')[0];
+
+ await sendWebhook(webhook_url, {
+ session_key,
+ state: 'connected',
+ phone: phone
+ });
+ }
+ });
+
+ return sock;
+}
+
+/**
+ * Cleanup session: remove from memory and delete auth files
+ */
+async function cleanupSession(session_key) {
+ const sock = sessions.get(session_key);
+ if (sock) {
+ try { sock.end(); } catch (e) { } // Gracefully close socket without logout
+ sessions.delete(session_key);
+ }
+ retryCounters.delete(session_key);
+
+ // Wipe the auth directory so a fresh session can be created next time
+ const sessionFolder = path.join(SESSIONS_DIR, session_key);
+ if (fs.existsSync(sessionFolder)) {
+ fs.rmSync(sessionFolder, { recursive: true, force: true });
+ console.log(`[Cleanup] Deleted session folder for ${session_key}`);
+ }
+}
+
+/**
+ * Disconnect session: logout from WhatsApp and cleanup
+ */
+async function disconnectSession(session_key) {
+ const sock = sessions.get(session_key);
+ if (sock) {
+ try { sock.logout(); } catch (e) { } // best effort logout
+ sessions.delete(session_key);
+ }
+ retryCounters.delete(session_key);
+
+ // Completely wipe the auth directory
+ const sessionFolder = path.join(SESSIONS_DIR, session_key);
+ if (fs.existsSync(sessionFolder)) {
+ fs.rmSync(sessionFolder, { recursive: true, force: true });
+ console.log(`[Disconnect] Deleted session folder for ${session_key}`);
+ }
+}
+
+const { exec } = require('child_process');
+const os = require('os');
+
+function convertToOggOpus(base64Audio) {
+ return new Promise((resolve, reject) => {
+ const timeId = Date.now() + Math.random().toString(36).substring(7);
+ const inputPath = path.join(os.tmpdir(), `input_${timeId}.tmp`);
+ const outputPath = path.join(os.tmpdir(), `output_${timeId}.ogg`);
+
+ fs.writeFile(inputPath, Buffer.from(base64Audio, 'base64'), (err) => {
+ if (err) return reject(err);
+ exec(`ffmpeg -i ${inputPath} -c:a libopus -y ${outputPath}`, (execErr) => {
+ fs.unlink(inputPath, () => {});
+ if (execErr) return reject(execErr);
+ fs.readFile(outputPath, (readErr, data) => {
+ fs.unlink(outputPath, () => {});
+ if (readErr) return reject(readErr);
+ resolve(data.toString('base64'));
+ });
+ });
+ });
+ });
+}
+
+/**
+ * Send a message using an active session
+ */
+async function sendMessage(session_key, phone, message, mediaUrl = null, audioBase64 = null, mimetype = null, imageBase64 = null) {
+ const sock = sessions.get(session_key);
+ if (!sock) {
+ throw new Error(`Session ${session_key} is not active or connected`);
+ }
+
+ // Use the LID JID if we have a mapping for this phone number.
+ // This is CRITICAL: Signal E2EE sessions are bound to the LID JID,
+ // so replying to the phone JID creates a separate session that
+ // conflicts with the LID session, causing "Waiting for this message".
+ let jid;
+ if (phone.includes('@')) {
+ jid = phone;
+ } else if (phoneToLid.has(phone)) {
+ jid = phoneToLid.get(phone);
+ console.log(`[LID] Routing reply to ${phone} via LID: ${jid}`);
+ } else {
+ jid = `${phone}@s.whatsapp.net`;
+ }
+ let sentMsg;
+
+ if (imageBase64) {
+ const buffer = Buffer.from(imageBase64, 'base64');
+ sentMsg = await sock.sendMessage(jid, {
+ image: buffer,
+ caption: message || ''
+ });
+ } else if (audioBase64) {
+ let finalAudioBase64 = audioBase64;
+ let finalMime = mimetype || 'audio/mp4';
+
+ // If it's MP3, convert to OGG Opus to ensure iPhone PTT compatibility
+ if (finalMime.includes('mpeg') || finalMime.includes('mp3')) {
+ try {
+ console.log(`[Baileys] Converting MP3 to OGG Opus for native PTT...`);
+ finalAudioBase64 = await convertToOggOpus(audioBase64);
+ finalMime = 'audio/ogg; codecs=opus';
+ } catch (err) {
+ console.error(`[Baileys] FFmpeg conversion failed:`, err.message);
+ // Fallback to sending as normal audio if conversion fails
+ finalMime = 'audio/mp4';
+ }
+ }
+
+ const buffer = Buffer.from(finalAudioBase64, 'base64');
+ const isMp3 = finalMime.includes('mpeg') || finalMime.includes('mp3');
+
+ sentMsg = await sock.sendMessage(jid, {
+ audio: buffer,
+ mimetype: finalMime,
+ ptt: !isMp3 // PTT enabled for OGG/MP4, disabled for raw MP3
+ });
+ } else if (mediaUrl) {
+ const ext = mediaUrl.split('.').pop().toLowerCase();
+ if (['jpg', 'jpeg', 'png', 'webp'].includes(ext)) {
+ sentMsg = await sock.sendMessage(jid, { image: { url: mediaUrl }, caption: message });
+ } else if (['mp4', 'mkv', 'avi'].includes(ext)) {
+ sentMsg = await sock.sendMessage(jid, { video: { url: mediaUrl }, caption: message });
+ } else {
+ sentMsg = await sock.sendMessage(jid, { document: { url: mediaUrl }, caption: message, fileName: mediaUrl.split('/').pop() });
+ }
+ } else {
+ sentMsg = await sock.sendMessage(jid, { text: message });
+ }
+
+ // Cache outbound messages for E2EE decryption retries
+ if (sentMsg && sentMsg.key && sentMsg.key.id && sentMsg.message) {
+ recentMessages.set(sentMsg.key.id, sentMsg.message);
+ if (recentMessages.size > 2000) {
+ const firstKey = recentMessages.keys().next().value;
+ recentMessages.delete(firstKey);
+ }
+ }
+
+ return sentMsg;
+}
+
+async function checkContact(session_key, phone) {
+ const sock = sessions.get(session_key);
+ if (!sock) {
+ throw new Error(`Session ${session_key} is not active or connected`);
+ }
+ const jid = phone.includes('@') ? phone : `${phone}@s.whatsapp.net`;
+ try {
+ const result = await sock.onWhatsApp(jid);
+ if (result && result.length > 0) {
+ return result[0];
+ }
+ return { exists: false, jid };
+ } catch (err) {
+ console.error(`[Baileys] Error checking contact ${jid}:`, err.message);
+ throw err;
+ }
+}
+
+function getActiveSessions() {
+ return Array.from(sessions.keys());
+}
+
+module.exports = {
+ startSession,
+ disconnectSession,
+ sendMessage,
+ getActiveSessions,
+ checkContact
+};
+
diff --git a/whatsapp-gateway/package.json b/whatsapp-gateway/package.json
new file mode 100644
index 0000000..5837100
--- /dev/null
+++ b/whatsapp-gateway/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "nabeh-whatsapp-gateway",
+ "version": "1.0.0",
+ "description": "WhatsApp Baileys microservice for Nabeh",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "@whiskeysockets/baileys": "^6.7.9",
+ "axios": "^1.7.2",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "node-cache": "^5.1.2",
+ "pino": "^9.2.0"
+ }
+}
diff --git a/whatsapp-gateway/server.js b/whatsapp-gateway/server.js
new file mode 100644
index 0000000..09f75c4
--- /dev/null
+++ b/whatsapp-gateway/server.js
@@ -0,0 +1,125 @@
+const fs = require('fs');
+const path = require('path');
+const dotenv = require('dotenv');
+
+// Find .env file identically to how PHP bootstrap does it
+const envPaths = [
+ path.join(__dirname, '.env'),
+ path.join(__dirname, '../.env'),
+ path.join(__dirname, '../backend/.env'),
+ path.join(__dirname, '../../../.env')
+];
+
+for (const p of envPaths) {
+ if (fs.existsSync(p)) {
+ dotenv.config({ path: p });
+ console.log(`Loaded environment from ${p}`);
+ break;
+ }
+}
+
+const express = require('express');
+const cors = require('cors');
+const { startSession, disconnectSession, sendMessage, getActiveSessions, checkContact } = require('./baileys-client');
+
+const app = express();
+app.use(cors());
+app.use(express.json({ limit: '50mb' }));
+app.use(express.urlencoded({ limit: '50mb', extended: true }));
+
+const PORT = process.env.PORT || 3722;
+
+// Health check endpoint (Public)
+app.get('/health', (req, res) => {
+ res.json({ status: 'healthy', service: 'Nabeh WhatsApp Gateway' });
+});
+
+// Security Middleware: Protect all /api/ routes
+app.use('/api', (req, res, next) => {
+ const secret = req.header('X-Webhook-Secret');
+ if (!process.env.WEBHOOK_SECRET || secret !== process.env.WEBHOOK_SECRET) {
+ return res.status(403).json({ error: 'Unauthorized gateway access' });
+ }
+ next();
+});
+
+// Start or retrieve a session
+app.post('/api/sessions/start', async (req, res) => {
+ const { session_key, webhook_url } = req.body;
+
+ if (!session_key || !webhook_url) {
+ return res.status(400).json({ error: 'Missing session_key or webhook_url' });
+ }
+
+ try {
+ await startSession(session_key, webhook_url);
+ res.json({ status: 'success', message: 'Session started or retrieved' });
+ } catch (err) {
+ console.error(`Error starting session ${session_key}:`, err);
+ res.status(500).json({ error: 'Failed to start session' });
+ }
+});
+
+// Disconnect and remove a session (e.g., when banned or logged out)
+app.post('/api/sessions/disconnect', async (req, res) => {
+ const { session_key } = req.body;
+
+ if (!session_key) {
+ return res.status(400).json({ error: 'Missing session_key' });
+ }
+
+ try {
+ await disconnectSession(session_key);
+ res.json({ status: 'success', message: 'Session disconnected and cleaned up' });
+ } catch (err) {
+ console.error(`Error disconnecting session ${session_key}:`, err);
+ res.status(500).json({ error: 'Failed to disconnect session' });
+ }
+});
+
+// Get list of active session keys in memory
+app.get('/api/sessions/active', (req, res) => {
+ res.json({ status: 'success', active_sessions: getActiveSessions() });
+});
+
+// Check if contact is on WhatsApp
+app.post('/api/contacts/check', async (req, res) => {
+ const { session_key, phone } = req.body;
+
+ if (!session_key || !phone) {
+ return res.status(400).json({ error: 'Missing session_key or phone' });
+ }
+
+ try {
+ const result = await checkContact(session_key, phone);
+ res.json({ status: 'success', data: result });
+ } catch (err) {
+ console.error(`Error checking contact ${phone} via ${session_key}:`, err);
+ res.status(500).json({ error: err.message || 'Failed to check contact' });
+ }
+});
+
+// Send outbound message
+app.post('/api/messages/send', async (req, res) => {
+ const { session_key, phone, message, media_url, audio, mimetype, image } = req.body;
+
+ if (!session_key || !phone) {
+ return res.status(400).json({ error: 'Missing session_key or phone' });
+ }
+
+ if (!message && !audio && !media_url && !image) {
+ return res.status(400).json({ error: 'Missing message, audio, media_url, or image' });
+ }
+
+ try {
+ const result = await sendMessage(session_key, phone, message, media_url, audio, mimetype, image);
+ res.json({ status: 'success', data: result });
+ } catch (err) {
+ console.error(`Error sending message via ${session_key} to ${phone}:`, err);
+ res.status(500).json({ error: err.message || 'Failed to send message' });
+ }
+});
+
+app.listen(PORT, () => {
+ console.log(`🚀 Nabeh WhatsApp Gateway running on port ${PORT}`);
+});