465 lines
18 KiB
PHP
Executable File
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;
|
|
}
|
|
}
|