namespace GmRelay.DiscordBot.Features.Sessions; using NetCord; using NetCord.Rest; using NetCord.Services.ApplicationCommands; public class DiscordRescheduleCommand : ApplicationCommandModule { private readonly DiscordRescheduleHandler _handler; private readonly ILogger _logger; public DiscordRescheduleCommand(DiscordRescheduleHandler handler, ILogger logger) { _handler = handler; _logger = logger; } [SlashCommand("reschedule", "Initiate reschedule voting for a session")] public async Task ExecuteAsync( [SlashCommandParameter(Name = "session", Description = "Session ID to reschedule")] string sessionIdText, [SlashCommandParameter(Name = "option1", Description = "First time option (YYYY-MM-DD HH:mm)")] string option1, [SlashCommandParameter(Name = "option2", Description = "Second time option (YYYY-MM-DD HH:mm)")] string option2, [SlashCommandParameter(Name = "option3", Description = "Third time option (optional)")] string? option3 = null, [SlashCommandParameter(Name = "deadline", Description = "Voting deadline (YYYY-MM-DD HH:mm)")] string deadline = "") { _logger.LogInformation( "reschedule called by user {UserId} ({UserType}) in guild {GuildId}", Context.User.Id, Context.User.GetType().Name, Context.Interaction.GuildId); var guildId = Context.Interaction.GuildId ?? throw new InvalidOperationException("This command can only be used in a guild."); var member = Context.User as GuildInteractionUser; if (member is null) { _logger.LogError("Context.User is not GuildInteractionUser. Actual type: {ActualType}", Context.User.GetType().Name); throw new InvalidOperationException("Guild member data not available in interaction."); } var resolvedPermissions = (ulong)member.Permissions; _logger.LogInformation("Resolved permissions for user {UserId}: {Permissions}", Context.User.Id, resolvedPermissions); ulong guildOwnerId = 0; try { var guild = await Context.Client.Rest.GetGuildAsync(guildId); guildOwnerId = guild.OwnerId; _logger.LogInformation("Guild owner id: {OwnerId}", guildOwnerId); } catch (RestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) { _logger.LogWarning( ex, "Bot is not a REST member of guild {GuildId}; using resolved permissions from interaction payload", guildId); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error fetching guild {GuildId}", guildId); } if (!Guid.TryParse(sessionIdText, out var sessionId)) { await Context.Interaction.SendResponseAsync( InteractionCallback.Message("❌ Некорректный ID сессии.")); return; } var options = new List { option1, option2 }; if (!string.IsNullOrWhiteSpace(option3)) options.Add(option3); var parsedOptions = new List(); foreach (var opt in options) { var result = DiscordNewSessionHandler.ParseTimeInput(opt); if (!result.IsSuccess) { await Context.Interaction.SendResponseAsync( InteractionCallback.Message($"❌ {opt}: {result.Error}")); return; } parsedOptions.Add(result.Value); } var deadlineResult = DiscordNewSessionHandler.ParseTimeInput(deadline); if (!deadlineResult.IsSuccess) { await Context.Interaction.SendResponseAsync( InteractionCallback.Message($"❌ Дедлайн: {deadlineResult.Error}")); return; } if (deadlineResult.Value >= parsedOptions.Min()) { await Context.Interaction.SendResponseAsync( InteractionCallback.Message("❌ Дедлайн должен быть раньше первого варианта времени.")); return; } // Defer the response to avoid Discord 3-second interaction timeout await Context.Interaction.SendResponseAsync(InteractionCallback.DeferredMessage()); try { _logger.LogInformation("Initiating reschedule for session {SessionId} in guild {GuildId}", sessionId, guildId); var result = await _handler.HandleAsync( guildId: guildId.ToString(), channelId: Context.Channel!.Id.ToString(), userId: Context.User.Id, userDisplayName: Context.User.GlobalName ?? Context.User.Username, resolvedPermissions: resolvedPermissions, guildOwnerId: guildOwnerId, sessionId: sessionId, options: parsedOptions, deadline: deadlineResult.Value, CancellationToken.None); _logger.LogInformation("Reschedule voting started for session {SessionId}, proposal {ProposalId}", sessionId, result.ProposalId); await Context.Interaction.ModifyResponseAsync(message => { message.Content = $"🗳 Голосование за перенос запущено! Дедлайн: {deadlineResult.Value:yyyy-MM-dd HH:mm} UTC."; }); } catch (UnauthorizedAccessException ex) { _logger.LogWarning(ex, "Unauthorized reschedule attempt by user {UserId}", Context.User.Id); await Context.Interaction.ModifyResponseAsync(message => { message.Content = $":no_entry: {ex.Message}"; }); } catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Invalid reschedule request by user {UserId}", Context.User.Id); await Context.Interaction.ModifyResponseAsync(message => { message.Content = $":warning: {ex.Message}"; }); } catch (Exception ex) { _logger.LogError(ex, "Failed to initiate reschedule for session {SessionId}", sessionId); await Context.Interaction.ModifyResponseAsync(message => { message.Content = ":boom: Ошибка при запуске голосования."; }); } } }