You've already forked Curso-lenguaje-python
mareas_bot: Fix errors. Update dependencies. Add enhancements
This commit is contained in:
@@ -1,13 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Bot de Telegram para mostrar la tabla de mareas de tablademareas.com
|
||||||
|
|
||||||
|
Comandos:
|
||||||
|
- /start: Iniciar el bot
|
||||||
|
- /reset: Borrar el historial
|
||||||
|
- /help: Mostrar este mensaje de ayuda
|
||||||
|
"""
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import redis
|
import redis
|
||||||
import json
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from dotenv import load_dotenv
|
||||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
||||||
from telegram.ext import ApplicationBuilder, CommandHandler, CallbackQueryHandler
|
from telegram.ext import ApplicationBuilder, CommandHandler, CallbackQueryHandler
|
||||||
from dotenv import load_dotenv
|
|
||||||
import aiohttp
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
# Cargar variables de entorno
|
# Cargar variables de entorno
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -20,20 +30,36 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Excluir mensajes de nivel INFO de httpx
|
# Excluir mensajes de nivel INFO de httpx
|
||||||
|
|
||||||
|
|
||||||
class ExcludeInfoFilter(logging.Filter):
|
class ExcludeInfoFilter(logging.Filter):
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
return record.levelno != logging.INFO
|
return record.levelno != logging.INFO
|
||||||
|
|
||||||
|
|
||||||
httpx_logger = logging.getLogger("httpx")
|
httpx_logger = logging.getLogger("httpx")
|
||||||
httpx_logger.addFilter(ExcludeInfoFilter())
|
httpx_logger.addFilter(ExcludeInfoFilter())
|
||||||
|
|
||||||
# Helper: Quitar dominio de la URL para que el callback_data sea corto
|
# Helper: Quitar dominio de la URL para que el callback_data sea corto
|
||||||
|
|
||||||
|
|
||||||
def shorten_url(url):
|
def shorten_url(url):
|
||||||
domain = "https://tablademareas.com"
|
domain = "https://tablademareas.com"
|
||||||
if url.startswith(domain):
|
if url.startswith(domain):
|
||||||
return url[len(domain):]
|
return url[len(domain):]
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
# Helper: Obtener el objeto chat, ya sea de update.message o de update.callback_query.message
|
||||||
|
|
||||||
|
|
||||||
|
def get_chat(update: Update):
|
||||||
|
if update.message:
|
||||||
|
return update.message.chat
|
||||||
|
elif update.callback_query and update.callback_query.message:
|
||||||
|
return update.callback_query.message.chat
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Conexión a Redis
|
# Conexión a Redis
|
||||||
try:
|
try:
|
||||||
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD')
|
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD')
|
||||||
@@ -47,11 +73,14 @@ except Exception as e:
|
|||||||
r = None
|
r = None
|
||||||
|
|
||||||
# Funciones de caché
|
# Funciones de caché
|
||||||
|
|
||||||
|
|
||||||
def cache_set(key, value, expire=3600):
|
def cache_set(key, value, expire=3600):
|
||||||
if r:
|
if r:
|
||||||
r.set(key, json.dumps(value), ex=expire)
|
r.set(key, json.dumps(value), ex=expire)
|
||||||
logging.info(f"🗃️ Guardado en caché: {key}")
|
logging.info(f"🗃️ Guardado en caché: {key}")
|
||||||
|
|
||||||
|
|
||||||
def cache_get(key):
|
def cache_get(key):
|
||||||
if r:
|
if r:
|
||||||
data = r.get(key)
|
data = r.get(key)
|
||||||
@@ -61,8 +90,11 @@ def cache_get(key):
|
|||||||
logging.info(f"🚫 Cache MISS para {key}")
|
logging.info(f"🚫 Cache MISS para {key}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Conexión a PostgreSQL
|
# Conexión a PostgreSQL
|
||||||
DATABASE_URL = os.getenv('DATABASE_URL')
|
DATABASE_URL = os.getenv('DATABASE_URL')
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
try:
|
try:
|
||||||
conn = psycopg2.connect(DATABASE_URL)
|
conn = psycopg2.connect(DATABASE_URL)
|
||||||
@@ -71,6 +103,7 @@ def get_db_connection():
|
|||||||
logging.error(f"❌ Error conectando a PostgreSQL: {e}")
|
logging.error(f"❌ Error conectando a PostgreSQL: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
if not conn:
|
if not conn:
|
||||||
@@ -98,12 +131,15 @@ def init_db():
|
|||||||
""")
|
""")
|
||||||
logging.info("✅ Base de datos inicializada correctamente.")
|
logging.info("✅ Base de datos inicializada correctamente.")
|
||||||
|
|
||||||
|
|
||||||
# Obtener token de Telegram
|
# Obtener token de Telegram
|
||||||
TELEGRAM_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
|
TELEGRAM_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
|
||||||
|
|
||||||
# Función para obtener datos de la web con caché
|
# Función para obtener datos de la web con caché
|
||||||
|
|
||||||
|
|
||||||
async def fetch(url):
|
async def fetch(url):
|
||||||
url = url.split("#")[0]
|
url = url.split("#")[0] # eliminar fragmentos
|
||||||
cached_data = cache_get(url)
|
cached_data = cache_get(url)
|
||||||
if cached_data:
|
if cached_data:
|
||||||
return cached_data
|
return cached_data
|
||||||
@@ -119,6 +155,8 @@ async def fetch(url):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Función para registrar eventos
|
# Función para registrar eventos
|
||||||
|
|
||||||
|
|
||||||
def log_user_event(telegram_id, event_type, event_data):
|
def log_user_event(telegram_id, event_type, event_data):
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
if not conn:
|
if not conn:
|
||||||
@@ -132,6 +170,8 @@ def log_user_event(telegram_id, event_type, event_data):
|
|||||||
logging.info(f"📝 Evento registrado: {event_type} - {event_data}")
|
logging.info(f"📝 Evento registrado: {event_type} - {event_data}")
|
||||||
|
|
||||||
# Comando /start
|
# Comando /start
|
||||||
|
|
||||||
|
|
||||||
async def start(update: Update, context):
|
async def start(update: Update, context):
|
||||||
user = update.message.from_user
|
user = update.message.from_user
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
@@ -149,6 +189,8 @@ async def start(update: Update, context):
|
|||||||
await show_continents(update)
|
await show_continents(update)
|
||||||
|
|
||||||
# Comando /reset
|
# Comando /reset
|
||||||
|
|
||||||
|
|
||||||
async def reset(update: Update, context):
|
async def reset(update: Update, context):
|
||||||
user = update.message.from_user
|
user = update.message.from_user
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
@@ -162,23 +204,42 @@ async def reset(update: Update, context):
|
|||||||
log_user_event(user.id, "reset_command", {})
|
log_user_event(user.id, "reset_command", {})
|
||||||
await update.message.reply_text("Tu historial ha sido borrado.")
|
await update.message.reply_text("Tu historial ha sido borrado.")
|
||||||
|
|
||||||
# Función para obtener continentes
|
# Comando /help
|
||||||
|
|
||||||
|
|
||||||
|
async def help_command(update: Update, context):
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Opciones:\n- /start: Iniciar el bot\n- /reset: Borrar el historial\n- /help: Mostrar este mensaje de ayuda"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Función para obtener continentes (nivel 0)
|
||||||
|
|
||||||
|
|
||||||
async def show_continents(update: Update):
|
async def show_continents(update: Update):
|
||||||
|
chat = get_chat(update)
|
||||||
full_url = "https://tablademareas.com/"
|
full_url = "https://tablademareas.com/"
|
||||||
response_text = await fetch(full_url)
|
response_text = await fetch(full_url)
|
||||||
if response_text is None:
|
if response_text is None:
|
||||||
await update.message.reply_text("Error al obtener los continentes.")
|
if update.message:
|
||||||
|
await update.message.reply_text("Error al obtener los continentes.")
|
||||||
|
else:
|
||||||
|
await update.callback_query.edit_message_text("Error al obtener los continentes.")
|
||||||
return
|
return
|
||||||
soup = BeautifulSoup(response_text, 'html.parser')
|
soup = BeautifulSoup(response_text, 'html.parser')
|
||||||
continentes = soup.select('div#sitios_continentes a')
|
continentes = soup.select('div#sitios_continentes a')
|
||||||
|
|
||||||
keyboard = [[InlineKeyboardButton(c.text.strip(), callback_data=f"continent:{shorten_url(c['href'])}")]
|
keyboard = [[InlineKeyboardButton(c.text.strip(), callback_data=f"continent:{shorten_url(c['href'])}")]
|
||||||
for c in continentes]
|
for c in continentes]
|
||||||
logging.info(f"🌍 Mostrando continentes a {update.message.chat.username}({update.message.chat.id})")
|
logging.info(f"🌍 Mostrando continentes a {chat.username}({chat.id})")
|
||||||
log_user_event(update.message.chat.id, "show_continents", {})
|
log_user_event(chat.id, "show_continents", {})
|
||||||
await update.message.reply_text('Selecciona un continente:', reply_markup=InlineKeyboardMarkup(keyboard))
|
|
||||||
|
if update.message:
|
||||||
|
await update.message.reply_text('Selecciona un continente:', reply_markup=InlineKeyboardMarkup(keyboard))
|
||||||
|
else:
|
||||||
|
await update.callback_query.edit_message_text('Selecciona un continente:', reply_markup=InlineKeyboardMarkup(keyboard))
|
||||||
|
|
||||||
# Callback para continente: mostrar países
|
# Callback para continente: mostrar países
|
||||||
|
|
||||||
|
|
||||||
async def continent_callback(update: Update, context):
|
async def continent_callback(update: Update, context):
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
@@ -190,13 +251,20 @@ async def continent_callback(update: Update, context):
|
|||||||
return
|
return
|
||||||
soup = BeautifulSoup(response_text, 'html.parser')
|
soup = BeautifulSoup(response_text, 'html.parser')
|
||||||
paises = soup.select('a.sitio_reciente_a')
|
paises = soup.select('a.sitio_reciente_a')
|
||||||
keyboard = [[InlineKeyboardButton(p.text.strip(), callback_data=f"country:{shorten_url(p['href'])}")]
|
buttons = sorted(
|
||||||
for p in paises]
|
[InlineKeyboardButton(p.text.strip(
|
||||||
logging.info(f"🌎 {query.from_user.username} ({query.from_user.id}) seleccionó un continente")
|
), callback_data=f"country:{shorten_url(p['href'])}") for p in paises],
|
||||||
|
key=lambda btn: btn.text.lower()
|
||||||
|
)
|
||||||
|
keyboard = [[btn] for btn in buttons]
|
||||||
|
logging.info(
|
||||||
|
f"🌎 {query.from_user.username} ({query.from_user.id}) seleccionó un continente")
|
||||||
log_user_event(query.from_user.id, "continent_selected", {"url": full_url})
|
log_user_event(query.from_user.id, "continent_selected", {"url": full_url})
|
||||||
await query.edit_message_text('Selecciona un país:', reply_markup=InlineKeyboardMarkup(keyboard))
|
await query.edit_message_text('Selecciona un país:', reply_markup=InlineKeyboardMarkup(keyboard))
|
||||||
|
|
||||||
# Callback para país: mostrar provincias y corregir mensaje
|
# Callback para país: mostrar provincias
|
||||||
|
|
||||||
|
|
||||||
async def country_callback(update: Update, context):
|
async def country_callback(update: Update, context):
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
@@ -208,13 +276,20 @@ async def country_callback(update: Update, context):
|
|||||||
return
|
return
|
||||||
soup = BeautifulSoup(response_text, 'html.parser')
|
soup = BeautifulSoup(response_text, 'html.parser')
|
||||||
provincias = soup.select('a.sitio_reciente_a')
|
provincias = soup.select('a.sitio_reciente_a')
|
||||||
keyboard = [[InlineKeyboardButton(p.text.strip(), callback_data=f"province:{shorten_url(p['href'])}")]
|
buttons = sorted(
|
||||||
for p in provincias]
|
[InlineKeyboardButton(p.text.strip(
|
||||||
logging.info(f"🚢 {query.from_user.username} ({query.from_user.id}) seleccionó un país")
|
), callback_data=f"province:{shorten_url(p['href'])}") for p in provincias],
|
||||||
|
key=lambda btn: btn.text.lower()
|
||||||
|
)
|
||||||
|
keyboard = [[btn] for btn in buttons]
|
||||||
|
logging.info(
|
||||||
|
f"🚢 {query.from_user.username} ({query.from_user.id}) seleccionó un país")
|
||||||
log_user_event(query.from_user.id, "country_selected", {"url": full_url})
|
log_user_event(query.from_user.id, "country_selected", {"url": full_url})
|
||||||
await query.edit_message_text('Selecciona una provincia:', reply_markup=InlineKeyboardMarkup(keyboard))
|
await query.edit_message_text('Selecciona una provincia:', reply_markup=InlineKeyboardMarkup(keyboard))
|
||||||
|
|
||||||
# Callback para provincia: mostrar puertos
|
# Callback para provincia: mostrar puertos
|
||||||
|
|
||||||
|
|
||||||
async def province_callback(update: Update, context):
|
async def province_callback(update: Update, context):
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
@@ -229,33 +304,61 @@ async def province_callback(update: Update, context):
|
|||||||
if not puertos:
|
if not puertos:
|
||||||
await query.edit_message_text("No se encontraron puertos en la página.")
|
await query.edit_message_text("No se encontraron puertos en la página.")
|
||||||
return
|
return
|
||||||
keyboard = [[InlineKeyboardButton(p.text.strip(), callback_data=f"port:{shorten_url(p['href'])}")]
|
buttons = []
|
||||||
for p in puertos]
|
for p in puertos:
|
||||||
logging.info(f"⚓ {query.from_user.username} ({query.from_user.id}) seleccionó una provincia")
|
href = p.get('href')
|
||||||
|
if not href:
|
||||||
|
continue
|
||||||
|
station_container = p.find("div", class_="sitio_estacion")
|
||||||
|
port_name = None
|
||||||
|
if station_container:
|
||||||
|
first_div = station_container.find("div", recursive=False)
|
||||||
|
if first_div:
|
||||||
|
port_name = first_div.get_text(strip=True)
|
||||||
|
if not port_name:
|
||||||
|
port_name = p.text.strip()
|
||||||
|
buttons.append(InlineKeyboardButton(
|
||||||
|
port_name, callback_data=f"port:{shorten_url(href)}"))
|
||||||
|
buttons = sorted(buttons, key=lambda btn: btn.text.lower())
|
||||||
|
keyboard = [[btn] for btn in buttons]
|
||||||
|
logging.info(
|
||||||
|
f"⚓ {query.from_user.username} ({query.from_user.id}) seleccionó una provincia")
|
||||||
log_user_event(query.from_user.id, "province_selected", {"url": full_url})
|
log_user_event(query.from_user.id, "province_selected", {"url": full_url})
|
||||||
await query.edit_message_text('Selecciona un puerto:', reply_markup=InlineKeyboardMarkup(keyboard))
|
await query.edit_message_text('Selecciona un puerto:', reply_markup=InlineKeyboardMarkup(keyboard))
|
||||||
|
|
||||||
# Callback para puerto: acción final (mostrar enlace)
|
# Callback para puerto: acción final
|
||||||
|
|
||||||
|
|
||||||
async def port_callback(update: Update, context):
|
async def port_callback(update: Update, context):
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
short_url = query.data.split(":", 1)[1]
|
short_url = query.data.split(":", 1)[1]
|
||||||
full_url = "https://tablademareas.com" + short_url
|
full_url = "https://tablademareas.com" + short_url
|
||||||
logging.info(f"🚩 {query.from_user.username} ({query.from_user.id}) seleccionó un puerto")
|
logging.info(
|
||||||
|
f"🚩 {query.from_user.username} ({query.from_user.id}) seleccionó un puerto")
|
||||||
log_user_event(query.from_user.id, "port_selected", {"url": full_url})
|
log_user_event(query.from_user.id, "port_selected", {"url": full_url})
|
||||||
await query.edit_message_text(f"Enlace del puerto: {full_url}")
|
await query.edit_message_text(f"Enlace del puerto: {full_url}")
|
||||||
|
|
||||||
|
# Función principal para iniciar el bot
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = ApplicationBuilder().token(TELEGRAM_TOKEN).build()
|
app = ApplicationBuilder().token(TELEGRAM_TOKEN).build()
|
||||||
app.add_handler(CommandHandler("start", start))
|
app.add_handler(CommandHandler("start", start))
|
||||||
app.add_handler(CommandHandler("reset", reset))
|
app.add_handler(CommandHandler("reset", reset))
|
||||||
app.add_handler(CallbackQueryHandler(continent_callback, pattern='^continent:'))
|
app.add_handler(CommandHandler("help", help_command))
|
||||||
app.add_handler(CallbackQueryHandler(country_callback, pattern='^country:'))
|
app.add_handler(CallbackQueryHandler(
|
||||||
app.add_handler(CallbackQueryHandler(province_callback, pattern='^province:'))
|
continent_callback, pattern='^continent:'))
|
||||||
|
app.add_handler(CallbackQueryHandler(
|
||||||
|
country_callback, pattern='^country:'))
|
||||||
|
app.add_handler(CallbackQueryHandler(
|
||||||
|
province_callback, pattern='^province:'))
|
||||||
app.add_handler(CallbackQueryHandler(port_callback, pattern='^port:'))
|
app.add_handler(CallbackQueryHandler(port_callback, pattern='^port:'))
|
||||||
logging.info("🤖 Iniciando el bot...")
|
logging.info("🤖 Iniciando el bot...")
|
||||||
init_db()
|
init_db()
|
||||||
app.run_polling()
|
app.run_polling()
|
||||||
|
|
||||||
|
|
||||||
|
# Ejecutar la función principal si el script se ejecuta directamente
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
aiohttp==3.10.9
|
aiohttp==3.11.12
|
||||||
beautifulsoup4==4.12.3
|
beautifulsoup4==4.13.3
|
||||||
psycopg2-binary==2.9.10
|
psycopg2-binary==2.9.10
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
python-telegram-bot>=20.0
|
python-telegram-bot>=21.10
|
||||||
redis==5.2.1
|
redis==5.2.1
|
||||||
requests==2.31.0
|
requests==2.32.3
|
||||||
Reference in New Issue
Block a user