218 lines
8.9 KiB
C#
218 lines
8.9 KiB
C#
using Dapper;
|
||
using GmRelay.Shared.Domain;
|
||
using GmRelay.Shared.Rendering;
|
||
using Npgsql;
|
||
using Telegram.Bot;
|
||
|
||
namespace GmRelay.Bot.Features.Sessions.CreateSession;
|
||
|
||
public sealed record LeaveSessionCommand(
|
||
Guid SessionId,
|
||
long TelegramUserId,
|
||
string CallbackQueryId,
|
||
long ChatId,
|
||
int MessageId);
|
||
|
||
internal sealed record LeaveSessionInfoDto(string Title, Guid BatchId, string Status, int? MaxPlayers);
|
||
internal sealed record LeaveSessionParticipantDto(Guid ParticipantRowId, string DisplayName, string RegistrationStatus);
|
||
internal sealed record LeaveSessionPromotionDto(Guid ParticipantRowId, string DisplayName);
|
||
|
||
public sealed class LeaveSessionHandler(
|
||
NpgsqlDataSource dataSource,
|
||
ITelegramBotClient bot,
|
||
ILogger<LeaveSessionHandler> logger)
|
||
{
|
||
public async Task HandleAsync(LeaveSessionCommand command, CancellationToken ct)
|
||
{
|
||
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
||
await using var transaction = await connection.BeginTransactionAsync(ct);
|
||
var transactionCommitted = false;
|
||
|
||
try
|
||
{
|
||
var session = await connection.QuerySingleOrDefaultAsync<LeaveSessionInfoDto>(
|
||
"""
|
||
SELECT title AS Title,
|
||
batch_id AS BatchId,
|
||
status AS Status,
|
||
max_players AS MaxPlayers
|
||
FROM sessions
|
||
WHERE id = @SessionId
|
||
FOR UPDATE
|
||
""",
|
||
new { command.SessionId },
|
||
transaction);
|
||
|
||
if (session is null)
|
||
{
|
||
await transaction.RollbackAsync(ct);
|
||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
|
||
return;
|
||
}
|
||
|
||
if (SessionStatus.IsCancelled(session.Status))
|
||
{
|
||
await transaction.RollbackAsync(ct);
|
||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия уже отменена.", cancellationToken: ct);
|
||
return;
|
||
}
|
||
|
||
var participant = await connection.QuerySingleOrDefaultAsync<LeaveSessionParticipantDto>(
|
||
"""
|
||
SELECT sp.id AS ParticipantRowId,
|
||
p.display_name AS DisplayName,
|
||
sp.registration_status AS RegistrationStatus
|
||
FROM session_participants sp
|
||
JOIN players p ON p.id = sp.player_id
|
||
WHERE sp.session_id = @SessionId
|
||
AND p.telegram_id = @TelegramUserId
|
||
AND sp.is_gm = false
|
||
FOR UPDATE OF sp
|
||
""",
|
||
new { command.SessionId, command.TelegramUserId },
|
||
transaction);
|
||
|
||
if (participant is null)
|
||
{
|
||
await transaction.RollbackAsync(ct);
|
||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Вы не записаны на эту сессию.", cancellationToken: ct);
|
||
return;
|
||
}
|
||
|
||
await connection.ExecuteAsync(
|
||
"""
|
||
DELETE FROM session_participants
|
||
WHERE id = @ParticipantRowId
|
||
""",
|
||
new { participant.ParticipantRowId },
|
||
transaction);
|
||
|
||
var activeParticipantsAfterLeave = await connection.ExecuteScalarAsync<int>(
|
||
"""
|
||
SELECT COUNT(*)
|
||
FROM session_participants
|
||
WHERE session_id = @SessionId
|
||
AND is_gm = false
|
||
AND registration_status = @Active
|
||
""",
|
||
new { command.SessionId, Active = ParticipantRegistrationStatus.Active },
|
||
transaction);
|
||
|
||
var waitlistedParticipants = await connection.ExecuteScalarAsync<int>(
|
||
"""
|
||
SELECT COUNT(*)
|
||
FROM session_participants
|
||
WHERE session_id = @SessionId
|
||
AND is_gm = false
|
||
AND registration_status = @Waitlisted
|
||
""",
|
||
new { command.SessionId, Waitlisted = ParticipantRegistrationStatus.Waitlisted },
|
||
transaction);
|
||
|
||
string? promotedDisplayName = null;
|
||
if (SessionCapacityRules.ShouldPromoteAfterParticipantLeaves(
|
||
participant.RegistrationStatus,
|
||
session.MaxPlayers,
|
||
activeParticipantsAfterLeave,
|
||
waitlistedParticipants))
|
||
{
|
||
var promoted = await connection.QuerySingleAsync<LeaveSessionPromotionDto>(
|
||
"""
|
||
SELECT sp.id AS ParticipantRowId,
|
||
p.display_name AS DisplayName
|
||
FROM session_participants sp
|
||
JOIN players p ON p.id = sp.player_id
|
||
WHERE sp.session_id = @SessionId
|
||
AND sp.is_gm = false
|
||
AND sp.registration_status = @Waitlisted
|
||
ORDER BY sp.created_at ASC, sp.id ASC
|
||
LIMIT 1
|
||
FOR UPDATE OF sp
|
||
""",
|
||
new { command.SessionId, Waitlisted = ParticipantRegistrationStatus.Waitlisted },
|
||
transaction);
|
||
|
||
await connection.ExecuteAsync(
|
||
"""
|
||
UPDATE session_participants
|
||
SET registration_status = @Active,
|
||
rsvp_status = @Pending,
|
||
responded_at = NULL
|
||
WHERE id = @ParticipantRowId
|
||
""",
|
||
new
|
||
{
|
||
promoted.ParticipantRowId,
|
||
Active = ParticipantRegistrationStatus.Active,
|
||
Pending = RsvpStatus.Pending
|
||
},
|
||
transaction);
|
||
|
||
promotedDisplayName = promoted.DisplayName;
|
||
}
|
||
|
||
var batchSessions = (await connection.QueryAsync<SessionBatchDto>(
|
||
"""
|
||
SELECT id AS SessionId,
|
||
scheduled_at AS ScheduledAt,
|
||
status AS Status,
|
||
max_players AS MaxPlayers
|
||
FROM sessions
|
||
WHERE batch_id = @BatchId
|
||
ORDER BY scheduled_at
|
||
""",
|
||
new { session.BatchId },
|
||
transaction)).ToList();
|
||
|
||
var batchParticipants = (await connection.QueryAsync<ParticipantBatchDto>(
|
||
"""
|
||
SELECT sp.session_id AS SessionId,
|
||
p.display_name AS DisplayName,
|
||
p.telegram_username AS TelegramUsername,
|
||
sp.registration_status AS RegistrationStatus
|
||
FROM session_participants sp
|
||
JOIN players p ON sp.player_id = p.id
|
||
JOIN sessions s ON sp.session_id = s.id
|
||
WHERE s.batch_id = @BatchId AND sp.is_gm = false
|
||
ORDER BY sp.registration_status ASC, sp.created_at ASC, sp.responded_at ASC, p.created_at ASC
|
||
""",
|
||
new { session.BatchId },
|
||
transaction)).ToList();
|
||
|
||
await transaction.CommitAsync(ct);
|
||
transactionCommitted = true;
|
||
|
||
var renderResult = SessionBatchRenderer.Render(session.Title, batchSessions, batchParticipants);
|
||
|
||
await bot.EditMessageText(
|
||
chatId: command.ChatId,
|
||
messageId: command.MessageId,
|
||
text: renderResult.Text,
|
||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||
replyMarkup: renderResult.Markup,
|
||
cancellationToken: ct);
|
||
|
||
var callbackText = participant.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted
|
||
? "Вы удалены из листа ожидания."
|
||
: promotedDisplayName is null
|
||
? "Вы отписались от сессии."
|
||
: $"Вы отписались от сессии. Место получил(а) {promotedDisplayName}.";
|
||
|
||
await bot.AnswerCallbackQuery(command.CallbackQueryId, callbackText, cancellationToken: ct);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogError(ex, "Ошибка при самостоятельной отмене записи на сессию {SessionId}", command.SessionId);
|
||
if (!transactionCommitted)
|
||
{
|
||
await transaction.RollbackAsync(ct);
|
||
}
|
||
|
||
var errorText = transactionCommitted
|
||
? "Запись снята, но не удалось обновить сообщение расписания."
|
||
: "Произошла ошибка при отмене записи.";
|
||
await bot.AnswerCallbackQuery(command.CallbackQueryId, errorText, cancellationToken: ct);
|
||
}
|
||
}
|
||
}
|