diff --git a/backend/app/Controllers/SuperAdminController.php b/backend/app/Controllers/SuperAdminController.php
index 469bc53..295668e 100644
--- a/backend/app/Controllers/SuperAdminController.php
+++ b/backend/app/Controllers/SuperAdminController.php
@@ -195,4 +195,61 @@ class SuperAdminController extends BaseController
$response->status(500)->json(['error' => 'Failed to approve billing: ' . $e->getMessage()]);
}
}
+
+ /**
+ * Export WhatsApp chats to a public text file
+ * POST /api/admin/export-chats
+ */
+ public function exportChats(Request $request, Response $response): void
+ {
+ if (!$this->verifySuperAdmin($request, $response)) {
+ return;
+ }
+
+ // Get the active session for the Super Admin company (ID 1)
+ $session = \App\Models\WhatsAppSession::findByCompany(1);
+ if (!$session || $session['status'] !== 'connected') {
+ $response->status(404)->json(['error' => 'Super Admin WhatsApp session not active or connected']);
+ return;
+ }
+
+ // Send request to the WhatsApp gateway to export chats
+ $gatewayUrl = rtrim(getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722', '/');
+ if (substr($gatewayUrl, -4) === '/api') {
+ $exportUrl = substr($gatewayUrl, 0, -4) . '/api/chats/export';
+ } else {
+ $exportUrl = $gatewayUrl . '/api/chats/export';
+ }
+
+ $payload = json_encode([
+ 'session_key' => $session['session_key']
+ ]);
+
+ $ch = curl_init($exportUrl);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+ 'X-Webhook-Secret: ' . getenv('WEBHOOK_SECRET')
+ ]);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+
+ $result = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode === 200) {
+ $data = json_decode($result, true);
+ $response->json([
+ 'status' => 'success',
+ 'message' => 'Chat history exported successfully',
+ 'download_url' => '/whatsapp_chats_history.txt'
+ ]);
+ } else {
+ $err = json_decode($result, true);
+ $errMsg = $err['error'] ?? 'HTTP Code ' . $httpCode;
+ $response->status(500)->json(['error' => 'Failed to export chats: ' . $errMsg]);
+ }
+ }
}
diff --git a/backend/public/admin.html b/backend/public/admin.html
index 9d18ab6..f52b107 100644
--- a/backend/public/admin.html
+++ b/backend/public/admin.html
@@ -580,6 +580,12 @@
+
@@ -806,7 +812,10 @@
save_changes: 'حفظ وترقية الباقة',
saving: 'جاري الحفظ والترقية...',
success_update: 'تم تحديث اشتراك الشركة وترقية الباقة بنجاح!',
- fail_update: 'فشل تفعيل الاشتراك الجديد. يرجى المحاولة لاحقاً.'
+ fail_update: 'فشل تفعيل الاشتراك الجديد. يرجى المحاولة لاحقاً.',
+ export_chats: 'تصدير المحادثات',
+ exporting: 'جاري التصدير...',
+ export_success: 'تم تصدير سجل المحادثات بنجاح!'
},
en: {
logout: 'Log Out',
@@ -835,7 +844,10 @@
save_changes: 'Apply Subscription',
saving: 'Updating...',
success_update: 'Company subscription plan updated successfully!',
- fail_update: 'Failed to update subscription. Please try again.'
+ fail_update: 'Failed to update subscription. Please try again.',
+ export_chats: 'Export Chats',
+ exporting: 'Exporting...',
+ export_success: 'Chat history exported successfully!'
}
};
@@ -858,6 +870,7 @@
duration_days: 30
},
isSubmitting: false,
+ exportLoading: false,
toast: {
show: false,
message: '',
@@ -912,6 +925,32 @@
this.redirectToLogin();
},
+ async exportChatsHistory() {
+ this.exportLoading = true;
+ try {
+ const response = await fetch('/api/admin/export-chats', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.token}`,
+ 'Accept': 'application/json'
+ }
+ });
+
+ const result = await response.json();
+ this.exportLoading = false;
+
+ if (result.status === 'success') {
+ this.showToast(this.t('export_success'), false);
+ window.open(result.download_url, '_blank');
+ } else {
+ this.showToast(result.error || 'Failed to export chats', true);
+ }
+ } catch (err) {
+ this.exportLoading = false;
+ this.showToast('Network error while exporting chats', true);
+ }
+ },
+
async fetchStats() {
try {
const response = await fetch('/api/admin/stats', {
diff --git a/backend/public/index.php b/backend/public/index.php
index 48419f1..d4c345d 100644
--- a/backend/public/index.php
+++ b/backend/public/index.php
@@ -109,6 +109,7 @@ $router->post('/api/otp/send', [\App\Controllers\OTPController::class
$router->get('/api/admin/stats', [\App\Controllers\SuperAdminController::class, 'getStats'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/admin/companies/subscribe', [\App\Controllers\SuperAdminController::class, 'subscribeCompany'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/admin/companies/approve-billing', [\App\Controllers\SuperAdminController::class, 'approveBilling'], [\App\Middlewares\AuthMiddleware::class]);
+$router->post('/api/admin/export-chats', [\App\Controllers\SuperAdminController::class, 'exportChats'], [\App\Middlewares\AuthMiddleware::class]);
// Billing & Subscription Routes
$router->get('/api/plans', [\App\Controllers\BillingController::class, 'getPlans'], [\App\Middlewares\AuthMiddleware::class]);
diff --git a/whatsapp-gateway/baileys-client.js b/whatsapp-gateway/baileys-client.js
index e60190d..74120ad 100644
--- a/whatsapp-gateway/baileys-client.js
+++ b/whatsapp-gateway/baileys-client.js
@@ -1,6 +1,6 @@
const baileys = require('@whiskeysockets/baileys');
const makeWASocket = baileys.default || baileys.makeWASocket || baileys;
-const { useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, downloadMediaMessage, makeCacheableSignalKeyStore } = baileys;
+const { useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, downloadMediaMessage, makeCacheableSignalKeyStore, makeInMemoryStore } = baileys;
const pino = require('pino');
const NodeCache = require('node-cache');
const axios = require('axios');
@@ -11,6 +11,8 @@ 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
+const sessionStores = new Map(); // Store active stores in memory
+const storeIntervals = new Map(); // Store save intervals in memory
// Global retry counter cache — persists across socket reconnects
// This is CRITICAL for E2EE: tracks message retry attempts so Baileys can
@@ -57,6 +59,28 @@ async function startSession(session_key, webhook_url) {
const sessionFolder = path.join(SESSIONS_DIR, session_key);
const { state, saveCreds } = await useMultiFileAuthState(sessionFolder);
+ // Initialize InMemoryStore for chat history
+ const store = makeInMemoryStore({ logger: pino({ level: 'silent' }) });
+ const storeFile = path.join(SESSIONS_DIR, `${session_key}_store.json`);
+ if (fs.existsSync(storeFile)) {
+ try {
+ store.readFromFile(storeFile);
+ console.log(`[Store] Loaded store from file for ${session_key}`);
+ } catch (e) {
+ console.error(`[Store] Failed to load store file for ${session_key}:`, e.message);
+ }
+ }
+
+ // Periodically save store to file every 10 seconds
+ const storeInterval = setInterval(() => {
+ try {
+ store.writeToFile(storeFile);
+ } catch (e) {
+ console.error(`[Store] Failed to write store file for ${session_key}:`, e.message);
+ }
+ }, 10000);
+ storeIntervals.set(session_key, storeInterval);
+
// Fetch the latest WhatsApp Web version to avoid 405 rejection
let version;
try {
@@ -95,6 +119,9 @@ async function startSession(session_key, webhook_url) {
const sock = makeWASocket(socketConfig);
console.log(`[Session] Socket created for ${session_key}`);
+ store.bind(sock.ev);
+ sessionStores.set(session_key, store);
+
sessions.set(session_key, sock);
sock.ev.on('creds.update', saveCreds);
@@ -299,6 +326,13 @@ async function startSession(session_key, webhook_url) {
* Cleanup session: remove from memory and delete auth files
*/
async function cleanupSession(session_key) {
+ const interval = storeIntervals.get(session_key);
+ if (interval) {
+ clearInterval(interval);
+ storeIntervals.delete(session_key);
+ }
+ sessionStores.delete(session_key);
+
const sock = sessions.get(session_key);
if (sock) {
try { sock.end(); } catch (e) { } // Gracefully close socket without logout
@@ -318,6 +352,18 @@ async function cleanupSession(session_key) {
* Disconnect session: logout from WhatsApp and cleanup
*/
async function disconnectSession(session_key) {
+ const interval = storeIntervals.get(session_key);
+ if (interval) {
+ clearInterval(interval);
+ storeIntervals.delete(session_key);
+ }
+ sessionStores.delete(session_key);
+
+ const storeFile = path.join(SESSIONS_DIR, `${session_key}_store.json`);
+ if (fs.existsSync(storeFile)) {
+ try { fs.unlinkSync(storeFile); } catch (e) {}
+ }
+
const sock = sessions.get(session_key);
if (sock) {
try { sock.logout(); } catch (e) { } // best effort logout
@@ -455,6 +501,72 @@ async function checkContact(session_key, phone) {
}
}
+async function exportChatHistory(session_key) {
+ const store = sessionStores.get(session_key);
+ if (!store) {
+ throw new Error(`No store found for session ${session_key}`);
+ }
+
+ const chats = store.chats.all();
+ let outputText = `==================================================\n`;
+ outputText += `سجل محادثات منصة نبيه - جلسة: ${session_key}\n`;
+ outputText += `تاريخ التصدير: ${new Date().toLocaleString('ar-EG')}\n`;
+ outputText += `==================================================\n\n`;
+
+ for (const chat of chats) {
+ const jid = chat.id;
+ const phone = jid.split('@')[0];
+ const name = chat.name || 'عميل غير مسمى';
+
+ // Skip groups or broadcasts
+ if (jid.endsWith('@g.us') || jid.endsWith('@broadcast')) {
+ continue;
+ }
+
+ const messages = store.messages[jid]?.all() || [];
+ if (messages.length === 0) continue;
+
+ outputText += `--------------------------------------------------\n`;
+ outputText += `المحادثة مع: ${name} (${phone})\n`;
+ outputText += `--------------------------------------------------\n`;
+
+ for (const msg of messages) {
+ const fromMe = msg.key.fromMe;
+ const sender = fromMe ? 'المنصة (نبيه)' : name;
+
+ const body = msg.message?.conversation ||
+ msg.message?.extendedTextMessage?.text ||
+ msg.message?.imageMessage?.caption ||
+ msg.message?.videoMessage?.caption || '';
+
+ let dateStr = 'تاريخ غير معروف';
+ if (msg.messageTimestamp) {
+ const timestamp = Number(msg.messageTimestamp) * 1000;
+ dateStr = new Date(timestamp).toLocaleString('ar-EG');
+ }
+
+ if (body) {
+ outputText += `[${dateStr}] ${sender}: ${body}\n`;
+ } else if (msg.message?.audioMessage) {
+ outputText += `[${dateStr}] ${sender}: [رسالة صوتية]\n`;
+ } else if (msg.message?.imageMessage) {
+ outputText += `[${dateStr}] ${sender}: [صورة]\n`;
+ } else {
+ outputText += `[${dateStr}] ${sender}: [رسالة غير معروفة]\n`;
+ }
+ }
+ outputText += `\n\n`;
+ }
+
+ const publicDir = path.join(__dirname, '..', 'backend', 'public');
+ if (!fs.existsSync(publicDir)) {
+ fs.mkdirSync(publicDir, { recursive: true });
+ }
+ const filePath = path.join(publicDir, 'whatsapp_chats_history.txt');
+ fs.writeFileSync(filePath, outputText, 'utf8');
+ return filePath;
+}
+
function getActiveSessions() {
return Array.from(sessions.keys());
}
@@ -464,6 +576,7 @@ module.exports = {
disconnectSession,
sendMessage,
getActiveSessions,
- checkContact
+ checkContact,
+ exportChatHistory
};
diff --git a/whatsapp-gateway/server.js b/whatsapp-gateway/server.js
index 09f75c4..4327821 100644
--- a/whatsapp-gateway/server.js
+++ b/whatsapp-gateway/server.js
@@ -20,7 +20,7 @@ for (const p of envPaths) {
const express = require('express');
const cors = require('cors');
-const { startSession, disconnectSession, sendMessage, getActiveSessions, checkContact } = require('./baileys-client');
+const { startSession, disconnectSession, sendMessage, getActiveSessions, checkContact, exportChatHistory } = require('./baileys-client');
const app = express();
app.use(cors());
@@ -99,6 +99,28 @@ app.post('/api/contacts/check', async (req, res) => {
}
});
+// Export chat history to file
+app.post('/api/chats/export', async (req, res) => {
+ const { session_key } = req.body;
+
+ if (!session_key) {
+ return res.status(400).json({ error: 'Missing session_key' });
+ }
+
+ try {
+ const filePath = await exportChatHistory(session_key);
+ res.json({
+ status: 'success',
+ message: 'Chat history exported successfully',
+ file_name: 'whatsapp_chats_history.txt',
+ path: filePath
+ });
+ } catch (err) {
+ console.error(`Error exporting chats for session ${session_key}:`, err);
+ res.status(500).json({ error: err.message || 'Failed to export chats' });
+ }
+});
+
// Send outbound message
app.post('/api/messages/send', async (req, res) => {
const { session_key, phone, message, media_url, audio, mimetype, image } = req.body;