diff --git a/Myriad/Rest/Types/Requests/MessageRequest.cs b/Myriad/Rest/Types/Requests/MessageRequest.cs index 992eb08e..57f49e24 100644 --- a/Myriad/Rest/Types/Requests/MessageRequest.cs +++ b/Myriad/Rest/Types/Requests/MessageRequest.cs @@ -9,5 +9,6 @@ namespace Myriad.Rest.Types.Requests public bool Tts { get; set; } public AllowedMentions? AllowedMentions { get; set; } public Embed? Embed { get; set; } + public MessageComponent[]? Components { get; set; } } } \ No newline at end of file diff --git a/Myriad/Types/Application/ApplicationCommandInteractionData.cs b/Myriad/Types/Application/ApplicationCommandInteractionData.cs index 3c4543a3..e9401485 100644 --- a/Myriad/Types/Application/ApplicationCommandInteractionData.cs +++ b/Myriad/Types/Application/ApplicationCommandInteractionData.cs @@ -2,8 +2,10 @@ { public record ApplicationCommandInteractionData { - public ulong Id { get; init; } - public string Name { get; init; } - public ApplicationCommandInteractionDataOption[] Options { get; init; } + public ulong? Id { get; init; } + public string? Name { get; init; } + public ApplicationCommandInteractionDataOption[]? Options { get; init; } + public string? CustomId { get; init; } + public MessageComponent.ComponentType? ComponentType { get; init; } } } \ No newline at end of file diff --git a/Myriad/Types/Application/Interaction.cs b/Myriad/Types/Application/Interaction.cs index cc269f3a..2b4834d8 100644 --- a/Myriad/Types/Application/Interaction.cs +++ b/Myriad/Types/Application/Interaction.cs @@ -5,7 +5,8 @@ public enum InteractionType { Ping = 1, - ApplicationCommand = 2 + ApplicationCommand = 2, + MessageComponent = 3 } public ulong Id { get; init; } @@ -15,5 +16,6 @@ public ulong ChannelId { get; init; } public GuildMember Member { get; init; } public string Token { get; init; } + public Message? Message { get; init; } } } \ No newline at end of file diff --git a/Myriad/Types/Application/InteractionResponse.cs b/Myriad/Types/Application/InteractionResponse.cs index 12e1259d..9f07e3ec 100644 --- a/Myriad/Types/Application/InteractionResponse.cs +++ b/Myriad/Types/Application/InteractionResponse.cs @@ -5,10 +5,10 @@ public enum ResponseType { Pong = 1, - Acknowledge = 2, - ChannelMessage = 3, ChannelMessageWithSource = 4, - AckWithSource = 5 + DeferredChannelMessageWithSource = 5, + DeferredUpdateMessage = 6, + UpdateMessage = 7 } public ResponseType Type { get; init; } diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index a7cb88c6..1b7ec77d 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -64,6 +64,7 @@ namespace Myriad.Types [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional ReferencedMessage { get; init; } + public MessageComponent[]? Components { get; init; } public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); diff --git a/Myriad/Types/MessageComponent.cs b/Myriad/Types/MessageComponent.cs new file mode 100644 index 00000000..f68e50c9 --- /dev/null +++ b/Myriad/Types/MessageComponent.cs @@ -0,0 +1,29 @@ +namespace Myriad.Types +{ + public record MessageComponent + { + public ComponentType Type { get; init; } + public ButtonStyle? Style { get; init; } + public string? Label { get; init; } + public Emoji? Emoji { get; init; } + public string? CustomId { get; init; } + public string? Url { get; init; } + public bool? Disabled { get; init; } + public MessageComponent[]? Components { get; init; } + + public enum ComponentType + { + ActionRow = 1, + Button = 2 + } + + public enum ButtonStyle + { + Primary = 1, + Secondary = 2, + Success = 3, + Danger = 4, + Link = 5 + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index f9328af4..abc11458 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -109,6 +109,8 @@ namespace PluralKit.Bot await HandleEvent(shard, mdb); if (evt is MessageReactionAddEvent mra) await HandleEvent(shard, mra); + if (evt is InteractionCreateEvent ic) + await HandleEvent(shard, ic); // Update shard status for shards immediately on connect if (evt is ReadyEvent re) diff --git a/PluralKit.Bot/Handlers/InteractionCreated.cs b/PluralKit.Bot/Handlers/InteractionCreated.cs new file mode 100644 index 00000000..4ac75910 --- /dev/null +++ b/PluralKit.Bot/Handlers/InteractionCreated.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; + +using Autofac; + +using Myriad.Gateway; +using Myriad.Types; + +namespace PluralKit.Bot +{ + public class InteractionCreated: IEventHandler + { + private readonly InteractionDispatchService _interactionDispatch; + private readonly ILifetimeScope _services; + + public InteractionCreated(InteractionDispatchService interactionDispatch, ILifetimeScope services) + { + _interactionDispatch = interactionDispatch; + _services = services; + } + + public async Task Handle(Shard shard, InteractionCreateEvent evt) + { + if (evt.Type == Interaction.InteractionType.MessageComponent) + { + var customId = evt.Data?.CustomId; + if (customId != null) + { + var ctx = new InteractionContext(evt, _services); + await _interactionDispatch.Dispatch(customId, ctx); + } + } + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index f96a64a8..28bfac5f 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -72,6 +72,7 @@ namespace PluralKit.Bot builder.RegisterType().As>().As>(); builder.RegisterType().As>(); builder.RegisterType().As>(); + builder.RegisterType().As>(); // Event handler queue builder.RegisterType>().AsSelf().SingleInstance(); @@ -91,6 +92,7 @@ namespace PluralKit.Bot builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); // Sentry stuff builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope(); diff --git a/PluralKit.Bot/Services/InteractionDispatchService.cs b/PluralKit.Bot/Services/InteractionDispatchService.cs new file mode 100644 index 00000000..4579223f --- /dev/null +++ b/PluralKit.Bot/Services/InteractionDispatchService.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +using NodaTime; + +using Serilog; + +namespace PluralKit.Bot +{ + public class InteractionDispatchService: IDisposable + { + private static readonly Duration DefaultExpiry = Duration.FromMinutes(15); + + private readonly ConcurrentDictionary _handlers = new(); + private readonly CancellationTokenSource _cts = new(); + private readonly IClock _clock; + private readonly ILogger _logger; + private readonly Task _cleanupWorker; + + public InteractionDispatchService(IClock clock, ILogger logger) + { + _clock = clock; + _logger = logger.ForContext(); + + _cleanupWorker = CleanupLoop(_cts.Token); + } + + public async ValueTask Dispatch(string customId, InteractionContext context) + { + if (!Guid.TryParse(customId, out var customIdGuid)) + return false; + + if (!_handlers.TryGetValue(customIdGuid, out var handler)) + return false; + + await handler.Callback.Invoke(context); + return true; + } + + public string Register(Func callback, Duration? expiry = null) + { + var key = Guid.NewGuid(); + var handler = new RegisteredInteraction + { + Callback = callback, + Expiry = _clock.GetCurrentInstant() + (expiry ?? DefaultExpiry) + }; + + _handlers[key] = handler; + return key.ToString(); + } + + private async Task CleanupLoop(CancellationToken ct) + { + while (true) + { + DoCleanup(); + await Task.Delay(TimeSpan.FromMinutes(1), ct); + } + } + + private void DoCleanup() + { + var now = _clock.GetCurrentInstant(); + var removedCount = 0; + foreach (var (key, value) in _handlers.ToArray()) + { + if (value.Expiry < now) + { + _handlers.TryRemove(key, out _); + removedCount++; + } + } + + _logger.Debug("Removed {ExpiredInteractions} expired interactions", removedCount); + } + + private struct RegisteredInteraction + { + public Instant Expiry; + public Func Callback; + } + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Utils/InteractionContext.cs b/PluralKit.Bot/Utils/InteractionContext.cs new file mode 100644 index 00000000..70c616d7 --- /dev/null +++ b/PluralKit.Bot/Utils/InteractionContext.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; + +using Autofac; + +using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Types; + +namespace PluralKit.Bot +{ + public class InteractionContext + { + private readonly InteractionCreateEvent _evt; + private readonly ILifetimeScope _services; + + public InteractionContext(InteractionCreateEvent evt, ILifetimeScope services) + { + _evt = evt; + _services = services; + } + + public ulong ChannelId => _evt.ChannelId; + public ulong? MessageId => _evt.Message?.Id; + public GuildMember User => _evt.Member; + public string Token => _evt.Token; + public string? CustomId => _evt.Data?.CustomId; + public InteractionCreateEvent Event => _evt; + + public async Task Reply(string content) + { + await Respond(InteractionResponse.ResponseType.ChannelMessageWithSource, + new InteractionApplicationCommandCallbackData + { + Content = content, + Flags = Message.MessageFlags.Ephemeral + }); + } + + public async Task Respond(InteractionResponse.ResponseType type, InteractionApplicationCommandCallbackData data) + { + var rest = _services.Resolve(); + await rest.CreateInteractionResponse(_evt.Id, _evt.Token, new InteractionResponse {Type = type, Data = data}); + } + } +} \ No newline at end of file