Files
nabeh/backend/app/Controllers/WhatsAppController.php
2026-05-22 00:27:36 +03:00

325 lines
13 KiB
PHP

<?php
namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Models\WhatsAppSession;
/**
* Handles WhatsApp Session Management and communicates with Baileys Node.js Gateway
*/
class WhatsAppController extends BaseController
{
/**
* Get the current WhatsApp connection status for the company
*/
public function status(Request $request, Response $response)
{
$companyId = $request->company_id; // Added by AuthMiddleware
$session = WhatsAppSession::findOrCreate($companyId);
// Strip sensitive/internal data before sending to frontend
unset($session['phone_hash']);
$response->json([
'status' => 'success',
'data' => $session
]);
}
/**
* Request a new connection/QR code from the Baileys service
*/
public function requestQr(Request $request, Response $response)
{
$companyId = $request->company_id;
$session = WhatsAppSession::findOrCreate($companyId);
// Temporarily set to connecting
WhatsAppSession::updateState($session['id'], ['status' => 'connecting']);
// Call Baileys Node.js Service on port 3722
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
$nodeUrl = 'http://127.0.0.1:3722/api/sessions/start';
$payload = json_encode([
'session_key' => $session['session_key'],
'webhook_url' => $appUrl . '/api/whatsapp/webhook'
]);
$ch = curl_init($nodeUrl);
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, 5);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Note: Even if it fails immediately, the webhook will try to correct the state
if ($httpCode >= 200 && $httpCode < 300) {
$response->json([
'status' => 'success',
'message' => 'Connection requested. Please poll status to get QR code.'
]);
} else {
// Revert state on failure
WhatsAppSession::updateState($session['id'], ['status' => 'disconnected']);
$response->status(500)->json([
'status' => 'error',
'message' => 'Failed to reach WhatsApp Gateway.'
]);
}
}
/**
* Disconnect the current WhatsApp session
*/
public function disconnect(Request $request, Response $response)
{
$companyId = $request->company_id;
$session = WhatsAppSession::findByCompany($companyId);
if ($session && $session['status'] !== 'disconnected') {
// Call Baileys Node.js Service to disconnect
$nodeUrl = 'http://127.0.0.1:3722/api/sessions/disconnect';
$payload = json_encode(['session_key' => $session['session_key']]);
$ch = curl_init($nodeUrl);
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, 5);
curl_exec($ch);
curl_close($ch);
WhatsAppSession::updateState($session['id'], [
'status' => 'disconnected',
'qr_code' => null,
'phone' => null,
'phone_hash' => null
]);
}
$response->json(['status' => 'success', 'message' => 'Session disconnected']);
}
/**
* Webhook called by Baileys Node.js server to sync state
*/
public function webhook(Request $request, Response $response)
{
// Internal Security Check
$secret = $request->getHeader('X-Webhook-Secret');
if ($secret !== getenv('WEBHOOK_SECRET')) {
$response->status(403)->json(['error' => 'Unauthorized webhook access']);
return;
}
$body = $request->getBody();
if (empty($body['session_key']) || empty($body['state'])) {
$response->status(400)->json(['error' => 'Missing session_key or state']);
return;
}
$session = WhatsAppSession::findBySessionKey($body['session_key']);
if (!$session) {
$response->status(404)->json(['error' => 'Session not found']);
return;
}
// Handle message received events
if ($body['state'] === 'message_received') {
if (empty($body['message'])) {
$response->status(400)->json(['error' => 'Missing message payload']);
return;
}
$msgData = $body['message'];
// 1. Find or create the contact in the CRM
$contact = \App\Models\Contact::findByPhone($session['company_id'], $msgData['phone']);
if (!$contact) {
// Determine a fallback name
$contactName = !empty($msgData['name']) ? $msgData['name'] : 'WA-' . substr($msgData['phone'], -4);
\App\Models\Contact::createSecure([
'company_id' => $session['company_id'],
'name' => $contactName,
'phone' => $msgData['phone'],
'notes' => 'Auto-created via incoming WhatsApp message'
]);
}
// 2. Log the incoming message in history log
\App\Models\MessageLog::logMessage([
'company_id' => $session['company_id'],
'session_id' => $session['id'],
'contact_phone' => $msgData['phone'],
'direction' => 'inbound',
'message_type' => 'text',
'message_body' => $msgData['body'],
'whatsapp_message_id' => $msgData['id'],
'status' => 'read'
]);
// 3. Placeholder for Phase 5 Gemini AI auto-reply
if (function_exists('fastcgi_finish_request')) {
$response->status(200);
$response->setHeader('Content-Type', 'application/json; charset=utf-8');
$allowedOrigin = getenv('ALLOWED_ORIGIN') ?: '*';
$response->setHeader('Access-Control-Allow-Origin', $allowedOrigin);
$response->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
$response->sendHeaders();
http_response_code(200);
echo json_encode([
'status' => 'success',
'message' => 'Incoming message logged and auto-reply queued'
], JSON_UNESCAPED_UNICODE);
fastcgi_finish_request();
$this->triggerAutoReply($session, $msgData);
exit;
} else {
// Fallback for environment without PHP-FPM
$this->triggerAutoReply($session, $msgData);
$response->json(['status' => 'success', 'message' => 'Incoming message logged']);
return;
}
}
// Handle connection state sync
$updateData = [
'status' => $body['state'] // 'waiting_qr', 'connected', 'disconnected'
];
if ($body['state'] === 'waiting_qr' && !empty($body['qr_code'])) {
$updateData['qr_code'] = $body['qr_code'];
} elseif ($body['state'] === 'connected') {
$updateData['qr_code'] = null; // Clear QR when connected
if (!empty($body['phone'])) {
$updateData['phone'] = $body['phone'];
}
} elseif ($body['state'] === 'disconnected') {
$updateData['qr_code'] = null;
}
WhatsAppSession::updateState($session['id'], $updateData);
$response->json(['status' => 'success']);
}
/**
* Trigger Gemini AI Auto-Replies or Keyword rules (Phase 5)
*/
private function triggerAutoReply(array $session, array $msgData)
{
try {
$rule = \App\Models\ChatbotRule::findActiveForRule($session['company_id'], $session['id']);
if (!$rule || !$rule['is_active']) {
return;
}
$incomingText = trim($msgData['body']);
if (empty($incomingText)) {
return;
}
$replyText = null;
if ($rule['trigger_type'] === 'keyword') {
$keywords = array_filter(array_map('trim', explode(',', $rule['keyword'])));
$matched = false;
foreach ($keywords as $kw) {
if (mb_stripos($incomingText, $kw) !== false) {
$matched = true;
break;
}
}
if ($matched) {
$replyText = $rule['ai_prompt']; // Under keyword rules, ai_prompt stores the predefined static reply
}
} elseif ($rule['trigger_type'] === 'gemini_ai') {
$apiKey = $rule['gemini_api_key'] ?: getenv('GEMINI_API_KEY');
if (empty($apiKey)) {
error_log("[Chatbot Warning] Gemini API Key is not set globally or for company " . $session['company_id']);
return;
}
$systemPrompt = $rule['ai_prompt'] ?: 'You are a helpful customer support assistant.';
$replyText = \App\Services\GeminiService::generateResponse($apiKey, $systemPrompt, $incomingText);
}
if (!empty($replyText)) {
// Send reply back to the contact via Node.js Gateway
$gatewayUrl = rtrim(getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722', '/');
if (substr($gatewayUrl, -4) === '/api') {
$sendUrl = $gatewayUrl . '/messages/send';
} else {
$sendUrl = $gatewayUrl . '/api/messages/send';
}
$payload = json_encode([
'session_key' => $session['session_key'],
'phone' => $msgData['phone'],
'message' => $replyText
]);
$ch = curl_init($sendUrl);
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, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$status = 'failed';
$errorMsg = null;
$waMsgId = null;
if ($httpCode === 200) {
$status = 'sent';
$resData = json_decode($response, true);
$waMsgId = $resData['data']['key']['id'] ?? null;
} else {
$resData = json_decode($response, true);
$errorMsg = $resData['error'] ?? 'HTTP Code ' . $httpCode;
error_log("[Chatbot Gateway Error] failed to send auto-reply: " . $errorMsg);
}
// Log the outbound auto-reply message
\App\Models\MessageLog::logMessage([
'company_id' => $session['company_id'],
'session_id' => $session['id'],
'contact_phone' => $msgData['phone'],
'direction' => 'outbound',
'message_type' => 'text',
'message_body' => $replyText,
'whatsapp_message_id' => $waMsgId,
'status' => $status,
'error_message' => $errorMsg
]);
}
} catch (\Exception $e) {
error_log("[Chatbot Exception] Error: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
}
}
}