fix(edu): review findings for checker.py i18n — group chat language via resolve_lang/chat: keys (default uk), weekday normalization with _norm_day + logging, drop dead translation keys, html.escape, single lang lookup, distinct whitelist emoji
This commit is contained in:
@@ -6,6 +6,7 @@ import re
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from html import escape
|
||||
|
||||
import redis
|
||||
from playwright.async_api import async_playwright
|
||||
@@ -55,6 +56,9 @@ KEY_SUBSCRIBERS = 'bot:subscribers'
|
||||
KEY_PHPSESSID = 'EDU_PHPSESSID'
|
||||
KEY_WEBINAR_HISTORY = 'bot:webinar_history' # Stores last 3 webinars
|
||||
|
||||
# Marker shown by the site when there are no active online lessons
|
||||
NO_WEBINAR_MARKER = 'Жодного онлайн уроку зараз'
|
||||
|
||||
# Initialize Redis
|
||||
try:
|
||||
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
|
||||
@@ -83,7 +87,7 @@ TRANSLATIONS = {
|
||||
'usage_adduser': 'Использование: /adduser [user_id]',
|
||||
'usage_removeuser': 'Использование: /removeuser [user_id]',
|
||||
'whitelist_enabled': '✅ Белый список включен',
|
||||
'whitelist_disabled': '✅ Белый список отключен',
|
||||
'whitelist_disabled': '🚫 Белый список отключен',
|
||||
'whitelist_title': '📋 <b>Белый список:</b>\n',
|
||||
'subscribers_title': '👥 <b>Подписчики:</b>\n',
|
||||
'empty': 'Пусто',
|
||||
@@ -99,12 +103,44 @@ TRANSLATIONS = {
|
||||
'webinar_found': '🎓 <b>Новый вебинар!</b>\n\n',
|
||||
'webinar_item': '📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}',
|
||||
'select_language': '🌐 <b>Выберите язык / Оберіть мову / Select language:</b>',
|
||||
'language_changed': '✅ Язык изменен на русский',
|
||||
'language_changed': '✅ Язык изменен на {lang}',
|
||||
'flag_ru': '🇷🇺 Русский',
|
||||
'flag_uk': '🇺🇦 Українська',
|
||||
'flag_en': '🇬🇧 English',
|
||||
'history_cleared': '✅ История вебинаров очищена',
|
||||
'history_clear_failed': '❌ Ошибка при очистке истории',
|
||||
'today': '📌 Сегодня',
|
||||
'tomorrow': '📌 Завтра',
|
||||
'week': '📅 Эта неделя',
|
||||
'month': '📅 Весь месяц',
|
||||
'no_events': 'Нет событий',
|
||||
'no_lessons': 'Нет уроков',
|
||||
'free_period': 'Свободно',
|
||||
'unsubscribed': '🔕 Вы отписались от уведомлений.',
|
||||
'diary_title': '📅 <b>Дневник</b> — выберите период:',
|
||||
'diary_week_title': '📅 <b>Неделя {start} – {end}</b>',
|
||||
'schedule_title': '📅 <b>{weekday} — {class_num} класс</b>',
|
||||
'loading_diary': '🔄 Загружаю дневник...',
|
||||
'diary_load_failed': '❌ Не удалось загрузить дневник.',
|
||||
'diary_no_session': '❌ Не удалось загрузить дневник. Нет сессии или ошибка.',
|
||||
'diary_next_month': '❌ Данные за следующий месяц недоступны. Перейдите на сайт.',
|
||||
'schedule_load_failed': '❌ Не удалось загрузить расписание.',
|
||||
'class_not_set': '❌ Класс не настроен. Используйте /setclass.',
|
||||
'class_not_found': '❌ Классы не найдены в расписании.',
|
||||
'select_class': '🎒 <b>Выберите класс</b> — сохранится и больше не спросится:',
|
||||
'admin_class_not_set': '⚠️ Администратор еще не настроил класс для этой группы. Используйте /setclass 11',
|
||||
'loading_schedule': '🔄 Загружаю расписание...',
|
||||
'invalid_class_num': '❌ Неверный номер класса.',
|
||||
'class_saved': '✅ Класс <b>{class_num}</b> сохранен.',
|
||||
'admin_only_msg': '⚠️ Только администратор может настроить класс для группы.',
|
||||
'truncation': '\n\n✂️ ...(обрезано)',
|
||||
'day_mon': 'Пн',
|
||||
'day_tue': 'Вт',
|
||||
'day_wed': 'Ср',
|
||||
'day_thu': 'Чт',
|
||||
'day_fri': 'Пт',
|
||||
'day_sat': 'Сб',
|
||||
'day_sun': 'Вс',
|
||||
},
|
||||
'uk': {
|
||||
'welcome': "👋 Привіт, {name}!\n\nЯ бот-сповіщувач про вебінари. Я повідомлятиму вас, коли з'явиться новий вебінар.\nВи підписані на сповіщення.",
|
||||
@@ -122,7 +158,7 @@ TRANSLATIONS = {
|
||||
'usage_adduser': 'Використання: /adduser [user_id]',
|
||||
'usage_removeuser': 'Використання: /removeuser [user_id]',
|
||||
'whitelist_enabled': '✅ Білий список увімкнено',
|
||||
'whitelist_disabled': '✅ Білий список вимкнено',
|
||||
'whitelist_disabled': '🚫 Білий список вимкнено',
|
||||
'whitelist_title': '📋 <b>Білий список:</b>\n',
|
||||
'subscribers_title': '👥 <b>Підписники:</b>\n',
|
||||
'empty': 'Порожньо',
|
||||
@@ -138,12 +174,44 @@ TRANSLATIONS = {
|
||||
'webinar_found': '🎓 <b>Новий вебінар!</b>\n\n',
|
||||
'webinar_item': '📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}',
|
||||
'select_language': '🌐 <b>Виберіть мову / Выберите язык / Select language:</b>',
|
||||
'language_changed': '✅ Мову змінено на українську',
|
||||
'language_changed': '✅ Мову змінено на {lang}',
|
||||
'flag_ru': '🇷🇺 Русский',
|
||||
'flag_uk': '🇺🇦 Українська',
|
||||
'flag_en': '🇬🇧 English',
|
||||
'history_cleared': '✅ Історія вебінарів очищена',
|
||||
'history_clear_failed': '❌ Помилка при очищенні історії',
|
||||
'today': '📌 Сьогодні',
|
||||
'tomorrow': '📌 Завтра',
|
||||
'week': '📅 Цей тиждень',
|
||||
'month': '📅 Весь місяць',
|
||||
'no_events': 'Немає подій',
|
||||
'no_lessons': 'Немає уроків',
|
||||
'free_period': 'Вільно',
|
||||
'unsubscribed': '🔕 Ви відписалися від сповіщень.',
|
||||
'diary_title': '📅 <b>Щоденник</b> — виберіть період:',
|
||||
'diary_week_title': '📅 <b>Тиждень {start} – {end}</b>',
|
||||
'schedule_title': '📅 <b>{weekday} — {class_num} клас</b>',
|
||||
'loading_diary': '🔄 Завантажую щоденник...',
|
||||
'diary_load_failed': '❌ Не вдалося завантажити щоденник.',
|
||||
'diary_no_session': '❌ Не вдалося завантажити щоденник. Немає сесії або помилка.',
|
||||
'diary_next_month': '❌ Дані за наступний місяць недоступні. Перейдіть на сайт.',
|
||||
'schedule_load_failed': '❌ Не вдалося завантажити розклад.',
|
||||
'class_not_set': '❌ Клас не налаштовано. Використайте /setclass.',
|
||||
'class_not_found': '❌ Класи не знайдені в розкладі.',
|
||||
'select_class': '🎒 <b>Оберіть клас</b> — збережеться і більше не питатиметься:',
|
||||
'admin_class_not_set': '⚠️ Адміністратор ще не налаштував клас для цієї групи. Використайте /setclass 11',
|
||||
'loading_schedule': '🔄 Завантажую розклад...',
|
||||
'invalid_class_num': '❌ Невірний номер класу.',
|
||||
'class_saved': '✅ Клас <b>{class_num}</b> збережено.',
|
||||
'admin_only_msg': '⚠️ Тільки адміністратор може налаштувати клас для групи.',
|
||||
'truncation': '\n\n✂️ ...(обрізано)',
|
||||
'day_mon': 'Пн',
|
||||
'day_tue': 'Вт',
|
||||
'day_wed': 'Ср',
|
||||
'day_thu': 'Чт',
|
||||
'day_fri': 'Пт',
|
||||
'day_sat': 'Сб',
|
||||
'day_sun': 'Нд',
|
||||
},
|
||||
'en': {
|
||||
'welcome': '👋 Hello, {name}!\n\nI am the Webinar Checker Bot. I will notify you when a new webinar appears.\nYou have been subscribed to notifications.',
|
||||
@@ -161,7 +229,7 @@ TRANSLATIONS = {
|
||||
'usage_adduser': 'Usage: /adduser [user_id]',
|
||||
'usage_removeuser': 'Usage: /removeuser [user_id]',
|
||||
'whitelist_enabled': '✅ Whitelist Enabled',
|
||||
'whitelist_disabled': '✅ Whitelist Disabled',
|
||||
'whitelist_disabled': '🚫 Whitelist Disabled',
|
||||
'whitelist_title': '📋 <b>Whitelist:</b>\n',
|
||||
'subscribers_title': '👥 <b>Subscribers:</b>\n',
|
||||
'empty': 'Empty',
|
||||
@@ -177,22 +245,57 @@ TRANSLATIONS = {
|
||||
'webinar_found': '🎓 <b>New webinar found!</b>\n\n',
|
||||
'webinar_item': '📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}',
|
||||
'select_language': '🌐 <b>Select language / Виберіть мову / Выберите язык:</b>',
|
||||
'language_changed': '✅ Language changed to English',
|
||||
'language_changed': '✅ Language changed to {lang}',
|
||||
'flag_ru': '🇷🇺 Русский',
|
||||
'flag_uk': '🇺🇦 Українська',
|
||||
'flag_en': '🇬🇧 English',
|
||||
'history_cleared': '✅ Webinar history cleared',
|
||||
'history_clear_failed': '❌ Error clearing history',
|
||||
'today': '📌 Today',
|
||||
'tomorrow': '📌 Tomorrow',
|
||||
'week': '📅 This week',
|
||||
'month': '📅 Whole month',
|
||||
'no_events': 'No events',
|
||||
'no_lessons': 'No lessons',
|
||||
'free_period': 'Free',
|
||||
'unsubscribed': '🔕 You have unsubscribed from notifications.',
|
||||
'diary_title': '📅 <b>Diary</b> — choose a period:',
|
||||
'diary_week_title': '📅 <b>Week {start} – {end}</b>',
|
||||
'schedule_title': '📅 <b>{weekday} — {class_num} class</b>',
|
||||
'loading_diary': '🔄 Loading diary...',
|
||||
'diary_load_failed': '❌ Failed to load diary.',
|
||||
'diary_no_session': '❌ Failed to load diary. No session or error.',
|
||||
'diary_next_month': '❌ Next month data is not available. Please visit the website.',
|
||||
'schedule_load_failed': '❌ Failed to load schedule.',
|
||||
'class_not_set': '❌ Class is not set. Use /setclass.',
|
||||
'class_not_found': '❌ No classes found in the schedule.',
|
||||
'select_class': '🎒 <b>Choose a class</b> — it will be saved and not asked again:',
|
||||
'admin_class_not_set': '⚠️ Admin has not set a class for this group yet. Use /setclass 11',
|
||||
'loading_schedule': '🔄 Loading schedule...',
|
||||
'invalid_class_num': '❌ Invalid class number.',
|
||||
'class_saved': '✅ Class <b>{class_num}</b> saved.',
|
||||
'admin_only_msg': '⚠️ Only an admin can set the class for this group.',
|
||||
'truncation': '\n\n✂️ ...(truncated)',
|
||||
'day_mon': 'Mon',
|
||||
'day_tue': 'Tue',
|
||||
'day_wed': 'Wed',
|
||||
'day_thu': 'Thu',
|
||||
'day_fri': 'Fri',
|
||||
'day_sat': 'Sat',
|
||||
'day_sun': 'Sun',
|
||||
},
|
||||
}
|
||||
|
||||
# --- Language Helper Functions ---
|
||||
|
||||
|
||||
LANG_NAME_MAP = {'ru': 'Русский', 'uk': 'Українська', 'en': 'English'}
|
||||
|
||||
|
||||
def get_user_language(user_id: int) -> str:
|
||||
"""Get user's preferred language from Redis. Default: Ukrainian."""
|
||||
"""Get user's preferred language from Redis. Default: English."""
|
||||
lang = redis_client.get(f'user:{user_id}:language')
|
||||
return lang if lang in ['ru', 'uk', 'en'] else 'uk'
|
||||
return lang if lang in ['ru', 'uk', 'en'] else 'en'
|
||||
|
||||
|
||||
def set_user_language(user_id: int, lang: str):
|
||||
@@ -203,23 +306,87 @@ def set_user_language(user_id: int, lang: str):
|
||||
|
||||
|
||||
def t(user_id: int, key: str, **kwargs) -> str:
|
||||
"""Translate message for user with optional formatting."""
|
||||
"""Translate message for user with optional formatting.
|
||||
|
||||
Fallback chain: user lang → en → uk → ru, otherwise return the key.
|
||||
"""
|
||||
lang = get_user_language(user_id)
|
||||
message = TRANSLATIONS.get(lang, TRANSLATIONS['uk']).get(key, key)
|
||||
message = None
|
||||
for fallback in (lang, 'en', 'uk', 'ru'):
|
||||
message = TRANSLATIONS.get(fallback, {}).get(key)
|
||||
if message is not None:
|
||||
break
|
||||
if message is None:
|
||||
return key
|
||||
if kwargs:
|
||||
return message.format(**kwargs)
|
||||
return message
|
||||
|
||||
|
||||
def get_language_keyboard():
|
||||
def _tr(lang: str, key: str, **kwargs) -> str:
|
||||
"""Translate by explicit lang code (for format_* helpers).
|
||||
|
||||
Fallback chain: lang → en → uk → ru, otherwise return the key.
|
||||
"""
|
||||
message = None
|
||||
for fallback in (lang, 'en', 'uk', 'ru'):
|
||||
message = TRANSLATIONS.get(fallback, {}).get(key)
|
||||
if message is not None:
|
||||
break
|
||||
if message is None:
|
||||
return key
|
||||
if kwargs:
|
||||
return message.format(**kwargs)
|
||||
return message
|
||||
|
||||
|
||||
def get_chat_language(chat_id: int) -> str:
|
||||
"""Get group chat language from Redis. Default: Ukrainian."""
|
||||
lang = redis_client.get(f'chat:{chat_id}:language')
|
||||
return lang if lang in ['ru', 'uk', 'en'] else 'uk'
|
||||
|
||||
|
||||
def set_chat_language(chat_id: int, lang: str):
|
||||
"""Save group chat language preference to Redis."""
|
||||
if lang in ['ru', 'uk', 'en']:
|
||||
redis_client.set(f'chat:{chat_id}:language', lang)
|
||||
logger.info(f'Chat {chat_id} language set to {lang}')
|
||||
|
||||
|
||||
def resolve_lang(chat, user_id=None) -> str:
|
||||
"""Resolve effective language: private → user lang, groups → chat lang (default 'uk')."""
|
||||
chat_id = getattr(chat, 'id', chat)
|
||||
chat_type = getattr(chat, 'type', None)
|
||||
if chat_type is None:
|
||||
try:
|
||||
is_private = int(chat_id) > 0
|
||||
except (TypeError, ValueError):
|
||||
is_private = True
|
||||
if is_private:
|
||||
uid = user_id if user_id is not None else chat_id
|
||||
return get_user_language(int(uid))
|
||||
return get_chat_language(int(chat_id))
|
||||
if chat_type in (ChatType.PRIVATE, 'private'):
|
||||
uid = user_id if user_id is not None else chat_id
|
||||
return get_user_language(int(uid))
|
||||
return get_chat_language(chat_id)
|
||||
|
||||
|
||||
def t_chat(chat, user_id, key: str, **kwargs) -> str:
|
||||
"""Translate using chat-resolved language (private → user, groups → chat 'uk' default)."""
|
||||
return _tr(resolve_lang(chat, user_id), key, **kwargs)
|
||||
|
||||
|
||||
def get_language_keyboard(user_id: int | None = None):
|
||||
"""Generate language selection keyboard."""
|
||||
lang = get_user_language(user_id) if user_id is not None else 'en'
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton('🇷🇺 Русский', callback_data='lang_ru'),
|
||||
InlineKeyboardButton('🇺🇦 Українська', callback_data='lang_uk'),
|
||||
InlineKeyboardButton(_tr(lang, 'flag_ru'), callback_data='lang_ru'),
|
||||
InlineKeyboardButton(_tr(lang, 'flag_uk'), callback_data='lang_uk'),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton('🇬🇧 English', callback_data='lang_en'),
|
||||
InlineKeyboardButton(_tr(lang, 'flag_en'), callback_data='lang_en'),
|
||||
],
|
||||
]
|
||||
return InlineKeyboardMarkup(keyboard)
|
||||
@@ -272,38 +439,51 @@ def get_admin_keyboard(user_id: int):
|
||||
|
||||
# --- Diary Functions ---
|
||||
|
||||
SCHEDULE_WEEKDAYS_FULL = ['Понеділок', 'Вівторок', 'Середа', 'Четвер', "П'ятниця", 'Субота', 'Неділя']
|
||||
SCHEDULE_WEEKDAYS_FULL = {
|
||||
'uk': ['Понеділок', 'Вівторок', 'Середа', 'Четвер', "П'ятниця", 'Субота', 'Неділя'],
|
||||
'ru': ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'],
|
||||
'en': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
|
||||
}
|
||||
# Canonical weekday names used in callback_data and schedule lookup (site language).
|
||||
SCHEDULE_WEEKDAYS_CANONICAL = SCHEDULE_WEEKDAYS_FULL['uk']
|
||||
SCHEDULE_CACHE_TTL = 18000 # 5 hours
|
||||
|
||||
DIARY_MONTH_NAMES = [
|
||||
'',
|
||||
'Січня',
|
||||
'Лютого',
|
||||
'Березня',
|
||||
'Квітня',
|
||||
'Травня',
|
||||
'Червня',
|
||||
'Липня',
|
||||
'Серпня',
|
||||
'Вересня',
|
||||
'Жовтня',
|
||||
'Листопада',
|
||||
'Грудня',
|
||||
]
|
||||
|
||||
DIARY_WEEKDAYS_SHORT = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Нд']
|
||||
def _norm_day(s):
|
||||
"""Normalize weekday name for tolerant matching (case, spaces, apostrophes)."""
|
||||
if not isinstance(s, str):
|
||||
return ''
|
||||
s = s.strip().casefold()
|
||||
for _q in ('\u2019', '\u2018', '\u02bc', '`'):
|
||||
s = s.replace(_q, "'")
|
||||
s = re.sub(r'\s+', ' ', s)
|
||||
return s.strip()
|
||||
|
||||
|
||||
def get_diary_keyboard():
|
||||
_SCHEDULE_WEEKDAYS_CANONICAL_NORM = [_norm_day(d) for d in SCHEDULE_WEEKDAYS_CANONICAL]
|
||||
|
||||
DIARY_WEEKDAYS_SHORT = {
|
||||
'uk': ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Нд'],
|
||||
'ru': ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'],
|
||||
'en': ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
}
|
||||
|
||||
|
||||
def get_diary_keyboard(user_id: int | None = None):
|
||||
today = datetime.now()
|
||||
lang = get_user_language(user_id) if user_id is not None else 'en'
|
||||
today_label = _tr(lang, 'today')
|
||||
tomorrow_label = _tr(lang, 'tomorrow')
|
||||
week_label = _tr(lang, 'week')
|
||||
month_label = _tr(lang, 'month')
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(f'📌 Сьогодні ({today.day}.{today.month:02d})', callback_data='diary_today'),
|
||||
InlineKeyboardButton('📌 Завтра', callback_data='diary_tomorrow'),
|
||||
InlineKeyboardButton(f'{today_label} ({today.day}.{today.month:02d})', callback_data='diary_today'),
|
||||
InlineKeyboardButton(tomorrow_label, callback_data='diary_tomorrow'),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton('📅 Цей тиждень', callback_data='diary_week'),
|
||||
InlineKeyboardButton('📅 Весь місяць', callback_data='diary_month'),
|
||||
InlineKeyboardButton(week_label, callback_data='diary_week'),
|
||||
InlineKeyboardButton(month_label, callback_data='diary_month'),
|
||||
],
|
||||
]
|
||||
return InlineKeyboardMarkup(keyboard)
|
||||
@@ -416,41 +596,44 @@ def _parse_diary_month(text: str) -> str:
|
||||
return text.strip()
|
||||
|
||||
|
||||
def format_diary_day(data: dict, day_num: int) -> str:
|
||||
def format_diary_day(data: dict, day_num: int, lang: str = 'en') -> str:
|
||||
days = data.get('days', {})
|
||||
month_str = _parse_diary_month(data.get('monthFullText', ''))
|
||||
day_data = days.get(str(day_num))
|
||||
lines = [f'📅 <b>{day_num} {month_str}</b>', '─' * 18]
|
||||
if not day_data or not day_data.get('events'):
|
||||
lines.append('Немає подій')
|
||||
lines.append(_tr(lang, 'no_events'))
|
||||
else:
|
||||
for e in day_data['events']:
|
||||
lines.append(f'📌 {e}')
|
||||
lines.append(f'📌 {escape(e)}')
|
||||
lines.append(f'\n🔗 {DIARY_URL}')
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def format_diary_week(data: dict, today: datetime) -> str:
|
||||
def format_diary_week(data: dict, today: datetime, lang: str = 'en') -> str:
|
||||
days = data.get('days', {})
|
||||
_parse_diary_month(data.get('monthFullText', ''))
|
||||
monday = today - timedelta(days=today.weekday())
|
||||
sunday = monday + timedelta(days=6)
|
||||
lines = [f'📅 <b>Тиждень {monday.day}.{monday.month} – {sunday.day}.{sunday.month}</b>\n']
|
||||
start = f'{monday.day}.{monday.month}'
|
||||
end = f'{sunday.day}.{sunday.month}'
|
||||
short = DIARY_WEEKDAYS_SHORT.get(lang, DIARY_WEEKDAYS_SHORT['en'])
|
||||
lines = [_tr(lang, 'diary_week_title', start=start, end=end) + '\n']
|
||||
for i in range(7):
|
||||
d = monday + timedelta(days=i)
|
||||
day_data = days.get(str(d.day))
|
||||
lines.append(f'─ <b>{DIARY_WEEKDAYS_SHORT[i]} {d.day}.{d.month}</b> ─')
|
||||
lines.append(f'─ <b>{short[i]} {d.day}.{d.month}</b> ─')
|
||||
if not day_data or not day_data.get('events'):
|
||||
lines.append('Немає подій\n')
|
||||
lines.append(_tr(lang, 'no_events') + '\n')
|
||||
else:
|
||||
for e in day_data['events']:
|
||||
lines.append(f'📌 {e}')
|
||||
lines.append(f'📌 {escape(e)}')
|
||||
lines.append('')
|
||||
lines.append(f'🔗 {DIARY_URL}')
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def format_diary_month(data: dict) -> str:
|
||||
def format_diary_month(data: dict, lang: str = 'en') -> str:
|
||||
days = data.get('days', {})
|
||||
month_str = _parse_diary_month(data.get('monthFullText', ''))
|
||||
lines = [f'📅 <b>{month_str}</b>\n']
|
||||
@@ -460,10 +643,10 @@ def format_diary_month(data: dict) -> str:
|
||||
weekday = day_data.get('weekday', '')
|
||||
lines.append(f'─ <b>{weekday} {day_num}</b> ─')
|
||||
if not events:
|
||||
lines.append('Немає подій\n')
|
||||
lines.append(_tr(lang, 'no_events') + '\n')
|
||||
else:
|
||||
for e in events:
|
||||
lines.append(f'📌 {e}')
|
||||
lines.append(f'📌 {escape(e)}')
|
||||
lines.append('')
|
||||
lines.append(f'🔗 {DIARY_URL}')
|
||||
return '\n'.join(lines)
|
||||
@@ -486,26 +669,37 @@ async def _get_diary_data(context: ContextTypes.DEFAULT_TYPE) -> dict | None:
|
||||
# --- Schedule Functions ---
|
||||
|
||||
|
||||
def get_schedule_day_keyboard():
|
||||
def get_schedule_day_keyboard(user_id: int | None = None):
|
||||
lang = get_user_language(user_id) if user_id is not None else 'en'
|
||||
full = SCHEDULE_WEEKDAYS_FULL.get(lang, SCHEDULE_WEEKDAYS_FULL['en'])
|
||||
today = datetime.now()
|
||||
tomorrow = today + timedelta(days=1)
|
||||
today_label = _tr(lang, 'today')
|
||||
tomorrow_label = _tr(lang, 'tomorrow')
|
||||
short_keys = ['day_mon', 'day_tue', 'day_wed', 'day_thu', 'day_fri', 'day_sat', 'day_sun']
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(f'📌 Сьогодні ({SCHEDULE_WEEKDAYS_FULL[today.weekday()]})', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[today.weekday()]}'),
|
||||
InlineKeyboardButton(f'📌 Завтра ({SCHEDULE_WEEKDAYS_FULL[tomorrow.weekday()]})', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[tomorrow.weekday()]}'),
|
||||
InlineKeyboardButton(
|
||||
f'{today_label} ({full[today.weekday()]})',
|
||||
callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[today.weekday()]}',
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
f'{tomorrow_label} ({full[tomorrow.weekday()]})',
|
||||
callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[tomorrow.weekday()]}',
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton('Пн', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[0]}'),
|
||||
InlineKeyboardButton('Вт', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[1]}'),
|
||||
InlineKeyboardButton('Ср', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[2]}'),
|
||||
InlineKeyboardButton(_tr(lang, short_keys[0]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[0]}'),
|
||||
InlineKeyboardButton(_tr(lang, short_keys[1]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[1]}'),
|
||||
InlineKeyboardButton(_tr(lang, short_keys[2]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[2]}'),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton('Чт', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[3]}'),
|
||||
InlineKeyboardButton('Пт', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[4]}'),
|
||||
InlineKeyboardButton('Сб', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[5]}'),
|
||||
InlineKeyboardButton(_tr(lang, short_keys[3]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[3]}'),
|
||||
InlineKeyboardButton(_tr(lang, short_keys[4]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[4]}'),
|
||||
InlineKeyboardButton(_tr(lang, short_keys[5]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[5]}'),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton('Нд', callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_FULL[6]}'),
|
||||
InlineKeyboardButton(_tr(lang, short_keys[6]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[6]}'),
|
||||
],
|
||||
]
|
||||
return InlineKeyboardMarkup(keyboard)
|
||||
@@ -563,7 +757,7 @@ def _parse_schedule_cell(cell_html: str) -> list:
|
||||
|
||||
def _parse_schedule_html(table_html: str) -> dict:
|
||||
"""Parse schedule HTML table into {classes: [...], weekdays: {day: [...]}}."""
|
||||
result = {'classes': [], 'weekdays': {}}
|
||||
result = {'classes': [], 'weekdays': {}, 'weekday_norm': {}}
|
||||
row_matches = re.finditer(r'<tr([^>]*)>(.*?)</tr>', table_html, re.DOTALL)
|
||||
|
||||
current_day = None
|
||||
@@ -578,6 +772,7 @@ def _parse_schedule_html(table_html: str) -> dict:
|
||||
day_text = re.sub(r'<[^>]+>', '', cells[0]).strip()
|
||||
current_day = day_text
|
||||
result['weekdays'][current_day] = []
|
||||
result['weekday_norm'][_norm_day(day_text)] = current_day
|
||||
classes_header = []
|
||||
for cell in cells[1:]:
|
||||
cls = re.sub(r'<[^>]+>', '', cell).strip()
|
||||
@@ -654,6 +849,7 @@ async def fetch_schedule_data(phpsessid: str) -> dict | None:
|
||||
f.write(table_html)
|
||||
|
||||
data = _parse_schedule_html(table_html)
|
||||
logger.info(list(data['weekdays'].keys()))
|
||||
logger.info(f'Schedule parsed: {len(data["weekdays"])} days, classes={data["classes"]}')
|
||||
|
||||
return data
|
||||
@@ -685,17 +881,28 @@ async def _get_schedule_data(context: ContextTypes.DEFAULT_TYPE) -> dict | None:
|
||||
return data
|
||||
|
||||
|
||||
def format_schedule_day(data: dict, weekday: str, class_num) -> str:
|
||||
def format_schedule_day(data: dict, weekday: str, class_num, lang: str = 'en') -> str:
|
||||
days = data.get('weekdays', {})
|
||||
entries = days.get(weekday, [])
|
||||
norm_map = data.get('weekday_norm') or {_norm_day(k): k for k in days}
|
||||
norm = _norm_day(weekday)
|
||||
day_key = norm_map.get(norm)
|
||||
if day_key is None:
|
||||
logger.warning(f'Schedule weekday not matched: {weekday!r}')
|
||||
entries = days.get(day_key, []) if day_key is not None else []
|
||||
try:
|
||||
class_key = int(class_num)
|
||||
except (TypeError, ValueError):
|
||||
class_key = class_num
|
||||
lines = [f'📅 <b>{weekday} — {class_num} клас</b>', '─' * 20]
|
||||
try:
|
||||
idx = _SCHEDULE_WEEKDAYS_CANONICAL_NORM.index(norm)
|
||||
display_weekday = SCHEDULE_WEEKDAYS_FULL.get(lang, SCHEDULE_WEEKDAYS_FULL['en'])[idx]
|
||||
except ValueError:
|
||||
logger.warning(f'Schedule weekday not in canonical list: {weekday!r}')
|
||||
display_weekday = weekday
|
||||
lines = [_tr(lang, 'schedule_title', weekday=display_weekday, class_num=class_num), '─' * 20]
|
||||
|
||||
if not entries:
|
||||
lines.append('Немає уроків')
|
||||
lines.append(_tr(lang, 'no_lessons'))
|
||||
else:
|
||||
for entry in entries:
|
||||
time_str = entry.get('time', '').replace(' ', '–')
|
||||
@@ -703,13 +910,13 @@ def format_schedule_day(data: dict, weekday: str, class_num) -> str:
|
||||
|
||||
lessons = entry.get('classes', {}).get(class_key, [])
|
||||
if not lessons:
|
||||
lines.append(' Вільно')
|
||||
lines.append(f' {_tr(lang, "free_period")}')
|
||||
else:
|
||||
for i, lesson in enumerate(lessons):
|
||||
note_part = f' ({lesson["note"]})' if lesson.get('note') else ''
|
||||
lines.append(f' 📌 <b>{lesson["subject"]}</b>{note_part}')
|
||||
note_part = f' ({escape(lesson["note"])})' if lesson.get('note') else ''
|
||||
lines.append(f' 📌 <b>{escape(lesson["subject"])}</b>{note_part}')
|
||||
if lesson.get('teacher'):
|
||||
lines.append(f' {lesson["teacher"]}')
|
||||
lines.append(f' {escape(lesson["teacher"])}')
|
||||
if i < len(lessons) - 1:
|
||||
lines.append(' ──')
|
||||
|
||||
@@ -734,10 +941,10 @@ async def start(update: Update, _context: ContextTypes.DEFAULT_TYPE):
|
||||
# Add to subscribers (Chat ID!)
|
||||
redis_client.sadd(KEY_SUBSCRIBERS, chat.id)
|
||||
|
||||
msg = t(chat.id, 'welcome', name=user.first_name)
|
||||
msg = t_chat(chat, user.id, 'welcome', name=escape(user.first_name))
|
||||
|
||||
if user.id == ADMIN_ID and chat.type == 'private':
|
||||
msg += t(chat.id, 'welcome_admin')
|
||||
msg += t_chat(chat, user.id, 'welcome_admin')
|
||||
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user.id))
|
||||
else:
|
||||
await update.message.reply_text(msg, parse_mode='HTML')
|
||||
@@ -746,11 +953,11 @@ async def start(update: Update, _context: ContextTypes.DEFAULT_TYPE):
|
||||
async def help_command(update: Update, _context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle /help command."""
|
||||
user_id = update.effective_user.id
|
||||
chat_id = update.effective_chat.id
|
||||
msg = t(chat_id, 'help_title') + t(chat_id, 'help_commands')
|
||||
chat = update.effective_chat
|
||||
msg = t_chat(chat, user_id, 'help_title') + t_chat(chat, user_id, 'help_commands')
|
||||
|
||||
if user_id == ADMIN_ID and update.effective_chat.type == 'private':
|
||||
msg += t(chat_id, 'help_admin')
|
||||
msg += t_chat(chat, user_id, 'help_admin')
|
||||
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user_id))
|
||||
else:
|
||||
await update.message.reply_text(msg, parse_mode='HTML')
|
||||
@@ -762,19 +969,13 @@ async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
chat = update.effective_chat
|
||||
|
||||
# Permission check: Whitelisted user OR Group Admin
|
||||
lang = resolve_lang(chat, user.id)
|
||||
if not (is_whitelisted(user.id) or await is_group_admin(update, context)):
|
||||
await update.message.reply_text(t(chat.id, 'access_denied')) # Or specific "admin only" message
|
||||
await update.message.reply_text(_tr(lang, 'access_denied')) # Or specific "admin only" message
|
||||
return
|
||||
|
||||
redis_client.srem(KEY_SUBSCRIBERS, chat.id)
|
||||
await update.message.reply_text(
|
||||
t(chat.id, 'whitelist_disabled')
|
||||
.replace(' whitelist', ' notifications')
|
||||
.replace('Білий список', 'Сповіщення')
|
||||
.replace('Белый список', 'Уведомления')
|
||||
if chat.id
|
||||
else 'Unsubscribed'
|
||||
)
|
||||
await update.message.reply_text(_tr(lang, 'unsubscribed'))
|
||||
|
||||
|
||||
async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
@@ -787,7 +988,7 @@ async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
return
|
||||
|
||||
await update.message.reply_text(
|
||||
t(chat.id, 'select_language'), parse_mode='HTML', reply_markup=get_language_keyboard()
|
||||
t_chat(chat, user.id, 'select_language'), parse_mode='HTML', reply_markup=get_language_keyboard()
|
||||
)
|
||||
|
||||
|
||||
@@ -860,7 +1061,7 @@ async def diary_command(update: Update, _context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text(t(user.id, 'access_denied'))
|
||||
return
|
||||
await update.message.reply_text(
|
||||
'📅 <b>Щоденник</b> — виберіть період:', parse_mode='HTML', reply_markup=get_diary_keyboard()
|
||||
t(user.id, 'diary_title'), parse_mode='HTML', reply_markup=get_diary_keyboard(user.id)
|
||||
)
|
||||
|
||||
|
||||
@@ -870,48 +1071,49 @@ async def diary_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await query.answer()
|
||||
|
||||
if user_id != ADMIN_ID and not is_whitelisted(user_id):
|
||||
await query.edit_message_text('⛔ Доступ заборонено.')
|
||||
await query.edit_message_text(t(user_id, 'access_denied'))
|
||||
return
|
||||
|
||||
data = query.data
|
||||
if data == 'diary_refresh':
|
||||
context.user_data.pop('diary_cache', None)
|
||||
await query.edit_message_text('🔄 Завантажую щоденник...')
|
||||
await query.edit_message_text(t(user_id, 'loading_diary'))
|
||||
diary_data = await _get_diary_data(context)
|
||||
if not diary_data:
|
||||
await query.edit_message_text('❌ Не вдалося завантажити щоденник. Немає сесії або помилка.')
|
||||
await query.edit_message_text(t(user_id, 'diary_no_session'))
|
||||
return
|
||||
await query.edit_message_text(
|
||||
'📅 <b>Щоденник</b> — виберіть період:', parse_mode='HTML', reply_markup=get_diary_keyboard()
|
||||
t(user_id, 'diary_title'), parse_mode='HTML', reply_markup=get_diary_keyboard(user_id)
|
||||
)
|
||||
return
|
||||
|
||||
await query.edit_message_text('🔄 Завантажую щоденник...')
|
||||
await query.edit_message_text(t(user_id, 'loading_diary'))
|
||||
diary_data = await _get_diary_data(context)
|
||||
if not diary_data:
|
||||
await query.edit_message_text('❌ Не вдалося завантажити щоденник.')
|
||||
await query.edit_message_text(t(user_id, 'diary_load_failed'))
|
||||
return
|
||||
|
||||
today = datetime.now()
|
||||
lang = get_user_language(user_id)
|
||||
if data == 'diary_today':
|
||||
text = format_diary_day(diary_data, today.day)
|
||||
text = format_diary_day(diary_data, today.day, lang=lang)
|
||||
elif data == 'diary_tomorrow':
|
||||
tomorrow = today + timedelta(days=1)
|
||||
if tomorrow.day < today.day:
|
||||
text = '❌ Дані за наступний місяць недоступні. Перейдіть на сайт.'
|
||||
text = t(user_id, 'diary_next_month')
|
||||
else:
|
||||
text = format_diary_day(diary_data, tomorrow.day)
|
||||
text = format_diary_day(diary_data, tomorrow.day, lang=lang)
|
||||
elif data == 'diary_week':
|
||||
text = format_diary_week(diary_data, today)
|
||||
text = format_diary_week(diary_data, today, lang=lang)
|
||||
elif data == 'diary_month':
|
||||
text = format_diary_month(diary_data)
|
||||
text = format_diary_month(diary_data, lang=lang)
|
||||
else:
|
||||
return
|
||||
|
||||
if len(text) > 4096:
|
||||
text = text[:4090] + '\n\n✂️ ...(обрізано)'
|
||||
text = text[:4090] + t(user_id, 'truncation')
|
||||
|
||||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_diary_keyboard())
|
||||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_diary_keyboard(user_id))
|
||||
|
||||
|
||||
# --- Schedule Command Handlers ---
|
||||
@@ -921,28 +1123,29 @@ async def _schedule_text_for(context, chat, user_id, weekday) -> tuple:
|
||||
"""Load schedule data and render one weekday for the stored class."""
|
||||
schedule_data = await _get_schedule_data(context)
|
||||
if not schedule_data:
|
||||
return None, '❌ Не вдалося завантажити розклад.'
|
||||
return None, t(user_id, 'schedule_load_failed')
|
||||
class_num = _get_chat_class(chat, user_id)
|
||||
if not class_num:
|
||||
return None, '❌ Клас не налаштовано. Використайте /setclass.'
|
||||
text = format_schedule_day(schedule_data, weekday, class_num)
|
||||
return None, t(user_id, 'class_not_set')
|
||||
text = format_schedule_day(schedule_data, weekday, class_num, lang=get_user_language(user_id))
|
||||
if len(text) > 4096:
|
||||
text = text[:4090] + '\n\n✂️ ...(обрізано)'
|
||||
text = text[:4090] + t(user_id, 'truncation')
|
||||
return text, None
|
||||
|
||||
|
||||
async def _show_class_picker(update, context) -> bool:
|
||||
"""Fetch classes from schedule and render the class-picker keyboard."""
|
||||
user_id = update.effective_user.id
|
||||
schedule_data = await _get_schedule_data(context)
|
||||
if not schedule_data:
|
||||
await update.message.reply_text('❌ Не вдалося завантажити розклад.')
|
||||
await update.message.reply_text(t(user_id, 'schedule_load_failed'))
|
||||
return False
|
||||
classes = schedule_data.get('classes', [])
|
||||
if not classes:
|
||||
await update.message.reply_text('❌ Класи не знайдені в розкладі.')
|
||||
await update.message.reply_text(t(user_id, 'class_not_found'))
|
||||
return False
|
||||
await update.message.reply_text(
|
||||
'🎒 <b>Оберіть клас</b> — збережеться і більше не питатиметься:',
|
||||
t(user_id, 'select_class'),
|
||||
parse_mode='HTML',
|
||||
reply_markup=get_schedule_class_keyboard(classes),
|
||||
)
|
||||
@@ -960,19 +1163,19 @@ async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
||||
if not stored_class:
|
||||
if chat.type != 'private' and not await is_group_admin(update, context):
|
||||
await update.message.reply_text('⚠️ Адміністратор ще не налаштував клас для цієї групи. Використайте /setclass 11')
|
||||
await update.message.reply_text(t(user.id, 'admin_class_not_set'))
|
||||
return
|
||||
await _show_class_picker(update, context)
|
||||
return
|
||||
|
||||
await update.message.reply_text('🔄 Завантажую розклад...')
|
||||
await update.message.reply_text(t(user.id, 'loading_schedule'))
|
||||
today = datetime.now()
|
||||
weekday = SCHEDULE_WEEKDAYS_FULL[today.weekday()]
|
||||
weekday = SCHEDULE_WEEKDAYS_CANONICAL[today.weekday()]
|
||||
text, err = await _schedule_text_for(context, chat, user.id, weekday)
|
||||
if err:
|
||||
await update.message.reply_text(err)
|
||||
return
|
||||
await update.message.reply_text(text, parse_mode='HTML', reply_markup=get_schedule_day_keyboard())
|
||||
await update.message.reply_text(text, parse_mode='HTML', reply_markup=get_schedule_day_keyboard(user.id))
|
||||
|
||||
|
||||
async def setclass_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
@@ -990,10 +1193,10 @@ async def setclass_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
try:
|
||||
class_num = int(context.args[0])
|
||||
except ValueError:
|
||||
await update.message.reply_text('❌ Невірний номер класу.')
|
||||
await update.message.reply_text(t(user.id, 'invalid_class_num'))
|
||||
return
|
||||
_set_chat_class(chat, user.id, class_num)
|
||||
await update.message.reply_text(f'✅ Клас <b>{class_num}</b> збережено.')
|
||||
await update.message.reply_text(t(user.id, 'class_saved', class_num=class_num), parse_mode='HTML')
|
||||
return
|
||||
|
||||
await _show_class_picker(update, context)
|
||||
@@ -1005,7 +1208,7 @@ async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await query.answer()
|
||||
|
||||
if user_id != ADMIN_ID and not is_whitelisted(user_id):
|
||||
await query.edit_message_text('⛔ Доступ заборонено.')
|
||||
await query.edit_message_text(t(user_id, 'access_denied'))
|
||||
return
|
||||
|
||||
data = query.data
|
||||
@@ -1015,46 +1218,50 @@ async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
try:
|
||||
class_num = int(class_num_str)
|
||||
except ValueError:
|
||||
await query.edit_message_text('❌ Невірний номер класу.')
|
||||
await query.edit_message_text(t(user_id, 'invalid_class_num'))
|
||||
return
|
||||
|
||||
chat = update.effective_chat
|
||||
if chat.type != 'private' and not await is_group_admin(update, context):
|
||||
await query.edit_message_text('⚠️ Тільки адміністратор може налаштувати клас для групи.')
|
||||
await query.edit_message_text(t(user_id, 'admin_only_msg'))
|
||||
return
|
||||
|
||||
_set_chat_class(chat, user_id, class_num)
|
||||
await query.edit_message_text('🔄 Завантажую розклад...')
|
||||
await query.edit_message_text(t(user_id, 'loading_schedule'))
|
||||
|
||||
today = datetime.now()
|
||||
weekday = SCHEDULE_WEEKDAYS_FULL[today.weekday()]
|
||||
weekday = SCHEDULE_WEEKDAYS_CANONICAL[today.weekday()]
|
||||
text, err = await _schedule_text_for(context, chat, user_id, weekday)
|
||||
if err:
|
||||
await query.edit_message_text(err)
|
||||
return
|
||||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_schedule_day_keyboard())
|
||||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_schedule_day_keyboard(user_id))
|
||||
return
|
||||
|
||||
if data.startswith('schedule_day_'):
|
||||
weekday = data[len('schedule_day_'):]
|
||||
await query.edit_message_text('🔄 Завантажую розклад...')
|
||||
try:
|
||||
_SCHEDULE_WEEKDAYS_CANONICAL_NORM.index(_norm_day(weekday))
|
||||
except ValueError:
|
||||
logger.warning(f'Schedule callback weekday not in canonical list: {weekday!r}')
|
||||
await query.edit_message_text(t(user_id, 'loading_schedule'))
|
||||
text, err = await _schedule_text_for(context, query.message.chat, user_id, weekday)
|
||||
if err:
|
||||
await query.edit_message_text(err)
|
||||
return
|
||||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_schedule_day_keyboard())
|
||||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_schedule_day_keyboard(user_id))
|
||||
return
|
||||
|
||||
if data == 'schedule_refresh':
|
||||
context.user_data.pop('schedule_cache', None)
|
||||
await query.edit_message_text('🔄 Завантажую розклад...')
|
||||
await query.edit_message_text(t(user_id, 'loading_schedule'))
|
||||
today = datetime.now()
|
||||
weekday = SCHEDULE_WEEKDAYS_FULL[today.weekday()]
|
||||
weekday = SCHEDULE_WEEKDAYS_CANONICAL[today.weekday()]
|
||||
text, err = await _schedule_text_for(context, query.message.chat, user_id, weekday)
|
||||
if err:
|
||||
await query.edit_message_text(err)
|
||||
return
|
||||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_schedule_day_keyboard())
|
||||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_schedule_day_keyboard(user_id))
|
||||
return
|
||||
|
||||
|
||||
@@ -1103,17 +1310,31 @@ async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await query.message.reply_text(t(user_id, 'check_completed', count=result))
|
||||
|
||||
|
||||
async def language_callback(update: Update, _context: ContextTypes.DEFAULT_TYPE):
|
||||
async def language_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle language selection button clicks."""
|
||||
query = update.callback_query
|
||||
user_id = query.from_user.id
|
||||
chat = update.effective_chat
|
||||
data = query.data
|
||||
|
||||
if data.startswith('lang_'):
|
||||
lang = data.split('_')[1]
|
||||
set_user_language(user_id, lang)
|
||||
await query.answer()
|
||||
await query.edit_message_text(t(user_id, 'language_changed'), parse_mode='HTML')
|
||||
is_private = chat is None or getattr(chat, 'type', 'private') in (ChatType.PRIVATE, 'private')
|
||||
if is_private:
|
||||
set_user_language(user_id, lang)
|
||||
await query.answer()
|
||||
await query.edit_message_text(
|
||||
t(user_id, 'language_changed', lang=LANG_NAME_MAP.get(lang, lang)), parse_mode='HTML'
|
||||
)
|
||||
else:
|
||||
if not await is_group_admin(update, context):
|
||||
await query.answer(t_chat(chat, user_id, 'admin_only'), show_alert=True)
|
||||
return
|
||||
set_chat_language(chat.id, lang)
|
||||
await query.answer()
|
||||
await query.edit_message_text(
|
||||
_tr(lang, 'language_changed', lang=LANG_NAME_MAP.get(lang, lang)), parse_mode='HTML'
|
||||
)
|
||||
|
||||
|
||||
# --- Webinar Checking Job ---
|
||||
@@ -1198,7 +1419,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
content = await page.content()
|
||||
|
||||
# Check if "no webinar" message is present
|
||||
if 'Жодного онлайн уроку зараз' not in content:
|
||||
if NO_WEBINAR_MARKER not in content:
|
||||
logger.info('!!! WEBINAR FOUND !!!')
|
||||
|
||||
# Extract webinar details from table rows
|
||||
@@ -1209,7 +1430,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
row = rows.nth(i)
|
||||
text = await row.inner_text()
|
||||
|
||||
if 'Жодного онлайн уроку зараз' not in text:
|
||||
if NO_WEBINAR_MARKER not in text:
|
||||
# Extract name (topic) from first column
|
||||
name_elem = row.locator('td').nth(0)
|
||||
name = await name_elem.inner_text()
|
||||
@@ -1289,9 +1510,9 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
# Build message in user's language
|
||||
# sub_id comes from redis set as string, convert to int for translation lookup
|
||||
webinar_items = '\n\n'.join(
|
||||
[t(int(sub_id), 'webinar_item', name=w['name'], url=w['url']) for w in new_webinars]
|
||||
[t_chat(int(sub_id), int(sub_id), 'webinar_item', name=escape(w['name']), url=escape(w['url'])) for w in new_webinars]
|
||||
)
|
||||
message = t(int(sub_id), 'webinar_found') + webinar_items
|
||||
message = t_chat(int(sub_id), int(sub_id), 'webinar_found') + webinar_items
|
||||
|
||||
await context.bot.send_message(chat_id=sub_id, text=message, parse_mode='HTML')
|
||||
logger.info(f'Notification sent to {sub_id}')
|
||||
|
||||
Reference in New Issue
Block a user