Bot Discord - Commit completo con todos los cambios

This commit is contained in:
Admin
2026-01-16 20:24:38 -06:00
commit cf8ecfcf64
151 changed files with 28808 additions and 0 deletions

851
telegram/TelegramSender.php Executable file
View File

@@ -0,0 +1,851 @@
<?php
require_once __DIR__ . '/../includes/logger.php';
require_once __DIR__ . '/../src/Translate.php';
class TelegramSender
{
private $botToken;
private $apiUrl = 'https://api.telegram.org/bot';
private $pdo;
public function __construct($botToken, $pdo)
{
$this->botToken = $botToken;
$this->pdo = $pdo;
}
public function sendMessage($chatId, $content, $options = [], $addTranslateButton = false, $messageLanguage = 'es', $originalFullContent = null)
{
try {
custom_log("Iniciando envío de mensaje a chat $chatId");
if (empty(trim($content))) {
custom_log("Error: Contenido vacío para el chat $chatId");
return false;
}
// Obtener el language_code del destinatario
$recipientLang = 'es'; // Idioma por defecto
try {
$stmt = $this->pdo->prepare("SELECT language_code FROM recipients WHERE platform_id = ? AND platform = 'telegram'");
$stmt->execute([$chatId]);
$dbLang = $stmt->fetchColumn();
if ($dbLang) {
$recipientLang = $dbLang;
}
} catch (PDOException $e) {
custom_log("Error al obtener language_code del destinatario: " . $e->getMessage());
}
custom_log("[DEBUG] Idioma del destinatario ($chatId): " . $recipientLang);
$parts = $this->parseContent($content);
if (empty($parts)) {
custom_log("No se pudo parsear el contenido para el chat $chatId");
return false;
}
$messageIds = [];
$all_sent_successfully = true;
$totalParts = count($parts);
custom_log("Procesando $totalParts partes del mensaje para el chat $chatId");
foreach ($parts as $index => $part) {
$isLastPart = ($index === $totalParts - 1);
$partOptions = $isLastPart ? $options : [];
try {
custom_log("Procesando parte " . ($index + 1) . "/$totalParts de tipo: " . $part['type']);
$response = null;
$partSuccess = false;
if ($part['type'] === 'text') {
// Asegurarse de que el parámetro addTranslateButton se pase correctamente
if ($isLastPart && $addTranslateButton) {
$partOptions['add_translate_button'] = true;
}
$response = $this->sendText($chatId, $part['content'], $partOptions);
$partSuccess = ($response && ($response['ok'] ?? false));
} elseif ($part['type'] === 'image') {
// Verificar la URL de la imagen antes de intentar enviarla
$imageUrl = $part['url'] ?? '';
custom_log("Verificando URL de imagen: " . $imageUrl);
if ($this->isValidImageUrl($imageUrl)) {
// Asegurarse de que el parámetro addTranslateButton se pase correctamente
if ($isLastPart && $addTranslateButton) {
$partOptions['add_translate_button'] = true;
}
$response = $this->sendPhoto($chatId, $imageUrl, $part['caption'] ?? '', $partOptions);
$partSuccess = ($response && ($response['ok'] ?? false));
} else {
$errorMsg = "URL de imagen no válida o inaccesible: " . $imageUrl;
custom_log($errorMsg);
// Enviar un mensaje de error al chat
$errorText = "⚠️ No se pudo cargar una imagen: " . basename($imageUrl) . "\n";
if (!empty($part['caption'])) {
$errorText .= "📝 " . $part['caption'] . "\n";
}
$errorText .= "🔗 " . $imageUrl;
$this->sendText($chatId, $errorText);
$partSuccess = false;
}
}
if ($partSuccess) {
if (isset($response['result']['message_id'])) {
$messageIds[] = [
'success' => true,
'message_id' => $response['result']['message_id'],
'type' => $part['type']
];
}
} else {
$error = $response['description'] ?? 'Error desconocido al procesar la parte del mensaje';
custom_log("Error al enviar parte " . ($index + 1) . "/$totalParts: " . json_encode($error));
// Agregar el error a los IDs de mensaje para referencia
$messageIds[] = [
'success' => false,
'error' => $error,
'type' => $part['type'],
'content' => $part['type'] === 'image' ? $part['url'] : substr($part['content'] ?? '', 0, 100) . '...'
];
$all_sent_successfully = false;
}
} catch (Exception $e) {
custom_log("Excepción al procesar parte: " . $e->getMessage());
$all_sent_successfully = false;
}
if (!$isLastPart) {
usleep(500000); // 500ms de pausa
}
}
// Si hubo algún error, devolver información detallada
if (!$all_sent_successfully) {
$failedParts = array_filter($messageIds, function($msg) {
return isset($msg['success']) && $msg['success'] === false;
});
custom_log("No se pudieron enviar " . count($failedParts) . " partes del mensaje");
// Si no se pudo enviar ninguna parte, devolver falso
if (count($failedParts) === count($messageIds)) {
return false;
}
}
// Obtener los mensajes enviados exitosamente
$sentMessages = array_filter($messageIds, function($msg) {
return !empty($msg['success']) && !empty($msg['message_id']);
});
// Guardar el mensaje saliente en telegram_interactions con el idioma del remitente
// Esto se hace una vez, después de que todas las partes del mensaje han sido enviadas.
if ($all_sent_successfully && !empty($messageIds)) {
$lastSentMessage = end($messageIds);
$lastMessageId = $lastSentMessage['message_id'] ?? null;
$contentToSave = $originalFullContent ?? $content; // Use originalFullContent or the assembled content
if ($lastMessageId) {
// Ensure contentToSave is not NULL
if ($contentToSave === null) {
$contentToSave = '';
}
try {
$stmt = $this->pdo->prepare("INSERT INTO telegram_interactions (chat_id, message_text, direction, telegram_message_id, language_code) VALUES (?, ?, 'out', ?, ?)");
$stmt->execute([$chatId, $contentToSave, $lastMessageId, $messageLanguage]);
custom_log("[DEBUG] Mensaje completo guardado con idioma: $messageLanguage y message_id: " . $lastMessageId);
} catch (PDOException $e) {
custom_log("Error al guardar mensaje saliente en telegram_interactions (después del loop): " . $e->getMessage());
}
}
}
// Lógica para los botones de traducción basados en supported_languages
if ($addTranslateButton && !empty($sentMessages)) {
$lastMessage = end($sentMessages);
$lastMessageId = $lastMessage['message_id'] ?? null;
if ($lastMessageId) {
$messageLang = strtolower($messageLanguage);
$buttons = [];
try {
$stmt = $this->pdo->query("SELECT language_code, flag_emoji FROM supported_languages WHERE is_active = 1 ORDER BY language_name ASC");
$langs = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($langs as $lang) {
$code = strtolower($lang['language_code']);
if ($code === $messageLang) continue; // no traducir al mismo idioma
$flag = $lang['flag_emoji'] ?: '';
$buttons[] = [
'text' => $flag !== '' ? $flag : strtoupper($code),
'callback_data' => 'translate:' . $lastMessageId . ':' . $code
];
}
} catch (Exception $e) {
custom_log('[DEBUG] No se pudieron obtener idiomas activos para botones: ' . $e->getMessage());
}
if (!empty($buttons)) {
// Armar filas de hasta 6 botones por fila para no saturar
$inline = [];
$row = [];
foreach ($buttons as $i => $btn) {
$row[] = $btn;
if (count($row) >= 6) {
$inline[] = $row;
$row = [];
}
}
if (!empty($row)) $inline[] = $row;
$keyboard = ['inline_keyboard' => $inline];
custom_log('[DEBUG] Enviando ' . count($buttons) . ' botones de traducción para mensaje ' . $lastMessageId);
$this->editMessageReplyMarkup($chatId, $lastMessageId, json_encode($keyboard));
} else {
custom_log('[DEBUG] No hay idiomas activos distintos a ' . $messageLang . ' para mostrar botones.');
}
} else {
custom_log('[DEBUG] No se muestra botón de traducción. Razón: Falta message ID');
}
}
// Devolver los IDs de los mensajes que se enviaron correctamente
return $messageIds;
} catch (Exception $e) {
custom_log("Error crítico en sendMessage para el chat $chatId: " . $e->getMessage());
return false;
}
}
public function deleteMessage($chatId, $messageId)
{
$response = $this->apiRequest('deleteMessage', [
'chat_id' => $chatId,
'message_id' => $messageId
]);
if (!isset($response['ok']) || $response['ok'] !== true) {
throw new Exception("Error de Telegram al eliminar");
}
return true;
}
public function editMessageText($chatId, $messageId, $text, $options = [])
{
$params = array_merge([
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => $text
], $options);
return $this->apiRequest('editMessageText', $params);
}
public function editMessageReplyMarkup($chatId, $messageId, $replyMarkup)
{
$params = [
'chat_id' => $chatId,
'message_id' => $messageId
];
// Solo agregar reply_markup si no está vacío
if (!empty($replyMarkup)) {
$params['reply_markup'] = $replyMarkup;
} else {
// Para eliminar el teclado, necesitamos enviar un objeto JSON vacío
$params['reply_markup'] = '{"inline_keyboard":[]}';
}
return $this->apiRequest('editMessageReplyMarkup', $params);
}
public function answerCallbackQuery($callbackQueryId, $options = [])
{
$params = array_merge([
'callback_query_id' => $callbackQueryId
], $options);
return $this->apiRequest('answerCallbackQuery', $params);
}
private function parseContent($html)
{
$parts = [];
if (empty(trim($html))) return [];
if (strip_tags($html) === $html) {
return [['type' => 'text', 'content' => $html]];
}
preg_match_all('/(<img[^>]+src=[\'\"]([^\'\"]+)[\'\"][^>]*>)/i', $html, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
if (empty($matches)) {
return [['type' => 'text', 'content' => $html]];
}
$last_pos = 0;
foreach ($matches as $match) {
$full_match = $match[0][0];
$image_url = $match[2][0];
$offset = $match[0][1];
$text_before = substr($html, $last_pos, $offset - $last_pos);
if (!empty(trim($text_before))) {
$parts[] = ['type' => 'text', 'content' => trim($text_before)];
}
// Convertir rutas relativas a absolutas si es necesario
if (!preg_match('/^https?:\/\//', $image_url)) {
$base_url = rtrim($_ENV['APP_URL'], '/');
$image_url = $base_url . '/' . ltrim($image_url, '/');
custom_log("[DEBUG] URL de imagen convertida: " . $image_url);
// Verificar si la URL es accesible
$headers = @get_headers($image_url);
if ($headers && strpos($headers[0], '200') === false) {
error_log("[ERROR] La imagen no es accesible en: " . $image_url);
} else {
error_log("[DEBUG] La imagen es accesible en: " . $image_url);
}
}
$parts[] = ['type' => 'image', 'url' => $image_url, 'caption' => ''];
$last_pos = $offset + strlen($full_match);
}
$remaining_text = substr($html, $last_pos);
if (!empty(trim($remaining_text))) {
$parts[] = ['type' => 'text', 'content' => trim($remaining_text)];
}
return $parts;
}
private function splitLongText($text, $maxLength = 4000)
{
if (mb_strlen($text) <= $maxLength) return [$text];
$parts = [];
$paragraphs = preg_split('/\n\s*\n/', $text);
$currentPart = '';
foreach ($paragraphs as $paragraph) {
if ((mb_strlen($currentPart) + mb_strlen($paragraph) + 2) <= $maxLength) {
$currentPart .= (empty($currentPart) ? '' : "\n\n") . $paragraph;
} else {
if (!empty($currentPart)) $parts[] = $currentPart;
$currentPart = $paragraph;
while (mb_strlen($currentPart) > $maxLength) {
$chunk = mb_substr($currentPart, 0, $maxLength);
$parts[] = $chunk;
$currentPart = mb_substr($currentPart, $maxLength);
}
}
}
if (!empty($currentPart)) $parts[] = $currentPart;
return $parts;
}
/**
* Envía un mensaje de texto a un chat de Telegram, manejando mensajes largos y rate limiting
*
* @param int|string $chatId ID del chat de Telegram
* @param string $text Texto a enviar
* @param array $options Opciones adicionales para el mensaje
* @param int $retryCount Número de reintentos actual (uso interno)
* @param int $maxRetries Número máximo de reintentos
* @return array|null Respuesta de la API o null en caso de error
*/
private function sendText($chatId, $text, $options = [], $retryCount = 0, $maxRetries = 3)
{
// Asegurarse de que $options sea un array
if (!is_array($options)) {
custom_log("ADVERTENCIA: Las opciones no son un array. Valor recibido: " . print_r($options, true));
$options = [];
}
// Verificar si se debe agregar el botón de traducción
$addTranslateButton = $options['add_translate_button'] ?? false;
unset($options['add_translate_button']); // Eliminar para evitar conflictos
// Configuración de depuración
$debug = $options['debug'] ?? false;
unset($options['debug']);
// Limitar la longitud de los mensajes de depuración
$debugText = $debug ? substr($text, 0, 200) : '';
$logPrefix = "[SEND_TEXT] [Chat: $chatId]";
custom_log("$logPrefix === INICIO ===");
custom_log("$logPrefix Tamaño del texto: " . mb_strlen($text, 'UTF-8') . " caracteres");
if ($debug) {
custom_log("$logPrefix Texto (primeros 200 caracteres): $debugText");
custom_log("$logPrefix Opciones: " . json_encode($options));
custom_log("$logPrefix Mostrar botón de traducción: " . ($addTranslateButton ? 'Sí' : 'No'));
}
// Limpiar el texto de etiquetas HTML innecesarias
$clean_text = trim(strip_tags($text));
if (empty($clean_text)) {
custom_log("$logPrefix ERROR: El texto está vacío después de limpiar etiquetas");
return null;
}
// Dividir el texto en partes manejables
$textParts = $this->splitLongText($clean_text);
$totalParts = count($textParts);
$allResponses = [];
$allSuccessful = true;
custom_log("$logPrefix Texto dividido en $totalParts partes");
foreach ($textParts as $index => $part) {
$isLastPart = ($index === $totalParts - 1);
$partNumber = $index + 1;
// Configurar parámetros básicos del mensaje
$params = [
'chat_id' => $chatId,
'text' => $part,
'disable_web_page_preview' => true,
'parse_mode' => 'HTML',
'disable_notification' => $options['disable_notification'] ?? false
];
// Agregar opciones adicionales solo a la última parte del mensaje
if ($isLastPart) {
// Mantener solo las opciones permitidas por la API de Telegram
$allowedOptions = [
'reply_markup', 'reply_to_message_id', 'allow_sending_without_reply',
'entities', 'protect_content', 'message_thread_id'
];
foreach ($allowedOptions as $option) {
if (isset($options[$option])) {
$params[$option] = $options[$option];
}
}
if ($debug) {
custom_log("$logPrefix [Parte $partNumber/$totalParts] Opciones aplicadas: " .
json_encode(array_keys($params)));
}
}
// Intentar enviar la parte del mensaje con reintentos
$attempt = 0;
$maxAttempts = 3;
$response = null;
while ($attempt < $maxAttempts) {
$attempt++;
custom_log("$logPrefix [Parte $partNumber/$totalParts] Intento $attempt de $maxAttempts");
$response = $this->apiRequest('sendMessage', $params);
// Verificar si la solicitud fue exitosa
if (!empty($response) && ($response['ok'] ?? false)) {
$allResponses[] = $response;
if ($debug) {
$msgId = $response['result']['message_id'] ?? 'desconocido';
custom_log("$logPrefix [Parte $partNumber/$totalParts] Enviado exitosamente (ID: $msgId)");
}
// Pequeña pausa entre mensajes para evitar rate limiting
if (!$isLastPart) {
usleep(500000); // 0.5 segundos
}
break; // Salir del bucle de reintentos
} else {
$errorCode = $response['error_code'] ?? 'desconocido';
$errorDesc = $response['description'] ?? 'Error desconocido';
custom_log("$logPrefix [Parte $partNumber/$totalParts] Error $errorCode: $errorDesc");
// Manejar errores específicos
if (strpos($errorDesc, 'Too Many Requests') !== false) {
$retryAfter = $response['parameters']['retry_after'] ?? 5; // Por defecto 5 segundos
custom_log("$logPrefix Rate limit alcanzado. Reintentando en $retryAfter segundos...");
sleep($retryAfter);
continue; // Reintentar
}
// Para otros errores, esperar un tiempo antes de reintentar
if ($attempt < $maxAttempts) {
$backoffTime = pow(2, $attempt); // Retroceso exponencial
custom_log("$logPrefix Reintentando en $backoffTime segundos...");
sleep($backoffTime);
} else {
custom_log("$logPrefix Se agotaron los reintentos para esta parte del mensaje");
$allSuccessful = false;
}
}
}
// Si no se pudo enviar después de los reintentos, marcar como fallido
if (empty($response) || !($response['ok'] ?? false)) {
$allSuccessful = false;
// Si es un error crítico, detener el envío de las partes restantes
$errorCode = $response['error_code'] ?? 0;
$criticalErrors = [400, 403, 404]; // Errores que indican que no tiene sentido seguir intentando
if (in_array($errorCode, $criticalErrors)) {
custom_log("$logPrefix Error crítico ($errorCode). Deteniendo el envío de las partes restantes.");
break;
}
}
}
// Devolver la última respuesta exitosa o null si hubo errores
$result = $allSuccessful ? end($allResponses) : null;
custom_log("$logPrefix === FIN === " . ($allSuccessful ? 'ÉXITO' : 'CON ERRORES'));
return $result;
}
/**
* Envía una foto a un chat de Telegram con manejo de errores y reintentos
*
* @param int|string $chatId ID del chat de Telegram
* @param string $photoUrl URL de la foto a enviar
* @param string $caption Texto opcional para la foto
* @param array $options Opciones adicionales para el mensaje
* @param int $retryCount Número de reintentos actual (uso interno)
* @param int $maxRetries Número máximo de reintentos
* @return array|bool Respuesta de la API o false en caso de error
*/
private function sendPhoto($chatId, $photoUrl, $caption = '', $options = [], $retryCount = 0, $maxRetries = 3)
{
$logPrefix = "[SEND_PHOTO] [Chat: $chatId]";
custom_log("$logPrefix === INICIO ===");
try {
// Validar y normalizar la URL de la imagen
$photoUrl = $this->normalizeAndValidateImageUrl($photoUrl, $logPrefix);
// Si la URL no es válida, intentar enviar un mensaje de error
if ($photoUrl === false) {
$fallbackMessage = $this->createPhotoErrorFallback($photoUrl, $caption, "URL de imagen no válida o inaccesible");
return $this->sendText($chatId, $fallbackMessage, $options);
}
// Configurar parámetros básicos para el envío de la foto
$params = [
'chat_id' => $chatId,
'photo' => $photoUrl,
'caption' => $caption,
'parse_mode' => 'HTML',
'disable_notification' => $options['disable_notification'] ?? false
];
// Agregar opciones adicionales permitidas por la API de Telegram
$allowedOptions = [
'reply_markup', 'reply_to_message_id', 'allow_sending_without_reply',
'protect_content', 'message_thread_id'
];
foreach ($allowedOptions as $option) {
if (isset($options[$option])) {
$params[$option] = $options[$option];
}
}
// Realizar el envío con manejo de reintentos
$result = $this->sendPhotoWithRetry($params, $photoUrl, $retryCount, $maxRetries, $logPrefix);
// Si el envío falló después de los reintentos, enviar un mensaje de error
if ($result === false || (isset($result['ok']) && !$result['ok'])) {
$errorDesc = $result['description'] ?? 'Error desconocido';
custom_log("$logPrefix Error al enviar foto después de $maxRetries intentos: $errorDesc");
$fallbackMessage = $this->createPhotoErrorFallback($photoUrl, $caption, $errorDesc);
return $this->sendText($chatId, $fallbackMessage, $options);
}
custom_log("$logPrefix Foto enviada exitosamente");
return $result;
} catch (Exception $e) {
custom_log("$logPrefix Excepción: " . $e->getMessage() . " - URL: $photoUrl");
// En caso de error inesperado, intentar enviar un mensaje de texto
$fallbackMessage = $this->createPhotoErrorFallback(
$photoUrl,
$caption,
"Error inesperado: " . $e->getMessage()
);
return $this->sendText($chatId, $fallbackMessage, $options);
} finally {
custom_log("$logPrefix === FIN ===");
}
}
/**
* Normaliza y valida una URL de imagen, convirtiendo URLs relativas a absolutas si es necesario
*
* @param string $url URL de la imagen a normalizar y validar
* @param string $logPrefix Prefijo para los mensajes de log
* @return string|false URL normalizada o false si no es válida
*/
private function normalizeAndValidateImageUrl($url, $logPrefix = '')
{
if (empty($url)) {
custom_log("${logPrefix} URL de imagen vacía");
return false;
}
// Si la URL es relativa, intentar convertirla a absoluta
if (!preg_match('/^https?:\/\//i', $url)) {
$baseUrl = rtrim($_ENV['APP_URL'], '/');
$absoluteUrl = $baseUrl . '/' . ltrim($url, '/');
custom_log("${logPrefix} URL relativa detectada, convirtiendo a absoluta: $absoluteUrl");
$url = $absoluteUrl;
}
// Verificar si la URL es accesible y es una imagen
if ($this->isValidImageUrl($url)) {
custom_log("${logPrefix} URL de imagen válida: $url");
return $url;
}
custom_log("${logPrefix} URL de imagen no válida o inaccesible: $url");
return false;
}
/**
* Crea un mensaje de error estandarizado para cuando falla el envío de una foto
*
* @param string $photoUrl URL de la foto que falló
* @param string $caption Texto de la foto (opcional)
* @param string $error Descripción del error
* @return string Mensaje de error formateado
*/
private function createPhotoErrorFallback($photoUrl, $caption, $error)
{
$message = "⚠️ *Error al procesar la imagen*\n";
$message .= "\n🔗 *URL*: " . (strlen($photoUrl) > 100 ? substr($photo_url, 0, 100) . '...' : $photoUrl);
if (!empty($error)) {
$message .= "\n\n❌ *Error*: $error";
}
if (!empty($caption)) {
$message .= "\n\n📝 *Descripción*: " .
(mb_strlen($caption) > 200 ? mb_substr($caption, 0, 200) . '...' : $caption);
}
return $message;
}
/**
* Envía una foto a Telegram con manejo de reintentos y rate limiting
*
* @param array $params Parámetros para la API de Telegram
* @param string $photoUrl URL de la foto (solo para registro)
* @param int $retryCount Número de reintentos actual
* @param int $maxRetries Número máximo de reintentos
* @param string $logPrefix Prefijo para los mensajes de log
* @return array|false Respuesta de la API o false en caso de error
*/
private function sendPhotoWithRetry($params, $photoUrl, $retryCount = 0, $maxRetries = 3, $logPrefix = '')
{
$attempt = 0;
$lastError = null;
while ($attempt <= $maxRetries) {
$attempt++;
custom_log("${logPrefix} Intento $attempt de $maxRetries - Enviando foto...");
$response = $this->apiRequest('sendPhoto', $params);
// Si la solicitud fue exitosa, devolver la respuesta
if (!empty($response) && ($response['ok'] ?? false)) {
custom_log("${logPrefix} Foto enviada exitosamente en el intento $attempt");
return $response;
}
// Si hay un error, registrarlo y verificar si se debe reintentar
$errorCode = $response['error_code'] ?? 'desconocido';
$errorDesc = $response['description'] ?? 'Error desconocido';
$lastError = $errorDesc;
custom_log("${logPrefix} Error en el intento $attempt: [$errorCode] $errorDesc");
// Si es un error de rate limit, esperar el tiempo indicado
if (isset($response['parameters']['retry_after'])) {
$retryAfter = $response['parameters']['retry_after'];
custom_log("${logPrefix} Rate limit alcanzado. Esperando $retryAfter segundos...");
sleep($retryAfter);
continue;
}
// Para otros errores, esperar un tiempo antes de reintentar (backoff exponencial)
if ($attempt < $maxRetries) {
$backoffTime = pow(2, $attempt); // 2, 4, 8, 16... segundos
custom_log("${logPrefix} Reintentando en $backoffTime segundos...");
sleep($backoffTime);
}
}
// Si llegamos aquí, se agotaron los reintentos
custom_log("${logPrefix} Se agotaron los $maxRetries intentos. Último error: $lastError");
return $response ?? ['ok' => false, 'description' => $lastError ?? 'Error desconocido'];
}
/**
* Verifica si una URL es una imagen accesible
*/
private function isValidImageUrl($url)
{
if (empty($url)) {
custom_log("[DEBUG] isValidImageUrl: URL vacía.");
return false;
}
if (!preg_match('/^https?:\/\//', $url)) {
$baseUrl = rtrim($_ENV['APP_URL'], '/');
$url = $baseUrl . '/' . ltrim($url, '/');
custom_log("[DEBUG] isValidImageUrl: URL convertida a absoluta: " . $url);
}
if (!filter_var($url, FILTER_VALIDATE_URL)) {
custom_log("[DEBUG] isValidImageUrl: URL no válida después de la conversión: " . $url);
return false;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
$headers = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
custom_log("[DEBUG] isValidImageUrl: Verificación con cURL para " . $url . " - Código HTTP: " . $httpCode . " - Content-Type: " . $contentType);
if ($httpCode != 200) {
custom_log("[DEBUG] isValidImageUrl: Código de estado HTTP no es 200.");
return false;
}
if ($contentType === null) {
custom_log("[DEBUG] isValidImageUrl: Content-Type es nulo.");
return false;
}
$isImage = (bool)preg_match('/^image\/(jpe?g|png|gif|webp|bmp|svg\+?xml)/i', $contentType);
custom_log("[DEBUG] isValidImageUrl: Es imagen (" . ($isImage ? 'Sí' : 'No') . ").");
return $isImage;
}
/**
* Realiza una petición a la API de Telegram con manejo de reintentos y rate limiting
*
* @param string $method Método de la API de Telegram
* @param array $params Parámetros de la petición
* @param int $retryCount Número de reintentos actual (para uso interno)
* @param int $maxRetries Número máximo de reintentos
* @return array Respuesta de la API de Telegram
*/
private function apiRequest($method, $params, $retryCount = 0, $maxRetries = 3)
{
$url = $this->apiUrl . $this->botToken . '/' . $method;
custom_log("[TELEGRAM API REQUEST] Method: {$method} | Params: " . json_encode($params));
// Inicializar cURL
$ch = curl_init($url);
// Configurar opciones de cURL
curl_setopt_array($ch, [
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_CONNECTTIMEOUT => 10, // 10 segundos para conectar
CURLOPT_TIMEOUT => 30, // 30 segundos para la ejecución total
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
]);
// Ejecutar la petición
$response_body = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// Manejar errores de cURL
if ($response_body === false) {
custom_log("[TELEGRAM API ERROR] cURL Error: " . $error);
// Si hay un error de conexión, reintentar con backoff exponencial
if ($retryCount < $maxRetries) {
$backoffTime = pow(2, $retryCount); // Backoff exponencial (2, 4, 8 segundos...)
custom_log("Reintentando en {" . $backoffTime . "} segundos... (Intento " . ($retryCount + 1) . " de $maxRetries)");
sleep($backoffTime);
return $this->apiRequest($method, $params, $retryCount + 1, $maxRetries);
}
return ['ok' => false, 'description' => 'Error de conexión: ' . $error];
}
// Decodificar la respuesta
$response_array = json_decode($response_body, true);
// Si hay un error en la decodificación JSON
if (json_last_error() !== JSON_ERROR_NONE) {
custom_log("[TELEGRAM API ERROR] Error al decodificar JSON: " . json_last_error_msg());
return ['ok' => false, 'description' => 'Error al decodificar la respuesta JSON'];
}
// Registrar la respuesta
custom_log("[TELEGRAM API RESPONSE] HTTP Code: $http_code | Body: " . $response_body);
// Manejar errores de rate limiting (código 429)
if ($http_code === 429) {
$retry_after = $response_array['parameters']['retry_after'] ?? 5; // Por defecto 5 segundos
if ($retryCount < $maxRetries) {
custom_log("Rate limit alcanzado. Reintentando en {" . $retry_after . "} segundos... (Intento " . ($retryCount + 1) . " de $maxRetries)");
sleep($retry_after);
return $this->apiRequest($method, $params, $retryCount + 1, $maxRetries);
}
custom_log("Se alcanzó el número máximo de reintentos para el método $method");
}
// Manejar otros códigos de error HTTP
if ($http_code < 200 || $http_code >= 400) {
$error_msg = $response_array['description'] ?? 'Error desconocido';
custom_log("[TELEGRAM API ERROR] HTTP $http_code: $error_msg");
// Si es un error 5xx, reintentar con backoff exponencial
if ($http_code >= 500 && $retryCount < $maxRetries) {
$backoffTime = pow(2, $retryCount);
custom_log("Error del servidor. Reintentando en {" . $backoffTime . "} segundos... (Intento " . ($retryCount + 1) . " de $maxRetries)");
sleep($backoffTime);
return $this->apiRequest($method, $params, $retryCount + 1, $maxRetries);
}
return ['ok' => false, 'description' => "Error HTTP $http_code: $error_msg"];
}
return $response_array;
}
}

View File

@@ -0,0 +1,161 @@
<?php
require_once __DIR__ . '/includes/session_check.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/src/TelegramSender.php';
require_once __DIR__ . '/src/HtmlToTelegramHtmlConverter.php';
require_once __DIR__ . '/config/config.php';
header('Content-Type: application/json');
// Solo para administradores
if ($_SESSION['role'] !== 'admin') {
echo json_encode(['success' => false, 'error' => 'Acceso denegado']);
exit;
}
$chatId = $_POST['chat_id'] ?? null;
$message = trim($_POST['message'] ?? '');
if (!$chatId || !$message) {
echo json_encode(['success' => false, 'error' => 'Faltan parámetros']);
exit;
}
$botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? '';
if (empty($botToken)) {
echo json_encode(['success' => false, 'error' => 'Token de Telegram no configurado']);
exit;
}
try {
$telegram = new TelegramSender($botToken, $pdo);
$converter = new HtmlToTelegramHtmlConverter();
// Verificar si el mensaje es un comando (comienza con #)
if (strpos($message, '#') === 0) {
// Extraer el comando (eliminando el # inicial y cualquier espacio en blanco)
$command = trim(substr($message, 1));
// Depuración: Mostrar el comando que se está buscando
error_log("Buscando comando: " . $command);
// Buscar el comando en la base de datos (insensible a mayúsculas/minúsculas)
$sql = "SELECT id, telegram_command, message_content FROM recurrent_messages WHERE LOWER(telegram_command) = LOWER(?)";
error_log("SQL: " . $sql . " - Parámetro: " . $command);
$stmt = $pdo->prepare($sql);
$stmt->execute([$command]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
// Depuración: Mostrar si se encontró el comando
if ($template) {
error_log("Comando encontrado - ID: " . $template['id'] . ", Comando: " . $template['telegram_command']);
// Usar el convertidor HTML para manejar correctamente el formato
$convertedContent = $converter->convert($template['message_content']);
// Usar el método sendMessage de TelegramSender que ya maneja la división de mensajes e imágenes
error_log("Enviando mensaje con comando: " . $template['telegram_command']);
$messageIds = $telegram->sendMessage($chatId, $convertedContent, ['parse_mode' => 'HTML']);
if ($messageIds === false) {
$error = "Error al enviar el mensaje a través de la API de Telegram";
error_log($error);
throw new Exception($error);
}
// Verificar si hubo errores en el envío de las partes
$failedParts = array_filter($messageIds, function($part) {
return isset($part['success']) && $part['success'] === false;
});
if (!empty($failedParts)) {
$error = "Algunas partes del mensaje no se pudieron enviar. Detalles: " . json_encode($failedParts);
error_log($error);
throw new Exception($error);
}
// Guardar tanto el comando como la respuesta en la base de datos
$stmt = $pdo->prepare(
"INSERT INTO telegram_interactions (chat_id, message_text, direction) VALUES (?, ?, 'out')"
);
// Guardar el comando original
$stmt->execute([$chatId, $message]);
// Guardar la respuesta automática (solo la primera parte para evitar llenar la base de datos)
$firstPart = mb_substr($template['message_content'], 0, 1000) . (mb_strlen($template['message_content']) > 1000 ? '...' : '');
$stmt->execute([$chatId, $firstPart]);
echo json_encode(['success' => true, 'is_command' => true]);
exit;
} else {
error_log("Comando NO encontrado en la base de datos: " . $command);
// Depuración adicional: Mostrar todos los comandos disponibles
$allCommands = $pdo->query("SELECT id, telegram_command FROM recurrent_messages WHERE telegram_command IS NOT NULL")->fetchAll(PDO::FETCH_ASSOC);
error_log("Comandos disponibles: " . print_r($allCommands, true));
}
}
// Si no es un comando o no se encontró el comando, enviar el mensaje normalmente
error_log("Enviando mensaje normal");
$messageIds = $telegram->sendMessage($chatId, $message, [], true);
if ($messageIds === false) {
$error = "Error al enviar el mensaje a través de la API de Telegram";
error_log($error);
throw new Exception($error);
}
// Verificar si hubo errores en el envío de las partes
$failedParts = array_filter($messageIds, function($part) {
return isset($part['success']) && $part['success'] === false;
});
if (!empty($failedParts)) {
$error = "Algunas partes del mensaje no se pudieron enviar. Detalles: " . json_encode($failedParts);
error_log($error);
throw new Exception($error);
}
// Guardar el mensaje saliente en la base de datos
$stmt = $pdo->prepare(
"INSERT INTO telegram_interactions (chat_id, message_text, direction) VALUES (?, ?, 'out')"
);
$stmt->execute([$chatId, $message]);
echo json_encode(['success' => true, 'is_command' => false]);
} catch (Exception $e) {
$errorMsg = 'Error en send_telegram_reply: ' . $e->getMessage();
error_log($errorMsg);
// Intentar enviar un mensaje de error al chat si es posible
try {
if (isset($telegram) && $chatId) {
$errorDetails = "⚠️ Error al procesar tu solicitud. Por favor, inténtalo de nuevo más tarde.";
$telegram->sendMessage($chatId, $errorDetails);
// También registrar el error en la base de datos
if (isset($pdo)) {
$stmt = $pdo->prepare(
"INSERT INTO telegram_interactions (chat_id, message_text, direction, error) VALUES (?, ?, 'out', 1)"
);
$stmt->execute([$chatId, "[ERROR] " . $e->getMessage()]);
}
}
} catch (Exception $innerE) {
error_log("Error al notificar al usuario: " . $innerE->getMessage());
}
// Devolver el error al cliente
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
'debug' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]
]);
}

View File

@@ -0,0 +1,75 @@
<?php
require_once __DIR__ . '/../../includes/session_check.php';
require_once __DIR__ . '/../../includes/db.php';
require_once __DIR__ . '/../TelegramSender.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['action'])) {
header('Location: ../sent_messages.php');
exit();
}
$action = $_POST['action'];
if ($action === 'delete_message') {
if (!isset($_POST['sent_message_id'], $_POST['platform_message_id'], $_POST['chat_id'])) {
header('Location: ../sent_messages.php?error=missing_data');
exit();
}
$sentMessageId = $_POST['sent_message_id'];
$telegramMessageIdsJson = $_POST['platform_message_id'];
$chatId = $_POST['chat_id'];
$telegramMessageIds = json_decode($telegramMessageIdsJson, true);
// If decoding fails or the result is not an array, treat it as a single ID
if (json_last_error() !== JSON_ERROR_NONE || !is_array($telegramMessageIds)) {
$telegramMessageIds = [$telegramMessageIdsJson];
}
$telegramSender = new TelegramSender(TELEGRAM_BOT_TOKEN);
$allDeleted = true;
$error_messages = [];
try {
foreach ($telegramMessageIds as $messageId) {
// Skip if the ID is empty or invalid
if (empty($messageId) || !is_numeric($messageId)) {
error_log("Skipping invalid Telegram message ID: " . var_export($messageId, true));
continue;
}
try {
$telegramSender->deleteMessage($chatId, $messageId);
usleep(300000); // 300ms pause to avoid rate limits
} catch (Exception $e) {
$allDeleted = false;
$error_messages[] = "Failed to delete message ID {$messageId}: " . $e->getMessage();
error_log(end($error_messages)); // Log the last error
}
}
if ($allDeleted) {
// If all messages were deleted successfully, remove the record from DB
$stmt = $pdo->prepare("DELETE FROM sent_messages WHERE id = ?");
$stmt->execute([$sentMessageId]);
header('Location: ../sent_messages.php?success=deleted&platform=Telegram');
} else {
// If some deletions failed, redirect with an error message
$errorMessage = implode('; ', $error_messages);
header('Location: ../sent_messages.php?error=delete_failed&platform=Telegram&message=' . urlencode($errorMessage));
}
exit();
} catch (Exception $e) {
// Catch any other unexpected errors during the process
$errorMessage = "An unexpected error occurred: " . $e->getMessage();
error_log($errorMessage);
header('Location: ../sent_messages.php?error=unexpected_error&platform=Telegram&message=' . urlencode($errorMessage));
exit();
}
} // Closing brace for "if ($action === 'delete_message')"
// Fallback redirect if action is not 'delete_message'
header('Location: ../sent_messages.php');
exit();

621
telegram/admin/chat_telegram.php Executable file
View File

@@ -0,0 +1,621 @@
<?php
require_once __DIR__ . '/../../includes/session_check.php';
require_once __DIR__ . '/../../includes/db.php';
// Solo para administradores
if ($_SESSION['role'] !== 'admin') {
header('HTTP/1.0 403 Forbidden');
die('Acceso denegado.');
}
// Obtener todos los usuarios de Telegram que han interactuado o son miembros de grupos
$stmt = $pdo->query(
"SELECT DISTINCT r.platform_id, r.name
FROM recipients r
WHERE r.platform = 'telegram' AND r.type = 'user'
ORDER BY r.name ASC"
);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
require_once __DIR__ . '/../../templates/header.php';
?>
<style>
.chat-container {
display: flex;
height: calc(100vh - 200px); /* Ajustar altura */
}
/* Estilos para el selector de emojis */
.emoji-picker {
position: fixed;
width: 250px;
max-height: 300px;
overflow-y: auto;
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 9999;
display: none;
flex-wrap: wrap;
gap: 5px;
}
.emoji-option {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
font-size: 1.5em;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
user-select: none;
}
.emoji-option:hover {
background-color: #f8f9fa;
transform: scale(1.2);
}
.emoji-option:active {
transform: scale(0.95);
}
.input-group-prepend .btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group-append .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
#message-text {
border-radius: 0;
}
.message-text {
white-space: pre-wrap;
word-wrap: break-word;
}
.user-list {
border-right: 1px solid #dee2e6;
overflow-y: auto;
}
.chat-history {
flex-grow: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
}
.message-form {
padding: 1rem;
border-top: 1px solid #dee2e6;
}
.message {
max-width: 70%;
padding: 0.5rem 1rem;
border-radius: 1rem;
margin-bottom: 0.5rem;
}
.message.in {
background-color: #6495ED; /* Azul claro (CornflowerBlue) */
color: white;
align-self: flex-start;
}
.message.out {
background-color: #6495ED; /* Azul claro (CornflowerBlue) */
color: white;
align-self: flex-end;
}
.user-list .list-group-item.active {
background-color: #0d6efd;
border-color: #0d6efd;
}
</style>
<div class="container-fluid">
<h1 class="mt-4">Chat de Soporte de Telegram</h1>
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="chat-container">
<!-- Columna de Usuarios -->
<div class="col-md-4 col-lg-3 user-list">
<div class="list-group list-group-flush">
<?php foreach ($users as $user): ?>
<a href="#" class="list-group-item list-group-item-action" data-chat-id="<?= $user['platform_id'] ?>">
<?= htmlspecialchars($user['name']) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Columna de Chat -->
<div class="col-md-8 col-lg-9 d-flex flex-column">
<div id="chat-history" class="chat-history">
<div class="text-center text-muted my-auto">
<i class="bi bi-arrow-left-circle-fill fs-1"></i>
<p>Selecciona un usuario para ver la conversación.</p>
</div>
</div>
<div id="message-form-container" class="message-form" style="display: none;">
<form id="message-form">
<input type="hidden" id="chat-id-input" name="chat_id">
<div class="input-group" style="position: relative;">
<div class="input-group-prepend">
<button type="button" id="emoji-trigger" class="btn btn-outline-secondary" onclick="toggleEmojiPicker()">
<i class="bi bi-emoji-smile"></i>
</button>
</div>
<input type="text" id="message-text" name="message" class="form-control" placeholder="Escribe tu respuesta..." required>
<div class="input-group-append">
<button type="submit" class="btn btn-primary">Enviar</button>
</div>
<!-- Selector de emojis simplificado -->
<div id="emoji-picker" style="
display: none;
position: absolute;
bottom: 100%;
left: 0;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
width: 250px;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;">
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
<span class="emoji-option" onclick="insertEmoji('😊')" style="cursor: pointer; font-size: 24px;">😊</span>
<span class="emoji-option" onclick="insertEmoji('😂')" style="cursor: pointer; font-size: 24px;">😂</span>
<span class="emoji-option" onclick="insertEmoji('😍')" style="cursor: pointer; font-size: 24px;">😍</span>
<span class="emoji-option" onclick="insertEmoji('😎')" style="cursor: pointer; font-size: 24px;">😎</span>
<span class="emoji-option" onclick="insertEmoji('😢')" style="cursor: pointer; font-size: 24px;">😢</span>
<span class="emoji-option" onclick="insertEmoji('😡')" style="cursor: pointer; font-size: 24px;">😡</span>
<span class="emoji-option" onclick="insertEmoji('😴')" style="cursor: pointer; font-size: 24px;">😴</span>
<span class="emoji-option" onclick="insertEmoji('😷')" style="cursor: pointer; font-size: 24px;">😷</span>
<span class="emoji-option" onclick="insertEmoji('🤔')" style="cursor: pointer; font-size: 24px;">🤔</span>
<span class="emoji-option" onclick="insertEmoji('🤗')" style="cursor: pointer; font-size: 24px;">🤗</span>
<span class="emoji-option" onclick="insertEmoji('👍')" style="cursor: pointer; font-size: 24px;">👍</span>
<span class="emoji-option" onclick="insertEmoji('👎')" style="cursor: pointer; font-size: 24px;">👎</span>
<span class="emoji-option" onclick="insertEmoji('👏')" style="cursor: pointer; font-size: 24px;">👏</span>
<span class="emoji-option" onclick="insertEmoji('🙌')" style="cursor: pointer; font-size: 24px;">🙌</span>
<span class="emoji-option" onclick="insertEmoji('🤝')" style="cursor: pointer; font-size: 24px;">🤝</span>
<span class="emoji-option" onclick="insertEmoji('📱')" style="cursor: pointer; font-size: 24px;">📱</span>
<span class="emoji-option" onclick="insertEmoji('💻')" style="cursor: pointer; font-size: 24px;">💻</span>
<span class="emoji-option" onclick="insertEmoji('📷')" style="cursor: pointer; font-size: 24px;">📷</span>
<span class="emoji-option" onclick="insertEmoji('🎮')" style="cursor: pointer; font-size: 24px;">🎮</span>
<span class="emoji-option" onclick="insertEmoji('📚')" style="cursor: pointer; font-size: 24px;">📚</span>
</div>
</div>
</div>
<script>
// Función para mostrar/ocultar el selector de emojis
function toggleEmojiPicker() {
const picker = document.getElementById('emoji-picker');
if (picker.style.display === 'none' || !picker.style.display) {
picker.style.display = 'block';
} else {
picker.style.display = 'none';
}
return false;
}
// Función para insertar un emoji en el campo de texto
function insertEmoji(emoji) {
const input = document.getElementById('message-text');
const start = input.selectionStart;
const end = input.selectionEnd;
const text = input.value;
input.value = text.substring(0, start) + emoji + text.substring(end);
input.focus();
input.setSelectionRange(start + emoji.length, start + emoji.length);
document.getElementById('emoji-picker').style.display = 'none';
}
// Cerrar el selector al hacer clic fuera de él
document.addEventListener('click', function(e) {
const picker = document.getElementById('emoji-picker');
const trigger = document.getElementById('emoji-trigger');
if (picker && trigger &&
e.target !== picker && !picker.contains(e.target) &&
e.target !== trigger && !trigger.contains(e.target)) {
picker.style.display = 'none';
}
});
</script>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/../../templates/footer.php'; ?>
<script>
// Función para insertar emojis en el campo de texto
function insertEmoji(emoji) {
const input = document.getElementById('message-text');
const start = input.selectionStart;
const end = input.selectionEnd;
const text = input.value;
// Insertar el emoji en la posición del cursor
input.value = text.substring(0, start) + emoji + text.substring(end);
// Mover el cursor después del emoji insertado
const newPos = start + emoji.length;
input.selectionStart = input.selectionEnd = newPos;
// Enfocar el campo de texto
input.focus();
}
document.addEventListener('DOMContentLoaded', function() {
const userLinks = document.querySelectorAll('.user-list .list-group-item');
const chatHistory = document.getElementById('chat-history');
const formContainer = document.getElementById('message-form-container');
const messageForm = document.getElementById('message-form');
const chatIdInput = document.getElementById('chat-id-input');
const messageTextInput = document.getElementById('message-text');
let activeChatId = null;
let lastMessageId = 0;
let refreshInterval = null;
userLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
// Marcar usuario activo
userLinks.forEach(l => l.classList.remove('active'));
this.classList.add('active');
// Detener el intervalo anterior si existe
if (refreshInterval) {
clearInterval(refreshInterval);
}
// Establecer el nuevo chat activo
activeChatId = this.getAttribute('data-chat-id');
chatIdInput.value = activeChatId;
lastMessageId = 0; // Reiniciar el ID del último mensaje
// Cargar el historial y comenzar a actualizar
loadChatHistory(activeChatId);
// Iniciar la actualización automática con un intervalo dinámico
let refreshDelay = 500; // Comenzar con 0.5 segundos
function startRefreshTimer() {
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = setInterval(() => {
if (activeChatId && !document.hidden) {
checkForNewMessages(activeChatId);
}
}, refreshDelay);
}
// Iniciar el temporizador
startRefreshTimer();
});
});
function loadChatHistory(chatId, onlyNew = false) {
// Usar un timestamp para evitar caché del navegador
const timestamp = new Date().getTime();
const url = `get_chat_history.php?chat_id=${chatId}${onlyNew ? '&last_id=' + lastMessageId : ''}&_=${timestamp}`;
if (!onlyNew) {
chatHistory.innerHTML = '<div class="text-center text-muted my-auto"><div class="spinner-border" role="status"><span class="visually-hidden">Cargando...</span></div></div>';
formContainer.style.display = 'block';
}
fetch(url)
.then(response => response.json())
.then(data => {
console.log('Datos recibidos:', data); // Depuración
if (onlyNew && (!data.history || data.history.length === 0)) {
return; // No hay mensajes nuevos
}
if (!onlyNew) {
chatHistory.innerHTML = '';
}
let hasNewMessages = false;
if (data.success && data.history && data.history.length > 0) {
// Ordenar los mensajes por ID para asegurar el orden correcto
data.history.sort((a, b) => a.id - b.id);
// Ordenar los mensajes por ID para asegurar el orden correcto
data.history.sort((a, b) => a.id - b);
data.history.forEach(msg => {
// Solo agregar mensajes más recientes que el último mostrado
if (msg.id > lastMessageId || !onlyNew) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', msg.direction);
// Usar innerHTML para permitir que el enlace de traducción se añada correctamente
messageDiv.innerHTML = `<div class="message-text">${msg.message_text.replace(/\n/g, '<br>')}</div>`;
// Add translate link if needed
// For testing, always show for incoming messages
if (msg.direction === 'in') { // msg.language_code !== 'es'
const translateLink = document.createElement('a');
translateLink.href = '#';
translateLink.textContent = ' (Traducir)';
translateLink.classList.add('translate-link');
translateLink.setAttribute('data-message-id', msg.id);
messageDiv.appendChild(translateLink);
}
// Agregar el mensaje al historial
chatHistory.appendChild(messageDiv);
// Actualizar el ID del último mensaje
if (msg.id > lastMessageId) {
lastMessageId = msg.id;
}
hasNewMessages = true;
}
});
// Desplazarse al final solo si hay mensajes nuevos
if (hasNewMessages) {
chatHistory.scrollTop = chatHistory.scrollHeight;
}
} else if (!onlyNew) {
chatHistory.innerHTML = '<div class="text-center text-muted my-auto">No hay mensajes en esta conversación.</div>';
}
// Si no es una actualización, desplazarse al final
if (!onlyNew) {
chatHistory.scrollTop = chatHistory.scrollHeight;
}
// Actualizar lastMessageId si viene en la respuesta
if (data.last_id && data.last_id > lastMessageId) {
lastMessageId = data.last_id;
}
})
.catch(error => {
if (!onlyNew) {
console.error('Error al cargar el historial:', error);
chatHistory.innerHTML = '<div class="text-center text-danger my-auto">Error al cargar el historial. Intenta recargar la página.</div>';
}
});
}
// Función para verificar mensajes nuevos
function checkForNewMessages(chatId) {
if (!document.hidden && chatId) {
// Usar fetch con headers para evitar caché
fetch(`get_chat_history.php?chat_id=${chatId}&last_id=${lastMessageId}&_=${new Date().getTime()}`, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
})
.then(response => response.json())
.then(data => {
console.log('Datos recibidos en checkForNewMessages:', data); // Depuración
if (data.success && data.history && data.history.length > 0) {
let hasNewMessages = false;
// Ordenar los mensajes por ID para asegurar el orden correcto
data.history.sort((a, b) => a.id - b);
data.history.forEach(msg => {
if (msg.id > lastMessageId) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', msg.direction);
messageDiv.textContent = msg.message_text;
chatHistory.appendChild(messageDiv);
if (msg.id > lastMessageId) {
lastMessageId = msg.id;
}
hasNewMessages = true;
}
});
if (hasNewMessages) {
chatHistory.scrollTop = chatHistory.scrollHeight;
// Si hay mensajes nuevos, reiniciar el temporizador con el intervalo más corto
refreshDelay = 2000;
startRefreshTimer();
}
}
if (data.last_id && data.last_id > lastMessageId) {
lastMessageId = data.last_id;
}
})
.catch(error => {
console.error('Error al verificar mensajes nuevos:', error);
});
}
}
// Actualizar el chat cuando la pestaña vuelve a estar activa
document.addEventListener('visibilitychange', function() {
if (!document.hidden && activeChatId) {
loadChatHistory(activeChatId);
}
// Inicializar el selector de emojis
console.log('Inicializando selector de emojis...');
let emojiPickerVisible = false;
const emojiTrigger = document.getElementById('emoji-trigger');
const emojiPicker = document.getElementById('emoji-picker');
const messageTextInput = document.getElementById('message-text');
console.log('Elementos del selector de emojis:', {
emojiTrigger: emojiTrigger ? 'Encontrado' : 'No encontrado',
emojiPicker: emojiPicker ? 'Encontrado' : 'No encontrado',
messageTextInput: messageTextInput ? 'Encontrado' : 'No encontrado'
});
// Asegurarse de que el selector esté oculto inicialmente
if (emojiPicker) {
emojiPicker.style.display = 'none';
emojiPicker.style.backgroundColor = '#fff';
emojiPicker.style.border = '2px solid red';
emojiPicker.innerHTML = '<div style="padding: 10px;">Selector de emojis</div>' + emojiPicker.innerHTML;
}
// Mostrar/ocultar selector de emojis
if (emojiTrigger) {
emojiTrigger.addEventListener('click', function(e) {
console.log('Botón de emoji clickeado');
e.preventDefault();
e.stopPropagation();
if (!emojiPicker) {
console.error('El selector de emojis no se encontró en el DOM');
return false;
}
if (emojiPickerVisible) {
console.log('Ocultando selector de emojis');
emojiPicker.style.display = 'none';
} else {
console.log('Mostrando selector de emojis');
emojiPicker.style.display = 'block';
// Posicionar el selector debajo del botón
const rect = emojiTrigger.getBoundingClientRect();
emojiPicker.style.position = 'fixed';
emojiPicker.style.top = (rect.bottom + window.scrollY) + 'px';
emojiPicker.style.left = (rect.left + window.scrollX) + 'px';
emojiPicker.style.zIndex = '9999';
console.log('Posición del selector:', emojiPicker.style.top, emojiPicker.style.left);
}
emojiPickerVisible = !emojiPickerVisible;
return false;
});
} else {
console.error('No se pudo encontrar el botón de emoji');
}
// Cerrar el selector al hacer clic fuera
document.addEventListener('click', function(e) {
if (!emojiPicker || !emojiTrigger) return;
const isClickInside = emojiPicker.contains(e.target) || emojiTrigger.contains(e.target);
if (emojiPickerVisible && !isClickInside) {
console.log('Clic fuera del selector, ocultando...');
emojiPicker.style.display = 'none';
emojiPickerVisible = false;
}
});
// Manejar clics en emojis
if (emojiPicker) {
emojiPicker.addEventListener('click', function(e) {
console.log('Clic en el selector de emojis');
const emojiOption = e.target.closest('.emoji-option');
if (emojiOption) {
const emojiChar = emojiOption.getAttribute('data-emoji');
console.log('Emoji seleccionado:', emojiChar);
if (emojiChar) {
insertEmoji(emojiChar);
emojiPicker.style.display = 'none';
emojiPickerVisible = false;
}
}
});
}
});
// Evento para enviar mensaje
messageForm.addEventListener('submit', function(e) {
e.preventDefault();
const messageText = messageTextInput.value.trim();
if (!messageText) return;
const formData = new FormData(this);
// Optimistic UI update
const messageDiv = document.createElement('div');
messageDiv.className = `message out`;
messageDiv.innerHTML = `
<div class="message-text">${messageText.replace(/\n/g, '<br>')}</div>
<div class="message-time">${new Date().toLocaleTimeString()}</div>
`;
chatHistory.appendChild(messageDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
messageTextInput.value = '';
fetch('admin_send_message.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (!data.success) {
alert('Error al enviar el mensaje: ' + data.error);
// Opcional: eliminar el mensaje de la UI si falla
chatHistory.removeChild(messageDiv);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error de red al enviar el mensaje.');
chatHistory.removeChild(messageDiv);
});
});
// Evento para traducir mensaje
chatHistory.addEventListener('click', function(e) {
if (e.target.classList.contains('translate-link')) {
e.preventDefault();
const messageId = e.target.getAttribute('data-message-id');
const messageDiv = e.target.parentElement;
fetch(`translate_message.php?message_id=${messageId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const translatedText = document.createElement('div');
translatedText.classList.add('translated-text');
translatedText.textContent = data.translated_text;
messageDiv.appendChild(translatedText);
e.target.remove(); // Remove the translate link
} else {
alert('Error al traducir el mensaje: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error de red al traducir el mensaje.');
});
}
});
});
</script>

View File

@@ -0,0 +1,338 @@
<?php
require_once __DIR__ . '/../../includes/session_check.php';
require_once __DIR__ . '/../../includes/db.php';
// Crear las tablas necesarias si no existen
try {
$pdo->exec("
-- Tabla para registrar interacciones con el bot
CREATE TABLE IF NOT EXISTS telegram_bot_interactions (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
username VARCHAR(255),
first_name VARCHAR(255),
last_name VARCHAR(255),
interaction_type VARCHAR(50) NOT NULL,
interaction_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_interaction (user_id, interaction_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Tabla para la configuración del mensaje del bot
CREATE TABLE IF NOT EXISTS telegram_bot_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
message_text TEXT NOT NULL,
button_text VARCHAR(50) DEFAULT '¡Únete a nuestro grupo!',
group_invite_link VARCHAR(255) DEFAULT 'https://t.me/+Hzh0G-1-BY41ZDNj',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
register_users BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Insertar configuración por defecto si no existe
INSERT IGNORE INTO telegram_bot_messages
(message_text, button_text, group_invite_link, is_active, register_users)
VALUES (
'¡Hola {user_name}! 👋\n\nGracias por interactuar con nuestro bot. Únete a nuestro grupo principal para mantenerte actualizado con nuestras novedades.',
'¡Únete a nuestro grupo!',
'https://t.me/+Hzh0G-1-BY41ZDNj',
TRUE,
TRUE
);
");
} catch (PDOException $e) {
die("Error al crear las tablas: " . $e->getMessage());
}
// Manejar el envío del formulario
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$messageText = $_POST['message_text'] ?? '';
$buttonText = $_POST['button_text'] ?? '¡Únete a nuestro grupo!';
$groupInviteLink = $_POST['group_invite_link'] ?? 'https://t.me/+Hzh0G-1-BY41ZDNj';
$isActive = isset($_POST['is_active']) ? 1 : 0;
$registerUsers = isset($_POST['register_users']) ? 1 : 0;
try {
$pdo->beginTransaction();
// Actualizar la configuración
$stmt = $pdo->prepare("
UPDATE telegram_bot_messages
SET message_text = ?,
button_text = ?,
group_invite_link = ?,
is_active = ?,
register_users = ?
WHERE id = 1
");
$stmt->execute([
$messageText,
$buttonText,
$groupInviteLink,
$isActive,
$registerUsers
]);
$pdo->commit();
$successMessage = "¡Configuración guardada con éxito!";
} catch (PDOException $e) {
$pdo->rollBack();
$errorMessage = "Error al guardar la configuración: " . $e->getMessage();
}
}
// Obtener la configuración actual
try {
$stmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
$config = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$config) {
throw new Exception("No se encontró la configuración del mensaje del bot");
}
} catch (Exception $e) {
$errorMessage = "Error al cargar la configuración: " . $e->getMessage();
$config = [
'message_text' => '¡Hola {user_name}! 👋\n\nGracias por interactuar con nuestro bot. Únete a nuestro grupo principal para mantenerte actualizado con nuestras novedades.',
'button_text' => '¡Únete a nuestro grupo!',
'group_invite_link' => 'https://t.me/+Hzh0G-1-BY41ZDNj',
'is_active' => true,
'register_users' => true
];
}
// Obtener estadísticas de interacciones
try {
$statsStmt = $pdo->query("
SELECT
COUNT(*) as total_interactions,
COUNT(DISTINCT user_id) as unique_users
FROM telegram_bot_interactions
");
$stats = $statsStmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$errorMessage = "Error al cargar las estadísticas: " . $e->getMessage();
$stats = [];
}
$pageTitle = 'Interacciones del Bot de Telegram';
require_once __DIR__ . '/../../templates/header.php';
?>
<div class="container-fluid">
<h1 class="mt-4" data-translate="true">Interacciones del Bot de Telegram</h1>
<p class="text-muted" data-translate="true">Configura el mensaje que se enviará cuando los usuarios interactúen con tu bot.</p>
<?php if (isset($successMessage)): ?>
<div class="alert alert-success" data-translate="true"><?= htmlspecialchars($successMessage) ?></div>
<?php endif; ?>
<?php if (isset($errorMessage)): ?>
<div class="alert alert-danger" data-translate="true"><?= htmlspecialchars($errorMessage) ?></div>
<?php endif; ?>
<div class="row">
<div class="col-md-8">
<div class="card shadow-sm mb-4">
<div class="card-header">
<h5 class="mb-0" data-translate="true">Configuración del Mensaje del Bot</h5>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="message_text" class="form-label" data-translate="true">Mensaje de Respuesta</label>
<textarea class="form-control" id="message_text" name="message_text" rows="5" required><?= htmlspecialchars($config['message_text'] ?? '') ?></textarea>
<div class="form-text" data-translate="true">
Puedes usar los siguientes placeholders que serán reemplazados automáticamente:
<ul class="mb-0">
<li><code>{user_name}</code> - El nombre del usuario que interactúa con el bot.</li>
<li><code>{group_link}</code> - El enlace al grupo principal.</li>
</ul>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="button_text" class="form-label" data-translate="true">Texto del Botón</label>
<input type="text" class="form-control" id="button_text" name="button_text"
value="<?= htmlspecialchars($config['button_text'] ?? '') ?>" required>
</div>
<div class="col-md-6">
<label for="group_invite_link" class="form-label" data-translate="true">Enlace de Invitación al Grupo</label>
<input type="url" class="form-control" id="group_invite_link" name="group_invite_link"
value="<?= htmlspecialchars($config['group_invite_link'] ?? '') ?>" required>
</div>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="is_active" name="is_active"
<?= ($config['is_active'] ?? true) ? 'checked' : '' ?>>
<label class="form-check-label" for="is_active" data-translate="true">Activar mensaje de respuesta automática</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="register_users" name="register_users"
<?= ($config['register_users'] ?? true) ? 'checked' : '' ?>>
<label class="form-check-label" for="register_users" data-translate="true">Registrar usuarios que interactúan con el bot</label>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save-fill me-1"></i> <span data-translate="true">Guardar Cambios</span>
</button>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0" data-translate="true">Configuración de Webhook</h5>
</div>
<div class="card-body">
<div class="mb-4">
<h6 data-translate="true">Webhook Unificado</h6>
<p class="small text-muted" data-translate="true">Este único webhook maneja tanto las interacciones directas con el bot como los mensajes de bienvenida a nuevos miembros en un grupo.</p>
<div class="input-group mb-2">
<input type="text" class="form-control" value="<?= rtrim($_ENV['APP_URL'] ?? 'https://pruebaspons.duckdns.org', '/') ?>/telegram/webhook/telegram_bot_webhook.php?auth_token=<?= urlencode($_ENV['TELEGRAM_WEBHOOK_TOKEN'] ?? '') ?>" id="unifiedWebhookUrl" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard(document.getElementById('unifiedWebhookUrl'))">
<i class="bi bi-clipboard"></i> Copiar
</button>
</div>
</div>
<div class="mt-4">
<h6 data-translate="true">Comando de Configuración con cURL</h6>
<p class="small text-muted" data-translate="true">Usa este comando en tu terminal para configurar el webhook en Telegram. Solo necesitas hacerlo una vez.</p>
<div class="mb-3">
<div class="input-group">
<input type="text" class="form-control form-control-sm" value="curl -F 'url=<?= rtrim($_ENV['APP_URL'] ?? 'https://pruebaspons.duckdns.org', '/') ?>/telegram/webhook/telegram_bot_webhook.php?auth_token=<?= urlencode($_ENV['TELEGRAM_WEBHOOK_TOKEN'] ?? '') ?>' https://api.telegram.org/bot<?= $_ENV['TELEGRAM_BOT_TOKEN'] ?? '' ?>/setWebhook" id="curlUnifiedCommand" readonly>
<button class="btn btn-outline-secondary btn-sm" type="button" onclick="copyToClipboard(document.getElementById('curlUnifiedCommand'))">
<i class="bi bi-clipboard"></i>
</button>
</div>
</div>
</div>
<div class="alert alert-success mt-3 p-2 small">
<i class="bi bi-check-circle-fill me-1"></i>
<span data-translate="true">Este es el <strong>único webhook</strong> que necesitas configurar.</span>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm mb-4">
<div class="card-header">
<h5 class="mb-0" data-translate="true">Vista Previa</h5>
</div>
<div class="card-body">
<div class="telegram-message">
<div class="telegram-message-avatar">
<img src="https://cdn4.telegram.org/file/vaG9VXzJ9Us3druVCEMoQv-1wwhBWO1hieJQxGZA9fY/1lZkbjXz6CQ" alt="Bot" class="rounded-circle" width="40">
</div>
<div class="telegram-message-content">
<div class="telegram-message-username">Bot de Interacción</div>
<div class="telegram-message-text">
<?= nl2br(htmlspecialchars(str_replace(
['{user_name}', '{group_link}'],
['UsuarioEjemplo', $config['group_invite_link'] ?? ''],
$config['message_text'] ?? ''
))) ?>
</div>
<?php if (!empty($config['group_invite_link'])): ?>
<div class="mt-2">
<a href="<?= htmlspecialchars($config['group_invite_link']) ?>"
class="btn btn-telegram"
target="_blank">
<?= htmlspecialchars($config['button_text'] ?? '') ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php if (!empty($stats)): ?>
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0" data-translate="true">Estadísticas</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span data-translate="true">Usuarios únicos registrados:</span>
<span class="fw-bold"><?= number_format($stats['unique_users'] ?? 0) ?></span>
</div>
<div class="d-flex justify-content-between">
<span data-translate="true">Total de interacciones:</span>
<span class="fw-bold"><?= number_format($stats['total_interactions'] ?? 0) ?></span>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<style>
.telegram-message {
display: flex;
padding: 8px 16px;
margin-bottom: 1rem;
background-color: #182533;
border-radius: 8px;
color: #e1e9f2;
max-width: 100%;
}
.telegram-message-avatar {
margin-right: 12px;
flex-shrink: 0;
}
.telegram-message-avatar img {
border-radius: 50%;
}
.telegram-message-username {
font-weight: 500;
color: #6ab3f3;
margin-bottom: 4px;
}
.telegram-message-text {
margin-top: 2px;
line-height: 1.4;
white-space: pre-line;
}
.btn-telegram {
background-color: #2AABEE;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
text-decoration: none;
display: inline-block;
}
.btn-telegram:hover {
background-color: #229ED9;
color: white;
}
</style>
<script>
function copyToClipboard(button) {
const input = button.closest('.input-group').querySelector('input');
input.select();
document.execCommand('copy');
// Cambiar el ícono temporalmente
const icon = button.querySelector('i');
const originalClass = icon.className;
icon.className = 'bi bi-check';
// Restaurar después de 2 segundos
setTimeout(() => {
icon.className = originalClass;
}, 2000);
}
</script>
<?php require_once __DIR__ . '/../../templates/footer.php'; ?>

View File

@@ -0,0 +1,142 @@
<?php
require_once __DIR__ . '/../../includes/session_check.php';
require_once __DIR__ . '/../../includes/db.php';
// --- Lógica para el Mensaje de Bienvenida ---
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS telegram_welcome_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
chat_id BIGINT NOT NULL UNIQUE,
welcome_message TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
");
} catch (PDOException $e) {
die("Error al verificar la tabla de bienvenida: " . $e->getMessage());
}
// --- Lógica para los Ajustes Generales ---
// Manejar el envío de formularios
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Guardar Mensaje de Bienvenida
if (isset($_POST['save_welcome_message'])) {
$welcomeMessage = $_POST['welcome_message'] ?? '';
$isActive = isset($_POST['is_active']) ? 1 : 0;
$chatId = '-1002578350881'; // Manteniendo el ID de chat original para esta función
try {
$stmt = $pdo->prepare("
INSERT INTO telegram_welcome_messages (chat_id, welcome_message, is_active)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE welcome_message = ?, is_active = ?;
");
$stmt->execute([$chatId, $welcomeMessage, $isActive, $welcomeMessage, $isActive]);
$successMessage = "¡Configuración de bienvenida guardada con éxito!";
} catch (PDOException $e) {
$errorMessage = "Error al guardar la configuración de bienvenida: " . $e->getMessage();
}
}
// Guardar Ajustes Generales (Grupo por defecto)
if (isset($_POST['save_general_settings'])) {
$defaultGroupId = $_POST['default_announcement_group'] ?? '';
try {
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?");
$stmt->execute(['telegram_default_announcement_group', $defaultGroupId, $defaultGroupId]);
$successMessage = "¡Ajustes generales guardados con éxito!";
} catch (PDOException $e) {
$errorMessage = "Error al guardar los ajustes generales: " . $e->getMessage();
}
}
}
// --- Obtener datos para mostrar en la página ---
// Configuración del Mensaje de Bienvenida
$stmt_welcome = $pdo->prepare("SELECT * FROM telegram_welcome_messages WHERE chat_id = ?");
$stmt_welcome->execute(['-1002578350881']);
$welcomeConfig = $stmt_welcome->fetch(PDO::FETCH_ASSOC);
if (!$welcomeConfig) {
$welcomeConfig = [
'welcome_message' => '¡Bienvenido/a {user_name} a nuestro grupo! Esperamos que disfrutes tu estancia.',
'is_active' => true
];
}
// Grupos de Telegram para el selector
$stmt_groups = $pdo->query("SELECT name, platform_id FROM recipients WHERE platform = 'telegram' AND type = 'channel' ORDER BY name ASC");
$telegramGroups = $stmt_groups->fetchAll(PDO::FETCH_ASSOC);
// Configuración del grupo por defecto
$stmt_settings = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
$stmt_settings->execute(['telegram_default_announcement_group']);
$defaultGroupSetting = $stmt_settings->fetchColumn();
$pageTitle = 'Configuración de Telegram';
require_once __DIR__ . '/../../templates/header.php';
?>
<div class="container-fluid">
<h1 class="mt-4" data-translate="true">Configuración de Telegram</h1>
<?php if (isset($successMessage)): ?>
<div class="alert alert-success" data-translate="true"><?= htmlspecialchars($successMessage) ?></div>
<?php endif; ?>
<?php if (isset($errorMessage)): ?>
<div class="alert alert-danger" data-translate="true"><?= htmlspecialchars($errorMessage) ?></div>
<?php endif; ?>
<!-- Card para Mensaje de Bienvenida -->
<div class="card shadow-sm mb-4">
<div class="card-header"><h5 class="mb-0" data-translate="true">Gestionar Mensaje de Bienvenida</h5></div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="welcome_message" class="form-label" data-translate="true">Mensaje de Bienvenida</label>
<textarea class="form-control" id="welcome_message" name="welcome_message" rows="5"><?= htmlspecialchars($welcomeConfig['welcome_message']) ?></textarea>
<div class="form-text" data-translate="true">
Puedes usar <code>{user_name}</code> y <code>{chat_title}</code>.
</div>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="is_active" name="is_active" <?= $welcomeConfig['is_active'] ? 'checked' : '' ?>>
<label class="form-check-label" for="is_active" data-translate="true">Activar mensaje de bienvenida</label>
</div>
<button type="submit" name="save_welcome_message" class="btn btn-primary">
<i class="bi bi-save-fill me-1"></i> <span data-translate="true">Guardar Mensaje de Bienvenida</span>
</button>
</form>
</div>
</div>
<!-- Card para Ajustes Generales -->
<div class="card shadow-sm mb-4">
<div class="card-header"><h5 class="mb-0" data-translate="true">Ajustes de Comandos</h5></div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="default_announcement_group" class="form-label" data-translate="true">Grupo de Anuncios por Defecto</label>
<select class="form-select" id="default_announcement_group" name="default_announcement_group">
<option value="" data-translate="true">-- No seleccionado --</option>
<?php foreach ($telegramGroups as $group): ?>
<option value="<?= htmlspecialchars($group['platform_id']) ?>" <?= ($defaultGroupSetting == $group['platform_id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($group['name']) ?> (ID: <?= htmlspecialchars($group['platform_id']) ?>)
</option>
<?php endforeach; ?>
</select>
<div class="form-text" data-translate="true">Selecciona el grupo al que se enviarán los mensajes al usar el comando "Enviar a grupo #comando" desde el chat del bot.</div>
</div>
<button type="submit" name="save_general_settings" class="btn btn-primary">
<i class="bi bi-save-fill me-1"></i> <span data-translate="true">Guardar Ajustes de Comandos</span>
</button>
</form>
</div>
</div>
</div>
<?php
require_once __DIR__ . '/../../templates/footer.php';
?>

View File

@@ -0,0 +1,73 @@
<?php
class HtmlToTelegramHtmlConverter
{
/**
* Convierte un fragmento de HTML a un formato compatible con el parse_mode=HTML de Telegram.
*
* @param string $html El HTML de entrada.
* @return string El HTML adaptado para Telegram.
*/
public function convert($html)
{
if (empty(trim($html))) {
return '';
}
// 1. Decodificar entidades HTML como &nbsp; a espacios reales
$html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// 2. Reemplazar párrafos por saltos de línea para mantener la separación.
// Quita la etiqueta de apertura y reemplaza la de cierre con un salto de línea.
$html = str_ireplace('<p>', '', $html);
$html = str_ireplace('</p>', "\n", $html);
// 3. Asegurar que <br> también se convierta en un salto de línea.
$html = preg_replace('/<br\s*\/?>/i', "\n", $html);
// 4. Eliminar etiquetas no soportadas por Telegram, pero manteniendo las básicas y <img>.
// Telegram soporta: <b>, <strong>, <i>, <em>, <u>, <ins>, <s>, <strike>, <del>, <a>, <code>, <pre>
// Mantenemos <img> para que nuestro parser interno (TelegramSender) lo procese.
$allowed_tags = '<b><strong><i><em><u><ins><s><strike><del><a><code><pre><img>';
$html = strip_tags($html, $allowed_tags);
// 5. Limpiar múltiples saltos de línea consecutivos, pero NO los espacios en blanco.
// Esto previene más de dos saltos de línea seguidos, que Telegram ignora.
$html = trim($html);
$html = preg_replace('/[\r\n]{3,}/', "\n\n", $html);
// 6. Corregir etiquetas que pudieran quedar abiertas (buena práctica).
$html = $this->fixUnmatchedTags($html);
error_log("HTML convertido para Telegram: " . $html);
return $html;
}
/**
* Corrige etiquetas HTML no cerradas o mal formadas en el mensaje.
* @param string $html El HTML a corregir.
* @return string El HTML con las etiquetas corregidas.
*/
private function fixUnmatchedTags($html) {
$tags = ['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'strike', 'del', 'code', 'pre'];
foreach ($tags as $tag) {
// Contar etiquetas de apertura y cierre
$openCount = substr_count(strtolower($html), "<$tag>");
$closeCount = substr_count(strtolower($html), "</$tag>");
// Si hay más etiquetas de cierre que de apertura, eliminar las sobrantes
if ($closeCount > $openCount) {
$diff = $closeCount - $openCount;
$html = preg_replace("#</{$tag}>#i", '', $html, $diff);
}
// Si hay más etiquetas de apertura que de cierre, agregar las que faltan al final
elseif ($openCount > $closeCount) {
$diff = $openCount - $closeCount;
$html .= str_repeat("</{$tag}>", $diff);
}
}
return $html;
}
}

View File

@@ -0,0 +1,567 @@
<?php
// --- INICIO DE DEPURACIÓN DE DATOS CRUDOS ---
$raw_content = file_get_contents("php://input");
file_put_contents(__DIR__ . '/../../logs/raw_telegram_update.log', date('[Y-m-d H:i:s] ') . $raw_content . "\n", FILE_APPEND);
// --- FIN DE DEPURACIÓN DE DATOS CRUDOS ---
require_once __DIR__ . '/../../includes/logger.php';
custom_log("--- INICIO DE EJECUCIÓN DEL WEBHOOK ---");
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../includes/db.php';
require_once __DIR__ . '/../../src/TelegramSender.php';
require_once __DIR__ . '/../../src/HtmlToTelegramHtmlConverter.php';
require_once __DIR__ . '/../../src/Translate.php';
require_once __DIR__ . '/../../src/CommandLocker.php';
// Configuración de manejo de errores
ini_set('display_errors', 0);
error_reporting(E_ALL);
ini_set('log_errors', 1);
// Verificación de autenticación
$authToken = $_GET['auth_token'] ?? '';
$expectedToken = $_ENV['TELEGRAM_WEBHOOK_TOKEN'] ?? '';
if (!empty($expectedToken) && $authToken !== $expectedToken) {
http_response_code(403);
custom_log("Acceso no autorizado: token inválido.");
exit('Acceso no autorizado');
}
// Verificar token del bot
$botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? '';
if (empty($botToken)) {
http_response_code(500);
custom_log("Token de bot no configurado.");
exit('Token de bot no configurado');
}
// Obtener datos de la actualización
$content = file_get_contents("php://input");
$update = json_decode($content, true);
if (!$update) {
http_response_code(400);
exit('Datos de actualización no válidos');
}
custom_log("Update recibido: " . json_encode($update, JSON_PRETTY_PRINT));
try {
$telegram = new TelegramSender($botToken, $pdo);
$translator = new Translate();
$commandLocker = new CommandLocker($pdo);
$message = $update['message'] ?? $update['channel_post'] ?? null;
$callbackQuery = $update['callback_query'] ?? null;
$userChatMode = null; // Inicializar para evitar errores
// --- INICIO DE LA NUEVA LÓGICA DE CHAT AGENT/IA ---
$chatId = $message['chat']['id'] ?? $callbackQuery['message']['chat']['id'] ?? null;
$userId = $message['from']['id'] ?? $callbackQuery['from']['id'] ?? null;
if (!$chatId) {
custom_log("No se pudo determinar el chat_id. Saliendo.");
exit();
}
$isPrivateChat = (isset($message['chat']['type']) && $message['chat']['type'] === 'private') ||
(isset($callbackQuery['message']['chat']['type']) && $callbackQuery['message']['chat']['type'] === 'private');
if ($isPrivateChat && $userId) {
custom_log("[MODO AGENTE] Chat privado detectado. UserID: $userId");
if ($callbackQuery) {
$data = $callbackQuery['data'];
if ($data === 'platicar_bot' || $data === 'usar_ia') {
$newMode = ($data === 'platicar_bot') ? 'bot' : 'ia';
$stmt = $pdo->prepare("UPDATE recipients SET chat_mode = ? WHERE platform_id = ? AND platform = 'telegram'");
$stmt->execute([$newMode, $userId]);
$responseText = $newMode === 'bot'
? "🤖 Modo cambiado a 'Platicar con bot'. Ahora puedes usar los comandos normales como /comandos."
: "🧠 Modo cambiado a 'Usar IA'. Todo lo que escribas será procesado por la IA.\n\nEscribe /agente para volver a este menú.";
$telegram->answerCallbackQuery($callbackQuery['id']);
$telegram->editMessageText($chatId, $callbackQuery['message']['message_id'], $responseText);
custom_log("[MODO AGENTE] Usuario $userId cambió al modo: $newMode");
exit('ok');
}
}
$stmt = $pdo->prepare("SELECT chat_mode FROM recipients WHERE platform_id = ? AND platform = 'telegram'");
$stmt->execute([$userId]);
$userChatMode = $stmt->fetchColumn();
if ($userChatMode === false) {
custom_log("[NUEVO USUARIO] Usuario $userId no encontrado. Iniciando proceso de bienvenida.");
// 1. Obtener la configuración del mensaje de bienvenida
$configStmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
$welcomeConfig = $configStmt->fetch(PDO::FETCH_ASSOC);
// Verificar si el registro de usuarios está activo
if ($welcomeConfig && $welcomeConfig['register_users']) {
// 2. Registrar al nuevo usuario
$from = $message['from'];
$userName = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
$languageCode = $from['language_code'] ?? 'es';
$insertStmt = $pdo->prepare(
"INSERT INTO recipients (platform_id, name, type, platform, language_code, chat_mode)
VALUES (?, ?, 'user', 'telegram', ?, 'agent')
ON DUPLICATE KEY UPDATE name = VALUES(name), language_code = VALUES(language_code)"
);
$insertStmt->execute([$userId, $userName, $languageCode]);
custom_log("[NUEVO USUARIO] Usuario $userId ($userName) registrado con modo 'agent'.");
// 3. Enviar el mensaje de bienvenida si está activo
if ($welcomeConfig['is_active']) {
$messageText = str_replace('{user_name}', htmlspecialchars($userName), $welcomeConfig['message_text']);
$keyboard = [
'inline_keyboard' => [
[
['text' => $welcomeConfig['button_text'], 'url' => $welcomeConfig['group_invite_link']]
]
]
];
$telegram->sendMessage($chatId, $messageText, ['reply_markup' => json_encode($keyboard)]);
custom_log("[NUEVO USUARIO] Mensaje de bienvenida enviado a $userId.");
}
} else {
custom_log("[NUEVO USUARIO] El registro de usuarios está desactivado en la configuración. No se realiza ninguna acción.");
}
// Salir después de gestionar al nuevo usuario. La interacción ha terminado.
}
custom_log("[MODO AGENTE] Modo actual del usuario $userId: $userChatMode");
if (isset($message['text']) && trim($message['text']) === '/agente') {
$stmt = $pdo->prepare("UPDATE recipients SET chat_mode = 'agent' WHERE platform_id = ? AND platform = 'telegram'");
$stmt->execute([$userId]);
$userChatMode = 'agent';
custom_log("[MODO AGENTE] Usuario $userId usó /agente. Reseteando a modo 'agent'.");
}
switch ($userChatMode) {
case 'agent':
$keyboard = [
'inline_keyboard' => [
[
['text' => '🤖 Platicar con bot', 'callback_data' => 'platicar_bot'],
['text' => '🧠 Usar IA', 'callback_data' => 'usar_ia']
]
]
];
$telegram->sendMessage($chatId, "👋 Hola! ¿Cómo quieres interactuar?", ['reply_markup' => json_encode($keyboard)]);
break;
case 'ia':
custom_log("[DEBUG] Entrando en el caso 'ia'.");
$n8nWebhookUrl = $_ENV['N8N_IA_WEBHOOK_URL'] ?? null;
custom_log("[DEBUG] URL de n8n leída desde .env: " . ($n8nWebhookUrl ? $n8nWebhookUrl : 'NO ENCONTRADA'));
if ($n8nWebhookUrl && isset($message['text'])) {
$postData = [
'chat_id' => $chatId,
'user_id' => $userId,
'message' => $message['text'],
'name' => trim(($message['from']['first_name'] ?? '') . ' ' . ($message['from']['last_name'] ?? ''))
];
custom_log("[DEBUG] Preparando para enviar a n8n. Datos: " . json_encode($postData));
$ch = curl_init($n8nWebhookUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($curlError) {
custom_log("[ERROR] Error en cURL al llamar a n8n: " . $curlError);
} else {
custom_log("[DEBUG] Llamada a n8n completada. Código HTTP: $httpCode. Respuesta: " . $response);
}
custom_log("[MODO AGENTE] Mensaje de $userId reenviado a n8n (modo IA).");
} else {
if (!$n8nWebhookUrl) {
custom_log("[ERROR] La variable N8N_IA_WEBHOOK_URL no está configurada en el .env.");
}
if (!isset($message['text'])) {
custom_log("[DEBUG] No se reenvió a n8n porque no era un mensaje de texto.");
}
}
exit('ok');
case 'bot':
break;
}
}
$isPrivateChatInBotMode = ($isPrivateChat && isset($userChatMode) && $userChatMode === 'bot');
if (!$isPrivateChat || ($isPrivateChat && $userChatMode !== 'ia')) {
custom_log("Ejecutando lógica original. Razón: " . (!$isPrivateChat ? "No es chat privado" : "Chat privado en modo BOT"));
if (isset($message['new_chat_members'])) {
foreach ($message['new_chat_members'] as $newMember) {
if (isset($newMember['is_bot']) && $newMember['is_bot']) continue;
$userId = $newMember['id'];
$userName = trim(($newMember['first_name'] ?? '') . ' ' . ($newMember['last_name'] ?? ''));
$languageCode = $newMember['language_code'] ?? 'es';
$stmt = $pdo->prepare("INSERT INTO recipients (platform_id, name, type, platform, language_code) VALUES (?, ?, 'user', 'telegram', ?) ON DUPLICATE KEY UPDATE name = VALUES(name), language_code = VALUES(language_code)");
$stmt->execute([$userId, $userName, $languageCode]);
custom_log("Nuevo miembro $userName ($userId) registrado/actualizado.");
}
exit();
}
if ($callbackQuery) {
$callbackId = $callbackQuery['id'];
$data = $callbackQuery['data'];
$message = $callbackQuery['message'];
$chatId = $message['chat']['id'];
$messageId = $message['message_id'];
custom_log("[CALLBACK] Recibido callback original: " . $data);
if (strpos($data, 'translate_manual:') === 0) {
$targetLang = substr($data, strlen('translate_manual:'));
$originalMessage = $callbackQuery['message'];
$originalText = $originalMessage['text'] ?? $originalMessage['caption'] ?? '';
if (!empty($originalText)) {
try {
$sourceLang = $translator->detectLanguage($originalText);
if ($sourceLang && $sourceLang !== $targetLang) {
$translatedText = $translator->translateHtml($originalText, $sourceLang, $targetLang);
$telegram->answerCallbackQuery($callbackId, ['text' => $translatedText, 'show_alert' => true]);
} else {
$telegram->answerCallbackQuery($callbackId, ['text' => 'El mensaje ya está en este idioma o no se pudo detectar el idioma original.', 'show_alert' => false]);
}
} catch (Exception $e) {
custom_log("[CALLBACK] Excepción en traducción manual: " . $e->getMessage());
$telegram->answerCallbackQuery($callbackId, ['text' => '⚠️ Error interno al traducir.']);
}
} else {
$telegram->answerCallbackQuery($callbackId, ['text' => 'No se encontró texto para traducir.']);
}
exit('ok');
}
if (strpos($data, 'translate:') === 0) {
$parts = explode(':', $data);
if (count($parts) === 3) {
$originalMessageId = $parts[1];
$targetLang = $parts[2];
try {
$stmt = $pdo->prepare("SELECT message_text FROM telegram_interactions WHERE telegram_message_id = ? AND chat_id = ? ORDER BY id DESC LIMIT 1");
$stmt->execute([$originalMessageId, $chatId]);
$originalContent = $stmt->fetchColumn();
if ($originalContent) {
// Detectar idioma real del contenido y aplicar fallback si coincide con el destino
$plain = strip_tags(html_entity_decode($originalContent, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
$sourceLang = $translator->detectLanguage($plain) ?? 'es';
if ($sourceLang === $targetLang) {
$fallbackSrc = 'es';
if ($fallbackSrc !== $targetLang) {
$sourceLang = $fallbackSrc;
}
}
$translatedText = $translator->translateHtml($originalContent, $sourceLang, $targetLang);
if ($translatedText) {
$telegram->sendMessage($chatId, $translatedText, ['parse_mode' => 'HTML'], false, $targetLang);
// Conservar los botones para permitir traducir a otros idiomas
$telegram->answerCallbackQuery($callbackId, ['text' => '✅ Traducción enviada']);
} else {
$telegram->answerCallbackQuery($callbackId, ['text' => '⚠️ Error al traducir.']);
}
} else {
$telegram->answerCallbackQuery($callbackId, ['text' => '⚠️ No se encontró el texto original.']);
}
} catch (Exception $e) {
custom_log("[CALLBACK] Excepción al traducir: " . $e->getMessage());
$telegram->answerCallbackQuery($callbackId, ['text' => '⚠️ Error interno.']);
}
}
$telegram->answerCallbackQuery($callbackId);
exit();
}
}
// Determinar si hay texto para procesar, ya sea en 'text' o 'caption'
$text = null;
$isCaption = false;
if (isset($message['text'])) {
$text = trim($message['text']);
} elseif (isset($message['caption'])) {
$text = trim($message['caption']);
$isCaption = true;
}
if ($text !== null) {
$from = $message['from'] ?? null;
$userId = $from['id'] ?? null;
$messageId = $message['message_id'] ?? null;
if (!$userId) {
custom_log("No se pudo determinar el ID de usuario para la lógica original.");
exit();
}
// Un caption no puede ser un comando, así que solo se comprueba si no es un caption.
$isCommand = !$isCaption && ((strpos($text, '/') === 0) || (strpos($text, '#') === 0));
if ($isCommand) {
handleCommand($pdo, $telegram, $commandLocker, $translator, $text, $from, $chatId, $messageId);
} else {
handleRegularMessage($pdo, $telegram, $commandLocker, $translator, $text, $from, $chatId, $messageId);
}
}
}
http_response_code(200);
exit('ok');
} catch (Exception $e) {
custom_log("Error al procesar la actualización de Telegram: " . $e->getMessage());
http_response_code(500);
exit('Error interno del servidor');
}
function handleCommand($pdo, $telegram, $commandLocker, $translator, $text, $from, $chatId, $messageId) {
$userId = $from['id'];
$detectedLang = $translator->detectLanguage($text) ?? 'es';
if (strpos($text, '/setlang') === 0 || strpos($text, '/setlanguage') === 0) {
$parts = explode(' ', $text, 2);
if (count($parts) < 2 || strlen(trim($parts[1])) !== 2) {
$telegram->sendMessage($chatId, "❌ Formato incorrecto. Usa: /setlang es", [], true);
return;
}
$newLangCode = strtolower(trim($parts[1]));
$userName = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
$stmt = $pdo->prepare("SELECT id FROM recipients WHERE platform_id = ? AND platform = 'telegram'");
$stmt->execute([$userId]);
$userExists = $stmt->fetch();
if ($userExists) {
$stmt = $pdo->prepare("UPDATE recipients SET language_code = ?, name = ? WHERE platform_id = ? AND platform = 'telegram'");
$stmt->execute([$newLangCode, $userName, $userId]);
} else {
$stmt = $pdo->prepare("INSERT INTO recipients (platform_id, name, type, platform, language_code) VALUES (?, ?, 'user', 'telegram', ?)");
$stmt->execute([$userId, $userName, $newLangCode]);
}
$telegram->sendMessage($chatId, "✅ Tu idioma ha sido establecido a '" . strtoupper($newLangCode) . "'.", [], true, $detectedLang);
} elseif (strpos($text, '/bienvenida') === 0) {
custom_log("[COMANDO] Usuario $userId solicitó el mensaje de bienvenida.");
$configStmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
$welcomeConfig = $configStmt->fetch(PDO::FETCH_ASSOC);
if ($welcomeConfig && $welcomeConfig['is_active']) {
$userName = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
$messageText = str_replace('{user_name}', htmlspecialchars($userName), $welcomeConfig['message_text']);
$keyboard = [
'inline_keyboard' => [
[
['text' => $welcomeConfig['button_text'], 'url' => $welcomeConfig['group_invite_link']]
]
]
];
$telegram->sendMessage($chatId, $messageText, ['reply_markup' => json_encode($keyboard)]);
custom_log("[COMANDO] Mensaje de bienvenida enviado a $userId por solicitud.");
} else {
custom_log("[COMANDO] /bienvenida solicitado, pero el mensaje está inactivo.");
$telegram->sendMessage($chatId, " La función de bienvenida no está activa en este momento.", [], true, $detectedLang);
}
} elseif (strpos($text, '/comandos') === 0) {
$stmt = $pdo->query("SELECT telegram_command, name FROM recurrent_messages WHERE telegram_command IS NOT NULL AND telegram_command != '' ORDER BY name ASC");
$commands = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response = "";
if (empty($commands)) {
$response = " No hay comandos personalizados disponibles.";
} else {
$response = "📋 <b>LISTA DE COMANDOS DISPONIBLES</b>\n\n";
foreach ($commands as $index => $cmd) {
$command = htmlspecialchars(trim($cmd['telegram_command']));
if (strpos($command, '#') !== 0) $command = '#' . $command;
$name = htmlspecialchars(trim($cmd['name']));
$response .= ($index + 1) . ". <code>" . $command . "</code> - " . $name . "\n";
}
$response .= "\n Escribe el comando para usarlo.";
}
$stmt = $pdo->prepare("SELECT language_code FROM recipients WHERE platform_id = ? AND platform = 'telegram'");
$stmt->execute([$userId]);
$userLang = $stmt->fetchColumn();
if (!$userLang || $userLang === 'es') {
$converter = new HtmlToTelegramHtmlConverter();
$convertedContent = $converter->convert($response);
$telegram->sendMessage($chatId, $convertedContent, ['parse_mode' => 'HTML'], true, 'es');
} else {
$plainText = strip_tags(str_replace(['<br>', '</p>'], "\n", $response));
$plainText = html_entity_decode($plainText, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$telegram->sendMessage($chatId, $plainText, ['parse_mode' => 'HTML'], true, 'es');
}
} elseif (strpos($text, '#') === 0) {
$command = ltrim($text, '#');
if ($commandLocker->isCommandProcessing($command, $chatId, 'command')) {
$telegram->sendMessage($chatId, "El comando `#{$command}` ya está siendo procesado. Por favor, espera un momento.", [], true, 'es');
return;
}
$lockId = $commandLocker->acquireLock($command, $chatId, 'command', ['user_id' => $userId, 'original_message' => $text]);
if ($lockId === false) {
$telegram->sendMessage($chatId, "No se pudo procesar el comando. Inténtalo de nuevo.", [], true, 'es');
return;
}
try {
$stmt = $pdo->prepare("SELECT message_content FROM recurrent_messages WHERE telegram_command = ?");
$stmt->execute([$command]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if ($template) {
$content = $template['message_content'];
$converter = new HtmlToTelegramHtmlConverter();
$content = $converter->convert($content);
$detectedLang = $translator->detectLanguage(strip_tags($content)) ?? 'es';
if ($detectedLang !== 'es') {
$translatedContent = $translator->translateHtml($content, $detectedLang, 'es');
if ($translatedContent) $content = $translatedContent;
}
usleep(500000);
$result = $telegram->sendMessage($chatId, $content, ['parse_mode' => 'HTML'], true, 'es');
// TelegramSender::sendMessage devuelve un array de partes enviadas con message_id
$sentMessageId = null;
if (is_array($result)) {
// Tomar el último message_id exitoso
for ($i = count($result) - 1; $i >= 0; $i--) {
if (isset($result[$i]['success']) && $result[$i]['success'] && isset($result[$i]['message_id'])) {
$sentMessageId = $result[$i]['message_id'];
break;
}
}
} elseif (isset($result['result']['message_id'])) {
$sentMessageId = $result['result']['message_id'];
}
if (!empty($sentMessageId)) {
$commandLocker->releaseLock($lockId, $sentMessageId);
// Añadir botones de traducción dinámicos
$langStmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1");
$activeLangs = $langStmt->fetchAll(PDO::FETCH_ASSOC);
if (count($activeLangs) > 1) {
$keyboard = [];
foreach ($activeLangs as $lang) {
$keyboard[] = ['text' => ($lang['flag_emoji'] ?? '') . ' ' . $lang['language_name'], 'callback_data' => 'translate:' . $sentMessageId . ':' . $lang['language_code']];
}
// Agrupar botones en filas de a 2
$inline_keyboard = array_chunk($keyboard, 2);
$replyMarkup = json_encode(['inline_keyboard' => $inline_keyboard]);
$telegram->editMessageReplyMarkup($chatId, $sentMessageId, $replyMarkup);
}
} else {
$commandLocker->failLock($lockId, 'Error al enviar el mensaje');
$telegram->sendMessage($chatId, "Ocurrió un error al procesar el comando.", [], true, 'es');
}
} else {
$commandLocker->releaseLock($lockId);
$telegram->sendMessage($chatId, "El comando `#{$command}` no fue encontrado.", [], true, $detectedLang);
}
} catch (Exception $e) {
if (isset($lockId)) $commandLocker->failLock($lockId, 'Excepción: ' . $e->getMessage());
custom_log("Error al procesar comando #{$command}: " . $e->getMessage());
$telegram->sendMessage($chatId, "Ocurrió un error inesperado.", [], true, 'es');
}
}
}
function handleRegularMessage($pdo, $telegram, $commandLocker, $translator, $text, $from, $chatId, $messageId) {
$userId = $from['id'];
try {
// 1. Detectar el idioma del mensaje entrante
$detectedLang = $translator->detectLanguage(strip_tags($text)) ?? 'es';
// 2. Guardar la interacción original en la base de datos
try {
$stmt = $pdo->prepare("INSERT INTO telegram_interactions (chat_id, message_text, direction, language_code) VALUES (?, ?, 'in', ?)");
$stmt->execute([$chatId, $text, $detectedLang]);
} catch (PDOException $e) {
custom_log("[ERROR] No se pudo guardar el mensaje entrante en telegram_interactions: " . $e->getMessage());
}
// 3. Obtener idiomas activos
$langStmt = $pdo->query("SELECT language_code FROM supported_languages WHERE is_active = 1");
$activeLangs = $langStmt->fetchAll(PDO::FETCH_COLUMN);
// 4. Filtrar los idiomas de destino (todos los activos menos el original)
$targetLangs = array_filter($activeLangs, function($lang) use ($detectedLang) {
return $lang !== $detectedLang;
});
// 5. Si no hay idiomas a los que traducir, no hacer nada
if (empty($targetLangs)) {
custom_log("[QUEUE] No se requieren traducciones para el mensaje de Telegram #$messageId desde '$detectedLang'.");
return;
}
// 6. Unir los idiomas de destino en un solo string y encolar una única tarea
$targetLangsStr = implode(',', $targetLangs);
$sql = "INSERT INTO translation_queue (message_id, chat_id, user_id, text_to_translate, source_lang, target_lang, platform) VALUES (?, ?, ?, ?, ?, ?, 'telegram')";
$stmt = $pdo->prepare($sql);
$stmt->execute([
$messageId,
$chatId,
$userId,
$text,
$detectedLang,
$targetLangsStr
]);
custom_log("[QUEUE] Mensaje de Telegram #$messageId encolado para traducción de '$detectedLang' a '{$targetLangsStr}'.");
} catch (Exception $e) {
custom_log("[ERROR] Error en handleRegularMessage al encolar traducción: " . $e->getMessage());
}
}