feat: notify on new webinar, storing N last webinars in redis

This commit is contained in:
2025-11-27 16:33:15 +01:00
parent a54b7b000f
commit 3bcabcce4b
+88 -15
View File
@@ -1,6 +1,7 @@
import os import os
import logging import logging
import redis import redis
import json
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
@@ -28,6 +29,7 @@ KEY_WHITELIST = "bot:whitelist"
KEY_WHITELIST_ENABLED = "bot:whitelist_enabled" KEY_WHITELIST_ENABLED = "bot:whitelist_enabled"
KEY_SUBSCRIBERS = "bot:subscribers" KEY_SUBSCRIBERS = "bot:subscribers"
KEY_PHPSESSID = "EDU_PHPSESSID" KEY_PHPSESSID = "EDU_PHPSESSID"
KEY_WEBINAR_HISTORY = "bot:webinar_history" # Stores last 5 webinars
# Initialize Redis # Initialize Redis
try: try:
@@ -196,6 +198,30 @@ async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
# --- Webinar Checking Job --- # --- 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): async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
"""Background job to check for webinars using Async Playwright. """Background job to check for webinars using Async Playwright.
@@ -216,7 +242,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
# --------------------- # ---------------------
return None return None
webinar_details = [] current_webinars = [] # List of dicts with name, url, and formatted text
content = "" content = ""
try: try:
@@ -259,9 +285,26 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
count = await rows.count() count = await rows.count()
for i in range(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: 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: else:
logger.info("No webinars found (expected message present)") logger.info("No webinars found (expected message present)")
@@ -287,7 +330,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
return None return None
# --- DEBUG LOGGING (Saving last response content) --- # --- 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: try:
with open('response.html', 'w', encoding='utf-8') as f: with open('response.html', 'w', encoding='utf-8') as f:
f.write(content) 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}") logger.error(f"Failed to write debug HTML: {e}")
# ----------------------------------------------------- # -----------------------------------------------------
# Notify subscribers if webinars found # Check for NEW webinars and notify
if webinar_details: if current_webinars:
message = "🎓 <b>Найден вебинар!</b>\n\n" + "\n\n".join([f"📌 {d}" for d in webinar_details]) # Get stored webinar history
subscribers = redis_client.smembers(KEY_SUBSCRIBERS) stored_keys = get_stored_webinars()
logger.info(f"Stored webinar keys: {stored_keys}")
for sub_id in subscribers: # Find new webinars (not in history)
try: new_webinars = []
await context.bot.send_message(chat_id=sub_id, text=message, parse_mode='HTML') current_keys = []
logger.info(f"Notification sent to {sub_id}")
except Exception as e:
logger.error(f"Failed to send to {sub_id}: {e}")
return len(webinar_details) 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 = "🎓 <b>Новий вебінар!</b>\n\n" + "\n\n".join([
f"📌 <b>{w['name']}</b>\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(current_webinars)
# --- Main --- # --- Main ---