From 3bcabcce4b9f80ff0888d5376dfe5eff5752b2b0 Mon Sep 17 00:00:00 2001 From: mr-forust Date: Thu, 27 Nov 2025 16:33:15 +0100 Subject: [PATCH] feat: notify on new webinar, storing N last webinars in redis --- edu_master/webinar-checker/checker.py | 103 ++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 15 deletions(-) diff --git a/edu_master/webinar-checker/checker.py b/edu_master/webinar-checker/checker.py index e0217f2..6eb5f50 100644 --- a/edu_master/webinar-checker/checker.py +++ b/edu_master/webinar-checker/checker.py @@ -1,6 +1,7 @@ import os import logging import redis +import json from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes @@ -28,6 +29,7 @@ KEY_WHITELIST = "bot:whitelist" KEY_WHITELIST_ENABLED = "bot:whitelist_enabled" KEY_SUBSCRIBERS = "bot:subscribers" KEY_PHPSESSID = "EDU_PHPSESSID" +KEY_WEBINAR_HISTORY = "bot:webinar_history" # Stores last 5 webinars # Initialize Redis try: @@ -196,6 +198,30 @@ async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): # --- Webinar Checking Job --- +def get_webinar_key(name: str, url: str) -> str: + """Generate unique key for a webinar based on name and URL.""" + return f"{name}|{url}" + +def get_stored_webinars() -> list: + """Get list of stored webinar keys from Redis.""" + data = redis_client.get(KEY_WEBINAR_HISTORY) + if data: + try: + return json.loads(data) + except Exception as e: + logger.error(f"Failed to parse webinar history: {e}") + return [] + +def store_webinars(webinar_keys: list): + """Store up to 5 most recent webinar keys in Redis.""" + # Keep only last 5 + webinar_keys = webinar_keys[-5:] + try: + redis_client.set(KEY_WEBINAR_HISTORY, json.dumps(webinar_keys)) + logger.info(f"Stored {len(webinar_keys)} webinar(s) in history") + except Exception as e: + logger.error(f"Failed to store webinar history: {e}") + async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE): """Background job to check for webinars using Async Playwright. @@ -216,7 +242,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE): # --------------------- return None - webinar_details = [] + current_webinars = [] # List of dicts with name, url, and formatted text content = "" try: @@ -259,9 +285,26 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE): count = await rows.count() for i in range(count): - text = await rows.nth(i).inner_text() + row = rows.nth(i) + text = await row.inner_text() + if "Жодного онлайн уроку зараз" not in text: - webinar_details.append(text.strip()) + # Extract name (topic) from first column + name_elem = row.locator('td').nth(0) + name = await name_elem.inner_text() + name = name.strip() + + # Extract join URL from fourth column + url_elem = row.locator('td').nth(3).locator('a[href*="/webinar/join/"]').first + url = await url_elem.get_attribute('href') + + if name and url: + current_webinars.append({ + 'name': name, + 'url': url, + 'text': text.strip() + }) + logger.info(f"Found webinar: {name} -> {url}") else: logger.info("No webinars found (expected message present)") @@ -287,7 +330,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE): return None # --- DEBUG LOGGING (Saving last response content) --- - if not webinar_details and content: #if no webinars found, save the page content + if not current_webinars and content: #if no webinars found, save the page content try: with open('response.html', 'w', encoding='utf-8') as f: f.write(content) @@ -296,19 +339,49 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE): logger.error(f"Failed to write debug HTML: {e}") # ----------------------------------------------------- - # Notify subscribers if webinars found - if webinar_details: - message = "🎓 Найден вебинар!\n\n" + "\n\n".join([f"📌 {d}" for d in webinar_details]) - subscribers = redis_client.smembers(KEY_SUBSCRIBERS) + # Check for NEW webinars and notify + if current_webinars: + # Get stored webinar history + stored_keys = get_stored_webinars() + logger.info(f"Stored webinar keys: {stored_keys}") - for sub_id in subscribers: - try: - await context.bot.send_message(chat_id=sub_id, text=message, parse_mode='HTML') - logger.info(f"Notification sent to {sub_id}") - except Exception as e: - logger.error(f"Failed to send to {sub_id}: {e}") + # Find new webinars (not in history) + new_webinars = [] + current_keys = [] + + for webinar in current_webinars: + key = get_webinar_key(webinar['name'], webinar['url']) + current_keys.append(key) + + if key not in stored_keys: + new_webinars.append(webinar) + logger.info(f"NEW webinar detected: {webinar['name']}") + + # Update stored history with current webinars + # Merge old and new, keeping only last 5 + updated_keys = stored_keys + [k for k in current_keys if k not in stored_keys] + store_webinars(updated_keys) + + # Notify subscribers ONLY about NEW webinars + if new_webinars: + message = "🎓 Новий вебінар!\n\n" + "\n\n".join([ + f"📌 {w['name']}\n🔗 https://edu.edu.vn.ua{w['url']}" + for w in new_webinars + ]) + + subscribers = redis_client.smembers(KEY_SUBSCRIBERS) + logger.info(f"Sending notification about {len(new_webinars)} new webinar(s) to {len(subscribers)} subscriber(s)") + + for sub_id in subscribers: + try: + await context.bot.send_message(chat_id=sub_id, text=message, parse_mode='HTML') + logger.info(f"Notification sent to {sub_id}") + except Exception as e: + logger.error(f"Failed to send to {sub_id}: {e}") + else: + logger.info(f"Found {len(current_webinars)} webinar(s), but all are already known") - return len(webinar_details) + return len(current_webinars) # --- Main ---