diff --git a/edu_master/webinar-checker/checker.py b/edu_master/webinar-checker/checker.py
index 65043cf..2daeedc 100644
--- a/edu_master/webinar-checker/checker.py
+++ b/edu_master/webinar-checker/checker.py
@@ -1,7 +1,10 @@
import os
+import re
import logging
import redis
import json
+import time
+from datetime import datetime, timedelta
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ChatMember
from telegram.constants import ChatType
@@ -30,6 +33,7 @@ def _env(key, default=None):
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
EDU_WEBINAR_PATH = _env('EDU_URL_WEBINAR', '/webinar/useractive')
WEBINAR_URL = f"{EDU_BASE.rstrip('/')}/{EDU_WEBINAR_PATH.lstrip('/')}"
+DIARY_URL = f"{EDU_BASE.rstrip('/')}/user/diary"
WEBINAR_CHECK_INTERVAL = int(_env('WEBINAR_CHECK_INTERVAL', 60))
REDIS_HOST = _env('REDIS_HOST', 'redis')
@@ -252,6 +256,205 @@ def get_admin_keyboard(user_id: int):
]
return InlineKeyboardMarkup(keyboard)
+# --- Diary Functions ---
+
+DIARY_MONTH_NAMES = ['', 'Січня', 'Лютого', 'Березня', 'Квітня', 'Травня', 'Червня',
+ 'Липня', 'Серпня', 'Вересня', 'Жовтня', 'Листопада', 'Грудня']
+
+DIARY_WEEKDAYS_SHORT = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Нд']
+
+def get_diary_keyboard():
+ today = datetime.now()
+ keyboard = [
+ [
+ InlineKeyboardButton(f"📌 Сьогодні ({today.day}.{today.month:02d})", callback_data="diary_today"),
+ InlineKeyboardButton("📌 Завтра", callback_data="diary_tomorrow"),
+ ],
+ [
+ InlineKeyboardButton("📅 Цей тиждень", callback_data="diary_week"),
+ InlineKeyboardButton("📅 Весь місяць", callback_data="diary_month"),
+ ],
+ ]
+ return InlineKeyboardMarkup(keyboard)
+
+def _parse_calendar_html(table_html: str) -> tuple:
+ """Parse calendar HTML table into (month_text, {day_num: {weekday, events}})."""
+ days = {}
+ weekdays = []
+ rows = re.findall(r'
]*>(.*?)
', table_html, re.DOTALL)
+
+ month_text = ''
+ for r_idx, row in enumerate(rows):
+ cells = re.findall(r']*>(.*?)', row, re.DOTALL)
+
+ if r_idx == 0:
+ # Month navigation row: extract "Травень 2026" from nav text
+ raw = re.sub(r'<[^>]+>', ' ', row).strip()
+ raw = re.sub(r'\s+', ' ', raw)
+ m = re.search(r'([А-Яа-яіїєґ\']+\s*:?\s*\d{4})', raw)
+ if m:
+ month_text = m.group(1).replace(' : ', ' ').strip()
+ else:
+ month_text = raw
+ elif r_idx == 1:
+ # Day names row
+ for cell in cells:
+ name = re.sub(r'<[^>]+>', '', cell).strip()
+ if name:
+ weekdays.append(name)
+ else:
+ # Data rows: each cell = a day
+ for col_idx, cell in enumerate(cells):
+ # Extract day number — first number in the cell text
+ text = re.sub(r'<[^>]+>', ' ', cell).strip()
+ text = re.sub(r'\s+', ' ', text)
+ dm = re.match(r'(\d+)', text)
+ if not dm:
+ continue
+ day_num = dm.group(1)
+
+ # Extract events: title attribute (full name) of ALL tags inside the cell
+ events = []
+ for a_match in re.finditer(r']*>(.*?)', cell, re.DOTALL):
+ a_tag = a_match.group(0)
+ # Prefer the title attribute (contains full name, not truncated)
+ title_m = re.search(r'title\s*=\s*"([^"]*)"', a_tag)
+ if title_m:
+ et = title_m.group(1).strip()
+ else:
+ et = re.sub(r'<[^>]+>', '', a_match.group(1)).strip()
+ if et:
+ events.append(et)
+
+ weekday = weekdays[col_idx] if col_idx < len(weekdays) else ''
+ days[day_num] = {'weekday': weekday, 'events': events}
+
+ return month_text, days
+
+
+async def fetch_diary_data(phpsessid: str) -> dict | None:
+ logger.info("Fetching diary data via Playwright...")
+ try:
+ async with async_playwright() as p:
+ browser = await p.chromium.connect(PLAYWRIGHT_WS)
+ try:
+ context_browser = await browser.new_context(user_agent=USER_AGENT)
+ await context_browser.add_cookies([{
+ 'name': 'PHPSESSID',
+ 'value': phpsessid,
+ 'domain': 'edu.edu.vn.ua',
+ 'path': '/'
+ }])
+ page = await context_browser.new_page()
+
+ try:
+ await page.goto(DIARY_URL, wait_until='domcontentloaded')
+ await page.wait_for_selector('table.calendar', timeout=10000)
+ await page.wait_for_timeout(1500)
+
+ table_html = await page.evaluate("""
+ () => {
+ const t = document.querySelector('table.calendar');
+ return t ? t.outerHTML : null;
+ }
+ """)
+ if not table_html:
+ logger.error("table.calendar not found in DOM")
+ return None
+
+ # Debug: save HTML for troubleshooting
+ try:
+ with open('/tmp/diary_debug.html', 'w', encoding='utf-8') as f:
+ f.write(table_html)
+ except Exception:
+ pass
+
+ month_text, days = _parse_calendar_html(table_html)
+ logger.info(f"Diary parsed: month={month_text!r}, days_with_events={sum(1 for d in days.values() if d['events'])}/{len(days)}")
+
+ return {'monthFullText': month_text, 'days': days}
+
+ except Exception as e:
+ logger.error(f"Error parsing diary: {e}")
+ return None
+ finally:
+ await page.close()
+ await context_browser.close()
+ finally:
+ await browser.close()
+ except Exception as e:
+ logger.error(f"Playwright error in diary fetch: {e}")
+ return None
+
+def _parse_diary_month(text: str) -> str:
+ match = re.search(r'([А-Яа-яіїєґ\']+\s*:\s*\d{4})', text)
+ if match:
+ return match.group(1).replace(' : ', ' ').strip()
+ return text.strip()
+
+def format_diary_day(data: dict, day_num: int) -> str:
+ days = data.get('days', {})
+ month_str = _parse_diary_month(data.get('monthFullText', ''))
+ day_data = days.get(str(day_num))
+ lines = [f"📅 {day_num} {month_str}", "─" * 18]
+ if not day_data or not day_data.get('events'):
+ lines.append("Немає подій")
+ else:
+ for e in day_data['events']:
+ lines.append(f"📌 {e}")
+ lines.append(f"\n🔗 {DIARY_URL}")
+ return "\n".join(lines)
+
+def format_diary_week(data: dict, today: datetime) -> str:
+ days = data.get('days', {})
+ month_str = _parse_diary_month(data.get('monthFullText', ''))
+ monday = today - timedelta(days=today.weekday())
+ sunday = monday + timedelta(days=6)
+ lines = [f"📅 Тиждень {monday.day}.{monday.month} – {sunday.day}.{sunday.month}\n"]
+ for i in range(7):
+ d = monday + timedelta(days=i)
+ day_data = days.get(str(d.day))
+ lines.append(f"─ {DIARY_WEEKDAYS_SHORT[i]} {d.day}.{d.month} ─")
+ if not day_data or not day_data.get('events'):
+ lines.append("Немає подій\n")
+ else:
+ for e in day_data['events']:
+ lines.append(f"📌 {e}")
+ lines.append("")
+ lines.append(f"🔗 {DIARY_URL}")
+ return "\n".join(lines)
+
+def format_diary_month(data: dict) -> str:
+ days = data.get('days', {})
+ month_str = _parse_diary_month(data.get('monthFullText', ''))
+ lines = [f"📅 {month_str}\n"]
+ for day_num in sorted(days.keys(), key=int):
+ day_data = days[day_num]
+ events = day_data.get('events', [])
+ weekday = day_data.get('weekday', '')
+ lines.append(f"─ {weekday} {day_num} ─")
+ if not events:
+ lines.append("Немає подій\n")
+ else:
+ for e in events:
+ lines.append(f"📌 {e}")
+ lines.append("")
+ lines.append(f"🔗 {DIARY_URL}")
+ return "\n".join(lines)
+
+async def _get_diary_data(context: ContextTypes.DEFAULT_TYPE) -> dict | None:
+ cached = context.user_data.get('diary_cache')
+ now_ts = time.time()
+ if cached and (now_ts - cached.get('timestamp', 0)) < 300:
+ return cached['data']
+ phpsessid = redis_client.get(KEY_PHPSESSID)
+ if not phpsessid:
+ return None
+ data = await fetch_diary_data(phpsessid)
+ if data:
+ context.user_data['diary_cache'] = {'data': data, 'timestamp': now_ts}
+ return data
+
# --- Command Handlers ---
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -376,6 +579,74 @@ async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
logger.error(f"Failed to clear history: {e}")
await update.message.reply_text(t(admin_id, 'history_clear_failed'))
+async def diary_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ user = update.effective_user
+ chat = update.effective_chat
+ if not is_whitelisted(user.id):
+ await update.message.reply_text(t(user.id, 'access_denied'))
+ return
+ await update.message.reply_text(
+ "📅 Щоденник — виберіть період:",
+ parse_mode='HTML',
+ reply_markup=get_diary_keyboard()
+ )
+
+async def diary_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ query = update.callback_query
+ user_id = query.from_user.id
+ await query.answer()
+
+ if user_id != ADMIN_ID:
+ if not is_whitelisted(user_id):
+ await query.edit_message_text("⛔ Доступ заборонено.")
+ return
+
+ data = query.data
+ if data == "diary_refresh":
+ context.user_data.pop('diary_cache', None)
+ await query.edit_message_text("🔄 Завантажую щоденник...")
+ diary_data = await _get_diary_data(context)
+ if not diary_data:
+ await query.edit_message_text("❌ Не вдалося завантажити щоденник. Немає сесії або помилка.")
+ return
+ await query.edit_message_text(
+ "📅 Щоденник — виберіть період:",
+ parse_mode='HTML',
+ reply_markup=get_diary_keyboard()
+ )
+ return
+
+ await query.edit_message_text("🔄 Завантажую щоденник...")
+ diary_data = await _get_diary_data(context)
+ if not diary_data:
+ await query.edit_message_text("❌ Не вдалося завантажити щоденник.")
+ return
+
+ today = datetime.now()
+ if data == "diary_today":
+ text = format_diary_day(diary_data, today.day)
+ elif data == "diary_tomorrow":
+ tomorrow = today + timedelta(days=1)
+ if tomorrow.day < today.day:
+ text = "❌ Дані за наступний місяць недоступні. Перейдіть на сайт."
+ else:
+ text = format_diary_day(diary_data, tomorrow.day)
+ elif data == "diary_week":
+ text = format_diary_week(diary_data, today)
+ elif data == "diary_month":
+ text = format_diary_month(diary_data)
+ else:
+ return
+
+ if len(text) > 4096:
+ text = text[:4090] + "\n\n✂️ ...(обрізано)"
+
+ await query.edit_message_text(
+ text,
+ parse_mode='HTML',
+ reply_markup=get_diary_keyboard()
+ )
+
# --- Admin Callbacks ---
async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -649,8 +920,10 @@ def main():
app.add_handler(CommandHandler("adduser", add_user))
app.add_handler(CommandHandler("removeuser", remove_user))
app.add_handler(CommandHandler("clearhistory", clear_history))
+ app.add_handler(CommandHandler("diary", diary_command))
- # Callback handlers - language selection first, then admin panel
+ # Callback handlers - diary first, then language selection, then admin panel
+ app.add_handler(CallbackQueryHandler(diary_callback, pattern="^diary_"))
app.add_handler(CallbackQueryHandler(language_callback, pattern="^lang_"))
app.add_handler(CallbackQueryHandler(admin_callback))