Files
GmRelayBot/src/GmRelay.Shared/Features/Confirmation/SendConfirmation/SendConfirmationHandler.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

218 lines
7.2 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.SendConfirmation;
internal sealed record ConfirmationSessionRow(
Guid Id,
string Title,
DateTime ScheduledAt,
Guid GroupId,
string Platform,
string ExternalGroupId,
string DisplayName,
string? ExternalChannelId,
int? ThreadId,
string NotificationMode);
internal sealed record ConfirmationParticipantRow(
string Platform,
string ExternalUserId,
string DisplayName,
string? ExternalUsername,
string RsvpStatus,
string RegistrationStatus,
bool IsGm);
public sealed class SendConfirmationHandler(
NpgsqlDataSource dataSource,
IPlatformMessenger messenger,
PlatformDirectNotificationSender directSender,
ILogger<SendConfirmationHandler> logger) : ISendConfirmationHandler
{
public async Task HandleAsync(Guid sessionId, CancellationToken ct)
{
await using var connection = await dataSource.OpenConnectionAsync(ct);
var session = await connection.QuerySingleOrDefaultAsync<ConfirmationSessionRow>(
"""
SELECT s.id,
s.title,
s.scheduled_at AS ScheduledAt,
s.group_id AS GroupId,
g.platform AS Platform,
g.external_group_id AS ExternalGroupId,
g.name AS DisplayName,
g.external_channel_id AS ExternalChannelId,
s.thread_id AS ThreadId,
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;
}
var participants = (await connection.QueryAsync<ConfirmationParticipantRow>(
"""
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.created_at ASC
""",
new { SessionId = sessionId, Active = ParticipantRegistrationStatus.Active }))
.Select(ToParticipant)
.ToList();
if (participants.Count == 0)
{
logger.LogWarning("Session {SessionId} has no non-GM participants", sessionId);
return;
}
var group = CreateGroup(session);
var message = await messenger.SendConfirmationRequestAsync(
new PlatformConfirmationRequest(
group,
session.Id,
session.Title,
session.ScheduledAt,
participants),
ct);
await connection.ExecuteAsync(
"""
UPDATE sessions
SET status = @Status,
confirmation_message_id = @MessageId,
confirmation_sent_at = now(),
updated_at = now()
WHERE id = @SessionId
AND confirmation_sent_at IS NULL
""",
new
{
SessionId = sessionId,
Status = SessionStatus.ConfirmationSent,
MessageId = TryGetTelegramMessageId(message)
});
await PersistPlatformMessageAsync(
connection,
message,
session.GroupId,
session.Id,
batchId: null,
purpose: "confirmation");
var mode = SessionNotificationModeExtensions.FromDatabaseValue(session.NotificationMode);
if (mode.ShouldSendDirectMessages())
{
await directSender.SendAsync(
PlatformDirectSessionNotificationKind.ConfirmationRequest,
participants.Select(p => p.User),
session.Id,
session.Title,
session.ScheduledAt,
joinLink: null,
actorDisplayName: null,
reason: null,
ct);
}
logger.LogInformation(
"Confirmation sent for session {SessionId} ({Title}), platform={Platform}, message_id={MessageId}",
sessionId,
session.Title,
message.Platform,
message.ExternalMessageId);
}
private static PlatformSessionParticipant ToParticipant(ConfirmationParticipantRow row) =>
new(
new PlatformUser(
ParsePlatform(row.Platform),
row.ExternalUserId,
row.DisplayName,
row.ExternalUsername),
row.RsvpStatus,
row.RegistrationStatus,
row.IsGm);
private static PlatformGroup CreateGroup(ConfirmationSessionRow row) =>
new(
ParsePlatform(row.Platform),
row.ExternalGroupId,
row.DisplayName,
row.ExternalChannelId,
row.ThreadId?.ToString(CultureInfo.InvariantCulture));
private static PlatformKind ParsePlatform(string platform) =>
Enum.Parse<PlatformKind>(platform, ignoreCase: true);
private static int? TryGetTelegramMessageId(PlatformMessageRef message) =>
message.Platform == PlatformKind.Telegram &&
int.TryParse(message.ExternalMessageId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var messageId)
? messageId
: null;
private static Task PersistPlatformMessageAsync(
NpgsqlConnection connection,
PlatformMessageRef message,
Guid groupId,
Guid? sessionId,
Guid? batchId,
string purpose) =>
connection.ExecuteAsync(
"""
INSERT INTO platform_messages (
platform,
group_id,
batch_id,
session_id,
external_channel_id,
external_thread_id,
external_message_id,
purpose)
VALUES (
@Platform,
@GroupId,
@BatchId,
@SessionId,
@ExternalChannelId,
@ExternalThreadId,
@ExternalMessageId,
@Purpose)
""",
new
{
Platform = message.Platform.ToString(),
GroupId = groupId,
BatchId = batchId,
SessionId = sessionId,
ExternalChannelId = message.ExternalGroupId,
message.ExternalThreadId,
message.ExternalMessageId,
Purpose = purpose
});
}