316 lines
12 KiB
C#
316 lines
12 KiB
C#
using Dapper;
|
|
using GmRelay.Shared.Domain;
|
|
using Npgsql;
|
|
using Telegram.Bot;
|
|
using Telegram.Bot.Types.ReplyMarkups;
|
|
|
|
namespace GmRelay.Bot.Features.Confirmation.HandleRsvp;
|
|
|
|
public sealed record HandleRsvpCommand(
|
|
Guid SessionId,
|
|
long TelegramUserId,
|
|
string Status,
|
|
string CallbackQueryId,
|
|
long ChatId,
|
|
int MessageId);
|
|
|
|
internal sealed record RsvpCounts(int Total, int Confirmed, int Declined);
|
|
|
|
internal sealed record SessionContext(
|
|
string Title,
|
|
DateTime ScheduledAt,
|
|
string Status,
|
|
long GmTelegramId,
|
|
long TelegramChatId);
|
|
|
|
internal sealed record ParticipantRsvp(
|
|
long TelegramId,
|
|
string DisplayName,
|
|
string? TelegramUsername,
|
|
string RsvpStatus);
|
|
|
|
public sealed class HandleRsvpHandler(
|
|
NpgsqlDataSource dataSource,
|
|
ITelegramBotClient bot,
|
|
ILogger<HandleRsvpHandler> logger)
|
|
{
|
|
public async Task HandleAsync(HandleRsvpCommand command, CancellationToken ct)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
await using var transaction = await connection.BeginTransactionAsync(ct);
|
|
|
|
var participantExists = await connection.ExecuteScalarAsync<bool>(
|
|
"""
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM session_participants sp
|
|
JOIN players p ON p.id = sp.player_id
|
|
WHERE sp.session_id = @SessionId
|
|
AND p.telegram_id = @TelegramUserId
|
|
AND sp.is_gm = false
|
|
AND sp.registration_status = @Active
|
|
)
|
|
""",
|
|
new { command.SessionId, command.TelegramUserId, Active = ParticipantRegistrationStatus.Active },
|
|
transaction);
|
|
|
|
if (!participantExists)
|
|
{
|
|
await bot.AnswerCallbackQuery(
|
|
callbackQueryId: command.CallbackQueryId,
|
|
text: "Вы не являетесь участником этой сессии.",
|
|
cancellationToken: ct);
|
|
return;
|
|
}
|
|
|
|
var updated = await connection.ExecuteAsync(
|
|
"""
|
|
UPDATE session_participants
|
|
SET rsvp_status = @Status,
|
|
responded_at = now()
|
|
WHERE session_id = @SessionId
|
|
AND player_id = (SELECT id FROM players WHERE telegram_id = @TelegramUserId)
|
|
AND registration_status = @Active
|
|
AND rsvp_status != @Status
|
|
""",
|
|
new { command.SessionId, command.TelegramUserId, command.Status, Active = ParticipantRegistrationStatus.Active },
|
|
transaction);
|
|
|
|
if (updated == 0)
|
|
{
|
|
var alreadyText = command.Status == RsvpStatus.Confirmed
|
|
? "Вы уже подтвердили участие."
|
|
: "Вы уже отказались от участия.";
|
|
|
|
await bot.AnswerCallbackQuery(
|
|
callbackQueryId: command.CallbackQueryId,
|
|
text: alreadyText,
|
|
cancellationToken: ct);
|
|
return;
|
|
}
|
|
|
|
var session = await connection.QuerySingleAsync<SessionContext>(
|
|
"""
|
|
SELECT s.title,
|
|
s.scheduled_at AS ScheduledAt,
|
|
s.status AS Status,
|
|
g.gm_telegram_id AS GmTelegramId,
|
|
g.telegram_chat_id AS TelegramChatId
|
|
FROM sessions s
|
|
JOIN game_groups g ON g.id = s.group_id
|
|
WHERE s.id = @SessionId
|
|
""",
|
|
new { command.SessionId },
|
|
transaction);
|
|
|
|
if (command.Status == RsvpStatus.Declined)
|
|
{
|
|
var decision = RsvpFlowRules.Evaluate(command.Status, session.Status, totalParticipants: 0, confirmedParticipants: 0);
|
|
|
|
if (decision.ShouldRevertSessionToConfirmationSent)
|
|
{
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
UPDATE sessions
|
|
SET status = @ConfirmationSent, updated_at = now()
|
|
WHERE id = @SessionId AND status = @Confirmed
|
|
""",
|
|
new
|
|
{
|
|
command.SessionId,
|
|
ConfirmationSent = SessionStatus.ConfirmationSent,
|
|
Confirmed = SessionStatus.Confirmed
|
|
},
|
|
transaction);
|
|
}
|
|
|
|
var declinedPlayer = await connection.QuerySingleAsync<string>(
|
|
"SELECT display_name FROM players WHERE telegram_id = @TelegramUserId",
|
|
new { command.TelegramUserId },
|
|
transaction);
|
|
|
|
await transaction.CommitAsync(ct);
|
|
|
|
try
|
|
{
|
|
await bot.SendMessage(
|
|
chatId: session.GmTelegramId,
|
|
text: $"🚨 Отмена! {declinedPlayer} не сможет прийти на игру «{session.Title}».",
|
|
cancellationToken: ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to send decline alert to GM for session {SessionId}", command.SessionId);
|
|
}
|
|
|
|
await bot.AnswerCallbackQuery(
|
|
callbackQueryId: command.CallbackQueryId,
|
|
text: decision.CallbackText,
|
|
cancellationToken: ct);
|
|
}
|
|
else
|
|
{
|
|
var counts = await connection.QuerySingleAsync<RsvpCounts>(
|
|
"""
|
|
SELECT
|
|
count(*) AS Total,
|
|
count(*) FILTER (WHERE rsvp_status = @Confirmed) AS Confirmed,
|
|
count(*) FILTER (WHERE rsvp_status = @Declined) AS Declined
|
|
FROM session_participants
|
|
WHERE session_id = @SessionId AND is_gm = false
|
|
AND registration_status = @Active
|
|
""",
|
|
new
|
|
{
|
|
command.SessionId,
|
|
Confirmed = RsvpStatus.Confirmed,
|
|
Declined = RsvpStatus.Declined,
|
|
Active = ParticipantRegistrationStatus.Active
|
|
},
|
|
transaction);
|
|
|
|
var decision = RsvpFlowRules.Evaluate(command.Status, session.Status, counts.Total, counts.Confirmed);
|
|
|
|
if (decision.ShouldMarkSessionConfirmed)
|
|
{
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
UPDATE sessions
|
|
SET status = @Confirmed, updated_at = now()
|
|
WHERE id = @SessionId
|
|
""",
|
|
new { command.SessionId, Confirmed = SessionStatus.Confirmed },
|
|
transaction);
|
|
}
|
|
|
|
await transaction.CommitAsync(ct);
|
|
|
|
if (decision.ShouldNotifyGroup)
|
|
{
|
|
try
|
|
{
|
|
await bot.SendMessage(
|
|
chatId: session.TelegramChatId,
|
|
text: $"🎉 Игра «{session.Title}» подтверждена! Все участники на месте.",
|
|
cancellationToken: ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to send group confirmation for session {SessionId}", command.SessionId);
|
|
}
|
|
}
|
|
|
|
if (decision.ShouldNotifyGm)
|
|
{
|
|
try
|
|
{
|
|
await bot.SendMessage(
|
|
chatId: session.GmTelegramId,
|
|
text: $"✅ Все подтвердили участие в «{session.Title}» ({session.ScheduledAt.FormatMoscow()} МСК).",
|
|
cancellationToken: ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to send GM confirmation for session {SessionId}", command.SessionId);
|
|
}
|
|
}
|
|
|
|
await bot.AnswerCallbackQuery(
|
|
callbackQueryId: command.CallbackQueryId,
|
|
text: decision.CallbackText,
|
|
cancellationToken: ct);
|
|
}
|
|
|
|
await UpdateConfirmationMessage(command, session, ct);
|
|
}
|
|
|
|
private async Task UpdateConfirmationMessage(HandleRsvpCommand command, SessionContext session, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
var participants = (await connection.QueryAsync<ParticipantRsvp>(
|
|
"""
|
|
SELECT p.telegram_id AS TelegramId,
|
|
p.display_name AS DisplayName,
|
|
p.telegram_username AS TelegramUsername,
|
|
sp.rsvp_status AS RsvpStatus
|
|
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
|
|
ORDER BY sp.responded_at NULLS LAST
|
|
""",
|
|
new { command.SessionId, Active = ParticipantRegistrationStatus.Active })).ToList();
|
|
|
|
var confirmed = participants.Where(p => p.RsvpStatus == RsvpStatus.Confirmed).ToList();
|
|
var declined = participants.Where(p => p.RsvpStatus == RsvpStatus.Declined).ToList();
|
|
var pending = participants.Where(p => p.RsvpStatus == RsvpStatus.Pending).ToList();
|
|
|
|
var lines = new List<string>
|
|
{
|
|
$"🎲 Подтвердите участие в «{session.Title}»",
|
|
$"📅 {session.ScheduledAt.FormatMoscow()} (МСК)",
|
|
string.Empty
|
|
};
|
|
|
|
foreach (var participant in confirmed)
|
|
{
|
|
lines.Add($" ✅ {FormatName(participant)}");
|
|
}
|
|
|
|
foreach (var participant in declined)
|
|
{
|
|
lines.Add($" ❌ ~~{FormatName(participant)}~~");
|
|
}
|
|
|
|
foreach (var participant in pending)
|
|
{
|
|
lines.Add($" ⏳ {FormatName(participant)}");
|
|
}
|
|
|
|
lines.Add(string.Empty);
|
|
|
|
if (confirmed.Count == participants.Count)
|
|
{
|
|
lines.Add($"Статус: ✅ все подтвердили ({confirmed.Count}/{participants.Count})");
|
|
}
|
|
else if (declined.Count > 0)
|
|
{
|
|
lines.Add($"Статус: ⚠️ есть отказы ({confirmed.Count}/{participants.Count} подтвердили)");
|
|
}
|
|
else
|
|
{
|
|
lines.Add($"Статус: ожидаем подтверждения ({confirmed.Count}/{participants.Count})");
|
|
}
|
|
|
|
var text = string.Join("\n", lines);
|
|
|
|
var replyMarkup = confirmed.Count == participants.Count
|
|
? null
|
|
: new InlineKeyboardMarkup([
|
|
[
|
|
InlineKeyboardButton.WithCallbackData("✅ Буду", $"rsvp:confirm:{command.SessionId}"),
|
|
InlineKeyboardButton.WithCallbackData("❌ Не смогу", $"rsvp:decline:{command.SessionId}")
|
|
]
|
|
]);
|
|
|
|
await bot.EditMessageText(
|
|
chatId: command.ChatId,
|
|
messageId: command.MessageId,
|
|
text: text,
|
|
replyMarkup: replyMarkup,
|
|
cancellationToken: ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to update confirmation message for session {SessionId}", command.SessionId);
|
|
}
|
|
}
|
|
|
|
private static string FormatName(ParticipantRsvp participant) =>
|
|
participant.TelegramUsername is not null ? $"@{participant.TelegramUsername}" : participant.DisplayName;
|
|
}
|