using Dapper; using GmRelay.Bot.Features.Notifications; using GmRelay.Shared.Domain; using Npgsql; using Telegram.Bot; using Telegram.Bot.Types.ReplyMarkups; namespace GmRelay.Bot.Features.Confirmation.SendConfirmation; // ── DTOs for Dapper mapping ────────────────────────────────────────── internal sealed record SessionInfo( Guid Id, string Title, DateTime ScheduledAt, Guid GroupId, long TelegramChatId, string NotificationMode); internal sealed record ParticipantInfo( long TelegramId, string DisplayName, string? TelegramUsername); // ── Handler ────────────────────────────────────────────────────────── /// /// Sends the interactive confirmation message (inline keyboard) to the group chat. /// Called by SessionSchedulerService at T-24h. /// public sealed class SendConfirmationHandler( NpgsqlDataSource dataSource, ITelegramBotClient bot, DirectSessionNotificationSender directSender, ILogger logger) { public async Task HandleAsync(Guid sessionId, CancellationToken ct) { await using var connection = await dataSource.OpenConnectionAsync(ct); // 1. Load session + group info var session = await connection.QuerySingleOrDefaultAsync( """ SELECT s.id, s.title, s.scheduled_at AS ScheduledAt, s.group_id AS GroupId, g.telegram_chat_id AS TelegramChatId, s.notification_mode AS NotificationMode FROM sessions s JOIN game_groups g ON g.id = s.group_id WHERE s.id = @SessionId AND s.status = @Planned """, new { SessionId = sessionId, Planned = SessionStatus.Planned }); if (session is null) { logger.LogWarning("Session {SessionId} not found or not in Planned status", sessionId); return; } // 2. Load non-GM participants var participants = (await connection.QueryAsync( """ SELECT p.telegram_id AS TelegramId, p.display_name AS DisplayName, p.telegram_username AS TelegramUsername 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 = @Active """, new { SessionId = sessionId, Active = ParticipantRegistrationStatus.Active })).ToList(); if (participants.Count == 0) { logger.LogWarning("Session {SessionId} has no non-GM participants", sessionId); return; } // 3. Build confirmation message var playerList = string.Join("\n", participants.Select(p => $" ⏳ {FormatPlayerName(p)}")); var text = $""" 🎲 Подтвердите участие в «{session.Title}» 📅 {session.ScheduledAt.FormatMoscow()} (МСК) {playerList} Статус: ожидаем подтверждения (0/{participants.Count}) """; var keyboard = new InlineKeyboardMarkup([ [ InlineKeyboardButton.WithCallbackData("✅ Буду", $"rsvp:confirm:{sessionId}"), InlineKeyboardButton.WithCallbackData("❌ Не смогу", $"rsvp:decline:{sessionId}") ] ]); // 4. Send to group var message = await bot.SendMessage( chatId: session.TelegramChatId, text: text, replyMarkup: keyboard, cancellationToken: ct); // 5. Update session status and store message ID await connection.ExecuteAsync( """ UPDATE sessions SET status = @Status, confirmation_message_id = @MessageId, updated_at = now() WHERE id = @SessionId """, new { SessionId = sessionId, Status = SessionStatus.ConfirmationSent, MessageId = message.MessageId }); var mode = SessionNotificationModeExtensions.FromDatabaseValue(session.NotificationMode); if (mode.ShouldSendDirectMessages()) { var directText = $""" 🎲 Подтвердите участие в игре 📌 {System.Net.WebUtility.HtmlEncode(session.Title)} 📅 {session.ScheduledAt.FormatMoscow()} (МСК) Ответьте кнопкой в групповом сообщении расписания. """; await directSender.SendAsync( participants.Select(p => new DirectNotificationRecipient(p.TelegramId, p.DisplayName)), directText, "confirmation", sessionId, ct); } logger.LogInformation( "Confirmation sent for session {SessionId} ({Title}), message_id={MessageId}", sessionId, session.Title, message.MessageId); } internal static string FormatPlayerName(ParticipantInfo p) => p.TelegramUsername is not null ? $"@{p.TelegramUsername}" : p.DisplayName; }