diff --git a/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs b/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs index 70279c949..cef336e63 100644 --- a/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs +++ b/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; @@ -11,6 +10,9 @@ namespace DisCatSharp.ApplicationCommands.Checks; +/// +/// The application command equality checks. +/// internal static class ApplicationCommandEqualityChecks { /// @@ -20,42 +22,54 @@ internal static class ApplicationCommandEqualityChecks /// Command to check against. /// The discord client. /// Whether the equal check is performed for a guild command. - internal static bool IsEqualTo(this DiscordApplicationCommand ac1, - DiscordApplicationCommand targetApplicationCommand, DiscordClient client, bool isGuild) + internal static bool IsEqualTo( + this DiscordApplicationCommand ac1, + DiscordApplicationCommand targetApplicationCommand, DiscordClient client, bool isGuild + ) { if (targetApplicationCommand is null || ac1 is null) return false; DiscordApplicationCommand sourceApplicationCommand = new( - ac1.Name, ac1.Description, ac1.Options, - ac1.Type, - ac1.NameLocalizations, ac1.DescriptionLocalizations, - ac1.DefaultMemberPermissions, ac1.DmPermission ?? true, - ac1.IsNsfw, ac1.AllowedContexts, ac1.IntegrationTypes - ); + ac1.Name, ac1.Description, ac1.Options, + ac1.Type, + ac1.NameLocalizations, ac1.DescriptionLocalizations, + ac1.DefaultMemberPermissions, ac1.DmPermission ?? true, + ac1.IsNsfw, ac1.AllowedContexts, ac1.IntegrationTypes + ); if (sourceApplicationCommand.DefaultMemberPermissions == Permissions.None && - targetApplicationCommand.DefaultMemberPermissions == null) + targetApplicationCommand.DefaultMemberPermissions == null) sourceApplicationCommand.DefaultMemberPermissions = null; if (isGuild) { sourceApplicationCommand.DmPermission = null; targetApplicationCommand.DmPermission = null; + sourceApplicationCommand.IntegrationTypes = null; + targetApplicationCommand.IntegrationTypes = null; + sourceApplicationCommand.AllowedContexts = null; + targetApplicationCommand.AllowedContexts = null; } else { - sourceApplicationCommand.IntegrationTypes ??= new() { ApplicationCommandIntegrationTypes.InstalledToGuild }; - targetApplicationCommand.IntegrationTypes ??= new() { ApplicationCommandIntegrationTypes.InstalledToGuild }; + sourceApplicationCommand.IntegrationTypes ??= new() + { + ApplicationCommandIntegrationTypes.InstalledToGuild + }; + targetApplicationCommand.IntegrationTypes ??= new() + { + ApplicationCommandIntegrationTypes.InstalledToGuild + }; } client.Logger.Log(ApplicationCommandsExtension.ApplicationCommandsLogLevel, - "[AC Change Check] Command {name}\n\n[{jsonOne},{jsontwo}]\n\n", ac1.Name, - JsonConvert.SerializeObject(sourceApplicationCommand), - JsonConvert.SerializeObject(targetApplicationCommand)); + "[AC Change Check] Command {name}\n\n[{jsonOne},{jsontwo}]\n\n", ac1.Name, + JsonConvert.SerializeObject(sourceApplicationCommand), + JsonConvert.SerializeObject(targetApplicationCommand)); return ac1.Type == targetApplicationCommand.Type && sourceApplicationCommand.SoftEqual(targetApplicationCommand, - ac1.Type, ApplicationCommandsExtension.Configuration?.EnableLocalization ?? false, isGuild); + ac1.Type, client, ApplicationCommandsExtension.Configuration?.EnableLocalization ?? false, isGuild); } /// @@ -67,8 +81,10 @@ internal static bool IsEqualTo(this DiscordApplicationCommand ac1, /// The application command type. /// Whether localization is enabled. /// Whether the equal check is performed for a guild command. - internal static bool SoftEqual(this DiscordApplicationCommand source, DiscordApplicationCommand target, - ApplicationCommandType type, bool localizationEnabled = false, bool guild = false) + internal static bool SoftEqual( + this DiscordApplicationCommand source, DiscordApplicationCommand target, + ApplicationCommandType type, BaseDiscordClient client, bool localizationEnabled = false, bool guild = false + ) { bool? sDmPerm = source.DmPermission ?? true; bool? tDmPerm = target.DmPermission ?? true; @@ -76,23 +92,24 @@ internal static bool SoftEqual(this DiscordApplicationCommand source, DiscordApp return localizationEnabled ? type switch { - ApplicationCommandType.ChatInput => DeepEqual(source, target, true, sDmPerm, tDmPerm), + ApplicationCommandType.ChatInput => DeepEqual(source, target, client, true, sDmPerm, tDmPerm), _ => source.Name == target.Name - && source.Type == target.Type && source.NameLocalizations == target.NameLocalizations - && source.DefaultMemberPermissions == target.DefaultMemberPermissions - && sDmPerm == tDmPerm && source.IsNsfw == target.IsNsfw - && source.AllowedContexts.NullableSequenceEqual(target.AllowedContexts) && - source.IntegrationTypes.NullableSequenceEqual(target.IntegrationTypes) + && source.Type == target.Type && source.NameLocalizations == target.NameLocalizations + && source.DefaultMemberPermissions == target.DefaultMemberPermissions + && sDmPerm == tDmPerm && source.IsNsfw == target.IsNsfw + && source.AllowedContexts.NullableSequenceEqual(target.AllowedContexts) && + source.IntegrationTypes.NullableSequenceEqual(target.IntegrationTypes) && + source.RawNameLocalizations.AreDictionariesEqual(target.RawNameLocalizations) } : type switch { - ApplicationCommandType.ChatInput => DeepEqual(source, target, false, sDmPerm, tDmPerm), + ApplicationCommandType.ChatInput => DeepEqual(source, target, client, false, sDmPerm, tDmPerm), _ => source.Name == target.Name - && source.Type == target.Type - && source.DefaultMemberPermissions == target.DefaultMemberPermissions - && sDmPerm == tDmPerm && source.IsNsfw == target.IsNsfw - && source.AllowedContexts.NullableSequenceEqual(target.AllowedContexts) && - source.IntegrationTypes.NullableSequenceEqual(target.IntegrationTypes) + && source.Type == target.Type + && source.DefaultMemberPermissions == target.DefaultMemberPermissions + && sDmPerm == tDmPerm && source.IsNsfw == target.IsNsfw + && source.AllowedContexts.NullableSequenceEqual(target.AllowedContexts) && + source.IntegrationTypes.NullableSequenceEqual(target.IntegrationTypes) }; sDmPerm = null; @@ -100,23 +117,20 @@ internal static bool SoftEqual(this DiscordApplicationCommand source, DiscordApp return localizationEnabled ? type switch { - ApplicationCommandType.ChatInput => DeepEqual(source, target, true, sDmPerm, tDmPerm), + ApplicationCommandType.ChatInput => DeepEqual(source, target, client, true, sDmPerm, tDmPerm), _ => source.Name == target.Name - && source.Type == target.Type && source.NameLocalizations == target.NameLocalizations - && source.DefaultMemberPermissions == target.DefaultMemberPermissions - && sDmPerm == tDmPerm && source.IsNsfw == target.IsNsfw - && source.AllowedContexts.NullableSequenceEqual(target.AllowedContexts) && - source.IntegrationTypes.NullableSequenceEqual(target.IntegrationTypes) + && source.Type == target.Type && source.NameLocalizations == target.NameLocalizations + && source.DefaultMemberPermissions == target.DefaultMemberPermissions + && sDmPerm == tDmPerm && source.IsNsfw == target.IsNsfw + && source.RawNameLocalizations.AreDictionariesEqual(target.RawNameLocalizations) } : type switch { - ApplicationCommandType.ChatInput => DeepEqual(source, target, false, sDmPerm, tDmPerm), + ApplicationCommandType.ChatInput => DeepEqual(source, target, client, false, sDmPerm, tDmPerm), _ => source.Name == target.Name - && source.Type == target.Type - && source.DefaultMemberPermissions == target.DefaultMemberPermissions - && sDmPerm == tDmPerm && source.IsNsfw == target.IsNsfw - && source.AllowedContexts.NullableSequenceEqual(target.AllowedContexts) && - source.IntegrationTypes.NullableSequenceEqual(target.IntegrationTypes) + && source.Type == target.Type + && source.DefaultMemberPermissions == target.DefaultMemberPermissions + && sDmPerm == tDmPerm && source.IsNsfw == target.IsNsfw }; } @@ -127,13 +141,33 @@ internal static bool SoftEqual(this DiscordApplicationCommand source, DiscordApp /// The source enumerable. /// The target enumerable. /// Whether both nullable enumerable are equal. - internal static bool NullableSequenceEqual(this IEnumerable? source, IEnumerable? target) + internal static bool NullableSequenceEqual(this List? source, List? target) { if (source is not null && target is not null) - return source.OrderBy(x => x).SequenceEqual(target.OrderBy(x => x)); + return source.All(target.Contains) && source.Count == target.Count; + + return source is null && target is null; + } + + /// + /// Checks whether two dictionaries are equal. + /// + /// The source dictionary. + /// The target dictionary. + /// Whether both dictionaries are equal. + internal static bool AreDictionariesEqual(this Dictionary sourceDictionary, Dictionary targetDictionary) + { + if (sourceDictionary?.Count != targetDictionary?.Count) + return false; + + if (sourceDictionary is null && targetDictionary is null) + return true; + + foreach (var kvp in sourceDictionary) + if (!targetDictionary.TryGetValue(kvp.Key, out var value) || value != kvp.Value) + return false; - return (source is null || target is not null) && - (source is not null || target is null); + return true; } /// @@ -145,29 +179,33 @@ internal static bool NullableSequenceEqual(this IEnumerable? source, IEnum /// Whether localization is enabled. /// The source dm permission. /// The target dm permission. - internal static bool DeepEqual(DiscordApplicationCommand source, DiscordApplicationCommand target, - bool localizationEnabled = false, bool? sDmPerm = null, bool? tDmPerm = null) + internal static bool DeepEqual( + DiscordApplicationCommand source, DiscordApplicationCommand target, BaseDiscordClient client, + bool localizationEnabled = false, bool? sDmPerm = null, bool? tDmPerm = null + ) { var name = source.Name; var rootCheck = source.Name == target.Name && - source.Description == target.Description && - source.Type == target.Type && - source.DefaultMemberPermissions == target.DefaultMemberPermissions && - sDmPerm == tDmPerm && - source.IsNsfw == target.IsNsfw - && source.AllowedContexts.NullableSequenceEqual(target.AllowedContexts) && - source.IntegrationTypes.NullableSequenceEqual(target.IntegrationTypes); + source.Description == target.Description && + source.Type == target.Type && + source.DefaultMemberPermissions == target.DefaultMemberPermissions && + sDmPerm == tDmPerm && + source.IsNsfw == target.IsNsfw && + source.AllowedContexts.NullableSequenceEqual(target.AllowedContexts) && + source.IntegrationTypes.NullableSequenceEqual(target.IntegrationTypes); if (localizationEnabled) rootCheck = rootCheck && - source.NameLocalizations.Localizations.NullableSequenceEqual(target.NameLocalizations.Localizations) && - source.DescriptionLocalizations.Localizations.NullableSequenceEqual(target.DescriptionLocalizations - .Localizations); + source.RawNameLocalizations.AreDictionariesEqual(target.RawNameLocalizations) && + source.RawDescriptionLocalizations.AreDictionariesEqual(target.RawDescriptionLocalizations); - // Compare the Options using recursion var optionsEqual = DeepEqualOptions(source.Options, target.Options, localizationEnabled); + if (optionsEqual.Reason is not null) + client.Logger.Log(ApplicationCommandsExtension.ApplicationCommandsLogLevel, "Inequality found in options of {name} - {reason}", name, optionsEqual.Reason); + if (!rootCheck) + client.Logger.Log(ApplicationCommandsExtension.ApplicationCommandsLogLevel, "Inequality found in root of {name}", name); - return rootCheck && optionsEqual; + return rootCheck && optionsEqual.Equal; } /// @@ -176,17 +214,19 @@ internal static bool DeepEqual(DiscordApplicationCommand source, DiscordApplicat /// Source options. /// Options to check against. /// Whether localization is enabled. - private static bool DeepEqualOptions(IReadOnlyList? sourceOptions, - IReadOnlyList? targetOptions, bool localizationEnabled) + private static (bool Equal, string? Reason) DeepEqualOptions( + IReadOnlyList? sourceOptions, + IReadOnlyList? targetOptions, bool localizationEnabled + ) { if (sourceOptions == null && targetOptions == null) - return true; + return (true, null); - if ((sourceOptions != null && targetOptions == null) || (sourceOptions == null && targetOptions != null)) - return false; + if (sourceOptions != null && targetOptions == null || sourceOptions == null && targetOptions != null) + return (false, "source or target option null, but not both"); if (sourceOptions!.Count != targetOptions!.Count) - return false; + return (false, "source option count mismatches target option count"); for (var i = 0; i < sourceOptions.Count; i++) { @@ -194,47 +234,47 @@ private static bool DeepEqualOptions(IReadOnlyList x.Name), Formatting.None); var j2 = JsonConvert.SerializeObject(targetOption.Choices.OrderBy(x => x.Name), Formatting.None); if (j1 != j2) - return false; + return (false, "choice mismatch - " + sourceOption.Name); } - if ((sourceOption.ChannelTypes is null && targetOption.ChannelTypes is not null) || - (sourceOption.ChannelTypes is not null && targetOption.ChannelTypes is null) || - (sourceOption.ChannelTypes is not null && targetOption.ChannelTypes is not null && - !sourceOption.ChannelTypes.OrderBy(x => x).All(targetOption.ChannelTypes.OrderBy(x => x).Contains))) - return false; + if (sourceOption.ChannelTypes is null && targetOption.ChannelTypes is not null || + sourceOption.ChannelTypes is not null && targetOption.ChannelTypes is null || + sourceOption.ChannelTypes is not null && targetOption.ChannelTypes is not null && + !sourceOption.ChannelTypes.OrderBy(x => x).All(targetOption.ChannelTypes.OrderBy(x => x).Contains)) + return (false, "channel type mismatch - " + sourceOption.Name); - if (!DeepEqualOptions(sourceOption.Options, targetOption.Options, localizationEnabled)) - return false; + var rec = DeepEqualOptions(sourceOption.Options, targetOption.Options, localizationEnabled); + if (!rec.Equal) + return (false, "Options Recursive - " + sourceOption.Name + " -- " + rec.Reason); if (!optionCheck) - return false; + return (false, "OptionCheck - " + sourceOption.Name); } - return true; + return (true, null); } }