feat(platform): route scheduler notifications through platform messenger
PR Checks / test-and-build (pull_request) Successful in 7m9s
PR Checks / test-and-build (pull_request) Successful in 7m9s
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
namespace GmRelay.Shared.Features.Reminders.SendJoinLink;
|
||||
|
||||
public interface ISendJoinLinkHandler
|
||||
{
|
||||
Task HandleAsync(Guid sessionId, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
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<SendJoinLinkHandler> logger) : ISendJoinLinkHandler
|
||||
{
|
||||
public async Task HandleAsync(Guid sessionId, CancellationToken ct)
|
||||
{
|
||||
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
||||
|
||||
var session = await connection.QuerySingleOrDefaultAsync<JoinLinkSessionRow>(
|
||||
"""
|
||||
SELECT s.id,
|
||||
s.group_id AS GroupId,
|
||||
s.title,
|
||||
s.join_link AS JoinLink,
|
||||
s.scheduled_at AS ScheduledAt,
|
||||
COALESCE(g.platform, 'Telegram') AS Platform,
|
||||
COALESCE(g.external_group_id, g.telegram_chat_id::TEXT) AS ExternalGroupId,
|
||||
g.name AS DisplayName,
|
||||
COALESCE(g.external_channel_id, g.telegram_chat_id::TEXT) 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 (
|
||||
(COALESCE(g.platform, 'Telegram') = 'Telegram' AND s.link_message_id IS NULL)
|
||||
OR (
|
||||
COALESCE(g.platform, 'Telegram') <> 'Telegram'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM platform_messages pm
|
||||
WHERE pm.session_id = s.id
|
||||
AND pm.platform = COALESCE(g.platform, 'Telegram')
|
||||
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<JoinLinkPlayerRow>(
|
||||
"""
|
||||
SELECT COALESCE(p.platform, 'Telegram') AS Platform,
|
||||
COALESCE(p.external_user_id, p.telegram_id::TEXT) AS ExternalUserId,
|
||||
p.display_name AS DisplayName,
|
||||
COALESCE(p.external_username, p.telegram_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<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
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user