Address review feedback from PR #119: - LeaveClubMembershipAsync: was rejecting Pending rows because the SQL required status = 'Active', so clicking "Отозвать заявку" on a Pending membership surfaced a misleading "Active membership X not found" InvalidOperationException. Now the method first tries Active -> Left and falls back to Pending -> Rejected so the same UI flow covers both states. - PublicClub.razor TrySubmitApplicationAsync: removed the empty-input guard that contradicted the "(необязательно)" label and the server side (AuthorizedMembershipService already trims and accepts null). No tests broken (493 still passing), no public-API changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🎲 GM-Relay: TTRPG Session Scheduling Bot & Web Dashboard
GM-Relay — это комплексное решение для Мастеров Подземелий (ГМов), состоящее из высокопроизводительного Telegram-бота, Discord worker и удобного веб-интерфейса. Предназначено для автоматизации записи игроков на сессии, управления расписанием и проведения игр.
Проект разработан с упором на производительность, архитектуру Vertical Slice, Native AOT (для бота) и удобство развертывания с использованием .NET Aspire.
Текущая версия: v3.6.0.
✨ Key Features
🤖 Telegram Bot
- 📅 Создание расписаний (Batch Sessions): Создавайте сразу несколько игр одним сообщением изменения (на недельный месяц в перед).
- 🖼 Обложки расписаний: И batch-посту можно прикрепить фото к
/newsessionили указать строкуКартинка: https://...; бот отправит обложку перед сообщением записи. - ⚡ Быстрые повторы расписания: Для регулярной кампании можно указать одну дату, количество игр и интервал, а бот сам развернёт повторяющийся batch.
- ✋ Интерактивная запись и выход: Игроки записываются на конкретные даты и самостоятельно снимают запись нажатием одной кнопки.
- 👥 Лимит мест и лист ожидания: ГМ задаёт максимальный состав, бот не переполняет сессию, автоматически ведёт очередь ожидания и освобождённое место отдаёт первому ожидающему.
- 📁 Поддержка Форумов (Telegram Topics): Если
/newsessionзапущен в теме форума Telegram, расписание и групповые уведомления остаются в этой теме; при запуске из корня форума бот создает отдельную тему и сообщает о необходимости прав admin/Manage Topics, если их не хватает. - ❌ Управление сессиями: Owner и назначенные co-GM могут создавать, отменять, удалять и переносить игры из Telegram через
/listsessions; публичный пост записи показывает только кнопки игроков. - 🔄 Голосование за перенос: Быстрый поиск свободного места с через свободное недель и кнопками новых времени и дедлайном.
- 🔔 Уведомления: Игрок получают за 24 часа, напоминание за 1 час, ссылку перед игрой, отмены и переносы; групповые уведомления при этом остаются.
- 🕐 Режим уведомлений batch: Для каждой пачки можно выбрать
В группе и в личкуилиТолько в группе. - ⬆️ Управление очередью: Веб-интерфейс показывает заполненность, лист ожидания и позволяет ГМу поднять первого игрока из очереди.
- 🔄 Автоматическая синхронизация: Любые изменения в веб-интерфейсе мгновенно обновляют сообщения с расписанием в подключенных Telegram- и Discord-каналах.
Discord Bot
- Slash-команды
/newsessionи/listsessions: GM создаёт сессии и публикует актуальное расписание прямо в Discord-канале. - Кнопки Join/Leave с ephemeral-ответами: игроки нажимают Join/Leave в Discord-сообщении; бот отвечает ephemeral-сообщением и обновляет schedule message.
- RSVP (подтверждения) за 24ч до сессии: scheduler публикует запрос подтверждения в Discord-канале, игроки отвечают кнопками, а GM получает итоги RSVP.
- DM-напоминания за 1ч и ссылки перед игрой: one-hour reminders и join-link notifications отправляются в Discord DM при включённых личных уведомлениях; сбои DM логируются без публичного fallback.
- Reschedule voting (голосование за перенос): deadline-сервис обновляет Discord vote message и schedule message через
IPlatformMessenger. - Лимиты и waitlist: при заполненном составе игрок попадает в waitlist, а при выходе участника первый ожидающий автоматически продвигается в основной состав.
🌐 Web Dashboard (Blazor Server)
- 🔐 Авторизация через Telegram: Telegram Login Widget с HMAC-SHA256 валидацией.
- 📱 Telegram Mini App Dashboard: Мобильная панель открывается из Telegram, проверяет
initDataна сервере, учитывает safe-area телефона и верхнюю панель Telegram. - ✏️ Редактирование: Детальное изменение дат, названий и статусов сессий.
- 🤝 Co-GM и делегирование: Owner назначает помощников по Telegram ID; co-GM управляет расписанием, но не может назначать других co-GM.
- 🌍 Публичные страницы клубов: Owner и co-GM включают read-only страницу
/club/{slug}и отдельные ссылки/s/{sessionId}только для опубликованных сессий; состав игроков и приватные join-ссылки не показываются. - 🧑🏫 Публичные профили мастеров: мастер управляет профилем из
/profile, публикует описание на/gm/{slug}, а публичные клубы, игры и каталог ссылаются на профиль без раскрытия platform identifiers. - 📚 Портфолио завершённых приключений: Owner и co-GM собирают завершённые сессии в портфолио-игры на странице
/group/{id}/portfolio, привязывают ссылки на прошедшие сессии и публикуют публичную страницу/portfolio/{slug}с обложкой, описанием, системой/форматом и составом мастеров. - ⭐ Модерируемые отзывы игроков: участники прошедших сессий могут оставить отзыв на
/portfolio/{slug}/reviewс явным согласием на публикацию; мастера модерируют отзывы (Approved/Rejected/Hidden) в редакторе портфолио, и только одобренные отзывы видны публичной странице. - 🖼 Обложки портфолио: мастера загружают JPG/PNG/WEBP-обложки в редакторе портфолио; файлы сохраняются в Docker volume
portfolio_coversи обслуживаются по пути/portfolio-covers/{storageKey}; конфигурация пути —PortfolioCovers__StoragePathвcompose.yaml. - 📋 Шаблоны кампаний: Вкладка
Шаблоныотдельно от страницы группы: сохранение типовых параметров и запуск нового batch из шаблона. - 📦 Bulk-операции для Batch Sessions:
- обновить общий
title/linkу всей пачки; - перенести пачку на фиксированный шаг в днях;
- клонировать batch на следующую неделю или месяц.
- обновить общий
- ⬆️ Управление очередью: Заполненность, лист ожидания и ручное повышение игрока из очереди.
- 📜 История изменений сессий: Страница
/session/{id}/historyпоказывает аудит-лог всех значимых изменений (время, ссылка, название, участники, статус) с указанием акторов и дат. - 📊 Статистика посещаемости: Страница
/group/{id}/statsпоказывает долю присутствия, количество пропусков и среднюю явку по каждому игроку группы. - 🔄 Автосинхронизация: Изменения в вебе мгновенно перерисовывают platform message расписания через
IPlatformMessenger.
🛠 Технологический стек
| Компонент | Технология |
|---|---|
| Язык | C# 14 (.NET 10) |
| Архитектура | Vertical Slice + общая библиотека GmRelay.Shared |
| Боты | Telegram.Bot (Native AOT), NetCord Gateway (Discord worker внутри GmRelay.Bot) |
| Веб | Blazor Server |
| Оркестрация | .NET Aspire (GmRelay.AppHost) |
| БД | PostgreSQL |
| ORM | Dapper + Dapper.AOT (source generators) |
| Миграции | DbUp |
| Развёртывание | Docker Compose, Multi-arch (AMD64/ARM64) |
Note
При использовании Dapper в режиме Native AOT все SQL-запросы используют строго типизированные DTO; динамические типы (
dynamic) не поддерживаются.
🚀 Быстрый старт (Docker Compose)
Требования: Docker и Docker Compose.
1. Настройка окружения
cp .env.example .env
Ключевые переменные .env:
# Токен от @BotFather (используется ботом и как секретный ключ веб-авторизации)
TELEGRAM_BOT_TOKEN=ваш_токен_здесь
# Токен Discord application bot
DISCORD_BOT_TOKEN=ваш_discord_токен_здесь
# Discord OAuth (для Web Dashboard)
DISCORD_CLIENT_ID=ваш_discord_client_id_здесь
DISCORD_CLIENT_SECRET=ваш_discord_client_secret_здесь
DISCORD_REDIRECT_URI=https://your-domain.example/auth/discord/callback
# Имя бота без @ (для Telegram Login Widget)
TELEGRAM_BOT_USERNAME=ваше_имя_бота_здесь
# HTTPS URL Mini App, например https://your-domain.example/miniapp
TELEGRAM_MINI_APP_URL=https://your-domain.example/miniapp
POSTGRES_PASSWORD=ваш_надежный_пароль
GMRELAY_WEB_PORT=8080
Настройка в @BotFather:
- Команда
/setdomainдля работы виджета авторизации на вашем домене. - Для Mini App настройте домен Web Dashboard и menu button на URL из
TELEGRAM_MINI_APP_URL. - Начиная с v1.9.3 дополнительных действий для фикса входа не требуется: fallback выполняется внутри активного Telegram WebView по тому же HTTPS-адресу
/miniapp.
2. Запуск
docker compose up -d
Автоматически выполняется:
- создание Docker-сети и volume PostgreSQL;
- подъём PostgreSQL (
db:5432); - запуск бота с плавной миграцией (DbUp);
- запуск Discord Gateway worker на NetCord (healthcheck на
:8082); - запуск веб-приложения с подключением к БД и Telegram API.
3. Первоначальная настройка
- Напишите боту
/start. - Создайте группу через
/newgroup. - Откройте Mini App или Web Dashboard для расширенного управления.
- Для Discord пригласите application bot на сервер с правами
botиapplications.commands. СкопируйтеDISCORD_BOT_TOKENв.env;DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRETиDISCORD_REDIRECT_URIнужны только для входа в Web Dashboard через Discord. - Перезапустите Docker Compose (
docker compose up -d), а затем в Discord создайте сессию через/newsessionили опубликуйте расписание через/listsessions; игроки записываются и выходят кнопками в опубликованном сообщении.
📚 Портфолио завершённых приключений
Начиная с v3.6.0 ГМы могут публиковать завершённые кампании в виде постоянных портфолио-страниц с обложкой, описанием, системой/форматом, составом мастеров и модерируемыми отзывами игроков.
Возможности
- Управление портфолио — в
/group/{id}/portfolioвладелец и co-GM создают портфолио-игры из прошедших сессий, выбирают мастеров, заполняют описание, загружают обложку и публикуют поpublic_slug. - Публичная страница
/portfolio/{slug}— read-only карточка приключения с обложкой, описанием, составом мастеров (только публичные профили) и одобренными отзывами. - Отзывы участников — на
/portfolio/{slug}/reviewаутентифицированные игроки, чьи идентификаторы участвовали в одной из привязанных сессий без пометки GM, отправляют отзыв с явным согласием на публикацию; один отзыв на игрока, повторная отправка запрещена. - Модерация отзывов — на странице редактора портфолио владелец/co-GM видит очередь
Pendingи переводит отзывы вApproved,RejectedилиHidden; толькоApprovedотзывы попадают в публичную выдачу. - Публикация под требования — портфолио-игра публикуется только при заполненном slug, описании, обложке, минимум одной завершённой сессии и хотя бы одном мастере группы.
Хранение обложек
Загруженные обложки хранятся в Docker volume portfolio_covers (по умолчанию имя gmrelay_portfolio_covers), обслуживаются веб-приложением по пути /portfolio-covers/{storageKey} с кешированием Cache-Control: public, max-age=31536000, immutable.
В .env можно переопределить имя volume:
PORTFOLIO_COVERS_VOLUME_NAME=gmrelay_portfolio_covers
В compose.yaml это значение пробрасывается в сервис web через volumes.portfolio_covers.name; путь к каталогу внутри контейнера — /app/portfolio-covers (настраивается через PortfolioCovers__StoragePath).
Хранилище инкапсулировано интерфейсом IPortfolioCoverStorage с реализацией LocalPortfolioCoverStorage (файловая система), что оставляет границу для замены на S3-совместимое хранилище без изменения кода портфолио-сервисов.
💾 Backup и восстановление
Проект включает автоматический ежедневный backup PostgreSQL через сервис db-backup в Docker Compose.
Как это работает
- Каждый день в 03:00 выполняется
pg_dumpбазыgmrelay_db. - Дампы сжимаются (
gzip) и сохраняются в volumepgbackups(/backups). - Формат имени:
gmrelay_db_YYYYMMDD_HHMMSS.sql.gz. - Ротация: по умолчанию хранятся последние 7 дней (настраивается через
BACKUP_RETENTION_DAYS).
Проверка бэкапов
docker compose exec db-backup ls -la /backups
Ручное создание дампа
docker compose exec db-backup sh -c "pg_dump -h db -U gmrelay -d gmrelay_db | gzip > /backups/gmrelay_db_manual.sql.gz"
Восстановление из бэкапа
# Использовать последний автоматический бэкап
./scripts/restore.sh
# Или указать конкретный файл
./scripts/restore.sh backups/gmrelay_db_20260512_030000.sql.gz
Warning
Восстановление перезаписывает текущую базу данных. Убедитесь, что вы понимаете последствия, прежде чем запускать
restore.sh.
Переменные окружения (опциональные)
BACKUP_RETENTION_DAYS=7
BACKUP_VOLUME_NAME=game_pgbackups
🗂 Структура репозитория
├── src/
│ ├── GmRelay.AppHost/ # .NET Aspire orchestrator
│ ├── GmRelay.Bot/ # Telegram- и Discord-бот (Native AOT + NetCord Gateway worker)
│ ├── GmRelay.ServiceDefaults/ # Aspire service defaults
│ ├── GmRelay.Shared/ # Общие доменные модели
│ └── GmRelay.Web/ # Blazor Server dashboard
├── tests/
│ └── GmRelay.Bot.Tests/ # xUnit + NSubstitute
├── compose.yaml # Docker Compose (AMD64 + ARM64)
└── .env.example # Шаблон переменных окружения
👨💻 Для разработчиков
- Архитектура: проект следует Vertical Slice с явным DI. Подробности — в ADR-001 и ADR-002.
- Добавление обработчика: из-за Native AOT все DI-регистрации выполняются вручную в
src/GmRelay.Bot/Program.cs(assembly scanning не используется). - Миграции: SQL-скрипты добавляются как embedded resources в
src/GmRelay.Bot/Migrations/и применяются автоматически при старте бота через DbUp. - Тесты:
dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --verbosity normal - Сборка:
dotnet build
📜 Лицензия
MIT License. См. LICENSE.
Построено с ❤️ для TTRPG-сообщества.