040b0a3cdb
PR Checks / test-and-build (pull_request) Failing after 13m15s
- Добавлены миграции 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>
229 lines
7.5 KiB
C#
229 lines
7.5 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.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,
|
|
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<JoinLinkPlayerRow>(
|
|
"""
|
|
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<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
|
|
});
|
|
}
|