Add lesson schedule command
This commit is contained in:
parent
5b294415d0
commit
9295538f5f
3 changed files with 226 additions and 10 deletions
|
|
@ -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.2–x3 (`games/casino.py`).
|
- `/penis_casino` — слоты с множителем x1.2–x3 (`games/casino.py`).
|
||||||
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).
|
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).
|
||||||
|
|
|
||||||
76
config.py
76
config.py
|
|
@ -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
155
main.py
|
|
@ -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="Сколько минут до перекура"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue