Files
GmRelayBot/src/GmRelay.Bot/Features/Sessions/CreateSession/CancelSessionHandler.cs
T
Toutsu 040b0a3cdb
PR Checks / test-and-build (pull_request) Failing after 13m15s
refactor: завершить platform migration и удалить deprecated telegram_* scaffolding
- Добавлены миграции V024 (backfill + deprecation comments + calendar_subscriptions platform identity) и V025 (backfill proposed_by_external_user_id)
- Все Bot handlers переведены с telegram_id/chat_id на platform + external_*
- Shared handlers очищены от COALESCE fallback с telegram_* колонками
- DiscordBot очищен от COALESCE fallback
- Web SessionService и CalendarSubscriptionService переведены на external_*
- HandleRsvpHandler: убран legacy UNION с gm_telegram_id, теперь только group_managers
- RescheduleVotingFinalizer: переведен на external_username/external_user_id
- Tests: добавлены asserts для V024/V025
- Версия обновлена до 3.1.0

Bump version → 3.1.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:41:15 +03:00

148 lines
6.4 KiB
C#

using Dapper;
using GmRelay.Bot.Features.Notifications;
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 CancelSessionCommand(
Guid SessionId,
long TelegramUserId,
string CallbackQueryId,
long ChatId,
int? MessageThreadId,
int MessageId);
// DTOs for AOT compilation
internal sealed record CancelSessionInfoDto(string Title, Guid BatchId, int? BatchMessageId, bool CanManage, string NotificationMode);
public sealed class CancelSessionHandler(
NpgsqlDataSource dataSource,
IPlatformMessenger messenger,
DirectSessionNotificationSender directSender,
ILogger<CancelSessionHandler> logger)
{
public async Task HandleAsync(CancelSessionCommand command, CancellationToken ct)
{
await using var connection = await dataSource.OpenConnectionAsync(ct);
await using var transaction = await connection.BeginTransactionAsync(ct);
// 1. Проверяем, что запрос делает управляющий данной группы.
var session = await connection.QuerySingleOrDefaultAsync<CancelSessionInfoDto>(
"""
SELECT s.title AS Title,
s.batch_id AS BatchId,
s.batch_message_id AS BatchMessageId,
s.notification_mode AS NotificationMode,
EXISTS (
SELECT 1
FROM group_managers gm
JOIN players p ON p.id = gm.player_id
WHERE gm.group_id = s.group_id
AND p.platform = 'Telegram'
AND p.external_user_id = @ExternalUserId
) AS CanManage
FROM sessions s
WHERE s.id = @SessionId
""",
new { command.SessionId, ExternalUserId = command.TelegramUserId.ToString() }, transaction);
if (session == null)
{
await AnswerAsync(command.CallbackQueryId, "Сессия не найдена.", ct);
return;
}
if (!session.CanManage)
{
await AnswerAsync(command.CallbackQueryId, "Только owner или co-GM может отменять сессию.", ct, showAlert: true);
return;
}
// 2. Отменяем сессию
await connection.ExecuteAsync(
"UPDATE sessions SET status = @Status WHERE id = @Id",
new { Id = command.SessionId, Status = SessionStatus.Cancelled },
transaction);
// 3. Загружаем весь батч для перерисовки
var batchSessions = await connection.QueryAsync<SessionBatchDto>(
@"SELECT id as SessionId, scheduled_at as ScheduledAt, status as Status, max_players as MaxPlayers, join_link as JoinLink
FROM sessions
WHERE batch_id = @BatchId
ORDER BY scheduled_at",
new { BatchId = session.BatchId }, transaction);
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 { BatchId = session.BatchId }, transaction);
var directRecipients = (await connection.QueryAsync<DirectNotificationRecipient>(
"""
SELECT p.external_user_id::BIGINT AS TelegramId,
p.display_name AS DisplayName
FROM session_participants sp
JOIN players p ON sp.player_id = p.id
WHERE sp.session_id = @SessionId
AND sp.is_gm = false
AND sp.registration_status = @Active
""",
new { command.SessionId, Active = ParticipantRegistrationStatus.Active },
transaction)).ToList();
await transaction.CommitAsync(ct);
// 4. Перерисовываем сообщение
var view = SessionBatchViewBuilder.Build(session.Title, batchSessions.ToList(), batchParticipants.ToList());
try
{
var messageId = session.BatchMessageId ?? command.MessageId;
await messenger.UpdateScheduleAsync(
new PlatformScheduleMessage(
TelegramPlatformIds.Group(command.ChatId, command.MessageThreadId),
view,
TelegramPlatformIds.Message(command.ChatId, command.MessageThreadId, messageId)),
ct);
await AnswerAsync(command.CallbackQueryId, "Сессия отменена!", ct);
// Опционально: написать отдельное сообщение в чат
await messenger.SendGroupMessageAsync(
TelegramPlatformIds.Group(command.ChatId, command.MessageThreadId),
$"❌ <b>Внимание!</b> Сессия \"{System.Net.WebUtility.HtmlEncode(session.Title)}\" отменена.",
ct);
var mode = SessionNotificationModeExtensions.FromDatabaseValue(session.NotificationMode);
if (mode.ShouldSendDirectMessages())
{
await directSender.SendAsync(
directRecipients,
$"❌ <b>Сессия отменена</b>\n\n📌 <b>{System.Net.WebUtility.HtmlEncode(session.Title)}</b>",
"session-cancelled",
command.SessionId,
ct);
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to update batch message after cancelling session {SessionId}", command.SessionId);
await AnswerAsync(command.CallbackQueryId, "Ошибка при обновлении сообщения.", ct);
}
}
private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct, bool showAlert = false) =>
messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text, showAlert), ct);
}