Files
sistema_para_juego/bot_telegram_daemon.php

465 lines
18 KiB
PHP
Executable File

<?php
/**
* Telegram Bot Daemon
* Ejecutar con Supervisor: php bot_telegram_daemon.php
* Este demonio maneja interacciones en tiempo real y procesa mensajes programados y recurrentes.
*/
use TelegramBot\Api\BotApi;
use TelegramBot\Api\Types\Update;
use TelegramBot\Api\Types\Inline\InlineKeyboardMarkup;
use Carbon\Carbon;
require_once __DIR__ . '/vendor/autoload.php';
// Cargar variables de entorno
if (file_exists(__DIR__ . '/.env')) {
$lines = file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') === false) continue;
list($key, $value) = explode('=', $line, 2);
$_ENV[trim($key)] = trim($value);
}
}
// --- Funciones de Ayuda ---
/**
* Obtiene una conexión a la base de datos.
* @return PDO
*/
function getDBConnection() {
$host = $_ENV['DB_HOST'];
$db = $_ENV['DB_NAME'];
$user = $_ENV['DB_USER'];
$pass = $_ENV['DB_PASS'];
$port = $_ENV['DB_PORT'];
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
return new PDO($dsn, $user, $pass, $options);
}
/**
* Procesa el contenido HTML para enviar a Telegram.
* @param string $htmlContent
* @return array ['cleanContent' => string, 'imageUrl' => ?string]
*/
function processContentForTelegram(string $htmlContent): array {
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $htmlContent, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_clear_errors();
$imageUrl = null;
$images = $dom->getElementsByTagName('img');
if ($images->length > 0) {
$imageUrl = $images->item(0)->getAttribute('src');
}
// Limpiar texto: quitar tags HTML y decodificar entidades
$cleanContent = strip_tags($htmlContent);
$cleanContent = html_entity_decode($cleanContent, ENT_QUOTES, 'UTF-8');
$cleanContent = trim($cleanContent);
return ['cleanContent' => $cleanContent, 'imageUrl' => $imageUrl];
}
/**
* Envía un mensaje a un chat de Telegram.
* @param BotApi $bot
* @param string $chatId
* @param string $content
* @param ?string $imageUrl
* @return array|false Respuesta de la API de Telegram o false si falla.
*/
function sendMessageTelegram(BotApi $bot, string $chatId, string $content, ?string $imageUrl = null) {
try {
if ($imageUrl) {
return $bot->sendPhoto($chatId, $imageUrl, $content);
} else {
return $bot->sendMessage($chatId, $content);
}
} catch (\Throwable $e) {
error_log("Error enviando mensaje Telegram a $chatId: " . $e->getMessage());
return false;
}
}
/**
* Calcula la próxima fecha de envío para un mensaje recurrente.
* @param array $recurrence
* @return Carbon
*/
function calculateNextSendTime(array $recurrence): Carbon {
$tz = $_ENV['TIME_ZONE_ENVIOS'] ?? 'UTC';
$time = $recurrence['hora_envio']; // HH:MM:SS
// Iniciar con la hora actual en la zona horaria correcta y luego ajustar la hora del día
$next = Carbon::now($tz)->setTimeFromTimeString($time);
switch ($recurrence['frecuencia']) {
case 'diario':
// Si la hora ya pasó hoy, programar para mañana
if ($next->isPast()) {
$next->addDay();
}
break;
case 'semanal':
$dayOfWeekIso = (int)$recurrence['dia_semana']; // Lunes=1, ..., Domingo=7
// Establecer el día de la semana
$next->dayOfWeekIso($dayOfWeekIso);
// Si la fecha/hora resultante ya pasó esta semana, ir a la semana que viene.
if ($next->isPast()) {
$next->addWeek();
}
break;
case 'mensual':
$dayOfMonth = (int)$recurrence['dia_mes'];
// Si el día actual es mayor que el día programado, o si es el mismo día pero la hora ya pasó,
// primero avanza al próximo mes para evitar errores.
if (Carbon::now($tz)->day > $dayOfMonth || (Carbon::now($tz)->day == $dayOfMonth && $next->isPast())) {
$next->addMonthNoOverflow();
}
// Establece el día del mes. Usar `day()` es más seguro en caso de meses con menos días.
$next->day(min($dayOfMonth, $next->daysInMonth));
break;
}
// Convertir a UTC para la base de datos
return $next->setTimezone('UTC');
}
/**
* Procesa los mensajes programados que están pendientes de envío para Telegram.
* @param PDO $db
* @param BotApi $bot
*/
function processScheduledMessagesTelegram(PDO $db, BotApi $bot) {
echo "[Telegram Scheduler] Verificando mensajes programados..." . PHP_EOL;
$stmt = $db->prepare("SELECT * FROM mensajes_telegram WHERE estado = 'pendiente' AND tipo_envio = 'programado' AND fecha_envio <= UTC_TIMESTAMP()");
$stmt->execute();
$messages = $stmt->fetchAll();
foreach ($messages as $msg) {
echo "[Telegram Scheduler] Procesando mensaje programado ID: {$msg['id']}" . PHP_EOL;
$contentPayload = processContentForTelegram($msg['contenido']);
$telegramResponse = sendMessageTelegram($bot, $msg['chat_id'], $contentPayload['cleanContent'], $contentPayload['imageUrl']);
if ($telegramResponse) {
$updateStmt = $db->prepare("UPDATE mensajes_telegram SET estado = 'enviado', mensaje_telegram_id = ? WHERE id = ?");
$updateStmt->execute([$telegramResponse->getMessageId(), $msg['id']]);
echo "[Telegram Scheduler] Mensaje programado ID: {$msg['id']} enviado." . PHP_EOL;
} else {
$updateStmt = $db->prepare("UPDATE mensajes_telegram SET estado = 'fallido' WHERE id = ?");
$updateStmt->execute([$msg['id']]);
echo "[Telegram Scheduler] Error al enviar mensaje programado ID: {$msg['id']}." . PHP_EOL;
}
}
}
/**
* Procesa los mensajes recurrentes para Telegram.
* @param PDO $db
* @param BotApi $bot
*/
function processRecurringMessagesTelegram(PDO $db, BotApi $bot) {
echo "[Telegram Scheduler] Verificando mensajes recurrentes..." . PHP_EOL;
// 1. Inicializar `proximo_envio` para nuevos mensajes recurrentes
$initStmt = $db->query("SELECT * FROM recurrentes_telegram WHERE proximo_envio IS NULL AND activo = 1");
foreach ($initStmt->fetchAll() as $recurrence) {
$nextSendTime = calculateNextSendTime($recurrence);
$updateStmt = $db->prepare("UPDATE recurrentes_telegram SET proximo_envio = ? WHERE id = ?");
$updateStmt->execute([$nextSendTime->toDateTimeString(), $recurrence['id']]);
echo "[Telegram Scheduler] Inicializado proximo_envio para recurrente ID: {$recurrence['id']}" . PHP_EOL;
}
// 2. Procesar mensajes recurrentes listos para ser enviados
$stmt = $db->prepare("
SELECT r.*, m.contenido, m.chat_id, m.usuario_id
FROM recurrentes_telegram r
JOIN mensajes_telegram m ON r.mensaje_id = m.id
WHERE r.activo = 1 AND r.proximo_envio <= UTC_TIMESTAMP()
");
$stmt->execute();
$recurrences = $stmt->fetchAll();
foreach ($recurrences as $rec) {
echo "[Telegram Scheduler] Procesando mensaje recurrente ID: {$rec['id']}" . PHP_EOL;
$contentPayload = processContentForTelegram($rec['contenido']);
$telegramResponse = sendMessageTelegram($bot, $rec['chat_id'], $contentPayload['cleanContent'], $contentPayload['imageUrl']);
if ($telegramResponse) {
// Registrar este envío específico en el historial
$logStmt = $db->prepare("
INSERT INTO mensajes_telegram (usuario_id, chat_id, contenido, estado, mensaje_telegram_id, fecha_envio, tipo_envio)
VALUES (?, ?, ?, 'enviado', ?, NOW(), 'recurrente_enviado')
");
$logStmt->execute([$rec['usuario_id'], $rec['chat_id'], $rec['contenido'], $telegramResponse->getMessageId()]);
// Calcular y actualizar el próximo envío
$nextSendTime = calculateNextSendTime($rec);
$updateStmt = $db->prepare("UPDATE recurrentes_telegram SET proximo_envio = ? WHERE id = ?");
$updateStmt->execute([$nextSendTime->toDateTimeString(), $rec['id']]);
echo "[Telegram Scheduler] Mensaje recurrente ID: {$rec['id']} enviado. Próximo envío: " . $nextSendTime->toDateTimeString() . PHP_EOL;
} else {
error_log("[Telegram Scheduler] Error al enviar mensaje recurrente ID: {$rec['id']}.");
// No se actualiza el proximo_envio para reintentar en la próxima ejecución si falló
}
}
}
/**
* Registra o actualiza un destinatario de Telegram en la base de datos.
* @param PDO $db
* @param string $id
* @param string $type
* @param ?string $name
* @param ?string $username
*/
function registerTelegramRecipient(PDO $db, $id, $type, $name, $username = null) {
if (empty($id)) return;
$name = $name ?: ($username ?: "Destinatario {$id}");
$stmt = $db->prepare("SELECT id FROM destinatarios_telegram WHERE telegram_id = ?");
$stmt->execute([$id]);
if ($stmt->fetch()) {
// Actualizar
$updateStmt = $db->prepare(
"UPDATE destinatarios_telegram SET nombre = ?, username = ?, ultima_interaccion = NOW() WHERE telegram_id = ?"
);
$updateStmt->execute([$name, $username, $id]);
} else {
// Insertar
$insertStmt = $db->prepare(
"INSERT INTO destinatarios_telegram (telegram_id, tipo, nombre, username, fecha_registro, ultima_interaccion) VALUES (?, ?, ?, ?, NOW(), NOW())"
);
$insertStmt->execute([$id, $type, $name, $username]);
echo "[Recipient Registrar] Nuevo destinatario: {$name} ({$id}) de tipo {$type}" . PHP_EOL;
}
}
// --- Inicio del Bot de Polling ---
$bot = new BotApi($_ENV['TELEGRAM_BOT_TOKEN']);
echo "Telegram Bot iniciado", PHP_EOL;
$offset = 0;
while (true) {
try {
$updates = $bot->getUpdates($offset, 100, 1);
$db = getDBConnection(); // Obtener conexión a DB en cada iteración del bucle (o manejar persistencia)
// --- Procesar mensajes programados y recurrentes ---
processScheduledMessagesTelegram($db, $bot);
processRecurringMessagesTelegram($db, $bot);
foreach ($updates as $update) {
$offset = $update->getUpdateId() + 1;
// --- Registrar/Actualizar Destinatarios ---
$message = $update->getMessage();
$callbackQuery = $update->getCallbackQuery();
$chat = null;
$user = null;
if ($message) {
$chat = $message->getChat();
$user = $message->getFrom();
} elseif ($callbackQuery) {
$chat = $callbackQuery->getMessage()->getChat();
$user = $callbackQuery->getFrom();
}
if ($chat) {
registerTelegramRecipient(
$db,
$chat->getId(),
$chat->getType(),
$chat->getTitle() ?: $chat->getFirstName(),
$chat->getUsername()
);
}
if ($user && (!$chat || $user->getId() != $chat->getId())) {
registerTelegramRecipient(
$db,
$user->getId(),
'usuario',
$user->getFirstName(),
$user->getUsername()
);
}
// Manejar comandos de texto
if ($message && !empty($message->getText())) {
$text = $message->getText();
if (strpos($text, '/') === 0) { // Es un comando
$command = explode(' ', $text)[0];
$chatId = $message->getChat()->getId();
$user = $message->getFrom();
switch ($command) {
case '/start':
// Reutilizar la lógica de bienvenida para el comando /start
sendWelcomeMessage($bot, $chatId, $user);
break;
case '/help':
$responseText = "Comandos disponibles:\n/start - Muestra el mensaje de bienvenida.\n/help - Muestra esta ayuda.";
sendMessageTelegram($bot, $chatId, $responseText);
break;
default:
// Comprobar si es un comando de plantilla
$cmdName = ltrim($command, '/');
$stmt = $db->prepare("SELECT contenido FROM plantillas_telegram WHERE comando = ?");
$stmt->execute([$cmdName]);
$plantilla = $stmt->fetch();
if ($plantilla) {
$contentPayload = processContentForTelegram($plantilla['contenido']);
sendMessageTelegram($bot, $chatId, $contentPayload['cleanContent'], $contentPayload['imageUrl']);
} else {
sendMessageTelegram($bot, $chatId, "Comando no reconocido.");
}
break;
}
}
}
// Manejar callback queries (botones de idioma)
if ($callbackQuery) {
$userId = $callbackQuery->getFrom()->getId();
$username = $callbackQuery->getFrom()->getUsername() ?? $callbackQuery->getFrom()->getFirstName();
$data = $callbackQuery->getData();
echo "Callback recibido de $username: $data", PHP_EOL;
if (strpos($data, 'lang_select_') === 0) {
$langCode = substr($data, strlen('lang_select_'));
try {
// La lógica para guardar el idioma ya está cubierta por registerTelegramRecipient,
// pero aquí actualizamos específicamente 'idioma_detectado'.
$stmt = $db->prepare("UPDATE destinatarios_telegram SET idioma_detectado = ? WHERE telegram_id = ?");
$stmt->execute([$langCode, $userId]);
echo "Preferencia de idioma guardada en BD para $username.", PHP_EOL;
$bot->answerCallbackQuery($callbackQuery->getId(), "✅ Idioma seleccionado: $langCode", true);
} catch (\Exception $e) {
echo "Error guardando preferencia de idioma: " . $e->getMessage(), PHP_EOL;
$bot->answerCallbackQuery($callbackQuery->getId(), "Error guardando preferencia", true);
}
}
}
// Manejar nuevos miembros (bienvenida)
if ($message && $newMembers = $message->getNewChatMembers()) {
foreach ($newMembers as $member) {
sendWelcomeMessage($bot, $message->getChat()->getId(), $member);
}
}
}
usleep(500000); // 0.5 segundos de pausa para no saturar la API
} catch (\Exception $e) {
echo "Error en polling: " . $e->getMessage(), PHP_EOL;
// Log error
try {
$db = getDBConnection();
$stmt = $db->prepare("INSERT INTO logs_telegram (origen, nivel, descripcion, datos_json) VALUES ('bot', 'error', ?, ?)");
$stmt->execute([
"Error en polling: " . $e->getMessage(),
json_encode(['trace' => $e->getTraceAsString()])
]);
} catch (\Exception $ex) {}
sleep(5); // Esperar un poco más en caso de error
}
}
function sendWelcomeMessage($bot, $chatId, $user) {
try {
$db = getDBConnection(); // Obtener conexión a DB
// Obtener configuración de bienvenida
$stmt = $db->query("SELECT * FROM bienvenida_telegram WHERE activo = 1 LIMIT 1");
$config = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$config) return;
// Obtener idiomas activos
$stmt = $db->query("SELECT codigo, nombre, nombre_nativo, bandera FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
$idiomas = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Preparar mensaje
$username = $user->getUsername() ? '@' . $user->getUsername() : $user->getFirstName();
$texto = str_replace('{usuario}', $username, $config['texto']);
// Limpiar HTML simple
$texto = strip_tags($texto);
// Crear botones de idioma
$keyboard = [];
$currentRow = [];
foreach ($idiomas as $lang) {
if (count($currentRow) >= 3) {
$keyboard[] = $currentRow;
$currentRow = [];
}
$label = $lang['bandera'] ?: $lang['nombre'];
$currentRow[] = [
'text' => $label,
'callback_data' => 'lang_select_' . $lang['codigo']
];
}
if (!empty($currentRow)) {
$keyboard[] = $currentRow;
}
// Crear el objeto de markup para los botones
$replyMarkup = new InlineKeyboardMarkup($keyboard);
// Enviar mensaje
$bot->sendMessage(
$chatId,
$texto,
'HTML', // Usar HTML parse mode para el mensaje de bienvenida
false,
null,
$replyMarkup
);
echo "Mensaje de bienvenida enviado a $username", PHP_EOL;
} catch (\Exception $e) {
echo "Error enviando bienvenida: " . $e->getMessage(), PHP_EOL;
}
}