Files
GmRelayBot/src/GmRelay.DiscordBot/Features/Sessions/DiscordRescheduleVotingDeadlineService.cs
T
Toutsu 2a707e4825
PR Checks / test-and-build (pull_request) Successful in 7m9s
feat(platform): route scheduler notifications through platform messenger
2026-05-21 12:30:35 +03:00

197 lines
7.3 KiB
C#

namespace GmRelay.DiscordBot.Features.Sessions;
using Dapper;
using GmRelay.Shared.Features.Sessions.RescheduleSession;
using GmRelay.Shared.Platform;
using GmRelay.Shared.Rendering;
using Npgsql;
public sealed class DiscordRescheduleVotingDeadlineService(
NpgsqlDataSource dataSource,
RescheduleVotingFinalizer finalizer,
IPlatformMessenger messenger,
ILogger<DiscordRescheduleVotingDeadlineService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await ProcessDueProposals(stoppingToken);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await ProcessDueProposals(stoppingToken);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
}
}
private async Task ProcessDueProposals(CancellationToken ct)
{
try
{
var proposalIds = await finalizer.GetDueProposalIdsAsync("Discord", ct);
foreach (var id in proposalIds)
{
await TryFinalizeAsync(id, ct);
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to process Discord reschedule proposals");
}
}
private async Task TryFinalizeAsync(Guid proposalId, CancellationToken ct)
{
try
{
var result = await finalizer.FinalizeAsync(proposalId, ct);
if (result is null)
return;
if (result.SourcePlatform != "Discord")
return;
await TryUpdateDiscordVoteMessage(result, ct);
if (result.SelectedOption is not null)
{
await TryUpdateBatchScheduleAsync(result, ct);
}
logger.LogInformation(
"Finalized Discord reschedule proposal {ProposalId} for session {SessionId} with outcome {Outcome}",
proposalId,
result.SessionId,
result.Decision.Outcome);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to finalize Discord proposal {ProposalId}", proposalId);
}
}
private async Task TryUpdateDiscordVoteMessage(RescheduleVotingFinalizerResult result, CancellationToken ct)
{
try
{
await using var connection = await dataSource.OpenConnectionAsync(ct);
var msgRef = await connection.QuerySingleOrDefaultAsync<PlatformMessageRefDto>(
"""
SELECT g.external_group_id AS ExternalGroupId,
COALESCE(pm.external_channel_id, g.external_channel_id, g.external_group_id) AS ExternalChannelId,
pm.external_message_id AS ExternalMessageId
FROM platform_messages pm
JOIN game_groups g ON g.id = pm.group_id
WHERE pm.session_id = @SessionId AND pm.purpose = 'reschedule_vote' AND pm.platform = 'Discord'
ORDER BY pm.created_at DESC
LIMIT 1
""",
new { result.SessionId });
if (msgRef is null)
return;
var group = CreateDiscordGroup(msgRef);
await messenger.UpdateRescheduleVoteAsync(
new PlatformRescheduleVoteUpdate(
group,
new PlatformMessageRef(
PlatformKind.Discord,
msgRef.ExternalGroupId,
null,
msgRef.ExternalMessageId),
result.ProposalId,
result.SessionId,
result.Title,
result.CurrentScheduledAt,
result.VotingDeadlineAt,
result.Decision,
result.SelectedOption,
result.Options,
result.Votes,
result.Participants),
ct);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to update Discord vote message for session {SessionId}", result.SessionId);
}
}
private async Task TryUpdateBatchScheduleAsync(RescheduleVotingFinalizerResult result, CancellationToken ct)
{
try
{
await using var connection = await dataSource.OpenConnectionAsync(ct);
var batchRef = await connection.QuerySingleOrDefaultAsync<PlatformMessageRefDto>(
"""
SELECT g.external_group_id AS ExternalGroupId,
COALESCE(pm.external_channel_id, g.external_channel_id, g.external_group_id) AS ExternalChannelId,
pm.external_message_id AS ExternalMessageId
FROM platform_messages pm
JOIN game_groups g ON g.id = pm.group_id
WHERE pm.batch_id = @BatchId AND pm.purpose = 'schedule' AND pm.platform = 'Discord'
ORDER BY pm.created_at DESC
LIMIT 1
""",
new { result.BatchId });
if (batchRef is null)
return;
var sessions = (await connection.QueryAsync<SessionBatchDto>(
"SELECT id AS SessionId, scheduled_at AS ScheduledAt, status AS Status, max_players AS MaxPlayers, join_link AS JoinLink FROM sessions WHERE batch_id = @BatchId ORDER BY scheduled_at",
new { result.BatchId })).ToList();
var participants = (await connection.QueryAsync<ParticipantBatchDto>(
"""
SELECT sp.session_id AS SessionId,
p.display_name AS DisplayName,
COALESCE(p.external_username, p.telegram_username) AS TelegramUsername,
sp.registration_status AS RegistrationStatus
FROM session_participants sp
JOIN players p ON p.id = sp.player_id
JOIN sessions s ON sp.session_id = s.id
WHERE s.batch_id = @BatchId AND sp.is_gm = false
ORDER BY sp.registration_status ASC, sp.created_at ASC
""",
new { result.BatchId })).ToList();
var view = SessionBatchViewBuilder.Build(result.Title, sessions, participants);
var group = CreateDiscordGroup(batchRef);
await messenger.UpdateScheduleAsync(
new PlatformScheduleMessage(
group,
view,
new PlatformMessageRef(
PlatformKind.Discord,
batchRef.ExternalGroupId,
null,
batchRef.ExternalMessageId)),
ct);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to update Discord batch schedule for session {SessionId}", result.SessionId);
}
}
private static PlatformGroup CreateDiscordGroup(PlatformMessageRefDto message) =>
new(
PlatformKind.Discord,
message.ExternalGroupId,
message.ExternalGroupId,
message.ExternalChannelId);
internal sealed record PlatformMessageRefDto(
string ExternalGroupId,
string ExternalChannelId,
string ExternalMessageId);
}