Refactor favorites management to use Redis
- Removed local favorites.json file and related file handling in the code. - Implemented Redis caching for managing favorite articles, including methods to set, get, and check favorites. - Updated TelegramManager and server API to interact with Redis for favorite operations. - Added search functionality for articles in Redis, enhancing user experience. - Adjusted frontend components to support searching and displaying articles from Redis.
This commit is contained in:
@@ -82,6 +82,16 @@ class RedisArticleCache:
|
||||
try:
|
||||
key = self._get_article_key(article)
|
||||
# Guardar toda la información del artículo como JSON
|
||||
# Verificar si el artículo ya existe para mantener el estado de favorito
|
||||
existing_value = self._redis_client.get(key)
|
||||
is_favorite = False
|
||||
if existing_value:
|
||||
try:
|
||||
existing_data = json.loads(existing_value)
|
||||
is_favorite = existing_data.get('is_favorite', False)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
article_data = {
|
||||
'id': article.get_id(),
|
||||
'title': article.get_title(),
|
||||
@@ -94,6 +104,7 @@ class RedisArticleCache:
|
||||
'images': article.get_images(),
|
||||
'modified_at': article.get_modified_at(),
|
||||
'platform': article.get_platform(),
|
||||
'is_favorite': is_favorite, # Mantener el estado de favorito
|
||||
}
|
||||
self._redis_client.setex(key, NOTIFIED_ARTICLE_TTL, json.dumps(article_data))
|
||||
except Exception as e:
|
||||
@@ -121,12 +132,65 @@ class RedisArticleCache:
|
||||
'images': article.get_images(),
|
||||
'modified_at': article.get_modified_at(),
|
||||
'platform': article.get_platform(),
|
||||
'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")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error añadiendo artículos a Redis: {e}")
|
||||
|
||||
def set_favorite(self, platform, article_id, is_favorite=True):
|
||||
"""Marca o desmarca un artículo como favorito en Redis"""
|
||||
try:
|
||||
key = f"notified:{platform}:{article_id}"
|
||||
value = self._redis_client.get(key)
|
||||
if value:
|
||||
article_data = json.loads(value)
|
||||
article_data['is_favorite'] = is_favorite
|
||||
# Mantener el TTL existente o usar el default
|
||||
ttl = self._redis_client.ttl(key)
|
||||
if ttl > 0:
|
||||
self._redis_client.setex(key, ttl, json.dumps(article_data))
|
||||
else:
|
||||
self._redis_client.setex(key, NOTIFIED_ARTICLE_TTL, json.dumps(article_data))
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error marcando favorito en Redis: {e}")
|
||||
return False
|
||||
|
||||
def get_favorites(self):
|
||||
"""Obtiene todos los artículos marcados como favoritos"""
|
||||
try:
|
||||
keys = self._redis_client.keys('notified:*')
|
||||
favorites = []
|
||||
for key in keys:
|
||||
value = self._redis_client.get(key)
|
||||
if value:
|
||||
try:
|
||||
article_data = json.loads(value)
|
||||
if article_data.get('is_favorite', False):
|
||||
favorites.append(article_data)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return favorites
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error obteniendo favoritos de Redis: {e}")
|
||||
return []
|
||||
|
||||
def is_favorite(self, platform, article_id):
|
||||
"""Verifica si un artículo es favorito"""
|
||||
try:
|
||||
key = f"notified:{platform}:{article_id}"
|
||||
value = self._redis_client.get(key)
|
||||
if value:
|
||||
article_data = json.loads(value)
|
||||
return article_data.get('is_favorite', False)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verificando favorito en Redis: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def create_article_cache(cache_type='memory', **kwargs):
|
||||
|
||||
@@ -10,6 +10,7 @@ import json
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from managers.article_cache import create_article_cache
|
||||
|
||||
ITEM_HTML = """
|
||||
<b>Artículo:</b> {title}
|
||||
@@ -24,8 +25,6 @@ ITEM_HTML = """
|
||||
<b>{price} {currency}</b>
|
||||
"""
|
||||
|
||||
FAVORITES_FILE = "favorites.json"
|
||||
|
||||
class TelegramManager:
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
@@ -43,9 +42,8 @@ class TelegramManager:
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
|
||||
# Inicializar archivo de favoritos
|
||||
self._favorites_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), FAVORITES_FILE)
|
||||
self._ensure_favorites_file()
|
||||
# Inicializar Redis para favoritos
|
||||
self._article_cache = self._init_redis_cache()
|
||||
|
||||
# Añadir handlers para comandos y callbacks
|
||||
self._add_handlers()
|
||||
@@ -61,6 +59,31 @@ class TelegramManager:
|
||||
token = config['telegram_token']
|
||||
telegram_channel = config['telegram_channel']
|
||||
return token, telegram_channel
|
||||
|
||||
def _init_redis_cache(self):
|
||||
"""Inicializa Redis cache para favoritos"""
|
||||
try:
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_file = os.path.join(os.path.dirname(base_dir), 'config.yaml')
|
||||
with open(config_file, 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
cache_config = config.get('cache', {})
|
||||
if cache_config.get('type') == 'redis':
|
||||
redis_config = cache_config.get('redis', {})
|
||||
return create_article_cache(
|
||||
cache_type='redis',
|
||||
redis_host=redis_config.get('host', 'localhost'),
|
||||
redis_port=redis_config.get('port', 6379),
|
||||
redis_db=redis_config.get('db', 0),
|
||||
redis_password=redis_config.get('password')
|
||||
)
|
||||
else:
|
||||
self.logger.warning("Redis no configurado para favoritos, se requiere Redis")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error inicializando Redis para favoritos: {e}")
|
||||
return None
|
||||
|
||||
def escape_html(self, text):
|
||||
return html.escape(str(text))
|
||||
@@ -87,13 +110,29 @@ class TelegramManager:
|
||||
first_image_url = images_url[0] if images_url else None
|
||||
|
||||
|
||||
# Verificar si el artículo ya es favorito
|
||||
is_favorite = False
|
||||
if self._article_cache:
|
||||
is_favorite = self._article_cache.is_favorite(article.get_platform(), article.get_id())
|
||||
|
||||
# Crear botones inline para el primer mensaje del grupo
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("⭐ Añadir a favoritos", callback_data=f"fav_{article.get_id()}_{search_name}"),
|
||||
InlineKeyboardButton("Ir al anuncio", url=f"{article.get_url()}")
|
||||
if is_favorite:
|
||||
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()}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("Ir al anuncio", url=f"{article.get_url()}")
|
||||
]
|
||||
]
|
||||
else:
|
||||
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()}")
|
||||
]
|
||||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
# Enviar un mensaje adicional con los botones (reply al primer mensaje del grupo)
|
||||
@@ -106,30 +145,6 @@ class TelegramManager:
|
||||
message_thread_id=thread_id
|
||||
)
|
||||
|
||||
def _ensure_favorites_file(self):
|
||||
"""Crea el archivo de favoritos si no existe"""
|
||||
if not os.path.exists(self._favorites_file):
|
||||
with open(self._favorites_file, 'w') as f:
|
||||
json.dump([], f)
|
||||
self.logger.info(f"Archivo de favoritos creado: {self._favorites_file}")
|
||||
|
||||
def _load_favorites(self):
|
||||
"""Carga los favoritos desde el archivo JSON"""
|
||||
try:
|
||||
with open(self._favorites_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error al cargar favoritos: {e}")
|
||||
return []
|
||||
|
||||
def _save_favorites(self, favorites):
|
||||
"""Guarda los favoritos en el archivo JSON"""
|
||||
try:
|
||||
with open(self._favorites_file, 'w') as f:
|
||||
json.dump(favorites, f, indent=2, ensure_ascii=False)
|
||||
self.logger.info(f"Favoritos guardados: {len(favorites)} items")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error al guardar favoritos: {e}")
|
||||
|
||||
def _add_handlers(self):
|
||||
"""Añade los handlers para comandos y callbacks"""
|
||||
@@ -163,63 +178,78 @@ class TelegramManager:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
# Extraer el ID del artículo y el nombre de búsqueda del callback_data
|
||||
callback_data = query.data
|
||||
parts = callback_data.split("_", 2) # fav_ID_search_name
|
||||
if not self._article_cache:
|
||||
await query.edit_message_text("❌ Redis no está disponible para favoritos")
|
||||
return
|
||||
|
||||
if len(parts) < 3:
|
||||
# Extraer plataforma, ID del artículo y nombre de búsqueda del callback_data
|
||||
# Formato: fav_platform_id_search_name
|
||||
callback_data = query.data
|
||||
parts = callback_data.split("_", 3)
|
||||
|
||||
if len(parts) < 4:
|
||||
await query.edit_message_text("❌ Error al procesar favorito")
|
||||
return
|
||||
|
||||
article_id = parts[1]
|
||||
search_name = parts[2]
|
||||
platform = parts[1]
|
||||
article_id = parts[2]
|
||||
search_name = parts[3] if len(parts) > 3 else "Unknown"
|
||||
|
||||
# Ahora el mensaje original es el mismo mensaje del keyboard
|
||||
original_message = query.message
|
||||
|
||||
if not original_message:
|
||||
await query.edit_message_text("❌ No se pudo encontrar el mensaje original")
|
||||
return
|
||||
|
||||
# Extraer información del caption
|
||||
caption = original_message.caption or ""
|
||||
|
||||
# Crear el objeto favorito
|
||||
favorite = {
|
||||
"id": article_id,
|
||||
"search_name": search_name,
|
||||
"caption": caption,
|
||||
"date_added": datetime.now().isoformat(),
|
||||
"message_link": f"https://t.me/c/{str(self._channel).replace('-100', '')}/{original_message.message_id}"
|
||||
}
|
||||
|
||||
# Cargar favoritos existentes
|
||||
favorites = self._load_favorites()
|
||||
|
||||
# Verificar si ya existe
|
||||
if any(fav["id"] == article_id for fav in favorites):
|
||||
# Verificar si ya es favorito
|
||||
if self._article_cache.is_favorite(platform, article_id):
|
||||
await query.message.reply_text("ℹ️ Este artículo ya está en favoritos")
|
||||
return
|
||||
|
||||
# Añadir nuevo favorito
|
||||
favorites.append(favorite)
|
||||
self._save_favorites(favorites)
|
||||
# Obtener la URL del artículo desde Redis
|
||||
url = ""
|
||||
try:
|
||||
redis_client = self._article_cache._redis_client
|
||||
key = f"notified:{platform}:{article_id}"
|
||||
value = redis_client.get(key)
|
||||
if value:
|
||||
article_data = json.loads(value)
|
||||
url = article_data.get('url', '')
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error obteniendo URL: {e}")
|
||||
|
||||
# Marcar como favorito en Redis
|
||||
success = self._article_cache.set_favorite(platform, article_id, is_favorite=True)
|
||||
|
||||
if not success:
|
||||
await query.edit_message_text("❌ No se pudo encontrar el artículo en Redis")
|
||||
return
|
||||
|
||||
# Actualizar el mensaje del botón
|
||||
new_keyboard = [
|
||||
[InlineKeyboardButton("✅ En favoritos", callback_data=f"already_fav_{article_id}")],
|
||||
[InlineKeyboardButton("🗑️ Quitar de favoritos", callback_data=f"unfav_{article_id}")]
|
||||
]
|
||||
# Como el mensaje original es una foto con caption, usar edit_message_reply_markup
|
||||
if url:
|
||||
new_keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("✅ En favoritos", callback_data=f"already_fav_{platform}_{article_id}"),
|
||||
InlineKeyboardButton("🗑️ Quitar", callback_data=f"unfav_{platform}_{article_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("Ir al anuncio", url=url)
|
||||
]
|
||||
]
|
||||
else:
|
||||
new_keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("✅ En favoritos", callback_data=f"already_fav_{platform}_{article_id}"),
|
||||
InlineKeyboardButton("🗑️ Quitar", callback_data=f"unfav_{platform}_{article_id}")
|
||||
]
|
||||
]
|
||||
await query.edit_message_reply_markup(
|
||||
reply_markup=InlineKeyboardMarkup(new_keyboard)
|
||||
)
|
||||
|
||||
self.logger.info(f"Artículo {article_id} añadido a favoritos")
|
||||
self.logger.info(f"Artículo {article_id} ({platform}) marcado como favorito")
|
||||
|
||||
async def handle_favs_command(self, update: telegram.Update, context: telegram.ext.ContextTypes.DEFAULT_TYPE):
|
||||
"""Maneja el comando /favs para mostrar los favoritos"""
|
||||
favorites = self._load_favorites()
|
||||
if not self._article_cache:
|
||||
await update.message.reply_text("❌ Redis no está disponible para favoritos")
|
||||
return
|
||||
|
||||
favorites = self._article_cache.get_favorites()
|
||||
|
||||
if not favorites:
|
||||
await update.message.reply_text("📭 No tienes favoritos guardados aún")
|
||||
@@ -229,17 +259,15 @@ class TelegramManager:
|
||||
message = f"⭐ <b>Tus Favoritos ({len(favorites)})</b>\n\n"
|
||||
|
||||
for idx, fav in enumerate(favorites, 1):
|
||||
# Extraer título del caption (está después de "Artículo:")
|
||||
caption_lines = fav["caption"].split("\n")
|
||||
title = "Sin título"
|
||||
for line in caption_lines:
|
||||
if line.startswith("<b>Artículo:</b>"):
|
||||
title = line.replace("<b>Artículo:</b>", "").strip()
|
||||
break
|
||||
title = fav.get('title', 'Sin título')
|
||||
platform = fav.get('platform', 'N/A').upper()
|
||||
price = fav.get('price', 'N/A')
|
||||
currency = fav.get('currency', '€')
|
||||
url = fav.get('url', '#')
|
||||
|
||||
message += f"{idx}. {title}\n"
|
||||
message += f" 📂 Búsqueda: <i>{fav['search_name']}</i>\n"
|
||||
message += f" 🔗 <a href='{fav['message_link']}'>Ver mensaje</a>\n\n"
|
||||
message += f"{idx}. <b>{self.escape_html(title)}</b>\n"
|
||||
message += f" 🏷️ {platform} - {price} {currency}\n"
|
||||
message += f" 🔗 <a href='{url}'>Ver anuncio</a>\n\n"
|
||||
|
||||
# Dividir el mensaje si es muy largo
|
||||
if len(message) > 4000:
|
||||
@@ -254,37 +282,57 @@ class TelegramManager:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
# Extraer el ID del artículo del callback_data
|
||||
callback_data = query.data
|
||||
parts = callback_data.split("_", 1) # unfav_ID
|
||||
if not self._article_cache:
|
||||
await query.edit_message_text("❌ Redis no está disponible para favoritos")
|
||||
return
|
||||
|
||||
if len(parts) < 2:
|
||||
# Extraer plataforma e ID del artículo del callback_data
|
||||
# Formato: unfav_platform_id
|
||||
callback_data = query.data
|
||||
parts = callback_data.split("_", 2)
|
||||
|
||||
if len(parts) < 3:
|
||||
await query.edit_message_text("❌ Error al procesar")
|
||||
return
|
||||
|
||||
article_id = parts[1]
|
||||
platform = parts[1]
|
||||
article_id = parts[2]
|
||||
|
||||
# Cargar favoritos existentes
|
||||
favorites = self._load_favorites()
|
||||
# Obtener la URL del artículo desde Redis antes de desmarcar
|
||||
url = ""
|
||||
try:
|
||||
redis_client = self._article_cache._redis_client
|
||||
key = f"notified:{platform}:{article_id}"
|
||||
value = redis_client.get(key)
|
||||
if value:
|
||||
article_data = json.loads(value)
|
||||
url = article_data.get('url', '')
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error obteniendo URL: {e}")
|
||||
|
||||
# Buscar y eliminar el favorito
|
||||
original_count = len(favorites)
|
||||
favorites = [fav for fav in favorites if fav["id"] != article_id]
|
||||
# Desmarcar como favorito en Redis
|
||||
success = self._article_cache.set_favorite(platform, article_id, is_favorite=False)
|
||||
|
||||
if len(favorites) == original_count:
|
||||
if not success:
|
||||
await query.edit_message_text("ℹ️ Este artículo no estaba en favoritos")
|
||||
return
|
||||
|
||||
# Guardar favoritos actualizados
|
||||
self._save_favorites(favorites)
|
||||
|
||||
# Restaurar el botón original
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("⭐ Añadir a favoritos", callback_data=f"fav_{article_id}_unknown")]
|
||||
]
|
||||
# Como el mensaje original es una foto con caption, usar edit_message_reply_markup
|
||||
if url:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("⭐ Añadir a favoritos", callback_data=f"fav_{platform}_{article_id}_Unknown"),
|
||||
InlineKeyboardButton("Ir al anuncio", url=url)
|
||||
]
|
||||
]
|
||||
else:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("⭐ Añadir a favoritos", callback_data=f"fav_{platform}_{article_id}_Unknown")
|
||||
]
|
||||
]
|
||||
await query.edit_message_reply_markup(
|
||||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||||
)
|
||||
|
||||
self.logger.info(f"Artículo {article_id} eliminado de favoritos")
|
||||
self.logger.info(f"Artículo {article_id} ({platform}) eliminado de favoritos")
|
||||
Reference in New Issue
Block a user