"""Async scheduler for daily birthday notifications.""" from telebot.async_telebot import AsyncTeleBot from datetime import datetime, timedelta, date from typing import Dict, List, Tuple from collections import defaultdict from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger import pytz from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from bot.database import get_db_session, User, Chat, UserChat from bot.messages import format_birthday_greeting, format_multiple_birthdays_greetings from bot.config import Config from bot.handlers.command_handlers import get_birthdays_in_range from bot.logger import get_logger logger = get_logger(__name__) async def send_birthday_notifications(bot: AsyncTeleBot) -> None: """Send birthday notifications to all chats for users with birthday today.""" logger.info("Running daily birthday notification job") async for db in get_db_session(): try: today = datetime.now().date() day = today.day month = today.month logger.debug(f"Checking birthdays for {day:02d}.{month:02d}") # Get all users with birthday today result = await db.execute( select(User).filter( User.birthday_day == day, User.birthday_month == month ) ) users_with_birthday = result.scalars().all() if not users_with_birthday: logger.info(f"No birthdays found for {day:02d}.{month:02d}") return logger.info(f"Found {len(users_with_birthday)} user(s) with birthday today") # Group users by chat: chat_id -> list of (first_name, theme, user_id) chat_users: Dict[int, List[Tuple[str, str, int]]] = defaultdict(list) # For each user, find all their chats for user in users_with_birthday: # Get all chats where user is a member result = await db.execute( select(Chat).join(UserChat).filter( UserChat.user_id == user.user_id, Chat.bot_is_admin == True ) ) user_chats = result.scalars().all() for chat in user_chats: try: # Check if user is still in chat member = await bot.get_chat_member(chat.chat_id, user.user_id) if member.status not in ['left', 'kicked']: chat_users[chat.chat_id].append( (user.first_name, user.preference_theme, user.user_id) ) except Exception: # User might have blocked bot or bot was removed from chat pass # Send greetings grouped by chat for chat_id, users_data in chat_users.items(): try: logger.info(f"Sending birthday greetings to chat {chat_id} for {len(users_data)} user(s)") if len(users_data) == 1: # Single user - use simple format first_name, theme, user_id = users_data[0] greeting = format_birthday_greeting(first_name, theme, user_id) await bot.send_message(chat_id, greeting, parse_mode='HTML') logger.debug(f"Sent single birthday greeting to chat {chat_id} for user {user_id}") else: # Multiple users - use special format greeting = format_multiple_birthdays_greetings(users_data) await bot.send_message(chat_id, greeting, parse_mode='HTML') logger.debug(f"Sent multiple birthday greetings to chat {chat_id}") except Exception as e: # Handle errors: bot removed from chat, etc. if hasattr(e, 'error_code') and e.error_code == 403: logger.warning(f"Bot blocked or removed from chat {chat_id}, updating status") # Bot was blocked or removed from chat result = await db.execute(select(Chat).filter(Chat.chat_id == chat_id)) chat = result.scalar_one_or_none() if chat: chat.bot_is_admin = False await db.commit() else: logger.error(f"Error sending birthday greeting to chat {chat_id}: {e}", exc_info=True) finally: await db.close() logger.info("Daily birthday notification job completed") async def send_monthly_birthday_overview(bot: AsyncTeleBot) -> None: """Send monthly birthday overview (like /month command) to all chats on 1st of each month.""" logger.info("Running monthly birthday overview job") sent_count = 0 async for db in get_db_session(): try: today = datetime.now().date() logger.debug(f"Sending monthly overview for date: {today}") # Get all chats where bot is admin result = await db.execute(select(Chat).filter(Chat.bot_is_admin == True)) admin_chats = result.scalars().all() logger.info(f"Found {len(admin_chats)} admin chat(s) for monthly overview") for chat in admin_chats: try: # Get birthdays for next 31 days birthdays = await get_birthdays_in_range(db, chat.chat_id, today, days=31) if not birthdays: # Don't send message if no birthdays logger.debug(f"No birthdays in next 31 days for chat {chat.chat_id}") continue # Format message (same as /month command) message_text = "πŸŽ‚ Π”Π½ΠΈ роТдСния Π½Π° блиТайшиС 31 дСнь:\n\n" for date_str, names in sorted(birthdays.items()): names_list = ", ".join(names) message_text += f"β€’ {date_str}: {names_list}\n" await bot.send_message(chat.chat_id, message_text) sent_count += 1 logger.info(f"Sent monthly overview to chat {chat.chat_id} ({len(birthdays)} dates)") except Exception as e: # Handle errors: bot removed from chat, etc. if hasattr(e, 'error_code') and e.error_code == 403: logger.warning(f"Bot blocked or removed from chat {chat.chat_id}, updating status") # Bot was blocked or removed from chat chat.bot_is_admin = False await db.commit() else: logger.error(f"Error sending monthly overview to chat {chat.chat_id}: {e}", exc_info=True) finally: await db.close() logger.info(f"Monthly birthday overview job completed. Sent to {sent_count} chat(s)") def setup_scheduler(bot: AsyncTeleBot) -> AsyncIOScheduler: """Setup and start the scheduler for daily birthday notifications.""" # Parse notification time time_str: str = Config.NOTIFICATION_TIME try: hour, minute = map(int, time_str.split(':')) except (ValueError, AttributeError): hour, minute = 9, 0 # Default to 9:00 # Get timezone timezone_str: str = Config.TIMEZONE try: tz = pytz.timezone(timezone_str) except (pytz.exceptions.UnknownTimeZoneError, AttributeError): tz = pytz.UTC # Create scheduler scheduler = AsyncIOScheduler(timezone=tz) # Add daily job scheduler.add_job( send_birthday_notifications, trigger=CronTrigger(hour=hour, minute=minute), args=[bot], id='daily_birthday_notifications', name='Send daily birthday notifications', replace_existing=True ) # Add monthly job (1st day of each month) scheduler.add_job( send_monthly_birthday_overview, trigger=CronTrigger(day=1, hour=hour, minute=minute), args=[bot], id='monthly_birthday_overview', name='Send monthly birthday overview', replace_existing=True ) return scheduler