146 lines
5.5 KiB
Python
146 lines
5.5 KiB
Python
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
|