From d01d0b932b5b79ca8405e284bb0f619d33453ec0 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 1 Aug 2026 22:40:27 +0300 Subject: [PATCH] Deploy: 2026-08-01 22:40:27 --- .../app/Controllers/WhatsAppController.php | 2 + backend/app/Models/WhatsAppSession.php | 99 +++++++++++++++++-- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/backend/app/Controllers/WhatsAppController.php b/backend/app/Controllers/WhatsAppController.php index 4b7bbaa..83c6465 100644 --- a/backend/app/Controllers/WhatsAppController.php +++ b/backend/app/Controllers/WhatsAppController.php @@ -152,6 +152,7 @@ class WhatsAppController extends BaseController 'status' => 'disconnected' ]); + WhatsAppSession::clearSessionsCache($companyId); $newSession = WhatsAppSession::findSecure((int)$id); $response->json([ @@ -197,6 +198,7 @@ class WhatsAppController extends BaseController curl_close($ch); WhatsAppSession::delete((int)$sessionId); + WhatsAppSession::clearSessionsCache($companyId); $response->json(['status' => 'success', 'message' => 'Session deleted successfully']); } diff --git a/backend/app/Models/WhatsAppSession.php b/backend/app/Models/WhatsAppSession.php index 179e058..49e1b27 100644 --- a/backend/app/Models/WhatsAppSession.php +++ b/backend/app/Models/WhatsAppSession.php @@ -3,22 +3,96 @@ namespace App\Models; use App\Core\Security; - use App\Core\Database; +use App\Core\Cache; /** * WhatsAppSession Model - * Handles the whatsapp_sessions table with encryption for phone and QR code. + * Handles the whatsapp_sessions table with encryption for phone and QR code, + * Redis caching of connected sessions, and Round-Robin distribution for outbound messages. */ class WhatsAppSession extends BaseModel { protected static string $table = 'whatsapp_sessions'; /** - * Get the session for a specific company + * Clear cached connected sessions for a company */ - public static function findByCompany(int $companyId) + public static function clearSessionsCache(int $companyId): void { + Cache::delete("connected_sessions:company_{$companyId}"); + } + + /** + * Get all active connected sessions for a company (cached via Redis if available) + */ + public static function getConnectedSessionsForCompany(int $companyId): array + { + $cacheKey = "connected_sessions:company_{$companyId}"; + $cached = Cache::get($cacheKey); + if ($cached !== null && is_array($cached)) { + return $cached; + } + + $sessions = Database::select( + "SELECT * FROM " . static::$table . " WHERE company_id = ? AND status = 'connected' ORDER BY id ASC", + [$companyId] + ); + + foreach ($sessions as &$session) { + $session['phone'] = $session['phone'] ? Security::decrypt($session['phone']) : null; + $session['qr_code'] = $session['qr_code'] ? Security::decrypt($session['qr_code']) : null; + } + + Cache::set($cacheKey, $sessions, 3000); // Cache for 50 minutes + return $sessions; + } + + /** + * Get next connected session using Round-Robin strategy via Redis + */ + public static function getRoundRobinSession(int $companyId): ?array + { + $connectedSessions = static::getConnectedSessionsForCompany($companyId); + + if (empty($connectedSessions)) { + // Fallback to legacy single session if no connected sessions are found + return static::findByCompany($companyId, false); + } + + $count = count($connectedSessions); + if ($count === 1) { + return $connectedSessions[0]; + } + + $rrKey = "rr_index:company_{$companyId}"; + $currentIndex = Cache::get($rrKey); + if ($currentIndex === null || !is_numeric($currentIndex)) { + $currentIndex = 0; + } else { + $currentIndex = (int)$currentIndex; + } + + $selectedIndex = $currentIndex % $count; + $nextIndex = ($selectedIndex + 1) % $count; + + Cache::set($rrKey, $nextIndex, 86400); + + return $connectedSessions[$selectedIndex]; + } + + /** + * Get the session for a specific company (defaults to Round-Robin among connected sessions) + */ + public static function findByCompany(int $companyId, bool $useRoundRobin = true) + { + if ($useRoundRobin) { + $session = static::getRoundRobinSession($companyId); + if ($session) { + return $session; + } + } + $session = Database::selectOne( "SELECT * FROM " . static::$table . " WHERE company_id = ? LIMIT 1", [$companyId] @@ -110,7 +184,7 @@ class WhatsAppSession extends BaseModel */ public static function findOrCreate(int $companyId, string $name = 'Main WhatsApp') { - $session = static::findByCompany($companyId); + $session = static::findByCompany($companyId, false); if ($session) { return $session; } @@ -124,11 +198,12 @@ class WhatsAppSession extends BaseModel 'status' => 'disconnected' ]); - return static::findByCompany($companyId); + static::clearSessionsCache($companyId); + return static::findByCompany($companyId, false); } /** - * Update session state securely + * Update session state securely and clear Redis cache */ public static function updateState(int $id, array $data) { @@ -141,6 +216,14 @@ class WhatsAppSession extends BaseModel $data['qr_code'] = Security::encrypt($data['qr_code']); } - return static::update($id, $data); + $session = Database::selectOne("SELECT company_id FROM " . static::$table . " WHERE id = ? LIMIT 1", [$id]); + $result = static::update($id, $data); + + if ($session && !empty($session['company_id'])) { + static::clearSessionsCache((int)$session['company_id']); + } + + return $result; } } +