From 18400dc53a4023476b54c24be548e73176ccef1d Mon Sep 17 00:00:00 2001 From: Kwoth Date: Sun, 17 Apr 2022 09:58:30 +0200 Subject: [PATCH] Fixed a nullref message when the bot is loading medusae. Added support for multiple embeds in features which support custom embeds in the form of {plainText:text-here, embeds: [embedObject, embedObject, embedObject]} --- .../Common/Medusa/Config/MedusaConfig.cs | 5 +- .../Medusa/Config/MedusaConfigService.cs | 8 ++- src/NadekoBot/Common/Replacements/Replacer.cs | 8 +++ .../Common/SmartText/SmartEmbedTextArray.cs | 28 +++++++++++ src/NadekoBot/Common/SmartText/SmartText.cs | 50 +++++++++++++++---- .../UserPunish/UserPunishService.cs | 2 +- src/NadekoBot/Modules/Help/HelpService.cs | 3 +- .../strings/impl/RedisBotStringsProvider.cs | 4 +- src/NadekoBot/_Extensions/Extensions.cs | 8 ++- .../_Extensions/IMessageChannelExtensions.cs | 11 ++-- 10 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 src/NadekoBot/Common/SmartText/SmartEmbedTextArray.cs diff --git a/src/NadekoBot/Common/Medusa/Config/MedusaConfig.cs b/src/NadekoBot/Common/Medusa/Config/MedusaConfig.cs index 158ea4b79..a9fdafd7f 100644 --- a/src/NadekoBot/Common/Medusa/Config/MedusaConfig.cs +++ b/src/NadekoBot/Common/Medusa/Config/MedusaConfig.cs @@ -1,4 +1,5 @@ -using Cloneable; +#nullable enable +using Cloneable; using NadekoBot.Common.Yml; namespace Nadeko.Medusa; @@ -10,7 +11,7 @@ public sealed partial class MedusaConfig : ICloneable public int Version { get; set; } = 1; [Comment("List of medusae automatically loaded at startup")] - public List Loaded { get; set; } + public List? Loaded { get; set; } public MedusaConfig() { diff --git a/src/NadekoBot/Common/Medusa/Config/MedusaConfigService.cs b/src/NadekoBot/Common/Medusa/Config/MedusaConfigService.cs index fddfe7dd0..95d604319 100644 --- a/src/NadekoBot/Common/Medusa/Config/MedusaConfigService.cs +++ b/src/NadekoBot/Common/Medusa/Config/MedusaConfigService.cs @@ -18,7 +18,7 @@ public sealed class MedusaConfigService : ConfigServiceBase, IMedu } public IReadOnlyCollection GetLoadedMedusae() - => Data.Loaded.ToList(); + => Data.Loaded?.ToList() ?? new List(); public void AddLoadedMedusa(string name) { @@ -26,6 +26,9 @@ public sealed class MedusaConfigService : ConfigServiceBase, IMedu ModifyConfig(conf => { + if (conf.Loaded is null) + conf.Loaded = new(); + if(!conf.Loaded.Contains(name)) conf.Loaded.Add(name); }); @@ -37,6 +40,9 @@ public sealed class MedusaConfigService : ConfigServiceBase, IMedu ModifyConfig(conf => { + if (conf.Loaded is null) + conf.Loaded = new(); + conf.Loaded.Remove(name); }); } diff --git a/src/NadekoBot/Common/Replacements/Replacer.cs b/src/NadekoBot/Common/Replacements/Replacer.cs index 000da0a94..0f657bd2d 100644 --- a/src/NadekoBot/Common/Replacements/Replacer.cs +++ b/src/NadekoBot/Common/Replacements/Replacer.cs @@ -36,9 +36,17 @@ public class Replacer { SmartEmbedText embedData => Replace(embedData), SmartPlainText plain => Replace(plain), + SmartEmbedTextArray arr => Replace(arr), _ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type") }; + public SmartEmbedTextArray Replace(SmartEmbedTextArray embedArr) + => new() + { + Embeds = embedArr.Embeds.Map(Replace), + PlainText = Replace(embedArr.PlainText) + }; + public SmartPlainText Replace(SmartPlainText plainText) => Replace(plainText.Text); diff --git a/src/NadekoBot/Common/SmartText/SmartEmbedTextArray.cs b/src/NadekoBot/Common/SmartText/SmartEmbedTextArray.cs new file mode 100644 index 000000000..aa250e18b --- /dev/null +++ b/src/NadekoBot/Common/SmartText/SmartEmbedTextArray.cs @@ -0,0 +1,28 @@ +#nullable disable +namespace NadekoBot; + +public sealed record SmartEmbedTextArray : SmartText +{ + public string PlainText { get; set; } + public SmartEmbedText[] Embeds { get; set; } + + public bool IsValid + => Embeds?.All(x => x.IsValid) ?? false; + + public EmbedBuilder[] GetEmbedBuilders() + { + if (Embeds is null) + return Array.Empty(); + + return Embeds.Map(em => em.GetEmbed()); + } + + public void NormalizeFields() + { + if (Embeds is null) + return; + + foreach(var eb in Embeds) + eb.NormalizeFields(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/SmartText/SmartText.cs b/src/NadekoBot/Common/SmartText/SmartText.cs index 23fb5b8af..f8b795e2f 100644 --- a/src/NadekoBot/Common/SmartText/SmartText.cs +++ b/src/NadekoBot/Common/SmartText/SmartText.cs @@ -1,5 +1,5 @@ #nullable disable -using Newtonsoft.Json; +using System.Text.Json; namespace NadekoBot; @@ -11,6 +11,15 @@ public abstract record SmartText public bool IsPlainText => this is SmartPlainText; + public bool IsEmbedArray + => this is SmartEmbedTextArray; + + private static readonly JsonSerializerOptions _opts = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + public static SmartText operator +(SmartText text, string input) => text switch { @@ -19,6 +28,10 @@ public abstract record SmartText PlainText = set.PlainText + input }, SmartPlainText spt => new SmartPlainText(spt.Text + input), + SmartEmbedTextArray arr => arr with + { + PlainText = arr.PlainText + input + }, _ => throw new ArgumentOutOfRangeException(nameof(text)) }; @@ -30,27 +43,46 @@ public abstract record SmartText PlainText = input + set.PlainText }, SmartPlainText spt => new SmartPlainText(input + spt.Text), + SmartEmbedTextArray arr => arr with + { + PlainText = input + arr.PlainText + }, _ => throw new ArgumentOutOfRangeException(nameof(text)) }; + [CanBeNull] public static SmartText CreateFrom(string input) { - if (string.IsNullOrWhiteSpace(input) || !input.TrimStart().StartsWith("{")) + if (string.IsNullOrWhiteSpace(input)) return new SmartPlainText(input); try { - var smartEmbedText = JsonConvert.DeserializeObject(input); + var doc = JsonDocument.Parse(input); + var root = doc.RootElement; + if (root.ValueKind == JsonValueKind.Object) + { + if (root.TryGetProperty("embeds", out _)) + { + var arr = root.Deserialize(_opts); - if (smartEmbedText is null) - throw new FormatException(); + if (arr is null) + return new SmartPlainText(input); - smartEmbedText.NormalizeFields(); + arr!.NormalizeFields(); + return arr; + } - if (!smartEmbedText.IsValid) - return new SmartPlainText(input); + var obj = root.Deserialize(_opts); - return smartEmbedText; + if (obj is null) + return new SmartPlainText(input); + + obj.NormalizeFields(); + return obj; + } + + return new SmartPlainText(input); } catch { diff --git a/src/NadekoBot/Modules/Administration/UserPunish/UserPunishService.cs b/src/NadekoBot/Modules/Administration/UserPunish/UserPunishService.cs index 0ff810c41..46353863c 100644 --- a/src/NadekoBot/Modules/Administration/UserPunish/UserPunishService.cs +++ b/src/NadekoBot/Modules/Administration/UserPunish/UserPunishService.cs @@ -530,7 +530,7 @@ public class UserPunishService : INService, IReadyExecutor return default; // if template is an embed, send that embed with replacements // otherwise, treat template as a regular string with replacements - else if (!SmartText.CreateFrom(template).IsEmbed) + else if (SmartText.CreateFrom(template) is not { IsEmbed: true } or { IsEmbedArray: true }) { template = JsonConvert.SerializeObject(new { diff --git a/src/NadekoBot/Modules/Help/HelpService.cs b/src/NadekoBot/Modules/Help/HelpService.cs index 2f5b96095..dc23ab9a0 100644 --- a/src/NadekoBot/Modules/Help/HelpService.cs +++ b/src/NadekoBot/Modules/Help/HelpService.cs @@ -81,8 +81,7 @@ public class HelpService : IExecNoCommand, INService em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs)); em.AddField(_strings.GetText(strs.usage), - string.Join("\n", - Array.ConvertAll(com.RealRemarksArr(_strings,_medusae, culture, prefix), 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)) .WithOkColor(); diff --git a/src/NadekoBot/Services/strings/impl/RedisBotStringsProvider.cs b/src/NadekoBot/Services/strings/impl/RedisBotStringsProvider.cs index 990bb545d..70f766392 100644 --- a/src/NadekoBot/Services/strings/impl/RedisBotStringsProvider.cs +++ b/src/NadekoBot/Services/strings/impl/RedisBotStringsProvider.cs @@ -46,7 +46,7 @@ public class RedisBotStringsProvider : IBotStringsProvider if (descStr == default) return null; - var args = Array.ConvertAll(argsStr.Split('&'), HttpUtility.UrlDecode); + var args = argsStr.Split('&').Map(HttpUtility.UrlDecode); return new() { Args = args, @@ -68,7 +68,7 @@ public class RedisBotStringsProvider : IBotStringsProvider { var hashFields = localeStrings .Select(x => new HashEntry($"{x.Key}::args", - string.Join('&', Array.ConvertAll(x.Value.Args, HttpUtility.UrlEncode)))) + string.Join('&', x.Value.Args.Map(HttpUtility.UrlEncode)))) .Concat(localeStrings.Select(x => new HashEntry($"{x.Key}::desc", x.Value.Desc))) .ToArray(); diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 7b5a4c269..9a86ac4fb 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -23,6 +23,11 @@ public static class Extensions x.Embed = set.GetEmbed().Build(); x.Content = set.PlainText?.SanitizeMentions() ?? ""; }), + SmartEmbedTextArray set => msg.ModifyAsync(x => + { + x.Embeds = set.GetEmbedBuilders().Map(eb => eb.Build()); + x.Content = set.PlainText?.SanitizeMentions() ?? ""; + }), SmartPlainText spt => msg.ModifyAsync(x => { x.Content = spt.Text.SanitizeMentions(); @@ -116,8 +121,7 @@ public static class Extensions args = strings.GetCommandStrings(cmd.Summary, culture).Args; } - return Array.ConvertAll(args, - arg => GetFullUsage(cmd.Name, arg, prefix)); + return args.Map(arg => GetFullUsage(cmd.Name, arg, prefix)); } private static string GetFullUsage(string commandName, string args, string prefix) diff --git a/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs b/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs index 7e45a2b97..06b8ba184 100644 --- a/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs +++ b/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs @@ -16,19 +16,22 @@ public static class MessageChannelExtensions public static Task SendAsync( this IMessageChannel channel, string? plainText, - Embed? embed, + Embed? embed = null, + Embed[]? embeds = null, bool sanitizeAll = false) { plainText = sanitizeAll ? plainText?.SanitizeAllMentions() ?? "" : plainText?.SanitizeMentions() ?? ""; - return channel.SendMessageAsync(plainText, embed: embed); + return channel.SendMessageAsync(plainText, embed: embed, embeds: embeds); } public static Task SendAsync(this IMessageChannel channel, SmartText text, bool sanitizeAll = false) => text switch { - SmartEmbedText set => channel.SendAsync(set.PlainText, set.GetEmbed().Build(), sanitizeAll), - SmartPlainText st => channel.SendAsync(st.Text, null, sanitizeAll), + SmartEmbedText set => channel.SendAsync(set.PlainText, set.GetEmbed().Build(), sanitizeAll: sanitizeAll), + SmartPlainText st => channel.SendAsync(st.Text, null, sanitizeAll: sanitizeAll), + SmartEmbedTextArray arr => channel.SendAsync(arr.PlainText, + embeds: arr.GetEmbedBuilders().Map(e => e.Build())), _ => throw new ArgumentOutOfRangeException(nameof(text)) };