using NadekoBot.Common.Configs; using NadekoBot.Db.Models; using System.Collections.ObjectModel; namespace NadekoBot.Extensions; public sealed partial class ResponseBuilder { private ICommandContext? ctx; private IMessageChannel? channel; private string? plainText; private IReadOnlyCollection? 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 readonly BotConfigService _bcs; private EmbedBuilder? embedBuilder; private NadekoInteractionBase? inter; private Stream? fileStream; private string? fileName; private EmbedColor color = EmbedColor.Ok; private LocStr? embedLocDesc; 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) 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 BuildAsync(bool ephemeral) { var targetChannel = await InternalResolveChannel() ?? throw new ArgumentNullException(nameof(channel)); var msgReference = CreateMessageReference(targetChannel); 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 = user ?? ctx?.User, Embed = finalEmbed, Embeds = embeds?.Map(x => x.Build()), SanitizeMentions = sanitizeMentions ? new(AllowedMentionTypes.Users) : AllowedMentions.All, Ephemeral = ephemeral, Interaction = inter }; return buildModel; } public async Task SendAsync(bool ephemeral = false) { var model = await BuildAsync(ephemeral); var sentMsg = await SendAsync(model); return sentMsg; } public async Task SendAsync(ResponseMessageModel model) { IUserMessage sentMsg; if (fileStream is Stream stream) { sentMsg = await model.TargetChannel.SendFileAsync(stream, filename: fileName, model.Text, embed: model.Embed, 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); } 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; private async Task 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 guildId = InternalResolveGuildId(targetChannel); return locStr is LocStr ls ? _bs.GetText(ls.Key, guildId, locParams) : plainText; } private string GetText(LocStr locStr, IMessageChannel targetChannel) { 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) { plainText = set.PlainText ?? plainText; embedBuilder = set.GetEmbed(); } else if (text is SmartEmbedTextArray ser) { plainText = ser.Content ?? plainText; embeds = ser.GetEmbedBuilders(); } return this; } private void InternalCreateEmbed( string? title, string text, string? url = null, string? footer = null) { 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? url = null, string? footer = null) { 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) { InternalCreateEmbed(null, text); color = EmbedColor.Ok; return this; } public ResponseBuilder Confirm(LocStr str) { embedLocDesc = str; color = EmbedColor.Ok; return this; } public ResponseBuilder Pending(string text) { InternalCreateEmbed(null, text); color = EmbedColor.Pending; return this; } public ResponseBuilder Pending(LocStr str) { embedLocDesc = str; color = EmbedColor.Pending; return this; } public ResponseBuilder Error(string text) { InternalCreateEmbed(null, text); color = EmbedColor.Error; return this; } public ResponseBuilder Error(LocStr str) { embedLocDesc = str; color = EmbedColor.Error; return this; } 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 Channel(IMessageChannel ch) { channel = ch; return this; } public ResponseBuilder Sanitize(bool shouldSantize = true) { sanitizeMentions = shouldSantize; return this; } public ResponseBuilder Context(ICommandContext context) { ctx = context; return this; } public ResponseBuilder Message(IUserMessage message) { msg = message; return this; } public ResponseBuilder User(IUser usr) { user = usr; return this; } public ResponseBuilder NoReply() { shouldReply = false; return this; } public ResponseBuilder Interaction(NadekoInteractionBase? interaction) { inter = interaction; return this; } public ResponseBuilder Embeds(IReadOnlyCollection inputEmbeds) { embeds = inputEmbeds; return this; } public ResponseBuilder File(Stream stream, string name) { fileStream = stream; fileName = name; return this; } public PaginatedResponseBuilder Paginated() => new(this); } public class PaginatedResponseBuilder { protected readonly ResponseBuilder _builder; public PaginatedResponseBuilder(ResponseBuilder builder) { _builder = builder; } public SourcedPaginatedResponseBuilder Items(IReadOnlyCollection items) => new SourcedPaginatedResponseBuilder(_builder) .Items(items); public SourcedPaginatedResponseBuilder PageItems(Func>> items) => new SourcedPaginatedResponseBuilder(_builder) .PageItems(items); } public sealed class SourcedPaginatedResponseBuilder : PaginatedResponseBuilder { private IReadOnlyCollection? items; public Func, int, Task> PageFunc { get; private set; } = static delegate { return Task.FromResult(new()); }; public Func>> ItemsFunc { get; set; } = static delegate { return Task.FromResult>(ReadOnlyCollection.Empty); }; public Func>? InteractionFunc { get; private set; } public int? Elems { 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 Items(IReadOnlyCollection col) { items = col; Elems = col.Count; ItemsFunc = (i) => Task.FromResult(items.Skip(i * ItemsPerPage).Take(ItemsPerPage).ToArray() as IReadOnlyCollection); return this; } public SourcedPaginatedResponseBuilder TotalElements(int i) { Elems = i; return this; } public SourcedPaginatedResponseBuilder PageItems(Func>> func) { Elems = null; ItemsFunc = func; return this; } public SourcedPaginatedResponseBuilder PageSize(int i) { ItemsPerPage = i; return this; } public SourcedPaginatedResponseBuilder CurrentPage(int i) { InitialPage = i; return this; } public SourcedPaginatedResponseBuilder Page(Func, int, EmbedBuilder> pageFunc) { PageFunc = (xs, x) => Task.FromResult(pageFunc(xs, x)); return this; } public SourcedPaginatedResponseBuilder Page(Func, int, Task> pageFunc) { PageFunc = pageFunc; return this; } public SourcedPaginatedResponseBuilder AddFooter(bool addFooter = true) { AddPaginatedFooter = addFooter; return this; } public SourcedPaginatedResponseBuilder Ephemeral() { IsEphemeral = true; return this; } public Task SendAsync() { var paginationSender = new ResponseBuilder.PaginationSender( this, _builder); return paginationSender.SendAsync(IsEphemeral); } public SourcedPaginatedResponseBuilder Interaction(Func> func) { InteractionFunc = func; //async (i) => await func(i); return this; } public SourcedPaginatedResponseBuilder Interaction(NadekoInteractionBase inter) { InteractionFunc = _ => Task.FromResult(inter); return this; } }