fix: Fixed .h not working on some commands

add: Added select menu for the .mdls command
dev: Reworked the way interactions are created and sent. It is much better but far from perfect
This commit is contained in:
Kwoth
2024-05-19 23:24:52 +00:00
parent 78b328dc18
commit e2066f433f
22 changed files with 335 additions and 234 deletions

View File

@@ -12,7 +12,7 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
private TypedKey<KeepReport> _keepReportKey = new("cleanup:report"); private TypedKey<KeepReport> _keepReportKey = new("cleanup:report");
private TypedKey<bool> _keepTriggerKey = new("cleanup:trigger"); private TypedKey<bool> _keepTriggerKey = new("cleanup:trigger");
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private ConcurrentDictionary<int, ulong[]> guildIds; private ConcurrentDictionary<int, ulong[]> guildIds = new();
private readonly IBotCredsProvider _creds; private readonly IBotCredsProvider _creds;
private readonly DbService _db; private readonly DbService _db;

View File

@@ -68,9 +68,9 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
[Cmd] [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; return;
} }
@@ -81,7 +81,7 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
return; return;
} }
await ExprAddInternalAsync(key, message); await ExprAddInternalAsync(trigger, response);
} }
[Cmd] [Cmd]

View File

@@ -74,7 +74,7 @@ public partial class Gambling : GamblingModule<GamblingService>
var stats = await _gamblingTxTracker.GetAllAsync(); var stats = await _gamblingTxTracker.GetAllAsync();
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithOkColor(); .WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n"; var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
str += "――――――――――――――――――――\n"; str += "――――――――――――――――――――\n";
@@ -119,15 +119,15 @@ public partial class Gambling : GamblingModule<GamblingService>
// [21:03] Bob Page: Kinda remids me of US economy // [21:03] Bob Page: Kinda remids me of US economy
var embed = _sender.CreateEmbed() var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.economy_state)) .WithTitle(GetText(strs.economy_state))
.AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot)) .AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot))
.AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%") .AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
.AddField(GetText(strs.currency_planted), N(ec.Planted)) .AddField(GetText(strs.currency_planted), N(ec.Planted))
.AddField(GetText(strs.owned_waifus_total), N(ec.Waifus)) .AddField(GetText(strs.owned_waifus_total), N(ec.Waifus))
.AddField(GetText(strs.bot_currency), N(ec.Bot)) .AddField(GetText(strs.bot_currency), N(ec.Bot))
.AddField(GetText(strs.bank_accounts), N(ec.Bank)) .AddField(GetText(strs.bank_accounts), N(ec.Bank))
.AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank)) .AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank))
.WithOkColor(); .WithOkColor();
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table // 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(); await Response().Embed(embed).SendAsync();
@@ -155,17 +155,14 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
private NadekoInteraction CreateRemindMeInteraction(int period) private NadekoInteraction CreateRemindMeInteraction(int period)
{ => _inter
return _inter
.Create(ctx.User.Id, .Create(ctx.User.Id,
new SimpleInteraction<DateTime>( new ButtonBuilder(
new ButtonBuilder( label: "Remind me",
label: "Remind me", emote: Emoji.Parse("⏰"),
emote: Emoji.Parse("⏰"), customId: "timely:remind_me"),
customId: "timely:remind_me"), (smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
RemindTimelyAction, );
DateTime.UtcNow.Add(TimeSpan.FromHours(period))));
}
[Cmd] [Cmd]
public async Task Timely() public async Task Timely()
@@ -311,9 +308,9 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
var embed = _sender.CreateEmbed() var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() .WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}"))) ?? $"{userId}")))
.WithOkColor(); .WithOkColor();
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var tr in trs) foreach (var tr in trs)
@@ -408,7 +405,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await Response().Confirm(strs.has(Format.Code(userId.ToString()), cur)).SendAsync(); 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); var balance = await _bank.GetBalanceAsync(ctx.User.Id);
@@ -419,11 +416,11 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
private NadekoInteraction CreateCashInteraction() private NadekoInteraction CreateCashInteraction()
=> _inter.Create<object>(ctx.User.Id, => _inter.Create(ctx.User.Id,
new(new( new ButtonBuilder(
customId: "cash:bank_show_balance", customId: "cash:bank_show_balance",
emote: new Emoji("🏦")), emote: new Emoji("🏦")),
BankAction)); BankAction);
[Cmd] [Cmd]
[Priority(1)] [Priority(1)]
@@ -732,10 +729,10 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithDescription(Format.Bold(str)) .WithDescription(Format.Bold(str))
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture)) .AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture))
.WithOkColor(); .WithOkColor();
await Response().Embed(eb).SendAsync(); await Response().Embed(eb).SendAsync();
} }
@@ -787,7 +784,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return await uow.Set<DiscordUser>().GetTopRichest(_client.CurrentUser.Id, curPage); return await uow.Set<DiscordUser>().GetTopRichest(_client.CurrentUser.Id, curPage);
} }
} }
var res = Response() var res = Response()
.Paginated(); .Paginated();
@@ -799,8 +796,9 @@ public partial class Gambling : GamblingModule<GamblingService>
.CurrentPage(page) .CurrentPage(page)
.Page((toSend, curPage) => .Page((toSend, curPage) =>
{ {
var embed = _sender.CreateEmbed().WithOkColor() var embed = _sender.CreateEmbed()
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard)); .WithOkColor()
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
if (!toSend.Any()) if (!toSend.Any())
{ {
@@ -923,11 +921,11 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(sb.ToString()) .WithDescription(sb.ToString())
.AddField(GetText(strs.multiplier), $"{result.Multiplier:0.##}x", true) .AddField(GetText(strs.multiplier), $"{result.Multiplier:0.##}x", true)
.AddField(GetText(strs.won), $"{(long)result.Won}", true) .AddField(GetText(strs.won), $"{(long)result.Won}", true)
.WithAuthor(ctx.User); .WithAuthor(ctx.User);
await Response().Embed(eb).SendAsync(); await Response().Embed(eb).SendAsync();

View File

@@ -76,9 +76,12 @@ public partial class Gambling
.WithOkColor(); .WithOkColor();
var bb = new ButtonBuilder(emote: Emoji.Parse("🔁"), customId: "slot:again", label: "Pull Again"); var bb = new ButtonBuilder(emote: Emoji.Parse("🔁"), customId: "slot:again", label: "Pull Again");
var si = new SimpleInteraction<long>(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, var msg = await ctx.Channel.SendFileAsync(imgStream,
"result.png", "result.png",
embed: eb.Build(), embed: eb.Build(),

View File

@@ -4,6 +4,7 @@ using NadekoBot.Modules.Help.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Text; using System.Text;
using Nadeko.Common.Medusa; using Nadeko.Common.Medusa;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
namespace NadekoBot.Modules.Help; namespace NadekoBot.Modules.Help;
@@ -86,11 +87,31 @@ public sealed partial class Help : NadekoModule<HelpService>
topLevelModules.Add(m); 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() await Response()
.Paginated() .Paginated()
.Items(topLevelModules) .Items(topLevelModules)
.PageSize(12) .PageSize(12)
.CurrentPage(page) .CurrentPage(page)
.Interaction(inter)
.AddFooter(false) .AddFooter(false)
.Page((items, _) => .Page((items, _) =>
{ {
@@ -442,7 +463,7 @@ public sealed partial class Help : NadekoModule<HelpService>
.SendAsync(); .SendAsync();
private Task SelfhostAction(SocketMessageComponent smc, object _) private Task SelfhostAction(SocketMessageComponent smc)
=> smc.RespondConfirmAsync(_sender, => smc.RespondConfirmAsync(_sender,
""" """
- In case you don't want or cannot Donate to NadekoBot project, but you - In case you don't want or cannot Donate to NadekoBot project, but you
@@ -460,11 +481,11 @@ public sealed partial class Help : NadekoModule<HelpService>
public async Task Donate() public async Task Donate()
{ {
var selfhostInter = _inter.Create(ctx.User.Id, var selfhostInter = _inter.Create(ctx.User.Id,
new SimpleInteraction<object>(new ButtonBuilder( new ButtonBuilder(
emote: new Emoji("🖥️"), emote: new Emoji("🖥️"),
customId: "donate:selfhosting", customId: "donate:selfhosting",
label: "Selfhosting"), label: "Selfhosting"),
SelfhostAction)); SelfhostAction);
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithOkColor() .WithOkColor()

View File

@@ -117,35 +117,35 @@ public partial class Utility
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(0)] [Priority(0)]
public Task Repeat(ITextChannel ch, StoopidTime interval, [Leftover] string message) public Task Repeat(ITextChannel channel, StoopidTime interval, [Leftover] string message)
=> Repeat(ch, null, interval, message); => Repeat(channel, null, interval, message);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(1)] [Priority(1)]
public Task Repeat(GuildDateTime dt, [Leftover] string message) public Task Repeat(GuildDateTime timeOfDay, [Leftover] string message)
=> Repeat(dt, null, message); => Repeat(timeOfDay, null, message);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(1)] [Priority(1)]
public Task Repeat(ITextChannel channel, GuildDateTime dt, [Leftover] string message) public Task Repeat(ITextChannel channel, GuildDateTime timeOfDay, [Leftover] string message)
=> Repeat(channel, dt, null, message); => Repeat(channel, timeOfDay, null, message);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(2)] [Priority(2)]
public Task Repeat(GuildDateTime? dt, StoopidTime? interval, [Leftover] string message) public Task Repeat(GuildDateTime? timeOfDay, StoopidTime? interval, [Leftover] string message)
=> Repeat(ctx.Channel, dt, interval, message); => Repeat(ctx.Channel, timeOfDay, interval, message);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(3)] [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) [Leftover] string message)
{ {
if (channel is not ITextChannel txtCh || txtCh.GuildId != ctx.Guild.Id) if (channel is not ITextChannel txtCh || txtCh.GuildId != ctx.Guild.Id)
@@ -155,7 +155,7 @@ public partial class Utility
if (!perms.SendMessages) if (!perms.SendMessages)
return; 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 not null, that means user specified it (don't change it)
// if interval is null set the default to: // if interval is null set the default to:

View File

@@ -475,7 +475,8 @@ public partial class Xp : NadekoModule<XpService>
emote: Emoji.Parse("👐"), emote: Emoji.Parse("👐"),
isDisabled: ownedItem.IsUsing); isDisabled: ownedItem.IsUsing);
var inter = new SimpleInteraction<(string key, XpShopItemType type)?>( var inter = _inter.Create(
ctx.User.Id,
button, button,
OnShopUse, OnShopUse,
(key, itemType)); (key, itemType));
@@ -488,7 +489,8 @@ public partial class Xp : NadekoModule<XpService>
"xpshop:buy", "xpshop:buy",
emote: Emoji.Parse("💰")); emote: Emoji.Parse("💰"));
var inter = new SimpleInteraction<(string key, XpShopItemType type)?>( var inter = _inter.Create(
ctx.User.Id,
button, button,
OnShopBuy, OnShopBuy,
(key, itemType)); (key, itemType));
@@ -507,10 +509,10 @@ public partial class Xp : NadekoModule<XpService>
NadekoInteraction GetUseInteraction() NadekoInteraction GetUseInteraction()
{ {
return _inter.Create(ctx.User.Id, return _inter.Create(ctx.User.Id,
new SimpleInteraction<object>( new(label: "Use", customId: "xpshop:use_item", emote: Emoji.Parse("👐")),
new ButtonBuilder(label: "Use", customId: "xpshop:use_item", emote: Emoji.Parse("👐")), async (_, state) => await XpShopUse(state.type, state.key),
async (smc, _) => await XpShopUse(type, key) (type, key)
)); );
} }
if (result != BuyResult.Success) if (result != BuyResult.Success)
@@ -551,11 +553,8 @@ public partial class Xp : NadekoModule<XpService>
await ctx.OkAsync(); 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 (key, type) = state;
var result = await _service.UseShopItemAsync(ctx.User.Id, type, key); var result = await _service.UseShopItemAsync(ctx.User.Id, type, key);
@@ -567,11 +566,8 @@ public partial class Xp : NadekoModule<XpService>
} }
} }
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 (key, type) = state;
var result = await _service.BuyShopItemAsync(ctx.User.Id, type, key); var result = await _service.BuyShopItemAsync(ctx.User.Id, type, key);

View File

@@ -2,7 +2,22 @@
public interface INadekoInteractionService public interface INadekoInteractionService
{ {
public NadekoInteraction Create(
ulong userId,
ButtonBuilder button,
Func<SocketMessageComponent, Task> onTrigger,
bool singleUse = true);
public NadekoInteraction Create<T>( public NadekoInteraction Create<T>(
ulong userId, ulong userId,
SimpleInteraction<T> inter); ButtonBuilder button,
Func<SocketMessageComponent, T, Task> onTrigger,
in T state,
bool singleUse = true);
NadekoInteraction Create(
ulong userId,
SelectMenuBuilder menu,
Func<SocketMessageComponent, Task> onTrigger,
bool singleUse = true);
} }

View File

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

View File

@@ -0,0 +1,21 @@
namespace NadekoBot;
public sealed class NadekoButtonInteraction : NadekoInteraction
{
public NadekoButtonInteraction(
DiscordSocketClient client,
ulong authorId,
ButtonBuilder button,
Func<SocketMessageComponent, Task> 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);
}

View File

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

View File

@@ -0,0 +1,21 @@
namespace NadekoBot;
public sealed class NadekoSelectInteraction : NadekoInteraction
{
public NadekoSelectInteraction(
DiscordSocketClient client,
ulong authorId,
SelectMenuBuilder menu,
Func<SocketMessageComponent, Task> 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);
}

View File

@@ -1,9 +1,8 @@
namespace NadekoBot; namespace NadekoBot;
public sealed class NadekoInteraction public abstract class NadekoInteraction
{ {
private readonly ulong _authorId; private readonly ulong _authorId;
private readonly ButtonBuilder _button;
private readonly Func<SocketMessageComponent, Task> _onClick; private readonly Func<SocketMessageComponent, Task> _onClick;
private readonly bool _onlyAuthor; private readonly bool _onlyAuthor;
public DiscordSocketClient Client { get; } public DiscordSocketClient Client { get; }
@@ -11,19 +10,24 @@ public sealed class NadekoInteraction
private readonly TaskCompletionSource<bool> _interactionCompletedSource; private readonly TaskCompletionSource<bool> _interactionCompletedSource;
private IUserMessage message = null!; private IUserMessage message = null!;
private readonly string _customId;
private readonly bool _singleUse;
public NadekoInteraction(DiscordSocketClient client, public NadekoInteraction(
DiscordSocketClient client,
ulong authorId, ulong authorId,
ButtonBuilder button, string customId,
Func<SocketMessageComponent, Task> onClick, Func<SocketMessageComponent, Task> onClick,
bool onlyAuthor) bool onlyAuthor,
bool singleUse = true)
{ {
_authorId = authorId; _authorId = authorId;
_button = button; _customId = customId;
_onClick = onClick; _onClick = onClick;
_onlyAuthor = onlyAuthor; _onlyAuthor = onlyAuthor;
_singleUse = singleUse;
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); _interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
Client = client; Client = client;
} }
@@ -32,12 +36,15 @@ public sealed class NadekoInteraction
message = msg; message = msg;
Client.InteractionCreated += OnInteraction; 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; Client.InteractionCreated -= OnInteraction;
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build()); await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
} }
private Task OnInteraction(SocketInteraction arg) private Task OnInteraction(SocketInteraction arg)
{ {
if (arg is not SocketMessageComponent smc) if (arg is not SocketMessageComponent smc)
@@ -49,33 +56,25 @@ public sealed class NadekoInteraction
if (_onlyAuthor && smc.User.Id != _authorId) if (_onlyAuthor && smc.User.Id != _authorId)
return Task.CompletedTask; return Task.CompletedTask;
if (smc.Data.CustomId != _button.CustomId) if (smc.Data.CustomId != _customId)
return Task.CompletedTask; return Task.CompletedTask;
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
await ExecuteOnActionAsync(smc);
// this should only be a thing on single-response buttons
_interactionCompletedSource.TrySetResult(true); _interactionCompletedSource.TrySetResult(true);
await ExecuteOnActionAsync(smc);
if (!smc.HasResponded) if (!smc.HasResponded)
{ {
await smc.DeferAsync(); await smc.DeferAsync();
} }
}); });
return Task.CompletedTask; return Task.CompletedTask;
} }
public MessageComponent CreateComponent() public abstract void AddTo(ComponentBuilder cb);
{
var comp = new ComponentBuilder()
.WithButton(_button);
return comp.Build();
}
public Task ExecuteOnActionAsync(SocketMessageComponent smc) public Task ExecuteOnActionAsync(SocketMessageComponent smc)
=> _onClick(smc); => _onClick(smc);

View File

@@ -1,8 +0,0 @@
namespace NadekoBot;
/// <summary>
/// Represents essential interacation data
/// </summary>
/// <param name="Emote">Emote which will show on a button</param>
/// <param name="CustomId">Custom interaction id</param>
public record NadekoInteractionData(IEmote Emote, string CustomId, string? Text = null);

View File

@@ -9,12 +9,39 @@ public class NadekoInteractionService : INadekoInteractionService, INService
_client = client; _client = client;
} }
public NadekoInteraction Create(
ulong userId,
ButtonBuilder button,
Func<SocketMessageComponent, Task> onTrigger,
bool singleUse = true)
=> new NadekoButtonInteraction(_client,
userId,
button,
onTrigger,
onlyAuthor: true,
singleUse: singleUse);
public NadekoInteraction Create<T>( public NadekoInteraction Create<T>(
ulong userId, ulong userId,
SimpleInteraction<T> inter) ButtonBuilder button,
=> new NadekoInteraction(_client, Func<SocketMessageComponent, T, Task> onTrigger,
in T state,
bool singleUse = true)
=> Create(userId,
button,
((Func<T, Func<SocketMessageComponent, Task>>)((data)
=> smc => onTrigger(smc, data)))(state),
singleUse);
public NadekoInteraction Create(
ulong userId,
SelectMenuBuilder menu,
Func<SocketMessageComponent, Task> onTrigger,
bool singleUse = true)
=> new NadekoSelectInteraction(_client,
userId, userId,
inter.Button, menu,
inter.TriggerAsync, onTrigger,
onlyAuthor: true); onlyAuthor: true,
singleUse: singleUse);
} }

View File

@@ -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<T> : SimpleInteractionBase
{
public override ButtonBuilder Button { get; }
private readonly Func<SocketMessageComponent, T, Task> _onClick;
private readonly T? _state;
public SimpleInteraction(ButtonBuilder button, Func<SocketMessageComponent, T?, Task> onClick, T? state = default)
{
Button = button;
_onClick = onClick;
_state = state;
}
public override async Task TriggerAsync(SocketMessageComponent smc)
{
await _onClick(smc, _state!);
}
}

View File

@@ -34,34 +34,79 @@ public partial class ResponseBuilder
if (_paginationBuilder.AddPaginatedFooter) if (_paginationBuilder.AddPaginatedFooter)
embed.AddPaginatedFooter(currentPage, lastPage); embed.AddPaginatedFooter(currentPage, lastPage);
SimpleInteractionBase? maybeInter = null; NadekoInteraction? maybeInter = null;
async Task<ComponentBuilder> 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() var leftBtnInter = new NadekoButtonInteraction(_client,
.WithStyle(ButtonStyle.Primary) model.User?.Id ?? 0,
.WithCustomId(BUTTON_LEFT) leftButton,
.WithDisabled(lastPage == 0) (smc) =>
.WithEmote(InteractionHelpers.ArrowLeft) {
.WithDisabled(currentPage <= 0)); 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) if (_paginationBuilder.InteractionFunc is not null)
{ {
maybeInter = await _paginationBuilder.InteractionFunc(currentPage); maybeInter = await _paginationBuilder.InteractionFunc(currentPage);
if (maybeInter is not null)
cb.WithButton(maybeInter.Button);
} }
cb.WithButton(new ButtonBuilder() var rightButton = new ButtonBuilder()
.WithStyle(ButtonStyle.Primary) .WithStyle(ButtonStyle.Primary)
.WithCustomId(BUTTON_RIGHT) .WithCustomId(BUTTON_RIGHT)
.WithDisabled(lastPage is not null && (lastPage == 0 || currentPage >= lastPage)) .WithDisabled(lastPage == 0)
.WithEmote(InteractionHelpers.ArrowRight)); .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) async Task UpdatePageAsync(SocketMessageComponent smc)
@@ -71,75 +116,37 @@ public partial class ResponseBuilder
if (_paginationBuilder.AddPaginatedFooter) if (_paginationBuilder.AddPaginatedFooter)
toSend.AddPaginatedFooter(currentPage, lastPage); 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 => await smc.ModifyOriginalResponseAsync(x =>
{ {
x.Embed = toSend.Build(); 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 var msg = await model.TargetChannel
.SendMessageAsync(model.Text, .SendMessageAsync(model.Text,
embed: embed.Build(), embed: embed.Build(),
components: component, components: cb.Build(),
messageReference: model.MessageReference); 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) if (lastPage == 0 && _paginationBuilder.InteractionFunc is null)
return; return;
_client.InteractionCreated += OnInteractionAsync; await Task.WhenAll(left.RunAsync(msg), extra?.RunAsync(msg) ?? Task.CompletedTask, right.RunAsync(msg));
await Task.Delay(30_000);
_client.InteractionCreated -= OnInteractionAsync;
await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build()); await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build());
} }

View File

@@ -395,7 +395,7 @@ public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilde
return Task.FromResult<IReadOnlyCollection<T>>(ReadOnlyCollection<T>.Empty); return Task.FromResult<IReadOnlyCollection<T>>(ReadOnlyCollection<T>.Empty);
}; };
public Func<int, Task<SimpleInteractionBase>>? InteractionFunc { get; private set; } public Func<int, Task<NadekoInteraction>>? InteractionFunc { get; private set; }
public int? Elems { get; private set; } = 1; public int? Elems { get; private set; } = 1;
public int ItemsPerPage { get; private set; } = 9; public int ItemsPerPage { get; private set; } = 9;
@@ -478,13 +478,13 @@ public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilde
return paginationSender.SendAsync(IsEphemeral); return paginationSender.SendAsync(IsEphemeral);
} }
public SourcedPaginatedResponseBuilder<T> Interaction(Func<int, Task<SimpleInteractionBase>> func) public SourcedPaginatedResponseBuilder<T> Interaction(Func<int, Task<NadekoInteraction>> func)
{ {
InteractionFunc = func; //async (i) => await func(i); InteractionFunc = func; //async (i) => await func(i);
return this; return this;
} }
public SourcedPaginatedResponseBuilder<T> Interaction(SimpleInteractionBase inter) public SourcedPaginatedResponseBuilder<T> Interaction(NadekoInteraction inter)
{ {
InteractionFunc = _ => Task.FromResult(inter); InteractionFunc = _ => Task.FromResult(inter);
return this; return this;

View File

@@ -40,19 +40,29 @@ public sealed class CommandsUtilityService : ICommandsUtilityService, INService
var culture = _loc.GetCultureInfo(guild); var culture = _loc.GetCultureInfo(guild);
var em = _sender.CreateEmbed() 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); _dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides);
var reqs = GetCommandRequirements(com, (GuildPermission?)overrides); var reqs = GetCommandRequirements(com, (GuildPermission?)overrides);
if (reqs.Any()) if (reqs.Any())
em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs)); em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs));
var paramList = _strings.GetCommandStrings(com.Name, culture)?.Params;
em em
.WithOkColor() .WithOkColor()
.AddField(_strings.GetText(strs.usage), .AddField(_strings.GetText(strs.usage),
string.Join("\n", com.RealRemarksArr(_strings, _medusae, culture, prefix).Map(arg => Format.Code(arg)))) string.Join("\n", com.RealRemarksArr(_strings, _medusae, culture, prefix).Map(arg => Format.Code(arg))))
.WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild)); .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); var opt = GetNadekoOptionType(com.Attributes);
if (opt is not null) if (opt is not null)
{ {

View File

@@ -1,6 +1,6 @@
h: h:
- help
- h - h
- help
gencmdlist: gencmdlist:
- gencmdlist - gencmdlist
donate: donate:
@@ -37,7 +37,7 @@ boost:
boostmsg: boostmsg:
- boostmsg - boostmsg
boostdel: boostdel:
- boostde - boostdel
logserver: logserver:
- logserver - logserver
logignore: logignore:

View File

@@ -211,23 +211,23 @@ repeat:
desc: "The amount of time between each repetition." desc: "The amount of time between each repetition."
message: message:
desc: "The text to be repeated at the specified intervals or times." desc: "The text to be repeated at the specified intervals or times."
- ch: - channel:
desc: "The channel where the message will be sent." desc: "The channel where the message will be sent."
interval: interval:
desc: "The amount of time between each repetition." desc: "The amount of time between each repetition."
message: message:
desc: "The text to be repeated at the specified intervals or times." 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." 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: message:
desc: "The text to be repeated at the specified intervals or times." desc: "The text to be repeated at the specified intervals or times."
- channel: - channel:
desc: "The channel where the message will be repeated." 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." 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: message:
desc: "The text to be repeated at the specified intervals or times." 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." 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: interval:
desc: "The amount of time between each repetition." 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." desc: "The text to be repeated at the specified intervals or times."
- channel: - channel:
desc: "The channel where the message will be repeated." 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." 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: interval:
desc: "The amount of time between each repetition." desc: "The amount of time between each repetition."
@@ -370,10 +370,10 @@ expradd:
ex: ex:
- '"hello" Hi there %user.mention%' - '"hello" Hi there %user.mention%'
params: params:
- key: - trigger:
desc: "The trigger word that sets off the response when typed by a user." desc: "The trigger word that sets off the response when typed by a user."
message: response:
desc: "The text of the message that triggers the response when typed by a user." desc: "The text of the message that shows up when a user types the trigger word."
expraddserver: 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: <https://nadekobot.readthedocs.io/en/latest/custom-reactions/>' 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: <https://nadekobot.readthedocs.io/en/latest/custom-reactions/>'
ex: ex:
@@ -1257,7 +1257,7 @@ quoteshow:
ex: ex:
- 123 - 123
params: params:
- id: - quoteId:
desc: "The unique identifier for the quote being queried." desc: "The unique identifier for the quote being queried."
quotesearch: 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' 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: ex:
- 123456 - 123456
params: params:
- id: - quoteId:
desc: "The unique identifier for the quote to be displayed." desc: "The unique identifier for the quote to be displayed."
quotedelete: 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. 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: ex:
- 123456 - 123456
params: params:
- id: - quoteId:
desc: "The unique identifier for the quote being deleted." desc: "The unique identifier for the quote being deleted."
quotedeleteauthor: quotedeleteauthor:
desc: Deletes all quotes by the specified author. If the author is not you, then ManageMessage server permission is required. 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: ex:
- '' - ''
params: params:
- {} - {}

View File

@@ -1099,5 +1099,6 @@
"todo_archive_not_found": "Archived todo list not found.", "todo_archive_not_found": "Archived todo list not found.",
"todo_archived_list": "Archived Todo List", "todo_archived_list": "Archived Todo List",
"search_results": "Search results", "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"
} }