feat(platform): route scheduler notifications through platform messenger
PR Checks / test-and-build (pull_request) Successful in 7m9s

This commit is contained in:
2026-05-21 12:30:35 +03:00
parent 5dbec1a0a4
commit 2a707e4825
49 changed files with 2158 additions and 846 deletions
@@ -1,19 +1,15 @@
namespace GmRelay.DiscordBot.Features.Sessions;
using Dapper;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Features.Sessions.RescheduleSession;
using GmRelay.Shared.Platform;
using GmRelay.DiscordBot.Rendering;
using GmRelay.Shared.Rendering;
using NetCord;
using NetCord.Rest;
using Npgsql;
public sealed class DiscordRescheduleVotingDeadlineService(
NpgsqlDataSource dataSource,
RescheduleVotingFinalizer finalizer,
RestClient restClient,
IPlatformMessenger messenger,
ILogger<DiscordRescheduleVotingDeadlineService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -27,7 +23,9 @@ public sealed class DiscordRescheduleVotingDeadlineService(
await ProcessDueProposals(stoppingToken);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { }
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
}
}
private async Task ProcessDueProposals(CancellationToken ct)
@@ -57,10 +55,8 @@ public sealed class DiscordRescheduleVotingDeadlineService(
if (result.SourcePlatform != "Discord")
return;
// Update Discord vote message
await TryUpdateDiscordVoteMessage(result, ct);
// If approved, update batch schedule
if (result.SelectedOption is not null)
{
await TryUpdateBatchScheduleAsync(result, ct);
@@ -68,7 +64,9 @@ public sealed class DiscordRescheduleVotingDeadlineService(
logger.LogInformation(
"Finalized Discord reschedule proposal {ProposalId} for session {SessionId} with outcome {Outcome}",
proposalId, result.SessionId, result.Decision.Outcome);
proposalId,
result.SessionId,
result.Decision.Outcome);
}
catch (Exception ex)
{
@@ -83,10 +81,13 @@ public sealed class DiscordRescheduleVotingDeadlineService(
await using var connection = await dataSource.OpenConnectionAsync(ct);
var msgRef = await connection.QuerySingleOrDefaultAsync<PlatformMessageRefDto>(
"""
SELECT external_channel_id AS ExternalChannelId, external_message_id AS ExternalMessageId
FROM platform_messages
WHERE session_id = @SessionId AND purpose = 'reschedule_vote' AND platform = 'Discord'
ORDER BY created_at DESC
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 });
@@ -94,31 +95,27 @@ public sealed class DiscordRescheduleVotingDeadlineService(
if (msgRef is null)
return;
var (embed, actionRow) = DiscordRescheduleVotingRenderer.Render(
result.Title, result.CurrentScheduledAt, result.VotingDeadlineAt,
result.Options, result.Participants, result.Votes);
var group = CreateDiscordGroup(msgRef);
var channelId = ulong.Parse(msgRef.ExternalChannelId);
var messageId = ulong.Parse(msgRef.ExternalMessageId);
// Disable buttons after finalization
var disabledRow = new ActionRowProperties();
foreach (var btn in actionRow.OfType<ButtonProperties>())
{
disabledRow.Add(new ButtonProperties(btn.CustomId, btn.Label ?? string.Empty, ButtonStyle.Secondary) { Disabled = true });
}
var resultText = result.SelectedOption is not null
? $"Голосование завершено. Победил вариант {result.SelectedOption.DisplayOrder}: **{result.SelectedOption.ProposedAt.FormatMoscow()}** (МСК)."
: $"Голосование завершено. {result.Decision.Reason}";
var updatedEmbed = embed.WithDescription($"{embed.Description}\n\n{resultText}");
await restClient.ModifyMessageAsync(channelId, messageId, options =>
{
options.Embeds = new[] { updatedEmbed };
options.Components = new[] { disabledRow };
});
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)
{
@@ -130,14 +127,16 @@ public sealed class DiscordRescheduleVotingDeadlineService(
{
try
{
// Query batch schedule message ref
await using var connection = await dataSource.OpenConnectionAsync(ct);
var batchRef = await connection.QuerySingleOrDefaultAsync<PlatformMessageRefDto>(
"""
SELECT external_channel_id AS ExternalChannelId, external_message_id AS ExternalMessageId
FROM platform_messages
WHERE batch_id = @BatchId AND purpose = 'schedule' AND platform = 'Discord'
ORDER BY created_at DESC
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 });
@@ -145,14 +144,16 @@ public sealed class DiscordRescheduleVotingDeadlineService(
if (batchRef is null)
return;
// Rebuild schedule view and update Discord message
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
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
@@ -162,16 +163,18 @@ public sealed class DiscordRescheduleVotingDeadlineService(
new { result.BatchId })).ToList();
var view = SessionBatchViewBuilder.Build(result.Title, sessions, participants);
var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(view);
var group = CreateDiscordGroup(batchRef);
var channelId = ulong.Parse(batchRef.ExternalChannelId);
var messageId = ulong.Parse(batchRef.ExternalMessageId);
await restClient.ModifyMessageAsync(channelId, messageId, options =>
{
options.Embeds = embeds;
options.Components = actionRows;
});
await messenger.UpdateScheduleAsync(
new PlatformScheduleMessage(
group,
view,
new PlatformMessageRef(
PlatformKind.Discord,
batchRef.ExternalGroupId,
null,
batchRef.ExternalMessageId)),
ct);
}
catch (Exception ex)
{
@@ -179,5 +182,15 @@ public sealed class DiscordRescheduleVotingDeadlineService(
}
}
internal sealed record PlatformMessageRefDto(string ExternalChannelId, string ExternalMessageId);
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);
}