150 lines
5.4 KiB
C#
150 lines
5.4 KiB
C#
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 ──────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Sends the interactive confirmation message (inline keyboard) to the group chat.
|
|
/// Called by SessionSchedulerService at T-24h.
|
|
/// </summary>
|
|
public sealed class SendConfirmationHandler(
|
|
NpgsqlDataSource dataSource,
|
|
ITelegramBotClient bot,
|
|
DirectSessionNotificationSender directSender,
|
|
ILogger<SendConfirmationHandler> 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<SessionInfo>(
|
|
"""
|
|
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<ParticipantInfo>(
|
|
"""
|
|
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 = $"""
|
|
🎲 <b>Подтвердите участие в игре</b>
|
|
|
|
📌 <b>{System.Net.WebUtility.HtmlEncode(session.Title)}</b>
|
|
📅 {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;
|
|
}
|