From 9295538f5f92c657da26373e08944e64ad704bd8 Mon Sep 17 00:00:00 2001 From: q Date: Tue, 17 Mar 2026 13:29:10 +0300 Subject: [PATCH] Add lesson schedule command --- README.md | 5 +- config.py | 76 ++++++++++++++++++++++++++ main.py | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 226 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 32e9f72..1b53fc8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ - вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`); - делает мемы из присланных фото (`/gen_mem Up:... Down:...`); - подсказывает, сколько минут до перекура по заданному расписанию (`/break`); +- показывает расписание, текущую и следующую пару (`/schedule`); - умеет базарить (`/talk`) с контекстом 100 сообщений; - авто-ответы в чате (10% шанс + триггер-слова); - миниигра `/penis` + `/top_penis`; @@ -43,6 +44,7 @@ docker compose down - `/maket <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240). - `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down. - `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур. +- `/schedule` — показывает расписание на сегодня, текущую и следующую пару. - `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений). - `/penis` — миниигра, 1 раз в 24 часа. - `/top_penis` — топ по длине. @@ -73,7 +75,8 @@ docker compose down - `/maket` подбирает размер текста, чтобы уместить его в область макета. - `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой. -- `/break` считает по расписанию в `config.py`. +- `/break` считает по расписанию в `config.py` и показывает следующую пару. +- Названия пар и верхняя/нижняя неделя лежат в `config.py` в `LESSON_SCHEDULE`. - `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite). - `/penis_casino` — слоты с множителем x1.2–x3 (`games/casino.py`). - `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`). diff --git a/config.py b/config.py index 9c7aaae..e3ac69a 100644 --- a/config.py +++ b/config.py @@ -56,6 +56,81 @@ TEMPLATES = { def make_time(h: int, m: int) -> time: return time(hour=h, minute=m) +# Если у тебя наоборот, просто поменяй на False. +UPPER_WEEK_IS_ODD = True + +PAIR_TIMES = { + 1: (make_time(8, 0), make_time(9, 35)), + 2: (make_time(9, 50), make_time(11, 25)), + 3: (make_time(11, 55), make_time(13, 30)), + 4: (make_time(13, 45), make_time(15, 20)), + 5: (make_time(15, 50), make_time(17, 25)), +} + +LESSON_SCHEDULE = { + "upper": { + 0: [ + {"pair": 1, "title": "Пары нету"}, + {"pair": 2, "title": "История Россия (семинар), каб. 504"}, + {"pair": 3, "title": "Первая Подгруппа: Основы программирование, каб. 606. Вторая подгруппа Глод."}, + {"pair": 4, "title": "Первая Подгруппа: Глод. Вторая подгруппа Основы программирование, каб 606"}, + {"pair": 5, "title": "Общекулутрная и профессиональная коммуникации."}, + ], + 1: [ + {"pair": 1, "title": "Нету пары"}, + {"pair": 2, "title": "Физика (лаб), каб. 241"}, + {"pair": 3, "title": "Математика (лек), каб 514"}, + ], + 2: [ + {"pair": None, "title": "Англ ебучий"}, + ], + 3: [ + {"pair": 1, "title": "Физика (лек), каб 245"}, + {"pair": 2, "title": "Физика (прак), каб 245"}, + {"pair": 3, "title": "Маетимака (прак), каб 503"}, + {"pair": 4, "title": "Безопасность жизнедятельности (прак) каб 503"}, + {"pair": 5, "title": "Общекультурная и профессиональная коммуникации (прак), каб 503"}, + ], + 4: [ + {"pair": 1, "title": "Нету пары"}, + {"pair": 2, "title": "История России (лек), каб 510"}, + {"pair": 3, "title": "Основы программирования (лек), каб 510"}, + {"pair": 4, "title": "Физическая культура спорта (прак), каб 503"}, + ], + }, + "lower": { + 0: [ + {"pair": 1, "title": "Пары нету"}, + {"pair": 2, "title": "История Россия (семинар), каб. 504"}, + {"pair": 3, "title": "Первая Подгруппа: Основы программирование, каб. 606. Вторая подгруппа Глод."}, + {"pair": 4, "title": "Первая Подгруппа: Глод. Вторая подгруппа Основы программирование, каб 606"}, + {"pair": 5, "title": "Общекулутрная и профессиональная коммуникации."}, + ], + 1: [ + {"pair": 1, "title": "Нету пары"}, + {"pair": 2, "title": "Физика (лаб), каб. 241"}, + {"pair": 3, "title": "Математика (лек), каб 514"}, + {"pair": 4, "title": "Математика (прак), каб 503"}, + ], + 2: [ + {"pair": None, "title": "Англ ебучий"}, + ], + 3: [ + {"pair": 1, "title": "Физика (лек), каб 245"}, + {"pair": 2, "title": "Физика (прак), каб 245"}, + {"pair": 3, "title": "Маетимака (прак), каб 503"}, + {"pair": 4, "title": "Безопасность жизнедятельности (прак) каб 503"}, + {"pair": 5, "title": "Общекультурная и профессиональная коммуникации (прак), каб 503"}, + ], + 4: [ + {"pair": 1, "title": "Нету пары"}, + {"pair": 2, "title": "История России (лек), каб 510"}, + {"pair": 3, "title": "Основы программирования (лек), каб 510"}, + {"pair": 4, "title": "Физическая культура спорта (прак), каб 503"}, + ], + }, +} + SCHEDULE = { 0: [ # Понедельник (make_time(9, 50), make_time(10, 35), 5), @@ -112,6 +187,7 @@ def get_hint_text(templates_str: str) -> str: f"• /maket «id» «текст» — доступные: {templates_str}.\n" "• /gen_mem Up:верх Down:низ — отправь с фото.\n" "• /break — узнать время до перекура.\n" + "• /schedule — расписание, текущая и следующая пара.\n" "• /penis — миниигра 1 раз в 24 часа.\n" "• /top_penis — топ по длине.\n" "• /talk [текст] — ИИ базарит на фене.\n" diff --git a/main.py b/main.py index 7a623d7..021b9ff 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ # Системные импорты import asyncio, json, logging, os, re, random, sqlite3, threading +from dataclasses import dataclass from datetime import datetime, time, timedelta from io import BytesIO from pathlib import Path @@ -28,6 +29,26 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) _polychaetsi_lock = threading.Lock() _polychaetsi_word_re = re.compile(r"[а-яё]+", re.IGNORECASE) +_no_lesson_re = re.compile(r"(нету\s+пары|нет\s+пары|пары\s+нету|пары\s+нет)", re.IGNORECASE) + +_weekday_names = { + 0: "Понедельник", + 1: "Вторник", + 2: "Среда", + 3: "Четверг", + 4: "Пятница", + 5: "Суббота", + 6: "Воскресенье", +} + + +@dataclass(frozen=True) +class LessonSlot: + pair_number: int | None + title: str + start: time | None + end: time | None + is_free: bool # --- Инициализация и вспомогательные функции --- @@ -71,12 +92,125 @@ def _format_delta_ms(delta: timedelta) -> str: seconds, milliseconds = divmod(rem, 1_000) return f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}" + +def _normalize_now(now: datetime | None = None) -> datetime: + if now is None: + return datetime.now(ZoneInfo(config.DEFAULT_TZ)) + if now.tzinfo is None: + return now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ)) + return now + + +def _current_week_key(now: datetime) -> str: + week_number = now.isocalendar().week + is_upper = (week_number % 2 == 1) == config.UPPER_WEEK_IS_ODD + return "upper" if is_upper else "lower" + + +def _build_lesson_slot(raw_entry: dict) -> LessonSlot: + pair_number = raw_entry.get("pair") + start_t, end_t = config.PAIR_TIMES.get(pair_number, (None, None)) + title = str(raw_entry.get("title", "")).strip() or "Пара не указана" + return LessonSlot( + pair_number=pair_number, + title=title, + start=start_t, + end=end_t, + is_free=bool(_no_lesson_re.search(title)), + ) + + +def get_today_lessons(now: datetime | None = None) -> tuple[str, list[LessonSlot]]: + now = _normalize_now(now) + week_key = _current_week_key(now) + raw_day = config.LESSON_SCHEDULE.get(week_key, {}).get(now.weekday(), []) + lessons = [_build_lesson_slot(item) for item in raw_day] + return week_key, lessons + + +def _lesson_bounds(now: datetime, lesson: LessonSlot) -> tuple[datetime | None, datetime | None]: + if lesson.start is None or lesson.end is None: + return None, None + start_dt = now.replace(hour=lesson.start.hour, minute=lesson.start.minute, second=0, microsecond=0) + end_dt = now.replace(hour=lesson.end.hour, minute=lesson.end.minute, second=0, microsecond=0) + return start_dt, end_dt + + +def _find_current_lesson(now: datetime, lessons: list[LessonSlot]) -> LessonSlot | None: + for lesson in lessons: + start_dt, end_dt = _lesson_bounds(now, lesson) + if start_dt and end_dt and start_dt <= now < end_dt: + return lesson + return None + + +def _find_next_lesson(now: datetime, lessons: list[LessonSlot]) -> LessonSlot | None: + for lesson in lessons: + start_dt, _ = _lesson_bounds(now, lesson) + if start_dt and start_dt > now and not lesson.is_free: + return lesson + return None + + +def _format_lesson_brief(lesson: LessonSlot) -> str: + prefix = f"{lesson.pair_number} пара" if lesson.pair_number is not None else "Пара" + if lesson.start and lesson.end: + return f"{prefix} ({lesson.start:%H:%M}-{lesson.end:%H:%M}) — {lesson.title}" + return f"{prefix} — {lesson.title}" + + +def _format_lesson_line(lesson: LessonSlot) -> str: + if lesson.pair_number is None: + return f"• {lesson.title}" + if lesson.start and lesson.end: + return f"{lesson.pair_number}. {lesson.start:%H:%M}-{lesson.end:%H:%M} — {lesson.title}" + return f"{lesson.pair_number}. {lesson.title}" + + +def next_lesson_text(now: datetime | None = None) -> str: + now = _normalize_now(now) + _, lessons = get_today_lessons(now) + next_lesson = _find_next_lesson(now, lessons) + if not next_lesson: + return "Следующей пары сегодня нет." + return f"Следующая пара: {_format_lesson_brief(next_lesson)}" + + +def build_schedule_text(now: datetime | None = None) -> str: + now = _normalize_now(now) + week_key, lessons = get_today_lessons(now) + current_lesson = _find_current_lesson(now, lessons) + next_lesson = _find_next_lesson(now, lessons) + week_label = "Верхняя" if week_key == "upper" else "Нижняя" + day_label = _weekday_names.get(now.weekday(), "Сегодня") + + lines = [f"{week_label} неделя", day_label] + if lessons: + lines.append("") + lines.extend(_format_lesson_line(lesson) for lesson in lessons) + else: + lines.append("") + lines.append("На сегодня расписание пустое.") + + lines.append("") + if current_lesson: + if current_lesson.is_free: + lines.append(f"Сейчас: {current_lesson.pair_number} пара свободна") + else: + lines.append(f"Сейчас: {_format_lesson_brief(current_lesson)}") + else: + lines.append("Сейчас: пары нет") + + if next_lesson: + lines.append(f"Следующая: {_format_lesson_brief(next_lesson)}") + else: + lines.append("Следующая: сегодня нет") + + return "\n".join(lines) + def minutes_until_break(now: datetime | None = None) -> str: """Логика расписания перекуров.""" - if now is None: - now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) - elif now.tzinfo is None: - now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ)) + now = _normalize_now(now) day = now.weekday() today_schedule = config.SCHEDULE.get(day, []) @@ -103,10 +237,7 @@ def minutes_until_break(now: datetime | None = None) -> str: def next_break_window(now: datetime | None = None) -> tuple[datetime | None, datetime | None]: """Возвращает (начало, конец) ближайшего перекура или (None, None).""" - if now is None: - now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) - elif now.tzinfo is None: - now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ)) + now = _normalize_now(now) day = now.weekday() today_schedule = config.SCHEDULE.get(day, []) @@ -744,9 +875,13 @@ async def handle_help(message: Message): async def handle_start(message: Message): await message.answer("Соси") +async def handle_schedule_cmd(message: Message): + now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) + await message.answer(build_schedule_text(now)) + async def handle_break_cmd(message: Message): now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) - await message.answer(minutes_until_break(now)) + await message.answer(f"{minutes_until_break(now)}\n{next_lesson_text(now)}") # Ставим будильник на ближайший перекур для этого чата start_dt, _ = next_break_window(now) @@ -1023,6 +1158,7 @@ def main(): # Регистрация хендлеров dp.message.register(handle_start, CommandStart()) dp.message.register(handle_help, Command("help")) + dp.message.register(handle_schedule_cmd, Command("schedule")) dp.message.register(handle_break_cmd, Command("break")) dp.message.register(handle_maket_cmd, Command("maket")) dp.message.register(handle_size_cmd, Command("size")) @@ -1045,6 +1181,7 @@ def main(): commands = [ BotCommand(command="start", description="Приветствие"), BotCommand(command="help", description="Подсказки по командам"), + BotCommand(command="schedule", description="расписание пар"), BotCommand(command="maket", description="Вписать текст в макет 1"), BotCommand(command="gen_mem", description="Мем из фото (ответом)"), BotCommand(command="break", description="Сколько минут до перекура"),