using GmRelay.DiscordBot.Infrastructure.Discord; using GmRelay.Shared.Domain; using GmRelay.Shared.Features.Confirmation.HandleRsvp; using GmRelay.Shared.Features.Sessions.CreateSession; using GmRelay.Shared.Platform; using System.Globalization; using NetCord; using NetCord.Rest; using NetCord.Services.ComponentInteractions; namespace GmRelay.DiscordBot.Features.Sessions; public sealed class DiscordSessionInteractionModule( JoinSessionHandler joinSessionHandler, LeaveSessionHandler leaveSessionHandler, HandleRsvpHandler rsvpHandler, DiscordRescheduleVoteHandler voteHandler, DiscordInteractionReplyCache interactionReplies, ILogger logger) : ComponentInteractionModule { [ComponentInteraction("join_session")] public async Task JoinAsync(string sessionId) { if (!Guid.TryParse(sessionId, out var parsedSessionId)) { await RespondAsync(CreateEphemeralReply("Session button is outdated.")); return; } var input = CreateInput(parsedSessionId); await RespondAsync(InteractionCallback.DeferredMessage(MessageFlags.Ephemeral)); try { await joinSessionHandler.HandleAsync( DiscordSessionInteractionMapper.CreateJoinCommand(input), CancellationToken.None); } catch (Exception ex) { logger.LogError(ex, "Failed to handle Discord join interaction for session {SessionId}", parsedSessionId); await CompleteResponseAsync("Не удалось обработать кнопку."); return; } await CompleteWithStoredReplyAsync(input.InteractionId); } [ComponentInteraction("leave_session")] public async Task LeaveAsync(string sessionId) { if (!Guid.TryParse(sessionId, out var parsedSessionId)) { await RespondAsync(CreateEphemeralReply("Session button is outdated.")); return; } var input = CreateInput(parsedSessionId); await RespondAsync(InteractionCallback.DeferredMessage(MessageFlags.Ephemeral)); try { await leaveSessionHandler.HandleAsync( DiscordSessionInteractionMapper.CreateLeaveCommand(input), CancellationToken.None); } catch (Exception ex) { logger.LogError(ex, "Failed to handle Discord leave interaction for session {SessionId}", parsedSessionId); await CompleteResponseAsync("Не удалось обработать кнопку."); return; } await CompleteWithStoredReplyAsync(input.InteractionId); } [ComponentInteraction("rsvp")] public async Task RsvpAsync(string status, string sessionId) { if (!Guid.TryParse(sessionId, out var parsedSessionId)) { await RespondAsync(CreateEphemeralReply("Session button is outdated.")); return; } var rsvpStatus = status switch { "confirm" => RsvpStatus.Confirmed, "decline" => RsvpStatus.Declined, _ => null }; if (rsvpStatus is null) { await RespondAsync(CreateEphemeralReply("Session button is outdated.")); return; } var input = CreateInput(parsedSessionId); await RespondAsync(InteractionCallback.DeferredMessage(MessageFlags.Ephemeral)); try { await rsvpHandler.HandleAsync( new HandleRsvpCommand( parsedSessionId, new PlatformUser( PlatformKind.Discord, Context.User.Id.ToString(CultureInfo.InvariantCulture), string.IsNullOrWhiteSpace(Context.User.GlobalName) ? Context.User.Username : Context.User.GlobalName, Context.User.Username), rsvpStatus, input.InteractionId, new PlatformGroup( PlatformKind.Discord, input.GuildId, input.GuildId, input.ChannelId), new PlatformMessageRef( PlatformKind.Discord, input.GuildId, null, input.MessageId)), CancellationToken.None); } catch (Exception ex) { logger.LogError(ex, "Failed to handle Discord RSVP interaction for session {SessionId}", parsedSessionId); await CompleteResponseAsync("РќРµ удалось обработать РєРЅРѕРїРєСѓ."); return; } await CompleteWithStoredReplyAsync(input.InteractionId); } [ComponentInteraction("reschedule_vote")] public async Task RescheduleVoteAsync(string optionId) { if (!Guid.TryParse(optionId, out var parsedOptionId)) { await RespondAsync(CreateEphemeralReply("Vote button is outdated.")); return; } var input = CreateInput(Guid.Empty); // sessionId not needed for vote routing var voteInput = new DiscordRescheduleVoteInput( parsedOptionId, Context.User.Id, Context.Interaction.Id.ToString(System.Globalization.CultureInfo.InvariantCulture), input.GuildId, input.ChannelId, input.MessageId); await RespondAsync(InteractionCallback.DeferredMessage(MessageFlags.Ephemeral)); string replyText; try { replyText = await voteHandler.HandleAsync(voteInput, CancellationToken.None); } catch (Exception ex) { logger.LogError(ex, "Failed to handle Discord reschedule vote for option {OptionId}", parsedOptionId); await CompleteResponseAsync("Не удалось обработать голос."); return; } await CompleteResponseAsync(replyText); } private DiscordSessionInteractionInput CreateInput(Guid sessionId) { var guild = Context.Guild ?? throw new InvalidOperationException("Session buttons can only be used in a guild."); var message = Context.Interaction.Message ?? throw new InvalidOperationException("Session button interaction must include a message."); return new DiscordSessionInteractionInput( SessionId: sessionId, InteractionId: Context.Interaction.Id.ToString(System.Globalization.CultureInfo.InvariantCulture), GuildId: guild.Id.ToString(CultureInfo.InvariantCulture), ChannelId: Context.Channel.Id.ToString(CultureInfo.InvariantCulture), MessageId: message.Id.ToString(CultureInfo.InvariantCulture), UserId: Context.User.Id, Username: Context.User.Username, DisplayName: Context.User.GlobalName); } private async Task CompleteWithStoredReplyAsync(string interactionId) { var reply = interactionReplies.Take(interactionId); await CompleteResponseAsync(reply?.Text ?? "Session updated."); } private Task CompleteResponseAsync(string text) => ModifyResponseAsync(options => options.Content = text); private static InteractionCallbackProperties CreateEphemeralReply(string text) => InteractionCallback.Message( new InteractionMessageProperties() .WithContent(text) .WithFlags(MessageFlags.Ephemeral)); }