Initial commit: GM-Relay Telegram Bot
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using Dapper;
|
||||
using GmRelay.Bot.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);
|
||||
|
||||
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,
|
||||
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
|
||||
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
|
||||
""",
|
||||
new { SessionId = sessionId })).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
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user