chore: batch lint fixes across userbot and edu_master
- S113: Add timeout=10 to all requests calls (74 fixes) - E722: Replace bare except: with except Exception: - B904: Replace redundant re-raise with bare raise - E402: Add noqa for intentional late imports after import_library() - S102/S307/S310/S311/S603/S605/S606/S607/S108: Add noqa for intentional usage - F601: Fix duplicate dict key in unsplash.py - N802: Rename ReplyCheck -> reply_check with backward compat alias - N813: Rename bs -> BS in icons.py - B007/B020: Rename loop var _j in animations.py - SIM102: Collapse nested if in autofwd.py - SIM113: Use enumerate() in calculator.py - A002: Add noqa for builtin shadowing in admlist.py - F811: Add noqa for cohere redefinition - edu_master: Fix ARG001, S108, S110, SIM117, apply --unsafe-fixes - Add modules_list.txt with full module inventory
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
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
|
||||
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
|
||||
import redis
|
||||
from playwright.async_api import async_playwright
|
||||
from telegram import ChatMember, InlineKeyboardButton, InlineKeyboardMarkup, Update
|
||||
from telegram.constants import ChatType
|
||||
from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes
|
||||
|
||||
# Logger
|
||||
logging.basicConfig(
|
||||
@@ -221,21 +222,21 @@ def is_whitelisted(user_id: int) -> bool:
|
||||
"""Check if user is allowed to use the bot."""
|
||||
if user_id == ADMIN_ID:
|
||||
return True
|
||||
|
||||
|
||||
enabled = redis_client.get(KEY_WHITELIST_ENABLED)
|
||||
if enabled == "0": # Whitelist disabled
|
||||
return True
|
||||
|
||||
|
||||
return redis_client.sismember(KEY_WHITELIST, str(user_id))
|
||||
|
||||
async def is_group_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
|
||||
"""Check if the user is an administrator in the group."""
|
||||
user = update.effective_user
|
||||
chat = update.effective_chat
|
||||
|
||||
|
||||
if chat.type in [ChatType.PRIVATE, "private"]:
|
||||
return True
|
||||
|
||||
|
||||
try:
|
||||
member = await context.bot.get_chat_member(chat.id, user.id)
|
||||
return member.status in [ChatMember.OWNER, ChatMember.ADMINISTRATOR]
|
||||
@@ -247,7 +248,7 @@ def get_admin_keyboard(user_id: int):
|
||||
"""Generate admin panel keyboard."""
|
||||
whitelist_enabled = redis_client.get(KEY_WHITELIST_ENABLED) != "0"
|
||||
toggle_text = t(user_id, 'toggle_whitelist_disable') if whitelist_enabled else t(user_id, 'toggle_whitelist_enable')
|
||||
|
||||
|
||||
keyboard = [
|
||||
[InlineKeyboardButton(toggle_text, callback_data="toggle_whitelist")],
|
||||
[InlineKeyboardButton(t(user_id, 'view_whitelist'), callback_data="view_whitelist")],
|
||||
@@ -292,10 +293,7 @@ def _parse_calendar_html(table_html: str) -> tuple:
|
||||
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
|
||||
month_text = m.group(1).replace(' : ', ' ').strip() if m else raw
|
||||
elif r_idx == 1:
|
||||
# Day names row
|
||||
for cell in cells:
|
||||
@@ -319,10 +317,7 @@ def _parse_calendar_html(table_html: str) -> tuple:
|
||||
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()
|
||||
et = title_m.group(1).strip() if title_m else re.sub(r'<[^>]+>', '', a_match.group(1)).strip()
|
||||
if et:
|
||||
events.append(et)
|
||||
|
||||
@@ -363,11 +358,9 @@ async def fetch_diary_data(phpsessid: str) -> dict | None:
|
||||
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
|
||||
with contextlib.suppress(Exception), \
|
||||
open('/tmp/diary_debug.html', 'w', encoding='utf-8') as f: # noqa: S108
|
||||
f.write(table_html)
|
||||
|
||||
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)}")
|
||||
@@ -407,7 +400,7 @@ def format_diary_day(data: dict, day_num: int) -> str:
|
||||
|
||||
def format_diary_week(data: dict, today: datetime) -> str:
|
||||
days = data.get('days', {})
|
||||
month_str = _parse_diary_month(data.get('monthFullText', ''))
|
||||
_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"]
|
||||
@@ -457,12 +450,12 @@ async def _get_diary_data(context: ContextTypes.DEFAULT_TYPE) -> dict | None:
|
||||
|
||||
# --- Command Handlers ---
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
async def start(update: Update, _context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle /start command."""
|
||||
user = update.effective_user
|
||||
chat = update.effective_chat
|
||||
logger.info(f"User {user.id} ({user.username}) started the bot in chat {chat.id} ({chat.type}).")
|
||||
|
||||
|
||||
# Check whitelist - MUST be the user executing the command
|
||||
if not is_whitelisted(user.id):
|
||||
await update.message.reply_text(t(user.id, 'access_denied'))
|
||||
@@ -470,21 +463,21 @@ 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)
|
||||
|
||||
|
||||
if user.id == ADMIN_ID and chat.type == "private":
|
||||
msg += t(chat.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')
|
||||
|
||||
async def help_command(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')
|
||||
|
||||
|
||||
if user_id == ADMIN_ID and update.effective_chat.type == "private":
|
||||
msg += t(chat_id, 'help_admin')
|
||||
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user_id))
|
||||
@@ -495,12 +488,12 @@ async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle /stop command (unsubscribe)."""
|
||||
user = update.effective_user
|
||||
chat = update.effective_chat
|
||||
|
||||
|
||||
# Permission check: Whitelisted user OR Group Admin
|
||||
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
|
||||
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")
|
||||
|
||||
@@ -508,7 +501,7 @@ async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle /language command."""
|
||||
user = update.effective_user
|
||||
chat = update.effective_chat
|
||||
|
||||
|
||||
# Permission check for groups
|
||||
if not (is_whitelisted(user.id) or await is_group_admin(update, context)):
|
||||
return
|
||||
@@ -525,11 +518,11 @@ async def add_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if admin_id != ADMIN_ID:
|
||||
await update.message.reply_text(t(admin_id, 'admin_only'))
|
||||
return
|
||||
|
||||
|
||||
if not context.args:
|
||||
await update.message.reply_text(t(admin_id, 'usage_adduser'))
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
user_id = int(context.args[0])
|
||||
redis_client.sadd(KEY_WHITELIST, str(user_id))
|
||||
@@ -544,17 +537,17 @@ async def remove_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if admin_id != ADMIN_ID:
|
||||
await update.message.reply_text(t(admin_id, 'admin_only'))
|
||||
return
|
||||
|
||||
|
||||
if not context.args:
|
||||
await update.message.reply_text(t(admin_id, 'usage_removeuser'))
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
user_id = int(context.args[0])
|
||||
if str(user_id) == str(ADMIN_ID):
|
||||
await update.message.reply_text(t(admin_id, 'cannot_remove_admin'))
|
||||
return
|
||||
|
||||
|
||||
removed = redis_client.srem(KEY_WHITELIST, str(user_id))
|
||||
if removed:
|
||||
await update.message.reply_text(t(admin_id, 'user_removed', user_id=user_id))
|
||||
@@ -564,13 +557,13 @@ async def remove_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
except ValueError:
|
||||
await update.message.reply_text(t(admin_id, 'invalid_user_id'))
|
||||
|
||||
async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
async def clear_history(update: Update, _context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Clear webinar history (admin only)."""
|
||||
admin_id = update.effective_user.id
|
||||
if admin_id != ADMIN_ID:
|
||||
await update.message.reply_text(t(admin_id, 'admin_only'))
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
redis_client.delete(KEY_WEBINAR_HISTORY)
|
||||
await update.message.reply_text(t(admin_id, 'history_cleared'))
|
||||
@@ -579,9 +572,8 @@ 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):
|
||||
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
|
||||
@@ -596,10 +588,9 @@ async def diary_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
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
|
||||
if user_id != ADMIN_ID and not is_whitelisted(user_id):
|
||||
await query.edit_message_text("⛔ Доступ заборонено.")
|
||||
return
|
||||
|
||||
data = query.data
|
||||
if data == "diary_refresh":
|
||||
@@ -653,7 +644,7 @@ async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Handle admin panel button clicks."""
|
||||
query = update.callback_query
|
||||
user_id = query.from_user.id
|
||||
|
||||
|
||||
if user_id != ADMIN_ID:
|
||||
await query.answer(t(user_id, 'admin_only'), show_alert=True)
|
||||
return
|
||||
@@ -678,11 +669,11 @@ async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
subs = redis_client.smembers(KEY_SUBSCRIBERS)
|
||||
msg = t(user_id, 'subscribers_title') + ("\n".join(subs) if subs else t(user_id, 'empty'))
|
||||
await query.message.reply_text(msg, parse_mode='HTML')
|
||||
|
||||
|
||||
elif data == "force_check":
|
||||
await query.message.reply_text(t(user_id, 'force_check_running'))
|
||||
result = await check_webinars_job(context)
|
||||
|
||||
|
||||
if result is None:
|
||||
await query.message.reply_text(t(user_id, 'check_failed'))
|
||||
elif result == 0:
|
||||
@@ -690,12 +681,12 @@ async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
else:
|
||||
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
|
||||
data = query.data
|
||||
|
||||
|
||||
if data.startswith("lang_"):
|
||||
lang = data.split("_")[1]
|
||||
set_user_language(user_id, lang)
|
||||
@@ -733,12 +724,12 @@ def store_webinars(webinar_keys: list):
|
||||
|
||||
async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Background job to check for webinars using Async Playwright.
|
||||
|
||||
|
||||
Returns:
|
||||
int: Number of webinars found, or None if check failed
|
||||
"""
|
||||
logger.info("Running webinar check...")
|
||||
|
||||
|
||||
phpsessid = redis_client.get(KEY_PHPSESSID)
|
||||
if not phpsessid:
|
||||
logger.warning("PHPSESSID missing. Skipping check.")
|
||||
@@ -753,16 +744,16 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
|
||||
current_webinars = [] # List of dicts with name, url, and formatted text
|
||||
content = ""
|
||||
|
||||
|
||||
try:
|
||||
async with async_playwright() as p:
|
||||
# Connect to remote Playwright service
|
||||
browser = await p.chromium.connect(PLAYWRIGHT_WS)
|
||||
|
||||
|
||||
try:
|
||||
# Create browser context with user agent
|
||||
context_browser = await browser.new_context(user_agent=USER_AGENT)
|
||||
|
||||
|
||||
# Add PHPSESSID cookie
|
||||
await context_browser.add_cookies([{
|
||||
'name': 'PHPSESSID',
|
||||
@@ -770,43 +761,43 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
'domain': 'edu.edu.vn.ua',
|
||||
'path': '/'
|
||||
}])
|
||||
|
||||
|
||||
# Create new page
|
||||
page = await context_browser.new_page()
|
||||
|
||||
|
||||
try:
|
||||
# Navigate to webinar page
|
||||
await page.goto(WEBINAR_URL, wait_until='domcontentloaded')
|
||||
|
||||
|
||||
# Wait for the table to load
|
||||
await page.wait_for_selector('#meetings table', timeout=10000)
|
||||
await page.wait_for_timeout(2000)
|
||||
|
||||
|
||||
# Get page content
|
||||
content = await page.content()
|
||||
|
||||
|
||||
# Check if "no webinar" message is present
|
||||
if "Жодного онлайн уроку зараз" not in content:
|
||||
logger.info("!!! WEBINAR FOUND !!!")
|
||||
|
||||
|
||||
# Extract webinar details from table rows
|
||||
rows = page.locator('#meetings table tbody tr')
|
||||
count = await rows.count()
|
||||
|
||||
|
||||
for i in range(count):
|
||||
row = rows.nth(i)
|
||||
text = await row.inner_text()
|
||||
|
||||
|
||||
if "Жодного онлайн уроку зараз" not in text:
|
||||
# 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,
|
||||
@@ -816,28 +807,26 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
logger.info(f"Found webinar: {name} -> {url}")
|
||||
else:
|
||||
logger.info("No webinars found (expected message present)")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking page: {e}. Saving content for debug.")
|
||||
# If page content is available, save it on error
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
if page and not content:
|
||||
content = await page.content()
|
||||
except Exception:
|
||||
pass # Ignore error during content retrieval on check error
|
||||
|
||||
|
||||
return None
|
||||
finally:
|
||||
await page.close()
|
||||
await context_browser.close()
|
||||
|
||||
|
||||
finally:
|
||||
await browser.close()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Playwright error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# --- DEBUG LOGGING (Saving last response content) ---
|
||||
if not current_webinars and content: #if no webinars found, save the page content
|
||||
try:
|
||||
@@ -853,29 +842,29 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
# Get stored webinar history
|
||||
stored_keys = get_stored_webinars()
|
||||
logger.info(f"Stored webinar keys: {stored_keys}")
|
||||
|
||||
|
||||
# Find new webinars (not in history)
|
||||
new_webinars = []
|
||||
current_keys = []
|
||||
|
||||
|
||||
for webinar in current_webinars:
|
||||
key = get_webinar_key(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:
|
||||
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:
|
||||
# Build message in user's language
|
||||
@@ -885,14 +874,14 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
||||
for w in new_webinars
|
||||
])
|
||||
message = t(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}")
|
||||
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 ---
|
||||
@@ -921,7 +910,7 @@ def main():
|
||||
app.add_handler(CommandHandler("removeuser", remove_user))
|
||||
app.add_handler(CommandHandler("clearhistory", clear_history))
|
||||
app.add_handler(CommandHandler("diary", diary_command))
|
||||
|
||||
|
||||
# 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_"))
|
||||
@@ -935,4 +924,4 @@ def main():
|
||||
app.run_polling()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user