migrate bot to schedule API

This commit is contained in:
Server 2026-08-30 11:43:38 +00:00
parent 41a5edde19
commit c75f14e588
11 changed files with 498 additions and 493 deletions

View file

@ -1,138 +1,74 @@
from telebot.types import (InlineQueryResultArticle, InputTextMessageContent, InlineQuery)
from models.bot import bot, Inlines
from models.user import validate
from models.schedule import generateDaySchedule, dayStrings
from telebot.types import InlineQuery
from models import logger
from models.bot import Inlines, bot
from models.schedule import dayStrings, generateDaySchedule
from models.user import validate
@bot.inline_handler(lambda query: True)
def default_query(query: InlineQuery):
user = validate(query)
inline = Inlines(query.id)
if not user:
logger.warn('Bot', 'Inlinue from unknown user invoked')
if not user.group:
inline.add(
'Расписание недоступно ❌',
'Запусти бота, чтобы использовать эту функцию',
'Не удалось получить расписание. Пользователь не запустил бота ❌ \n\nhttps://t.me/zatups_bot'
"Расписание недоступно ❌",
"Запусти бота, чтобы выбрать группу",
"Не удалось получить расписание. Сначала запусти бота: https://t.me/zatups_bot",
)
else:
if 'today':
s = user.schedule()
weektype = 'четная' if s.weekNumber%2 == 0 else 'нечетная'
description = dayStrings[s.weekDay]
output = generateDaySchedule(s, isToday = True) if s.weekDay < 5 else f'<b>{description}</b>\nСегодня пар нету 🥰'
schedule = user.schedule()
if not schedule:
inline.add(
'Расписание на сегодня 🔥',
f'{description}, {weektype.lower()} неделя',
output
"Расписание недоступно ❌",
"Сервер расписания сейчас недоступен",
"Не удалось получить расписание. Попробуй ещё раз позже.",
)
if 'tomorrow':
s = user.schedule()
output = False
description = dayStrings[(s.weekDay+1)%len(dayStrings)]
if s.weekDay in [4, 5]:
output = f'<b>{description}</b>\nЗавтра пар нету 🥰'
elif s.weekDay == 6:
s.weekDay = 0
s.weekNumber += 1
else:
s.weekDay += 1
if not output: output = generateDaySchedule(s)
weektype = 'четная' if s.weekNumber%2 == 0 else 'нечетная'
week_type = "четная" if schedule.weekNumber % 2 == 0 else "нечетная"
description = dayStrings[schedule.weekDay]
inline.add(
'Расписание на завтра 🍀',
f'{description}, {weektype.lower()} неделя',
output
"Расписание на сегодня 🔥",
f"{description}, {week_type} неделя",
generateDaySchedule(schedule, isToday=True),
)
if 'week':
s = user.schedule()
description = f'Сейчас {"четная" if s.weekNumber%2==0 else "нечетная"} неделя'
output = f'<i>Расписание на </i><b>эту</b><i> {"четную" if s.weekNumber%2==0 else "нечетную"} неделю</i>\n'
if s.weekDay == 6:
s.weekDay = 0
s.weekNumber += 1
description = f'Следующая {"четная" if s.weekNumber%2==0 else "нечетная"} неделя'
output = f'<i>Расписание на </i><b>следующую</b><i> {"четную" if s.weekNumber%2==0 else "нечетную"} неделю</i>\n'
for index, _ in enumerate(s.days):
s.weekDay = index
output += generateDaySchedule(s, False) +'\n\n'
tomorrow = user.schedule()
if tomorrow:
if tomorrow.weekDay == 6:
tomorrow.weekDay = 0
tomorrow.weekNumber += 1
else:
tomorrow.weekDay += 1
tomorrow_description = dayStrings[tomorrow.weekDay]
tomorrow_week_type = (
"четная" if tomorrow.weekNumber % 2 == 0 else "нечетная"
)
inline.add(
'Расписание на неделю 📅',
description,
output
"Расписание на завтра 🍀",
f"{tomorrow_description}, {tomorrow_week_type} неделя",
generateDaySchedule(tomorrow),
)
logger.log('Bot', f'Inline from {user.id}')
week = user.schedule()
if week:
title = "эту"
if week.weekDay == 6:
week.weekNumber += 1
title = "следующую"
week_type = "четную" if week.weekNumber % 2 == 0 else "нечетную"
output = f"<i>Расписание на </i><b>{title}</b><i> {week_type} неделю</i>\n"
for weekday in range(len(week.days)):
week.weekDay = weekday
output += generateDaySchedule(week, False) + "\n\n"
inline.add(
"Расписание на неделю 📅",
f"{week_type.capitalize()} неделя",
output,
)
logger.log("Bot", f"Inline from {user.id}")
try:
return inline.send()
except Exception as e:
logger.error('Bot', f'Inline.send() -> {e}')
except Exception as error:
logger.error("Bot", f"Inline.send() -> {error}")

View file

@ -30,21 +30,23 @@ def forceReturnToDefaultState(query: CallbackQuery):
@bot.message_handler(content_types = ['text'], func = lambda m: validate(m).state == 'default')
def defaultState(message: Message):
u = validate(message)
s = u.schedule()
# print(u.id, message.text)
if message.text[0] == '/': return ContinueHandling() # type: ignore
if message.text == f'{u.group} 🔄':
u.setState('savedGroups')
return send(u.id, f'<b>Сохраненные группы</b>\nЗдесь список групп, которые ранее были использованы, чтобы быстро между ними переключаться. \n\n<i>Активная группа: {u.group}</i>', Markup.saved_groups(u))
schedule_actions = {'Сегодня 🔥', 'Завтра 🍀', 'На неделю 📅', 'Другие дни 📁'}
s = u.schedule() if message.text in schedule_actions else None
if message.text in schedule_actions and not s:
return send(
u.id,
'Не удалось получить расписание. Попробуй ещё раз позже или перезапусти бота через /start.',
reply_markup=Markup.default(u),
)
match message.text:
case 'Сегодня 🔥':
if s.weekDay > 4:
return send(u.id, f'<b>{dayStrings[s.weekDay]}</b>\nСегодня пар нет 🥰')
return send(u.id, generateDaySchedule(s, isToday = True), reply_markup = Markup.default(u))
case 'Завтра 🍀':
@ -73,7 +75,7 @@ def defaultState(message: Message):
case 'Другие дни 📁':
u.setState('otherDays')
return send(u.id, f'<b>Сегодня {dayStrings[s.weekDay].lower()}, {"четная" if s.weekNumber%2==0 else "нечентная"} неделя</b>\n\nВыбери нужный день/пункт', reply_markup = Markup.other())
return send(u.id, f'<b>Сегодня {dayStrings[s.weekDay].lower()}, {"четная" if s.weekNumber%2==0 else "нечетная"} неделя</b>\n\nВыбери нужный день/пункт', reply_markup = Markup.other())
case 'Уведомления ⏰':
u.setState('notifications')
@ -170,4 +172,3 @@ def sendHelp(message: Message):
[bot.forward_message(adminID, message.from_user.id, message.id) for adminID in ADMINS] # type: ignore
send(message, '<b>Сообщение переслано разработчику 🔧</b>\nСпасибо за обращение', Markup.default(u))

View file

@ -1,97 +1,114 @@
import json
from telebot.types import Message
from telebot.types import Message, ReplyKeyboardRemove
from telebot.util import quick_markup
from models import logger
from models.user import User, validate, getAllUsers
from models.bot import (Markup_v1 as Markup, bot, send)
from models.schedule import generateDaySchedule
from models.bot import Markup_v1 as Markup, bot, send
from models.schedule import ScheduleAPIError, find_main_groups, group_exists
from models.user import getAllUsers, validate
@bot.message_handler(commands = ['start'])
@bot.message_handler(commands=["start"])
def start(message: Message):
u = validate(message)
if u and 'migrate' in message.text: # type: ignore
u.setState('default')
logger.log('Bot', f'{message.from_user.id} migrated') # type: ignore
send(message, 'Добро пожаловать снова 💞', reply_markup = Markup.default(u))
send(message, f'''<b>Что нового</b>
<blockquote>1. Теперь доступны все факультеты, курсы и группы
2. Затупсика можно упомянуть в любом чате, даже в личке и группе, и он отправит актуальное расписание!
<i>Для этого напиши <code>@zatups_bot</code> в любом чате</i>
3. Теперь расписания отображаются корректно всегда, даже с подгруппами</blockquote>
user = validate(message)
if user.group and "migrate" in (message.text or ""):
user.setState("default")
logger.log("Bot", f"{message.from_user.id} migrated")
return send(message, "Добро пожаловать снова 💞", reply_markup=Markup.default(user))
<i>Чтобы воспользоваться функцией отправки расписания в любом чате, напиши <code>@zatups_bot</code> в любом чате, выбери бота из списка, а после нажми на нужный день - сегодня, завтра, или на всю неделю.\nКстати - в следующий раз достаточно просто написать @ и телеграм сам выдаст затупсика в списке!</i>''')
return
logger.log('Bot', f'[{u.id}] /start detected, Total users: {len(list(getAllUsers()))+1}')
was_registered = bool(user.group)
logger.log("Bot", f"[{user.id}] /start detected, Total users: {len(list(getAllUsers()))}")
json.dump(
{
'state': 'selectTopic',
'topic': '',
'course': '',
'group': '',
'saved_groups': u.saved_groups if u else [],
'notifyEvening': False,
'notifyBeforeLesson': False,
"state": "selectGroup",
"group": None,
"saved_groups": user.saved_groups,
"notifications": [],
"valid": True,
"user": user.user,
"usage": user.usage,
"flags": user.flags,
},
open(f'./users/{message.from_user.id}.json', 'w', encoding='utf-8'), # type: ignore
ensure_ascii = False,
indent = 4
open(f"./users/{message.from_user.id}.json", "w", encoding="utf-8"),
ensure_ascii=False,
indent=4,
)
send(message, '''
<b>Бот на стадии разработки!</b>
- <i>Доступны не все функции, группы</i>
- <i>Возможны баги, фризы, затупы итд итп</i>
- <i>Бот изначально не планировался для публичного использования</i>
Просто имей это ввиду😥
''')
#EDIT
send(message, 'Выбери свой факультет' if not u else '<i>Вы начали процедуру перезапуска бота. Все настройки и данные удалены, включая уведомления</i>\nВыбери новый факультет', reply_markup = Markup.selectTopic())
prefix = (
"<i>Вы начали процедуру перезапуска бота. Уведомления отключены.</i>\n\n"
if was_registered
else ""
)
send(
message,
prefix + "Напиши название своей группы. Например: <code>АТ-501</code>",
reply_markup=ReplyKeyboardRemove(),
)
@bot.message_handler(content_types = ['text'], func = lambda m: ' нету ⚠' in m.text)
@bot.message_handler(content_types=["text"], func=lambda message: "нету ⚠" in message.text)
def notFoundHandler(message: Message):
bot.reply_to(message, '''
bot.reply_to(
message,
"""
<b>Не расстраивайся!</b>
<i>Напиши разработчику, чтобы он добавил твой факультет/курс/группу</i>
<blockquote>
К сожалению, для поддержки работы бота нужно много мощностей и материальные вложения.
Бюджет, выделенный на разработку бота - <i>2 банки пива</i>, поэтому в целях экономии в боте пока доступны не все группы
</blockquote>''', reply_markup = quick_markup({'Написать разработчику 💞': {'url': 'https://t.me/m/Gg00FvJmZDM6'}, 'Перезапустить бота 🤖': {'url': 'https://t.me/zatups_bot?start=r'}}, row_width = 1))
<i>Напиши разработчику, если твоей группы нет в расписании.</i>
""",
reply_markup=quick_markup(
{
"Написать разработчику 💞": {"url": "https://t.me/m/Gg00FvJmZDM6"},
"Перезапустить бота 🤖": {"url": "https://t.me/zatups_bot?start=r"},
},
row_width=1,
),
)
@bot.message_handler(content_types = ['text'], func = lambda m: validate(m).state == 'selectTopic')
def selectTopicState(message: Message):
u = validate(message)
u.topic = message.text
u.setState('selectCourse')
send(message, 'Выбери свой курс', reply_markup = Markup.selectCourse(u.topic))
@bot.message_handler(content_types = ['text'], func = lambda m: validate(m).state == 'selectCourse')
def selectCourseState(message: Message):
u = validate(message)
u.course = message.text
u.setState('selectGroup')
send(message, 'Выбери свою группу', reply_markup = Markup.selectGroup(u.topic, u.course))
@bot.message_handler(content_types = ['text'], func = lambda m: validate(m).state == 'selectGroup')
def selectGroupState(message: Message):
u = validate(message)
u.group = message.text
u.setState('default')
send(message, '''
def _finish_registration(message: Message, group_name: str) -> None:
user = validate(message)
user.group = group_name
if group_name not in user.saved_groups:
user.saved_groups.append(group_name)
user.valid = True
user.setState("default")
send(
message,
"""
<b>Приятного использования!</b>
Здесь можно посмотреть расписание в более приятном виде, а также настроить уведомления о парах!
Здесь можно посмотреть расписание и настроить уведомления о парах.
<i>Изменить факультет/курс/группу можно просто перезапустив бота</i>
''', reply_markup = Markup.default(u))
<i>Изменить группу можно через /start.</i>
""",
reply_markup=Markup.default(user),
)
@bot.message_handler(
content_types=["text"], func=lambda message: validate(message).state == "selectGroup"
)
def selectGroupState(message: Message):
query = (message.text or "").strip()
try:
matches = find_main_groups(query)
if len(matches) == 1 and group_exists(query):
return _finish_registration(message, matches[0])
except ScheduleAPIError as error:
logger.error("Schedule", f"[{message.from_user.id}] {error}")
return send(
message,
"Сервер расписания сейчас недоступен. Попробуй отправить название группы позже.",
)
if not matches:
return send(
message,
"Похожих групп не нашлось. Проверь название и попробуй ещё раз.",
reply_markup=Markup.selectGroup([]),
)
send(
message,
"Выбери свою группу из самых похожих вариантов или введи название точнее.",
reply_markup=Markup.selectGroup(matches),
)

View file

@ -11,6 +11,8 @@ def otherState(message: Message):
if message.text not in possibleResponces and message.text not in ['Четная 📅', 'Нечетная 📅']: return
result: str = ''
s = u.schedule()
if not s:
return send(message, 'Не удалось получить расписание. Попробуй ещё раз позже.')
if message.text in possibleResponces:
s.weekDay = possibleResponces.index(message.text)
result = generateDaySchedule(s)

View file

@ -8,7 +8,7 @@ TELEGRAM_PROXY: str | None = None
WEB_BASE_URL: str = 'https://zatups.example.com'
# Base URL for the schedule (should be runned separately)
SCHEDULE_BASE_URL: str = 'http://schedule_backend:8000/'
SCHEDULE_BASE_URL: str = 'http://10.9.8.3:18822'
# Telegram user IDs
ADMINS: list[int] = [5016590523, 5001115363]

View file

@ -19,18 +19,11 @@ class Lesson(Event):
t = s.today()
if t == []: continue
w = f'<b>Скоро начнется пара</b>\n'
for lessonList in t:
if len(lessonList) == 1:
if [lessonTime.hour, lessonTime.minute] == lessonList[0].start:
w += lessonList[0].text(isToday = True)
else:
lesson = lessonList[1 if s.weekNumber%2 == 0 else 0]
if lesson and [lessonTime.hour, lessonTime.minute] == lesson.start:
w += lesson.text(isToday = True)
for lesson in t:
if [lessonTime.hour, lessonTime.minute] == lesson.start:
w += lesson.text(isToday=True)
if len(w.split('\n')) > 2: send(user.id, w)
addEvent(Lesson)

View file

@ -1,111 +1,50 @@
import datetime
import html
import json
from typing import Any, Callable, Literal
from typing import Any
from .schedule import getFacCourseByGroup
from .user import User
def I(condition: bool): return 1 if condition else 0
FEATURE_LIST: dict[str, Callable[[User], int]] = {
"accounts": lambda _: 1,
"today_accounts": lambda u: I(u.usage["date"] == datetime.datetime.now().strftime("%Y.%m.%d")),
"today_interactions": lambda u: (
max(u.usage["count"], 0)
if u.usage["date"] == datetime.datetime.now().strftime("%Y.%m.%d")
else 0
),
"feature_groups": lambda u: I(len(u.saved_groups) > 1),
"feature_notifications": lambda u: I(len(u.notifications) > 0),
"feature_extended_schedule": lambda u: I('extended_schedule' in u.flags),
"fetaure_v2_ui": lambda u: I('v2_ui' in u.flags)
}
class Analytics:
def __init__(self):
self.issued_at: datetime.datetime = datetime.datetime.now()
self.custom: dict[str, Any] = {
"group_list": [],
}
self.total: dict[str, int] = {}
self.data: dict[
str, # faculty
dict[
str, # course
dict[str, int], # features
],
] = {}
self.issued_at = datetime.datetime.now()
self.users = 0
self.groups: dict[str, int] = {}
def add(self, u: User):
if u.state == "broken":
def add(self, user: User) -> None:
self.users += 1
if not user.group:
return
self.groups[user.group] = self.groups.get(user.group, 0) + 1
faculty, course = getFacCourseByGroup(u.group)
if not faculty:
return
faculty: str
course: str
# custom
[self.custom["group_list"].append(groupname) for groupname in u.saved_groups]
self.custom["group_list"] = list(set(self.custom["group_list"]))
# features
for feature_name, func in FEATURE_LIST.items():
result = func(u)
if feature_name not in self.total:
self.total[feature_name] = 0
self.total[feature_name] += result
if faculty not in self.data:
self.data[faculty] = {}
if course not in self.data[faculty]:
self.data[faculty][course] = {}
if feature_name not in self.data[faculty][course]:
self.data[faculty][course][feature_name] = 0
self.data[faculty][course][feature_name] += result
def toDict(self):
def toDict(self) -> dict[str, Any]:
return {
"version": 2,
"version": 3,
"issued_at": self.issued_at.isoformat(),
"total": self.total,
"data": self.data,
"custom": self.custom,
"users": self.users,
"groups": dict(sorted(self.groups.items())),
}
def append(self):
def append(self) -> None:
try:
w: list[dict[str, Any]] = json.load(
open("./web/analytics/data-v2.json", "r", encoding="utf-8")
)
except:
w = []
w.append(self.toDict())
w = w[-30:]
json.dump(
w,
open("./web/analytics/data-v2.json", "w", encoding="utf-8"),
ensure_ascii=False,
# indent=0,
)
with open("./web/analytics/data-v2.json", "r", encoding="utf-8") as file:
history: list[dict[str, Any]] = json.load(file)
except (FileNotFoundError, json.JSONDecodeError, OSError):
history = []
history.append(self.toDict())
with open("./web/analytics/data-v2.json", "w", encoding="utf-8") as file:
json.dump(history[-30:], file, ensure_ascii=False)
def text(self) -> str:
return (
f"""
<b>Аналитика за {self.issued_at.strftime("%m.%d")} 📊</b>"""
+ "\n".join(
[
f"{feature.capitalize()}: <code>{number}</code>"
for feature, number in self.total.items()
group_lines = [
f"{html.escape(group)}: <code>{count}</code>"
for group, count in sorted(self.groups.items())
]
)
+ """
<i><a href="https://zatups.sinya.ru/analytics">Сайт</a></i>
"""
groups = "\n".join(group_lines) if group_lines else "Нет зарегистрированных групп"
return (
f'<b>Аналитика за {self.issued_at.strftime("%d.%m")} 📊</b>\n'
f"Пользователи: <code>{self.users}</code>\n\n"
f"<b>Группы</b>\n{groups}"
)

View file

@ -5,7 +5,6 @@ import datetime
from telebot.types import ReplyKeyboardMarkup, InlineQueryResultArticle, InputTextMessageContent, Message, ReplyKeyboardRemove, WebAppInfo, KeyboardButton
from telebot import apihelper
import config
import utils.schedule_client as sc
if config.TELEGRAM_PROXY:
apihelper.proxy = {
@ -166,19 +165,18 @@ class Markup_v1:
return markup
@staticmethod
def selectGroup(query: str) -> ReplyKeyboardMarkup:
#EDIT
schedule = sc.ScheduleDB(api_url=config.SCHEDULE_BASE_URL)
groupList = schedule.query(sc.Group).filter_by(name=query)
def selectGroup(groups: list[str]) -> ReplyKeyboardMarkup:
markup = ReplyKeyboardMarkup(resize_keyboard=True)
for group in groupList:
for group in groups:
markup.add(group, row_width=1)
markup.add('Моей группы нет', row_width=1)
markup.add('Моей группы нету', row_width=1)
return markup
Markup = Markup_v1
def Receive(state: str, text: str | None = None, strict: bool = True):
return bot.message_handler(
content_types = ['text'],

View file

@ -1,143 +1,274 @@
import json
import datetime
import config as _config
from __future__ import annotations
import datetime
import difflib
import html
import json
import re
import threading
import time
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import quote
from urllib.request import Request, urlopen
from zoneinfo import ZoneInfo
import config
dayStrings = [
"Понедельник",
"Вторник",
"Среда",
"Четверг",
"Пятница",
"Суббота",
"Воскресенье",
]
MOSCOW = ZoneInfo("Europe/Moscow")
_CACHE_TTL = 300.0
_REQUEST_TIMEOUT = 10.0
_FAILURE_RETRY_DELAY = 30.0
_cache: dict[str, tuple[float, list[dict[str, Any]]]] = {}
_failures: dict[str, tuple[float, str]] = {}
_cache_lock = threading.RLock()
class ScheduleAPIError(RuntimeError):
pass
def _api_url(path: str) -> str:
return f"{config.SCHEDULE_BASE_URL.rstrip('/')}/{path.lstrip('/')}"
def _load_result(path: str) -> list[dict[str, Any]]:
now = time.monotonic()
with _cache_lock:
cached = _cache.get(path)
if cached and now - cached[0] < _CACHE_TTL:
return cached[1]
failure = _failures.get(path)
if failure and now < failure[0]:
if cached:
return cached[1]
raise ScheduleAPIError(failure[1])
request = Request(_api_url(path), headers={"Accept": "application/json"})
try:
with urlopen(request, timeout=_REQUEST_TIMEOUT) as response:
payload = json.load(response)
except (HTTPError, URLError, TimeoutError, OSError, ValueError) as error:
message = f"Не удалось получить {path}: {error}"
with _cache_lock:
_failures[path] = (now + _FAILURE_RETRY_DELAY, message)
if cached:
return cached[1]
raise ScheduleAPIError(message) from error
if payload.get("ok") is not True or not isinstance(payload.get("result"), list):
if cached:
return cached[1]
raise ScheduleAPIError(f"Сервер вернул некорректный ответ для {path}")
result = payload["result"]
with _cache_lock:
_cache[path] = (now, result)
_failures.pop(path, None)
return result
def clear_schedule_cache() -> None:
with _cache_lock:
_cache.clear()
_failures.clear()
def get_main_groups() -> list[dict[str, Any]]:
groups = _load_result("groups")
if not groups:
raise ScheduleAPIError("Сервер расписания пока не содержит групп")
return sorted(
(group for group in groups if group.get("parent_group_id") is None),
key=lambda group: str(group.get("name", "")).casefold(),
)
def _compact_group_name(value: str) -> str:
return re.sub(r"[^0-9a-zа-яё]", "", value.casefold())
def find_main_groups(query: str, limit: int = 8) -> list[str]:
query = query.strip()
if not query:
return []
groups = [str(group["name"]) for group in get_main_groups()]
compact_query = _compact_group_name(query)
exact = [name for name in groups if _compact_group_name(name) == compact_query]
if exact:
return exact[:1]
contains = [name for name in groups if compact_query in _compact_group_name(name)]
if contains:
return contains[:limit]
scored = sorted(
(
difflib.SequenceMatcher(None, compact_query, _compact_group_name(name)).ratio(),
name,
)
for name in groups
)
return [name for score, name in reversed(scored) if score >= 0.45][:limit]
def group_exists(group_name: str) -> bool:
target = _compact_group_name(group_name)
return any(
_compact_group_name(str(group["name"])) == target for group in get_main_groups()
)
def _minutes_to_time(value: int | None) -> list[int]:
if value is None:
return [0, 0]
return [value // 60, value % 60]
dayStrings = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье']
class Lesson:
#EDIT: фулл переделать потому-что формат поменялся сильно
def __init__(self, start_end: str, data: dict) -> None:
self.start: list[int] = [
int(start_end.split(' - ')[0].split(':')[0]),
int(start_end.split(' - ')[0].split(':')[1])
]
self.end: list[int] = [
int(start_end.split(' - ')[1].split(':')[0]),
int(start_end.split(' - ')[1].split(':')[1])
]
self.strTime: str = start_end
self.raw: str = data['raw']
self.ok: bool = data['ok']
if not self.ok: return
self.name: str = data['name']
self.type: str = data['type']
self.info: list[str] = data['info']
def __init__(self, data: dict[str, Any], subgroup_names: list[str]) -> None:
self.start = _minutes_to_time(data.get("time_start"))
self.end = _minutes_to_time(data.get("time_end"))
self.strTime = f"{self.start[0]:02d}:{self.start[1]:02d} - {self.end[0]:02d}:{self.end[1]:02d}"
self.name = str(data.get("name") or "Без названия")
self.type = str(data.get("type") or "пара")
self.teacher_ids = [str(value) for value in data.get("teacher_ids", [])]
self.room_ids = [str(value) for value in data.get("room_ids", [])]
self.subgroup_names = subgroup_names
self.info = self.teacher_ids + [f"ауд. {room}" for room in self.room_ids]
self.is_odd_week = bool(data.get("is_odd_week"))
self.is_even_week = bool(data.get("is_even_week"))
def text(self, isToday: bool = False) -> str:
date = datetime.datetime.now()
lessonStart = datetime.datetime(date.year, date.month, date.day, self.start[0], self.start[1], 10)
now = datetime.datetime.now(MOSCOW)
lesson_start = now.replace(
hour=self.start[0], minute=self.start[1], second=0, microsecond=0
)
lesson_end = now.replace(
hour=self.end[0], minute=self.end[1], second=0, microsecond=0
)
lesson_type = html.escape(self.type)
date_text = f"{lesson_type.capitalize()} <i>в {self.start[0]}:{self.start[1]:02d}</i>"
if isToday and lesson_start > now:
minutes = int((lesson_start - now).total_seconds() // 60)
if minutes < 30:
date_text = f"{lesson_type.capitalize()} <i>начнется через {max(minutes, 1)} мин.</i>"
elif isToday and lesson_start <= now < lesson_end:
minutes = int((lesson_end - now).total_seconds() // 60)
date_text = f"<i>Сейчас идет</i> {lesson_type}, закончится через {max(minutes, 1)} мин."
details: list[str] = []
details.extend(html.escape(teacher) for teacher in self.teacher_ids)
details.extend(
f'<a href="https://rasp.pgups.ru/schedule/room?room={quote(room)}">ауд. {html.escape(room)}</a>'
for room in self.room_ids
)
if self.subgroup_names:
details.append(html.escape(", ".join(self.subgroup_names)))
if self.ok:
dateStr: str = f'{self.type.capitalize()} <i>в {self.start[0]}:{self.start[1] if self.start[1] > 9 else "0"+str(self.start[1])}</i>'
if isToday:
if lessonStart > date and (lessonStart-date).seconds < 30*60:
dateStr = f'{self.type.capitalize()} <i>начнется через {max((lessonStart-date).seconds//60, 1)} мин.</i>'
elif lessonStart < date and (date-lessonStart).seconds < 31*60:
dateStr = f'<i>Сейчас идет</i> {self.type}'
elif lessonStart < date and (date-lessonStart).seconds >= 31*60 and (date-lessonStart).seconds < 89*60:
dateStr = f'{self.type.capitalize()} <i>закончится через {max(90-(date-lessonStart).seconds//60, 1)} мин.</i>'
wrb = ''
if self.info:
nd: list[str] = []
for data in self.info:
if data in self.type: continue
if 'подгр' in data:
nd.append(data.split('подгр')[0])
nd.append('\n<b>подгр'+data.split('подгр')[1]+f'</b>\n')
else: nd.append(data)
for data in nd:
# print()
# print(data, wrb)
if ('\n' in data or '\n' in wrb[-2:]):
wrb += data
elif 'ауд' in data.lower():
audn = '.'.join(data.replace(' ', '').split('.')[1:])
wrb += ', '+f'<a href="https://rasp.pgups.ru/schedule/room?room={audn}">{data}</a>'
else:
wrb += ', '+data
return f'''\
<blockquote>\
<b>{self.name}</b>
{dateStr}
{wrb[2:] if wrb else 'Дополнительная информация отсуствует'}</blockquote>'''
else:
return f'''\
<pre language="Error">\
Пара в {self.start[0]}:{self.start[1] if self.start[1] > 9 else '0'+str(self.start[1])}
{self.raw}</pre>'''
info = ", ".join(details) if details else "Дополнительная информация отсутствует"
return (
f"<blockquote><b>{html.escape(self.name)}</b>\n"
f"{date_text}\n{info}</blockquote>"
)
class Schedule:
def __init__(self, responce: dict) -> None:
self.weekNumber: int = responce['date']['weekNumber']
self.weekDay: int = responce['date']['weekDay']
def __init__(
self,
lessons: list[dict[str, Any]],
group_name: str,
groups: list[dict[str, Any]],
now: datetime.datetime | None = None,
) -> None:
current = now or datetime.datetime.now(MOSCOW)
self.weekNumber = current.isocalendar().week
self.weekDay = current.weekday()
self.raw: dict = responce['schedule']
self.days: list[list[list[Lesson]]] = []
base_group = next(
(
group
for group in groups
if _compact_group_name(str(group.get("name", "")))
== _compact_group_name(group_name)
and group.get("parent_group_id") is None
),
None,
)
if base_group is None:
raise ValueError(f"Группа {group_name!r} не найдена")
for dayInfo in self.raw.values():
currentDay = []
for start_end, lessons in dayInfo.items():
if lessons == [None] or lessons == [None, None]:
base_id = str(base_group["id"])
child_names = {
str(group["id"]): str(group["name"])
for group in groups
if group.get("parent_group_id") == base_id
}
target_ids = {base_id, *child_names}
self.days: list[list[Lesson]] = [[] for _ in range(5)]
for raw_lesson in lessons:
weekday = raw_lesson.get("weekday")
if not isinstance(weekday, int) or weekday not in range(1, 6):
continue
matched_ids = target_ids.intersection(map(str, raw_lesson.get("group_ids", [])))
if not matched_ids:
continue
subgroup_names = [
child_names[group_id] for group_id in matched_ids if group_id in child_names
]
self.days[weekday - 1].append(Lesson(raw_lesson, sorted(subgroup_names)))
lessonList: list[Lesson | None] = []
for lesson in lessons:
if lesson: lessonList.append(Lesson(start_end, lesson))
else: lessonList.append(None)
for day in self.days:
day.sort(key=lambda lesson: (lesson.start, lesson.name.casefold()))
currentDay.append(lessonList)
def today(self) -> list[Lesson]:
if self.weekDay not in range(5):
return []
even_week = self.weekNumber % 2 == 0
return [
lesson
for lesson in self.days[self.weekDay]
if (lesson.is_even_week if even_week else lesson.is_odd_week)
]
self.days.append(currentDay)
def today(self) -> list[list[Lesson]]:
return self.days[self.weekDay] if self.weekDay < 5 else []
def get_group_schedule(group_name: str) -> Schedule:
groups = _load_result("groups")
if not groups:
raise ScheduleAPIError("Сервер расписания пока не содержит групп")
lessons = _load_result("schedule/default")
return Schedule(lessons, group_name, groups)
def getFacCourseByGroup(group: str) -> tuple[str | None, str | None]:
for faculty, courses in _config.LOAD.items():
for course in courses:
path = f'./timetables/{faculty} {course}.json'.replace(' ', '_')
data: dict[str, dict] = json.load(
open(
path,
'r',
encoding = 'utf-8'
def generateDaySchedule(
schedule: Schedule, showOddWeekText: bool = True, isToday: bool = False
) -> str:
even_week = schedule.weekNumber % 2 == 0
weekday = schedule.weekDay
day_name = dayStrings[weekday]
header = (
f'<b>{day_name}, {"четная" if even_week else "нечетная"} неделя</b>\n'
if showOddWeekText
else f"<b>{day_name}</b>\n"
)
)
if group.lower() in [_.lower() for _ in data.keys()]:
return faculty, course
return None, None
lessons = schedule.today()
if not lessons:
return header + "Пар нет 🥰"
def getSchedulePathByGroup(group: str) -> str | None:
faculty, course = getFacCourseByGroup(group)
return None if not faculty else f'./timetables/{faculty} {course}.json'.replace(' ', '_')
def generateDaySchedule(schedule: Schedule, showOddWeekText: bool = True, isToday: bool = False) -> str:
isWeekOdd = schedule.weekNumber%2 == 0
weekDay = schedule.weekDay
schedule_today = schedule.today()
lastLessonEnds = [0,0]
scheduleResult = f'<b>{dayStrings[weekDay]}, {"четная" if isWeekOdd else "нечетная"} неделя</b>\n' if showOddWeekText else f'<b>{dayStrings[weekDay]}</b>'
for lessonList in schedule_today:
if len(lessonList) == 1:
scheduleResult += lessonList[0].text(isToday) + '\n'
lastLessonEnds = lessonList[0].end
else:
lesson = lessonList[1 if isWeekOdd else 0]
if lesson:
scheduleResult += lesson.text(isToday) + '\n'
lastLessonEnds = lesson.end
scheduleResult += f'Пары закончатся в <i>{lastLessonEnds[0]}:{lastLessonEnds[1] if lastLessonEnds[1] > 9 else "0"+str(lastLessonEnds[1])}</i>'
return scheduleResult
result = header + "\n".join(lesson.text(isToday) for lesson in lessons)
last_end = max(lesson.end for lesson in lessons)
return result + f"\nПары закончатся в <i>{last_end[0]}:{last_end[1]:02d}</i>"

View file

@ -1,39 +1,39 @@
import datetime
import hashlib
import json
import os
from typing import Any, Iterator
from telebot.types import CallbackQuery, InlineQuery, Message, ReplyKeyboardRemove
from telebot.types import CallbackQuery, InlineQuery, Message
from models import logger
from .schedule import Schedule, getSchedulePathByGroup
from .schedule import Schedule, ScheduleAPIError, get_group_schedule
class User:
def __init__(self, userID: int) -> None:
self.id: int = userID
try:
self.data: dict[str, str] = json.load(
self.data: dict[str, Any] = json.load(
open(f"./users/{self.id}.json", "r", encoding="utf-8")
)
except:
except FileNotFoundError:
self.data = {}
except (json.JSONDecodeError, OSError):
logger.error("Bot", f'INVALID "{userID}" SAVED DATA! RESET!')
self.data = {}
self.state: str | bool | None = self.data.get("state", None)
self.topic: str | None = self.data.get("topic", None)
self.course: str | None = self.data.get("course", None)
self.group: str | None = self.data.get("group", None)
self.notifications: list[str] = self.data.get("notifications", []) # type: ignore
self.valid: bool = self.data.get("valid", True)
self.saved_groups: list[str] = self.data.get("saved_groups", [])
saved_groups = self.data.get("saved_groups", [])
self.saved_groups = [
value for value in saved_groups if isinstance(value, str) and value.strip()
]
if self.group and self.group not in self.saved_groups:
self.saved_groups.append(self.group)
self.saved_groups = [t if "-" in t else "{null}" for t in self.saved_groups]
self.saved_groups = list(set(self.saved_groups))
try: self.saved_groups.remove("{null}")
except: ...
self.saved_groups = list(dict.fromkeys(self.saved_groups))
self.user: dict = self.data.get("user", {})
self.usage: dict = self.data.get(
@ -42,34 +42,29 @@ class User:
self.flags: list[str] = self.data.get('flags', [])
if "notifyEvening" in self.data.keys():
self.notifyEvening: str = self.data["notifyEvening"]
self.notifyBeforeLesson: str = self.data["notifyBeforeLesson"]
if self.data["notifyEvening"] and "evening" not in self.notifications:
self.notifications.append("evening")
if self.data["notifyBeforeLesson"] and "lesson" not in self.notifications:
if self.data.get("notifyBeforeLesson") and "lesson" not in self.notifications:
self.notifications.append("lesson")
self.save()
def schedule(self) -> Schedule | None:
date = datetime.datetime.now()
path = getSchedulePathByGroup(self.group)
if not path:
if not self.group:
self.valid = False
self.notifications = []
self.setState("broken")
self.save()
return None
return Schedule(
{
"date": {
"weekNumber": date.isocalendar().week,
"weekDay": date.weekday(),
},
"schedule": json.load(open(path, "r", encoding="utf-8"))[self.group],
}
)
try:
return get_group_schedule(self.group)
except ValueError:
self.valid = False
self.notifications = []
self.setState("broken")
return None
except ScheduleAPIError as error:
logger.error("Schedule", f"[{self.id}] {error}")
return None
def setState(self, newState: str) -> None:
self.state = newState
@ -79,8 +74,6 @@ class User:
return json.dump(
{
"state": self.state,
"topic": self.topic,
"course": self.course,
"group": self.group,
"notifications": self.notifications,
"valid": self.valid,
@ -103,9 +96,9 @@ def validate(ctx: Message | InlineQuery | CallbackQuery) -> User:
return False # type: ignore
def getAllUsers() -> list[User]: # type: ignore
def getAllUsers() -> Iterator[User]:
for _, _, filenames in os.walk("./users"):
for userFile in filenames:
if ".json" not in userFile:
if not userFile.endswith(".json"):
continue
yield User(int(userFile.replace(".json", ""))) # type: ignore
yield User(int(userFile.removesuffix(".json")))

View file

@ -20,7 +20,6 @@ class ICS:
else: # on this week
self.start_week_date = timezone.localize(datetime.now()) - timedelta(days=schedule.weekDay)
self.isWeekOdd = schedule.weekNumber%2 == 0
self.calendar = Calendar()
for target in range(5):
@ -28,10 +27,7 @@ class ICS:
event_date = self.start_week_date + timedelta(days=target)
# print(f'\n\n--- {target} -> {event_date.strftime("%Y-%m-%d")} ---')
for lessonList in schedule.today():
lesson = lessonList[0] if len(lessonList) == 1 else lessonList[1 if self.isWeekOdd else 0]
if not lesson: continue
for lesson in schedule.today():
event = Event()
event.name = f'{lesson.name}'
@ -59,4 +55,3 @@ class ICS:
path = f'./temp/{self.userID}.ics'
with open(path, 'w', encoding='utf-8', newline='') as file: file.writelines(content)
return path