040b0a3cdb
PR Checks / test-and-build (pull_request) Failing after 13m15s
- Добавлены миграции 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>
148 lines
6.4 KiB
C#
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);
|
|
}
|