diff --git a/src/NadekoBot/Modules/Administration/DangerousCommands/CleanupService.cs b/src/NadekoBot/Modules/Administration/DangerousCommands/CleanupService.cs index ba8fb9f90..e57d3a6c1 100644 --- a/src/NadekoBot/Modules/Administration/DangerousCommands/CleanupService.cs +++ b/src/NadekoBot/Modules/Administration/DangerousCommands/CleanupService.cs @@ -12,7 +12,7 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService private TypedKey _keepReportKey = new("cleanup:report"); private TypedKey _keepTriggerKey = new("cleanup:trigger"); private readonly DiscordSocketClient _client; - private ConcurrentDictionary guildIds; + private ConcurrentDictionary guildIds = new(); private readonly IBotCredsProvider _creds; private readonly DbService _db; diff --git a/src/NadekoBot/Modules/Expressions/NadekoExpressions.cs b/src/NadekoBot/Modules/Expressions/NadekoExpressions.cs index 29815a4cb..22bb3b515 100644 --- a/src/NadekoBot/Modules/Expressions/NadekoExpressions.cs +++ b/src/NadekoBot/Modules/Expressions/NadekoExpressions.cs @@ -68,9 +68,9 @@ public partial class NadekoExpressions : NadekoModule [Cmd] - public async Task ExprAdd(string key, [Leftover] string message) + public async Task ExprAdd(string trigger, [Leftover] string response) { - if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrWhiteSpace(response) || string.IsNullOrWhiteSpace(trigger)) { return; } @@ -81,7 +81,7 @@ public partial class NadekoExpressions : NadekoModule return; } - await ExprAddInternalAsync(key, message); + await ExprAddInternalAsync(trigger, response); } [Cmd] diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index 62a1f254d..9ccac5dad 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -74,7 +74,7 @@ public partial class Gambling : GamblingModule var stats = await _gamblingTxTracker.GetAllAsync(); var eb = _sender.CreateEmbed() - .WithOkColor(); + .WithOkColor(); var str = "` Feature `|`   Bet  `|`Paid Out`|`  RoI  `\n"; str += "――――――――――――――――――――\n"; @@ -119,15 +119,15 @@ public partial class Gambling : GamblingModule // [21:03] Bob Page: Kinda remids me of US economy var embed = _sender.CreateEmbed() - .WithTitle(GetText(strs.economy_state)) - .AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot)) - .AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%") - .AddField(GetText(strs.currency_planted), N(ec.Planted)) - .AddField(GetText(strs.owned_waifus_total), N(ec.Waifus)) - .AddField(GetText(strs.bot_currency), N(ec.Bot)) - .AddField(GetText(strs.bank_accounts), N(ec.Bank)) - .AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank)) - .WithOkColor(); + .WithTitle(GetText(strs.economy_state)) + .AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot)) + .AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%") + .AddField(GetText(strs.currency_planted), N(ec.Planted)) + .AddField(GetText(strs.owned_waifus_total), N(ec.Waifus)) + .AddField(GetText(strs.bot_currency), N(ec.Bot)) + .AddField(GetText(strs.bank_accounts), N(ec.Bank)) + .AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank)) + .WithOkColor(); // ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table await Response().Embed(embed).SendAsync(); @@ -155,17 +155,14 @@ public partial class Gambling : GamblingModule } private NadekoInteraction CreateRemindMeInteraction(int period) - { - return _inter + => _inter .Create(ctx.User.Id, - new SimpleInteraction( - new ButtonBuilder( - label: "Remind me", - emote: Emoji.Parse("⏰"), - customId: "timely:remind_me"), - RemindTimelyAction, - DateTime.UtcNow.Add(TimeSpan.FromHours(period)))); - } + new ButtonBuilder( + label: "Remind me", + emote: Emoji.Parse("⏰"), + customId: "timely:remind_me"), + (smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period))) + ); [Cmd] public async Task Timely() @@ -311,9 +308,9 @@ public partial class Gambling : GamblingModule } var embed = _sender.CreateEmbed() - .WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() - ?? $"{userId}"))) - .WithOkColor(); + .WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() + ?? $"{userId}"))) + .WithOkColor(); var sb = new StringBuilder(); foreach (var tr in trs) @@ -408,7 +405,7 @@ public partial class Gambling : GamblingModule await Response().Confirm(strs.has(Format.Code(userId.ToString()), cur)).SendAsync(); } - private async Task BankAction(SocketMessageComponent smc, object _) + private async Task BankAction(SocketMessageComponent smc) { var balance = await _bank.GetBalanceAsync(ctx.User.Id); @@ -419,11 +416,11 @@ public partial class Gambling : GamblingModule } private NadekoInteraction CreateCashInteraction() - => _inter.Create(ctx.User.Id, - new(new( - customId: "cash:bank_show_balance", - emote: new Emoji("🏦")), - BankAction)); + => _inter.Create(ctx.User.Id, + new ButtonBuilder( + customId: "cash:bank_show_balance", + emote: new Emoji("🏦")), + BankAction); [Cmd] [Priority(1)] @@ -732,10 +729,10 @@ public partial class Gambling : GamblingModule } var eb = _sender.CreateEmbed() - .WithAuthor(ctx.User) - .WithDescription(Format.Bold(str)) - .AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture)) - .WithOkColor(); + .WithAuthor(ctx.User) + .WithDescription(Format.Bold(str)) + .AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture)) + .WithOkColor(); await Response().Embed(eb).SendAsync(); } @@ -787,7 +784,7 @@ public partial class Gambling : GamblingModule return await uow.Set().GetTopRichest(_client.CurrentUser.Id, curPage); } } - + var res = Response() .Paginated(); @@ -799,8 +796,9 @@ public partial class Gambling : GamblingModule .CurrentPage(page) .Page((toSend, curPage) => { - var embed = _sender.CreateEmbed().WithOkColor() - .WithTitle(CurrencySign + " " + GetText(strs.leaderboard)); + var embed = _sender.CreateEmbed() + .WithOkColor() + .WithTitle(CurrencySign + " " + GetText(strs.leaderboard)); if (!toSend.Any()) { @@ -923,11 +921,11 @@ public partial class Gambling : GamblingModule } var eb = _sender.CreateEmbed() - .WithOkColor() - .WithDescription(sb.ToString()) - .AddField(GetText(strs.multiplier), $"{result.Multiplier:0.##}x", true) - .AddField(GetText(strs.won), $"{(long)result.Won}", true) - .WithAuthor(ctx.User); + .WithOkColor() + .WithDescription(sb.ToString()) + .AddField(GetText(strs.multiplier), $"{result.Multiplier:0.##}x", true) + .AddField(GetText(strs.won), $"{(long)result.Won}", true) + .WithAuthor(ctx.User); await Response().Embed(eb).SendAsync(); diff --git a/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs b/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs index 7cd41bdee..f8975a353 100644 --- a/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs +++ b/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs @@ -76,9 +76,12 @@ public partial class Gambling .WithOkColor(); var bb = new ButtonBuilder(emote: Emoji.Parse("🔁"), customId: "slot:again", label: "Pull Again"); - var si = new SimpleInteraction(bb, (_, amount) => Slot(amount), amount); + var inter = _inter.Create(ctx.User.Id, bb, smc => + { + smc.DeferAsync(); + return Slot(amount); + }); - var inter = _inter.Create(ctx.User.Id, si); var msg = await ctx.Channel.SendFileAsync(imgStream, "result.png", embed: eb.Build(), diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 1b3cd1d29..f70fbf0a8 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -4,6 +4,7 @@ using NadekoBot.Modules.Help.Services; using Newtonsoft.Json; using System.Text; using Nadeko.Common.Medusa; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; namespace NadekoBot.Modules.Help; @@ -86,11 +87,31 @@ public sealed partial class Help : NadekoModule topLevelModules.Add(m); } + var menu = new SelectMenuBuilder() + .WithPlaceholder("Select a module to see its commands") + .WithCustomId("modules"); + + foreach (var m in topLevelModules) + menu.AddOption(m.Name, m.Name, GetModuleEmoji(m.Name)); + + var inter = _inter.Create(ctx.User.Id, + menu, + async (smc) => + { + await smc.DeferAsync(); + var val = smc.Data.Values.FirstOrDefault(); + if (val is null) + return; + + await Commands(val); + }); + await Response() .Paginated() .Items(topLevelModules) .PageSize(12) .CurrentPage(page) + .Interaction(inter) .AddFooter(false) .Page((items, _) => { @@ -442,7 +463,7 @@ public sealed partial class Help : NadekoModule .SendAsync(); - private Task SelfhostAction(SocketMessageComponent smc, object _) + private Task SelfhostAction(SocketMessageComponent smc) => smc.RespondConfirmAsync(_sender, """ - In case you don't want or cannot Donate to NadekoBot project, but you @@ -460,11 +481,11 @@ public sealed partial class Help : NadekoModule public async Task Donate() { var selfhostInter = _inter.Create(ctx.User.Id, - new SimpleInteraction(new ButtonBuilder( - emote: new Emoji("🖥️"), - customId: "donate:selfhosting", - label: "Selfhosting"), - SelfhostAction)); + new ButtonBuilder( + emote: new Emoji("🖥️"), + customId: "donate:selfhosting", + label: "Selfhosting"), + SelfhostAction); var eb = _sender.CreateEmbed() .WithOkColor() diff --git a/src/NadekoBot/Modules/Utility/Repeater/RepeatCommands.cs b/src/NadekoBot/Modules/Utility/Repeater/RepeatCommands.cs index 83fdd188c..85a0604de 100644 --- a/src/NadekoBot/Modules/Utility/Repeater/RepeatCommands.cs +++ b/src/NadekoBot/Modules/Utility/Repeater/RepeatCommands.cs @@ -117,35 +117,35 @@ public partial class Utility [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(0)] - public Task Repeat(ITextChannel ch, StoopidTime interval, [Leftover] string message) - => Repeat(ch, null, interval, message); + public Task Repeat(ITextChannel channel, StoopidTime interval, [Leftover] string message) + => Repeat(channel, null, interval, message); [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(1)] - public Task Repeat(GuildDateTime dt, [Leftover] string message) - => Repeat(dt, null, message); + public Task Repeat(GuildDateTime timeOfDay, [Leftover] string message) + => Repeat(timeOfDay, null, message); [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(1)] - public Task Repeat(ITextChannel channel, GuildDateTime dt, [Leftover] string message) - => Repeat(channel, dt, null, message); + public Task Repeat(ITextChannel channel, GuildDateTime timeOfDay, [Leftover] string message) + => Repeat(channel, timeOfDay, null, message); [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(2)] - public Task Repeat(GuildDateTime? dt, StoopidTime? interval, [Leftover] string message) - => Repeat(ctx.Channel, dt, interval, message); + public Task Repeat(GuildDateTime? timeOfDay, StoopidTime? interval, [Leftover] string message) + => Repeat(ctx.Channel, timeOfDay, interval, message); [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(3)] - public async Task Repeat(IMessageChannel channel, GuildDateTime? dt, StoopidTime? interval, + public async Task Repeat(IMessageChannel channel, GuildDateTime? timeOfDay, StoopidTime? interval, [Leftover] string message) { if (channel is not ITextChannel txtCh || txtCh.GuildId != ctx.Guild.Id) @@ -155,7 +155,7 @@ public partial class Utility if (!perms.SendMessages) return; - var startTimeOfDay = dt?.InputTimeUtc.TimeOfDay; + var startTimeOfDay = timeOfDay?.InputTimeUtc.TimeOfDay; // if interval not null, that means user specified it (don't change it) // if interval is null set the default to: diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index 07c74d0e4..dba4afff2 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -475,7 +475,8 @@ public partial class Xp : NadekoModule emote: Emoji.Parse("👐"), isDisabled: ownedItem.IsUsing); - var inter = new SimpleInteraction<(string key, XpShopItemType type)?>( + var inter = _inter.Create( + ctx.User.Id, button, OnShopUse, (key, itemType)); @@ -488,7 +489,8 @@ public partial class Xp : NadekoModule "xpshop:buy", emote: Emoji.Parse("💰")); - var inter = new SimpleInteraction<(string key, XpShopItemType type)?>( + var inter = _inter.Create( + ctx.User.Id, button, OnShopBuy, (key, itemType)); @@ -507,10 +509,10 @@ public partial class Xp : NadekoModule NadekoInteraction GetUseInteraction() { return _inter.Create(ctx.User.Id, - new SimpleInteraction( - new ButtonBuilder(label: "Use", customId: "xpshop:use_item", emote: Emoji.Parse("👐")), - async (smc, _) => await XpShopUse(type, key) - )); + new(label: "Use", customId: "xpshop:use_item", emote: Emoji.Parse("👐")), + async (_, state) => await XpShopUse(state.type, state.key), + (type, key) + ); } if (result != BuyResult.Success) @@ -551,11 +553,8 @@ public partial class Xp : NadekoModule await ctx.OkAsync(); } - private async Task OnShopUse(SocketMessageComponent smc, (string? key, XpShopItemType type)? maybeState) + private async Task OnShopUse(SocketMessageComponent smc, (string key, XpShopItemType type) state) { - if (maybeState is not { } state) - return; - var (key, type) = state; var result = await _service.UseShopItemAsync(ctx.User.Id, type, key); @@ -567,11 +566,8 @@ public partial class Xp : NadekoModule } } - private async Task OnShopBuy(SocketMessageComponent smc, (string? key, XpShopItemType type)? maybeState) + private async Task OnShopBuy(SocketMessageComponent smc, (string key, XpShopItemType type) state) { - if (maybeState is not { } state) - return; - var (key, type) = state; var result = await _service.BuyShopItemAsync(ctx.User.Id, type, key); diff --git a/src/NadekoBot/_common/Interaction/INadekoInteractionService.cs b/src/NadekoBot/_common/Interaction/INadekoInteractionService.cs index b4be909a5..88c2eb246 100644 --- a/src/NadekoBot/_common/Interaction/INadekoInteractionService.cs +++ b/src/NadekoBot/_common/Interaction/INadekoInteractionService.cs @@ -2,7 +2,22 @@ public interface INadekoInteractionService { + public NadekoInteraction Create( + ulong userId, + ButtonBuilder button, + Func onTrigger, + bool singleUse = true); + public NadekoInteraction Create( ulong userId, - SimpleInteraction inter); + ButtonBuilder button, + Func onTrigger, + in T state, + bool singleUse = true); + + NadekoInteraction Create( + ulong userId, + SelectMenuBuilder menu, + Func onTrigger, + bool singleUse = true); } \ No newline at end of file diff --git a/src/NadekoBot/_common/Interaction/InteractionHelpers.cs b/src/NadekoBot/_common/Interaction/InteractionHelpers.cs new file mode 100644 index 000000000..39c2cc299 --- /dev/null +++ b/src/NadekoBot/_common/Interaction/InteractionHelpers.cs @@ -0,0 +1,7 @@ +namespace NadekoBot; + +public static class InteractionHelpers +{ + public static readonly IEmote ArrowLeft = Emote.Parse("<:x:1232256519844790302>"); + public static readonly IEmote ArrowRight = Emote.Parse("<:x:1232256515298295838>"); +} \ No newline at end of file diff --git a/src/NadekoBot/_common/Interaction/Models/NadekoButtonInteraction.cs b/src/NadekoBot/_common/Interaction/Models/NadekoButtonInteraction.cs new file mode 100644 index 000000000..7ceb158c7 --- /dev/null +++ b/src/NadekoBot/_common/Interaction/Models/NadekoButtonInteraction.cs @@ -0,0 +1,21 @@ +namespace NadekoBot; + +public sealed class NadekoButtonInteraction : NadekoInteraction +{ + public NadekoButtonInteraction( + DiscordSocketClient client, + ulong authorId, + ButtonBuilder button, + Func onClick, + bool onlyAuthor, + bool singleUse = true) + : base(client, authorId, button.CustomId, onClick, onlyAuthor, singleUse) + { + Button = button; + } + + public ButtonBuilder Button { get; } + + public override void AddTo(ComponentBuilder cb) + => cb.WithButton(Button); +} \ No newline at end of file diff --git a/src/NadekoBot/_common/Interaction/Models/NadekoInteractionExtensions.cs b/src/NadekoBot/_common/Interaction/Models/NadekoInteractionExtensions.cs new file mode 100644 index 000000000..dff0ed2f8 --- /dev/null +++ b/src/NadekoBot/_common/Interaction/Models/NadekoInteractionExtensions.cs @@ -0,0 +1,15 @@ +namespace NadekoBot; + +public static class NadekoInteractionExtensions +{ + public static MessageComponent CreateComponent( + this NadekoInteraction nadekoInteraction + ) + { + var cb = new ComponentBuilder(); + + nadekoInteraction.AddTo(cb); + + return cb.Build(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/_common/Interaction/Models/NadekoSelectInteraction.cs b/src/NadekoBot/_common/Interaction/Models/NadekoSelectInteraction.cs new file mode 100644 index 000000000..4eef84c21 --- /dev/null +++ b/src/NadekoBot/_common/Interaction/Models/NadekoSelectInteraction.cs @@ -0,0 +1,21 @@ +namespace NadekoBot; + +public sealed class NadekoSelectInteraction : NadekoInteraction +{ + public NadekoSelectInteraction( + DiscordSocketClient client, + ulong authorId, + SelectMenuBuilder menu, + Func onClick, + bool onlyAuthor, + bool singleUse = true) + : base(client, authorId, menu.CustomId, onClick, onlyAuthor, singleUse) + { + Menu = menu; + } + + public SelectMenuBuilder Menu { get; } + + public override void AddTo(ComponentBuilder cb) + => cb.WithSelectMenu(Menu); +} \ No newline at end of file diff --git a/src/NadekoBot/_common/Interaction/NadekoInteraction.cs b/src/NadekoBot/_common/Interaction/NadekoInteraction.cs index 83bfe1717..8f783ea0e 100644 --- a/src/NadekoBot/_common/Interaction/NadekoInteraction.cs +++ b/src/NadekoBot/_common/Interaction/NadekoInteraction.cs @@ -1,9 +1,8 @@ namespace NadekoBot; -public sealed class NadekoInteraction +public abstract class NadekoInteraction { private readonly ulong _authorId; - private readonly ButtonBuilder _button; private readonly Func _onClick; private readonly bool _onlyAuthor; public DiscordSocketClient Client { get; } @@ -11,19 +10,24 @@ public sealed class NadekoInteraction private readonly TaskCompletionSource _interactionCompletedSource; private IUserMessage message = null!; + private readonly string _customId; + private readonly bool _singleUse; - public NadekoInteraction(DiscordSocketClient client, + public NadekoInteraction( + DiscordSocketClient client, ulong authorId, - ButtonBuilder button, + string customId, Func onClick, - bool onlyAuthor) + bool onlyAuthor, + bool singleUse = true) { _authorId = authorId; - _button = button; + _customId = customId; _onClick = onClick; _onlyAuthor = onlyAuthor; + _singleUse = singleUse; _interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - + Client = client; } @@ -32,12 +36,15 @@ public sealed class NadekoInteraction message = msg; Client.InteractionCreated += OnInteraction; - await Task.WhenAny(Task.Delay(15_000), _interactionCompletedSource.Task); + if (_singleUse) + await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task); + else + await Task.Delay(30_000); Client.InteractionCreated -= OnInteraction; await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build()); } - + private Task OnInteraction(SocketInteraction arg) { if (arg is not SocketMessageComponent smc) @@ -49,33 +56,25 @@ public sealed class NadekoInteraction if (_onlyAuthor && smc.User.Id != _authorId) return Task.CompletedTask; - if (smc.Data.CustomId != _button.CustomId) + if (smc.Data.CustomId != _customId) return Task.CompletedTask; _ = Task.Run(async () => { - await ExecuteOnActionAsync(smc); - - // this should only be a thing on single-response buttons _interactionCompletedSource.TrySetResult(true); + await ExecuteOnActionAsync(smc); if (!smc.HasResponded) { await smc.DeferAsync(); } }); - + return Task.CompletedTask; } - public MessageComponent CreateComponent() - { - var comp = new ComponentBuilder() - .WithButton(_button); - - return comp.Build(); - } + public abstract void AddTo(ComponentBuilder cb); public Task ExecuteOnActionAsync(SocketMessageComponent smc) => _onClick(smc); diff --git a/src/NadekoBot/_common/Interaction/NadekoInteractionData.cs b/src/NadekoBot/_common/Interaction/NadekoInteractionData.cs deleted file mode 100644 index 1ad8c99e4..000000000 --- a/src/NadekoBot/_common/Interaction/NadekoInteractionData.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NadekoBot; - -/// -/// Represents essential interacation data -/// -/// Emote which will show on a button -/// Custom interaction id -public record NadekoInteractionData(IEmote Emote, string CustomId, string? Text = null); \ No newline at end of file diff --git a/src/NadekoBot/_common/Interaction/NadekoInteractionService.cs b/src/NadekoBot/_common/Interaction/NadekoInteractionService.cs index 5f586b2cb..d6c0f9702 100644 --- a/src/NadekoBot/_common/Interaction/NadekoInteractionService.cs +++ b/src/NadekoBot/_common/Interaction/NadekoInteractionService.cs @@ -9,12 +9,39 @@ public class NadekoInteractionService : INadekoInteractionService, INService _client = client; } + public NadekoInteraction Create( + ulong userId, + ButtonBuilder button, + Func onTrigger, + bool singleUse = true) + => new NadekoButtonInteraction(_client, + userId, + button, + onTrigger, + onlyAuthor: true, + singleUse: singleUse); + public NadekoInteraction Create( ulong userId, - SimpleInteraction inter) - => new NadekoInteraction(_client, + ButtonBuilder button, + Func onTrigger, + in T state, + bool singleUse = true) + => Create(userId, + button, + ((Func>)((data) + => smc => onTrigger(smc, data)))(state), + singleUse); + + public NadekoInteraction Create( + ulong userId, + SelectMenuBuilder menu, + Func onTrigger, + bool singleUse = true) + => new NadekoSelectInteraction(_client, userId, - inter.Button, - inter.TriggerAsync, - onlyAuthor: true); + menu, + onTrigger, + onlyAuthor: true, + singleUse: singleUse); } \ No newline at end of file diff --git a/src/NadekoBot/_common/Interaction/SimpleInteraction.cs b/src/NadekoBot/_common/Interaction/SimpleInteraction.cs deleted file mode 100644 index 1360ef7eb..000000000 --- a/src/NadekoBot/_common/Interaction/SimpleInteraction.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace NadekoBot; - -public static class InteractionHelpers -{ - public static readonly IEmote ArrowLeft = Emote.Parse("<:x:1232256519844790302>"); - public static readonly IEmote ArrowRight = Emote.Parse("<:x:1232256515298295838>"); -} - -public abstract class SimpleInteractionBase -{ - public abstract Task TriggerAsync(SocketMessageComponent smc); - public abstract ButtonBuilder Button { get; } -} - -public class SimpleInteraction : SimpleInteractionBase -{ - public override ButtonBuilder Button { get; } - private readonly Func _onClick; - private readonly T? _state; - - public SimpleInteraction(ButtonBuilder button, Func onClick, T? state = default) - { - Button = button; - _onClick = onClick; - _state = state; - } - - public override async Task TriggerAsync(SocketMessageComponent smc) - { - await _onClick(smc, _state!); - } -} \ No newline at end of file diff --git a/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs b/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs index 91daf9845..de0a7d67a 100644 --- a/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs +++ b/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs @@ -34,34 +34,79 @@ public partial class ResponseBuilder if (_paginationBuilder.AddPaginatedFooter) embed.AddPaginatedFooter(currentPage, lastPage); - SimpleInteractionBase? maybeInter = null; + NadekoInteraction? maybeInter = null; - async Task GetComponentBuilder() + var model = await _builder.BuildAsync(ephemeral); + + async Task<(NadekoButtonInteraction left, NadekoInteraction? extra, NadekoButtonInteraction right)> + GetInteractions() { - var cb = new ComponentBuilder(); + var leftButton = new ButtonBuilder() + .WithStyle(ButtonStyle.Primary) + .WithCustomId(BUTTON_LEFT) + .WithDisabled(lastPage == 0) + .WithEmote(InteractionHelpers.ArrowLeft) + .WithDisabled(currentPage <= 0); - cb.WithButton(new ButtonBuilder() - .WithStyle(ButtonStyle.Primary) - .WithCustomId(BUTTON_LEFT) - .WithDisabled(lastPage == 0) - .WithEmote(InteractionHelpers.ArrowLeft) - .WithDisabled(currentPage <= 0)); + var leftBtnInter = new NadekoButtonInteraction(_client, + model.User?.Id ?? 0, + leftButton, + (smc) => + { + try + { + if (currentPage > 0) + currentPage--; + + _ = UpdatePageAsync(smc); + } + catch (Exception ex) + { + Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message); + } + + return Task.CompletedTask; + }, + true, + singleUse: false); if (_paginationBuilder.InteractionFunc is not null) { maybeInter = await _paginationBuilder.InteractionFunc(currentPage); - - if (maybeInter is not null) - cb.WithButton(maybeInter.Button); } - cb.WithButton(new ButtonBuilder() - .WithStyle(ButtonStyle.Primary) - .WithCustomId(BUTTON_RIGHT) - .WithDisabled(lastPage is not null && (lastPage == 0 || currentPage >= lastPage)) - .WithEmote(InteractionHelpers.ArrowRight)); + var rightButton = new ButtonBuilder() + .WithStyle(ButtonStyle.Primary) + .WithCustomId(BUTTON_RIGHT) + .WithDisabled(lastPage == 0) + .WithEmote(InteractionHelpers.ArrowRight) + .WithDisabled(lastPage == 0 || currentPage > lastPage); - return cb; + var rightBtnInter = new NadekoButtonInteraction(_client, + model.User?.Id ?? 0, + rightButton, + (smc) => + { + try + { + if (currentPage >= lastPage) + return Task.CompletedTask; + + currentPage++; + + _ = UpdatePageAsync(smc); + } + catch (Exception ex) + { + Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message); + } + + return Task.CompletedTask; + }, + true, + singleUse: false); + + return (leftBtnInter, maybeInter, rightBtnInter); } async Task UpdatePageAsync(SocketMessageComponent smc) @@ -71,75 +116,37 @@ public partial class ResponseBuilder if (_paginationBuilder.AddPaginatedFooter) toSend.AddPaginatedFooter(currentPage, lastPage); - var component = (await GetComponentBuilder()).Build(); + var (left, extra, right) = (await GetInteractions()); + + var cb = new ComponentBuilder(); + left.AddTo(cb); + right.AddTo(cb); + extra?.AddTo(cb); await smc.ModifyOriginalResponseAsync(x => { x.Embed = toSend.Build(); - x.Components = component; + x.Components = cb.Build(); }); } - var model = await _builder.BuildAsync(ephemeral); + var (left, extra, right) = await GetInteractions(); + + var cb = new ComponentBuilder(); + left.AddTo(cb); + right.AddTo(cb); + extra?.AddTo(cb); - var component = (await GetComponentBuilder()).Build(); var msg = await model.TargetChannel .SendMessageAsync(model.Text, embed: embed.Build(), - components: component, + components: cb.Build(), messageReference: model.MessageReference); - async Task OnInteractionAsync(SocketInteraction si) - { - try - { - if (si is not SocketMessageComponent smc) - return; - - if (smc.Message.Id != msg.Id) - return; - - await si.DeferAsync(); - - if (smc.User.Id != model.User?.Id) - return; - - if (smc.Data.CustomId == BUTTON_LEFT) - { - if (currentPage == 0) - return; - - --currentPage; - _ = UpdatePageAsync(smc); - } - else if (smc.Data.CustomId == BUTTON_RIGHT) - { - if (currentPage >= lastPage) - return; - - ++currentPage; - _ = UpdatePageAsync(smc); - } - else if (maybeInter is { } inter && inter.Button.CustomId == smc.Data.CustomId) - { - await inter.TriggerAsync(smc); - _ = UpdatePageAsync(smc); - } - } - catch (Exception ex) - { - Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message); - } - } - if (lastPage == 0 && _paginationBuilder.InteractionFunc is null) return; - _client.InteractionCreated += OnInteractionAsync; - - await Task.Delay(30_000); - - _client.InteractionCreated -= OnInteractionAsync; + await Task.WhenAll(left.RunAsync(msg), extra?.RunAsync(msg) ?? Task.CompletedTask, right.RunAsync(msg)); await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build()); } diff --git a/src/NadekoBot/_common/Sender/ResponseBuilder.cs b/src/NadekoBot/_common/Sender/ResponseBuilder.cs index ed68d3ab1..2031be96f 100644 --- a/src/NadekoBot/_common/Sender/ResponseBuilder.cs +++ b/src/NadekoBot/_common/Sender/ResponseBuilder.cs @@ -395,7 +395,7 @@ public sealed class SourcedPaginatedResponseBuilder : PaginatedResponseBuilde return Task.FromResult>(ReadOnlyCollection.Empty); }; - public Func>? InteractionFunc { get; private set; } + public Func>? InteractionFunc { get; private set; } public int? Elems { get; private set; } = 1; public int ItemsPerPage { get; private set; } = 9; @@ -478,13 +478,13 @@ public sealed class SourcedPaginatedResponseBuilder : PaginatedResponseBuilde return paginationSender.SendAsync(IsEphemeral); } - public SourcedPaginatedResponseBuilder Interaction(Func> func) + public SourcedPaginatedResponseBuilder Interaction(Func> func) { InteractionFunc = func; //async (i) => await func(i); return this; } - public SourcedPaginatedResponseBuilder Interaction(SimpleInteractionBase inter) + public SourcedPaginatedResponseBuilder Interaction(NadekoInteraction inter) { InteractionFunc = _ => Task.FromResult(inter); return this; diff --git a/src/NadekoBot/_common/Services/Impl/CommandsUtilityService.cs b/src/NadekoBot/_common/Services/Impl/CommandsUtilityService.cs index bf002fd3c..b3b8920ee 100644 --- a/src/NadekoBot/_common/Services/Impl/CommandsUtilityService.cs +++ b/src/NadekoBot/_common/Services/Impl/CommandsUtilityService.cs @@ -40,19 +40,29 @@ public sealed class CommandsUtilityService : ICommandsUtilityService, INService var culture = _loc.GetCultureInfo(guild); var em = _sender.CreateEmbed() - .AddField(str, $"{com.RealSummary(_strings, _medusae, culture, prefix)}", true); + .AddField(str, $"{com.RealSummary(_strings, _medusae, culture, prefix)}", true); _dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides); var reqs = GetCommandRequirements(com, (GuildPermission?)overrides); if (reqs.Any()) em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs)); + var paramList = _strings.GetCommandStrings(com.Name, culture)?.Params; em .WithOkColor() .AddField(_strings.GetText(strs.usage), string.Join("\n", com.RealRemarksArr(_strings, _medusae, culture, prefix).Map(arg => Format.Code(arg)))) .WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild)); + if (paramList is not null and not []) + { + var pl = paramList + .Select(x => Format.Code($"{prefix}{com.Name} {x.Keys.Select(y => $"<{y}>").Join(' ')}")) + .Join('\n'); + + em.AddField(GetText(strs.overloads, guild), pl); + } + var opt = GetNadekoOptionType(com.Attributes); if (opt is not null) { diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index cab69d144..8b02fbfb0 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -1,6 +1,6 @@ h: - - help - h + - help gencmdlist: - gencmdlist donate: @@ -37,7 +37,7 @@ boost: boostmsg: - boostmsg boostdel: - - boostde + - boostdel logserver: - logserver logignore: diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index 46631b454..e4dbd8e1f 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -211,23 +211,23 @@ repeat: desc: "The amount of time between each repetition." message: desc: "The text to be repeated at the specified intervals or times." - - ch: + - channel: desc: "The channel where the message will be sent." interval: desc: "The amount of time between each repetition." message: desc: "The text to be repeated at the specified intervals or times." - - dt: + - timeOfDay: desc: "The time at which the message should be repeated, either once every specified amount of time or at a specific time of day." message: desc: "The text to be repeated at the specified intervals or times." - channel: desc: "The channel where the message will be repeated." - dt: + timeOfDay: desc: "The time at which the message should be repeated, either once every specified amount of time or at a specific time of day." message: desc: "The text to be repeated at the specified intervals or times." - - dt: + - timeOfDay: desc: "The time at which the message should be repeated, either once every specified amount of time or at a specific time of day." interval: desc: "The amount of time between each repetition." @@ -235,7 +235,7 @@ repeat: desc: "The text to be repeated at the specified intervals or times." - channel: desc: "The channel where the message will be repeated." - dt: + timeOfDay: desc: "The time at which the message should be repeated, either once every specified amount of time or at a specific time of day." interval: desc: "The amount of time between each repetition." @@ -370,10 +370,10 @@ expradd: ex: - '"hello" Hi there %user.mention%' params: - - key: + - trigger: desc: "The trigger word that sets off the response when typed by a user." - message: - desc: "The text of the message that triggers the response when typed by a user." + response: + desc: "The text of the message that shows up when a user types the trigger word." expraddserver: desc: 'Add an expression with a trigger and a response in this server. Bot will post a response whenever someone types the trigger word. Guide here: ' ex: @@ -1257,7 +1257,7 @@ quoteshow: ex: - 123 params: - - id: + - quoteId: desc: "The unique identifier for the quote being queried." quotesearch: desc: 'Shows a random quote given a search query. Partially matches in several ways: 1) Only content of any quote, 2) only by author, 3) keyword and content, 3) or keyword and author' @@ -1278,14 +1278,14 @@ quoteid: ex: - 123456 params: - - id: + - quoteId: desc: "The unique identifier for the quote to be displayed." quotedelete: desc: Deletes a quote with the specified ID. You have to either have the Manage Messages permission or be the creator of the quote to delete it. ex: - 123456 params: - - id: + - quoteId: desc: "The unique identifier for the quote being deleted." quotedeleteauthor: desc: Deletes all quotes by the specified author. If the author is not you, then ManageMessage server permission is required. @@ -4447,4 +4447,4 @@ cleanupguilddata: ex: - '' params: - - {} + - {} \ No newline at end of file diff --git a/src/NadekoBot/data/strings/responses/responses.en-US.json b/src/NadekoBot/data/strings/responses/responses.en-US.json index 41088dfb8..24303ffe5 100644 --- a/src/NadekoBot/data/strings/responses/responses.en-US.json +++ b/src/NadekoBot/data/strings/responses/responses.en-US.json @@ -1099,5 +1099,6 @@ "todo_archive_not_found": "Archived todo list not found.", "todo_archived_list": "Archived Todo List", "search_results": "Search results", - "queue_search_results": "Type the number of the search result to queue up that track." + "queue_search_results": "Type the number of the search result to queue up that track.", + "overloads": "Overloads" }