Telegram-бот, через который мастера бронируют общую мастерскую под свои мастер-классы. Двойных броней не бывает, между МК есть буфер.
aiogram 3, SQLite, APScheduler. Брони лежат в SQLite, в Google Calendar уходит зеркало. Крутится в docker compose на VPS.
| Команда | Кто | Что делает |
|---|---|---|
/book |
мастер, личка | бронь: дата, длительность, время, название |
/my |
мастер | свои брони: отмена, передача другому мастеру |
/week |
мастер, личка или чат | занятость на 7 дней: брони и события календаря (📌), имя мастера ведёт в его профиль |
/cancel |
мастер | прервать /book |
/start |
все, личка | мастеру справка, постороннему его ID |
/help |
все, личка | справка |
/id |
все | свой ID или ID чата |
/addmaster <id> [имя] |
админ | добавить мастера; в чате можно ответом на его сообщение |
/delmaster <id> |
админ | убрать мастера, оставшиеся брони покажет с кнопками отмены |
/masters |
админ | мастера: имя, @тег, ID |
/all |
админ, личка | то же на весь срок бронирования; отмена и передача любой брони, событию из календаря можно назначить мастера |
/manual |
админ | инструкция для мастеров, чтобы переслать или закрепить в чате |
Админы из ADMIN_IDS считаются мастерами. Брони и отмены анонсируются в чат мастеров, напоминания идут мастеру в личку и в чат (в чат с 23:00 до 08:00 без звука).
Бронировать можно круглосуточно, в том числе через полночь. Дата брони считается по началу. Длительность: кнопкой или текстом (9, 2,5, 2:30), от 15 минут до 24 часов. Над кнопками времени бот показывает, чем занят день: чужие брони и события календаря, с учётом перерыва.
Локально, сборка из исходников:
cp .env.example .env # вписать BOT_TOKEN
mkdir -p data secrets
sudo chown -R 10001 data # контейнер работает от uid 10001
echo '{}' > secrets/google-sa.json # заглушка, пока GCAL_CALENDAR_ID пуст
docker compose up -d --build
docker compose logs -fДальше в Telegram:
- Написать боту
/start, полученный ID вписать вADMIN_IDS, выполнитьdocker compose up -d. - Добавить бота в чат мастеров, там
/id, значение вписать вMASTERS_CHAT_ID, сноваdocker compose up -d. Privacy mode выключать не нужно. - Мастеров добавить через
/addmaster(проще всего ответом на сообщение мастера в чате).
Историю, публичную ссылку и темы в чате лучше включить до /id: при превращении в супергруппу ID меняется на -100.... Если это случится позже, бот перейдёт на новый ID сам и напишет админам, что вписать в .env.
Чтобы бота не добавляли в чужие группы: @BotFather, /setjoingroups, Disable.
Всё через .env, шаблон в .env.example.
| Переменная | По умолчанию | Назначение |
|---|---|---|
BOT_TOKEN |
нет, обязательна | токен от @BotFather |
ADMIN_IDS |
пусто | Telegram ID админов через запятую |
MASTERS_CHAT_ID |
пусто | чат для анонсов и напоминаний; если пусто, в чат ничего не постится |
GCAL_CALENDAR_ID |
пусто | ID календаря Google; если пусто, календарь выключен |
TZ |
Asia/Novosibirsk |
часовой пояс |
BUFFER_MINUTES |
30 |
пауза между МК, 0-720 |
SLOT_STEP_MINUTES |
30 |
шаг сетки начала: делитель суток от 15 до 240 |
DURATIONS_MINUTES |
60,90,120,180,240,300,360,480,540,600,720 |
кнопки длительности, 15-1440 минут |
DAYS_AHEAD |
21 |
на сколько дней вперёд показывать даты, 1-90 |
REMIND_BEFORE_MINUTES |
1440,60 |
за сколько минут напоминать; пустое значение отключает |
LOG_LEVEL |
INFO |
уровень логов |
BOT_IMAGE |
workshop-bot:latest |
образ для compose; на сервере указывает на GHCR |
DB_PATH |
/data/bot.db |
задаётся в compose.yml; вне контейнера data/bot.db |
GOOGLE_SA_FILE |
/run/secrets/google_sa |
задаётся в compose.yml, ключ берётся из secrets/google-sa.json |
BOT_IMAGE читает сам compose (подстановка в image:), остальное уходит в контейнер через env_file.
- В console.cloud.google.com создать проект, включить Google Calendar API.
- IAM & Admin, Service Accounts: создать аккаунт без ролей, выпустить JSON-ключ, сохранить как
secrets/google-sa.json. sudo chown 10001 secrets/google-sa.json && sudo chmod 400 secrets/google-sa.json- В настройках календаря дать email сервис-аккаунта право "Внесение изменений в мероприятия".
- "Идентификатор календаря" вписать в
GCAL_CALENDAR_ID, выполнитьdocker compose up -d.
В логе должно появиться Google Calendar: включён (...) и Google Calendar: чтение календаря проверено.
Как это работает:
- Ручные события календаря занимают время (с тем же буфером) и видны в
/weekс пометкой 📌. События со статусом "свободен" не учитываются. - Если календарь не отвечает или не читается, бронировать бот не даёт. При ошибке настройки пишет админам в личку.
- Если событие не создалось сразу, его досоздаст фоновая задача (раз в 5 минут). ID события вычисляется из брони, так что дублей не будет.
- Если событие брони удалить в календаре руками, бронь в течение 5 минут отменится, в чат и мастеру в личку уйдёт сообщение.
- Событию, созданному руками, админ может назначить мастера (
/all, кнопка под списком). Оно становится обычной бронью с напоминаниями и отменой через/my, само событие остаётся и переименовывается.
Отменой считается только явная пометка Google "удалено". Когда события нет совсем (например, сменили календарь в .env), бот создаёт его заново. Если за один проход пропало больше 5 событий, бот ничего не отменяет и пишет админам: руками столько не удаляют. Перенос и переименование события бот игнорирует, время меняется отменой и новой бронью.
.github/workflows/ci.yml, запускается на push в main, теги v*, pull request и вручную.
test:ruff check bot testsиpytest -qна Python 3.12.image: послеtestсобирает образ (linux/amd64) и пушит вghcr.io/<owner>/<repo>. На pull request только сборка, без пуша. Авторизация через встроенныйGITHUB_TOKEN, секреты заводить не нужно.
Теги образа: latest с default-ветки, sha-<short> всегда, с git-тега v1.2.3 ещё 1.2.3 и 1.2 (latest при этом не двигается). Имя образа в GHCR всегда в нижнем регистре.
Dependabot раз в неделю обновляет actions и патчи базового образа. Python-зависимости обновляются руками через make lock, потому что lock собирается в контейнере.
На сервере нужны compose.yml, .env, data/, secrets/ и, по желанию, Makefile ради make pull и make backup. build: из compose там не используется, образ берётся из BOT_IMAGE. В .env:
BOT_IMAGE=ghcr.io/<owner>/<repo>:latestОбновление (то же самое делает make pull):
docker compose pull && docker compose up -dПакет в GHCR после первого пуша приватный. Либо сделать его публичным (Package settings, Change visibility), либо один раз залогиниться на сервере с PAT со scope read:packages:
docker login ghcr.io -u <user>Релиз:
git tag v1.2.3 && git push origin v1.2.3Для отката или фиксации версии вписать в BOT_IMAGE тег 1.2.3 или sha-<short> вместо latest.
docker compose logs -f --tail=100После правки .env нужен docker compose up -d, а не restart: он не перечитывает .env. Что настройки подхватились, видно в логе по строке Запущен @...; админы: [...]; чат мастеров: ....
data/bot.db работает в WAL, поэтому cp при живом боте даст битую копию. make backup снимает копию через sqlite backup API (контейнер должен быть запущен) и кладёт в backups/bot-YYYY-MM-DD.db, каталог задаётся через BACKUP_DIR. Удобно держать в cron и забирать копии с сервера:
0 4 * * * make -C /opt/workshop-bot backup BACKUP_DIR=/backupВосстановление только при остановленном боте, одной цепочкой:
docker compose stop bot \
&& sudo rm -f data/bot.db-wal data/bot.db-shm \
&& sudo cp backups/bot-2026-10-01.db data/bot.db \
&& sudo chown 10001 data/bot.db \
&& docker compose start bot| Симптом | Причина | Что делать |
|---|---|---|
Не стартует, Ошибка настройки: ... |
неверное значение в .env |
в сообщении названа переменная; поправить, docker compose up -d |
база ... недоступна на запись, readonly database |
файлы в data не принадлежат uid 10001 (базу копировали) |
sudo chown -R 10001 data |
... не читается как JSON-ключ сервис-аккаунта |
GCAL_CALENDAR_ID задан, а в secrets/google-sa.json заглушка или нет прав |
положить ключ, chown 10001; либо очистить GCAL_CALENDAR_ID |
| Админам: "Чат мастеров ... недоступен: chat not found" | неверный MASTERS_CHAT_ID, у супергруппы он с -100 |
взять ID через /id в самом чате |
| Админам: "Не могу писать в чат мастеров ... bot was kicked" | бота удалили из чата | вернуть бота в чат |
| Админам: "Google Calendar не читается (HTTP 403/404 ...)" | неверный ID календаря или календарь не расшарен | проверить GCAL_CALENDAR_ID и доступ сервис-аккаунта |
| Брони есть, в календаре пусто | календарь расшарен только на просмотр | дать право "Внесение изменений в мероприятия"; брони досинхронизируются сами |
pip install -r requirements-dev.txt
make test # pytest: слоты, БД, календарь на моках, сценарии с фейковым Bot API
make lint # ruff check bot tests
make lock # перегенерировать requirements.lock в python:3.12-slim-bookwormОбраз собирается из requirements.lock (точные версии), в requirements.txt только верхний уровень. После make lock прогнать тесты.
Логика брони и отмены живёт в bot/services.py, хендлеры тонкие. Фоновые задачи (синхронизация календаря раз в 5 минут, напоминания раз в минуту) в bot/tasks.py.
Миграций схемы нет. Перед добавлением колонки прочитать комментарий над SCHEMA в bot/db.py.