197 lines
7.3 KiB
C#
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);
|
|
}
|