Add basic interactivity framework
This commit is contained in:
parent
b894a9f86e
commit
4bd2d06b0b
@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
namespace Myriad.Types
|
||||||
|
|
||||||
namespace Myriad.Types
|
|
||||||
{
|
{
|
||||||
public record ApplicationCommand
|
public record ApplicationCommand
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
public string? Name { get; init; }
|
public string? Name { get; init; }
|
||||||
public ApplicationCommandInteractionDataOption[]? Options { get; init; }
|
public ApplicationCommandInteractionDataOption[]? Options { get; init; }
|
||||||
public string? CustomId { get; init; }
|
public string? CustomId { get; init; }
|
||||||
public MessageComponent.ComponentType? ComponentType { get; init; }
|
public ComponentType? ComponentType { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,15 +1,14 @@
|
|||||||
using System.Collections.Generic;
|
using Myriad.Rest.Types;
|
||||||
|
|
||||||
using Myriad.Rest.Types;
|
|
||||||
|
|
||||||
namespace Myriad.Types
|
namespace Myriad.Types
|
||||||
{
|
{
|
||||||
public record InteractionApplicationCommandCallbackData
|
public record InteractionApplicationCommandCallbackData
|
||||||
{
|
{
|
||||||
public bool? Tts { get; init; }
|
public bool? Tts { get; init; }
|
||||||
public string Content { get; init; }
|
public string? Content { get; init; }
|
||||||
public Embed[]? Embeds { get; init; }
|
public Embed[]? Embeds { get; init; }
|
||||||
public AllowedMentions? AllowedMentions { get; init; }
|
public AllowedMentions? AllowedMentions { get; init; }
|
||||||
public Message.MessageFlags Flags { get; init; }
|
public Message.MessageFlags Flags { get; init; }
|
||||||
|
public MessageComponent[]? Components { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
11
Myriad/Types/Component/ButtonStyle.cs
Normal file
11
Myriad/Types/Component/ButtonStyle.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Myriad.Types
|
||||||
|
{
|
||||||
|
public enum ButtonStyle
|
||||||
|
{
|
||||||
|
Primary = 1,
|
||||||
|
Secondary = 2,
|
||||||
|
Success = 3,
|
||||||
|
Danger = 4,
|
||||||
|
Link = 5
|
||||||
|
}
|
||||||
|
}
|
8
Myriad/Types/Component/ComponentType.cs
Normal file
8
Myriad/Types/Component/ComponentType.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Myriad.Types
|
||||||
|
{
|
||||||
|
public enum ComponentType
|
||||||
|
{
|
||||||
|
ActionRow = 1,
|
||||||
|
Button = 2
|
||||||
|
}
|
||||||
|
}
|
@ -10,20 +10,5 @@ namespace Myriad.Types
|
|||||||
public string? Url { get; init; }
|
public string? Url { get; init; }
|
||||||
public bool? Disabled { get; init; }
|
public bool? Disabled { get; init; }
|
||||||
public MessageComponent[]? Components { 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Mail;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using Myriad.Utils;
|
using Myriad.Utils;
|
||||||
|
125
PluralKit.Bot/Interactive/BaseInteractive.cs
Normal file
125
PluralKit.Bot/Interactive/BaseInteractive.cs
Normal 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!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
PluralKit.Bot/Interactive/Button.cs
Normal file
25
PluralKit.Bot/Interactive/Button.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
43
PluralKit.Bot/Interactive/YesNoPrompt.cs
Normal file
43
PluralKit.Bot/Interactive/YesNoPrompt.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,14 @@ namespace PluralKit.Bot
|
|||||||
return true;
|
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)
|
public string Register(Func<InteractionContext, Task> callback, Duration? expiry = null)
|
||||||
{
|
{
|
||||||
var key = Guid.NewGuid();
|
var key = Guid.NewGuid();
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Autofac;
|
using Autofac;
|
||||||
@ -21,7 +22,8 @@ namespace PluralKit.Bot
|
|||||||
|
|
||||||
public ulong ChannelId => _evt.ChannelId;
|
public ulong ChannelId => _evt.ChannelId;
|
||||||
public ulong? MessageId => _evt.Message?.Id;
|
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 Token => _evt.Token;
|
||||||
public string? CustomId => _evt.Data?.CustomId;
|
public string? CustomId => _evt.Data?.CustomId;
|
||||||
public InteractionCreateEvent Event => _evt;
|
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>();
|
var rest = _services.Resolve<DiscordApiClient>();
|
||||||
await rest.CreateInteractionResponse(_evt.Id, _evt.Token, new InteractionResponse {Type = type, Data = data});
|
await rest.CreateInteractionResponse(_evt.Id, _evt.Token, new InteractionResponse {Type = type, Data = data});
|
||||||
|
Loading…
Reference in New Issue
Block a user