diff --git a/bot/database/competition_dao.py b/bot/database/competition_dao.py index 154fd3d..24d26e2 100644 --- a/bot/database/competition_dao.py +++ b/bot/database/competition_dao.py @@ -4,7 +4,7 @@ class CompetitionDao: - """Data access object for Task""" + """Data access object for Competition""" def __init__(self, session): self.session = session @@ -29,3 +29,14 @@ def add_competition( self.session.commit() self.session.refresh(new_competition) return new_competition + + def get_events_between(self, start_time: datetime, end_time: datetime): + """Получить события в заданном временном диапазоне.""" + return ( + self.session.query(Competition) + .filter( + Competition.date >= start_time, + Competition.date < end_time + ) + .all() + ) diff --git a/bot/main.py b/bot/main.py index 440f4be..d677d22 100644 --- a/bot/main.py +++ b/bot/main.py @@ -22,6 +22,7 @@ from settings import config from middlewares import AuthMiddleware from tasks import sync_education_tasks +from tasks import send_event_notifications # Включаем логирование, чтобы не пропустить важные сообщения logging.basicConfig(level=logging.INFO) @@ -66,7 +67,6 @@ async def main(): - scheduler = AsyncIOScheduler() # Добавляем периодическую задачу восстановления жизней # Выполняется каждое 10-е число месяца в 00:00 по МСК (UTC+3) @@ -82,14 +82,25 @@ async def main(): name="Восстановление жизней студентов", replace_existing=True, ) - + scheduler.add_job( + send_event_notifications, + args=[bot], + trigger=CronTrigger( + hour=6, + minute=0, + timezone=timezone("Europe/Moscow"), + ), + id="send_event_notifications", + name="Отправка уведомлений о событиях", + replace_existing=True, + ) # Запускаем планировщик scheduler.start() await bot.set_my_commands(commands) # Запускаем задачу синхронизации задач asyncio.create_task(sync_education_tasks(bot)) # Фоновая задача - await dp.start_polling(bot) + if __name__ == "__main__": asyncio.run(main()) diff --git a/bot/tasks.py b/bot/tasks.py index 580d0d6..866613b 100644 --- a/bot/tasks.py +++ b/bot/tasks.py @@ -1,11 +1,14 @@ import asyncio +from datetime import datetime, timedelta from aiogram import Bot from logging import getLogger +from pytz import timezone from utils.notifications import Notifications from database.db import get_db from database.user_dao import UserDAO +from database.competition_dao import CompetitionDao from utils.root_me import get_solved_tasks_of_student @@ -26,7 +29,10 @@ async def sync_education_tasks(bot: Bot): ) for task in user.tasks: task.completed = task.name in solved_tasks - if not task.completed and task.is_expired and not task.violation_recorded: + if ( + not task.completed and task.is_expired + and not task.violation_recorded + ): user.lives -= 1 user.violations += 1 task.violation_recorded = True # Отмечаем, что нарушение обработано @@ -35,9 +41,16 @@ async def sync_education_tasks(bot: Bot): ) logger.info(teacher_message) notify = Notifications(bot) - await notify.say_about_deadline_fail(teacher_message) - student_message = f"Ты потерял 1 HP за задачу {task.name}. 😢 Пожалуйста, старайся выполнять задания вовремя, чтобы избежать потерь.\ - Если у тебя есть вопросы или трудности, не стесняйся обращаться за помощью в общий чат." + await notify.say_about_deadline_fail( + teacher_message + ) + student_message = ( + f"Ты потерял 1 HP за задачу {task.name}. 😢 " + "Пожалуйста, старайся выполнять задания вовремя, " + "чтобы избежать потерь. Если у тебя есть вопросы " + "или трудности, не стесняйся обращаться за помощью " + "в общий чат." + ) logger.info(student_message) await notify._say_student(user, student_message) @@ -75,5 +88,42 @@ async def restore_student_lives(): logger.error(f"Ошибка при восстановлении жизней: {e}") -if __name__ == "__main__": - asyncio.run(sync_education_tasks()) +async def notify_event_participants(bot: Bot, event, prefix: str): + notify = Notifications(bot) + for participation in event.participations: + user = participation.user + msg = f"{prefix}: {event.name} в {event.date.strftime('%H:%M')}." + await notify._say_student(user, msg) + + +async def send_event_notifications(bot: Bot): + """Отправка уведомлений о событиях за 1 день и в день мероприятия.""" + try: + moscow_tz = timezone('Europe/Moscow') + now = datetime.now(moscow_tz) + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + today_end = today_start + timedelta(days=1) + tomorrow_start = today_start + timedelta(days=1) + tomorrow_end = today_start + timedelta(days=2) + + with get_db() as session: + dao = CompetitionDao(session) + today_events = dao.get_events_between(today_start, today_end) + tomorrow_events = dao.get_events_between( + tomorrow_start, tomorrow_end + ) + + for event in today_events: + logger.info(f"Уведомление о событии сегодня: {event.name}") + await notify_event_participants(bot, event, "Сегодня") + + for event in tomorrow_events: + logger.info(f"Уведомление о событии завтра: {event.name}") + await notify_event_participants(bot, event, "Завтра") + + except Exception as e: + logger.error(f"Ошибка при отправке уведомлений: {e}") + + +# Закомментировал __main__-блок, потому что этот файл подключается как модуль в основном боте, +# и запуск его напрямую приведёт к ошибке из-за отсутствия аргумента bot.