From 68401c9372847600b7a43fd20d080be5d9ad5908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Mu=C3=B1oz?= Date: Wed, 23 Apr 2025 14:45:43 -0500 Subject: [PATCH] Muchas mejoras --- .env.example | 3 +- bots3.py | 410 ++++++++++++++++++++++++++++++++++++++++++++--- requirements.txt | 42 ++--- 3 files changed, 409 insertions(+), 46 deletions(-) diff --git a/.env.example b/.env.example index 7f29ede..94a3b87 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,5 @@ TELEGRAM_BOT_TOKEN=AAAAAAAAAA:BBBBBBBBBBBBBBBBBBBBBBBBBBBB MINIO_ENDPOINT=https://play.min.io MINIO_ACCESS_KEY= MINIO_SECRET_KEY= - +AUTHORIZED_USER_IDS=xxxxxxx,xxxxxxx +AUTHORIZED_USERNAMES=xxxxxxx,xxxxxxxxx diff --git a/bots3.py b/bots3.py index 5ff48eb..0386647 100644 --- a/bots3.py +++ b/bots3.py @@ -5,6 +5,18 @@ import boto3 from dotenv import load_dotenv from telebot import types import os +from datetime import datetime, timedelta +import logging + +# Configurar logging +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +# Para debugging, descomenta esta línea +# logger.setLevel(logging.DEBUG) load_dotenv() @@ -13,7 +25,39 @@ MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT') MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY') MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY') -bot = telebot.TeleBot(TELEGRAM_BOT_TOKEN) +# Obtener usuarios autorizados desde variables de entorno +AUTHORIZED_USER_IDS_STR = os.getenv('AUTHORIZED_USER_IDS', '') +AUTHORIZED_USERNAMES_STR = os.getenv('AUTHORIZED_USERNAMES', '') + +# Convertir string de IDs a lista de enteros +AUTHORIZED_USER_IDS = [] +if AUTHORIZED_USER_IDS_STR: + try: + AUTHORIZED_USER_IDS = [int(user_id.strip()) for user_id in AUTHORIZED_USER_IDS_STR.split(',') if user_id.strip()] + logger.info(f"Cargados {len(AUTHORIZED_USER_IDS)} IDs de usuario autorizados desde variables de entorno") + except ValueError as e: + logger.error(f"Error al convertir IDs de usuario: {e}. Asegúrate de que sean valores numéricos separados por comas.") + +# Convertir string de nombres de usuario a lista +AUTHORIZED_USERNAMES = [] +if AUTHORIZED_USERNAMES_STR: + AUTHORIZED_USERNAMES = [username.strip() for username in AUTHORIZED_USERNAMES_STR.split(',') if username.strip()] + logger.info(f"Cargados {len(AUTHORIZED_USERNAMES)} nombres de usuario autorizados desde variables de entorno") + +# Si no hay usuarios autorizados definidos, mostrar advertencia +if not AUTHORIZED_USER_IDS and not AUTHORIZED_USERNAMES: + logger.warning("⚠️ No se han definido usuarios autorizados. El bot rechazará todas las solicitudes.") + +# Importante: Configuración de comandos para el bot +bot = telebot.TeleBot(TELEGRAM_BOT_TOKEN, parse_mode='MARKDOWN') + +# Registrar comandos en BotFather +bot.set_my_commands([ + telebot.types.BotCommand("start", "Iniciar el bot y mostrar menú principal"), + telebot.types.BotCommand("upload", "Subir un archivo a un bucket"), + telebot.types.BotCommand("list", "Listar archivos en un bucket"), + telebot.types.BotCommand("help", "Mostrar ayuda") +]) BUCKETS = [ ('bancoppel', 'Bancoppel'), @@ -40,42 +84,176 @@ BUCKETS = [ ('walmart', 'Walmart'), ] +# Diccionarios para rastrear selecciones y estados de usuario selected_buckets = {} +user_states = {} # Para rastrear en qué operación está cada usuario + +def is_user_authorized(user): + """Verifica si el usuario está autorizado para usar el bot""" + # Si recibimos un message o un call, extraer el from_user + if hasattr(user, 'from_user'): + user = user.from_user + + # Ahora user debería ser directamente el objeto from_user + user_id = user.id + username = user.username + + # Comprobar si el ID de usuario está en la lista de autorizados + if user_id in AUTHORIZED_USER_IDS: + logger.debug(f"Usuario {user_id} autorizado por ID") + return True + + # Comprobar si el nombre de usuario está en la lista de autorizados + if username and username.lower() in [name.lower() for name in AUTHORIZED_USERNAMES]: + logger.debug(f"Usuario @{username} autorizado por nombre de usuario") + return True + + logger.warning(f"Acceso denegado para: ID={user_id}, Username=@{username or 'None'}") + return False + +def send_unauthorized_message(message): + """Envía un mensaje al usuario no autorizado""" + user_info = f"ID: {message.from_user.id}, Username: @{message.from_user.username or 'None'}" + logger.warning(f"Intento de acceso no autorizado: {user_info}") + + bot.reply_to(message, "⛔ No estás autorizado para usar este bot. Contacta al administrador si crees que esto es un error.") @bot.message_handler(commands=['start']) def start(message): + if not is_user_authorized(message): + send_unauthorized_message(message) + return + + logger.info(f"Comando /start recibido de usuario {message.from_user.id} (@{message.from_user.username or 'None'})") + bot.reply_to(message, """ -**Hola! Este bot te permite subir archivos a tu servidor MinIO S3.** +**Hola! Este bot te permite interactuar con tu servidor MinIO S3.** -**Selecciona un bucket para subir tu archivo:** - -""", reply_markup=generar_teclado_buckets()) +**¿Qué deseas hacer?** +""", reply_markup=generar_teclado_acciones()) @bot.message_handler(commands=['help']) def help_message(message): + if not is_user_authorized(message): + send_unauthorized_message(message) + return + + logger.info(f"Comando /help recibido de usuario {message.from_user.id}") + bot.reply_to(message, """ **Bienvenido a la ayuda de este bot!** +Comandos disponibles: +- /start - Comenzar a usar el bot +- /upload - Subir un archivo +- /list - Listar archivos en un bucket +- /help - Mostrar este mensaje de ayuda + Para subir un archivo: -- Simplemente envía el archivo que deseas subir y selecciona el bucket al que deseas subirlo. - -Para ver la lista de comandos disponibles: -- Usa el comando /help +- Usa /upload y selecciona un bucket, luego envía el archivo. +Para obtener un enlace de descarga: +- Usa /list para ver los archivos disponibles y selecciona uno. """) -@bot.callback_query_handler(func=lambda call: True) +@bot.message_handler(commands=['upload']) +def upload_command(message): + if not is_user_authorized(message): + send_unauthorized_message(message) + return + + logger.info(f"Comando /upload recibido de usuario {message.from_user.id}") + + chat_id = message.chat.id + user_states[chat_id] = 'selecting_bucket_for_upload' + bot.send_message(chat_id, "**Selecciona un bucket para subir tu archivo:**", reply_markup=generar_teclado_buckets('upload')) + +@bot.message_handler(commands=['list']) +def list_command(message): + if not is_user_authorized(message): + send_unauthorized_message(message) + return + + logger.info(f"Comando /list recibido de usuario {message.from_user.id}") + + chat_id = message.chat.id + user_states[chat_id] = 'selecting_bucket_for_list' + bot.send_message(chat_id, "**Selecciona un bucket para ver sus archivos:**", reply_markup=generar_teclado_buckets('list')) + +@bot.callback_query_handler(func=lambda call: call.data.startswith(('upload_', 'list_', 'download_', 'action_'))) def handle_callback(call): - bucket_name = call.data chat_id = call.message.chat.id - selected_buckets[chat_id] = bucket_name - bot.answer_callback_query(callback_query_id=call.id, text=f'Selected bucket: {bucket_name}') - bot.send_message(chat_id, f"Bucket seleccionado: {bucket_name}\n\nAhora enviame el archivo que quieres almacenar en el nombre del bucket") + + # Verificar que el usuario esté autorizado usando call.from_user + if not is_user_authorized(call.from_user): + bot.answer_callback_query(callback_query_id=call.id, text="No estás autorizado para usar este bot", show_alert=True) + return + + logger.debug(f"Callback recibido: {call.data} de usuario {call.from_user.id}") + + # Manejar selección de acción + if call.data.startswith('action_'): + action = call.data.split('_')[1] + if action == 'upload': + user_states[chat_id] = 'selecting_bucket_for_upload' + bot.edit_message_text("**Selecciona un bucket para subir tu archivo:**", + chat_id=chat_id, + message_id=call.message.message_id, + reply_markup=generar_teclado_buckets('upload')) + elif action == 'list': + user_states[chat_id] = 'selecting_bucket_for_list' + bot.edit_message_text("**Selecciona un bucket para ver sus archivos:**", + chat_id=chat_id, + message_id=call.message.message_id, + reply_markup=generar_teclado_buckets('list')) + elif action == 'start': + # Manejar el regreso al menú principal + user_states[chat_id] = None + bot.edit_message_text("**Hola! Este bot te permite interactuar con tu servidor MinIO S3.**\n\n**¿Qué deseas hacer?**", + chat_id=chat_id, + message_id=call.message.message_id, + reply_markup=generar_teclado_acciones()) + bot.answer_callback_query(callback_query_id=call.id, text=f'Seleccionado: {action}') + return + + # Manejar selección de bucket para subir archivo + if call.data.startswith('upload_'): + bucket_name = call.data[7:] # Quitar 'upload_' del principio + selected_buckets[chat_id] = bucket_name + user_states[chat_id] = 'waiting_for_file' + bot.answer_callback_query(callback_query_id=call.id, text=f'Bucket seleccionado: {bucket_name}') + bot.send_message(chat_id, f"Bucket seleccionado: **{bucket_name}**\n\nAhora envíame el archivo que quieres almacenar.") + + # Manejar selección de bucket para listar archivos + elif call.data.startswith('list_'): + bucket_name = call.data[5:] # Quitar 'list_' del principio + selected_buckets[chat_id] = bucket_name + list_bucket_contents(chat_id, bucket_name, call.message.message_id) + bot.answer_callback_query(callback_query_id=call.id, text=f'Listando bucket: {bucket_name}') + + # Manejar selección de archivo para descargar + elif call.data.startswith('download_'): + parts = call.data.split('_', 2) # Máximo 2 divisiones + bucket_name = parts[1] + file_key = parts[2] + generate_download_link(chat_id, bucket_name, file_key) + bot.answer_callback_query(callback_query_id=call.id, text=f'Generando enlace para: {file_key}') @bot.message_handler(content_types=['document']) def handle_document(message): try: + # Verificar si el usuario está autorizado + if not is_user_authorized(message): + send_unauthorized_message(message) + return + chat_id = message.chat.id + + # Verificar si el usuario está en el estado correcto + if chat_id not in user_states or user_states[chat_id] != 'waiting_for_file': + bot.reply_to(message, "Por favor selecciona un bucket primero usando /upload.") + return + if chat_id not in selected_buckets: bot.reply_to(message, "Por favor selecciona un bucket primero.") return @@ -88,37 +266,221 @@ def handle_document(message): s3 = boto3.client('s3', endpoint_url=MINIO_ENDPOINT, aws_access_key_id=MINIO_ACCESS_KEY, - aws_secret_access_key=MINIO_SECRET_KEY) # Crear cliente de S3 con las claves de MinIO + aws_secret_access_key=MINIO_SECRET_KEY) + + # Registrar la operación + logger.info(f"Usuario {message.from_user.id} (@{message.from_user.username}) subiendo archivo {filename} al bucket {bucket_name}") # Define los metadatos que deseas adjuntar al archivo metadata = { 'user': str(message.from_user.id), + 'username': message.from_user.username or "sin_username", 'file_name': filename, - 'content_type': message.document.mime_type + 'content_type': message.document.mime_type, + 'upload_date': datetime.now().strftime("%Y-%m-%d %H:%M:%S") } # Sube el archivo con los metadatos definidos s3.put_object(Bucket=bucket_name, Key=filename, Body=file_bytes, Metadata=metadata) - bot.reply_to(message, f"Archivo {filename} subido exitosamente al bucket {bucket_name}") + bot.reply_to(message, f"✅ Archivo **{filename}** subido exitosamente al bucket **{bucket_name}**") - # Muestra los metadatos del archivo subido - bot.send_message(chat_id, f"Metadatos del archivo {filename}:\n\n{metadata}") + # Generar enlace de descarga temporal automáticamente después de subir + generate_download_link(chat_id, bucket_name, filename) + + # Reiniciar el estado del usuario + user_states[chat_id] = None + except Exception as e: - bot.reply_to(message, f"Error al subir el archivo: {e}") + logger.error(f"Error al subir archivo: {str(e)}") + bot.reply_to(message, f"❌ Error al subir el archivo: {e}") -def generar_teclado_buckets(): - keyboard = telebot.types.InlineKeyboardMarkup() +def list_bucket_contents(chat_id, bucket_name, message_id=None): + try: + # Registrar la operación + logger.info(f"Listando contenido del bucket {bucket_name} para el usuario {chat_id}") + + s3 = boto3.client('s3', endpoint_url=MINIO_ENDPOINT, + aws_access_key_id=MINIO_ACCESS_KEY, + aws_secret_access_key=MINIO_SECRET_KEY) + + response = s3.list_objects_v2(Bucket=bucket_name) + + if 'Contents' in response and response['Contents']: + # Crear un teclado con los archivos + keyboard = types.InlineKeyboardMarkup() + + for obj in response['Contents']: + file_key = obj['Key'] + file_size = format_size(obj['Size']) + last_modified = obj['LastModified'].strftime("%Y-%m-%d") + + button_text = f"{file_key} ({file_size}) - {last_modified}" + button_data = f"download_{bucket_name}_{file_key}" + + # Asegurarnos de que el callback_data no exceda los 64 bytes + if len(button_data) > 64: + # Truncar el nombre del archivo si es necesario + max_key_length = 64 - len(f"download_{bucket_name}_") - 3 # 3 para "..." + truncated_key = file_key[:max_key_length] + "..." + button_text = f"{truncated_key} ({file_size}) - {last_modified}" + button_data = f"download_{bucket_name}_{file_key[:max_key_length]}" + + keyboard.add(types.InlineKeyboardButton(text=button_text, callback_data=button_data)) + + # Agregar botón para volver + keyboard.add(types.InlineKeyboardButton("⬅️ Volver", callback_data="action_list")) + + message_text = f"📁 Archivos en el bucket **{bucket_name}**:\n\nSelecciona un archivo para generar un enlace de descarga:" + + if message_id: + bot.edit_message_text(message_text, chat_id=chat_id, message_id=message_id, reply_markup=keyboard) + else: + bot.send_message(chat_id, message_text, reply_markup=keyboard) + else: + if message_id: + bot.edit_message_text(f"📂 El bucket **{bucket_name}** está vacío.", + chat_id=chat_id, + message_id=message_id, + reply_markup=types.InlineKeyboardMarkup().add( + types.InlineKeyboardButton("⬅️ Volver", callback_data="action_list") + )) + else: + bot.send_message(chat_id, f"📂 El bucket **{bucket_name}** está vacío.", + reply_markup=types.InlineKeyboardMarkup().add( + types.InlineKeyboardButton("⬅️ Volver", callback_data="action_list") + )) + except Exception as e: + logger.error(f"Error al listar contenido del bucket {bucket_name}: {str(e)}") + error_message = f"❌ Error al listar archivos: {str(e)}" + if message_id: + bot.edit_message_text(error_message, chat_id=chat_id, message_id=message_id) + else: + bot.send_message(chat_id, error_message) + +def generate_download_link(chat_id, bucket_name, file_key): + try: + # Registrar la operación + logger.info(f"Generando enlace para archivo {file_key} en bucket {bucket_name} para usuario {chat_id}") + + s3 = boto3.client('s3', endpoint_url=MINIO_ENDPOINT, + aws_access_key_id=MINIO_ACCESS_KEY, + aws_secret_access_key=MINIO_SECRET_KEY) + + # Verificar que el archivo existe antes de generar el enlace + try: + s3.head_object(Bucket=bucket_name, Key=file_key) + except s3.exceptions.ClientError as e: + if e.response['Error']['Code'] == '404': + bot.send_message(chat_id, f"❌ Error: El archivo {file_key} no existe en el bucket {bucket_name}.") + return + else: + raise + + # Generar enlace válido por 12 horas (43200 segundos) + url = s3.generate_presigned_url( + 'get_object', + Params={'Bucket': bucket_name, 'Key': file_key}, + ExpiresIn=43200 + ) + + # Calcular la fecha de expiración + expiration_time = datetime.now() + timedelta(hours=12) + expiration_str = expiration_time.strftime("%Y-%m-%d %H:%M:%S") + + # Enviar mensaje con el enlace + message = (f"🔗 **Enlace de descarga para {file_key}**\n\n" + f"📍 Bucket: **{bucket_name}**\n" + f"⏱️ Enlace válido hasta: **{expiration_str}**\n\n" + f"{url}") + + bot.send_message(chat_id, message) + + except Exception as e: + logger.error(f"Error al generar enlace para {file_key}: {str(e)}") + bot.send_message(chat_id, f"❌ Error al generar el enlace de descarga: {str(e)}") + +def format_size(size_bytes): + """Formato legible para el tamaño de archivo""" + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if size_bytes < 1024.0: + return f"{size_bytes:.2f} {unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.2f} PB" + +def generar_teclado_acciones(): + """Genera el teclado de acciones principales""" + keyboard = types.InlineKeyboardMarkup(row_width=2) + keyboard.add( + types.InlineKeyboardButton("📤 Subir archivo", callback_data="action_upload"), + types.InlineKeyboardButton("📋 Listar archivos", callback_data="action_list") + ) + return keyboard + +def generar_teclado_buckets(action_prefix): + """Genera el teclado de buckets con un prefijo de acción""" + keyboard = types.InlineKeyboardMarkup() buckets_row = [] for bucket_name, bucket_text in BUCKETS: - buckets_row.append(telebot.types.InlineKeyboardButton(bucket_text, callback_data=bucket_name)) + callback_data = f"{action_prefix}_{bucket_name}" + buckets_row.append(types.InlineKeyboardButton(bucket_text, callback_data=callback_data)) if len(buckets_row) == 3: keyboard.row(*buckets_row) buckets_row = [] if buckets_row: keyboard.row(*buckets_row) + + # Agregar botón para volver + keyboard.row(types.InlineKeyboardButton("⬅️ Volver", callback_data="action_start")) return keyboard -bot.polling() +@bot.callback_query_handler(func=lambda call: call.data == "action_start") +def handle_start_action(call): + chat_id = call.message.chat.id + + # Verificar que el usuario esté autorizado usando call.from_user + if not is_user_authorized(call.from_user): + bot.answer_callback_query(callback_query_id=call.id, text="No estás autorizado para usar este bot", show_alert=True) + return + + bot.edit_message_text( + "**Hola! Este bot te permite interactuar con tu servidor MinIO S3.**\n\n**¿Qué deseas hacer?**", + chat_id=chat_id, + message_id=call.message.message_id, + reply_markup=generar_teclado_acciones() + ) + bot.answer_callback_query(callback_query_id=call.id, text="Menú principal") + +@bot.message_handler(func=lambda message: True) +def handle_unknown_command(message): + """Maneja cualquier mensaje que no coincida con los comandos anteriores""" + if not is_user_authorized(message): + send_unauthorized_message(message) + return + + logger.debug(f"Mensaje no reconocido: {message.text} de usuario {message.from_user.id}") + + # Si el usuario está en estado de espera para un archivo, recordarle que envíe un documento + chat_id = message.chat.id + if chat_id in user_states and user_states[chat_id] == 'waiting_for_file': + bot.reply_to(message, "Por favor, envía un archivo para subir al bucket seleccionado.") + return + + # Para cualquier otro mensaje, mostrar ayuda + bot.reply_to(message, "Comando no reconocido. Usa /help para ver los comandos disponibles.") + +if __name__ == "__main__": + logger.info("Bot iniciado. Esperando mensajes...") + # Imprimir mensaje de arranque con lista de usuarios autorizados + auth_ids = ", ".join([str(uid) for uid in AUTHORIZED_USER_IDS]) + auth_names = ", ".join(AUTHORIZED_USERNAMES) + logger.info(f"Usuarios autorizados por ID: {auth_ids}") + logger.info(f"Usuarios autorizados por nombre: {auth_names}") + + try: + # Intentar iniciar el bot con manejo de errores + bot.polling(none_stop=True, interval=0) + except Exception as e: + logger.error(f"Error al iniciar el bot: {str(e)}") diff --git a/requirements.txt b/requirements.txt index 1cdc4cd..5ca81d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,21 @@ -argon2-cffi==23.1.0 -argon2-cffi-bindings==21.2.0 -boto3==1.34.55 -botocore==1.34.55 -certifi==2024.2.2 -cffi==1.16.0 -charset-normalizer==3.3.2 -configparser==6.0.1 -idna==3.6 -jmespath==1.0.1 -pycparser==2.21 -pycryptodome==3.20.0 -pyTelegramBotAPI==4.16.1 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -pytz==2024.1 -requests==2.31.0 -s3transfer==0.10.0 -six==1.16.0 -typing_extensions==4.10.0 -urllib3==2.0.7 +argon2-cffi +argon2-cffi-bindings +boto3 +botocore +certifi +cffi +charset-normalizer +configparser +idna +jmespath +pycparser +pycryptodome +pyTelegramBotAPI +python-dateutil +python-dotenv +pytz +requests +s3transfer +six +typing_extensions +urllib3