Dapper.AOT generated a 19-parameter ctor for ShowcaseSessionRow based on the SELECT list in GetShowcaseSessionsAsync / GetShowcaseSessionAsync. After adding PublicationMode and IsMembersOnly to ShowcaseSessionDto in v3.7.0 the record itself was extended, but the SELECT still returned 19 columns, so the materializer threw "A parameterless default constructor or one matching signature (...) is required" and every request to /showcase returned 500. Add s.publication_mode and (s.publication_mode = 'ClubOnly') to both SELECT lists and propagate them through the ShowcaseSessionDto construction. The field list now matches the generated constructor exactly. Version bump 3.7.0 -> 3.7.1 (patch).
🎲 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-сообщества.