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.Reminders.SendJoinLink; internal sealed record JoinLinkSessionRow( Guid Id, Guid GroupId, string Title, string JoinLink, DateTime ScheduledAt, string Platform, string ExternalGroupId, string DisplayName, string? ExternalChannelId, int? ThreadId, string NotificationMode); internal sealed record JoinLinkPlayerRow( string Platform, string ExternalUserId, string DisplayName, string? ExternalUsername, string RsvpStatus, string RegistrationStatus, bool IsGm); public sealed class SendJoinLinkHandler( NpgsqlDataSource dataSource, IPlatformMessenger messenger, PlatformDirectNotificationSender directSender, ILogger logger) : ISendJoinLinkHandler { public async Task HandleAsync(Guid sessionId, CancellationToken ct) { await using var connection = await dataSource.OpenConnectionAsync(ct); var session = await connection.QuerySingleOrDefaultAsync( """ SELECT s.id, s.group_id AS GroupId, s.title, s.join_link AS JoinLink, s.scheduled_at AS ScheduledAt, 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 = @Confirmed AND ( (g.platform = 'Telegram' AND s.link_message_id IS NULL) OR ( g.platform <> 'Telegram' AND NOT EXISTS ( SELECT 1 FROM platform_messages pm WHERE pm.session_id = s.id AND pm.platform = g.platform AND pm.purpose = 'join_link' ) ) ) """, new { SessionId = sessionId, Confirmed = SessionStatus.Confirmed }); if (session is null) { logger.LogWarning("Session {SessionId} not eligible for join link", sessionId); return; } var players = (await connection.QueryAsync( """ 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.rsvp_status = @Confirmed AND sp.registration_status = @Active ORDER BY sp.created_at ASC """, new { SessionId = sessionId, Confirmed = RsvpStatus.Confirmed, Active = ParticipantRegistrationStatus.Active })) .Select(ToParticipant) .ToList(); var group = CreateGroup(session); var message = await messenger.SendJoinLinkNotificationAsync( new PlatformJoinLinkNotification( group, session.Id, session.Title, session.ScheduledAt, session.JoinLink, players), ct); await connection.ExecuteAsync( """ UPDATE sessions SET link_message_id = @MessageId, updated_at = now() WHERE id = @SessionId """, new { SessionId = sessionId, MessageId = TryGetTelegramMessageId(message) }); await PersistPlatformMessageAsync( connection, message, session.GroupId, session.Id, batchId: null, purpose: "join_link"); var mode = SessionNotificationModeExtensions.FromDatabaseValue(session.NotificationMode); if (mode.ShouldSendDirectMessages()) { await directSender.SendAsync( PlatformDirectSessionNotificationKind.JoinLink, players.Select(p => p.User), session.Id, session.Title, session.ScheduledAt, session.JoinLink, actorDisplayName: null, reason: null, ct); } logger.LogInformation( "Join link sent for session {SessionId} ({Title}), platform={Platform}, message_id={MessageId}", sessionId, session.Title, message.Platform, message.ExternalMessageId); } private static PlatformSessionParticipant ToParticipant(JoinLinkPlayerRow row) => new( new PlatformUser( ParsePlatform(row.Platform), row.ExternalUserId, row.DisplayName, row.ExternalUsername), row.RsvpStatus, row.RegistrationStatus, row.IsGm); private static PlatformGroup CreateGroup(JoinLinkSessionRow row) => new( ParsePlatform(row.Platform), row.ExternalGroupId, row.DisplayName, row.ExternalChannelId, row.ThreadId?.ToString(CultureInfo.InvariantCulture)); private static PlatformKind ParsePlatform(string platform) => Enum.Parse(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 }); }