check_updates.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. """
  2. Скрипт для автоматической проверки расписания.
  3. Работает в паре с Teleram ботом.
  4. - Проверяет пользователей.
  5. - Обновляет расписание.
  6. - Отправляет изменения в расписании пользователям.
  7. - Рассылает расписание на сегодня/завтра пользователям.
  8. - Удаляет пользователей.
  9. Author: Milinuri Nirvalen
  10. Ver: 0.10.1 (sp v5.7+2b, telegram v2.0)
  11. """
  12. from datetime import datetime
  13. from pathlib import Path
  14. from os import getenv
  15. import asyncio
  16. from aiogram import Bot
  17. from aiogram.exceptions import TelegramForbiddenError
  18. from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
  19. from loguru import logger
  20. from dotenv import load_dotenv
  21. from sp.intents import Intent
  22. from sp.messages import SPMessages, send_update, users_path
  23. from sp.utils import load_file, save_file
  24. load_dotenv()
  25. TELEGRAM_TOKEN = getenv("TELEGRAM_TOKEN")
  26. bot = Bot(TELEGRAM_TOKEN)
  27. logger.add("sp_data/updates.log")
  28. _TIMETAG_PATH = Path("sp_data/last_update")
  29. # Если данные мигрировали вследствии
  30. CHAT_MIGRATE_MESSAGE = """⚠️ У вашего чата сменился ID.
  31. Настройки чата были перемещены."""
  32. # Функкии для сбора клавиатур
  33. # ===========================
  34. def get_week_keyboard(cl: str) -> InlineKeyboardMarkup:
  35. return InlineKeyboardMarkup(inline_keyboard=[[
  36. InlineKeyboardButton(text="🏠Домой", callback_data="home"),
  37. InlineKeyboardButton(text="На неделю", callback_data=f"sc:{cl}:week"),
  38. InlineKeyboardButton(text="▷", callback_data=f"select_day:{cl}"),
  39. ]])
  40. def get_updates_keyboard(cl: str) -> InlineKeyboardMarkup:
  41. return InlineKeyboardMarkup(inline_keyboard=[[
  42. InlineKeyboardButton(text="◁", callback_data="home"),
  43. InlineKeyboardButton(text="Изменения", callback_data=f"updates:last:0:{cl}"),
  44. InlineKeyboardButton(text="Уроки", callback_data=f"sc:{cl}:today")
  45. ]])
  46. # Функции для обработки списка пользователей
  47. # ==========================================
  48. async def process_update(bot, hour: int, sp: SPMessages) -> None:
  49. """Проверяет обновления для одного пользователя (или чата).
  50. Отправляет расписани на сегодня/завтра в указанный час или
  51. список измнений в расписании, при наличии.
  52. Args:
  53. bot (bot): Экземпляр aiogram бота.
  54. hour (int): Текущий час.
  55. uid (str): ID чата для проверки.
  56. sp (SPMessages): Данные пользователя.
  57. """
  58. # Рассылка расписания в указанные часы
  59. if str(hour) in sp.user["hours"]:
  60. await bot.send_message(sp.uid,
  61. text=sp.send_today_lessons(Intent()),
  62. reply_markup=get_week_keyboard(sp.user["class_let"])
  63. )
  64. # Отправляем уведомления об обновлениях
  65. updates = sp.get_lessons_updates()
  66. if updates is not None:
  67. message = "🎉 У вас изменилось расписание!"
  68. message += f"\n{send_update(updates, cl=sp.user['class_let'])}"
  69. await bot.send_message(sp.uid, text=message,
  70. reply_markup=get_updates_keyboard(sp.user["class_let"]
  71. ))
  72. async def remove_users(remove_ids: list[str]):
  73. """Удаляет недействительные ID пользователей (чата).
  74. Если пользователь заблокировал бота.
  75. Если бота исключили из чата.
  76. Если пользователь удалил аккаунт.
  77. Args:
  78. remove_ids (list[str]) Список ID для удаления.
  79. """
  80. logger.info("Start remove users...")
  81. users = load_file(Path(users_path), {})
  82. for x in remove_ids:
  83. logger.info("Remove {}", x)
  84. del users[x]
  85. save_file(Path(users_path), users)
  86. def set_timetag(path: Path, timestamp: int) -> None:
  87. """Оставляет временную метку последней проверки обнолвения.
  88. После успешной работы скрипта записывает в файл временную метку.
  89. Метка может использватьна для проверки работаспособности
  90. скрипта обновлений.
  91. Args:
  92. path (Path): Путь к файлу временной метки.
  93. timestamp (int): Временная UNIXtime метка.
  94. """
  95. if not path.exists():
  96. path.parent.mkdir(parents=True, exist_ok=True)
  97. with open(path, "w") as f:
  98. f.write(str(timestamp))
  99. # Главная функция скрипта
  100. # =======================
  101. async def main() -> None:
  102. now = datetime.now()
  103. users = load_file(Path(users_path), {})
  104. remove_ids = []
  105. logger.info("Start of the update process...")
  106. for k, v in list(users.items()):
  107. # Если у пользователя отключены уведомления или не указан
  108. # класс по умолчанию -> пропускаем.
  109. if not v.get("notifications") or not v.get("class_let"):
  110. continue
  111. # Получаем экземпляр генератора сообщения пользователя
  112. # TODO: данные пользователя вновь загружаются из файла на
  113. # каждой итерации
  114. sp = SPMessages(k, v)
  115. try:
  116. logger.debug("{} {}", k, v)
  117. await process_update(bot, now.hour, sp)
  118. # Если что-то произошло с пользователем:
  119. # Заблокировал бота, исключил из чата, исчез сам ->
  120. # Удаляем пользователя (чат) из списка чатов.
  121. except TelegramForbiddenError:
  122. remove_ids.append(k)
  123. # Ловим все прочие исключения и отображаем их на экран
  124. except Exception as e:
  125. logger.exception(e)
  126. # Если данные изменились - записываем изменения в файл
  127. if remove_ids:
  128. await remove_users(remove_ids)
  129. # Осталяем временную метку успешного обновления
  130. set_timetag(_TIMETAG_PATH, int(now.timestamp()))
  131. # Запуск скрипта обновлений
  132. # =========================
  133. if __name__ == '__main__':
  134. asyncio.run(main())