014b5edd31
PR Checks / test-and-build (pull_request) Successful in 15m52s
Add format and location steps to the Telegram /newsession wizard, persist offline addresses in sessions.location_address, and render online links/offline addresses in schedule messages. Bump version to 3.10.0.
182 lines
7.6 KiB
C#
182 lines
7.6 KiB
C#
using Dapper;
|
|
using GmRelay.Shared.Domain;
|
|
using GmRelay.Shared.Platform;
|
|
using GmRelay.Shared.Rendering;
|
|
using Npgsql;
|
|
|
|
namespace GmRelay.Shared.Features.Sessions.RescheduleSession;
|
|
|
|
public sealed class HandleRescheduleTimeInputHandler(
|
|
NpgsqlDataSource dataSource)
|
|
{
|
|
public async Task<HandleRescheduleTimeInputResult> HandleAsync(
|
|
HandleRescheduleTimeInputCommand command, CancellationToken ct)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
var platform = command.User.Platform.ToString();
|
|
var externalGmId = command.User.ExternalUserId;
|
|
var externalGroupId = command.Group.ExternalGroupId;
|
|
|
|
var proposal = await connection.QuerySingleOrDefaultAsync<AwaitingProposalDto>(
|
|
"""
|
|
SELECT rp.id AS Id, rp.session_id AS SessionId, s.title AS Title, s.scheduled_at AS CurrentScheduledAt,
|
|
s.batch_id AS BatchId, s.batch_message_id AS BatchMessageId,
|
|
g.external_group_id AS ExternalGroupId,
|
|
s.thread_id AS ThreadId,
|
|
s.notification_mode AS NotificationMode
|
|
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.proposed_by_external_user_id = @ExternalGmId
|
|
AND rp.status = 'AwaitingTime'
|
|
AND g.platform = @Platform
|
|
AND g.external_group_id = @ExternalGroupId
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM group_managers gm
|
|
JOIN players manager_player ON manager_player.id = gm.player_id
|
|
WHERE gm.group_id = s.group_id
|
|
AND manager_player.platform = @Platform
|
|
AND manager_player.external_user_id = @ExternalGmId
|
|
)
|
|
ORDER BY rp.created_at DESC
|
|
LIMIT 1
|
|
""",
|
|
new { ExternalGmId = externalGmId, Platform = platform, ExternalGroupId = externalGroupId });
|
|
|
|
if (proposal is null)
|
|
return new HandleRescheduleTimeInputResult(false, false, null, null, null, null, [], [], [], null, default, null);
|
|
|
|
if (!RescheduleVotingInput.TryParse(command.Text, DateTimeOffset.UtcNow, out var votingInput, out var parseError))
|
|
{
|
|
return new HandleRescheduleTimeInputResult(
|
|
true, false, parseError, null, null, null, [], [], [], proposal.Title, proposal.CurrentScheduledAt, null);
|
|
}
|
|
|
|
var participants = (await connection.QueryAsync<VoteParticipantDto>(
|
|
"""
|
|
SELECT p.id AS PlayerId,
|
|
p.display_name AS DisplayName,
|
|
p.external_username AS TelegramUsername,
|
|
p.external_user_id::BIGINT 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
|
|
""",
|
|
new { proposal.SessionId, Active = ParticipantRegistrationStatus.Active })).ToList();
|
|
|
|
if (participants.Count == 0)
|
|
{
|
|
var newTime = votingInput.Options[0];
|
|
var view = await RescheduleImmediatelyAsync(connection, proposal, newTime, ct);
|
|
var replyText =
|
|
$"""✅ Сессия «{proposal.Title}» перенесена!\n\n📅 Новое время: <b>{newTime.ToOffset(TimeSpan.FromHours(3)).ToString("d MMMM yyyy, HH:mm", System.Globalization.CultureInfo.GetCultureInfo("ru-RU"))}</b> (МСК)\n\n<i>Участников нет — голосование не требуется.</i>""";
|
|
return new HandleRescheduleTimeInputResult(
|
|
true, true, replyText, view, null, null, [], [], [], proposal.Title, proposal.CurrentScheduledAt, proposal.BatchMessageId);
|
|
}
|
|
|
|
await using var transaction = await connection.BeginTransactionAsync(ct);
|
|
var options = votingInput.Options
|
|
.Select((proposedAt, index) => new RescheduleOptionDto(
|
|
Guid.NewGuid(),
|
|
index + 1,
|
|
proposedAt))
|
|
.ToList();
|
|
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
UPDATE reschedule_proposals
|
|
SET voting_deadline_at = @Deadline, status = 'Voting', vote_chat_id = @VoteChatId
|
|
WHERE id = @Id
|
|
""",
|
|
new { votingInput.Deadline, VoteChatId = externalGroupId, Id = proposal.Id },
|
|
transaction);
|
|
|
|
foreach (var option in options)
|
|
{
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
INSERT INTO reschedule_options (id, proposal_id, proposed_at, display_order)
|
|
VALUES (@OptionId, @ProposalId, @ProposedAt, @DisplayOrder)
|
|
""",
|
|
new
|
|
{
|
|
option.OptionId,
|
|
ProposalId = proposal.Id,
|
|
option.ProposedAt,
|
|
option.DisplayOrder
|
|
},
|
|
transaction);
|
|
}
|
|
|
|
await transaction.CommitAsync(ct);
|
|
|
|
return new HandleRescheduleTimeInputResult(
|
|
true,
|
|
false,
|
|
null,
|
|
null,
|
|
proposal.Id,
|
|
votingInput.Deadline,
|
|
options,
|
|
participants,
|
|
[],
|
|
proposal.Title,
|
|
proposal.CurrentScheduledAt,
|
|
null);
|
|
}
|
|
|
|
private static async Task<SessionBatchViewModel?> RescheduleImmediatelyAsync(
|
|
NpgsqlConnection connection,
|
|
AwaitingProposalDto proposal,
|
|
DateTimeOffset newTime,
|
|
CancellationToken ct)
|
|
{
|
|
await using var transaction = await connection.BeginTransactionAsync(ct);
|
|
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
UPDATE sessions
|
|
SET scheduled_at = @NewTime,
|
|
status = @Status,
|
|
confirmation_message_id = NULL,
|
|
confirmation_sent_at = NULL,
|
|
one_hour_reminder_processed_at = NULL,
|
|
updated_at = now()
|
|
WHERE id = @SessionId
|
|
""",
|
|
new { NewTime = newTime, proposal.SessionId, Status = SessionStatus.Planned },
|
|
transaction);
|
|
|
|
await connection.ExecuteAsync(
|
|
"UPDATE reschedule_proposals SET proposed_at = @NewTime, status = 'Approved' WHERE id = @Id",
|
|
new { NewTime = newTime, Id = proposal.Id },
|
|
transaction);
|
|
|
|
await transaction.CommitAsync(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 FROM sessions WHERE batch_id = @BatchId ORDER BY scheduled_at",
|
|
new { proposal.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 { proposal.BatchId })).ToList();
|
|
|
|
return SessionBatchViewBuilder.Build(proposal.Title, batchSessions, batchParticipants);
|
|
}
|
|
}
|