mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-12 02:08:27 -04:00
Part2 of the response system rework
This commit is contained in:
7
src/NadekoBot/_common/Sender/IMessageSenderService.cs
Normal file
7
src/NadekoBot/_common/Sender/IMessageSenderService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public interface IMessageSenderService
|
||||
{
|
||||
ResponseBuilder Response(IMessageChannel channel);
|
||||
ResponseBuilder Response(ICommandContext hannel);
|
||||
}
|
284
src/NadekoBot/_common/Sender/MessageChannelExtensions.cs
Normal file
284
src/NadekoBot/_common/Sender/MessageChannelExtensions.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class MessageChannelExtensions
|
||||
{
|
||||
public static ResponseBuilder Response(this IMessageChannel channel, IBotStrings bs, IEmbedBuilderService ebs)
|
||||
=> new ResponseBuilder(bs, ebs)
|
||||
.Channel(channel);
|
||||
|
||||
// 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<IEmbedBuilder>? 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);
|
||||
}
|
24
src/NadekoBot/_common/Sender/MessageSenderService.cs
Normal file
24
src/NadekoBot/_common/Sender/MessageSenderService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public sealed class MessageSenderService : IMessageSenderService, INService
|
||||
{
|
||||
private readonly IBotStrings _bs;
|
||||
private readonly IEmbedBuilderService _ebs;
|
||||
|
||||
public MessageSenderService(IBotStrings bs, IEmbedBuilderService ebs)
|
||||
{
|
||||
_bs = bs;
|
||||
_ebs = ebs;
|
||||
}
|
||||
|
||||
|
||||
public ResponseBuilder Response(IMessageChannel channel)
|
||||
=> new ResponseBuilder(_bs, _ebs)
|
||||
.Channel(channel);
|
||||
|
||||
public ResponseBuilder Response(ICommandContext ctx)
|
||||
=> new ResponseBuilder(_bs, _ebs)
|
||||
.Context(ctx);
|
||||
}
|
252
src/NadekoBot/_common/Sender/ResponseBuilder.cs
Normal file
252
src/NadekoBot/_common/Sender/ResponseBuilder.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public sealed 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 bool sanitizeMentions = true;
|
||||
private LocStr? locTxt;
|
||||
private object[] locParams = [];
|
||||
private bool shouldReply = true;
|
||||
private readonly IBotStrings _bs;
|
||||
private readonly IEmbedBuilderService _ebs;
|
||||
private EmbedBuilder? embedBuilder = null;
|
||||
|
||||
public ResponseBuilder(IBotStrings bs, IEmbedBuilderService ebs)
|
||||
{
|
||||
_bs = bs;
|
||||
_ebs = ebs;
|
||||
}
|
||||
|
||||
private MessageReference? CreateMessageReference(IMessageChannel targetChannel)
|
||||
{
|
||||
if (!shouldReply)
|
||||
return null;
|
||||
|
||||
var replyTo = msg ?? ctx?.Message;
|
||||
// what message are we replying to
|
||||
if (replyTo is null)
|
||||
return null;
|
||||
|
||||
// we have to have a channel where we are sending the message in order to know whether we can reply to it
|
||||
if (targetChannel.Id != replyTo.Channel.Id)
|
||||
return null;
|
||||
|
||||
return new(replyTo.Id,
|
||||
replyTo.Channel.Id,
|
||||
(replyTo.Channel as ITextChannel)?.GuildId,
|
||||
failIfNotExists: false);
|
||||
}
|
||||
|
||||
public async Task<IUserMessage> SendAsync()
|
||||
{
|
||||
var targetChannel = InternalResolveChannel() ?? throw new ArgumentNullException(nameof(channel));
|
||||
var msgReference = CreateMessageReference(targetChannel);
|
||||
|
||||
var txt = GetText(locTxt);
|
||||
|
||||
if (sanitizeMentions)
|
||||
txt = txt?.SanitizeMentions(true);
|
||||
|
||||
return await targetChannel.SendMessageAsync(
|
||||
txt,
|
||||
embed: embed ?? embedBuilder?.Build(),
|
||||
embeds: embeds?.Map(x => x.Build()),
|
||||
components: null,
|
||||
allowedMentions: sanitizeMentions ? new(AllowedMentionTypes.Users) : AllowedMentions.All,
|
||||
messageReference: msgReference);
|
||||
}
|
||||
|
||||
private ulong? InternalResolveGuildId(IMessageChannel? targetChannel)
|
||||
=> ctx?.Guild?.Id ?? (targetChannel as ITextChannel)?.GuildId;
|
||||
|
||||
private IMessageChannel? InternalResolveChannel()
|
||||
=> channel ?? ctx?.Channel ?? msg?.Channel;
|
||||
|
||||
private string? GetText(LocStr? locStr)
|
||||
{
|
||||
var targetChannel = InternalResolveChannel();
|
||||
var guildId = InternalResolveGuildId(targetChannel);
|
||||
return locStr is LocStr ls ? _bs.GetText(ls.Key, guildId, locParams) : plainText;
|
||||
}
|
||||
|
||||
private string GetText(LocStr locStr)
|
||||
{
|
||||
var targetChannel = InternalResolveChannel();
|
||||
var guildId = InternalResolveGuildId(targetChannel);
|
||||
return _bs.GetText(locStr.Key, guildId, locStr.Params);
|
||||
}
|
||||
|
||||
public ResponseBuilder Text(LocStr str)
|
||||
{
|
||||
locTxt = str;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Text(SmartText text)
|
||||
{
|
||||
if (text is SmartPlainText spt)
|
||||
plainText = spt.Text;
|
||||
else if (text is SmartEmbedText set)
|
||||
embed = set.GetEmbed().Build();
|
||||
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(
|
||||
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? url = null,
|
||||
string? footer = null)
|
||||
{
|
||||
var eb = CreateEmbedInternal(title, text, url, footer);
|
||||
embed = PaintEmbedInternal(eb, EmbedColor.Error).Build();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public ResponseBuilder Confirm(
|
||||
string? title,
|
||||
string? text,
|
||||
string? url = null,
|
||||
string? footer = null)
|
||||
{
|
||||
var eb = CreateEmbedInternal(title, text, url, footer);
|
||||
embed = PaintEmbedInternal(eb, EmbedColor.Error).Build();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Confirm(string text)
|
||||
=> InternalColoredText(text, EmbedColor.Ok);
|
||||
|
||||
public ResponseBuilder Confirm(LocStr str)
|
||||
=> Confirm(GetText(str));
|
||||
|
||||
public ResponseBuilder Pending(string text)
|
||||
=> InternalColoredText(text, EmbedColor.Ok);
|
||||
|
||||
public ResponseBuilder Pending(LocStr str)
|
||||
=> Pending(GetText(str));
|
||||
|
||||
public ResponseBuilder Error(string text)
|
||||
=> InternalColoredText(text, EmbedColor.Error);
|
||||
|
||||
public ResponseBuilder Error(LocStr str)
|
||||
=> Error(GetText(str));
|
||||
|
||||
|
||||
public ResponseBuilder UserBasedMentions()
|
||||
{
|
||||
sanitizeMentions = !((InternalResolveUser() as IGuildUser)?.GuildPermissions.MentionEveryone ?? false);
|
||||
return this;
|
||||
}
|
||||
|
||||
private IUser? InternalResolveUser()
|
||||
=> ctx?.User ?? user ?? msg?.Author;
|
||||
|
||||
public ResponseBuilder Embed(EmbedBuilder eb)
|
||||
{
|
||||
embedBuilder = eb;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Embed(Func<IEmbedBuilderService, EmbedBuilder> embedFactory)
|
||||
{
|
||||
// todo colors
|
||||
this.embed = embedFactory(_ebs).Build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Channel(IMessageChannel channel)
|
||||
{
|
||||
this.channel = channel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Sanitize(bool shouldSantize = true)
|
||||
{
|
||||
sanitizeMentions = shouldSantize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Context(ICommandContext ctx)
|
||||
{
|
||||
this.ctx = ctx;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Message(IUserMessage msg)
|
||||
{
|
||||
this.msg = msg;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder User(IUser user)
|
||||
{
|
||||
this.user = user;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder NoReply()
|
||||
{
|
||||
shouldReply = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Interaction(NadekoInteraction inter)
|
||||
{
|
||||
// todo implement
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder Embeds(IReadOnlyCollection<EmbedBuilder> inputEmbeds)
|
||||
{
|
||||
embeds = inputEmbeds;
|
||||
return this;
|
||||
}
|
||||
}
|
18
src/NadekoBot/_common/Sender/ResponseBuilderExtensions.cs
Normal file
18
src/NadekoBot/_common/Sender/ResponseBuilderExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
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);
|
||||
|
||||
public static EmbedBuilder WithOkColor(this EmbedBuilder eb)
|
||||
=> eb.WithColor(EmbedColor.Ok);
|
||||
|
||||
public static EmbedBuilder WithErrorColor(this EmbedBuilder eb)
|
||||
=> eb.WithColor(EmbedColor.Error);
|
||||
}
|
Reference in New Issue
Block a user