Add basic interactivity framework

This commit is contained in:
Ske 2021-05-30 16:45:29 +02:00
parent b894a9f86e
commit 4bd2d06b0b
12 changed files with 245 additions and 27 deletions

View File

@ -1,6 +1,4 @@
using System.Collections.Generic;
namespace Myriad.Types
namespace Myriad.Types
{
public record ApplicationCommand
{

View File

@ -6,6 +6,6 @@
public string? Name { get; init; }
public ApplicationCommandInteractionDataOption[]? Options { get; init; }
public string? CustomId { get; init; }
public MessageComponent.ComponentType? ComponentType { get; init; }
public ComponentType? ComponentType { get; init; }
}
}

View File

@ -1,15 +1,14 @@
using System.Collections.Generic;
using Myriad.Rest.Types;
using Myriad.Rest.Types;
namespace Myriad.Types
{
public record InteractionApplicationCommandCallbackData
{
public bool? Tts { get; init; }
public string Content { get; init; }
public string? Content { get; init; }
public Embed[]? Embeds { get; init; }
public AllowedMentions? AllowedMentions { get; init; }
public Message.MessageFlags Flags { get; init; }
public MessageComponent[]? Components { get; init; }
}
}

View File

@ -0,0 +1,11 @@
namespace Myriad.Types
{
public enum ButtonStyle
{
Primary = 1,
Secondary = 2,
Success = 3,
Danger = 4,
Link = 5
}
}

View File

@ -0,0 +1,8 @@
namespace Myriad.Types
{
public enum ComponentType
{
ActionRow = 1,
Button = 2
}
}

View File

@ -10,20 +10,5 @@ namespace Myriad.Types
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

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net.Mail;
using System.Text.Json.Serialization;
using Myriad.Utils;

View File

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
using Myriad.Rest.Types.Requests;
using Myriad.Types;
using NodaTime;
namespace PluralKit.Bot.Interactive
{
public abstract class BaseInteractive
{
private readonly Context _ctx;
private readonly List<Button> _buttons = new();
private readonly TaskCompletionSource _tcs = new();
private bool _running;
protected BaseInteractive(Context ctx)
{
_ctx = ctx;
}
public Duration Timeout { get; set; } = Duration.FromMinutes(5);
protected Button AddButton(Func<InteractionContext, Task> handler, string? label = null, ButtonStyle style = ButtonStyle.Secondary, bool disabled = false)
{
var dispatch = _ctx.Services.Resolve<InteractionDispatchService>();
var customId = dispatch.Register(handler, Timeout);
var button = new Button
{
Label = label,
Style = style,
Disabled = disabled,
CustomId = customId
};
_buttons.Add(button);
return button;
}
protected async Task Update(InteractionContext ctx, string? content = null, Embed? embed = null)
{
await ctx.Respond(InteractionResponse.ResponseType.UpdateMessage,
new InteractionApplicationCommandCallbackData
{
Content = content,
Embeds = embed != null ? new[] { embed } : null,
Components = GetComponents()
});
}
protected async Task Finish(InteractionContext ctx)
{
foreach (var button in _buttons)
button.Disabled = true;
await Update(ctx);
_tcs.TrySetResult();
}
protected async Task<Message> Send(string? content = null, Embed? embed = null)
{
return await _ctx.Rest.CreateMessage(_ctx.Channel.Id, new MessageRequest
{
Content = content,
Embed = embed,
Components = GetComponents()
});
}
public MessageComponent[] GetComponents()
{
return new MessageComponent[]
{
new()
{
Type = ComponentType.ActionRow,
Components = _buttons.Select(b => b.ToMessageComponent()).ToArray()
}
};
}
public void Setup(Context ctx)
{
var dispatch = ctx.Services.Resolve<InteractionDispatchService>();
foreach (var button in _buttons)
button.CustomId = dispatch.Register(button.Handler, Timeout);
}
public abstract Task Start();
public async Task Run()
{
if (_running)
throw new InvalidOperationException("Action is already running");
_running = true;
await Start();
var cts = new CancellationTokenSource(Timeout.ToTimeSpan());
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("Action timed out")));
try
{
await _tcs.Task;
}
finally
{
Cleanup();
}
}
private void Cleanup()
{
var dispatch = _ctx.Services.Resolve<InteractionDispatchService>();
foreach (var button in _buttons)
dispatch.Unregister(button.CustomId!);
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Threading.Tasks;
using Myriad.Types;
namespace PluralKit.Bot.Interactive
{
public class Button
{
public string? Label { get; set; }
public ButtonStyle Style { get; set; } = ButtonStyle.Secondary;
public string? CustomId { get; set; }
public bool Disabled { get; set; }
public Func<InteractionContext, Task> Handler { get; init; }
public MessageComponent ToMessageComponent() => new()
{
Type = ComponentType.Button,
Label = Label,
Style = Style,
CustomId = CustomId,
Disabled = Disabled
};
}
}

View File

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using Myriad.Types;
namespace PluralKit.Bot.Interactive
{
public class YesNoPrompt: BaseInteractive
{
public bool? Result { get; private set; }
public ulong? User { get; set; }
public string Message { get; set; } = "Are you sure?";
public string AcceptLabel { get; set; } = "OK";
public ButtonStyle AcceptStyle { get; set; } = ButtonStyle.Primary;
public string CancelLabel { get; set; } = "Cancel";
public ButtonStyle CancelStyle { get; set; } = ButtonStyle.Secondary;
public override async Task Start()
{
AddButton(ctx => OnButtonClick(ctx, true), AcceptLabel, AcceptStyle);
AddButton(ctx => OnButtonClick(ctx, false), CancelLabel, CancelStyle);
await Send(Message);
}
private async Task OnButtonClick(InteractionContext ctx, bool result)
{
if (ctx.User.Id != User)
{
await Update(ctx);
return;
}
Result = result;
await Finish(ctx);
}
public YesNoPrompt(Context ctx): base(ctx)
{
User = ctx.Author.Id;
}
}
}

View File

@ -39,6 +39,14 @@ namespace PluralKit.Bot
return true;
}
public void Unregister(string customId)
{
if (!Guid.TryParse(customId, out var customIdGuid))
return;
_handlers.TryRemove(customIdGuid, out _);
}
public string Register(Func<InteractionContext, Task> callback, Duration? expiry = null)
{
var key = Guid.NewGuid();

View File

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using Autofac;
@ -21,7 +22,8 @@ namespace PluralKit.Bot
public ulong ChannelId => _evt.ChannelId;
public ulong? MessageId => _evt.Message?.Id;
public GuildMember User => _evt.Member;
public GuildMember Member => _evt.Member;
public User User => _evt.Member.User;
public string Token => _evt.Token;
public string? CustomId => _evt.Data?.CustomId;
public InteractionCreateEvent Event => _evt;
@ -36,7 +38,23 @@ namespace PluralKit.Bot
});
}
public async Task Respond(InteractionResponse.ResponseType type, InteractionApplicationCommandCallbackData data)
public async Task Ignore()
{
await Respond(InteractionResponse.ResponseType.DeferredUpdateMessage, new InteractionApplicationCommandCallbackData
{
// Components = _evt.Message.Components
});
}
public async Task Acknowledge()
{
await Respond(InteractionResponse.ResponseType.UpdateMessage, new InteractionApplicationCommandCallbackData
{
Components = Array.Empty<MessageComponent>()
});
}
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});