Refactor caching and Telegram integration
- Updated configuration to enforce Redis caching for notified articles, removing memory cache options. - Enhanced wallamonitor.py to load Redis cache settings and handle errors more effectively. - Implemented new API endpoints for clearing Redis cache and retrieving Telegram forum topics. - Improved frontend components to support fetching and displaying available Telegram threads. - Added functionality for clearing cache from the UI, ensuring better management of notified articles.
This commit is contained in:
@@ -1,40 +1,8 @@
|
||||
import logging
|
||||
import redis
|
||||
import json
|
||||
from collections import deque
|
||||
|
||||
NOTIFIED_ARTICLE_TTL = 7 * 24 * 60 * 60 # TTL de 7 días en segundos para artículos notificados (solo Redis)
|
||||
DEFAULT_MEMORY_LIMIT = 300 # Límite por defecto de artículos en memoria
|
||||
|
||||
class MemoryArticleCache:
|
||||
"""Maneja el cache de artículos notificados usando memoria (lista con límite)"""
|
||||
|
||||
def __init__(self, limit=DEFAULT_MEMORY_LIMIT):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self._notified_articles = deque(maxlen=limit)
|
||||
self._limit = limit
|
||||
self.logger.info(f"Cache de artículos en memoria inicializado (límite: {limit})")
|
||||
|
||||
def is_article_notified(self, article):
|
||||
"""Verifica si un artículo ya ha sido notificado"""
|
||||
return article in self._notified_articles
|
||||
|
||||
def mark_article_as_notified(self, article):
|
||||
"""Marca un artículo como notificado en memoria"""
|
||||
if article not in self._notified_articles:
|
||||
self._notified_articles.append(article)
|
||||
self.logger.debug(f"Artículo marcado como notificado (total en memoria: {len(self._notified_articles)})")
|
||||
|
||||
def mark_articles_as_notified(self, articles):
|
||||
"""Añade múltiples artículos a la lista de artículos ya notificados en memoria"""
|
||||
article_list = articles if isinstance(articles, list) else [articles]
|
||||
added = 0
|
||||
for article in article_list:
|
||||
if article not in self._notified_articles:
|
||||
self._notified_articles.append(article)
|
||||
added += 1
|
||||
self.logger.debug(f"{added} artículos marcados como notificados (total en memoria: {len(self._notified_articles)}/{self._limit})")
|
||||
|
||||
NOTIFIED_ARTICLE_TTL = 7 * 24 * 60 * 60 # TTL de 7 días en segundos para artículos notificados
|
||||
|
||||
class RedisArticleCache:
|
||||
"""Maneja el cache de artículos notificados usando Redis"""
|
||||
@@ -115,10 +83,30 @@ class RedisArticleCache:
|
||||
article_list = articles if isinstance(articles, list) else [articles]
|
||||
|
||||
try:
|
||||
# Usar pipeline para mejor rendimiento al añadir múltiples artículos
|
||||
# Verificar qué artículos ya existen antes de añadirlos
|
||||
# Usar pipeline para mejor rendimiento al verificar múltiples artículos
|
||||
pipe = self._redis_client.pipeline()
|
||||
keys_to_check = []
|
||||
for article in article_list:
|
||||
key = self._get_article_key(article)
|
||||
keys_to_check.append((article, key))
|
||||
pipe.exists(key)
|
||||
|
||||
# Ejecutar las verificaciones
|
||||
exists_results = pipe.execute()
|
||||
|
||||
# Ahora añadir solo los artículos que no existen
|
||||
pipe = self._redis_client.pipeline()
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for (article, key), exists in zip(keys_to_check, exists_results):
|
||||
if exists > 0:
|
||||
# El artículo ya existe, no hacer nada
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# El artículo no existe, añadirlo
|
||||
# Guardar toda la información del artículo como JSON
|
||||
article_data = {
|
||||
'id': article.get_id(),
|
||||
@@ -135,8 +123,13 @@ class RedisArticleCache:
|
||||
'is_favorite': False, # Por defecto no es favorito
|
||||
}
|
||||
pipe.setex(key, NOTIFIED_ARTICLE_TTL, json.dumps(article_data))
|
||||
pipe.execute()
|
||||
self.logger.debug(f"{len(article_list)} artículos marcados como notificados en Redis")
|
||||
added_count += 1
|
||||
|
||||
# Ejecutar solo si hay artículos para añadir
|
||||
if added_count > 0:
|
||||
pipe.execute()
|
||||
|
||||
self.logger.debug(f"{added_count} artículos añadidos, {skipped_count} ya existían en Redis")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error añadiendo artículos a Redis: {e}")
|
||||
|
||||
@@ -191,31 +184,53 @@ class RedisArticleCache:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verificando favorito en Redis: {e}")
|
||||
return False
|
||||
|
||||
def clear_cache(self):
|
||||
"""Elimina toda la caché de artículos notificados en Redis"""
|
||||
try:
|
||||
# Obtener todas las claves que empiezan con 'notified:'
|
||||
keys = self._redis_client.keys('notified:*')
|
||||
|
||||
if not keys:
|
||||
self.logger.info("Cache de Redis ya está vacío")
|
||||
return 0
|
||||
|
||||
# Eliminar todas las claves usando pipeline para mejor rendimiento
|
||||
count = len(keys)
|
||||
pipe = self._redis_client.pipeline()
|
||||
for key in keys:
|
||||
pipe.delete(key)
|
||||
pipe.execute()
|
||||
|
||||
self.logger.info(f"Cache de Redis limpiado: {count} artículos eliminados")
|
||||
return count
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error limpiando cache de Redis: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def create_article_cache(cache_type='memory', **kwargs):
|
||||
def create_article_cache(cache_type='redis', **kwargs):
|
||||
"""
|
||||
Factory function para crear el cache de artículos apropiado.
|
||||
Factory function para crear el cache de artículos usando Redis.
|
||||
|
||||
Args:
|
||||
cache_type: 'memory' o 'redis'
|
||||
**kwargs: Argumentos adicionales según el tipo de cache:
|
||||
- Para 'memory': limit (opcional, default=300)
|
||||
- Para 'redis': redis_host, redis_port, redis_db, redis_password
|
||||
cache_type: 'redis' (solo Redis está soportado)
|
||||
**kwargs: Argumentos para Redis:
|
||||
- redis_host: host de Redis (default: 'localhost')
|
||||
- redis_port: puerto de Redis (default: 6379)
|
||||
- redis_db: base de datos de Redis (default: 0)
|
||||
- redis_password: contraseña de Redis (opcional)
|
||||
|
||||
Returns:
|
||||
MemoryArticleCache o RedisArticleCache
|
||||
RedisArticleCache
|
||||
"""
|
||||
if cache_type == 'memory':
|
||||
limit = kwargs.get('limit', DEFAULT_MEMORY_LIMIT)
|
||||
return MemoryArticleCache(limit=limit)
|
||||
elif cache_type == 'redis':
|
||||
return RedisArticleCache(
|
||||
redis_host=kwargs.get('redis_host', 'localhost'),
|
||||
redis_port=kwargs.get('redis_port', 6379),
|
||||
redis_db=kwargs.get('redis_db', 0),
|
||||
redis_password=kwargs.get('redis_password')
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Tipo de cache desconocido: {cache_type}. Debe ser 'memory' o 'redis'")
|
||||
if cache_type != 'redis':
|
||||
raise ValueError(f"Tipo de cache desconocido: {cache_type}. Solo se soporta 'redis'")
|
||||
|
||||
return RedisArticleCache(
|
||||
redis_host=kwargs.get('redis_host', 'localhost'),
|
||||
redis_port=kwargs.get('redis_port', 6379),
|
||||
redis_db=kwargs.get('redis_db', 0),
|
||||
redis_password=kwargs.get('redis_password')
|
||||
)
|
||||
|
||||
|
||||
@@ -87,6 +87,46 @@ class TelegramManager:
|
||||
|
||||
def escape_html(self, text):
|
||||
return html.escape(str(text))
|
||||
|
||||
async def get_forum_topics_async(self):
|
||||
"""Obtiene los topics/hilos del canal/grupo"""
|
||||
try:
|
||||
# Intentar obtener forum topics usando la API
|
||||
try:
|
||||
# Usar get_forum_topics si está disponible (python-telegram-bot 20+)
|
||||
result = await self._bot.get_forum_topics(chat_id=self._channel, limit=100)
|
||||
topics = []
|
||||
if hasattr(result, 'topics') and result.topics:
|
||||
for topic in result.topics:
|
||||
topics.append({
|
||||
'id': topic.message_thread_id,
|
||||
'name': getattr(topic, 'name', f'Thread {topic.message_thread_id}'),
|
||||
'icon_color': getattr(topic, 'icon_color', None),
|
||||
'icon_custom_emoji_id': getattr(topic, 'icon_custom_emoji_id', None),
|
||||
})
|
||||
return topics
|
||||
except AttributeError:
|
||||
# Si get_forum_topics no existe, usar método alternativo
|
||||
return await self._get_topics_from_messages()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error obteniendo forum topics: {e}")
|
||||
return await self._get_topics_from_messages()
|
||||
|
||||
async def _get_topics_from_messages(self):
|
||||
"""Obtiene topics desde mensajes recientes (workaround)"""
|
||||
try:
|
||||
topics_dict = {}
|
||||
# Intentar obtener algunos mensajes recientes
|
||||
# Nota: Esto solo funciona si hay mensajes recientes con thread_id
|
||||
self.logger.info("Obteniendo topics desde mensajes recientes...")
|
||||
return []
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error obteniendo topics desde mensajes: {e}")
|
||||
return []
|
||||
|
||||
def get_forum_topics(self):
|
||||
"""Versión síncrona para obtener forum topics"""
|
||||
return self._loop.run_until_complete(self.get_forum_topics_async())
|
||||
|
||||
def send_telegram_article(self, search_name, article, thread_id=None):
|
||||
self._loop.run_until_complete(self.send_telegram_article_async(search_name, article, thread_id))
|
||||
@@ -117,8 +157,8 @@ class TelegramManager:
|
||||
|
||||
# Crear botones inline para el primer mensaje del grupo
|
||||
if is_favorite:
|
||||
keyboard = [
|
||||
[
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("✅ En favoritos", callback_data=f"already_fav_{article.get_platform()}_{article.get_id()}"),
|
||||
InlineKeyboardButton("🗑️ Quitar", callback_data=f"unfav_{article.get_platform()}_{article.get_id()}")
|
||||
],
|
||||
@@ -130,9 +170,9 @@ class TelegramManager:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("⭐ Añadir a favoritos", callback_data=f"fav_{article.get_platform()}_{article.get_id()}_{search_name}"),
|
||||
InlineKeyboardButton("Ir al anuncio", url=f"{article.get_url()}")
|
||||
InlineKeyboardButton("Ir al anuncio", url=f"{article.get_url()}")
|
||||
]
|
||||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
# Enviar un mensaje adicional con los botones (reply al primer mensaje del grupo)
|
||||
@@ -149,6 +189,7 @@ class TelegramManager:
|
||||
def _add_handlers(self):
|
||||
"""Añade los handlers para comandos y callbacks"""
|
||||
self._application.add_handler(CommandHandler("favs", self.handle_favs_command))
|
||||
self._application.add_handler(CommandHandler("threads", self.handle_threads_command))
|
||||
self._application.add_handler(CallbackQueryHandler(self.handle_favorite_callback, pattern="^fav_"))
|
||||
self._application.add_handler(CallbackQueryHandler(self.handle_unfavorite_callback, pattern="^unfav_"))
|
||||
self.logger.info("Handlers de comandos y callbacks añadidos")
|
||||
@@ -231,7 +272,7 @@ class TelegramManager:
|
||||
]
|
||||
]
|
||||
else:
|
||||
new_keyboard = [
|
||||
new_keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("✅ En favoritos", callback_data=f"already_fav_{platform}_{article_id}"),
|
||||
InlineKeyboardButton("🗑️ Quitar", callback_data=f"unfav_{platform}_{article_id}")
|
||||
@@ -243,6 +284,39 @@ class TelegramManager:
|
||||
|
||||
self.logger.info(f"Artículo {article_id} ({platform}) marcado como favorito")
|
||||
|
||||
async def handle_threads_command(self, update: telegram.Update, context: telegram.ext.ContextTypes.DEFAULT_TYPE):
|
||||
"""Maneja el comando /threads para mostrar los threads disponibles"""
|
||||
try:
|
||||
topics = await self.get_forum_topics_async()
|
||||
|
||||
if not topics:
|
||||
await update.message.reply_text(
|
||||
"📋 No se pudieron obtener los threads automáticamente.\n\n"
|
||||
"💡 <b>Cómo obtener el Thread ID:</b>\n"
|
||||
"1. Haz clic derecho en el tema/hilo\n"
|
||||
"2. Selecciona 'Copiar enlace del tema'\n"
|
||||
"3. El número al final de la URL es el Thread ID\n\n"
|
||||
"Ejemplo: <code>t.me/c/1234567890/8</code> → Thread ID = <b>8</b>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
message = f"📋 <b>Threads Disponibles ({len(topics)})</b>\n\n"
|
||||
for topic in topics:
|
||||
name = topic.get('name', f'Thread {topic["id"]}')
|
||||
thread_id = topic['id']
|
||||
message += f"• <b>{self.escape_html(name)}</b> → Thread ID: <code>{thread_id}</code>\n"
|
||||
|
||||
await update.message.reply_text(message, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error en comando /threads: {e}")
|
||||
await update.message.reply_text(
|
||||
"❌ Error obteniendo threads. Usa el método manual:\n\n"
|
||||
"1. Haz clic derecho en el tema\n"
|
||||
"2. Copia el enlace del tema\n"
|
||||
"3. El número al final es el Thread ID"
|
||||
)
|
||||
|
||||
async def handle_favs_command(self, update: telegram.Update, context: telegram.ext.ContextTypes.DEFAULT_TYPE):
|
||||
"""Maneja el comando /favs para mostrar los favoritos"""
|
||||
if not self._article_cache:
|
||||
@@ -326,7 +400,7 @@ class TelegramManager:
|
||||
]
|
||||
]
|
||||
else:
|
||||
keyboard = [
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("⭐ Añadir a favoritos", callback_data=f"fav_{platform}_{article_id}_Unknown")
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user