Files
GmRelayBot/src/GmRelay.Shared/Features/Confirmation/HandleRsvp/HandleRsvpHandler.cs
T
Toutsu 040b0a3cdb
PR Checks / test-and-build (pull_request) Failing after 13m15s
refactor: завершить platform migration и удалить deprecated telegram_* scaffolding
- Добавлены миграции 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>
2026-05-26 16:41:15 +03:00

347 lines
12 KiB
C#

using System.Globalization;
using Dapper;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Features.Notifications;
using GmRelay.Shared.Platform;
using Microsoft.Extensions.Logging;
using Npgsql;
namespace GmRelay.Shared.Features.Confirmation.HandleRsvp;
public sealed record HandleRsvpCommand(
Guid SessionId,
PlatformUser User,
string Status,
string InteractionId,
PlatformGroup Group,
PlatformMessageRef ConfirmationMessage);
internal sealed record RsvpCounts(int Total, int Confirmed, int Declined);
internal sealed record RsvpSessionContext(
Guid GroupId,
string Title,
DateTime ScheduledAt,
string Status);
internal sealed record ParticipantRsvpRow(
string Platform,
string ExternalUserId,
string DisplayName,
string? ExternalUsername,
string RsvpStatus,
string RegistrationStatus,
bool IsGm);
internal sealed record RsvpRecipientRow(
string Platform,
string ExternalUserId,
string DisplayName,
string? ExternalUsername);
public sealed class HandleRsvpHandler(
NpgsqlDataSource dataSource,
IPlatformMessenger messenger,
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.platform = @Platform
AND p.external_user_id = @ExternalUserId
AND sp.is_gm = false
AND sp.registration_status = @Active
)
""",
new
{
command.SessionId,
Platform = command.User.Platform.ToString(),
command.User.ExternalUserId,
Active = ParticipantRegistrationStatus.Active
},
transaction);
if (!participantExists)
{
await messenger.AnswerInteractionAsync(
new PlatformInteractionReply(
command.InteractionId,
"Вы не являетесь участником этой сессии."),
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 platform = @Platform
AND external_user_id = @ExternalUserId
LIMIT 1
)
AND registration_status = @Active
AND rsvp_status != @Status
""",
new
{
command.SessionId,
command.Status,
Platform = command.User.Platform.ToString(),
command.User.ExternalUserId,
Active = ParticipantRegistrationStatus.Active
},
transaction);
if (updated == 0)
{
var alreadyText = command.Status == RsvpStatus.Confirmed
? "Вы уже подтвердили участие."
: "Вы уже отказались от участия.";
await messenger.AnswerInteractionAsync(
new PlatformInteractionReply(command.InteractionId, alreadyText),
ct);
return;
}
var session = await connection.QuerySingleAsync<RsvpSessionContext>(
"""
SELECT s.group_id AS GroupId,
s.title,
s.scheduled_at AS ScheduledAt,
s.status AS Status
FROM sessions s
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 gmRecipients = (await GetGmRecipientsAsync(connection, session.GroupId, transaction))
.ToList();
await transaction.CommitAsync(ct);
if (gmRecipients.Count > 0)
{
await messenger.SendRsvpOutcomeAsync(
new PlatformRsvpOutcomeNotification(
PlatformRsvpOutcomeKind.GmPlayerDeclined,
Group: null,
gmRecipients,
command.SessionId,
session.Title,
session.ScheduledAt,
ActorDisplayName: command.User.DisplayName),
ct);
}
await messenger.AnswerInteractionAsync(
new PlatformInteractionReply(command.InteractionId, decision.CallbackText),
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);
}
var gmRecipients = decision.ShouldNotifyGm
? (await GetGmRecipientsAsync(connection, session.GroupId, transaction)).ToList()
: [];
await transaction.CommitAsync(ct);
if (decision.ShouldNotifyGroup)
{
await messenger.SendRsvpOutcomeAsync(
new PlatformRsvpOutcomeNotification(
PlatformRsvpOutcomeKind.GroupAllConfirmed,
command.Group,
[],
command.SessionId,
session.Title,
session.ScheduledAt),
ct);
}
if (decision.ShouldNotifyGm && gmRecipients.Count > 0)
{
await messenger.SendRsvpOutcomeAsync(
new PlatformRsvpOutcomeNotification(
PlatformRsvpOutcomeKind.GmAllConfirmed,
Group: null,
gmRecipients,
command.SessionId,
session.Title,
session.ScheduledAt),
ct);
}
await messenger.AnswerInteractionAsync(
new PlatformInteractionReply(command.InteractionId, decision.CallbackText),
ct);
}
await UpdateConfirmationMessage(command, session, ct);
}
private async Task UpdateConfirmationMessage(
HandleRsvpCommand command,
RsvpSessionContext session,
CancellationToken ct)
{
try
{
await using var connection = await dataSource.OpenConnectionAsync(ct);
var participants = (await connection.QueryAsync<ParticipantRsvpRow>(
"""
SELECT p.platform AS Platform,
p.external_user_id AS ExternalUserId,
p.display_name AS DisplayName,
p.external_username AS ExternalUsername,
sp.rsvp_status AS RsvpStatus,
sp.registration_status AS RegistrationStatus,
sp.is_gm AS IsGm
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 }))
.Select(ToParticipant)
.ToList();
var disableActions = participants.Count > 0 &&
participants.All(participant => participant.RsvpStatus == RsvpStatus.Confirmed);
await messenger.UpdateConfirmationRequestAsync(
new PlatformRsvpMessageUpdate(
new PlatformConfirmationRequest(
command.Group,
command.SessionId,
session.Title,
session.ScheduledAt,
participants,
command.ConfirmationMessage),
disableActions),
ct);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to update confirmation message for session {SessionId}", command.SessionId);
}
}
private static async Task<IEnumerable<PlatformUser>> GetGmRecipientsAsync(
NpgsqlConnection connection,
Guid groupId,
NpgsqlTransaction transaction)
{
var rows = await connection.QueryAsync<RsvpRecipientRow>(
"""
SELECT DISTINCT
p.platform AS Platform,
p.external_user_id AS ExternalUserId,
p.display_name AS DisplayName,
p.external_username AS ExternalUsername
FROM group_managers gm
JOIN players p ON p.id = gm.player_id
WHERE gm.group_id = @GroupId
""",
new { GroupId = groupId },
transaction);
return rows.Select(row => new PlatformUser(
ParsePlatform(row.Platform),
row.ExternalUserId,
row.DisplayName,
row.ExternalUsername));
}
private static PlatformSessionParticipant ToParticipant(ParticipantRsvpRow row) =>
new(
new PlatformUser(
ParsePlatform(row.Platform),
row.ExternalUserId,
row.DisplayName,
row.ExternalUsername),
row.RsvpStatus,
row.RegistrationStatus,
row.IsGm);
private static PlatformKind ParsePlatform(string platform) =>
Enum.Parse<PlatformKind>(platform, ignoreCase: true);
}