189 lines
8.5 KiB
Python
189 lines
8.5 KiB
Python
"""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
|