refactor(shared): extract reschedule voting types to Shared
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
# Discord /newsession и /listsessions — Issue #28
|
||||||
|
|
||||||
|
## Что реализовано
|
||||||
|
- Slash-команда /newsession для создания игровых сессий прямо из Discord.
|
||||||
|
- Slash-команда /listsessions для просмотра предстоящих игр в сервере.
|
||||||
|
- DiscordPermissionChecker — проверка прав (owner / admin / manager).
|
||||||
|
- DiscordPlatformMessenger — реализация IPlatformMessenger для Discord (NetCord REST).
|
||||||
|
- Полная интеграция в DI (Program.cs).
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
- Vertical slice: каждая команда — отдельный файл (Command + Handler).
|
||||||
|
- Platform-agnostic SQL: используются колонки platform, external_group_id, external_user_id.
|
||||||
|
- Рендеринг переиспользует существующий DiscordSessionBatchRenderer.
|
||||||
|
|
||||||
|
## TDD
|
||||||
|
- 212 тестов, все зелёные.
|
||||||
|
- Source-level тесты проверяют паттерны: Dapper, Npgsql, транзакции, CancellationToken, платформенную нейтральность.
|
||||||
|
|
||||||
|
## Версия
|
||||||
|
- Minor bump: 2.3.0 → 2.4.0
|
||||||
|
- Синхронизировано: Directory.Build.props, compose.yaml, deploy.yml, NavMenu.razor.
|
||||||
|
|
||||||
|
Closes #28
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## 🛠 Patch 2.4.0 — Discord /newsession и /listsessions
|
||||||
|
|
||||||
|
Реализованы slash-команды Discord для создания сессий и просмотра расписания без Web Dashboard.
|
||||||
|
|
||||||
|
## 🧩 Что вошло в релиз
|
||||||
|
- src/GmRelay.DiscordBot/Features/Sessions/DiscordNewSessionCommand.cs — slash-команда /newsession с параметрами (title, time, seats, link)
|
||||||
|
- src/GmRelay.DiscordBot/Features/Sessions/DiscordNewSessionHandler.cs — handler создания batch + session в БД
|
||||||
|
- src/GmRelay.DiscordBot/Features/Sessions/DiscordListSessionsCommand.cs — slash-команда /listsessions
|
||||||
|
- src/GmRelay.DiscordBot/Features/Sessions/DiscordListSessionsHandler.cs — handler запроса активных сессий с embed-рендерингом
|
||||||
|
- src/GmRelay.DiscordBot/Infrastructure/Discord/DiscordPermissionChecker.cs — проверка прав через Discord permissions bitflag (Administrator = 0x8)
|
||||||
|
- src/GmRelay.DiscordBot/Infrastructure/Discord/DiscordPlatformMessenger.cs — реализация IPlatformMessenger для Discord через NetCord REST
|
||||||
|
- src/GmRelay.DiscordBot/Program.cs — регистрация DI: handlers, permission checker, messenger
|
||||||
|
- ests/GmRelay.Bot.Tests/Discord/ — 20+ TDD-тестов на парсинг, права, структуру, DI, рендеринг
|
||||||
|
- Синхронизированы версии: Directory.Build.props, NavMenu.razor, compose.yaml, deploy.yml → 2.4.0
|
||||||
|
|
||||||
|
## 🗺 Что это даёт
|
||||||
|
- Мастера (GM) могут создавать сессии прямо из Discord, не заходя в Web.
|
||||||
|
- Участники сервера видят расписание через /listsessions.
|
||||||
|
- Единая PostgreSQL модель для Telegram и Discord — никакого дублирования данных.
|
||||||
|
|
||||||
|
## 📦 Версия и деплой
|
||||||
|
- версия обновлена до 2.4.0
|
||||||
|
- Docker-образы используют тег 2.4.0
|
||||||
File diff suppressed because it is too large
Load Diff
+1
-6
@@ -1,6 +1,7 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using GmRelay.Bot.Features.Notifications;
|
using GmRelay.Bot.Features.Notifications;
|
||||||
using GmRelay.Shared.Domain;
|
using GmRelay.Shared.Domain;
|
||||||
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
using GmRelay.Shared.Platform;
|
using GmRelay.Shared.Platform;
|
||||||
using GmRelay.Shared.Rendering;
|
using GmRelay.Shared.Rendering;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
@@ -17,12 +18,6 @@ internal sealed record AwaitingProposalDto(
|
|||||||
Guid Id, Guid SessionId, string Title, DateTime CurrentScheduledAt,
|
Guid Id, Guid SessionId, string Title, DateTime CurrentScheduledAt,
|
||||||
Guid BatchId, int? BatchMessageId, long TelegramChatId, int? ThreadId, string NotificationMode);
|
Guid BatchId, int? BatchMessageId, long TelegramChatId, int? ThreadId, string NotificationMode);
|
||||||
|
|
||||||
internal sealed record VoteParticipantDto(
|
|
||||||
Guid PlayerId,
|
|
||||||
string DisplayName,
|
|
||||||
string? TelegramUsername,
|
|
||||||
long TelegramId = 0);
|
|
||||||
|
|
||||||
// ── Handler ──────────────────────────────────────────────────────────
|
// ── Handler ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using GmRelay.Shared.Domain;
|
using GmRelay.Shared.Domain;
|
||||||
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
using GmRelay.Shared.Platform;
|
using GmRelay.Shared.Platform;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Dapper;
|
|||||||
using GmRelay.Bot.Features.Notifications;
|
using GmRelay.Bot.Features.Notifications;
|
||||||
using GmRelay.Bot.Infrastructure.Scheduling;
|
using GmRelay.Bot.Infrastructure.Scheduling;
|
||||||
using GmRelay.Shared.Domain;
|
using GmRelay.Shared.Domain;
|
||||||
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
using GmRelay.Shared.Platform;
|
using GmRelay.Shared.Platform;
|
||||||
using GmRelay.Shared.Rendering;
|
using GmRelay.Shared.Rendering;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
|
|
||||||
|
public sealed record RescheduleOptionDto(
|
||||||
|
Guid OptionId,
|
||||||
|
int DisplayOrder,
|
||||||
|
DateTimeOffset ProposedAt);
|
||||||
|
|
||||||
|
public sealed record RescheduleOptionVoteDto(
|
||||||
|
Guid OptionId,
|
||||||
|
Guid PlayerId,
|
||||||
|
string DisplayName,
|
||||||
|
string? TelegramUsername);
|
||||||
|
|
||||||
|
public sealed record RescheduleOptionVoteCount(
|
||||||
|
Guid OptionId,
|
||||||
|
int VoteCount);
|
||||||
|
|
||||||
|
public sealed record VoteParticipantDto(
|
||||||
|
Guid PlayerId,
|
||||||
|
string DisplayName,
|
||||||
|
string? TelegramUsername,
|
||||||
|
long TelegramId = 0);
|
||||||
+10
-10
@@ -1,13 +1,13 @@
|
|||||||
namespace GmRelay.Bot.Features.Sessions.RescheduleSession;
|
namespace GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
|
|
||||||
internal enum RescheduleVoteOutcome
|
public enum RescheduleVoteOutcome
|
||||||
{
|
{
|
||||||
Pending,
|
Pending,
|
||||||
Rejected,
|
Rejected,
|
||||||
Approved
|
Approved
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record RescheduleVoteDecision(
|
public sealed record RescheduleVoteDecision(
|
||||||
RescheduleVoteOutcome Outcome,
|
RescheduleVoteOutcome Outcome,
|
||||||
string Reason,
|
string Reason,
|
||||||
Guid? SelectedOptionId = null,
|
Guid? SelectedOptionId = null,
|
||||||
@@ -15,7 +15,7 @@ internal sealed record RescheduleVoteDecision(
|
|||||||
bool ShouldRescheduleSession = false,
|
bool ShouldRescheduleSession = false,
|
||||||
bool ShouldResetParticipantRsvps = false);
|
bool ShouldResetParticipantRsvps = false);
|
||||||
|
|
||||||
internal static class RescheduleVoteRules
|
public static class RescheduleVoteRules
|
||||||
{
|
{
|
||||||
public static RescheduleVoteDecision SelectWinner(IReadOnlyList<RescheduleOptionVoteCount> voteCounts)
|
public static RescheduleVoteDecision SelectWinner(IReadOnlyList<RescheduleOptionVoteCount> voteCounts)
|
||||||
{
|
{
|
||||||
@@ -49,8 +49,8 @@ internal static class RescheduleVoteRules
|
|||||||
{
|
{
|
||||||
return new RescheduleVoteDecision(
|
return new RescheduleVoteDecision(
|
||||||
Outcome: RescheduleVoteOutcome.Rejected,
|
Outcome: RescheduleVoteOutcome.Rejected,
|
||||||
Reason: "\u041e\u0434\u0438\u043d \u0438\u0437 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432 \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b \u043f\u0435\u0440\u0435\u043d\u043e\u0441.",
|
Reason: "Один из участников отклонил перенос.",
|
||||||
CallbackText: "\u0412\u044b \u043f\u0440\u043e\u0433\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043b\u0438 \u043f\u0440\u043e\u0442\u0438\u0432 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0430.");
|
CallbackText: "Вы проголосовали против переноса.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var everyoneApproved = approvedParticipants == totalParticipants;
|
var everyoneApproved = approvedParticipants == totalParticipants;
|
||||||
@@ -58,11 +58,11 @@ internal static class RescheduleVoteRules
|
|||||||
return new RescheduleVoteDecision(
|
return new RescheduleVoteDecision(
|
||||||
Outcome: everyoneApproved ? RescheduleVoteOutcome.Approved : RescheduleVoteOutcome.Pending,
|
Outcome: everyoneApproved ? RescheduleVoteOutcome.Approved : RescheduleVoteOutcome.Pending,
|
||||||
Reason: everyoneApproved
|
Reason: everyoneApproved
|
||||||
? "\u0412\u0441\u0435 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u044b."
|
? "Все участники согласны."
|
||||||
: "\u0413\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442\u0441\u044f.",
|
: "Голосование продолжается.",
|
||||||
CallbackText: everyoneApproved
|
CallbackText: everyoneApproved
|
||||||
? "\u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043d\u043e\u0441! \u0412\u0441\u0435 \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u044b \u2014 \u0432\u0440\u0435\u043c\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043e."
|
? "Вы подтвердили перенос! Все согласны — время обновлено."
|
||||||
: "\u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043d\u043e\u0441!",
|
: "Вы подтвердили перенос!",
|
||||||
ShouldRescheduleSession: everyoneApproved,
|
ShouldRescheduleSession: everyoneApproved,
|
||||||
ShouldResetParticipantRsvps: everyoneApproved);
|
ShouldResetParticipantRsvps: everyoneApproved);
|
||||||
}
|
}
|
||||||
+2
-17
@@ -1,9 +1,9 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using GmRelay.Shared.Domain;
|
using GmRelay.Shared.Domain;
|
||||||
|
|
||||||
namespace GmRelay.Bot.Features.Sessions.RescheduleSession;
|
namespace GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
|
|
||||||
internal sealed record RescheduleVotingInput(
|
public sealed record RescheduleVotingInput(
|
||||||
IReadOnlyList<DateTimeOffset> Options,
|
IReadOnlyList<DateTimeOffset> Options,
|
||||||
DateTimeOffset Deadline)
|
DateTimeOffset Deadline)
|
||||||
{
|
{
|
||||||
@@ -93,18 +93,3 @@ internal sealed record RescheduleVotingInput(
|
|||||||
|| normalized.StartsWith("до:", StringComparison.Ordinal);
|
|| normalized.StartsWith("до:", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record RescheduleOptionDto(
|
|
||||||
Guid OptionId,
|
|
||||||
int DisplayOrder,
|
|
||||||
DateTimeOffset ProposedAt);
|
|
||||||
|
|
||||||
internal sealed record RescheduleOptionVoteDto(
|
|
||||||
Guid OptionId,
|
|
||||||
Guid PlayerId,
|
|
||||||
string DisplayName,
|
|
||||||
string? TelegramUsername);
|
|
||||||
|
|
||||||
internal sealed record RescheduleOptionVoteCount(
|
|
||||||
Guid OptionId,
|
|
||||||
int VoteCount);
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||||
using GmRelay.Bot.Features.Sessions.RescheduleSession;
|
using GmRelay.Bot.Features.Sessions.RescheduleSession;
|
||||||
using GmRelay.Shared.Domain;
|
using GmRelay.Shared.Domain;
|
||||||
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
using GmRelay.Shared.Rendering;
|
using GmRelay.Shared.Rendering;
|
||||||
using Telegram.Bot.Types.ReplyMarkups;
|
using Telegram.Bot.Types.ReplyMarkups;
|
||||||
using GmRelay.Bot.Infrastructure.Telegram;
|
using GmRelay.Bot.Infrastructure.Telegram;
|
||||||
|
|||||||
+1
@@ -1,4 +1,5 @@
|
|||||||
using GmRelay.Bot.Features.Sessions.RescheduleSession;
|
using GmRelay.Bot.Features.Sessions.RescheduleSession;
|
||||||
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
|
|
||||||
namespace GmRelay.Bot.Tests.Features.Sessions.RescheduleSession;
|
namespace GmRelay.Bot.Tests.Features.Sessions.RescheduleSession;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using GmRelay.Bot.Features.Sessions.RescheduleSession;
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
|
|
||||||
namespace GmRelay.Bot.Tests.Features.Sessions.RescheduleSession;
|
namespace GmRelay.Bot.Tests.Features.Sessions.RescheduleSession;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user