It's version 0.8.3 bug fix ТЕПЕРЬ УЖ точно восстановлена система слежения расписания

This commit is contained in:
Niken
2026-05-20 22:35:42 +03:00
parent bbd9c839d5
commit c0edc77a11
8 changed files with 82 additions and 27 deletions
+2 -2
View File
@@ -116,8 +116,8 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
save_message(msg.chat.id, msg.message_id) save_message(msg.chat.id, msg.message_id)
except Exception as e: except Exception as e:
logger.exception(f"Ошибка при обработке uid={uid}: {e}") logger.exception(f"Ошибка при обработке uid={uid}: {e}")
logger.info("Завершён цикл periodic_task, спим 6 часов") logger.info("Завершён цикл periodic_task, спим 24 часов")
await asyncio.sleep(21600) # каждые 6 часов await asyncio.sleep(86400) # каждые 24 часов
asyncio.create_task(periodic_task()) asyncio.create_task(periodic_task())
return 0 return 0
+20 -3
View File
@@ -113,16 +113,33 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
async def power_control(message: types.Message): async def power_control(message: types.Message):
args = message.text.split() args = message.text.split()
if len(args) < 2: if len(args) < 2:
days = state.watcher_days_ahead
status = "включена" if state.watcher_work else "выключена" status = "включена" if state.watcher_work else "выключена"
await message.answer(f"⏱️ Слежка расписания: {status}") await message.answer(f"⏱️ Слежка расписания: {status} (на {days} дн.)")
return return
command = args[1].lower() command = args[1].lower()
watcher_service = WatcherService(state, bot) watcher_service = WatcherService(state, bot)
if command == "on" and not state.watcher_work: if command == "on":
# Проверяем, есть ли параметр количества дней
days = 1
if len(args) > 2:
try:
days = int(args[2])
if days < 1:
await message.answer("❌ Количество дней должно быть >= 1")
return
except ValueError:
await message.answer("❌ Неверный формат дней. Используйте: /power on 3")
return
state.watcher_days_ahead = days
if not state.watcher_work:
await watcher_service.start() await watcher_service.start()
await message.answer("✅ Слежка расписания включена") await message.answer(f"✅ Слежка расписания включена (на {days} дн.)")
else:
await message.answer(f"✅ Количество дней изменено на {days} дн.")
elif command == "off" and state.watcher_work: elif command == "off" and state.watcher_work:
await watcher_service.stop() await watcher_service.stop()
await message.answer("❌ Слежка расписания выключена") await message.answer("❌ Слежка расписания выключена")
+1
View File
@@ -11,6 +11,7 @@ class BotState:
last_pinned: Dict[str, int] = None last_pinned: Dict[str, int] = None
watcher_work: bool = False watcher_work: bool = False
watcher_task: Optional[Task] = None watcher_task: Optional[Task] = None
watcher_days_ahead: int = 1
file_id_cache: Dict[str, str] = None file_id_cache: Dict[str, str] = None
last_day: Dict[str, int] = None last_day: Dict[str, int] = None
last_clip_hash: Dict[str, str] = None last_clip_hash: Dict[str, str] = None
+27 -5
View File
@@ -36,13 +36,16 @@ class ScheduleService:
target = target.replace(day=int(day_offset)) target = target.replace(day=int(day_offset))
return target.replace(hour=0, minute=0, second=0, microsecond=0) return target.replace(hour=0, minute=0, second=0, microsecond=0)
def _get_next_day(self, day_offeset: int = 0) -> datetime:
return datetime.now() + timedelta(days=day_offeset) def _next_target_date(self, day_offset: int = 0) -> datetime:
return (datetime.now() + timedelta(days=day_offset)).replace(hour=0, minute=0, second=0, microsecond=0)
async def _load_pdf_for_date( async def _load_pdf_for_date(
self, day_offset: int = 1 self, day_offset: int = 0
) -> Tuple[Optional[bytes], Optional[str], int, int]: ) -> Tuple[Optional[bytes], Optional[str], int, int]:
target = self._get_next_day(day_offset) target = self._resolve_target_date(day_offset)
day, month = target.day, target.month day, month = target.day, target.month
drive_file = await self.drive.find_for_date(target) drive_file = await self.drive.find_for_date(target)
@@ -57,6 +60,25 @@ class ScheduleService:
url = f"https://drive.google.com/file/d/{drive_file.file_id}/view" url = f"https://drive.google.com/file/d/{drive_file.file_id}/view"
return self._pdf_cache[drive_file.file_id], url, day, month return self._pdf_cache[drive_file.file_id], url, day, month
async def _load_pdf_for_watcher(
self, day_offset: int = 1
) -> Tuple[Optional[bytes], Optional[str], int, int]:
target = self._next_target_date(day_offset)
day, month = target.day, target.month
drive_file = await self.drive.find_for_date(target)
if not drive_file:
return None, None, day, month
if drive_file.file_id not in self._pdf_cache:
self._pdf_cache[drive_file.file_id] = await self.drive.download_pdf(
drive_file.file_id
)
url = f"https://drive.google.com/file/d/{drive_file.file_id}/view"
return self._pdf_cache[drive_file.file_id], url, day, month
@staticmethod @staticmethod
def exact_group_regex(group: str) -> re.Pattern: def exact_group_regex(group: str) -> re.Pattern:
pattern = rf"(^|{BOUNDARY}){re.escape(group)}({BOUNDARY}|$)" pattern = rf"(^|{BOUNDARY}){re.escape(group)}({BOUNDARY}|$)"
@@ -108,7 +130,7 @@ class ScheduleService:
return f"📅 Расписание для {day} числа:\n<pre>{body}</pre>" return f"📅 Расписание для {day} числа:\n<pre>{body}</pre>"
async def is_published_for(self, day_offset: int = 0) -> bool: async def is_published_for(self, day_offset: int = 0) -> bool:
target = self._resolve_target_date(day_offset) target = self._next_target_date(day_offset)
return await self.drive.find_for_date(target) is not None return await self.drive.find_for_date(target) is not None
async def get_schedule( async def get_schedule(
+24 -7
View File
@@ -1,8 +1,8 @@
import asyncio import asyncio
from random import randint from random import randint
from datetime import datetime, timedelta
from aiogram import Bot, types from aiogram import Bot, types
from config import Config from config import Config
from logging import getLogger from logging import getLogger
from models.state import BotState from models.state import BotState
@@ -47,6 +47,20 @@ class WatcherService:
Config.WATCHER_RANDOM_DELAY_MAX, 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): async def _watcher_loop(self):
"""Основной цикл слежки за появлением PDF на Google Drive.""" """Основной цикл слежки за появлением PDF на Google Drive."""
while self.state.watcher_work: while self.state.watcher_work:
@@ -71,12 +85,14 @@ class WatcherService:
Возвращает True, если расписание ещё недоступно ни для одной группы. Возвращает True, если расписание ещё недоступно ни для одной группы.
Возвращает False, если хотя бы одной группе отправили расписание. Возвращает False, если хотя бы одной группе отправили расписание.
""" """
target = self.schedule_service._resolve_target_date(0) days_ahead = self.state.watcher_days_ahead
target = self._get_target_date_with_weekend_handling(days_ahead)
logger.info( logger.info(
f"Проверяем Google Drive на расписание за {target.strftime('%d.%m.%Y')}" f"Проверяем Google Drive на расписание за {target.strftime('%d.%m.%Y')} "
f"(дней вперед: {days_ahead})"
) )
if not await self.schedule_service.is_published_for(0): if not await self.schedule_service.is_published_for(days_ahead):
return True return True
found_any = False found_any = False
@@ -84,14 +100,15 @@ class WatcherService:
logger.info( logger.info(
f"Проверяем расписание для {group} на {target.strftime('%d.%m.%Y')}" f"Проверяем расписание для {group} на {target.strftime('%d.%m.%Y')}"
) )
if await self._check_group_schedule(group, chat_id): if await self._check_group_schedule(group, chat_id, days_ahead):
found_any = True found_any = True
return not found_any return not found_any
async def _check_group_schedule(self, group: str, chat_id: int) -> bool: 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( text, url, data_day, data_month = await self.schedule_service.get_schedule(
group, 0 group, target.day
) )
if not self.schedule_service.is_schedule_missing(text): if not self.schedule_service.is_schedule_missing(text):
msg = await self.bot.send_message( msg = await self.bot.send_message(
-2
View File
@@ -36,6 +36,4 @@ if __name__ == "__main__":
def get_db(): def get_db():
if Config.DISABLE_STORAGE:
raise RuntimeError("Хранение отключено (DISABLE_STORAGE=1)")
return sqlite3.connect(DIR) return sqlite3.connect(DIR)
+6
View File
@@ -4,6 +4,8 @@ from config import Config
def save_message(chat_id: int, message_id: int): def save_message(chat_id: int, message_id: int):
if Config.DISABLE_STORAGE: if Config.DISABLE_STORAGE:
return return
if True:
return
from .DB import get_db from .DB import get_db
db = get_db() db = get_db()
@@ -17,6 +19,8 @@ def save_message(chat_id: int, message_id: int):
def load_messages(): def load_messages():
if Config.DISABLE_STORAGE: if Config.DISABLE_STORAGE:
return [] return []
if True:
return []
from .DB import get_db from .DB import get_db
db = get_db() db = get_db()
@@ -31,6 +35,8 @@ def load_messages():
def clear_messages(): def clear_messages():
if Config.DISABLE_STORAGE: if Config.DISABLE_STORAGE:
return return
if True:
return
from .DB import get_db from .DB import get_db
db = get_db() db = get_db()
-6
View File
@@ -4,8 +4,6 @@ _DEFAULT_GROUP = "30тс"
def save_user(user_id: int, group: str = _DEFAULT_GROUP): def save_user(user_id: int, group: str = _DEFAULT_GROUP):
if Config.DISABLE_STORAGE:
return
from .DB import get_db from .DB import get_db
db = get_db() db = get_db()
@@ -17,8 +15,6 @@ def save_user(user_id: int, group: str = _DEFAULT_GROUP):
def set_group(user_id: int, group: str = _DEFAULT_GROUP): def set_group(user_id: int, group: str = _DEFAULT_GROUP):
if Config.DISABLE_STORAGE:
return
from .DB import get_db from .DB import get_db
db = get_db() db = get_db()
@@ -37,8 +33,6 @@ def set_group(user_id: int, group: str = _DEFAULT_GROUP):
def get_group(user_id: int, default: str = _DEFAULT_GROUP) -> str: def get_group(user_id: int, default: str = _DEFAULT_GROUP) -> str:
if Config.DISABLE_STORAGE:
return default
from .DB import get_db from .DB import get_db
db = get_db() db = get_db()