292 lines
9.2 KiB
PHP
Executable File
292 lines
9.2 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* Worker para procesar mensajes de WhatsApp de forma asíncrona
|
|
*
|
|
* Uso:
|
|
* php worker.php [queue_name] [--daemon]
|
|
*
|
|
* Ejemplos:
|
|
* php worker.php messages # Procesa cola 'messages' una vez
|
|
* php worker.php --daemon # Daemon que escucha todas las colas
|
|
* php worker.php media --daemon # Daemon solo para cola 'media'
|
|
*
|
|
* Para ejecutar en producción con supervisor:
|
|
* supervisorctl start whatsapp-worker:*
|
|
*/
|
|
|
|
require_once __DIR__ . '/vendor/autoload.php';
|
|
require_once __DIR__ . '/config/config.php';
|
|
|
|
use WhatsApp\Queue\RedisQueue;
|
|
use Monolog\Logger;
|
|
use Monolog\Handler\StreamHandler;
|
|
use Monolog\Handler\RotatingFileHandler;
|
|
use Monolog\Formatter\LineFormatter;
|
|
|
|
// Configuración
|
|
$queueName = $argv[1] ?? null;
|
|
$isDaemon = in_array('--daemon', $argv) || in_array('-d', $argv);
|
|
$queues = ['messages', 'media', 'notifications']; // Colas a escuchar
|
|
|
|
// Si se especifica una cola, solo escuchar esa
|
|
if ($queueName && $queueName !== '--daemon' && $queueName !== '-d') {
|
|
$queues = [$queueName];
|
|
}
|
|
|
|
// Configurar Monolog
|
|
$logger = new Logger('worker');
|
|
$logPath = __DIR__ . '/logs/worker.log';
|
|
|
|
// Handler para archivo rotativo (mantener últimos 7 días)
|
|
$handler = new RotatingFileHandler($logPath, 7, Logger::DEBUG);
|
|
$formatter = new LineFormatter(
|
|
"[%datetime%] %channel%.%level_name%: %message% %context%\n",
|
|
"Y-m-d H:i:s",
|
|
true,
|
|
true
|
|
);
|
|
$handler->setFormatter($formatter);
|
|
$logger->pushHandler($handler);
|
|
|
|
// Handler para STDOUT en modo no-daemon
|
|
if (!$isDaemon) {
|
|
$consoleHandler = new StreamHandler('php://stdout', Logger::INFO);
|
|
$consoleHandler->setFormatter($formatter);
|
|
$logger->pushHandler($consoleHandler);
|
|
}
|
|
|
|
// Inicializar servicios
|
|
$queue = new RedisQueue(null, $logger);
|
|
$db = Database::getInstance();
|
|
$whatsappService = new WhatsAppService();
|
|
$botService = new BotService();
|
|
|
|
$logger->info("Worker started", [
|
|
'queues' => $queues,
|
|
'daemon' => $isDaemon,
|
|
'pid' => getmypid()
|
|
]);
|
|
|
|
// Manejadores de señales para shutdown graceful
|
|
$shutdown = false;
|
|
pcntl_async_signals(true);
|
|
pcntl_signal(SIGTERM, function() use (&$shutdown, $logger) {
|
|
$logger->info("Received SIGTERM, shutting down gracefully...");
|
|
$shutdown = true;
|
|
});
|
|
pcntl_signal(SIGINT, function() use (&$shutdown, $logger) {
|
|
$logger->info("Received SIGINT, shutting down gracefully...");
|
|
$shutdown = true;
|
|
});
|
|
|
|
/**
|
|
* Procesar un mensaje de la cola
|
|
*/
|
|
function processMessage($message, $queue, $botService, $whatsappService, $db, $logger) {
|
|
$data = $message['data'];
|
|
$attempts = $message['attempts'];
|
|
|
|
try {
|
|
$logger->debug("Processing message", [
|
|
'queue' => $queue,
|
|
'attempts' => $attempts,
|
|
'data' => array_keys($data)
|
|
]);
|
|
|
|
switch ($queue) {
|
|
case 'messages':
|
|
// Procesar mensaje de texto/interactivo
|
|
if (!isset($data['user']) || !isset($data['messageText'])) {
|
|
throw new Exception("Invalid message data: missing user or messageText");
|
|
}
|
|
|
|
$user = $data['user'];
|
|
$messageText = $data['messageText'];
|
|
$messageType = $data['messageType'] ?? 'text';
|
|
|
|
// Procesar con BotService
|
|
$botService->processMessage($user, $messageText, $messageType);
|
|
|
|
$logger->info("Message processed successfully", [
|
|
'user_id' => $user['id'],
|
|
'phone' => $user['phone_number'],
|
|
'type' => $messageType
|
|
]);
|
|
break;
|
|
|
|
case 'media':
|
|
// Procesar descarga de media
|
|
if (!isset($data['media_id']) && !isset($data['media_url'])) {
|
|
throw new Exception("Invalid media data: missing media_id or media_url");
|
|
}
|
|
|
|
$conversationId = $data['conversation_id'] ?? null;
|
|
$mediaId = $data['media_id'] ?? null;
|
|
$mediaUrl = $data['media_url'] ?? null;
|
|
$subdir = $data['subdir'] ?? date('Y/m');
|
|
|
|
$mediaService = new MediaService();
|
|
|
|
if ($mediaId) {
|
|
$result = $mediaService->fetchAndStoreFromGraph($mediaId, $subdir);
|
|
} else {
|
|
$result = $mediaService->fetchAndStoreFromUrl($mediaUrl, $subdir);
|
|
}
|
|
|
|
// Actualizar conversación con archivo local
|
|
if ($result && $conversationId) {
|
|
$update = [];
|
|
if (!empty($result['local_file'])) {
|
|
$update['local_file'] = $result['local_file'];
|
|
}
|
|
if (!empty($result['local_thumb'])) {
|
|
$update['local_thumb'] = $result['local_thumb'];
|
|
}
|
|
|
|
if (!empty($update)) {
|
|
$db->update('conversations', $update, 'id = :id', ['id' => $conversationId]);
|
|
}
|
|
}
|
|
|
|
$logger->info("Media processed successfully", [
|
|
'conversation_id' => $conversationId,
|
|
'media_id' => $mediaId,
|
|
'local_file' => $result['local_file'] ?? null
|
|
]);
|
|
break;
|
|
|
|
case 'notifications':
|
|
// Procesar notificación (enviar push, email, etc.)
|
|
if (!isset($data['type']) || !isset($data['message'])) {
|
|
throw new Exception("Invalid notification data");
|
|
}
|
|
|
|
$type = $data['type'];
|
|
$notificationMessage = $data['message'];
|
|
$userId = $data['user_id'] ?? null;
|
|
|
|
// Aquí puedes integrar servicios de notificación
|
|
// Por ahora solo lo registramos
|
|
$logger->info("Notification processed", [
|
|
'type' => $type,
|
|
'user_id' => $userId,
|
|
'message' => substr($notificationMessage, 0, 100)
|
|
]);
|
|
break;
|
|
|
|
default:
|
|
$logger->warning("Unknown queue type", ['queue' => $queue]);
|
|
}
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$logger->error("Failed to process message", [
|
|
'queue' => $queue,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
'attempts' => $attempts
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loop principal del worker
|
|
*/
|
|
$processedCount = 0;
|
|
$errorCount = 0;
|
|
$maxErrors = 10; // Detener si hay muchos errores consecutivos
|
|
|
|
do {
|
|
try {
|
|
// Procesar mensajes delayed cada 10 iteraciones
|
|
if ($processedCount % 10 === 0) {
|
|
foreach ($queues as $q) {
|
|
$queue->processDelayed($q);
|
|
}
|
|
}
|
|
|
|
// Esperar mensaje de cualquier cola (timeout 5 segundos)
|
|
$message = $queue->pop($queues, 5);
|
|
|
|
if (!$message) {
|
|
// No hay mensajes, continuar esperando
|
|
if (!$isDaemon) {
|
|
// En modo no-daemon, salir si no hay más mensajes
|
|
$logger->info("No more messages, exiting");
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Procesar mensaje
|
|
$success = processMessage(
|
|
$message,
|
|
$message['queue'],
|
|
$botService,
|
|
$whatsappService,
|
|
$db,
|
|
$logger
|
|
);
|
|
|
|
if ($success) {
|
|
$processedCount++;
|
|
$errorCount = 0; // Reset error counter
|
|
} else {
|
|
$errorCount++;
|
|
|
|
// Re-encolar con retry
|
|
if (!$queue->retry($message['queue'], $message, 3)) {
|
|
$logger->critical("Failed to retry message", [
|
|
'queue' => $message['queue']
|
|
]);
|
|
}
|
|
|
|
// Si hay muchos errores consecutivos, detener worker
|
|
if ($errorCount >= $maxErrors) {
|
|
$logger->critical("Too many consecutive errors, stopping worker", [
|
|
'error_count' => $errorCount
|
|
]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Limitar memoria (reiniciar si supera 128MB)
|
|
$memoryUsage = memory_get_usage(true) / 1024 / 1024;
|
|
if ($memoryUsage > 128) {
|
|
$logger->warning("Memory limit reached, restarting worker", [
|
|
'memory_mb' => round($memoryUsage, 2)
|
|
]);
|
|
break;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$logger->error("Worker error", [
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
$errorCount++;
|
|
if ($errorCount >= $maxErrors) {
|
|
break;
|
|
}
|
|
|
|
// Esperar un poco antes de reintentar
|
|
sleep(5);
|
|
}
|
|
|
|
} while ($isDaemon && !$shutdown);
|
|
|
|
// Cleanup
|
|
$queue->disconnect();
|
|
|
|
$logger->info("Worker stopped", [
|
|
'processed' => $processedCount,
|
|
'errors' => $errorCount
|
|
]);
|
|
|
|
exit(0);
|