226 lines
8.6 KiB
C#
226 lines
8.6 KiB
C#
using Dapper;
|
|
using GmRelay.Bot.Infrastructure.Telegram;
|
|
using GmRelay.Shared.Domain;
|
|
using GmRelay.Shared.Features.Notifications;
|
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
|
using GmRelay.Shared.Platform;
|
|
using GmRelay.Shared.Rendering;
|
|
using Npgsql;
|
|
|
|
namespace GmRelay.Bot.Features.Sessions.RescheduleSession;
|
|
|
|
internal sealed record TelegramProposalFieldsDto(
|
|
int? VoteMessageId,
|
|
int? BatchMessageId,
|
|
long TelegramChatId,
|
|
int? ThreadId);
|
|
|
|
public sealed class RescheduleVotingDeadlineService(
|
|
NpgsqlDataSource dataSource,
|
|
IPlatformMessenger messenger,
|
|
PlatformDirectNotificationSender directSender,
|
|
RescheduleVotingFinalizer finalizer,
|
|
ILogger<RescheduleVotingDeadlineService> 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("Telegram", ct);
|
|
|
|
foreach (var proposalId in proposalIds)
|
|
{
|
|
await FinalizeProposal(proposalId, ct);
|
|
}
|
|
}
|
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Failed to process due reschedule voting proposals");
|
|
}
|
|
}
|
|
|
|
private async Task FinalizeProposal(Guid proposalId, CancellationToken ct)
|
|
{
|
|
var result = await finalizer.FinalizeAsync(proposalId, ct);
|
|
if (result is null)
|
|
return;
|
|
|
|
if (result.SourcePlatform != "Telegram")
|
|
{
|
|
logger.LogInformation(
|
|
"Skipping Telegram message handling for proposal {ProposalId} with source platform {SourcePlatform}",
|
|
proposalId,
|
|
result.SourcePlatform);
|
|
return;
|
|
}
|
|
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
var telegramFields = await connection.QuerySingleOrDefaultAsync<TelegramProposalFieldsDto>(
|
|
"""
|
|
SELECT rp.vote_message_id AS VoteMessageId,
|
|
s.batch_message_id AS BatchMessageId,
|
|
g.external_group_id::BIGINT AS TelegramChatId,
|
|
s.thread_id AS ThreadId
|
|
FROM reschedule_proposals rp
|
|
JOIN sessions s ON s.id = rp.session_id
|
|
JOIN game_groups g ON g.id = s.group_id
|
|
WHERE rp.id = @ProposalId
|
|
""",
|
|
new { ProposalId = proposalId });
|
|
|
|
if (telegramFields is null)
|
|
{
|
|
logger.LogWarning("Could not find Telegram fields for proposal {ProposalId}", proposalId);
|
|
return;
|
|
}
|
|
|
|
var directRecipients = result.Participants
|
|
.Select(p => TelegramPlatformIds.User(p.TelegramId, p.DisplayName))
|
|
.ToList();
|
|
|
|
await TryUpdateVoteMessage(result, telegramFields, ct);
|
|
|
|
if (result.SelectedOption is not null)
|
|
{
|
|
await TryUpdateBatchMessage(result, telegramFields, ct);
|
|
}
|
|
|
|
var mode = SessionNotificationModeExtensions.FromDatabaseValue(result.NotificationMode);
|
|
if (mode.ShouldSendDirectMessages())
|
|
{
|
|
await SendDirectResult(result, directRecipients, ct);
|
|
}
|
|
|
|
logger.LogInformation(
|
|
"Updated Telegram messages for finalized reschedule proposal {ProposalId} for session {SessionId}",
|
|
result.ProposalId,
|
|
result.SessionId);
|
|
}
|
|
|
|
private async Task TryUpdateVoteMessage(
|
|
RescheduleVotingFinalizerResult result,
|
|
TelegramProposalFieldsDto telegramFields,
|
|
CancellationToken ct)
|
|
{
|
|
if (telegramFields.VoteMessageId is null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await messenger.UpdateRescheduleVoteAsync(
|
|
new PlatformRescheduleVoteUpdate(
|
|
TelegramPlatformIds.Group(telegramFields.TelegramChatId, telegramFields.ThreadId),
|
|
TelegramPlatformIds.Message(
|
|
telegramFields.TelegramChatId,
|
|
telegramFields.ThreadId,
|
|
telegramFields.VoteMessageId.Value),
|
|
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 finalized reschedule vote message for proposal {ProposalId}", result.ProposalId);
|
|
}
|
|
}
|
|
|
|
private async Task TryUpdateBatchMessage(
|
|
RescheduleVotingFinalizerResult result,
|
|
TelegramProposalFieldsDto telegramFields,
|
|
CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
var batchSessions = (await connection.QueryAsync<SessionBatchDto>(
|
|
"SELECT id AS SessionId, scheduled_at AS ScheduledAt, status AS Status, max_players AS MaxPlayers, join_link AS JoinLink, format AS Format, location_address AS LocationAddress, description AS Description, system AS System, duration_minutes AS DurationMinutes, is_one_shot AS IsOneShot FROM sessions WHERE batch_id = @BatchId ORDER BY scheduled_at",
|
|
new { result.BatchId })).ToList();
|
|
|
|
var batchParticipants = (await connection.QueryAsync<ParticipantBatchDto>(
|
|
"""
|
|
SELECT sp.session_id AS SessionId,
|
|
p.display_name AS DisplayName,
|
|
p.external_username AS TelegramUsername,
|
|
sp.registration_status AS RegistrationStatus
|
|
FROM session_participants sp
|
|
JOIN players p ON sp.player_id = p.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, sp.responded_at ASC, p.created_at ASC
|
|
""",
|
|
new { result.BatchId })).ToList();
|
|
|
|
if (telegramFields.BatchMessageId.HasValue)
|
|
{
|
|
var view = SessionBatchViewBuilder.Build(result.Title, batchSessions, batchParticipants);
|
|
|
|
await messenger.UpdateScheduleAsync(
|
|
new PlatformScheduleMessage(
|
|
TelegramPlatformIds.Group(telegramFields.TelegramChatId, telegramFields.ThreadId),
|
|
view,
|
|
TelegramPlatformIds.Message(telegramFields.TelegramChatId, telegramFields.ThreadId, telegramFields.BatchMessageId.Value)),
|
|
ct);
|
|
}
|
|
else
|
|
{
|
|
await messenger.SendGroupMessageAsync(
|
|
TelegramPlatformIds.Group(telegramFields.TelegramChatId, telegramFields.ThreadId),
|
|
$"Расписание обновлено после голосования за перенос сессии \"{System.Net.WebUtility.HtmlEncode(result.Title)}\".",
|
|
ct);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to update batch message for finalized proposal {ProposalId}", result.ProposalId);
|
|
}
|
|
}
|
|
|
|
private async Task SendDirectResult(
|
|
RescheduleVotingFinalizerResult result,
|
|
IReadOnlyList<PlatformUser> recipients,
|
|
CancellationToken ct)
|
|
{
|
|
await directSender.SendAsync(
|
|
result.SelectedOption is not null
|
|
? PlatformDirectSessionNotificationKind.RescheduleApproved
|
|
: PlatformDirectSessionNotificationKind.RescheduleRejected,
|
|
recipients,
|
|
result.SessionId,
|
|
result.Title,
|
|
result.SelectedOption?.ProposedAt.UtcDateTime ?? result.CurrentScheduledAt,
|
|
joinLink: null,
|
|
actorDisplayName: null,
|
|
reason: result.SelectedOption is null ? result.Decision.Reason : null,
|
|
ct);
|
|
}
|
|
}
|