Finished new response system

This commit is contained in:
Kwoth
2024-05-02 06:47:01 +00:00
parent a25adefc65
commit fc4858830c
102 changed files with 1811 additions and 1818 deletions

View File

@@ -7,7 +7,7 @@ public abstract class CleanupModuleBase : NadekoModule
{
try
{
var embed = new EmbedBuilder()
var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.sql_confirm_exec))
.WithDescription(name);

View File

@@ -1,8 +1,20 @@
namespace NadekoBot;
public class SimpleInteraction<T>
public static class InteractionHelpers
{
public ButtonBuilder Button { get; }
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;
@@ -13,7 +25,7 @@ public class SimpleInteraction<T>
_state = state;
}
public async Task TriggerAsync(SocketMessageComponent smc)
public override async Task TriggerAsync(SocketMessageComponent smc)
{
await _onClick(smc, _state!);
}

View File

@@ -19,6 +19,7 @@ public abstract class NadekoModule : ModuleBase
public INadekoInteractionService _inter { get; set; }
public IReplacementService repSvc { get; set; }
public IMessageSenderService _sender { get; set; }
public BotConfigService _bcs { get; set; }
protected string prefix
=> _cmdHandler.GetPrefix(ctx.Guild);
@@ -27,7 +28,7 @@ public abstract class NadekoModule : ModuleBase
=> Context;
public ResponseBuilder Response()
=> new ResponseBuilder(Strings)
=> new ResponseBuilder(Strings, _bcs, (DiscordSocketClient)ctx.Client)
.Context(ctx);
protected override void BeforeExecute(CommandInfo command)

View File

@@ -7,4 +7,6 @@ public interface IMessageSenderService
ResponseBuilder Response(IUser user);
ResponseBuilder Response(SocketMessageComponent smc);
NadekoEmbedBuilder CreateEmbed();
}

View File

@@ -1,280 +0,0 @@
namespace NadekoBot.Extensions;
public static class MessageChannelExtensions
{
// main overload that all other send methods reduce to
public static Task<IUserMessage> SendAsync(
this IMessageChannel channel,
string? plainText,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false,
MessageComponent? components = null,
IUserMessage? replyTo = null)
{
plainText = sanitizeAll
? plainText?.SanitizeAllMentions() ?? ""
: plainText?.SanitizeMentions() ?? "";
var msgReference = CreateMessageReference(channel, replyTo);
return channel.SendMessageAsync(plainText,
embed: embed,
embeds: embeds is null
? null
: embeds as Embed[] ?? embeds.ToArray(),
components: components,
messageReference: msgReference);
}
private static MessageReference? CreateMessageReference(IChannel source, IMessage? replyTo)
{
if (replyTo is null)
return null;
if (replyTo.Channel.Id != source.Id)
return null;
return new(replyTo.Id,
replyTo.Channel.Id,
(replyTo.Channel as ITextChannel)?.GuildId,
failIfNotExists: false);
}
public static async Task<IUserMessage> SendAsync(
this IMessageChannel channel,
string? plainText,
NadekoInteraction? inter,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false,
IUserMessage? replyTo = null)
{
var msg = await channel.SendAsync(plainText,
embed,
embeds,
sanitizeAll,
inter?.CreateComponent(),
replyTo);
if (inter is not null)
await inter.RunAsync(msg);
return msg;
}
public static Task<IUserMessage> SendAsync(
this IMessageChannel channel,
SmartText text,
bool sanitizeAll = false,
IUserMessage? replyTo = null)
=> text switch
{
SmartEmbedText set => channel.SendAsync(set.PlainText,
set.IsValid ? set.GetEmbed().Build() : null,
sanitizeAll: sanitizeAll,
replyTo: replyTo),
SmartPlainText st => channel.SendAsync(st.Text,
default(Embed),
sanitizeAll: sanitizeAll,
replyTo: replyTo),
SmartEmbedTextArray arr => channel.SendAsync(arr.Content,
embeds: arr.GetEmbedBuilders().Map(e => e.Build()),
sanitizeAll: sanitizeAll,
replyTo: replyTo),
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
public static Task<IUserMessage> EmbedAsync(
this IMessageChannel ch,
EmbedBuilder? embed,
string plainText = "",
IReadOnlyCollection<EmbedBuilder>? embeds = null,
NadekoInteraction? inter = null,
IUserMessage? replyTo = null)
=> ch.SendAsync(plainText,
inter,
embed: embed?.Build(),
embeds: embeds?.Map(x => x.Build()),
replyTo: replyTo);
// embed title and optional footer overloads
public static Task SendPaginatedConfirmAsync(
this ICommandContext ctx,
int currentPage,
Func<int, EmbedBuilder> pageFunc,
int totalElements,
int itemsPerPage,
bool addPaginatedFooter = true)
=> ctx.SendPaginatedConfirmAsync(currentPage,
x => Task.FromResult(pageFunc(x)),
totalElements,
itemsPerPage,
addPaginatedFooter);
private const string BUTTON_LEFT = "BUTTON_LEFT";
private const string BUTTON_RIGHT = "BUTTON_RIGHT";
private static readonly IEmote _arrowLeft = Emote.Parse("<:x:1232256519844790302>");
private static readonly IEmote _arrowRight = Emote.Parse("<:x:1232256515298295838>");
public static Task SendPaginatedConfirmAsync(
this ICommandContext ctx,
int currentPage,
Func<int, Task<EmbedBuilder>> pageFunc,
int totalElements,
int itemsPerPage,
bool addPaginatedFooter = true)
=> ctx.SendPaginatedConfirmAsync(currentPage,
pageFunc,
default(Func<int, ValueTask<SimpleInteraction<object>?>>),
totalElements,
itemsPerPage,
addPaginatedFooter);
public static async Task SendPaginatedConfirmAsync<T>(
this ICommandContext ctx,
int currentPage,
Func<int, Task<EmbedBuilder>> pageFunc,
Func<int, ValueTask<SimpleInteraction<T>?>>? interFactory,
int totalElements,
int itemsPerPage,
bool addPaginatedFooter = true)
{
var lastPage = (totalElements - 1) / itemsPerPage;
var embed = await pageFunc(currentPage);
if (addPaginatedFooter)
embed.AddPaginatedFooter(currentPage, lastPage);
SimpleInteraction<T>? maybeInter = null;
async Task<ComponentBuilder> GetComponentBuilder()
{
var cb = new ComponentBuilder();
cb.WithButton(new ButtonBuilder()
.WithStyle(ButtonStyle.Primary)
.WithCustomId(BUTTON_LEFT)
.WithDisabled(lastPage == 0)
.WithEmote(_arrowLeft)
.WithDisabled(currentPage <= 0));
if (interFactory is not null)
{
maybeInter = await interFactory(currentPage);
if (maybeInter is not null)
cb.WithButton(maybeInter.Button);
}
cb.WithButton(new ButtonBuilder()
.WithStyle(ButtonStyle.Primary)
.WithCustomId(BUTTON_RIGHT)
.WithDisabled(lastPage == 0 || currentPage >= lastPage)
.WithEmote(_arrowRight));
return cb;
}
async Task UpdatePageAsync(SocketMessageComponent smc)
{
var toSend = await pageFunc(currentPage);
if (addPaginatedFooter)
toSend.AddPaginatedFooter(currentPage, lastPage);
var component = (await GetComponentBuilder()).Build();
await smc.ModifyOriginalResponseAsync(x =>
{
x.Embed = toSend.Build();
x.Components = component;
});
}
var component = (await GetComponentBuilder()).Build();
var msg = await ctx.Channel.SendAsync(null, embed: embed.Build(), components: component, replyTo: ctx.Message);
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 != ctx.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 && interFactory is null)
return;
var client = (DiscordSocketClient)ctx.Client;
client.InteractionCreated += OnInteractionAsync;
await Task.Delay(30_000);
client.InteractionCreated -= OnInteractionAsync;
await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build());
}
private static readonly Emoji _okEmoji = new Emoji("✅");
private static readonly Emoji _warnEmoji = new Emoji("⚠️");
private static readonly Emoji _errorEmoji = new Emoji("❌");
public static Task ReactAsync(this ICommandContext ctx, MsgType type)
{
var emoji = type switch
{
MsgType.Error => _errorEmoji,
MsgType.Pending => _warnEmoji,
MsgType.Ok => _okEmoji,
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
return ctx.Message.AddReactionAsync(emoji);
}
public static Task OkAsync(this ICommandContext ctx)
=> ctx.ReactAsync(MsgType.Ok);
public static Task ErrorAsync(this ICommandContext ctx)
=> ctx.ReactAsync(MsgType.Error);
public static Task WarningAsync(this ICommandContext ctx)
=> ctx.ReactAsync(MsgType.Pending);
}

View File

@@ -1,3 +1,4 @@
using NadekoBot.Common.Configs;
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Extensions;
@@ -5,27 +6,52 @@ namespace NadekoBot.Extensions;
public sealed class MessageSenderService : IMessageSenderService, INService
{
private readonly IBotStrings _bs;
private readonly BotConfigService _bcs;
private readonly DiscordSocketClient _client;
public MessageSenderService(IBotStrings bs)
public MessageSenderService(IBotStrings bs, BotConfigService bcs, DiscordSocketClient client)
{
_bs = bs;
_bcs = bcs;
_client = client;
}
public ResponseBuilder Response(IMessageChannel channel)
=> new ResponseBuilder(_bs)
=> new ResponseBuilder(_bs, _bcs, _client)
.Channel(channel);
public ResponseBuilder Response(ICommandContext ctx)
=> new ResponseBuilder(_bs)
=> new ResponseBuilder(_bs, _bcs, _client)
.Context(ctx);
public ResponseBuilder Response(IUser user)
=> new ResponseBuilder(_bs)
=> new ResponseBuilder(_bs, _bcs, _client)
.User(user);
// todo fix interactions
public ResponseBuilder Response(SocketMessageComponent smc)
=> new ResponseBuilder(_bs)
=> new ResponseBuilder(_bs, _bcs, _client)
.Channel(smc.Channel);
public NadekoEmbedBuilder CreateEmbed()
=> new NadekoEmbedBuilder(_bcs);
}
public class NadekoEmbedBuilder : EmbedBuilder
{
private readonly BotConfig _bc;
public NadekoEmbedBuilder(BotConfigService bcs)
{
_bc = bcs.Data;
}
public EmbedBuilder WithOkColor()
=> WithColor(_bc.Color.Ok.ToDiscordColor());
public EmbedBuilder WithErrorColor()
=> WithColor(_bc.Color.Error.ToDiscordColor());
public EmbedBuilder WithPendingColor()
=> WithColor(_bc.Color.Pending.ToDiscordColor());
}

View File

@@ -7,24 +7,20 @@ public partial class ResponseBuilder
private const string BUTTON_LEFT = "BUTTON_LEFT";
private const string BUTTON_RIGHT = "BUTTON_RIGHT";
private static readonly IEmote _arrowLeft = Emote.Parse("<:x:1232256519844790302>");
private static readonly IEmote _arrowRight = Emote.Parse("<:x:1232256515298295838>");
private readonly SourcedPaginatedResponseBuilder<T> _paginationBuilder;
private readonly ResponseBuilder builder;
private readonly DiscordSocketClient client;
private readonly ResponseBuilder _builder;
private readonly DiscordSocketClient _client;
private int currentPage;
public PaginationSender(
SourcedPaginatedResponseBuilder<T> paginationBuilder,
ResponseBuilder builder
)
ResponseBuilder builder)
{
this._paginationBuilder = paginationBuilder;
this.builder = builder;
_paginationBuilder = paginationBuilder;
_builder = builder;
client = (DiscordSocketClient)builder.ctx.Client;
currentPage = 0;
_client = builder.Client;
currentPage = paginationBuilder.InitialPage;
}
public async Task SendAsync(bool ephemeral = false)
@@ -38,7 +34,7 @@ public partial class ResponseBuilder
if (_paginationBuilder.AddPaginatedFooter)
embed.AddPaginatedFooter(currentPage, lastPage);
SimpleInteraction<T>? maybeInter = null;
SimpleInteractionBase? maybeInter = null;
async Task<ComponentBuilder> GetComponentBuilder()
{
@@ -48,22 +44,22 @@ public partial class ResponseBuilder
.WithStyle(ButtonStyle.Primary)
.WithCustomId(BUTTON_LEFT)
.WithDisabled(lastPage == 0)
.WithEmote(_arrowLeft)
.WithEmote(InteractionHelpers.ArrowLeft)
.WithDisabled(currentPage <= 0));
// todo
// if (interFactory is not null)
// {
// maybeInter = await interFactory(currentPage);
//
// if (maybeInter is not null)
// cb.WithButton(maybeInter.Button);
// }
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 == 0 || currentPage >= lastPage)
.WithEmote(_arrowRight));
.WithEmote(InteractionHelpers.ArrowRight));
return cb;
}
@@ -84,7 +80,7 @@ public partial class ResponseBuilder
});
}
var model = builder.Build(ephemeral);
var model = await _builder.BuildAsync(ephemeral);
var component = (await GetComponentBuilder()).Build();
var msg = await model.TargetChannel
@@ -104,7 +100,8 @@ public partial class ResponseBuilder
return;
await si.DeferAsync();
if (smc.User.Id != model.User.Id)
if (smc.User.Id != model.User?.Id)
return;
if (smc.Data.CustomId == BUTTON_LEFT)
@@ -134,20 +131,15 @@ public partial class ResponseBuilder
Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message);
}
}
// todo re-add
// if (lastPage == 0 && interFactory is null)
// return;
if (lastPage == 0)
if (lastPage == 0 && _paginationBuilder.InteractionFunc is null)
return;
var client = this.client;
client.InteractionCreated += OnInteractionAsync;
_client.InteractionCreated += OnInteractionAsync;
await Task.Delay(30_000);
client.InteractionCreated -= OnInteractionAsync;
_client.InteractionCreated -= OnInteractionAsync;
await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build());
}

View File

@@ -1,29 +1,39 @@
namespace NadekoBot.Extensions;
using NadekoBot.Common.Configs;
using NadekoBot.Db.Models;
namespace NadekoBot.Extensions;
public sealed partial class ResponseBuilder
{
private ICommandContext? ctx = null;
private IMessageChannel? channel = null;
private Embed? embed = null;
private string? plainText = null;
private IReadOnlyCollection<EmbedBuilder>? embeds = null;
private IUserMessage? msg = null;
private IUser? user = null;
private ICommandContext? ctx;
private IMessageChannel? channel;
private string? plainText;
private IReadOnlyCollection<EmbedBuilder>? embeds;
private IUserMessage? msg;
private IUser? user;
private bool sanitizeMentions = true;
private LocStr? locTxt;
private object[] locParams = [];
private bool shouldReply = true;
private readonly IBotStrings _bs;
private EmbedBuilder? embedBuilder = null;
private readonly BotConfigService _bcs;
private EmbedBuilder? embedBuilder;
private NadekoInteraction? inter;
private Stream? fileStream = null;
private string? fileName = null;
private Stream? fileStream;
private string? fileName;
private EmbedColor color = EmbedColor.Ok;
private LocStr? embedLocDesc;
public ResponseBuilder(IBotStrings bs)
public DiscordSocketClient Client { get; set; }
public ResponseBuilder(IBotStrings bs, BotConfigService bcs, DiscordSocketClient client)
{
_bs = bs;
_bcs = bcs;
Client = client;
}
private MessageReference? CreateMessageReference(IMessageChannel targetChannel)
{
if (!shouldReply)
@@ -44,72 +54,116 @@ public sealed partial class ResponseBuilder
failIfNotExists: false);
}
public ResponseMessageModel Build(bool ephemeral = false)
public async Task<ResponseMessageModel> BuildAsync(bool ephemeral)
{
// todo use ephemeral in interactions
var targetChannel = InternalResolveChannel() ?? throw new ArgumentNullException(nameof(channel));
var targetChannel = await InternalResolveChannel() ?? throw new ArgumentNullException(nameof(channel));
var msgReference = CreateMessageReference(targetChannel);
var txt = GetText(locTxt);
// todo check message sanitization
var txt = GetText(locTxt, targetChannel);
if (embedLocDesc is LocStr ls)
{
InternalCreateEmbed(null, GetText(ls, targetChannel));
}
if (embedBuilder is not null)
PaintEmbedInternal(embedBuilder);
var finalEmbed = embedBuilder?.Build();
var buildModel = new ResponseMessageModel()
{
TargetChannel = targetChannel,
MessageReference = msgReference,
Text = txt,
User = ctx?.User,
Embed = embed ?? embedBuilder?.Build(),
User = user ?? ctx?.User,
Embed = finalEmbed,
Embeds = embeds?.Map(x => x.Build()),
SanitizeMentions = sanitizeMentions ? new(AllowedMentionTypes.Users) : AllowedMentions.All
SanitizeMentions = sanitizeMentions ? new(AllowedMentionTypes.Users) : AllowedMentions.All,
Ephemeral = ephemeral,
Interaction = inter
};
return buildModel;
}
public Task<IUserMessage> SendAsync(bool ephemeral = false)
public async Task<IUserMessage> SendAsync(bool ephemeral = false)
{
var model = Build(ephemeral);
return SendAsync(model);
var model = await BuildAsync(ephemeral);
var sentMsg = await SendAsync(model);
return sentMsg;
}
public async Task<IUserMessage> SendAsync(ResponseMessageModel model)
{
if (this.fileStream is Stream stream)
return await model.TargetChannel.SendFileAsync(stream,
IUserMessage sentMsg;
if (fileStream is Stream stream)
{
sentMsg = await model.TargetChannel.SendFileAsync(stream,
filename: fileName,
model.Text,
embed: model.Embed,
components: null,
components: inter?.CreateComponent(),
allowedMentions: model.SanitizeMentions,
messageReference: model.MessageReference);
}
else
{
sentMsg = await model.TargetChannel.SendMessageAsync(
model.Text,
embed: model.Embed,
embeds: model.Embeds,
components: inter?.CreateComponent(),
allowedMentions: model.SanitizeMentions,
messageReference: model.MessageReference);
}
return await model.TargetChannel.SendMessageAsync(
model.Text,
embed: model.Embed,
embeds: model.Embeds,
components: null,
allowedMentions: model.SanitizeMentions,
messageReference: model.MessageReference);
if (model.Interaction is not null)
{
await model.Interaction.RunAsync(sentMsg);
}
return sentMsg;
}
private EmbedBuilder PaintEmbedInternal(EmbedBuilder eb)
=> color switch
{
EmbedColor.Ok => eb.WithOkColor(),
EmbedColor.Pending => eb.WithPendingColor(),
EmbedColor.Error => eb.WithErrorColor(),
_ => throw new NotSupportedException()
};
private ulong? InternalResolveGuildId(IMessageChannel? targetChannel)
=> ctx?.Guild?.Id ?? (targetChannel as ITextChannel)?.GuildId;
// todo not good, has to go to the user
private IMessageChannel? InternalResolveChannel()
=> channel ?? ctx?.Channel ?? msg?.Channel;
private string? GetText(LocStr? locStr)
private async Task<IMessageChannel?> InternalResolveChannel()
{
if (user is not null)
{
var ch = await user.CreateDMChannelAsync();
if (ch is not null)
{
return ch;
}
}
return channel ?? ctx?.Channel ?? msg?.Channel;
}
private string? GetText(LocStr? locStr, IMessageChannel targetChannel)
{
var targetChannel = InternalResolveChannel();
var guildId = InternalResolveGuildId(targetChannel);
return locStr is LocStr ls ? _bs.GetText(ls.Key, guildId, locParams) : plainText;
}
private string GetText(LocStr locStr)
private string GetText(LocStr locStr, IMessageChannel targetChannel)
{
var targetChannel = InternalResolveChannel();
var guildId = InternalResolveGuildId(targetChannel);
return _bs.GetText(locStr.Key, guildId, locStr.Params);
}
@@ -125,91 +179,108 @@ public sealed partial class ResponseBuilder
if (text is SmartPlainText spt)
plainText = spt.Text;
else if (text is SmartEmbedText set)
embed = set.GetEmbed().Build();
embedBuilder = set.GetEmbed();
else if (text is SmartEmbedTextArray ser)
embeds = ser.GetEmbedBuilders();
return this;
}
private ResponseBuilder InternalColoredText(string text, EmbedColor color)
{
embed = new EmbedBuilder()
.WithColor(color)
.WithDescription(text)
.Build();
return this;
}
private EmbedBuilder CreateEmbedInternal(
private void InternalCreateEmbed(
string? title,
string? text,
string? url,
string? footer = null)
{
var embed = new EmbedBuilder()
.WithTitle(title)
.WithDescription(text);
if (!string.IsNullOrWhiteSpace(url))
embed = embed.WithUrl(url);
if (!string.IsNullOrWhiteSpace(footer))
embed = embed.WithFooter(footer);
return embed;
}
private EmbedBuilder PaintEmbedInternal(EmbedBuilder eb, EmbedColor color)
=> color switch
{
EmbedColor.Ok => eb.WithOkColor(),
EmbedColor.Pending => eb.WithPendingColor(),
EmbedColor.Error => eb.WithErrorColor(),
};
public ResponseBuilder Error(
string? title,
string? text,
string text,
string? url = null,
string? footer = null)
{
var eb = CreateEmbedInternal(title, text, url, footer);
embed = PaintEmbedInternal(eb, EmbedColor.Error).Build();
return this;
}
var eb = new NadekoEmbedBuilder(_bcs)
.WithDescription(text);
if (!string.IsNullOrWhiteSpace(title))
eb.WithTitle(title);
if (!string.IsNullOrWhiteSpace(url))
eb = eb.WithUrl(url);
if (!string.IsNullOrWhiteSpace(footer))
eb = eb.WithFooter(footer);
embedBuilder = eb;
}
public ResponseBuilder Confirm(
string? title,
string? text,
string text,
string? url = null,
string? footer = null)
{
var eb = CreateEmbedInternal(title, text, url, footer);
embed = PaintEmbedInternal(eb, EmbedColor.Error).Build();
InternalCreateEmbed(title, text, url, footer);
color = EmbedColor.Ok;
return this;
}
public ResponseBuilder Error(
string? title,
string text,
string? url = null,
string? footer = null)
{
InternalCreateEmbed(title, text, url, footer);
color = EmbedColor.Error;
return this;
}
public ResponseBuilder Pending(
string? title,
string text,
string? url = null,
string? footer = null)
{
InternalCreateEmbed(title, text, url, footer);
color = EmbedColor.Pending;
return this;
}
public ResponseBuilder Confirm(string text)
=> InternalColoredText(text, EmbedColor.Ok);
{
InternalCreateEmbed(null, text);
color = EmbedColor.Ok;
return this;
}
public ResponseBuilder Confirm(LocStr str)
=> Confirm(GetText(str));
{
embedLocDesc = str;
color = EmbedColor.Ok;
return this;
}
public ResponseBuilder Pending(string text)
=> InternalColoredText(text, EmbedColor.Ok);
{
InternalCreateEmbed(null, text);
color = EmbedColor.Pending;
return this;
}
public ResponseBuilder Pending(LocStr str)
=> Pending(GetText(str));
{
embedLocDesc = str;
color = EmbedColor.Pending;
return this;
}
public ResponseBuilder Error(string text)
=> InternalColoredText(text, EmbedColor.Error);
{
InternalCreateEmbed(null, text);
color = EmbedColor.Error;
return this;
}
public ResponseBuilder Error(LocStr str)
=> Error(GetText(str));
{
embedLocDesc = str;
color = EmbedColor.Error;
return this;
}
public ResponseBuilder UserBasedMentions()
{
@@ -220,17 +291,15 @@ public sealed partial class ResponseBuilder
private IUser? InternalResolveUser()
=> ctx?.User ?? user ?? msg?.Author;
// todo embed colors
public ResponseBuilder Embed(EmbedBuilder eb)
{
embedBuilder = eb;
return this;
}
public ResponseBuilder Channel(IMessageChannel channel)
public ResponseBuilder Channel(IMessageChannel ch)
{
this.channel = channel;
channel = ch;
return this;
}
@@ -240,21 +309,21 @@ public sealed partial class ResponseBuilder
return this;
}
public ResponseBuilder Context(ICommandContext ctx)
public ResponseBuilder Context(ICommandContext context)
{
this.ctx = ctx;
ctx = context;
return this;
}
public ResponseBuilder Message(IUserMessage msg)
public ResponseBuilder Message(IUserMessage message)
{
this.msg = msg;
msg = message;
return this;
}
public ResponseBuilder User(IUser user)
public ResponseBuilder User(IUser usr)
{
this.user = user;
user = usr;
return this;
}
@@ -266,7 +335,6 @@ public sealed partial class ResponseBuilder
public ResponseBuilder Interaction(NadekoInteraction? interaction)
{
// todo implement
inter = interaction;
return this;
}
@@ -277,10 +345,10 @@ public sealed partial class ResponseBuilder
return this;
}
public ResponseBuilder FileName(Stream fileStream, string fileName)
public ResponseBuilder File(Stream stream, string name)
{
this.fileStream = fileStream;
this.fileName = fileName;
fileStream = stream;
fileName = name;
return this;
}
@@ -300,27 +368,51 @@ public class PaginatedResponseBuilder
public SourcedPaginatedResponseBuilder<T> Items<T>(IReadOnlyCollection<T> items)
=> new SourcedPaginatedResponseBuilder<T>(_builder)
.Items(items);
public SourcedPaginatedResponseBuilder<T> PageItems<T>(Func<int, Task<IEnumerable<T>>> items)
=> new SourcedPaginatedResponseBuilder<T>(_builder)
.PageItems(items);
}
public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilder
{
private IReadOnlyCollection<T>? items;
public Func<IReadOnlyList<T>, int, Task<EmbedBuilder>> PageFunc { get; private set; }
public Func<int, Task<IEnumerable<T>>> ItemsFunc { get; set; }
public Func<IReadOnlyList<T>, int, Task<EmbedBuilder>> PageFunc { get; private set; } = static delegate
{
return Task.FromResult<EmbedBuilder>(new());
};
public Func<int, Task<IEnumerable<T>>> ItemsFunc { get; set; } = static delegate
{
return Task.FromResult(Enumerable.Empty<T>());
};
public Func<int, Task<SimpleInteractionBase>>? InteractionFunc { get; private set; }
public int TotalElements { get; private set; } = 1;
public int ItemsPerPage { get; private set; } = 9;
public bool AddPaginatedFooter { get; private set; } = true;
public bool IsEphemeral { get; private set; }
public int InitialPage { get; set; }
public SourcedPaginatedResponseBuilder(ResponseBuilder builder)
: base(builder)
{
}
public SourcedPaginatedResponseBuilder<T> Items(IReadOnlyCollection<T> items)
public SourcedPaginatedResponseBuilder<T> Items(IReadOnlyCollection<T> col)
{
this.items = items;
ItemsFunc = (i) => Task.FromResult(this.items.Skip(i * ItemsPerPage).Take(ItemsPerPage));
items = col;
TotalElements = col.Count;
ItemsFunc = (i) => Task.FromResult(items.Skip(i * ItemsPerPage).Take(ItemsPerPage));
return this;
}
public SourcedPaginatedResponseBuilder<T> PageItems(Func<int, Task<IEnumerable<T>>> func)
{
ItemsFunc = func;
return this;
}
@@ -337,24 +429,22 @@ public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilde
return this;
}
// todo use it
public int InitialPage { get; set; }
public SourcedPaginatedResponseBuilder<T> Page(Func<IReadOnlyList<T>, int, EmbedBuilder> pageFunc)
{
this.PageFunc = (xs, x) => Task.FromResult(pageFunc(xs, x));
PageFunc = (xs, x) => Task.FromResult(pageFunc(xs, x));
return this;
}
public SourcedPaginatedResponseBuilder<T> Page(Func<IReadOnlyList<T>, int, Task<EmbedBuilder>> pageFunc)
{
this.PageFunc = pageFunc;
PageFunc = pageFunc;
return this;
}
public SourcedPaginatedResponseBuilder<T> AddFooter()
public SourcedPaginatedResponseBuilder<T> AddFooter(bool addFooter = true)
{
AddPaginatedFooter = true;
AddPaginatedFooter = addFooter;
return this;
}
@@ -373,4 +463,10 @@ public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilde
return paginationSender.SendAsync(IsEphemeral);
}
public SourcedPaginatedResponseBuilder<T> Interaction(Func<int, Task<SimpleInteractionBase>> func)
{
InteractionFunc = async (i) => await func(i);
return this;
}
}

View File

@@ -2,17 +2,27 @@
public static class ResponseBuilderExtensions
{
// todo delete this
public static EmbedBuilder WithColor(this EmbedBuilder eb, EmbedColor color)
=> eb;
public static EmbedBuilder WithPendingColor(this EmbedBuilder eb)
=> eb.WithColor(EmbedColor.Error);
{
if (eb is NadekoEmbedBuilder neb)
return neb.WithPendingColor();
return eb;
}
public static EmbedBuilder WithOkColor(this EmbedBuilder eb)
=> eb.WithColor(EmbedColor.Ok);
{
if (eb is NadekoEmbedBuilder neb)
return neb.WithOkColor();
return eb;
}
public static EmbedBuilder WithErrorColor(this EmbedBuilder eb)
=> eb.WithColor(EmbedColor.Error);
{
if (eb is NadekoEmbedBuilder neb)
return neb.WithErrorColor();
return eb;
}
}

View File

@@ -1,10 +1,12 @@
public class ResponseMessageModel
{
public IMessageChannel TargetChannel { get; set; }
public MessageReference MessageReference { get; set; }
public string Text { get; set; }
public Embed Embed { get; set; }
public Embed[] Embeds { get; set; }
public AllowedMentions SanitizeMentions { get; set; }
public IUser User { get; set; }
public required IMessageChannel TargetChannel { get; set; }
public MessageReference? MessageReference { get; set; }
public string? Text { get; set; }
public Embed? Embed { get; set; }
public Embed[]? Embeds { get; set; }
public required AllowedMentions SanitizeMentions { get; set; }
public IUser? User { get; set; }
public bool Ephemeral { get; set; }
public NadekoInteraction? Interaction { get; set; }
}

View File

@@ -1,17 +0,0 @@
#nullable disable
namespace NadekoBot.Services;
// todo remove
public sealed class DiscordEmbedBuilderWrapper
{
// public EmbedBuilder WithColor(EmbedColor color)
// => color switch
// {
// EmbedColor.Ok => Wrap(embed.WithColor(_botConfig.Color.Ok.ToDiscordColor())),
// EmbedColor.Pending => Wrap(embed.WithColor(_botConfig.Color.Pending.ToDiscordColor())),
// EmbedColor.Error => Wrap(embed.WithColor(_botConfig.Color.Error.ToDiscordColor())),
// _ => throw new ArgumentOutOfRangeException(nameof(color), "Unsupported EmbedColor type")
// };
}

View File

@@ -39,7 +39,7 @@ public sealed class CommandsUtilityService : ICommandsUtilityService, INService
var culture = _loc.GetCultureInfo(guild);
var em = new EmbedBuilder()
var em = _sender.CreateEmbed()
.AddField(str, $"{com.RealSummary(_strings, _medusae, culture, prefix)}", true);
_dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides);

View File

@@ -0,0 +1,30 @@
namespace NadekoBot.Extensions;
public static class CommandContextExtensions
{
private static readonly Emoji _okEmoji = new Emoji("✅");
private static readonly Emoji _warnEmoji = new Emoji("⚠️");
private static readonly Emoji _errorEmoji = new Emoji("❌");
public static Task ReactAsync(this ICommandContext ctx, MsgType type)
{
var emoji = type switch
{
MsgType.Error => _errorEmoji,
MsgType.Pending => _warnEmoji,
MsgType.Ok => _okEmoji,
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
return ctx.Message.AddReactionAsync(emoji);
}
public static Task OkAsync(this ICommandContext ctx)
=> ctx.ReactAsync(MsgType.Ok);
public static Task ErrorAsync(this ICommandContext ctx)
=> ctx.ReactAsync(MsgType.Error);
public static Task WarningAsync(this ICommandContext ctx)
=> ctx.ReactAsync(MsgType.Pending);
}

View File

@@ -9,18 +9,25 @@ namespace NadekoBot.Extensions;
public static class Extensions
{
private static readonly Regex _urlRegex =
new(@"^(https?|ftp)://(?<path>[^\s/$.?#].[^\s]*)$", RegexOptions.Compiled);
/// <summary>
/// Converts <see cref="DateTime"/> to <see cref="DateOnly"/>
/// </summary>
/// <param name="dateTime"> The <see cref="DateTime"/> to convert. </param>
/// <returns> The <see cref="DateOnly"/>. </returns>
public static DateOnly ToDateOnly(this DateTime dateTime)
=> DateOnly.FromDateTime(dateTime);
/// <summary>
/// Determines if <see cref="DateTime"/> is before today
/// </summary>
/// <param name="date"> The <see cref="DateTime"/> to check. </param>
/// <returns> True if <see cref="DateTime"/> is before today. </returns>
public static bool IsBeforeToday(this DateTime date)
=> date < DateTime.UtcNow.Date;
private static readonly Regex _urlRegex =
new(@"^(https?|ftp)://(?<path>[^\s/$.?#].[^\s]*)$", RegexOptions.Compiled);
// public static EmbedBuilder WithAuthor(this EmbedBuilder eb, IUser author)
// => eb.WithAuthor(author.ToString()!, author.RealAvatarUrl().ToString());
public static Task EditAsync(this IUserMessage msg, SmartText text)
=> text switch
{

View File

@@ -1,72 +1,15 @@

namespace NadekoBot.Extensions;
namespace NadekoBot.Extensions;
public static class SocketMessageComponentExtensions
{
public static Task RespondAsync(
this SocketMessageComponent smc,
string? plainText,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false,
MessageComponent? components = null,
bool ephemeral = true)
{
plainText = sanitizeAll
? plainText?.SanitizeAllMentions() ?? ""
: plainText?.SanitizeMentions() ?? "";
return smc.RespondAsync(plainText,
embed: embed,
embeds: embeds is null
? null
: embeds as Embed[] ?? embeds.ToArray(),
components: components,
ephemeral: ephemeral);
}
public static Task RespondAsync(
this SocketMessageComponent smc,
SmartText text,
bool sanitizeAll = false,
bool ephemeral = true)
=> text switch
{
SmartEmbedText set => smc.RespondAsync(set.PlainText,
set.IsValid ? set.GetEmbed().Build() : null,
sanitizeAll: sanitizeAll,
ephemeral: ephemeral),
SmartPlainText st => smc.RespondAsync(st.Text,
default(Embed),
sanitizeAll: sanitizeAll,
ephemeral: ephemeral),
SmartEmbedTextArray arr => smc.RespondAsync(arr.Content,
embeds: arr.GetEmbedBuilders().Map(e => e.Build()),
ephemeral: ephemeral),
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
public static Task EmbedAsync(
this SocketMessageComponent smc,
EmbedBuilder? embed,
string plainText = "",
IReadOnlyCollection<EmbedBuilder>? embeds = null,
NadekoInteraction? inter = null,
bool ephemeral = false)
=> smc.RespondAsync(plainText,
embed: embed?.Build(),
embeds: embeds?.Map(x => x.Build()),
ephemeral: ephemeral);
public static Task RespondAsync(
public static async Task RespondAsync(
this SocketMessageComponent ch,
IMessageSenderService sender,
string text,
MsgType type,
bool ephemeral = false,
NadekoInteraction? inter = null)
bool ephemeral = false)
{
var embed = new EmbedBuilder().WithDescription(text);
var embed = sender.CreateEmbed().WithDescription(text);
embed = (type switch
{
@@ -76,12 +19,9 @@ public static class SocketMessageComponentExtensions
_ => throw new ArgumentOutOfRangeException(nameof(type))
});
return sender.Response(ch)
.Embed(embed)
.Interaction(inter)
.SendAsync(ephemeral: ephemeral);
await ch.RespondAsync(embeds: [embed.Build()], ephemeral: ephemeral);
}
// embed title and optional footer overloads
public static Task RespondConfirmAsync(