import asyncio from random import randint from datetime import datetime, timedelta from aiogram import Bot, types from config import Config from logging import getLogger from models.state import BotState from services.schedule_service import ScheduleService logger = getLogger(__name__) class WatcherService: def __init__(self, state: BotState, bot: Bot): self.state = state self.bot = bot self.schedule_service = ScheduleService() async def start(self): """Запуск слежки""" if self.state.watcher_work: return self.state.watcher_work = True self.state.watcher_task = asyncio.create_task(self._watcher_loop()) logger.info("Watcher запущен") async def stop(self): """Остановка слежки""" if not self.state.watcher_work: return self.state.watcher_work = False if self.state.watcher_task: self.state.watcher_task.cancel() try: await self.state.watcher_task except asyncio.CancelledError: pass logger.info("Watcher остановлен") @staticmethod def _next_delay() -> int: return Config.WATCHER_INTERVAL_SEC + randint( Config.WATCHER_RANDOM_DELAY_MIN, Config.WATCHER_RANDOM_DELAY_MAX, ) @staticmethod def _get_target_date_with_weekend_handling(days_ahead: int) -> datetime: """ Получить целевую дату с учетом выходных. Если целевая дата - воскресенье, переносится на понедельник. """ target = (datetime.now() + timedelta(days=days_ahead)).replace( hour=0, minute=0, second=0, microsecond=0 ) # weekday() returns 6 for Sunday if target.weekday() == 6: target += timedelta(days=1) return target async def _watcher_loop(self): """Основной цикл слежки за появлением PDF на Google Drive.""" while self.state.watcher_work: try: nothing_found = await self._check_all_groups() if nothing_found: delay = self._next_delay() logger.info(f"PDF/расписание не найдено, следующая проверка через {delay} с") await asyncio.sleep(delay) else: logger.info("Расписание найдено и отправлено, останавливаем watcher") self.state.watcher_work = False break except asyncio.CancelledError: break except Exception as e: logger.error(f"Ошибка в watcher_loop: {e}") await asyncio.sleep(60) async def _check_all_groups(self) -> bool: """ Возвращает True, если расписание ещё недоступно ни для одной группы. Возвращает False, если хотя бы одной группе отправили расписание. """ days_ahead = self.state.watcher_days_ahead target = self._get_target_date_with_weekend_handling(days_ahead) logger.info( f"Проверяем Google Drive на расписание за {target.strftime('%d.%m.%Y')} " f"(дней вперед: {days_ahead})" ) if not await self.schedule_service.is_published_for(days_ahead): return True found_any = False for group, chat_id in Config.GROUP_CHATS.items(): logger.info( f"Проверяем расписание для {group} на {target.strftime('%d.%m.%Y')}" ) if await self._check_group_schedule(group, chat_id, days_ahead): found_any = True return not found_any async def _check_group_schedule(self, group: str, chat_id: int, days_ahead: int) -> bool: target = self._get_target_date_with_weekend_handling(days_ahead) text, url, data_day, data_month = await self.schedule_service.get_schedule( group, target.day ) if not self.schedule_service.is_schedule_missing(text): msg = await self.bot.send_message( chat_id, ( f"🔔 Авто-расписание для {group} " f"на {data_day:02d}.{data_month:02d}\n\n{text}" ), parse_mode="HTML", ) try: await self.bot.pin_chat_message( chat_id, msg.message_id, disable_notification=False ) except Exception as e: logger.warning(f"Не удалось закрепить сообщение в {chat_id}: {e}") return True png, url, data_day, data_month = await self.schedule_service.get_pschedule( group, 0 ) if png: await self.bot.send_photo( chat_id, types.BufferedInputFile(png, filename=f"{group}.png"), caption=( f"🔔 АВАРИЙНЫЙ РЕЖИМ\n\n" f"Авто-расписание для {group} " f"на {data_day:02d}.{data_month:02d}" ), ) return True return False