feat: notify on new webinar, storing N last webinars in redis
This commit is contained in:
@@ -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 ---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user