Toutsu 29f6f6a827
PR Checks / test-and-build (pull_request) Successful in 8m17s
fix(web): include PublicationMode/IsMembersOnly in showcase SQL to fix /showcase 500
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).
2026-06-03 22:21:31 +03:00
2026-04-13 13:52:49 +03:00
2026-05-12 16:25:17 +03:00

🎲 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. Первоначальная настройка

  1. Напишите боту /start.
  2. Создайте группу через /newgroup.
  3. Откройте Mini App или Web Dashboard для расширенного управления.
  4. Для Discord пригласите application bot на сервер с правами bot и applications.commands. Скопируйте DISCORD_BOT_TOKEN в .env; DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET и DISCORD_REDIRECT_URI нужны только для входа в Web Dashboard через Discord.
  5. Перезапустите 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) и сохраняются в volume pgbackups (/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-сообщества.

S
Description
No description provided
Readme MIT 10 MiB
Languages
C# 80.8%
HTML 13.9%
CSS 3.6%
PLpgSQL 1%
Dockerfile 0.2%
Other 0.5%