a5624897e9
PR Checks / test-and-build (pull_request) Failing after 12m3s
- Add builder.Logging.AddConsole() to DiscordBot Program.cs so logs are visible in docker logs. - Add granular LogInformation/LogError calls to DiscordNewSessionCommand and DiscordRescheduleCommand to diagnose failures. - Use InteractionCallback.DeferredMessage() + ModifyResponseAsync pattern for /newsession and /reschedule to avoid Discord 3-second interaction timeout. - Bump version → 3.0.8 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
155 lines
6.5 KiB
C#
155 lines
6.5 KiB
C#
namespace GmRelay.DiscordBot.Features.Sessions;
|
|
|
|
using NetCord;
|
|
using NetCord.Rest;
|
|
using NetCord.Services.ApplicationCommands;
|
|
|
|
public class DiscordRescheduleCommand : ApplicationCommandModule<SlashCommandContext>
|
|
{
|
|
private readonly DiscordRescheduleHandler _handler;
|
|
private readonly ILogger<DiscordRescheduleCommand> _logger;
|
|
|
|
public DiscordRescheduleCommand(DiscordRescheduleHandler handler, ILogger<DiscordRescheduleCommand> 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<string> { option1, option2 };
|
|
if (!string.IsNullOrWhiteSpace(option3))
|
|
options.Add(option3);
|
|
|
|
var parsedOptions = new List<DateTimeOffset>();
|
|
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: Ошибка при запуске голосования.";
|
|
});
|
|
}
|
|
}
|
|
}
|