Add basic interactivity framework
This commit is contained in:
		| @@ -1,6 +1,4 @@ | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Myriad.Types | ||||
| namespace Myriad.Types | ||||
| { | ||||
|     public record ApplicationCommand | ||||
|     { | ||||
|   | ||||
| @@ -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; } | ||||
|     } | ||||
| } | ||||
| @@ -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; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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 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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Net.Mail; | ||||
| using System.Text.Json.Serialization; | ||||
|  | ||||
| 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; | ||||
|         } | ||||
|  | ||||
|         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(); | ||||
|   | ||||
| @@ -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}); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user