132 lines
5.4 KiB
C#
132 lines
5.4 KiB
C#
namespace GmRelay.DiscordBot.Features.Sessions;
|
|
|
|
using Dapper;
|
|
using GmRelay.DiscordBot.Rendering;
|
|
using GmRelay.Shared.Domain;
|
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
|
using GmRelay.Shared.Platform;
|
|
using Npgsql;
|
|
using NetCord.Rest;
|
|
|
|
public sealed record DiscordRescheduleVoteInput(
|
|
Guid OptionId, ulong UserId, string InteractionId,
|
|
string GuildId, string ChannelId, string MessageId);
|
|
|
|
public sealed class DiscordRescheduleVoteHandler(
|
|
NpgsqlDataSource dataSource,
|
|
RestClient restClient,
|
|
ILogger<DiscordRescheduleVoteHandler> logger)
|
|
{
|
|
public async Task<string> HandleAsync(DiscordRescheduleVoteInput input, CancellationToken ct)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
await using var transaction = await connection.BeginTransactionAsync(ct);
|
|
|
|
// 1. Load proposal + option
|
|
var proposal = await connection.QuerySingleOrDefaultAsync<VoteProposalDto>(
|
|
"""
|
|
SELECT rp.id AS Id, rp.session_id AS SessionId, rp.voting_deadline_at AS VotingDeadlineAt,
|
|
s.title AS Title, s.scheduled_at AS CurrentScheduledAt
|
|
FROM reschedule_options ro
|
|
JOIN reschedule_proposals rp ON rp.id = ro.proposal_id
|
|
JOIN sessions s ON s.id = rp.session_id
|
|
WHERE ro.id = @OptionId AND rp.status = 'Voting'
|
|
""",
|
|
new { input.OptionId },
|
|
transaction);
|
|
|
|
if (proposal is null)
|
|
return "Голосование уже завершено или не найдено.";
|
|
|
|
if (proposal.VotingDeadlineAt <= DateTimeOffset.UtcNow)
|
|
return "Дедлайн уже прошёл. Результаты скоро будут применены.";
|
|
|
|
// 2. Verify participant (Discord platform)
|
|
var playerId = await connection.ExecuteScalarAsync<Guid?>(
|
|
"""
|
|
SELECT p.id
|
|
FROM session_participants sp
|
|
JOIN players p ON p.id = sp.player_id
|
|
WHERE sp.session_id = @SessionId
|
|
AND p.platform = 'Discord'
|
|
AND p.external_user_id = @UserId
|
|
AND sp.is_gm = false
|
|
AND sp.registration_status = @Active
|
|
""",
|
|
new { proposal.SessionId, UserId = input.UserId.ToString(), Active = ParticipantRegistrationStatus.Active },
|
|
transaction);
|
|
|
|
if (playerId is null)
|
|
return "Вы не являетесь участником этой сессии.";
|
|
|
|
// 3. Upsert vote
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
INSERT INTO reschedule_option_votes (proposal_id, player_id, option_id)
|
|
VALUES (@ProposalId, @PlayerId, @OptionId)
|
|
ON CONFLICT (proposal_id, player_id) DO UPDATE
|
|
SET option_id = EXCLUDED.option_id, voted_at = now()
|
|
""",
|
|
new { ProposalId = proposal.Id, PlayerId = playerId.Value, input.OptionId },
|
|
transaction);
|
|
|
|
// 4. Reload participants, options, votes for re-rendering
|
|
var participants = (await connection.QueryAsync<VoteParticipantDto>(
|
|
"""
|
|
SELECT p.id AS PlayerId, p.display_name AS DisplayName, p.external_username AS TelegramUsername, 0 AS TelegramId
|
|
FROM session_participants sp
|
|
JOIN players p ON p.id = sp.player_id
|
|
WHERE sp.session_id = @SessionId AND sp.is_gm = false AND sp.registration_status = @Active
|
|
ORDER BY p.display_name
|
|
""",
|
|
new { proposal.SessionId, Active = ParticipantRegistrationStatus.Active },
|
|
transaction)).ToList();
|
|
|
|
var options = (await connection.QueryAsync<RescheduleOptionDto>(
|
|
"""
|
|
SELECT id AS OptionId, display_order AS DisplayOrder, proposed_at AS ProposedAt
|
|
FROM reschedule_options
|
|
WHERE proposal_id = @ProposalId
|
|
ORDER BY display_order
|
|
""",
|
|
new { ProposalId = proposal.Id },
|
|
transaction)).ToList();
|
|
|
|
var votes = (await connection.QueryAsync<RescheduleOptionVoteDto>(
|
|
"""
|
|
SELECT rov.option_id AS OptionId, p.id AS PlayerId, p.display_name AS DisplayName, p.external_username AS TelegramUsername
|
|
FROM reschedule_option_votes rov
|
|
JOIN players p ON p.id = rov.player_id
|
|
WHERE rov.proposal_id = @ProposalId
|
|
ORDER BY rov.voted_at, p.display_name
|
|
""",
|
|
new { ProposalId = proposal.Id },
|
|
transaction)).ToList();
|
|
|
|
await transaction.CommitAsync(ct);
|
|
|
|
// 5. Re-render and update Discord vote message
|
|
var (embed, actionRow) = DiscordRescheduleVotingRenderer.Render(
|
|
proposal.Title, proposal.CurrentScheduledAt, proposal.VotingDeadlineAt,
|
|
options, participants, votes);
|
|
|
|
var channelIdUlong = ulong.Parse(input.ChannelId);
|
|
var messageIdUlong = ulong.Parse(input.MessageId);
|
|
|
|
try
|
|
{
|
|
await restClient.ModifyMessageAsync(channelIdUlong, messageIdUlong, options =>
|
|
{
|
|
options.Embeds = new[] { embed };
|
|
options.Components = new[] { actionRow };
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to update Discord vote message for proposal {ProposalId}", proposal.Id);
|
|
}
|
|
|
|
return "Ваш голос учтён. До дедлайна его можно изменить.";
|
|
}
|
|
}
|