Files
GmRelayBot/src/GmRelay.DiscordBot/Features/Sessions/DiscordRescheduleCommand.cs
T
Toutsu f6d5281af8
PR Checks / test-and-build (pull_request) Successful in 8m46s
fix(discord): resolve slash commands from interaction payload instead of gateway cache
Context.Guild in NetCord resolves the Guild object from the gateway client cache
(cache.Guilds.GetValueOrDefault(guildId)), not from the interaction JSON payload.
After a bot restart, the guild may not yet be cached when the first slash command
arrives, causing Context.Guild to be null even though the command is invoked
inside a guild channel. This produced "This command can only be used in a guild."

Changes:
- DiscordListSessionsCommand: use Context.Interaction.GuildId instead of Context.Guild.Id
- DiscordNewSessionCommand: use Context.Interaction.GuildId + REST GetGuildAsync/GetGuildUserAsync
- DiscordRescheduleCommand: same as above
- DiscordSessionInteractionModule: same fix for button interactions (CreateInput)
- Add null guard in GetResolvedPermissions for safety
- Bump version to 3.0.5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:01:53 +03:00

121 lines
4.9 KiB
C#

namespace GmRelay.DiscordBot.Features.Sessions;
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 = "")
{
var guildId = Context.Interaction.GuildId
?? throw new InvalidOperationException("This command can only be used in a guild.");
var guild = await Context.Client.Rest.GetGuildAsync(guildId);
var member = await Context.Client.Rest.GetGuildUserAsync(guildId, Context.User.Id);
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;
}
var resolvedPermissions = GetResolvedPermissions(guild, member);
try
{
var result = await _handler.HandleAsync(
guildId: guild.Id.ToString(),
channelId: Context.Channel.Id.ToString(),
userId: Context.User.Id,
userDisplayName: Context.User.GlobalName ?? Context.User.Username,
resolvedPermissions: resolvedPermissions,
guildOwnerId: guild.OwnerId,
sessionId: sessionId,
options: parsedOptions,
deadline: deadlineResult.Value,
CancellationToken.None);
await Context.Interaction.SendResponseAsync(
InteractionCallback.Message(
$"🗳 Голосование за перенос запущено! Дедлайн: {deadlineResult.Value:yyyy-MM-dd HH:mm} UTC."));
}
catch (UnauthorizedAccessException ex)
{
await Context.Interaction.SendResponseAsync(
InteractionCallback.Message($":no_entry: {ex.Message}"));
}
catch (InvalidOperationException ex)
{
await Context.Interaction.SendResponseAsync(
InteractionCallback.Message($":warning: {ex.Message}"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initiate reschedule for session {SessionId}", sessionId);
await Context.Interaction.SendResponseAsync(
InteractionCallback.Message(":boom: Ошибка при запуске голосования."));
}
}
private static ulong GetResolvedPermissions(NetCord.Rest.RestGuild guild, NetCord.GuildUser member)
{
if (member is null)
return 0;
ulong resolved = 0;
foreach (var roleId in member.RoleIds)
{
if (guild.Roles.TryGetValue(roleId, out var role))
resolved |= (ulong)role.Permissions;
}
return resolved;
}
}