284 lines
10 KiB
PHP
284 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Webhook para recibir mensajes de WhatsApp
|
|
* Fecha: 13 de noviembre de 2025
|
|
*/
|
|
|
|
require_once __DIR__ . '/../config/config.php';
|
|
|
|
|
|
|
|
// Headers para API
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: GET, POST');
|
|
header('Access-Control-Allow-Headers: Content-Type');
|
|
|
|
class WhatsAppWebhook {
|
|
private $db;
|
|
private $whatsappService;
|
|
private $botService;
|
|
|
|
public function __construct() {
|
|
error_log('[webhook] __construct start');
|
|
try {
|
|
$this->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]);
|
|
} 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
|
|
$this->saveMessage([
|
|
'user_id' => $user['id'],
|
|
'message_id' => $messageId,
|
|
'direction' => 'incoming',
|
|
'message_type' => $messageType,
|
|
'content' => $messageText,
|
|
'media_url' => $mediaUrl,
|
|
'status' => 'received'
|
|
]);
|
|
|
|
// 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()]);
|
|
}
|
|
}
|
|
?>
|