refactor: make session join leave platform-neutral
PR Checks / test-and-build (pull_request) Successful in 5m3s

Convert join/leave interaction commands to PlatformUser, PlatformGroup, and PlatformMessageRef. Persist and look up participants by platform identity while keeping Telegram callbacks intact. Add V017 migration and TDD coverage. Bump version to 2.1.1.
This commit is contained in:
2026-05-18 13:30:48 +03:00
parent cb515b0e05
commit e791fc2f4a
12 changed files with 803 additions and 56 deletions
@@ -3,16 +3,15 @@ using GmRelay.Shared.Domain;
using GmRelay.Shared.Platform;
using GmRelay.Shared.Rendering;
using Npgsql;
using GmRelay.Bot.Infrastructure.Telegram;
namespace GmRelay.Bot.Features.Sessions.CreateSession;
public sealed record LeaveSessionCommand(
Guid SessionId,
long TelegramUserId,
string CallbackQueryId,
long ChatId,
int MessageId);
PlatformUser User,
string InteractionId,
PlatformGroup Group,
PlatformMessageRef ScheduleMessage);
internal sealed record LeaveSessionInfoDto(string Title, Guid BatchId, string Status, int? MaxPlayers);
internal sealed record LeaveSessionParticipantDto(Guid ParticipantRowId, string DisplayName, string RegistrationStatus);
@@ -47,17 +46,19 @@ public sealed class LeaveSessionHandler(
if (session is null)
{
await transaction.RollbackAsync(ct);
await AnswerAsync(command.CallbackQueryId, "Сессия не найдена.", ct);
await AnswerAsync(command.InteractionId, "Сессия не найдена.", ct);
return;
}
if (SessionStatus.IsCancelled(session.Status))
{
await transaction.RollbackAsync(ct);
await AnswerAsync(command.CallbackQueryId, "Сессия уже отменена.", ct);
await AnswerAsync(command.InteractionId, "Сессия уже отменена.", ct);
return;
}
var platform = command.User.Platform.ToString();
var participant = await connection.QuerySingleOrDefaultAsync<LeaveSessionParticipantDto>(
"""
SELECT sp.id AS ParticipantRowId,
@@ -66,17 +67,18 @@ public sealed class LeaveSessionHandler(
FROM session_participants sp
JOIN players p ON p.id = sp.player_id
WHERE sp.session_id = @SessionId
AND p.telegram_id = @TelegramUserId
AND p.platform = @Platform
AND p.external_user_id = @ExternalUserId
AND sp.is_gm = false
FOR UPDATE OF sp
""",
new { command.SessionId, command.TelegramUserId },
new { command.SessionId, Platform = platform, command.User.ExternalUserId },
transaction);
if (participant is null)
{
await transaction.RollbackAsync(ct);
await AnswerAsync(command.CallbackQueryId, "Вы не записаны на эту сессию.", ct);
await AnswerAsync(command.InteractionId, "Вы не записаны на эту сессию.", ct);
return;
}
@@ -170,7 +172,7 @@ public sealed class LeaveSessionHandler(
"""
SELECT sp.session_id AS SessionId,
p.display_name AS DisplayName,
p.telegram_username AS TelegramUsername,
COALESCE(p.external_username, p.telegram_username) AS TelegramUsername,
sp.registration_status AS RegistrationStatus
FROM session_participants sp
JOIN players p ON sp.player_id = p.id
@@ -187,9 +189,9 @@ public sealed class LeaveSessionHandler(
var view = SessionBatchViewBuilder.Build(session.Title, batchSessions, batchParticipants);
await messenger.UpdateScheduleAsync(
new PlatformScheduleMessage(
TelegramPlatformIds.Group(command.ChatId),
command.Group,
view,
TelegramPlatformIds.Message(command.ChatId, threadId: null, command.MessageId)),
command.ScheduleMessage),
ct);
var callbackText = participant.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted
@@ -198,7 +200,7 @@ public sealed class LeaveSessionHandler(
? "Вы отписались от сессии."
: $"Вы отписались от сессии. Место получил(а) {promotedDisplayName}.";
await AnswerAsync(command.CallbackQueryId, callbackText, ct);
await AnswerAsync(command.InteractionId, callbackText, ct);
}
catch (Exception ex)
{
@@ -211,10 +213,10 @@ public sealed class LeaveSessionHandler(
var errorText = transactionCommitted
? "Запись снята, но не удалось обновить сообщение расписания."
: "Произошла ошибка при отмене записи.";
await AnswerAsync(command.CallbackQueryId, errorText, ct);
await AnswerAsync(command.InteractionId, errorText, ct);
}
}
private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct) =>
messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text), ct);
private Task AnswerAsync(string interactionId, string text, CancellationToken ct) =>
messenger.AnswerInteractionAsync(new PlatformInteractionReply(interactionId, text), ct);
}