Add lesson schedule command

This commit is contained in:
q 2026-03-17 13:29:10 +03:00
parent 5b294415d0
commit 9295538f5f
3 changed files with 226 additions and 10 deletions

View file

@ -4,6 +4,7 @@
- вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`); - вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`);
- делает мемы из присланных фото (`/gen_mem Up:... Down:...`); - делает мемы из присланных фото (`/gen_mem Up:... Down:...`);
- подсказывает, сколько минут до перекура по заданному расписанию (`/break`); - подсказывает, сколько минут до перекура по заданному расписанию (`/break`);
- показывает расписание, текущую и следующую пару (`/schedule`);
- умеет базарить (`/talk`) с контекстом 100 сообщений; - умеет базарить (`/talk`) с контекстом 100 сообщений;
- авто-ответы в чате (10% шанс + триггер-слова); - авто-ответы в чате (10% шанс + триггер-слова);
- миниигра `/penis` + `/top_penis`; - миниигра `/penis` + `/top_penis`;
@ -43,6 +44,7 @@ docker compose down
- `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240). - `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240).
- `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down. - `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down.
- `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур. - `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур.
- `/schedule` — показывает расписание на сегодня, текущую и следующую пару.
- `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений). - `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений).
- `/penis` — миниигра, 1 раз в 24 часа. - `/penis` — миниигра, 1 раз в 24 часа.
- `/top_penis` — топ по длине. - `/top_penis` — топ по длине.
@ -73,7 +75,8 @@ docker compose down
- `/maket` подбирает размер текста, чтобы уместить его в область макета. - `/maket` подбирает размер текста, чтобы уместить его в область макета.
- `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой. - `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой.
- `/break` считает по расписанию в `config.py`. - `/break` считает по расписанию в `config.py` и показывает следующую пару.
- Названия пар и верхняя/нижняя неделя лежат в `config.py` в `LESSON_SCHEDULE`.
- `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite). - `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite).
- `/penis_casino` — слоты с множителем x1.2x3 (`games/casino.py`). - `/penis_casino` — слоты с множителем x1.2x3 (`games/casino.py`).
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`). - `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).

View file

@ -56,6 +56,81 @@ TEMPLATES = {
def make_time(h: int, m: int) -> time: def make_time(h: int, m: int) -> time:
return time(hour=h, minute=m) 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 = { SCHEDULE = {
0: [ # Понедельник 0: [ # Понедельник
(make_time(9, 50), make_time(10, 35), 5), (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" f"• /maket «id» «текст» — доступные: {templates_str}.\n"
"• /gen_mem Up:верх Down:низ — отправь с фото.\n" "• /gen_mem Up:верх Down:низ — отправь с фото.\n"
"• /break — узнать время до перекура.\n" "• /break — узнать время до перекура.\n"
"• /schedule — расписание, текущая и следующая пара.\n"
"• /penis — миниигра 1 раз в 24 часа.\n" "• /penis — миниигра 1 раз в 24 часа.\n"
"• /top_penis — топ по длине.\n" "• /top_penis — топ по длине.\n"
"• /talk [текст] — ИИ базарит на фене.\n" "• /talk [текст] — ИИ базарит на фене.\n"

155
main.py
View file

@ -1,5 +1,6 @@
# Системные импорты # Системные импорты
import asyncio, json, logging, os, re, random, sqlite3, threading import asyncio, json, logging, os, re, random, sqlite3, threading
from dataclasses import dataclass
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
@ -28,6 +29,26 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_polychaetsi_lock = threading.Lock() _polychaetsi_lock = threading.Lock()
_polychaetsi_word_re = re.compile(r"[а-яё]+", re.IGNORECASE) _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) seconds, milliseconds = divmod(rem, 1_000)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}" 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: def minutes_until_break(now: datetime | None = None) -> str:
"""Логика расписания перекуров.""" """Логика расписания перекуров."""
if now is None: now = _normalize_now(now)
now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ))
day = now.weekday() day = now.weekday()
today_schedule = config.SCHEDULE.get(day, []) 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]: def next_break_window(now: datetime | None = None) -> tuple[datetime | None, datetime | None]:
"""Возвращает (начало, конец) ближайшего перекура или (None, None).""" """Возвращает (начало, конец) ближайшего перекура или (None, None)."""
if now is None: now = _normalize_now(now)
now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ))
day = now.weekday() day = now.weekday()
today_schedule = config.SCHEDULE.get(day, []) today_schedule = config.SCHEDULE.get(day, [])
@ -744,9 +875,13 @@ async def handle_help(message: Message):
async def handle_start(message: Message): async def handle_start(message: Message):
await message.answer("Соси") 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): async def handle_break_cmd(message: Message):
now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) 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) start_dt, _ = next_break_window(now)
@ -1023,6 +1158,7 @@ def main():
# Регистрация хендлеров # Регистрация хендлеров
dp.message.register(handle_start, CommandStart()) dp.message.register(handle_start, CommandStart())
dp.message.register(handle_help, Command("help")) 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_break_cmd, Command("break"))
dp.message.register(handle_maket_cmd, Command("maket")) dp.message.register(handle_maket_cmd, Command("maket"))
dp.message.register(handle_size_cmd, Command("size")) dp.message.register(handle_size_cmd, Command("size"))
@ -1045,6 +1181,7 @@ def main():
commands = [ commands = [
BotCommand(command="start", description="Приветствие"), BotCommand(command="start", description="Приветствие"),
BotCommand(command="help", description="Подсказки по командам"), BotCommand(command="help", description="Подсказки по командам"),
BotCommand(command="schedule", description="расписание пар"),
BotCommand(command="maket", description="Вписать текст в макет 1"), BotCommand(command="maket", description="Вписать текст в макет 1"),
BotCommand(command="gen_mem", description="Мем из фото (ответом)"), BotCommand(command="gen_mem", description="Мем из фото (ответом)"),
BotCommand(command="break", description="Сколько минут до перекура"), BotCommand(command="break", description="Сколько минут до перекура"),