- WhatsAppService($canal): when 'turnero', swaps phoneNumberId to whatsapp_phone_number_id_turnero so outgoing messages actually go from the turnero number (not the main bot) - webhook.php: reads metadata.phone_number_id from the payload and compares it to whatsapp_phone_number_id_turnero; if it matches, saves canal='turnero' and skips bot processing - Previously all incoming messages had canal=NULL/bot so Chat Turnero never saw them, and all outgoing messages from Chat Turnero were sent via the wrong (main bot) number Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
614 lines
27 KiB
PHP
614 lines
27 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) {
|
|
// Soportar payloads con field 'conversations' y 'messages' (WhatsApp puede variar)
|
|
if ($change['field'] === 'conversations' || $change['field'] === 'messages' || isset($change['value']['messages'])) {
|
|
$this->processconversations($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 processconversations($value) {
|
|
// Detectar si este mensaje llegó al número turnero
|
|
$receivingPhoneId = $value['metadata']['phone_number_id'] ?? null;
|
|
$turneroPhoneId = getConfigFromDB('whatsapp_phone_number_id_turnero', '');
|
|
$isTurnero = $receivingPhoneId && $turneroPhoneId && ($receivingPhoneId === $turneroPhoneId);
|
|
|
|
// Aceptar tanto payloads con 'conversations' como con 'messages' (WhatsApp varía según la integración)
|
|
$items = [];
|
|
if (isset($value['conversations']) && is_array($value['conversations'])) {
|
|
$items = $value['conversations'];
|
|
} elseif (isset($value['messages']) && is_array($value['messages'])) {
|
|
$items = $value['messages'];
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
// Extraer nombres de contactos del payload (contacts[].profile.name)
|
|
$contactNames = [];
|
|
if (isset($value['contacts']) && is_array($value['contacts'])) {
|
|
foreach ($value['contacts'] as $contact) {
|
|
$waId = $contact['wa_id'] ?? null;
|
|
$name = $contact['profile']['name'] ?? null;
|
|
if ($waId && $name) {
|
|
$contactNames[$waId] = $name;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($items as $message) {
|
|
// En algunos payloads la estructura key es 'from' y 'id' (mensajes), en otros puede venir distinta; normalizamos
|
|
$phoneNumber = $message['from'] ?? ($message['wa_id'] ?? null);
|
|
$messageId = $message['id'] ?? ($message['message_id'] ?? null);
|
|
$timestamp = $message['timestamp'] ?? null;
|
|
|
|
// Si falta lo crítico, saltar
|
|
if (empty($phoneNumber) || empty($messageId)) {
|
|
continue;
|
|
}
|
|
|
|
// 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);
|
|
$contactName = $contactNames[$phoneNumber] ?? null;
|
|
if (!$user) {
|
|
$userId = $this->createUser($phoneNumber);
|
|
$user = $this->getUserById($userId);
|
|
// Guardar nombre del contacto de WhatsApp al crear usuario
|
|
if ($contactName && $userId) {
|
|
try {
|
|
$this->db->update('users', ['name' => $contactName], 'id = ? AND (name IS NULL OR name = ?)', [$userId, '']);
|
|
$user['name'] = $contactName;
|
|
} catch (Exception $e) {
|
|
error_log('[webhook] Error guardando nombre contacto: ' . $e->getMessage());
|
|
}
|
|
}
|
|
} else {
|
|
// Actualizar nombre si el usuario no tiene uno guardado
|
|
if ($contactName && (empty($user['name']))) {
|
|
try {
|
|
$this->db->update('users', ['name' => $contactName], 'id = ? AND (name IS NULL OR name = ?)', [$user['id'], '']);
|
|
$user['name'] = $contactName;
|
|
} catch (Exception $e) {
|
|
error_log('[webhook] Error actualizando nombre contacto: ' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Procesar diferentes tipos de mensaje
|
|
$messageText = '';
|
|
$messageType = 'text';
|
|
$mediaUrl = null;
|
|
|
|
if (isset($message['reaction']) || (isset($message['type']) && $message['type'] === 'reaction')) {
|
|
// Reacción (emoji) a un mensaje anterior
|
|
$messageType = 'reaction';
|
|
$emoji = $message['reaction']['emoji'] ?? ($message['emoji'] ?? '');
|
|
$reactionTo = $message['reaction']['message_id'] ?? ($message['context']['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['interactive'])) {
|
|
// Interactive replies (list or button) - normalize to text so bot can process
|
|
$messageType = 'text';
|
|
if (isset($message['interactive']['list_reply']['title'])) {
|
|
$messageText = $message['interactive']['list_reply']['title'];
|
|
// If title starts with a number ("1. Option"), convert to the number for menu selection
|
|
if (preg_match('/^\s*(\d+)\b/', $messageText, $m)) {
|
|
$messageText = $m[1];
|
|
}
|
|
} elseif (isset($message['interactive']['button_reply']['title'])) {
|
|
$messageText = $message['interactive']['button_reply']['title'];
|
|
if (preg_match('/^\s*(\d+)\b/', $messageText, $m)) {
|
|
$messageText = $m[1];
|
|
}
|
|
} else {
|
|
$messageText = json_encode($message['interactive']);
|
|
}
|
|
error_log(sprintf('[webhook] interactive received title=%s mapped=%s', substr($message['interactive']['list_reply']['title'] ?? ($message['interactive']['button_reply']['title'] ?? json_encode($message['interactive'])),0,200), substr($messageText,0,200)));
|
|
} elseif (isset($message['text']) || ($message['type'] ?? '') === 'text') {
|
|
$messageText = isset($message['text']['body']) ? $message['text']['body'] : ($message['body'] ?? '');
|
|
$messageType = 'text';
|
|
} elseif (isset($message['image'])) {
|
|
$messageText = $message['image']['caption'] ?? '';
|
|
$messageType = 'image';
|
|
// Preferir media ID (estable) sobre URL (expira en minutos)
|
|
$mediaUrl = $message['image']['id'] ?? ($message['image']['url'] ?? null);
|
|
$mimeType = $message['image']['mime_type'] ?? null;
|
|
$filename = $message['image']['filename'] ?? null;
|
|
|
|
} elseif (isset($message['audio'])) {
|
|
$messageType = 'audio';
|
|
$mediaUrl = $message['audio']['id'] ?? ($message['audio']['url'] ?? null);
|
|
$mimeType = $message['audio']['mime_type'] ?? null;
|
|
$filename = $message['audio']['filename'] ?? null;
|
|
|
|
} elseif (isset($message['video'])) {
|
|
$messageText = $message['video']['caption'] ?? '';
|
|
$messageType = 'video';
|
|
$mediaUrl = $message['video']['id'] ?? ($message['video']['url'] ?? null);
|
|
$mimeType = $message['video']['mime_type'] ?? null;
|
|
$filename = $message['video']['filename'] ?? null;
|
|
|
|
} elseif (isset($message['document'])) {
|
|
$messageText = $message['document']['filename'] ?? '';
|
|
$messageType = 'document';
|
|
$mediaUrl = $message['document']['id'] ?? ($message['document']['url'] ?? null);
|
|
$mimeType = $message['document']['mime_type'] ?? null;
|
|
$filename = $message['document']['filename'] ?? null;
|
|
|
|
} elseif (isset($message['sticker'])) {
|
|
$messageType = 'sticker';
|
|
$mediaUrl = $message['sticker']['id'] ?? ($message['sticker']['url'] ?? null);
|
|
$mimeType = $message['sticker']['mime_type'] ?? null;
|
|
$filename = $message['sticker']['filename'] ?? null;
|
|
|
|
}
|
|
|
|
// Log debug (temporal) para verificar cómo llegan media_url/filename
|
|
error_log(sprintf('[webhook] messageId=%s type=%s mediaUrl=%s filename=%s mime=%s', $messageId, $messageType, $mediaUrl ?? 'NULL', $filename ?? 'NULL', $mimeType ?? 'NULL'));
|
|
|
|
// ── Descargar media localmente de inmediato ──
|
|
// Si hay un media ID (numérico) o URL de Graph, descargarlo y guardarlo en uploads/media/
|
|
$localFile = null;
|
|
$localThumb = null;
|
|
if (!empty($mediaUrl) && in_array($messageType, ['image', 'audio', 'video', 'document', 'sticker'])) {
|
|
try {
|
|
set_time_limit(180); // Tiempo extra para descargar media pesado
|
|
$mediaService = new MediaService();
|
|
$subdir = date('Y/m'); // Organizar por año/mes
|
|
|
|
// Determinar si es un media ID (numérico) o una URL directa
|
|
if (preg_match('/^\d+$/', $mediaUrl)) {
|
|
// Es un media ID de WhatsApp → resolver vía Graph API y descargar
|
|
$stored = $mediaService->fetchAndStoreFromGraph($mediaUrl, $subdir);
|
|
} else {
|
|
// Es una URL directa → descargar directamente
|
|
$stored = $mediaService->fetchAndStoreFromUrl($mediaUrl, $subdir);
|
|
}
|
|
|
|
if (!empty($stored['local_file'])) {
|
|
$localFile = $stored['local_file'];
|
|
error_log("[webhook] Media descargado localmente: {$localFile}");
|
|
}
|
|
if (!empty($stored['local_thumb'])) {
|
|
$localThumb = $stored['local_thumb'];
|
|
}
|
|
} catch (Exception $mediaEx) {
|
|
error_log("[webhook] Error descargando media (se encolará para retry): " . $mediaEx->getMessage());
|
|
// Encolar en media_queue para reintento posterior
|
|
try {
|
|
$this->db->insert('media_queue', [
|
|
'media_id' => preg_match('/^\d+$/', $mediaUrl) ? $mediaUrl : '',
|
|
'media_url' => $mediaUrl,
|
|
'subdir' => date('Y/m'),
|
|
'status' => 'pending',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
error_log("[webhook] Media encolado en media_queue para retry");
|
|
} catch (Exception $qEx) {
|
|
error_log("[webhook] Error encolando media: " . $qEx->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
'canal' => $isTurnero ? 'turnero' : 'bot',
|
|
];
|
|
|
|
// Guardar whatsapp_media_id si el mediaUrl es un ID numérico de WhatsApp
|
|
if (!empty($mediaUrl) && preg_match('/^\d+$/', $mediaUrl)) {
|
|
$saveData['whatsapp_media_id'] = $mediaUrl;
|
|
}
|
|
|
|
// Añadir campos opcionales solo si existen para evitar errores en esquemas antiguos
|
|
if (isset($filename) && $filename !== null) {
|
|
$saveData['filename'] = $filename;
|
|
}
|
|
if (isset($mimeType) && $mimeType !== null) {
|
|
$saveData['mime_type'] = $mimeType;
|
|
}
|
|
// Si se descargó y almacenó localmente, añadir las rutas locales
|
|
if (isset($localFile) && $localFile) {
|
|
$saveData['local_file'] = $localFile;
|
|
}
|
|
if (isset($localThumb) && $localThumb) {
|
|
$saveData['local_thumb'] = $localThumb;
|
|
}
|
|
|
|
// 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'];
|
|
}
|
|
|
|
// Special handling for reaction messages: apply reaction to referenced message instead of creating separate record
|
|
if (isset($extraFields) && is_array($extraFields) && isset($messageType) && $messageType === 'reaction' && !empty($reactionTo)) {
|
|
try {
|
|
// Try to update the referenced message (by message_id or id)
|
|
$updated = $this->db->update(
|
|
'conversations',
|
|
['reaction_emoji' => $emoji, 'reaction_to_message_id' => $reactionTo],
|
|
'message_id = :mid OR id = :mid',
|
|
['mid' => $reactionTo]
|
|
);
|
|
|
|
// Optionally create a small notification about the reaction
|
|
try {
|
|
$this->db->insert('notifications', [
|
|
'user_id' => $user['id'],
|
|
'type' => 'reaction',
|
|
'message' => "Reacción recibida: {$emoji}",
|
|
'data' => json_encode(['message_id' => $reactionTo, 'emoji' => $emoji]),
|
|
'is_read' => 0,
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
} catch (Exception $ne) {
|
|
error_log('Failed to create reaction notification: ' . $ne->getMessage());
|
|
}
|
|
|
|
// We handled the reaction by updating the target message - skip creating a separate reaction record
|
|
continue;
|
|
} catch (Exception $e) {
|
|
// If update failed, fallback to saving the reaction as a message
|
|
error_log('Failed to apply reaction update: ' . $e->getMessage());
|
|
$saveData = array_merge($saveData, $extraFields);
|
|
}
|
|
} else {
|
|
// Añadir campos extra (si se detectaron)
|
|
if (isset($extraFields) && is_array($extraFields)) {
|
|
$saveData = array_merge($saveData, $extraFields);
|
|
}
|
|
}
|
|
|
|
// Filtrar campos según columnas existentes para evitar errores en esquemas antiguos
|
|
$saveData = $this->filterColumns('conversations', $saveData);
|
|
// Guardar mensaje con INSERT IGNORE para deduplicar webhooks duplicados.
|
|
// Si el message_id ya existe (segunda llamada paralela), rowCount=0 y saltamos el bot.
|
|
$conversationId = $this->saveMessage($saveData);
|
|
if (!$conversationId) {
|
|
// Mensaje ya procesado por una llamada concurrente, ignorar
|
|
error_log("[webhook] Duplicate webhook ignorado para message_id={$messageId}");
|
|
continue;
|
|
}
|
|
|
|
// Si el media se encoló (no se pudo descargar), actualizar media_queue con el conversation_id
|
|
if (!empty($mediaUrl) && empty($localFile) && in_array($messageType, ['image', 'audio', 'video', 'document', 'sticker'])) {
|
|
try {
|
|
$this->db->query(
|
|
"UPDATE media_queue SET conversation_id = :cid WHERE media_id = :mid AND conversation_id IS NULL ORDER BY id DESC LIMIT 1",
|
|
['cid' => $conversationId, 'mid' => (preg_match('/^\d+$/', $mediaUrl) ? $mediaUrl : '')]
|
|
);
|
|
} catch (Exception $e) {
|
|
error_log("[webhook] Error actualizando media_queue con conversation_id: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// Crear notificación para UI (nuevo mensaje entrante)
|
|
try {
|
|
$this->db->insert('notifications', [
|
|
'user_id' => $user['id'],
|
|
'type' => 'incoming_message',
|
|
'message' => substr($messageText, 0, 250),
|
|
'data' => json_encode(['message_id' => $messageId]),
|
|
'is_read' => 0,
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
} catch (Exception $e) {
|
|
error_log('Failed to create notification: ' . $e->getMessage());
|
|
}
|
|
|
|
// Empujar evento SSE en tiempo real
|
|
$this->pushSSEEvent('new_message', [
|
|
'user_id' => $user['id'],
|
|
'phone_number' => $user['phone_number'],
|
|
'name' => $user['name'] ?? $from,
|
|
'message' => substr($messageText, 0, 250),
|
|
'message_type' => $messageType,
|
|
'timestamp' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
// Procesar con bot solo si el mensaje llegó al número principal
|
|
if (!$isTurnero) {
|
|
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->processconversationstatuses($value['statuses']);
|
|
}
|
|
}
|
|
|
|
private function processconversationstatuses($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, $sendWelcome = true) {
|
|
$id = $this->db->insert('users', [
|
|
'phone_number' => $phoneNumber,
|
|
'status' => 'active',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
if ($sendWelcome && $id) {
|
|
try {
|
|
// Enviar mensaje de bienvenida usando el BotService (que consulta system_config)
|
|
$this->botService->sendWelcomeMessage($phoneNumber);
|
|
} catch (Throwable $t) {
|
|
error_log('Failed to send welcome message: ' . $t->getMessage());
|
|
}
|
|
}
|
|
|
|
return $id;
|
|
}
|
|
|
|
private function saveMessage($messageData) {
|
|
// INSERT IGNORE: si message_id ya existe (webhook duplicado), no inserta y devuelve 0
|
|
$cols = implode(', ', array_map(fn($c) => "`{$c}`", array_keys($messageData)));
|
|
$placeholders = implode(', ', array_map(fn($c) => ":{$c}", array_keys($messageData)));
|
|
$sql = "INSERT IGNORE INTO `conversations` ({$cols}) VALUES ({$placeholders})";
|
|
$stmt = $this->db->getConnection()->prepare($sql);
|
|
$stmt->execute($messageData);
|
|
if ($stmt->rowCount() === 0) {
|
|
return 0; // duplicado
|
|
}
|
|
return $this->db->getConnection()->lastInsertId();
|
|
}
|
|
|
|
/**
|
|
* Filtrar los datos para dejar solo las columnas que existen en la tabla
|
|
* Evita errores cuando la BD está en un esquema más antiguo
|
|
*/
|
|
private function filterColumns($table, array $data) {
|
|
static $columnsCache = [];
|
|
if (!isset($columnsCache[$table])) {
|
|
$cols = $this->db->fetchAll("SHOW COLUMNS FROM {$table}");
|
|
$columnsCache[$table] = array_map(function($c){ return $c['Field']; }, $cols);
|
|
}
|
|
$allowed = $columnsCache[$table];
|
|
return array_filter($data, function($v, $k) use ($allowed) {
|
|
return in_array($k, $allowed, true);
|
|
}, ARRAY_FILTER_USE_BOTH);
|
|
}
|
|
|
|
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')
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Empujar evento a través de SSE (Server-Sent Events)
|
|
* Notifica a todos los operadores conectados sobre nuevos eventos
|
|
*/
|
|
private function pushSSEEvent($eventType, $data) {
|
|
try {
|
|
// Llamar al endpoint push_event interno
|
|
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
|
|
|
|
$payload = json_encode([
|
|
'event_type' => $eventType,
|
|
'data' => $data,
|
|
'target_user_id' => 'all' // Notificar a todos los operadores
|
|
]);
|
|
|
|
// Hacer llamada asíncrona (fire and forget)
|
|
$ch = curl_init($pushUrl);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT_MS => 500, // Timeout corto
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'X-Push-Token: internal_push_secret_2026'
|
|
]
|
|
]);
|
|
|
|
// Ejecutar sin bloquear
|
|
curl_exec($ch);
|
|
curl_close($ch);
|
|
|
|
} catch (Exception $e) {
|
|
// No fallar si el push falla
|
|
error_log('pushSSEEvent failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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'] ?? '') === 'conversations' || ($change['field'] ?? '') === 'messages' || isset($change['value']['messages'])) && isset($change['value'])) {
|
|
try {
|
|
$this->processconversations($change['value']);
|
|
} catch (Exception $e) {
|
|
error_log("processPayload: processconversations 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()]);
|
|
}
|
|
}
|
|
?>
|