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('' . $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; } }