Add preliminary support for buttons

This commit is contained in:
Ske 2021-05-26 22:27:52 +02:00
parent 0b91e71384
commit d7c0592947
11 changed files with 217 additions and 7 deletions

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }

View File

@ -64,6 +64,7 @@ namespace Myriad.Types
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<Message?> ReferencedMessage { get; init; }
public MessageComponent[]? Components { get; init; }
public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId);

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -0,0 +1,34 @@
using System.Threading.Tasks;
using Autofac;
using Myriad.Gateway;
using Myriad.Types;
namespace PluralKit.Bot
{
public class InteractionCreated: IEventHandler<InteractionCreateEvent>
{
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);
}
}
}
}
}

View File

@ -72,6 +72,7 @@ namespace PluralKit.Bot
builder.RegisterType<MessageDeleted>().As<IEventHandler<MessageDeleteEvent>>().As<IEventHandler<MessageDeleteBulkEvent>>();
builder.RegisterType<MessageEdited>().As<IEventHandler<MessageUpdateEvent>>();
builder.RegisterType<ReactionAdded>().As<IEventHandler<MessageReactionAddEvent>>();
builder.RegisterType<InteractionCreated>().As<IEventHandler<InteractionCreateEvent>>();
// Event handler queue
builder.RegisterType<HandlerQueue<MessageCreateEvent>>().AsSelf().SingleInstance();
@ -91,6 +92,7 @@ namespace PluralKit.Bot
builder.RegisterType<LoggerCleanService>().AsSelf().SingleInstance();
builder.RegisterType<ErrorMessageService>().AsSelf().SingleInstance();
builder.RegisterType<CommandMessageService>().AsSelf().SingleInstance();
builder.RegisterType<InteractionDispatchService>().AsSelf().SingleInstance();
// Sentry stuff
builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope();

View File

@ -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<Guid, RegisteredInteraction> _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<InteractionDispatchService>();
_cleanupWorker = CleanupLoop(_cts.Token);
}
public async ValueTask<bool> 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<InteractionContext, Task> 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<InteractionContext, Task> Callback;
}
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}
}

View File

@ -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<DiscordApiClient>();
await rest.CreateInteractionResponse(_evt.Id, _evt.Token, new InteractionResponse {Type = type, Data = data});
}
}
}