db = Database::getInstance(); $this->whatsappService = new WhatsAppService(); $this->botService = new BotService(); } catch (Throwable $t) { error_log('[webhook] constructor failed: ' . $t->getMessage()); if (function_exists('writeLog')) { writeLog('ERROR', 'Webhook constructor failed: ' . $t->getMessage(), ['trace' => $t->getTraceAsString()]); } throw $t; } error_log('[webhook] __construct end'); } public function handleRequest() { error_log('[webhook] handleRequest start, method=' . ($_SERVER['REQUEST_METHOD'] ?? 'unknown')); $method = $_SERVER['REQUEST_METHOD']; if ($method === 'GET') { $this->verifyWebhook(); } elseif ($method === 'POST') { $this->processIncomingMessage(); } else { http_response_code(405); echo json_encode(['error' => 'Método no permitido']); } } private function verifyWebhook() { $verifyToken = $_GET['hub_verify_token'] ?? ''; $challenge = $_GET['hub_challenge'] ?? ''; $mode = $_GET['hub_mode'] ?? ''; if ($mode === 'subscribe' && $verifyToken === WEBHOOK_VERIFY_TOKEN) { echo $challenge; exit; } else { http_response_code(403); echo json_encode(['error' => 'Token de verificación inválido']); } } private function processIncomingMessage() { $input = file_get_contents('php://input'); $data = json_decode($input, true); // Registrar webhook en logs $this->logWebhook($input, json_encode(['status' => 'received']), 200); if (!$data || !isset($data['entry'])) { http_response_code(400); echo json_encode(['error' => 'Datos inválidos']); return; } try { foreach ($data['entry'] as $entry) { if (isset($entry['changes'])) { foreach ($entry['changes'] as $change) { if ($change['field'] === 'messages') { $this->processMessages($change['value']); } } } } echo json_encode(['status' => 'success']); } catch (Exception $e) { error_log("Error processing webhook: " . $e->getMessage()); http_response_code(500); echo json_encode(['error' => 'Error interno del servidor']); } } private function processMessages($value) { if (!isset($value['messages'])) { return; } foreach ($value['messages'] as $message) { $phoneNumber = $message['from']; $messageId = $message['id']; $timestamp = $message['timestamp']; // Verificar si ya procesamos este mensaje $existing = $this->db->fetch( "SELECT id FROM conversations WHERE message_id = :message_id", ['message_id' => $messageId] ); if ($existing) { continue; // Ya procesamos este mensaje } // Obtener o crear usuario $user = $this->getUserByPhone($phoneNumber); if (!$user) { $userId = $this->createUser($phoneNumber); $user = $this->getUserById($userId); } // Procesar diferentes tipos de mensaje $messageText = ''; $messageType = 'text'; $mediaUrl = null; if (isset($message['reaction'])) { // Reacción (emoji) a un mensaje anterior $messageType = 'reaction'; $emoji = $message['reaction']['emoji'] ?? ''; $reactionTo = $message['reaction']['message_id'] ?? ''; $messageText = json_encode(['emoji' => $emoji, 'message_id' => $reactionTo]); // Guardar campos específicos para reacciones $extraFields = [ 'reaction_to_message_id' => $reactionTo ? intval($reactionTo) : null, 'reaction_emoji' => $emoji ]; } elseif (isset($message['text'])) { $messageText = $message['text']['body']; $messageType = 'text'; } elseif (isset($message['image'])) { $messageText = $message['image']['caption'] ?? ''; $messageType = 'image'; $mediaUrl = $message['image']['id']; } elseif (isset($message['audio'])) { $messageType = 'audio'; $mediaUrl = $message['audio']['id']; } elseif (isset($message['video'])) { $messageText = $message['video']['caption'] ?? ''; $messageType = 'video'; $mediaUrl = $message['video']['id']; } elseif (isset($message['document'])) { $messageText = $message['document']['filename'] ?? ''; $messageType = 'document'; $mediaUrl = $message['document']['id']; } // Guardar mensaje en base de datos $saveData = [ 'user_id' => $user['id'], 'message_id' => $messageId, 'direction' => 'incoming', 'message_type' => $messageType, 'content' => $messageText, 'media_url' => $mediaUrl, 'status' => 'received', 'is_read' => 0 ]; // Si el mensaje incluye contexto (es respuesta a otro mensaje) if (isset($message['context']) && isset($message['context']['id'])) { $saveData['reply_to_message_id'] = $message['context']['id']; } // Añadir campos extra (si se detectaron) if (isset($extraFields) && is_array($extraFields)) { $saveData = array_merge($saveData, $extraFields); } $this->saveMessage($saveData); // Procesar con bot (protección contra excepciones externas) try { $this->botService->processMessage($user, $messageText, $messageType); } catch (Exception $e) { error_log("Bot processing failed: " . $e->getMessage()); } } // Procesar estados de mensajes (entregado, leído, etc.) if (isset($value['statuses'])) { $this->processMessageStatuses($value['statuses']); } } private function processMessageStatuses($statuses) { foreach ($statuses as $status) { $messageId = $status['id']; $newStatus = $status['status']; // sent, delivered, read, failed $this->db->update( 'conversations', ['status' => $newStatus], 'message_id = :message_id', ['message_id' => $messageId] ); } } private function getUserByPhone($phoneNumber) { return $this->db->fetch( "SELECT * FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber] ); } private function getUserById($userId) { return $this->db->fetch( "SELECT * FROM users WHERE id = :id", ['id' => $userId] ); } private function createUser($phoneNumber) { return $this->db->insert('users', [ 'phone_number' => $phoneNumber, 'status' => 'active', 'created_at' => date('Y-m-d H:i:s') ]); } private function saveMessage($messageData) { return $this->db->insert('conversations', $messageData); } private function logWebhook($requestBody, $responseBody, $statusCode) { if (ENABLE_LOGGING) { $this->db->insert('webhook_logs', [ 'request_body' => $requestBody, 'response_body' => $responseBody, 'status_code' => $statusCode, 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'created_at' => date('Y-m-d H:i:s') ]); } } /** * Procesar un payload (array) recibido manualmente (replay) * Útil para reproducir entradas desde la UI o scripts. */ public function processPayload(array $data) { error_log("processPayload START"); // Registrar como recibido (reproducción) $this->logWebhook(json_encode($data), json_encode(['status' => 'replayed']), 200); if (empty($data) || !isset($data['entry'])) { error_log("processPayload: empty or missing entry"); return false; } foreach ($data['entry'] as $entry) { if (isset($entry['changes'])) { foreach ($entry['changes'] as $change) { error_log("processPayload: processing change field=" . ($change['field'] ?? '')); if (($change['field'] ?? '') === 'messages' && isset($change['value'])) { try { $this->processMessages($change['value']); } catch (Exception $e) { error_log("processPayload: processMessages failed: " . $e->getMessage()); } } } } } error_log("processPayload END"); return true; } } // Procesar la solicitud (solo si no estamos en CLI) if (php_sapi_name() !== 'cli') { try { $webhook = new WhatsAppWebhook(); $webhook->handleRequest(); } catch (Throwable $t) { // Manejar excepciones fatales y errores (Throwable) error_log("Fatal error in webhook: " . $t->getMessage()); error_log($t->getTraceAsString()); if (function_exists('writeLog')) { writeLog('ERROR', 'Fatal error in webhook: ' . $t->getMessage(), ['trace' => $t->getTraceAsString()]); } http_response_code(500); echo json_encode(['error' => 'Error fatal del servidor', 'detail' => $t->getMessage()]); } } ?>