Aliasing fixed, finishing up almost

This commit is contained in:
Master Kwoth 2017-05-30 01:53:16 +02:00
parent 6d27271d4a
commit bb897fc43d
14 changed files with 182 additions and 151 deletions

View File

@ -9,6 +9,6 @@ namespace NadekoBot.DataStructures.ModuleBehaviors
/// </summary> /// </summary>
public interface IEarlyBlocker public interface IEarlyBlocker
{ {
Task<bool> TryBlockEarly(DiscordShardedClient client, IGuild guild, IUserMessage msg); Task<bool> TryBlockEarly(IGuild guild, IUserMessage msg);
} }
} }

View File

@ -0,0 +1,10 @@
using Discord;
using System.Threading.Tasks;
namespace NadekoBot.DataStructures.ModuleBehaviors
{
public interface IInputTransformer
{
Task<string> TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input);
}
}

View File

@ -16,7 +16,6 @@ namespace NadekoBot.Modules
public readonly string ModuleTypeName; public readonly string ModuleTypeName;
public readonly string LowerModuleTypeName; public readonly string LowerModuleTypeName;
//todo :thinking:
public NadekoStrings _strings { get; set; } public NadekoStrings _strings { get; set; }
public ILocalization _localization { get; set; } public ILocalization _localization { get; set; }

View File

@ -19,11 +19,11 @@ namespace NadekoBot.Modules.Utility
[Group] [Group]
public class CommandMapCommands : NadekoSubmodule public class CommandMapCommands : NadekoSubmodule
{ {
private readonly UtilityService _service; private readonly CommandMapService _service;
private readonly DbService _db; private readonly DbService _db;
private readonly DiscordShardedClient _client; private readonly DiscordShardedClient _client;
public CommandMapCommands(UtilityService service, DbService db, DiscordShardedClient client) public CommandMapCommands(CommandMapService service, DbService db, DiscordShardedClient client)
{ {
_service = service; _service = service;
_db = db; _db = db;

View File

@ -14,9 +14,9 @@ namespace NadekoBot.Modules.Utility
[Group] [Group]
public class CrossServerTextChannel : NadekoSubmodule public class CrossServerTextChannel : NadekoSubmodule
{ {
private readonly UtilityService _service; private readonly CrossServerTextService _service;
public CrossServerTextChannel(UtilityService service) public CrossServerTextChannel(CrossServerTextService service)
{ {
_service = service; _service = service;
} }

View File

@ -16,25 +16,23 @@ using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using Discord.WebSocket; using Discord.WebSocket;
using System.Diagnostics; using System.Diagnostics;
using NadekoBot.Modules;
using Color = Discord.Color; using Color = Discord.Color;
using NadekoBot.Services;
namespace NadekoBot.Services.Utility namespace NadekoBot.Modules.Utility
{ {
public partial class Utility : NadekoTopLevelModule public partial class Utility : NadekoTopLevelModule
{ {
private static ConcurrentDictionary<ulong, Timer> _rotatingRoleColors = new ConcurrentDictionary<ulong, Timer>(); private static ConcurrentDictionary<ulong, Timer> _rotatingRoleColors = new ConcurrentDictionary<ulong, Timer>();
private readonly DiscordShardedClient _client; private readonly DiscordShardedClient _client;
private readonly IStatsService _stats; private readonly IStatsService _stats;
private readonly UtilityService _service;
//private readonly MusicService _music; //private readonly MusicService _music;
private readonly IBotCredentials _creds; private readonly IBotCredentials _creds;
public Utility(UtilityService service, DiscordShardedClient client, IStatsService stats, IBotCredentials creds) public Utility(DiscordShardedClient client, IStatsService stats, IBotCredentials creds)
{ {
_client = client; _client = client;
_stats = stats; _stats = stats;
_service = service;
//_music = music; //_music = music;
_creds = creds; _creds = creds;
} }

View File

@ -105,10 +105,11 @@ namespace NadekoBot
//module services //module services
//todo 90 - autodiscover, DI, and add instead of manual like this //todo 90 - autodiscover, DI, and add instead of manual like this
#region utility #region utility
var utilityService = new UtilityService(AllGuildConfigs, Client); var crossServerTextService = new CrossServerTextService(AllGuildConfigs, Client);
var remindService = new RemindService(Client, BotConfig, Db); var remindService = new RemindService(Client, BotConfig, Db);
var repeaterService = new MessageRepeaterService(Client, AllGuildConfigs); var repeaterService = new MessageRepeaterService(Client, AllGuildConfigs);
var converterService = new ConverterService(Db); var converterService = new ConverterService(Db);
var commandMapService = new CommandMapService(AllGuildConfigs);
#endregion #endregion
#region Searches #region Searches
@ -142,7 +143,7 @@ namespace NadekoBot
var permissionsService = new PermissionsService(Db, BotConfig); var permissionsService = new PermissionsService(Db, BotConfig);
var blacklistService = new BlacklistService(BotConfig); var blacklistService = new BlacklistService(BotConfig);
var cmdcdsService = new CmdCdService(AllGuildConfigs); var cmdcdsService = new CmdCdService(AllGuildConfigs);
var filterService = new FilterService(AllGuildConfigs); var filterService = new FilterService(Client, AllGuildConfigs);
var globalPermsService = new GlobalPermissionService(BotConfig); var globalPermsService = new GlobalPermissionService(BotConfig);
#endregion #endregion
@ -162,7 +163,8 @@ namespace NadekoBot
.Add<CommandHandler>(commandHandler) .Add<CommandHandler>(commandHandler)
.Add<DbService>(Db) .Add<DbService>(Db)
//modules //modules
.Add<UtilityService>(utilityService) .Add(crossServerTextService)
.Add(commandMapService)
.Add(remindService) .Add(remindService)
.Add(repeaterService) .Add(repeaterService)
.Add(converterService) .Add(converterService)

View File

@ -88,27 +88,6 @@ namespace NadekoBot.Services
public Task StartHandling() public Task StartHandling()
{ {
_client.MessageReceived += MessageReceivedHandler; _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; return Task.CompletedTask;
} }
@ -182,10 +161,13 @@ namespace NadekoBot.Services
{ {
var execTime = Environment.TickCount; 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) foreach (var svc in _services)
{ {
if (svc is IEarlyBlocker blocker && 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); _log.Info("Blocked User: [{0}] Message: [{1}] Service: [{2}]", usrMsg.Author, usrMsg.Content, svc.GetType().Name);
return; return;
@ -194,7 +176,6 @@ namespace NadekoBot.Services
var exec2 = Environment.TickCount - execTime; var exec2 = Environment.TickCount - execTime;
foreach (var svc in _services) foreach (var svc in _services)
{ {
if (svc is IEarlyBlockingExecutor exec && if (svc is IEarlyBlockingExecutor exec &&
@ -208,44 +189,21 @@ namespace NadekoBot.Services
var exec3 = Environment.TickCount - execTime; var exec3 = Environment.TickCount - execTime;
string messageContent = usrMsg.Content; string messageContent = usrMsg.Content;
////todo alias mapping foreach (var svc in _services)
// if (guild != null) {
// { string newContent;
// if (Modules.Utility.Utility.CommandMapCommands.AliasMaps.TryGetValue(guild.Id, out ConcurrentDictionary<string, string> maps)) if (svc is IInputTransformer exec &&
// { (newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, messageContent).ConfigureAwait(false)) != messageContent.ToLowerInvariant())
{
// var keys = maps.Keys messageContent = newContent;
// .OrderByDescending(x => x.Length); break;
}
// 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;
// }
// }
// }
// execute the command and measure the time it took // execute the command and measure the time it took
if (messageContent.StartsWith(NadekoBot.Prefix)) 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; execTime = Environment.TickCount - execTime;
////todo commandHandler ////todo commandHandler
@ -281,8 +239,8 @@ namespace NadekoBot.Services
} }
public Task<bool> ExecuteCommandAsync(CommandContext context, int argPos, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) public Task<bool> ExecuteCommandAsync(CommandContext context, string input, int argPos, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
=> ExecuteCommand(context, context.Message.Content.Substring(argPos), serviceProvider, multiMatchHandling); => ExecuteCommand(context, input.Substring(argPos), serviceProvider, multiMatchHandling);
public async Task<bool> ExecuteCommand(CommandContext context, string input, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) public async Task<bool> ExecuteCommand(CommandContext context, string input, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
@ -333,25 +291,6 @@ namespace NadekoBot.Services
var module = cmd.Module.GetTopLevelModule(); var module = cmd.Module.GetTopLevelModule();
if (context.Guild != null) 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 //////future
////int price; ////int price;
////if (Permissions.CommandCostCommands.CommandCosts.TryGetValue(cmd.Aliases.First().Trim().ToLowerInvariant(), out price) && price > 0) ////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 // Bot will ignore commands which are ran more often than what specified by
// GlobalCommandsCooldown constant (miliseconds) // GlobalCommandsCooldown constant (miliseconds)
if (!UsersOnShortCooldown.Add(context.Message.Author.Id)) if (!UsersOnShortCooldown.Add(context.Message.Author.Id))

View File

@ -95,7 +95,7 @@ namespace NadekoBot.Services.Games
// "Games".ToLowerInvariant(), // "Games".ToLowerInvariant(),
// out int index)) // out int index))
//{ //{
// //todo print in guild actually // //todo 46 print in guild actually
// var returnMsg = // var returnMsg =
// $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand(guild)}** is preventing this action."; // $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand(guild)}** is preventing this action.";
// _log.Info(returnMsg); // _log.Info(returnMsg);

View File

@ -41,7 +41,7 @@ namespace NadekoBot.Services.Permissions
return words; return words;
} }
public FilterService(IEnumerable<GuildConfig> gcs) public FilterService(DiscordShardedClient _client, IEnumerable<GuildConfig> gcs)
{ {
_log = LogManager.GetCurrentClassLogger(); _log = LogManager.GetCurrentClassLogger();
@ -56,15 +56,22 @@ namespace NadekoBot.Services.Permissions
WordFilteringServers = new ConcurrentHashSet<ulong>(serverFiltering.Select(gc => gc.GuildId)); WordFilteringServers = new ConcurrentHashSet<ulong>(serverFiltering.Select(gc => gc.GuildId));
WordFilteringChannels = new ConcurrentHashSet<ulong>(gcs.SelectMany(gc => gc.FilterWordsChannelIds.Select(fwci => fwci.ChannelId))); WordFilteringChannels = new ConcurrentHashSet<ulong>(gcs.SelectMany(gc => gc.FilterWordsChannelIds.Select(fwci => fwci.ChannelId)));
}
//todo ignore guild admin
public async Task<bool> TryBlockEarly(DiscordShardedClient client, IGuild guild, IUserMessage msg)
=> await FilterInvites(client, guild, msg) || await FilterWords(client, guild, msg);
public async Task<bool> FilterWords(DiscordShardedClient client, IGuild guild, IUserMessage usrMsg) _client.MessageUpdated += (oldData, newMsg, channel)
=> FilterInvites((channel as ITextChannel)?.Guild, newMsg as IUserMessage);
}
public async Task<bool> 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<bool> FilterWords(IGuild guild, IUserMessage usrMsg)
{ {
if (guild is null) if (guild is null)
return false; return false;
if (usrMsg is null)
return false;
var filteredChannelWords = FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet<string>(); var filteredChannelWords = FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet<string>();
var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet<string>(); var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet<string>();
@ -91,10 +98,12 @@ namespace NadekoBot.Services.Permissions
return false; return false;
} }
public async Task<bool> FilterInvites(DiscordShardedClient client, IGuild guild, IUserMessage usrMsg) public async Task<bool> FilterInvites(IGuild guild, IUserMessage usrMsg)
{ {
if (guild is null) if (guild is null)
return false; return false;
if (usrMsg is null)
return false;
if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) || if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) ||
InviteFilteringServers.Contains(guild.Id)) && InviteFilteringServers.Contains(guild.Id)) &&

View File

@ -1,10 +1,15 @@
using NadekoBot.Services.Database.Models; using NadekoBot.DataStructures.ModuleBehaviors;
using NadekoBot.Services.Database.Models;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Linq; using System.Linq;
using Discord;
using Discord.WebSocket;
using System;
using System.Threading.Tasks;
namespace NadekoBot.Services.Permissions namespace NadekoBot.Services.Permissions
{ {
public class GlobalPermissionService public class GlobalPermissionService : ILateBlocker
{ {
public readonly ConcurrentHashSet<string> BlockedModules; public readonly ConcurrentHashSet<string> BlockedModules;
public readonly ConcurrentHashSet<string> BlockedCommands; public readonly ConcurrentHashSet<string> BlockedCommands;
@ -14,5 +19,20 @@ namespace NadekoBot.Services.Permissions
BlockedModules = new ConcurrentHashSet<string>(bc.BlockedModules.Select(x => x.Name)); BlockedModules = new ConcurrentHashSet<string>(bc.BlockedModules.Select(x => x.Name));
BlockedCommands = new ConcurrentHashSet<string>(bc.BlockedCommands.Select(x => x.Name)); BlockedCommands = new ConcurrentHashSet<string>(bc.BlockedCommands.Select(x => x.Name));
} }
public async Task<bool> 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;
}
} }
} }

View File

@ -169,10 +169,10 @@ namespace NadekoBot.Services.Permissions
return false; return false;
} }
else
{
var resetCommand = commandName == "resetperms"; var resetCommand = commandName == "resetperms";
//todo perms
PermissionCache pc = GetCache(guild.Id); PermissionCache pc = GetCache(guild.Id);
if (!resetCommand && !pc.Permissions.CheckPermissions(msg, commandName, moduleName, out int index)) if (!resetCommand && !pc.Permissions.CheckPermissions(msg, commandName, moduleName, out int index))
{ {
@ -191,6 +191,7 @@ namespace NadekoBot.Services.Permissions
//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, $"You need the **{pc.PermRole}** role in order to use permission commands."));
} }
} }
}
return false; return false;
} }

View File

@ -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<ulong, ConcurrentDictionary<string, string>> AliasMaps { get; } = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>();
//commandmap
public CommandMapService(IEnumerable<GuildConfig> gcs)
{
_log = LogManager.GetCurrentClassLogger();
AliasMaps = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>(
gcs.ToDictionary(
x => x.GuildId,
x => new ConcurrentDictionary<string, string>(x.CommandAliases
.Distinct(new CommandAliasEqualityComparer())
.ToDictionary(ca => ca.Trigger, ca => ca.Mapping))));
}
public async Task<string> 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<string, string> 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<CommandAlias>
{
public bool Equals(CommandAlias x, CommandAlias y) => x.Trigger == y.Trigger;
public int GetHashCode(CommandAlias obj) => obj.Trigger.GetHashCode();
}
}

View File

@ -9,21 +9,14 @@ using System.Threading.Tasks;
namespace NadekoBot.Services.Utility namespace NadekoBot.Services.Utility
{ {
public class UtilityService public class CrossServerTextService
{ {
public ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>> AliasMaps { get; } = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>(); public readonly ConcurrentDictionary<int, ConcurrentHashSet<ITextChannel>> Subscribers =
new ConcurrentDictionary<int, ConcurrentHashSet<ITextChannel>>();
private DiscordShardedClient _client;
public UtilityService(IEnumerable<GuildConfig> guildConfigs, DiscordShardedClient client) public CrossServerTextService(IEnumerable<GuildConfig> guildConfigs, DiscordShardedClient client)
{ {
//commandmap
AliasMaps = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>(
guildConfigs.ToDictionary(
x => x.GuildId,
x => new ConcurrentDictionary<string, string>(x.CommandAliases
.Distinct(new CommandAliasEqualityComparer())
.ToDictionary(ca => ca.Trigger, ca => ca.Mapping))));
//cross server
_client = client; _client = client;
_client.MessageReceived += Client_MessageReceived; _client.MessageReceived += Client_MessageReceived;
} }
@ -68,16 +61,5 @@ namespace NadekoBot.Services.Utility
private string GetMessage(ITextChannel channel, IGuildUser user, IUserMessage message) => private string GetMessage(ITextChannel channel, IGuildUser user, IUserMessage message) =>
$"**{channel.Guild.Name} | {channel.Name}** `{user.Username}`: " + message.Content.SanitizeMentions(); $"**{channel.Guild.Name} | {channel.Name}** `{user.Username}`: " + message.Content.SanitizeMentions();
public readonly ConcurrentDictionary<int, ConcurrentHashSet<ITextChannel>> Subscribers =
new ConcurrentDictionary<int, ConcurrentHashSet<ITextChannel>>();
private DiscordShardedClient _client;
}
public class CommandAliasEqualityComparer : IEqualityComparer<CommandAlias>
{
public bool Equals(CommandAlias x, CommandAlias y) => x.Trigger == y.Trigger;
public int GetHashCode(CommandAlias obj) => obj.Trigger.GetHashCode();
} }
} }