telegram.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. """
  2. Telegram обёртка над SParser.
  3. Author: Milinuri Nirvalen
  4. Ver: 1.4 (sp v4.3)
  5. Команды бота для BotFather:
  6. sc - Уроки на сегодня
  7. updates - Изменения в расписании
  8. counter - Счётчик уроков/кабинетов
  9. set_class - Изменить класс
  10. help - Главное меню
  11. info - ⚙Информация о боте
  12. """
  13. from sp import SPMessages
  14. from sp import Schedule
  15. from sp import load_file
  16. import re
  17. from datetime import datetime
  18. from pathlib import Path
  19. from typing import Optional
  20. from aiogram import types
  21. from aiogram import Bot
  22. from aiogram import Dispatcher
  23. from aiogram import executor
  24. from aiogram.types import InlineKeyboardButton
  25. from aiogram.types import InlineKeyboardMarkup
  26. from aiogram.types import ReplyKeyboardRemove
  27. from loguru import logger
  28. API_TOKEN = load_file(Path("sp_data/token.json"),
  29. {"token": "YOUR TG API TOKEN"})["token"]
  30. bot = Bot(API_TOKEN)
  31. dp = Dispatcher(bot)
  32. logger.add("sp_data/telegram.log")
  33. days_str = ["понедельник", "вторник", "сред", "четверг", "пятниц", "суббот"]
  34. days_names = ["понедельник", "вторник", "среда",
  35. "четверг", "пятница", "суббота", "сегодня", "неделя"]
  36. # Определение клавиатур бота
  37. # ==========================
  38. to_home_markup = InlineKeyboardMarkup().add(
  39. InlineKeyboardButton(text="🏠Домой", callback_data="home"))
  40. week_markup = [{"home": "🏠", "week {cl}": "На неделю", "select_day {cl}":"▷"}]
  41. sc_markup = [{"home": "🏠", "sc {cl}": "На сегодня", "select_day {cl}": "▷"}]
  42. counter_markup = [{"home": "◁", "count": "Уроки", "count cl": "Уроки {cl}",
  43. "count abinets": "Классы",
  44. "count cabinets cl": "Классы {cl}"}]
  45. home_murkup = [{"other": "🔧", "updates last 0 {cl}": "🔔", "sc {cl}": "📚"}]
  46. other_markup = [{"home": "◁", "set_class": "Сменить класс"},
  47. {"count": "Счётчик",}]
  48. def markup_generator(sp: SPMessages, pattern: dict, cl: Optional[str] = None,
  49. exclude: Optional[str] = None,
  50. row_width: Optional[int] = 3) -> InlineKeyboardMarkup:
  51. """Собиарает inline-клавиатуру по шаблону.
  52. Args:
  53. sp (SPMessages): Описание
  54. cl (str, optional): Выбранный класс для передачи в callback_data
  55. pattern (dict): Шаблон для сборки клавиатуры
  56. exclude (str, optional): Ключ кнопки для исключения
  57. row_width (int, optional): Количество кнопок в одной строке
  58. Returns:
  59. InlineKeyboardMarkup: Собранная клавиатура
  60. """
  61. markup = InlineKeyboardMarkup(row_width)
  62. cl = cl if cl is not None else sp.user["class_let"]
  63. for group_row in pattern:
  64. row = []
  65. for callback_data, text in group_row.items():
  66. if exclude is not None and callback_data == exclude:
  67. continue
  68. callback_data = callback_data.replace("{cl}", cl)
  69. text = text.replace("{cl}", cl)
  70. row.append(InlineKeyboardButton(text= text, callback_data= callback_data))
  71. markup.row(*row)
  72. return markup
  73. def gen_updates_markup(update_index: int, updates: list) -> InlineKeyboardMarkup:
  74. """Собирает inline-клввиатуру для постраничного просмотра списка
  75. изменений расписания.
  76. Args:
  77. update_index (int): Номер текущей страницы обновлений
  78. updates (list): Список всех страниц
  79. Returns:
  80. InlineKeyboardMarkup: Готовая inline-клавиатура
  81. """
  82. markup = InlineKeyboardMarkup(row_width= 4)
  83. markup_pattern = {
  84. "home": "🏠",
  85. "updates back": "◁",
  86. "updates switch": f"{update_index+1}/{len(updates)}",
  87. "updates next": "▷",
  88. }
  89. for k, v in markup_pattern.items():
  90. k += f" {update_index}"
  91. markup.insert(InlineKeyboardButton(text= v, callback_data= k))
  92. return markup
  93. def gen_select_day_markup(cl: str) -> InlineKeyboardMarkup:
  94. """Собирает inline-клавиатуру для выбора дня недели.
  95. Args:
  96. cl (str): Уточнение для какого класса выбиратеся день недели
  97. Returns:
  98. InlineKeyboardMarkup: inline-клавиатура для выбора для недели
  99. """
  100. markup = InlineKeyboardMarkup()
  101. for i, x in enumerate(days_names):
  102. markup.insert(InlineKeyboardButton(text= x,
  103. callback_data= f"sc_day {cl} {i}"))
  104. return markup
  105. # Тексты сообщений
  106. # ================
  107. HOME_MESSAGE = """💡 Некоторые примеры:
  108. -- 7в
  109. -- уроки 6а на вторник среду
  110. -- расписание на завтра для 8б
  111. -- 312 на вторник
  112. -- химия 228
  113. 🏫 Вы можете использовать:
  114. -- Класс: для которого нужно расписание.
  115. -- Дни: понедельник-суббота, сегодня, завтра, неделя.
  116. -- Урок: Все его упоминания.
  117. -- Кабинет: Расписание от его лица
  118. 🌟 Порадок и форма не имеют значения!"""
  119. INFO_MESSAGE = """
  120. О боте:
  121. :: Автор: @milinuri
  122. :: Версия: 1.4 (🔶Testing)
  123. 👀 По всем вопросам к @milinuri"""
  124. SET_CLASS_MESSAGE = """
  125. ⚠️ Для полноценной работы бота ему нужно знать ваш класс.
  126. 💡 Вы всегда сможете изменить класс командой /set_class
  127. Пожалуйста, следуюшим сообщением введите класс..."""
  128. # Опеределение команд бота
  129. # ========================
  130. @dp.message_handler(commands= ["start"])
  131. async def start_command(message: types.Message):
  132. sp = SPMessages(str(message.chat.id), Schedule())
  133. logger.info(message.chat.id)
  134. await message.delete()
  135. if sp.user["set_class"]:
  136. markup = markup_generator(sp, home_murkup)
  137. await message.answer(text= HOME_MESSAGE, reply_markup= markup)
  138. else:
  139. await message.answer(text= SET_CLASS_MESSAGE)
  140. @dp.message_handler(commands= ["help"])
  141. async def help_command(message: types.Message):
  142. sp = SPMessages(str(message.chat.id), Schedule())
  143. logger.info(message.chat.id)
  144. markup = markup_generator(sp, home_murkup)
  145. await message.answer(text= HOME_MESSAGE, reply_markup= markup)
  146. @dp.message_handler(commands= ["info"])
  147. async def info_command(message: types.Message):
  148. logger.info(message.chat.id)
  149. sp = SPMessages(str(message.chat.id), Schedule())
  150. await message.answer(text= sp.send_status()+INFO_MESSAGE,
  151. reply_markup= to_home_markup)
  152. @dp.message_handler(commands= ["updates"])
  153. async def updates_command(message: types.Message):
  154. sp = SPMessages(str(message.chat.id), Schedule())
  155. logger.info(message.chat.id)
  156. updates = sp.sc.get_updates()
  157. markup = gen_updates_markup(len(updates)-1, updates)
  158. await message.answer(text= sp.send_updates(updates[-1]),
  159. reply_markup= markup)
  160. @dp.message_handler(commands= ["counter"])
  161. async def lessons_command(message: types.Message):
  162. sp = SPMessages(str(message.chat.id), Schedule())
  163. logger.info(message.chat.id)
  164. markup = markup_generator(sp, counter_markup,
  165. exclude= "count", row_width= 4)
  166. await message.answer(text= sp.count_lessons(),
  167. reply_markup= markup)
  168. @dp.message_handler(commands= ["set_class"])
  169. async def set_class_command(message: types.Message):
  170. sp = SPMessages(str(message.chat.id), Schedule())
  171. logger.info(message.chat.id)
  172. sp.user["set_class"] = False
  173. sp.save_user()
  174. await message.answer(text= SET_CLASS_MESSAGE)
  175. @dp.message_handler(commands= ["sc"])
  176. async def sc_command(message: types.Message):
  177. sp = SPMessages(str(message.chat.id), Schedule())
  178. logger.info(message.chat.id)
  179. if sp.user["set_class"]:
  180. await message.answer(text= sp.send_today_lessons(),
  181. reply_markup= markup_generator(sp, week_markup))
  182. else:
  183. await message.answer(text= SET_CLASS_MESSAGE)
  184. # Главный обработчик сообщений
  185. # ============================
  186. @dp.message_handler()
  187. async def main_handler(message: types.Message):
  188. uid = str(message.chat.id)
  189. sc = Schedule()
  190. sp = SPMessages(uid, sc)
  191. logger.info("{} {}", uid, message.text)
  192. if sp.user["set_class"]:
  193. args = message.text.lower().strip().split()
  194. weekday = datetime.today().weekday()
  195. days = []
  196. cl = None
  197. lesson = None
  198. cabinet = None
  199. # Обработка входящего сообщения
  200. # -----------------------------
  201. for arg in args:
  202. if arg == "сегодня":
  203. days.append(weekday)
  204. elif arg == "завтра":
  205. days.append(datetime.today().weekday()+1)
  206. elif arg.startswith("недел"):
  207. days = [0, 1, 2, 3, 4, 5]
  208. elif arg in sc.lessons:
  209. cl = arg
  210. elif arg in sc.l_index:
  211. lesson = arg
  212. elif arg in sc.c_index:
  213. cabinet = arg
  214. else:
  215. # Если начало слова совпадает: пятниц... -а, -у, -ы...
  216. days += [i for i, k in enumerate(days_str) if arg.startswith(k)]
  217. # Отправка сообщения
  218. # ------------------
  219. logger.info(f"answer С:{cabinet} L:{lesson} D:{days} CL:{cl}")
  220. if cabinet:
  221. res = sp.search_cabinet(cabinet, lesson, days, cl)
  222. await message.answer(text= res, reply_markup= to_home_markup)
  223. elif lesson:
  224. res = sp.search_lesson(lesson, days, cl)
  225. await message.answer(text= res, reply_markup= to_home_markup)
  226. elif cl or days:
  227. markup = markup_generator(sp, week_markup, cl= cl)
  228. if days:
  229. await message.answer(text= sp.send_lessons(days= days, cl= cl),
  230. reply_markup= markup)
  231. else:
  232. await message.answer(text= sp.send_today_lessons(cl= cl),
  233. reply_markup= markup)
  234. else:
  235. await message.answer(text= "👀 Кажется, это пустой запрос...?")
  236. # Устаеновка класса по умолчанию
  237. # ==============================
  238. else:
  239. text = message.text.lower()
  240. await message.answer(text= sp.set_class(text))
  241. if text in sc.lessons:
  242. logger.info(f"Set class {text} ")
  243. markup = markup_generator(sp, home_murkup)
  244. await message.answer(text= HOME_MESSAGE, reply_markup= markup)
  245. # Обработчик inline-кнопок
  246. # ========================
  247. @dp.callback_query_handler()
  248. async def callback_handler(callback: types.CallbackQuery):
  249. header, *args = callback.data.split()
  250. uid = str(callback.message.chat.id)
  251. sp = SPMessages(uid, Schedule())
  252. logger.info("{}: {} {}", uid, header, args)
  253. # Вызов справки
  254. if header == "home":
  255. markup = markup_generator(sp, home_murkup)
  256. await callback.message.edit_text(text=HOME_MESSAGE, reply_markup= markup)
  257. # Вызов счётчика
  258. if header == "count":
  259. cabinets = True if "cabinets" in args else False
  260. cl = sp.user["class_let"] if "cl" in args else None
  261. text = sp.count_lessons(cabinets= cabinets, cl= cl)
  262. markup = markup_generator(sp, counter_markup, exclude= "count",
  263. row_width= 4)
  264. await callback.message.edit_text(text= text, reply_markup=markup)
  265. # Расписание на сегодня
  266. if header == "sc":
  267. text = sp.send_today_lessons(cl= args[0])
  268. markup = markup_generator(sp, week_markup, cl= args[0])
  269. await callback.message.edit_text(text= text, reply_markup= markup)
  270. # Расписание на неделю
  271. if header == "week":
  272. text = sp.send_lessons(days= [0, 1, 2, 3, 4, 5], cl= args[0])
  273. markup = markup_generator(sp, sc_markup, cl= args[0])
  274. await callback.message.edit_text(text= text, reply_markup= markup)
  275. # Клавиатура для выбора дня
  276. if header == "select_day":
  277. markup = gen_select_day_markup(args[0])
  278. await callback.message.edit_text(text= f"🏫 Для {args[0]}: ...",
  279. reply_markup= markup)
  280. # Расписани на определённый день
  281. if header == "sc_day":
  282. cl = args[0]
  283. day = int(args[1])
  284. if day == 6:
  285. text = sp.send_today_lessons(cl= cl)
  286. elif day == 7:
  287. text = sp.send_lessons(days=[0, 1, 2, 3, 4, 5], cl= cl)
  288. else:
  289. text = sp.send_lessons(days= [day], cl=cl)
  290. markup = markup_generator(sp, sc_markup, cl= cl)
  291. await callback.message.edit_text(text= text, reply_markup= markup)
  292. # Вызов меню обновлений
  293. if header == "updates":
  294. i = int(args[1])
  295. text = "🔔 Изменения в расписании:\n"
  296. updates = sp.sc.updates
  297. if args[0] == "next":
  298. i = (i+1) % len(updates)
  299. elif args[0] == "back":
  300. i = (i-1) % len(updates)
  301. elif args[0] == "last":
  302. i = len(updates)-1
  303. text += sp.send_update(updates[i])
  304. markup = gen_updates_markup(i, updates)
  305. await callback.message.edit_text(text= text, reply_markup= markup)
  306. # Вызоы меню инстрментов
  307. if header == "other":
  308. text = sp.send_status() + INFO_MESSAGE
  309. markup = markup_generator(sp, other_markup)
  310. await callback.message.edit_text(text= text, reply_markup= markup)
  311. # Смена класса пользователя
  312. if header == "set_class":
  313. sp.user["set_class"] = False
  314. sp.save_user()
  315. await callback.message.edit_text(text= SET_CLASS_MESSAGE)
  316. await callback.answer()
  317. # Запуск бота
  318. # ===========
  319. if __name__ == "__main__":
  320. executor.start_polling(dp)