diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlocker.cs b/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlocker.cs index e553f186..4bc0fc74 100644 --- a/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlocker.cs +++ b/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlocker.cs @@ -9,6 +9,6 @@ namespace NadekoBot.DataStructures.ModuleBehaviors /// public interface IEarlyBlocker { - Task TryBlockEarly(DiscordShardedClient client, IGuild guild, IUserMessage msg); + Task TryBlockEarly(IGuild guild, IUserMessage msg); } } diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/IINputTransformer.cs b/src/NadekoBot/DataStructures/ModuleBehaviors/IINputTransformer.cs new file mode 100644 index 00000000..3dd96464 --- /dev/null +++ b/src/NadekoBot/DataStructures/ModuleBehaviors/IINputTransformer.cs @@ -0,0 +1,10 @@ +using Discord; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures.ModuleBehaviors +{ + public interface IInputTransformer + { + Task TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input); + } +} diff --git a/src/NadekoBot/Modules/NadekoModule.cs b/src/NadekoBot/Modules/NadekoModule.cs index 7ef38f17..2b41125f 100644 --- a/src/NadekoBot/Modules/NadekoModule.cs +++ b/src/NadekoBot/Modules/NadekoModule.cs @@ -15,8 +15,7 @@ namespace NadekoBot.Modules public readonly string Prefix; public readonly string ModuleTypeName; public readonly string LowerModuleTypeName; - - //todo :thinking: + public NadekoStrings _strings { get; set; } public ILocalization _localization { get; set; } diff --git a/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs b/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs index 65be07de..e5294fc6 100644 --- a/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs @@ -19,11 +19,11 @@ namespace NadekoBot.Modules.Utility [Group] public class CommandMapCommands : NadekoSubmodule { - private readonly UtilityService _service; + private readonly CommandMapService _service; private readonly DbService _db; private readonly DiscordShardedClient _client; - public CommandMapCommands(UtilityService service, DbService db, DiscordShardedClient client) + public CommandMapCommands(CommandMapService service, DbService db, DiscordShardedClient client) { _service = service; _db = db; diff --git a/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs b/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs index 4af2c18d..de917054 100644 --- a/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs +++ b/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs @@ -14,9 +14,9 @@ namespace NadekoBot.Modules.Utility [Group] public class CrossServerTextChannel : NadekoSubmodule { - private readonly UtilityService _service; + private readonly CrossServerTextService _service; - public CrossServerTextChannel(UtilityService service) + public CrossServerTextChannel(CrossServerTextService service) { _service = service; } diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index f009e500..ae746cfb 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -16,25 +16,23 @@ using System.Collections.Generic; using Newtonsoft.Json; using Discord.WebSocket; using System.Diagnostics; -using NadekoBot.Modules; using Color = Discord.Color; +using NadekoBot.Services; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility { public partial class Utility : NadekoTopLevelModule { private static ConcurrentDictionary _rotatingRoleColors = new ConcurrentDictionary(); private readonly DiscordShardedClient _client; private readonly IStatsService _stats; - private readonly UtilityService _service; //private readonly MusicService _music; private readonly IBotCredentials _creds; - public Utility(UtilityService service, DiscordShardedClient client, IStatsService stats, IBotCredentials creds) + public Utility(DiscordShardedClient client, IStatsService stats, IBotCredentials creds) { _client = client; _stats = stats; - _service = service; //_music = music; _creds = creds; } diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 838a613f..009763b2 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -105,10 +105,11 @@ namespace NadekoBot //module services //todo 90 - autodiscover, DI, and add instead of manual like this #region utility - var utilityService = new UtilityService(AllGuildConfigs, Client); + var crossServerTextService = new CrossServerTextService(AllGuildConfigs, Client); var remindService = new RemindService(Client, BotConfig, Db); var repeaterService = new MessageRepeaterService(Client, AllGuildConfigs); var converterService = new ConverterService(Db); + var commandMapService = new CommandMapService(AllGuildConfigs); #endregion #region Searches @@ -142,7 +143,7 @@ namespace NadekoBot var permissionsService = new PermissionsService(Db, BotConfig); var blacklistService = new BlacklistService(BotConfig); var cmdcdsService = new CmdCdService(AllGuildConfigs); - var filterService = new FilterService(AllGuildConfigs); + var filterService = new FilterService(Client, AllGuildConfigs); var globalPermsService = new GlobalPermissionService(BotConfig); #endregion @@ -162,7 +163,8 @@ namespace NadekoBot .Add(commandHandler) .Add(Db) //modules - .Add(utilityService) + .Add(crossServerTextService) + .Add(commandMapService) .Add(remindService) .Add(repeaterService) .Add(converterService) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index bd6152d2..c90a7176 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -88,27 +88,6 @@ namespace NadekoBot.Services public Task StartHandling() { _client.MessageReceived += MessageReceivedHandler; - _client.MessageUpdated += (oldmsg, newMsg, channel) => - { - var ignore = Task.Run(() => - { - try - { - var usrMsg = newMsg as SocketUserMessage; - var guild = (usrMsg?.Channel as ITextChannel)?.Guild; - ////todo invite filtering - //if (guild != null && !await InviteFiltered(guild, usrMsg).ConfigureAwait(false)) - // await WordFiltered(guild, usrMsg).ConfigureAwait(false); - - } - catch (Exception ex) - { - _log.Warn(ex); - } - return Task.CompletedTask; - }); - return Task.CompletedTask; - }; return Task.CompletedTask; } @@ -182,18 +161,20 @@ namespace NadekoBot.Services { var execTime = Environment.TickCount; + //its nice to have early blockers and early blocking executors separate, but + //i could also have one interface with priorities, and just put early blockers on + //highest priority. :thinking: foreach (var svc in _services) { if (svc is IEarlyBlocker blocker && - await blocker.TryBlockEarly(_client, guild, usrMsg).ConfigureAwait(false)) + await blocker.TryBlockEarly(guild, usrMsg).ConfigureAwait(false)) { _log.Info("Blocked User: [{0}] Message: [{1}] Service: [{2}]", usrMsg.Author, usrMsg.Content, svc.GetType().Name); return; } } - var exec2 = Environment.TickCount - execTime; - + var exec2 = Environment.TickCount - execTime; foreach (var svc in _services) { @@ -208,44 +189,21 @@ namespace NadekoBot.Services var exec3 = Environment.TickCount - execTime; string messageContent = usrMsg.Content; - ////todo alias mapping - // if (guild != null) - // { - // if (Modules.Utility.Utility.CommandMapCommands.AliasMaps.TryGetValue(guild.Id, out ConcurrentDictionary maps)) - // { - - // var keys = maps.Keys - // .OrderByDescending(x => x.Length); - - // var lowerMessageContent = messageContent.ToLowerInvariant(); - // foreach (var k in keys) - // { - // string newMessageContent; - // if (lowerMessageContent.StartsWith(k + " ")) - // newMessageContent = maps[k] + messageContent.Substring(k.Length, messageContent.Length - k.Length); - // else if (lowerMessageContent == k) - // newMessageContent = maps[k]; - // else - // continue; - - // _log.Info(@"--Mapping Command-- - //GuildId: {0} - //Trigger: {1} - //Mapping: {2}", guild.Id, messageContent, newMessageContent); - // var oldMessageContent = messageContent; - // messageContent = newMessageContent; - - // try { await usrMsg.Channel.SendConfirmAsync($"{oldMessageContent} => {newMessageContent}").ConfigureAwait(false); } catch { } - // break; - // } - // } - // } - + foreach (var svc in _services) + { + string newContent; + if (svc is IInputTransformer exec && + (newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, messageContent).ConfigureAwait(false)) != messageContent.ToLowerInvariant()) + { + messageContent = newContent; + break; + } + } // execute the command and measure the time it took if (messageContent.StartsWith(NadekoBot.Prefix)) { - var exec = await Task.Run(() => ExecuteCommandAsync(new CommandContext(_client, usrMsg), NadekoBot.Prefix.Length, _services, MultiMatchHandling.Best)).ConfigureAwait(false); + var exec = await Task.Run(() => ExecuteCommandAsync(new CommandContext(_client, usrMsg), messageContent, NadekoBot.Prefix.Length, _services, MultiMatchHandling.Best)).ConfigureAwait(false); execTime = Environment.TickCount - execTime; ////todo commandHandler @@ -281,8 +239,8 @@ namespace NadekoBot.Services } - public Task ExecuteCommandAsync(CommandContext context, int argPos, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - => ExecuteCommand(context, context.Message.Content.Substring(argPos), serviceProvider, multiMatchHandling); + public Task ExecuteCommandAsync(CommandContext context, string input, int argPos, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + => ExecuteCommand(context, input.Substring(argPos), serviceProvider, multiMatchHandling); public async Task ExecuteCommand(CommandContext context, string input, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) @@ -333,25 +291,6 @@ namespace NadekoBot.Services var module = cmd.Module.GetTopLevelModule(); if (context.Guild != null) { - - ////todo perms - //PermissionCache pc = Permissions.GetCache(context.Guild.Id); - //if (!resetCommand && !pc.Permissions.CheckPermissions(context.Message, cmd.Aliases.First(), module.Name, out int index)) - //{ - // var returnMsg = $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand((SocketGuild)context.Guild)}** is preventing this action."; - // return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, returnMsg)); - //} - - - //if (module.Name == typeof(Permissions).Name) - //{ - // var guildUser = (IGuildUser)context.User; - // if (!guildUser.GetRoles().Any(r => r.Name.Trim().ToLowerInvariant() == pc.PermRole.Trim().ToLowerInvariant()) && guildUser.Id != guildUser.Guild.OwnerId) - // { - // return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, $"You need the **{pc.PermRole}** role in order to use permission commands.")); - // } - //} - //////future ////int price; ////if (Permissions.CommandCostCommands.CommandCosts.TryGetValue(cmd.Aliases.First().Trim().ToLowerInvariant(), out price) && price > 0) @@ -364,14 +303,6 @@ namespace NadekoBot.Services ////} } - ////todo perms - //if (cmd.Name != "resetglobalperms" && - // (GlobalPermissionCommands.BlockedCommands.Contains(cmd.Aliases.First().ToLowerInvariant()) || - // GlobalPermissionCommands.BlockedModules.Contains(module.Name.ToLowerInvariant()))) - //{ - // return new ExecuteCommandResult(cmd, null, SearchResult.FromError(CommandError.Exception, $"Command or module is blocked globally by the bot owner.")); - //} - // Bot will ignore commands which are ran more often than what specified by // GlobalCommandsCooldown constant (miliseconds) if (!UsersOnShortCooldown.Add(context.Message.Author.Id)) diff --git a/src/NadekoBot/Services/Games/ChatterbotService.cs b/src/NadekoBot/Services/Games/ChatterbotService.cs index 9690cd38..037754ba 100644 --- a/src/NadekoBot/Services/Games/ChatterbotService.cs +++ b/src/NadekoBot/Services/Games/ChatterbotService.cs @@ -95,7 +95,7 @@ namespace NadekoBot.Services.Games // "Games".ToLowerInvariant(), // out int index)) //{ - // //todo print in guild actually + // //todo 46 print in guild actually // var returnMsg = // $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand(guild)}** is preventing this action."; // _log.Info(returnMsg); diff --git a/src/NadekoBot/Services/Permissions/FilterService.cs b/src/NadekoBot/Services/Permissions/FilterService.cs index 4e10a441..0fced623 100644 --- a/src/NadekoBot/Services/Permissions/FilterService.cs +++ b/src/NadekoBot/Services/Permissions/FilterService.cs @@ -41,7 +41,7 @@ namespace NadekoBot.Services.Permissions return words; } - public FilterService(IEnumerable gcs) + public FilterService(DiscordShardedClient _client, IEnumerable gcs) { _log = LogManager.GetCurrentClassLogger(); @@ -56,15 +56,22 @@ namespace NadekoBot.Services.Permissions WordFilteringServers = new ConcurrentHashSet(serverFiltering.Select(gc => gc.GuildId)); WordFilteringChannels = new ConcurrentHashSet(gcs.SelectMany(gc => gc.FilterWordsChannelIds.Select(fwci => fwci.ChannelId))); - } - //todo ignore guild admin - public async Task TryBlockEarly(DiscordShardedClient client, IGuild guild, IUserMessage msg) - => await FilterInvites(client, guild, msg) || await FilterWords(client, guild, msg); - public async Task FilterWords(DiscordShardedClient client, IGuild guild, IUserMessage usrMsg) + _client.MessageUpdated += (oldData, newMsg, channel) + => FilterInvites((channel as ITextChannel)?.Guild, newMsg as IUserMessage); + } + + public async Task TryBlockEarly(IGuild guild, IUserMessage msg) + => !(msg.Author is IGuildUser gu) //it's never filtered outside of guilds, and never block administrators + ? false + : gu.GuildPermissions.Administrator && (await FilterInvites(guild, msg) || await FilterWords(guild, msg)); + + public async Task FilterWords(IGuild guild, IUserMessage usrMsg) { if (guild is null) return false; + if (usrMsg is null) + return false; var filteredChannelWords = FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet(); var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet(); @@ -91,10 +98,12 @@ namespace NadekoBot.Services.Permissions return false; } - public async Task FilterInvites(DiscordShardedClient client, IGuild guild, IUserMessage usrMsg) + public async Task FilterInvites(IGuild guild, IUserMessage usrMsg) { if (guild is null) return false; + if (usrMsg is null) + return false; if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) || InviteFilteringServers.Contains(guild.Id)) && diff --git a/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs b/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs index c034cdf0..96134c8d 100644 --- a/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs +++ b/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs @@ -1,10 +1,15 @@ -using NadekoBot.Services.Database.Models; +using NadekoBot.DataStructures.ModuleBehaviors; +using NadekoBot.Services.Database.Models; using System.Collections.Concurrent; using System.Linq; +using Discord; +using Discord.WebSocket; +using System; +using System.Threading.Tasks; namespace NadekoBot.Services.Permissions { - public class GlobalPermissionService + public class GlobalPermissionService : ILateBlocker { public readonly ConcurrentHashSet BlockedModules; public readonly ConcurrentHashSet BlockedCommands; @@ -14,5 +19,20 @@ namespace NadekoBot.Services.Permissions BlockedModules = new ConcurrentHashSet(bc.BlockedModules.Select(x => x.Name)); BlockedCommands = new ConcurrentHashSet(bc.BlockedCommands.Select(x => x.Name)); } + + public async Task TryBlockLate(DiscordShardedClient client, IUserMessage msg, IGuild guild, IMessageChannel channel, IUser user, string moduleName, string commandName) + { + await Task.Yield(); + commandName = commandName.ToLowerInvariant(); + + if (commandName != "resetglobalperms" && + (BlockedCommands.Contains(commandName) || + BlockedModules.Contains(moduleName.ToLowerInvariant()))) + { + return true; + //return new ExecuteCommandResult(cmd, null, SearchResult.FromError(CommandError.Exception, $"Command or module is blocked globally by the bot owner.")); + } + return false; + } } } diff --git a/src/NadekoBot/Services/Permissions/PermissionsService.cs b/src/NadekoBot/Services/Permissions/PermissionsService.cs index a26b3d38..bc7a77e5 100644 --- a/src/NadekoBot/Services/Permissions/PermissionsService.cs +++ b/src/NadekoBot/Services/Permissions/PermissionsService.cs @@ -169,26 +169,27 @@ namespace NadekoBot.Services.Permissions return false; } - - var resetCommand = commandName == "resetperms"; - - //todo perms - PermissionCache pc = GetCache(guild.Id); - if (!resetCommand && !pc.Permissions.CheckPermissions(msg, commandName, moduleName, out int index)) + else { - var returnMsg = $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand((SocketGuild)guild)}** is preventing this action."; - return true; - //return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, returnMsg)); - } + var resetCommand = commandName == "resetperms"; - - if (moduleName == "Permissions") - { - var roles = (user as SocketGuildUser)?.Roles ?? ((IGuildUser)user).RoleIds.Select(x => guild.GetRole(x)).Where(x => x != null); - if (!roles.Any(r => r.Name.Trim().ToLowerInvariant() == pc.PermRole.Trim().ToLowerInvariant()) && user.Id != ((IGuildUser)user).Guild.OwnerId) + PermissionCache pc = GetCache(guild.Id); + if (!resetCommand && !pc.Permissions.CheckPermissions(msg, commandName, moduleName, out int index)) { + var returnMsg = $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand((SocketGuild)guild)}** is preventing this action."; return true; - //return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, $"You need the **{pc.PermRole}** role in order to use permission commands.")); + //return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, returnMsg)); + } + + + if (moduleName == "Permissions") + { + var roles = (user as SocketGuildUser)?.Roles ?? ((IGuildUser)user).RoleIds.Select(x => guild.GetRole(x)).Where(x => x != null); + if (!roles.Any(r => r.Name.Trim().ToLowerInvariant() == pc.PermRole.Trim().ToLowerInvariant()) && user.Id != ((IGuildUser)user).Guild.OwnerId) + { + return true; + //return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, $"You need the **{pc.PermRole}** role in order to use permission commands.")); + } } } diff --git a/src/NadekoBot/Services/Utility/CommandMapService.cs b/src/NadekoBot/Services/Utility/CommandMapService.cs new file mode 100644 index 00000000..005af0d1 --- /dev/null +++ b/src/NadekoBot/Services/Utility/CommandMapService.cs @@ -0,0 +1,79 @@ +using NadekoBot.DataStructures.ModuleBehaviors; +using NadekoBot.Services.Database.Models; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; +using NLog; +using NadekoBot.Extensions; + +namespace NadekoBot.Services.Utility +{ + public class CommandMapService : IInputTransformer + { + private readonly Logger _log; + + public ConcurrentDictionary> AliasMaps { get; } = new ConcurrentDictionary>(); + //commandmap + public CommandMapService(IEnumerable gcs) + { + _log = LogManager.GetCurrentClassLogger(); + AliasMaps = new ConcurrentDictionary>( + gcs.ToDictionary( + x => x.GuildId, + x => new ConcurrentDictionary(x.CommandAliases + .Distinct(new CommandAliasEqualityComparer()) + .ToDictionary(ca => ca.Trigger, ca => ca.Mapping)))); + } + + public async Task TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input) + { + await Task.Yield(); + + if (guild == null || string.IsNullOrWhiteSpace(input)) + return input; + + //todo alias mapping + if (guild != null) + { + input = input.ToLowerInvariant(); + if (AliasMaps.TryGetValue(guild.Id, out ConcurrentDictionary maps)) + { + var keys = maps.Keys + .OrderByDescending(x => x.Length); + + foreach (var k in keys) + { + string newInput; + if (input.StartsWith(k + " ")) + newInput = maps[k] + input.Substring(k.Length, input.Length - k.Length); + else if (input == k) + newInput = maps[k]; + else + continue; + + _log.Info(@"--Mapping Command-- + GuildId: {0} + Trigger: {1} + Mapping: {2}", guild.Id, input, newInput); + + try { await channel.SendConfirmAsync($"{input} => {newInput}").ConfigureAwait(false); } catch { } + return newInput; + } + } + } + + return input; + } + } + + public class CommandAliasEqualityComparer : IEqualityComparer + { + public bool Equals(CommandAlias x, CommandAlias y) => x.Trigger == y.Trigger; + + public int GetHashCode(CommandAlias obj) => obj.Trigger.GetHashCode(); + } +} diff --git a/src/NadekoBot/Services/Utility/UtilityService.cs b/src/NadekoBot/Services/Utility/UtilityService.cs index eca8eaac..46b52d21 100644 --- a/src/NadekoBot/Services/Utility/UtilityService.cs +++ b/src/NadekoBot/Services/Utility/UtilityService.cs @@ -9,21 +9,14 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Utility { - public class UtilityService + public class CrossServerTextService { - public ConcurrentDictionary> AliasMaps { get; } = new ConcurrentDictionary>(); + public readonly ConcurrentDictionary> Subscribers = + new ConcurrentDictionary>(); + private DiscordShardedClient _client; - public UtilityService(IEnumerable guildConfigs, DiscordShardedClient client) + public CrossServerTextService(IEnumerable guildConfigs, DiscordShardedClient client) { - //commandmap - AliasMaps = new ConcurrentDictionary>( - guildConfigs.ToDictionary( - x => x.GuildId, - x => new ConcurrentDictionary(x.CommandAliases - .Distinct(new CommandAliasEqualityComparer()) - .ToDictionary(ca => ca.Trigger, ca => ca.Mapping)))); - - //cross server _client = client; _client.MessageReceived += Client_MessageReceived; } @@ -68,16 +61,5 @@ namespace NadekoBot.Services.Utility private string GetMessage(ITextChannel channel, IGuildUser user, IUserMessage message) => $"**{channel.Guild.Name} | {channel.Name}** `{user.Username}`: " + message.Content.SanitizeMentions(); - - public readonly ConcurrentDictionary> Subscribers = - new ConcurrentDictionary>(); - private DiscordShardedClient _client; - } - - public class CommandAliasEqualityComparer : IEqualityComparer - { - public bool Equals(CommandAlias x, CommandAlias y) => x.Trigger == y.Trigger; - - public int GetHashCode(CommandAlias obj) => obj.Trigger.GetHashCode(); } }