From a16a979c63af71ce9dccbec19b3190ef094a1788 Mon Sep 17 00:00:00 2001 From: Olly Hearn Date: Wed, 28 Jan 2026 13:41:59 +0300 Subject: [PATCH] feat: 01 schedule --- handlers/command_handlers.py | 12 ++--- handlers/scheduler.py | 88 +++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/handlers/command_handlers.py b/handlers/command_handlers.py index ce01ceb..1423327 100644 --- a/handlers/command_handlers.py +++ b/handlers/command_handlers.py @@ -98,7 +98,7 @@ def register_command_handlers(bot: telebot.TeleBot) -> None: @bot.message_handler(commands=['month'], chat_types=['group', 'supergroup']) def handle_month(message: telebot.types.Message) -> None: - """Handle /month command - show birthdays for next 30 days.""" + """Handle /month command - show birthdays for next 31 days.""" chat_id = message.chat.id db = get_db_session() @@ -109,16 +109,16 @@ def register_command_handlers(bot: telebot.TeleBot) -> None: bot.reply_to(message, "Мне нужны права администратора для выполнения этой команды.") return - # Get birthdays for next 30 days + # Get birthdays for next 31 days today = datetime.now().date() - birthdays = get_birthdays_in_range(db, chat_id, today, days=30) + birthdays = get_birthdays_in_range(db, chat_id, today, days=31) if not birthdays: - bot.reply_to(message, "На ближайшие 30 дней дней рождений не запланировано.") + bot.reply_to(message, "На ближайшие 31 день дней рождений не запланировано.") return # Format message - message_text = "🎂 Дни рождения на ближайшие 30 дней:\n\n" + 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" @@ -134,7 +134,7 @@ def register_command_handlers(bot: telebot.TeleBot) -> None: "📚 Команды бота:\n\n" "/stats - Показать статистику: сколько человек поделились днем рождения\n" "/week - Показать дни рождения на ближайшие 7 дней\n" - "/month - Показать дни рождения на ближайшие 30 дней\n" + "/month - Показать дни рождения на ближайшие 31 день\n" "/help - Показать это сообщение\n\n" "Чтобы поделиться своим днем рождения, напиши боту в личку /start\n\n" "from olly & cursor with <3" diff --git a/handlers/scheduler.py b/handlers/scheduler.py index cdf568b..7cf3c02 100644 --- a/handlers/scheduler.py +++ b/handlers/scheduler.py @@ -1,11 +1,12 @@ """Scheduler for daily birthday notifications.""" import telebot -from datetime import datetime +from datetime import datetime, timedelta, date from typing import Optional, Dict, List, Tuple from collections import defaultdict from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger import pytz +from sqlalchemy.orm import Session from database import get_db_session, User, Chat, UserChat from messages import format_birthday_greeting, format_multiple_birthdays_greetings from config import Config @@ -119,4 +120,89 @@ def setup_scheduler(bot: telebot.TeleBot) -> BlockingScheduler: 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 + + +def get_birthdays_in_range_for_chat(db: Session, chat_id: int, start_date: date, days: int) -> Dict[str, List[str]]: + """Get birthdays in the specified date range for users in the chat.""" + birthdays = defaultdict(list) + + # Get all users in this chat + users = db.query(User).join(UserChat).filter( + UserChat.chat_id == chat_id + ).distinct().all() + + end_date = start_date + timedelta(days=days) + + for user in users: + # Create birthday date for current year + try: + birthday_this_year = datetime(start_date.year, user.birthday_month, user.birthday_day).date() + except ValueError: + # Invalid date (e.g., Feb 29 in non-leap year) + continue + + # Check if birthday falls in range + if start_date <= birthday_this_year < end_date: + date_str = f"{user.birthday_day:02d}.{user.birthday_month:02d}" + birthdays[date_str].append(user.first_name) + else: + # Check next year if we're near year end + try: + birthday_next_year = datetime(start_date.year + 1, user.birthday_month, user.birthday_day).date() + if start_date <= birthday_next_year < end_date: + date_str = f"{user.birthday_day:02d}.{user.birthday_month:02d}" + birthdays[date_str].append(user.first_name) + except ValueError: + pass + + return birthdays + + +def send_monthly_birthday_overview(bot: telebot.TeleBot) -> None: + """Send monthly birthday overview (like /month command) to all chats on 1st of each month.""" + db = get_db_session() + try: + today = datetime.now().date() + + # Get all chats where bot is admin + admin_chats = db.query(Chat).filter(Chat.bot_is_admin == True).all() + + for chat in admin_chats: + try: + # Get birthdays for next 31 days + birthdays = get_birthdays_in_range_for_chat(db, chat.chat_id, today, days=31) + + if not birthdays: + # Don't send message if no birthdays + 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" + + bot.send_message(chat.chat_id, message_text) + except telebot.apihelper.ApiTelegramException as e: + # Handle errors: bot removed from chat, etc. + if e.error_code == 403: + # Bot was blocked or removed from chat + chat.bot_is_admin = False + db.commit() + # Silently continue for other errors + except Exception: + # Other errors - continue + pass + finally: + db.close()