From a4973ffbb3873ec8e84add7a8600e25050bc96cc Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 27 May 2017 19:42:23 +0200 Subject: [PATCH] Administration almost done, logcommands left --- .../Commands/AutoAssignRoleCommands.cs | 38 +-- .../Commands/GameChannelCommands.cs | 83 +----- .../Commands/LocalizationCommands.cs | 18 +- .../Administration/Commands/LogCommand.cs | 14 +- .../Administration/Commands/Migration.cs | 12 +- .../Commands/PlayingRotateCommands.cs | 107 ++----- .../Commands/ProtectionCommands.cs | 265 ++---------------- .../Commands/RatelimitCommand.cs | 97 +------ .../Commands/SelfAssignedRolesCommand.cs | 22 +- .../Commands/UserPunishCommands.cs | 1 - .../{Permissions => }/Pokemon/Pokemon.cs | 81 +++--- src/NadekoBot/NadekoBot.cs | 27 +- src/NadekoBot/NadekoBot.csproj | 12 +- .../Administration/AutoAssignRoleService.cs | 46 +++ .../Administration/GameVoiceChannelService.cs | 73 +++++ .../Administration/PlayingRotateService.cs | 104 +++++++ .../Administration/ProtectionService.cs | 182 ++++++++++++ .../Administration/ProtectionStats.cs | 26 ++ .../Administration/RatelimitService.cs | 53 ++++ .../Services/Administration/Ratelimiter.cs | 59 ++++ .../Services/Administration/SelfService.cs | 3 +- .../Services/Administration/UserSpamStats.cs | 53 ++++ .../Pokemon/PokeStats.cs | 4 +- .../Services/Pokemon/PokemonService.cs | 32 +++ .../Pokemon/PokemonType.cs | 2 +- src/NadekoBot/_Extensions/Extensions.cs | 3 + 26 files changed, 809 insertions(+), 608 deletions(-) rename src/NadekoBot/Modules/{Permissions => }/Pokemon/Pokemon.cs (79%) create mode 100644 src/NadekoBot/Services/Administration/AutoAssignRoleService.cs create mode 100644 src/NadekoBot/Services/Administration/GameVoiceChannelService.cs create mode 100644 src/NadekoBot/Services/Administration/PlayingRotateService.cs create mode 100644 src/NadekoBot/Services/Administration/ProtectionService.cs create mode 100644 src/NadekoBot/Services/Administration/ProtectionStats.cs create mode 100644 src/NadekoBot/Services/Administration/RatelimitService.cs create mode 100644 src/NadekoBot/Services/Administration/Ratelimiter.cs create mode 100644 src/NadekoBot/Services/Administration/UserSpamStats.cs rename src/NadekoBot/{Modules/Permissions => Services}/Pokemon/PokeStats.cs (85%) create mode 100644 src/NadekoBot/Services/Pokemon/PokemonService.cs rename src/NadekoBot/{Modules/Permissions => Services}/Pokemon/PokemonType.cs (95%) diff --git a/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs b/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs index 1b2cde7d..2213b676 100644 --- a/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs @@ -3,9 +3,7 @@ using Discord.Commands; using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; -using NLog; -using System; -using System.Collections.Concurrent; +using NadekoBot.Services.Administration; using System.Linq; using System.Threading.Tasks; @@ -16,31 +14,13 @@ namespace NadekoBot.Modules.Administration [Group] public class AutoAssignRoleCommands : NadekoSubmodule { - //guildid/roleid - private static ConcurrentDictionary AutoAssignedRoles { get; } + private readonly DbHandler _db; + private readonly AutoAssignRoleService _service; - static AutoAssignRoleCommands() + public AutoAssignRoleCommands(AutoAssignRoleService service, DbHandler db) { - var log = LogManager.GetCurrentClassLogger(); - - AutoAssignedRoles = new ConcurrentDictionary(NadekoBot.AllGuildConfigs.Where(x => x.AutoAssignRoleId != 0) - .ToDictionary(k => k.GuildId, v => v.AutoAssignRoleId)); - NadekoBot.Client.UserJoined += async (user) => - { - try - { - AutoAssignedRoles.TryGetValue(user.Guild.Id, out ulong roleId); - - if (roleId == 0) - return; - - var role = user.Guild.Roles.FirstOrDefault(r => r.Id == roleId); - - if (role != null) - await user.AddRoleAsync(role).ConfigureAwait(false); - } - catch (Exception ex) { log.Warn(ex); } - }; + _db = db; + _service = service; } [NadekoCommand, Usage, Description, Aliases] @@ -53,18 +33,18 @@ namespace NadekoBot.Modules.Administration if (Context.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) return; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var conf = uow.GuildConfigs.For(Context.Guild.Id, set => set); if (role == null) { conf.AutoAssignRoleId = 0; - AutoAssignedRoles.TryRemove(Context.Guild.Id, out ulong throwaway); + _service.AutoAssignedRoles.TryRemove(Context.Guild.Id, out ulong throwaway); } else { conf.AutoAssignRoleId = role.Id; - AutoAssignedRoles.AddOrUpdate(Context.Guild.Id, role.Id, (key, val) => role.Id); + _service.AutoAssignedRoles.AddOrUpdate(Context.Guild.Id, role.Id, (key, val) => role.Id); } await uow.CompleteAsync().ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs b/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs index 0e557b40..85489724 100644 --- a/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs @@ -1,19 +1,9 @@ using Discord; using Discord.Commands; -using Microsoft.EntityFrameworkCore; using NadekoBot.Attributes; using NadekoBot.Services; -using NadekoBot.Services.Database; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; -using Discord.WebSocket; -using NLog; -using NadekoBot.Extensions; +using NadekoBot.Services.Administration; namespace NadekoBot.Modules.Administration { @@ -22,64 +12,13 @@ namespace NadekoBot.Modules.Administration [Group] public class GameChannelCommands : NadekoSubmodule { - //private static readonly Timer _t; + private readonly DbHandler _db; + private readonly GameVoiceChannelService _service; - private static readonly ConcurrentHashSet gameVoiceChannels = new ConcurrentHashSet(); - - private static new readonly Logger _log; - - static GameChannelCommands() + public GameChannelCommands(GameVoiceChannelService service, DbHandler db) { - //_t = new Timer(_ => { - - //}, null, ); - - _log = LogManager.GetCurrentClassLogger(); - - gameVoiceChannels = new ConcurrentHashSet( - NadekoBot.AllGuildConfigs.Where(gc => gc.GameVoiceChannel != null) - .Select(gc => gc.GameVoiceChannel.Value)); - - NadekoBot.Client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; - - } - - private static Task Client_UserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState) - { - var _ = Task.Run(async () => - { - try - { - var gUser = usr as SocketGuildUser; - if (gUser == null) - return; - - var game = gUser.Game?.Name.TrimTo(50).ToLowerInvariant(); - - if (oldState.VoiceChannel == newState.VoiceChannel || - newState.VoiceChannel == null) - return; - - if (!gameVoiceChannels.Contains(newState.VoiceChannel.Id) || - string.IsNullOrWhiteSpace(game)) - return; - - var vch = gUser.Guild.VoiceChannels - .FirstOrDefault(x => x.Name.ToLowerInvariant() == game); - - if (vch == null) - return; - - await Task.Delay(1000).ConfigureAwait(false); - await gUser.ModifyAsync(gu => gu.Channel = vch).ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn(ex); - } - }); - - return Task.CompletedTask; + _db = db; + _service = service; } [NadekoCommand, Usage, Description, Aliases] @@ -96,20 +35,20 @@ namespace NadekoBot.Modules.Administration return; } ulong? id; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set); if (gc.GameVoiceChannel == vch.Id) { - gameVoiceChannels.TryRemove(vch.Id); + _service.GameVoiceChannels.TryRemove(vch.Id); id = gc.GameVoiceChannel = null; } else { if(gc.GameVoiceChannel != null) - gameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value); - gameVoiceChannels.Add(vch.Id); + _service.GameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value); + _service.GameVoiceChannels.Add(vch.Id); id = gc.GameVoiceChannel = vch.Id; } @@ -122,7 +61,7 @@ namespace NadekoBot.Modules.Administration } else { - gameVoiceChannels.Add(vch.Id); + _service.GameVoiceChannels.Add(vch.Id); await ReplyConfirmLocalized("gvc_enabled", Format.Bold(vch.Name)).ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Administration/Commands/LocalizationCommands.cs b/src/NadekoBot/Modules/Administration/Commands/LocalizationCommands.cs index c3de5bd8..9e5320c0 100644 --- a/src/NadekoBot/Modules/Administration/Commands/LocalizationCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/LocalizationCommands.cs @@ -18,7 +18,7 @@ namespace NadekoBot.Modules.Administration { //Română, România //Bahasa Indonesia, Indonesia - private ImmutableDictionary supportedLocales { get; } = new Dictionary() + private static ImmutableDictionary supportedLocales { get; } = new Dictionary() { //{"ar", "العربية" }, {"zh-TW", "繁體中文, 台灣" }, @@ -46,7 +46,7 @@ namespace NadekoBot.Modules.Administration [RequireContext(ContextType.Guild)] public async Task LanguageSet() { - var cul = NadekoBot.Localization.GetCultureInfo(Context.Guild); + var cul = _localization.GetCultureInfo(Context.Guild); await ReplyConfirmLocalized("lang_set_show", Format.Bold(cul.ToString()), Format.Bold(cul.NativeName)) .ConfigureAwait(false); } @@ -61,13 +61,13 @@ namespace NadekoBot.Modules.Administration CultureInfo ci; if (name.Trim().ToLowerInvariant() == "default") { - NadekoBot.Localization.RemoveGuildCulture(Context.Guild); - ci = NadekoBot.Localization.DefaultCultureInfo; + _localization.RemoveGuildCulture(Context.Guild); + ci = _localization.DefaultCultureInfo; } else { ci = new CultureInfo(name); - NadekoBot.Localization.SetGuildCulture(Context.Guild, ci); + _localization.SetGuildCulture(Context.Guild, ci); } await ReplyConfirmLocalized("lang_set", Format.Bold(ci.ToString()), Format.Bold(ci.NativeName)).ConfigureAwait(false); @@ -81,7 +81,7 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] public async Task LanguageSetDefault() { - var cul = NadekoBot.Localization.DefaultCultureInfo; + var cul = _localization.DefaultCultureInfo; await ReplyConfirmLocalized("lang_set_bot_show", cul, cul.NativeName).ConfigureAwait(false); } @@ -94,13 +94,13 @@ namespace NadekoBot.Modules.Administration CultureInfo ci; if (name.Trim().ToLowerInvariant() == "default") { - NadekoBot.Localization.ResetDefaultCulture(); - ci = NadekoBot.Localization.DefaultCultureInfo; + _localization.ResetDefaultCulture(); + ci = _localization.DefaultCultureInfo; } else { ci = new CultureInfo(name); - NadekoBot.Localization.SetDefaultCulture(ci); + _localization.SetDefaultCulture(ci); } await ReplyConfirmLocalized("lang_set_bot", Format.Bold(ci.ToString()), Format.Bold(ci.NativeName)).ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs index 76dd5e5f..09ee1314 100644 --- a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs @@ -35,11 +35,11 @@ namespace NadekoBot.Modules.Administration static LogCommands() { - Client = NadekoBot.Client; + Client = _client; _log = LogManager.GetCurrentClassLogger(); var sw = Stopwatch.StartNew(); - GuildLogSettings = new ConcurrentDictionary(NadekoBot.AllGuildConfigs + GuildLogSettings = new ConcurrentDictionary(gcs .ToDictionary(g => g.GuildId, g => g.LogSetting)); _timerReference = new Timer(async (state) => @@ -136,7 +136,7 @@ namespace NadekoBot.Modules.Administration await logChannel.EmbedAsync(embed).ConfigureAwait(false); - //var guildsMemberOf = NadekoBot.Client.GetGuilds().Where(g => g.Users.Select(u => u.Id).Contains(before.Id)).ToList(); + //var guildsMemberOf = _client.GetGuilds().Where(g => g.Users.Select(u => u.Id).Contains(before.Id)).ToList(); //foreach (var g in guildsMemberOf) //{ // LogSetting logSetting; @@ -840,7 +840,7 @@ namespace NadekoBot.Modules.Administration private static void UnsetLogSetting(ulong guildId, LogType logChannelType) { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var newLogSetting = uow.GuildConfigs.LogSettingsFor(guildId).LogSetting; switch (logChannelType) @@ -910,7 +910,7 @@ namespace NadekoBot.Modules.Administration { var channel = (ITextChannel)Context.Channel; LogSetting logSetting; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { logSetting = uow.GuildConfigs.LogSettingsFor(channel.Guild.Id).LogSetting; GuildLogSettings.AddOrUpdate(channel.Guild.Id, (id) => logSetting, (id, old) => logSetting); @@ -946,7 +946,7 @@ namespace NadekoBot.Modules.Administration { var channel = (ITextChannel)Context.Channel; int removed; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.GuildConfigs.LogSettingsFor(channel.Guild.Id); LogSetting logSetting = GuildLogSettings.GetOrAdd(channel.Guild.Id, (id) => config.LogSetting); @@ -986,7 +986,7 @@ namespace NadekoBot.Modules.Administration { var channel = (ITextChannel)Context.Channel; ulong? channelId = null; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var logSetting = uow.GuildConfigs.LogSettingsFor(channel.Guild.Id).LogSetting; GuildLogSettings.AddOrUpdate(channel.Guild.Id, (id) => logSetting, (id, old) => logSetting); diff --git a/src/NadekoBot/Modules/Administration/Commands/Migration.cs b/src/NadekoBot/Modules/Administration/Commands/Migration.cs index 802e5ae9..93dfcadf 100644 --- a/src/NadekoBot/Modules/Administration/Commands/Migration.cs +++ b/src/NadekoBot/Modules/Administration/Commands/Migration.cs @@ -8,7 +8,6 @@ using NadekoBot.Attributes; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using Newtonsoft.Json; -using NLog; using NadekoBot.Modules.Administration.Commands.Migration; using System.Collections.Concurrent; using NadekoBot.Extensions; @@ -23,12 +22,11 @@ namespace NadekoBot.Modules.Administration public class Migration : NadekoSubmodule { private const int CURRENT_VERSION = 1; + private readonly DbHandler _db; - private new static readonly Logger _log; - - static Migration() + public Migration(DbHandler db) { - _log = LogManager.GetCurrentClassLogger(); + _db = db; } [NadekoCommand, Usage, Description, Aliases] @@ -36,7 +34,7 @@ namespace NadekoBot.Modules.Administration public async Task MigrateData() { var version = 0; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { version = uow.BotConfig.GetOrCreate().MigrationVersion; } @@ -62,7 +60,7 @@ namespace NadekoBot.Modules.Administration private void Migrate0_9To1_0() { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var botConfig = uow.BotConfig.GetOrCreate(); MigrateConfig0_9(uow, botConfig); diff --git a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs index 0858d53e..1554b22d 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs @@ -1,14 +1,9 @@ using Discord.Commands; -using Discord.WebSocket; using NadekoBot.Attributes; -using NadekoBot.Extensions; using NadekoBot.Services; +using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; -using NLog; -using System; -using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace NadekoBot.Modules.Administration @@ -18,101 +13,31 @@ namespace NadekoBot.Modules.Administration [Group] public class PlayingRotateCommands : NadekoSubmodule { - public static List RotatingStatusMessages { get; } - public static volatile bool RotatingStatuses; - private readonly object _locker = new object(); - private new static Logger _log { get; } - private static readonly Timer _t; + private static readonly object _locker = new object(); + private readonly DbHandler _db; + private readonly PlayingRotateService _service; - private class TimerState + public PlayingRotateCommands(PlayingRotateService service, DbHandler db) { - public int Index { get; set; } + _db = db; + _service = service; } - static PlayingRotateCommands() - { - _log = LogManager.GetCurrentClassLogger(); - - RotatingStatusMessages = NadekoBot.BotConfig.RotatingStatusMessages; - RotatingStatuses = NadekoBot.BotConfig.RotatingStatuses; - - _t = new Timer(async (objState) => - { - try - { - var state = (TimerState)objState; - if (!RotatingStatuses) - return; - if (state.Index >= RotatingStatusMessages.Count) - state.Index = 0; - - if (!RotatingStatusMessages.Any()) - return; - var status = RotatingStatusMessages[state.Index++].Status; - if (string.IsNullOrWhiteSpace(status)) - return; - PlayingPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value())); - var shards = NadekoBot.Client.Shards; - for (int i = 0; i < shards.Count; i++) - { - var curShard = shards.ElementAt(i); - ShardSpecificPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(curShard))); - try { await shards.ElementAt(i).SetGameAsync(status).ConfigureAwait(false); } - catch (Exception ex) - { - _log.Warn(ex); - } - } - } - catch (Exception ex) - { - _log.Warn("Rotating playing status errored.\n" + ex); - } - }, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); - } - - public static Dictionary> PlayingPlaceholders { get; } = - new Dictionary> { - { "%servers%", () => NadekoBot.Client.Guilds.Count.ToString()}, - { "%users%", () => NadekoBot.Client.Guilds.Sum(s => s.Users.Count).ToString()}, - { "%playing%", () => { - var cnt = NadekoBot.MusicService.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); - if (cnt != 1) return cnt.ToString(); - try { - var mp = NadekoBot.MusicService.MusicPlayers.FirstOrDefault(); - return mp.Value.CurrentSong.SongInfo.Title; - } - catch { - return "No songs"; - } - } - }, - { "%queued%", () => NadekoBot.MusicService.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()}, - { "%time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()) }, - { "%shardcount%", () => NadekoBot.Client.Shards.Count.ToString() }, - }; - - public static Dictionary> ShardSpecificPlaceholders { get; } = - new Dictionary> { - { "%shardid%", (client) => client.ShardId.ToString()}, - { "%shardguilds%", (client) => client.Guilds.Count.ToString()}, - }; - [NadekoCommand, Usage, Description, Aliases] [OwnerOnly] public async Task RotatePlaying() { lock (_locker) { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.BotConfig.GetOrCreate(); - RotatingStatuses = config.RotatingStatuses = !config.RotatingStatuses; + _service.RotatingStatuses = config.RotatingStatuses = !config.RotatingStatuses; uow.Complete(); } } - if (RotatingStatuses) + if (_service.RotatingStatuses) await ReplyConfirmLocalized("ropl_enabled").ConfigureAwait(false); else await ReplyConfirmLocalized("ropl_disabled").ConfigureAwait(false); @@ -122,12 +47,12 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task AddPlaying([Remainder] string status) { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.BotConfig.GetOrCreate(); var toAdd = new PlayingStatus { Status = status }; config.RotatingStatusMessages.Add(toAdd); - RotatingStatusMessages.Add(toAdd); + _service.RotatingStatusMessages.Add(toAdd); await uow.CompleteAsync(); } @@ -138,13 +63,13 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task ListPlaying() { - if (!RotatingStatusMessages.Any()) + if (!_service.RotatingStatusMessages.Any()) await ReplyErrorLocalized("ropl_not_set").ConfigureAwait(false); else { var i = 1; await ReplyConfirmLocalized("ropl_list", - string.Join("\n\t", RotatingStatusMessages.Select(rs => $"`{i++}.` {rs.Status}"))) + string.Join("\n\t", _service.RotatingStatusMessages.Select(rs => $"`{i++}.` {rs.Status}"))) .ConfigureAwait(false); } @@ -157,7 +82,7 @@ namespace NadekoBot.Modules.Administration index -= 1; string msg; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.BotConfig.GetOrCreate(); @@ -165,7 +90,7 @@ namespace NadekoBot.Modules.Administration return; msg = config.RotatingStatusMessages[index].Status; config.RotatingStatusMessages.RemoveAt(index); - RotatingStatusMessages.RemoveAt(index); + _service.RotatingStatusMessages.RemoveAt(index); await uow.CompleteAsync(); } await ReplyConfirmLocalized("reprm", msg).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs index e89f14ba..1fc6682b 100644 --- a/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs @@ -5,246 +5,27 @@ using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using NLog; using System; -using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; -using System.Threading; -using System.Collections.Generic; +using NadekoBot.Services.Administration; namespace NadekoBot.Modules.Administration { public partial class Administration { - public enum ProtectionType - { - Raiding, - Spamming, - } - - public class AntiRaidStats - { - public AntiRaidSetting AntiRaidSettings { get; set; } - public int UsersCount { get; set; } - public ConcurrentHashSet RaidUsers { get; set; } = new ConcurrentHashSet(); - } - - public class AntiSpamStats - { - public AntiSpamSetting AntiSpamSettings { get; set; } - public ConcurrentDictionary UserStats { get; set; } - = new ConcurrentDictionary(); - } - - public class UserSpamStats : IDisposable - { - public int Count => timers.Count; - public string LastMessage { get; set; } - - private ConcurrentQueue timers { get; } - - public UserSpamStats(IUserMessage msg) - { - LastMessage = msg.Content.ToUpperInvariant(); - timers = new ConcurrentQueue(); - - ApplyNextMessage(msg); - } - - private readonly object applyLock = new object(); - public void ApplyNextMessage(IUserMessage message) - { - lock(applyLock){ - var upperMsg = message.Content.ToUpperInvariant(); - if (upperMsg != LastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any())) - { - LastMessage = upperMsg; - //todo c#7 - Timer old; - while(timers.TryDequeue(out old)) - old.Change(Timeout.Infinite, Timeout.Infinite); - } - var t = new Timer((_) => { - //todo c#7 - Timer __; - if(timers.TryDequeue(out __)) - __.Change(Timeout.Infinite, Timeout.Infinite); - }, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30)); - timers.Enqueue(t); - } - } - - public void Dispose() - { - //todo c#7 - Timer old; - while(timers.TryDequeue(out old)) - old.Change(Timeout.Infinite, Timeout.Infinite); - } - } - [Group] public class ProtectionCommands : NadekoSubmodule { - private static readonly ConcurrentDictionary _antiRaidGuilds = - new ConcurrentDictionary(); - // guildId | (userId|messages) - private static readonly ConcurrentDictionary _antiSpamGuilds = - new ConcurrentDictionary(); + private readonly ProtectionService _service; + private readonly MuteService _mute; + private readonly DbHandler _db; - private new static readonly Logger _log; - - static ProtectionCommands() + public ProtectionCommands(ProtectionService service, MuteService mute, DbHandler db) { - _log = LogManager.GetCurrentClassLogger(); - - foreach (var gc in NadekoBot.AllGuildConfigs) - { - var raid = gc.AntiRaidSetting; - var spam = gc.AntiSpamSetting; - - if (raid != null) - { - var raidStats = new AntiRaidStats() { AntiRaidSettings = raid }; - _antiRaidGuilds.TryAdd(gc.GuildId, raidStats); - } - - if (spam != null) - _antiSpamGuilds.TryAdd(gc.GuildId, new AntiSpamStats() { AntiSpamSettings = spam }); - } - - NadekoBot.Client.MessageReceived += (imsg) => - { - var msg = imsg as IUserMessage; - if (msg == null || msg.Author.IsBot) - return Task.CompletedTask; - - var channel = msg.Channel as ITextChannel; - if (channel == null) - return Task.CompletedTask; - var _ = Task.Run(async () => - { - try - { - AntiSpamStats spamSettings; - if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out spamSettings) || - spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new AntiSpamIgnore() - { - ChannelId = channel.Id - })) - return; - - var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, (id) => new UserSpamStats(msg), - (id, old) => - { - old.ApplyNextMessage(msg); return old; - }); - - if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold) - { - if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) - { - stats.Dispose(); - await PunishUsers(spamSettings.AntiSpamSettings.Action, ProtectionType.Spamming, (IGuildUser)msg.Author) - .ConfigureAwait(false); - } - } - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - }; - - NadekoBot.Client.UserJoined += (usr) => - { - if (usr.IsBot) - return Task.CompletedTask; - AntiRaidStats settings; - if (!_antiRaidGuilds.TryGetValue(usr.Guild.Id, out settings)) - return Task.CompletedTask; - if (!settings.RaidUsers.Add(usr)) - return Task.CompletedTask; - - var _ = Task.Run(async () => - { - try - { - ++settings.UsersCount; - - if (settings.UsersCount >= settings.AntiRaidSettings.UserThreshold) - { - var users = settings.RaidUsers.ToArray(); - settings.RaidUsers.Clear(); - - await PunishUsers(settings.AntiRaidSettings.Action, ProtectionType.Raiding, users).ConfigureAwait(false); - } - await Task.Delay(1000 * settings.AntiRaidSettings.Seconds).ConfigureAwait(false); - - settings.RaidUsers.TryRemove(usr); - --settings.UsersCount; - - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - }; - } - - private static async Task PunishUsers(PunishmentAction action, ProtectionType pt, params IGuildUser[] gus) - { - _log.Info($"[{pt}] - Punishing [{gus.Length}] users with [{action}] in {gus[0].Guild.Name} guild"); - foreach (var gu in gus) - { - switch (action) - { - case PunishmentAction.Mute: - try - { - await MuteCommands.MuteUser(gu).ConfigureAwait(false); - } - catch (Exception ex) { _log.Warn(ex, "I can't apply punishement"); } - break; - case PunishmentAction.Kick: - try - { - await gu.KickAsync().ConfigureAwait(false); - } - catch (Exception ex) { _log.Warn(ex, "I can't apply punishement"); } - break; - case PunishmentAction.Softban: - try - { - await gu.Guild.AddBanAsync(gu, 7).ConfigureAwait(false); - try - { - await gu.Guild.RemoveBanAsync(gu).ConfigureAwait(false); - } - catch - { - await gu.Guild.RemoveBanAsync(gu).ConfigureAwait(false); - // try it twice, really don't want to ban user if - // only kick has been specified as the punishement - } - } - catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } - break; - case PunishmentAction.Ban: - try - { - await gu.Guild.AddBanAsync(gu, 7).ConfigureAwait(false); - } - catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } - break; - } - } - await LogCommands.TriggeredAntiProtection(gus, action, pt).ConfigureAwait(false); + _service = service; + _mute = mute; + _db = db; } private string GetAntiSpamString(AntiSpamStats stats) @@ -282,9 +63,9 @@ namespace NadekoBot.Modules.Administration } AntiRaidStats throwaway; - if (_antiRaidGuilds.TryRemove(Context.Guild.Id, out throwaway)) + if (_service.AntiRaidGuilds.TryRemove(Context.Guild.Id, out throwaway)) { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiRaidSetting)); @@ -297,7 +78,7 @@ namespace NadekoBot.Modules.Administration try { - await MuteCommands.GetMuteRole(Context.Guild).ConfigureAwait(false); + await _mute.GetMuteRole(Context.Guild).ConfigureAwait(false); } catch (Exception ex) { @@ -316,9 +97,9 @@ namespace NadekoBot.Modules.Administration } }; - _antiRaidGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => stats); + _service.AntiRaidGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => stats); - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiRaidSetting)); @@ -339,10 +120,10 @@ namespace NadekoBot.Modules.Administration return; AntiSpamStats throwaway; - if (_antiSpamGuilds.TryRemove(Context.Guild.Id, out throwaway)) + if (_service.AntiSpamGuilds.TryRemove(Context.Guild.Id, out throwaway)) { throwaway.UserStats.ForEach(x => x.Value.Dispose()); - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiSpamSetting) .ThenInclude(x => x.IgnoredChannels)); @@ -356,7 +137,7 @@ namespace NadekoBot.Modules.Administration try { - await MuteCommands.GetMuteRole(Context.Guild).ConfigureAwait(false); + await _mute.GetMuteRole(Context.Guild).ConfigureAwait(false); } catch (Exception ex) { @@ -374,9 +155,9 @@ namespace NadekoBot.Modules.Administration } }; - _antiSpamGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => stats); + _service.AntiSpamGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => stats); - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiSpamSetting)); @@ -398,7 +179,7 @@ namespace NadekoBot.Modules.Administration ChannelId = channel.Id }; bool added; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels)); var spam = gc.AntiSpamSetting; @@ -410,7 +191,7 @@ namespace NadekoBot.Modules.Administration if (spam.IgnoredChannels.Add(obj)) { AntiSpamStats temp; - if (_antiSpamGuilds.TryGetValue(Context.Guild.Id, out temp)) + if (_service.AntiSpamGuilds.TryGetValue(Context.Guild.Id, out temp)) temp.AntiSpamSettings.IgnoredChannels.Add(obj); added = true; } @@ -418,7 +199,7 @@ namespace NadekoBot.Modules.Administration { spam.IgnoredChannels.Remove(obj); AntiSpamStats temp; - if (_antiSpamGuilds.TryGetValue(Context.Guild.Id, out temp)) + if (_service.AntiSpamGuilds.TryGetValue(Context.Guild.Id, out temp)) temp.AntiSpamSettings.IgnoredChannels.Remove(obj); added = false; } @@ -437,10 +218,10 @@ namespace NadekoBot.Modules.Administration public async Task AntiList() { AntiSpamStats spam; - _antiSpamGuilds.TryGetValue(Context.Guild.Id, out spam); + _service.AntiSpamGuilds.TryGetValue(Context.Guild.Id, out spam); AntiRaidStats raid; - _antiRaidGuilds.TryGetValue(Context.Guild.Id, out raid); + _service.AntiRaidGuilds.TryGetValue(Context.Guild.Id, out raid); if (spam == null && raid == null) { diff --git a/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs b/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs index 5422fd66..7c491fe7 100644 --- a/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; +using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; using NLog; using System; @@ -21,85 +22,13 @@ namespace NadekoBot.Modules.Administration [Group] public class RatelimitCommands : NadekoSubmodule { - public static ConcurrentDictionary RatelimitingChannels = new ConcurrentDictionary(); - public static ConcurrentDictionary> IgnoredRoles = new ConcurrentDictionary>(); - public static ConcurrentDictionary> IgnoredUsers = new ConcurrentDictionary>(); + private readonly RatelimitService _service; + private readonly DbHandler _db; - private new static readonly Logger _log; - - public class Ratelimiter + public RatelimitCommands(RatelimitService service, DbHandler db) { - public class RatelimitedUser - { - public ulong UserId { get; set; } - public int MessageCount { get; set; } = 0; - } - - public ulong ChannelId { get; set; } - - public int MaxMessages { get; set; } - public int PerSeconds { get; set; } - - public CancellationTokenSource CancelSource { get; set; } = new CancellationTokenSource(); - - public ConcurrentDictionary Users { get; set; } = new ConcurrentDictionary(); - - public bool CheckUserRatelimit(ulong id, ulong guildId, SocketGuildUser optUser) - { - if ((IgnoredUsers.TryGetValue(guildId, out HashSet ignoreUsers) && ignoreUsers.Contains(id)) || - (optUser != null && IgnoredRoles.TryGetValue(guildId, out HashSet ignoreRoles) && optUser.Roles.Any(x => ignoreRoles.Contains(x.Id)))) - return false; - - var usr = Users.GetOrAdd(id, (key) => new RatelimitedUser() { UserId = id }); - if (usr.MessageCount >= MaxMessages) - { - return true; - } - usr.MessageCount++; - var _ = Task.Run(async () => - { - try - { - await Task.Delay(PerSeconds * 1000, CancelSource.Token); - } - catch (OperationCanceledException) { } - usr.MessageCount--; - }); - return false; - } - } - - static RatelimitCommands() - { - _log = LogManager.GetCurrentClassLogger(); - - IgnoredRoles = new ConcurrentDictionary>( - NadekoBot.AllGuildConfigs - .ToDictionary(x => x.GuildId, - x => new HashSet(x.SlowmodeIgnoredRoles.Select(y => y.RoleId)))); - - IgnoredUsers = new ConcurrentDictionary>( - NadekoBot.AllGuildConfigs - .ToDictionary(x => x.GuildId, - x => new HashSet(x.SlowmodeIgnoredUsers.Select(y => y.UserId)))); - - NadekoBot.Client.MessageReceived += async (umsg) => - { - try - { - var usrMsg = umsg as SocketUserMessage; - var channel = usrMsg?.Channel as SocketTextChannel; - - if (channel == null || usrMsg.IsAuthor()) - return; - if (!RatelimitingChannels.TryGetValue(channel.Id, out Ratelimiter limiter)) - return; - - if (limiter.CheckUserRatelimit(usrMsg.Author.Id, channel.Guild.Id, usrMsg.Author as SocketGuildUser)) - await usrMsg.DeleteAsync(); - } - catch (Exception ex) { _log.Warn(ex); } - }; + _service = service; + _db = db; } [NadekoCommand, Usage, Description, Aliases] @@ -107,7 +36,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageMessages)] public async Task Slowmode() { - if (RatelimitingChannels.TryRemove(Context.Channel.Id, out Ratelimiter throwaway)) + if (_service.RatelimitingChannels.TryRemove(Context.Channel.Id, out Ratelimiter throwaway)) { throwaway.CancelSource.Cancel(); await ReplyConfirmLocalized("slowmode_disabled").ConfigureAwait(false); @@ -126,13 +55,13 @@ namespace NadekoBot.Modules.Administration await ReplyErrorLocalized("invalid_params").ConfigureAwait(false); return; } - var toAdd = new Ratelimiter() + var toAdd = new Ratelimiter(_service) { ChannelId = Context.Channel.Id, MaxMessages = msg, PerSeconds = perSec, }; - if(RatelimitingChannels.TryAdd(Context.Channel.Id, toAdd)) + if(_service.RatelimitingChannels.TryAdd(Context.Channel.Id, toAdd)) { await Context.Channel.SendConfirmAsync(GetText("slowmode_init"), GetText("slowmode_desc", Format.Bold(toAdd.MaxMessages.ToString()), Format.Bold(toAdd.PerSeconds.ToString()))) @@ -153,7 +82,7 @@ namespace NadekoBot.Modules.Administration HashSet usrs; bool removed; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { usrs = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.SlowmodeIgnoredUsers)) .SlowmodeIgnoredUsers; @@ -164,7 +93,7 @@ namespace NadekoBot.Modules.Administration await uow.CompleteAsync().ConfigureAwait(false); } - IgnoredUsers.AddOrUpdate(Context.Guild.Id, new HashSet(usrs.Select(x => x.UserId)), (key, old) => new HashSet(usrs.Select(x => x.UserId))); + _service.IgnoredUsers.AddOrUpdate(Context.Guild.Id, new HashSet(usrs.Select(x => x.UserId)), (key, old) => new HashSet(usrs.Select(x => x.UserId))); if(removed) await ReplyConfirmLocalized("slowmodewl_user_stop", Format.Bold(user.ToString())).ConfigureAwait(false); @@ -185,7 +114,7 @@ namespace NadekoBot.Modules.Administration HashSet roles; bool removed; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { roles = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.SlowmodeIgnoredRoles)) .SlowmodeIgnoredRoles; @@ -196,7 +125,7 @@ namespace NadekoBot.Modules.Administration await uow.CompleteAsync().ConfigureAwait(false); } - IgnoredRoles.AddOrUpdate(Context.Guild.Id, new HashSet(roles.Select(x => x.RoleId)), (key, old) => new HashSet(roles.Select(x => x.RoleId))); + _service.IgnoredRoles.AddOrUpdate(Context.Guild.Id, new HashSet(roles.Select(x => x.RoleId)), (key, old) => new HashSet(roles.Select(x => x.RoleId))); if (removed) await ReplyConfirmLocalized("slowmodewl_role_stop", Format.Bold(role.ToString())).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs b/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs index 71b90906..95eb3a96 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs @@ -18,14 +18,20 @@ namespace NadekoBot.Modules.Administration [Group] public class SelfAssignedRolesCommands : NadekoSubmodule { - + private readonly DbHandler _db; + + public SelfAssignedRolesCommands(DbHandler db) + { + _db = db; + } + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] public async Task AdSarm() { bool newval; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.GuildConfigs.For(Context.Guild.Id, set => set); newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages; @@ -49,7 +55,7 @@ namespace NadekoBot.Modules.Administration string msg; var error = false; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { roles = uow.SelfAssignedRoles.GetFromGuild(Context.Guild.Id); if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id)) @@ -84,7 +90,7 @@ namespace NadekoBot.Modules.Administration return; bool success; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { success = uow.SelfAssignedRoles.DeleteByGuildAndRoleId(role.Guild.Id, role.Id); await uow.CompleteAsync(); @@ -105,7 +111,7 @@ namespace NadekoBot.Modules.Administration var removeMsg = new StringBuilder(); var msg = new StringBuilder(); var roleCnt = 0; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var roleModels = uow.SelfAssignedRoles.GetFromGuild(Context.Guild.Id).ToList(); msg.AppendLine(); @@ -139,7 +145,7 @@ namespace NadekoBot.Modules.Administration public async Task Tesar() { bool areExclusive; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.GuildConfigs.For(Context.Guild.Id, set => set); @@ -160,7 +166,7 @@ namespace NadekoBot.Modules.Administration GuildConfig conf; SelfAssignedRole[] roles; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { conf = uow.GuildConfigs.For(Context.Guild.Id, set => set); roles = uow.SelfAssignedRoles.GetFromGuild(Context.Guild.Id).ToArray(); @@ -220,7 +226,7 @@ namespace NadekoBot.Modules.Administration bool autoDeleteSelfAssignedRoleMessages; IEnumerable roles; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { autoDeleteSelfAssignedRoleMessages = uow.GuildConfigs.For(Context.Guild.Id, set => set).AutoDeleteSelfAssignedRoleMessages; roles = uow.SelfAssignedRoles.GetFromGuild(Context.Guild.Id); diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index 5a2dfde6..f56d5017 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -25,7 +25,6 @@ namespace NadekoBot.Modules.Administration public UserPunishCommands(DbHandler db, MuteService muteService) { _db = db; - _muteService = muteService; } diff --git a/src/NadekoBot/Modules/Permissions/Pokemon/Pokemon.cs b/src/NadekoBot/Modules/Pokemon/Pokemon.cs similarity index 79% rename from src/NadekoBot/Modules/Permissions/Pokemon/Pokemon.cs rename to src/NadekoBot/Modules/Pokemon/Pokemon.cs index c81091c7..460e4be8 100644 --- a/src/NadekoBot/Modules/Permissions/Pokemon/Pokemon.cs +++ b/src/NadekoBot/Modules/Pokemon/Pokemon.cs @@ -7,38 +7,26 @@ using NadekoBot.Services.Database.Models; using System.Collections.Generic; using System.Threading.Tasks; using Discord; -using NLog; using System; -using Newtonsoft.Json; -using System.IO; -using System.Collections.Concurrent; +using NadekoBot.Services.Pokemon; namespace NadekoBot.Modules.Pokemon { - [NadekoModule("Pokemon", ">")] public class Pokemon : NadekoTopLevelModule { - private static readonly List _pokemonTypes = new List(); - private static readonly ConcurrentDictionary _stats = new ConcurrentDictionary(); - - public const string PokemonTypesFile = "data/pokemon_types.json"; + private readonly PokemonService _service; + private readonly DbHandler _db; + private readonly BotConfig _bc; + private readonly CurrencyHandler _ch; - private new static Logger _log { get; } - - static Pokemon() + public Pokemon(PokemonService pokemonService, DbHandler db, BotConfig bc, CurrencyHandler ch) { - _log = LogManager.GetCurrentClassLogger(); - if (File.Exists(PokemonTypesFile)) - { - _pokemonTypes = JsonConvert.DeserializeObject>(File.ReadAllText(PokemonTypesFile)); - } - else - { - _log.Warn(PokemonTypesFile + " is missing. Pokemon types not loaded."); - } + _service = pokemonService; + _db = db; + _bc = bc; + _ch = ch; } - private int GetDamage(PokemonType usertype, PokemonType targetType) { var rng = new Random(); @@ -50,14 +38,13 @@ namespace NadekoBot.Modules.Pokemon } return damage; - } - + } - private static PokemonType GetPokeType(ulong id) + private PokemonType GetPokeType(ulong id) { Dictionary setTypes; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { setTypes = uow.PokeGame.GetAll().ToDictionary(x => x.UserId, y => y.type); } @@ -66,17 +53,17 @@ namespace NadekoBot.Modules.Pokemon { return StringToPokemonType(setTypes[id]); } - var count = _pokemonTypes.Count; + var count = _service.PokemonTypes.Count; var remainder = Math.Abs((int)(id % (ulong)count)); - return _pokemonTypes[remainder]; + return _service.PokemonTypes[remainder]; } - private static PokemonType StringToPokemonType(string v) + private PokemonType StringToPokemonType(string v) { var str = v?.ToUpperInvariant(); - var list = _pokemonTypes; + var list = _service.PokemonTypes; foreach (var p in list) { if (str == p.Name) @@ -111,7 +98,7 @@ namespace NadekoBot.Modules.Pokemon // Checking stats first, then move //Set up the userstats - var userStats = _stats.GetOrAdd(user.Id, new PokeStats()); + var userStats = _service.Stats.GetOrAdd(user.Id, new PokeStats()); //Check if able to move //User not able if HP < 0, has made more than 4 attacks @@ -131,7 +118,7 @@ namespace NadekoBot.Modules.Pokemon return; } //get target stats - var targetStats = _stats.GetOrAdd(targetUser.Id, new PokeStats()); + var targetStats = _service.Stats.GetOrAdd(targetUser.Id, new PokeStats()); //If target's HP is below 0, no use attacking if (targetStats.Hp <= 0) @@ -195,8 +182,8 @@ namespace NadekoBot.Modules.Pokemon //update dictionary //This can stay the same right? - _stats[user.Id] = userStats; - _stats[targetUser.Id] = targetStats; + _service.Stats[user.Id] = userStats; + _service.Stats[targetUser.Id] = targetStats; await Context.Channel.SendConfirmAsync(Context.User.Mention + " " + response).ConfigureAwait(false); } @@ -228,9 +215,9 @@ namespace NadekoBot.Modules.Pokemon return; } - if (_stats.ContainsKey(targetUser.Id)) + if (_service.Stats.ContainsKey(targetUser.Id)) { - var targetStats = _stats[targetUser.Id]; + var targetStats = _service.Stats[targetUser.Id]; if (targetStats.Hp == targetStats.MaxHp) { await ReplyErrorLocalized("already_full", Format.Bold(targetUser.ToString())).ConfigureAwait(false); @@ -242,9 +229,9 @@ namespace NadekoBot.Modules.Pokemon var target = (targetUser.Id == user.Id) ? "yourself" : targetUser.Mention; if (amount > 0) { - if (!await CurrencyHandler.RemoveCurrencyAsync(user, $"Poke-Heal {target}", amount, true).ConfigureAwait(false)) + if (!await _ch.RemoveCurrencyAsync(user, $"Poke-Heal {target}", amount, true).ConfigureAwait(false)) { - await ReplyErrorLocalized("no_currency", NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("no_currency", _bc.CurrencySign).ConfigureAwait(false); return; } } @@ -254,16 +241,16 @@ namespace NadekoBot.Modules.Pokemon if (targetStats.Hp < 0) { //Could heal only for half HP? - _stats[targetUser.Id].Hp = (targetStats.MaxHp / 2); + _service.Stats[targetUser.Id].Hp = (targetStats.MaxHp / 2); if (target == "yourself") { - await ReplyConfirmLocalized("revive_yourself", NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("revive_yourself", _bc.CurrencySign).ConfigureAwait(false); return; } - await ReplyConfirmLocalized("revive_other", Format.Bold(targetUser.ToString()), NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("revive_other", Format.Bold(targetUser.ToString()), _bc.CurrencySign).ConfigureAwait(false); } - await ReplyConfirmLocalized("healed", Format.Bold(targetUser.ToString()), NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("healed", Format.Bold(targetUser.ToString()), _bc.CurrencySign).ConfigureAwait(false); } else { @@ -291,7 +278,7 @@ namespace NadekoBot.Modules.Pokemon var targetType = StringToPokemonType(typeTargeted); if (targetType == null) { - await Context.Channel.EmbedAsync(_pokemonTypes.Aggregate(new EmbedBuilder().WithDescription("List of the available types:"), + await Context.Channel.EmbedAsync(_service.PokemonTypes.Aggregate(new EmbedBuilder().WithDescription("List of the available types:"), (eb, pt) => eb.AddField(efb => efb.WithName(pt.Name) .WithValue(pt.Icon) .WithIsInline(true))) @@ -308,16 +295,16 @@ namespace NadekoBot.Modules.Pokemon var amount = 1; if (amount > 0) { - if (!await CurrencyHandler.RemoveCurrencyAsync(user, $"{user} change type to {typeTargeted}", amount, true).ConfigureAwait(false)) + if (!await _ch.RemoveCurrencyAsync(user, $"{user} change type to {typeTargeted}", amount, true).ConfigureAwait(false)) { - await ReplyErrorLocalized("no_currency", NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("no_currency", _bc.CurrencySign).ConfigureAwait(false); return; } } //Actually changing the type here - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var pokeUsers = uow.PokeGame.GetAll().ToArray(); var setTypes = pokeUsers.ToDictionary(x => x.UserId, y => y.type); @@ -344,7 +331,7 @@ namespace NadekoBot.Modules.Pokemon //Now for the response await ReplyConfirmLocalized("settype_success", targetType, - NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false); + _bc.CurrencySign).ConfigureAwait(false); } } } diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index bb5f9e74..eabdceed 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -97,8 +97,12 @@ namespace NadekoBot var soundcloud = new SoundCloudApiService(credentials); //module services + //todo 90 - Make this automatic var utilityService = new UtilityService(AllGuildConfigs, Client, BotConfig, db); +#region Searches var searchesService = new SearchesService(Client, google, db); + var streamNotificationService = new StreamNotificationService(db, Client, strings); +#endregion var clashService = new ClashOfClansService(Client, db, localization, strings); var musicService = new MusicService(google, strings, localization, db, soundcloud, credentials); var crService = new CustomReactionsService(db, Client); @@ -108,11 +112,16 @@ namespace NadekoBot var selfService = new SelfService(this, commandHandler, db, BotConfig); var vcRoleService = new VcRoleService(Client, AllGuildConfigs); var vPlusTService = new VplusTService(Client, AllGuildConfigs, strings, db); + var muteService = new MuteService(Client, AllGuildConfigs, db); + var ratelimitService = new RatelimitService(Client, AllGuildConfigs); + var protectionService = new ProtectionService(Client, AllGuildConfigs, muteService); + var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService); + var gameVcService = new GameVoiceChannelService(Client, db, AllGuildConfigs); + var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); #endregion - //initialize Services - Services = new NServiceProvider.ServiceProviderBuilder() //todo all Adds should be interfaces + Services = new NServiceProvider.ServiceProviderBuilder() .Add(localization) .Add(stats) .Add(images) @@ -129,14 +138,22 @@ namespace NadekoBot //modules .Add(utilityService) .Add(searchesService) + .Add(streamNotificationService) .Add(clashService) .Add(musicService) .Add(greetSettingsService) .Add(crService) .Add(gamesService) - .Add(selfService) - .Add(vcRoleService) - .Add(vPlusTService) + .Add(administrationService) + .Add(selfService) + .Add(vcRoleService) + .Add(vPlusTService) + .Add(muteService) + .Add(ratelimitService) + .Add(playingRotateService) + .Add(gameVcService) + .Add(autoAssignRoleService) + .Add(protectionService) .Build(); commandHandler.AddServices(Services); diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 5fb0e42b..b0f02199 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -39,7 +39,17 @@ + + + + + + + + + + @@ -61,7 +71,7 @@ - + diff --git a/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs b/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs new file mode 100644 index 00000000..46d26ddf --- /dev/null +++ b/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs @@ -0,0 +1,46 @@ +using Discord.WebSocket; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace NadekoBot.Services.Administration +{ + public class AutoAssignRoleService + { + private readonly Logger _log; + private readonly DiscordShardedClient _client; + + //guildid/roleid + public ConcurrentDictionary AutoAssignedRoles { get; } + + public AutoAssignRoleService(DiscordShardedClient client, IEnumerable gcs) + { + _log = LogManager.GetCurrentClassLogger(); + _client = client; + + AutoAssignedRoles = new ConcurrentDictionary( + gcs.Where(x => x.AutoAssignRoleId != 0) + .ToDictionary(k => k.GuildId, v => v.AutoAssignRoleId)); + + _client.UserJoined += async (user) => + { + try + { + AutoAssignedRoles.TryGetValue(user.Guild.Id, out ulong roleId); + + if (roleId == 0) + return; + + var role = user.Guild.Roles.FirstOrDefault(r => r.Id == roleId); + + if (role != null) + await user.AddRoleAsync(role).ConfigureAwait(false); + } + catch (Exception ex) { _log.Warn(ex); } + }; + } + } +} diff --git a/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs b/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs new file mode 100644 index 00000000..625d6626 --- /dev/null +++ b/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs @@ -0,0 +1,73 @@ +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class GameVoiceChannelService + { + public readonly ConcurrentHashSet GameVoiceChannels = new ConcurrentHashSet(); + + private readonly Logger _log; + private readonly DbHandler _db; + private readonly DiscordShardedClient _client; + + public GameVoiceChannelService(DiscordShardedClient client, DbHandler db, IEnumerable gcs) + { + _log = LogManager.GetCurrentClassLogger(); + _db = db; + _client = client; + + GameVoiceChannels = new ConcurrentHashSet( + gcs.Where(gc => gc.GameVoiceChannel != null) + .Select(gc => gc.GameVoiceChannel.Value)); + + _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + + } + + private Task Client_UserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState) + { + var _ = Task.Run(async () => + { + try + { + var gUser = usr as SocketGuildUser; + if (gUser == null) + return; + + var game = gUser.Game?.Name.TrimTo(50).ToLowerInvariant(); + + if (oldState.VoiceChannel == newState.VoiceChannel || + newState.VoiceChannel == null) + return; + + if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id) || + string.IsNullOrWhiteSpace(game)) + return; + + var vch = gUser.Guild.VoiceChannels + .FirstOrDefault(x => x.Name.ToLowerInvariant() == game); + + if (vch == null) + return; + + await Task.Delay(1000).ConfigureAwait(false); + await gUser.ModifyAsync(gu => gu.Channel = vch).ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Warn(ex); + } + }); + + return Task.CompletedTask; + } + } +} diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs new file mode 100644 index 00000000..a63592ff --- /dev/null +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -0,0 +1,104 @@ +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Music; +using NLog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + //todo 99 - Could make a placeholder service, which can work for any module + //and have replacements which are dependent on the types provided in the constructor + public class PlayingRotateService + { + public List RotatingStatusMessages { get; } + public volatile bool RotatingStatuses; + private readonly Timer _t; + private readonly DiscordShardedClient _client; + private readonly BotConfig _bc; + private readonly MusicService _music; + private readonly Logger _log; + + private class TimerState + { + public int Index { get; set; } + } + + public PlayingRotateService(DiscordShardedClient client, BotConfig bc, MusicService music) + { + _client = client; + _bc = bc; + _music = music; + _log = LogManager.GetCurrentClassLogger(); + + RotatingStatusMessages = _bc.RotatingStatusMessages; + RotatingStatuses = _bc.RotatingStatuses; + + _t = new Timer(async (objState) => + { + try + { + var state = (TimerState)objState; + if (!RotatingStatuses) + return; + if (state.Index >= RotatingStatusMessages.Count) + state.Index = 0; + + if (!RotatingStatusMessages.Any()) + return; + var status = RotatingStatusMessages[state.Index++].Status; + if (string.IsNullOrWhiteSpace(status)) + return; + PlayingPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(_client,_music))); + var shards = _client.Shards; + for (int i = 0; i < shards.Count; i++) + { + var curShard = shards.ElementAt(i); + ShardSpecificPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(curShard))); + try { await shards.ElementAt(i).SetGameAsync(status).ConfigureAwait(false); } + catch (Exception ex) + { + _log.Warn(ex); + } + } + } + catch (Exception ex) + { + _log.Warn("Rotating playing status errored.\n" + ex); + } + }, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + } + + public Dictionary> PlayingPlaceholders { get; } = + new Dictionary> { + { "%servers%", (c, ms) => c.Guilds.Count.ToString()}, + { "%users%", (c, ms) => c.Guilds.Sum(s => s.Users.Count).ToString()}, + { "%playing%", (c, ms) => { + var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); + if (cnt != 1) return cnt.ToString(); + try { + var mp = ms.MusicPlayers.FirstOrDefault(); + return mp.Value.CurrentSong.SongInfo.Title; + } + catch { + return "No songs"; + } + } + }, + { "%queued%", (c, ms) => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()}, + { "%time%", (c, ms) => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()) }, + { "%shardcount%", (c, ms) => c.Shards.Count.ToString() }, + }; + + public Dictionary> ShardSpecificPlaceholders { get; } = + new Dictionary> { + { "%shardid%", (client) => client.ShardId.ToString()}, + { "%shardguilds%", (client) => client.Guilds.Count.ToString()}, + }; + } +} diff --git a/src/NadekoBot/Services/Administration/ProtectionService.cs b/src/NadekoBot/Services/Administration/ProtectionService.cs new file mode 100644 index 00000000..e57f38ef --- /dev/null +++ b/src/NadekoBot/Services/Administration/ProtectionService.cs @@ -0,0 +1,182 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class ProtectionService + { + public readonly ConcurrentDictionary AntiRaidGuilds = + new ConcurrentDictionary(); + // guildId | (userId|messages) + public readonly ConcurrentDictionary AntiSpamGuilds = + new ConcurrentDictionary(); + + //todo sub LogCommands to this + public event Func, PunishmentAction, ProtectionType, Task> OnAntiProtectionTriggered = delegate { return Task.CompletedTask; }; + + private readonly Logger _log; + private readonly DiscordShardedClient _client; + private readonly MuteService _mute; + + public ProtectionService(DiscordShardedClient client, IEnumerable gcs, MuteService mute) + { + _log = LogManager.GetCurrentClassLogger(); + _client = client; + _mute = mute; + + foreach (var gc in gcs) + { + var raid = gc.AntiRaidSetting; + var spam = gc.AntiSpamSetting; + + if (raid != null) + { + var raidStats = new AntiRaidStats() { AntiRaidSettings = raid }; + AntiRaidGuilds.TryAdd(gc.GuildId, raidStats); + } + + if (spam != null) + AntiSpamGuilds.TryAdd(gc.GuildId, new AntiSpamStats() { AntiSpamSettings = spam }); + } + + _client.MessageReceived += (imsg) => + { + var msg = imsg as IUserMessage; + if (msg == null || msg.Author.IsBot) + return Task.CompletedTask; + + var channel = msg.Channel as ITextChannel; + if (channel == null) + return Task.CompletedTask; + var _ = Task.Run(async () => + { + try + { + if (!AntiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings) || + spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new AntiSpamIgnore() + { + ChannelId = channel.Id + })) + return; + + var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, (id) => new UserSpamStats(msg), + (id, old) => + { + old.ApplyNextMessage(msg); return old; + }); + + if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold) + { + if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) + { + stats.Dispose(); + await PunishUsers(spamSettings.AntiSpamSettings.Action, ProtectionType.Spamming, (IGuildUser)msg.Author) + .ConfigureAwait(false); + } + } + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + }; + + _client.UserJoined += (usr) => + { + if (usr.IsBot) + return Task.CompletedTask; + if (!AntiRaidGuilds.TryGetValue(usr.Guild.Id, out var settings)) + return Task.CompletedTask; + if (!settings.RaidUsers.Add(usr)) + return Task.CompletedTask; + + var _ = Task.Run(async () => + { + try + { + ++settings.UsersCount; + + if (settings.UsersCount >= settings.AntiRaidSettings.UserThreshold) + { + var users = settings.RaidUsers.ToArray(); + settings.RaidUsers.Clear(); + + await PunishUsers(settings.AntiRaidSettings.Action, ProtectionType.Raiding, users).ConfigureAwait(false); + } + await Task.Delay(1000 * settings.AntiRaidSettings.Seconds).ConfigureAwait(false); + + settings.RaidUsers.TryRemove(usr); + --settings.UsersCount; + + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + }; + } + + + private async Task PunishUsers(PunishmentAction action, ProtectionType pt, params IGuildUser[] gus) + { + _log.Info($"[{pt}] - Punishing [{gus.Length}] users with [{action}] in {gus[0].Guild.Name} guild"); + foreach (var gu in gus) + { + switch (action) + { + case PunishmentAction.Mute: + try + { + await _mute.MuteUser(gu).ConfigureAwait(false); + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishement"); } + break; + case PunishmentAction.Kick: + try + { + await gu.KickAsync().ConfigureAwait(false); + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishement"); } + break; + case PunishmentAction.Softban: + try + { + await gu.Guild.AddBanAsync(gu, 7).ConfigureAwait(false); + try + { + await gu.Guild.RemoveBanAsync(gu).ConfigureAwait(false); + } + catch + { + await gu.Guild.RemoveBanAsync(gu).ConfigureAwait(false); + // try it twice, really don't want to ban user if + // only kick has been specified as the punishement + } + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } + break; + case PunishmentAction.Ban: + try + { + await gu.Guild.AddBanAsync(gu, 7).ConfigureAwait(false); + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } + break; + } + } + await OnAntiProtectionTriggered(gus, action, pt).ConfigureAwait(false); + } + } +} diff --git a/src/NadekoBot/Services/Administration/ProtectionStats.cs b/src/NadekoBot/Services/Administration/ProtectionStats.cs new file mode 100644 index 00000000..01d9f5f5 --- /dev/null +++ b/src/NadekoBot/Services/Administration/ProtectionStats.cs @@ -0,0 +1,26 @@ +using Discord; +using NadekoBot.Services.Database.Models; +using System.Collections.Concurrent; + +namespace NadekoBot.Services.Administration +{ + public enum ProtectionType + { + Raiding, + Spamming, + } + + public class AntiRaidStats + { + public AntiRaidSetting AntiRaidSettings { get; set; } + public int UsersCount { get; set; } + public ConcurrentHashSet RaidUsers { get; set; } = new ConcurrentHashSet(); + } + + public class AntiSpamStats + { + public AntiSpamSetting AntiSpamSettings { get; set; } + public ConcurrentDictionary UserStats { get; set; } + = new ConcurrentDictionary(); + } +} diff --git a/src/NadekoBot/Services/Administration/RatelimitService.cs b/src/NadekoBot/Services/Administration/RatelimitService.cs new file mode 100644 index 00000000..0148d535 --- /dev/null +++ b/src/NadekoBot/Services/Administration/RatelimitService.cs @@ -0,0 +1,53 @@ +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace NadekoBot.Services.Administration +{ + public class RatelimitService + { + public ConcurrentDictionary RatelimitingChannels = new ConcurrentDictionary(); + public ConcurrentDictionary> IgnoredRoles = new ConcurrentDictionary>(); + public ConcurrentDictionary> IgnoredUsers = new ConcurrentDictionary>(); + + private readonly Logger _log; + private readonly DiscordShardedClient _client; + + public RatelimitService(DiscordShardedClient client, IEnumerable gcs) + { + _log = LogManager.GetCurrentClassLogger(); + _client = client; + + IgnoredRoles = new ConcurrentDictionary>( + gcs.ToDictionary(x => x.GuildId, + x => new HashSet(x.SlowmodeIgnoredRoles.Select(y => y.RoleId)))); + + IgnoredUsers = new ConcurrentDictionary>( + gcs.ToDictionary(x => x.GuildId, + x => new HashSet(x.SlowmodeIgnoredUsers.Select(y => y.UserId)))); + + _client.MessageReceived += async (umsg) => + { + try + { + var usrMsg = umsg as SocketUserMessage; + var channel = usrMsg?.Channel as SocketTextChannel; + + if (channel == null || usrMsg == null || usrMsg.IsAuthor(client)) + return; + if (!RatelimitingChannels.TryGetValue(channel.Id, out Ratelimiter limiter)) + return; + + if (limiter.CheckUserRatelimit(usrMsg.Author.Id, channel.Guild.Id, usrMsg.Author as SocketGuildUser)) + await usrMsg.DeleteAsync(); + } + catch (Exception ex) { _log.Warn(ex); } + }; + } + } +} diff --git a/src/NadekoBot/Services/Administration/Ratelimiter.cs b/src/NadekoBot/Services/Administration/Ratelimiter.cs new file mode 100644 index 00000000..168e8f60 --- /dev/null +++ b/src/NadekoBot/Services/Administration/Ratelimiter.cs @@ -0,0 +1,59 @@ +using Discord.WebSocket; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class Ratelimiter + { + private readonly RatelimitService _svc; + + public class RatelimitedUser + { + public ulong UserId { get; set; } + public int MessageCount { get; set; } = 0; + } + + public ulong ChannelId { get; set; } + public int MaxMessages { get; set; } + public int PerSeconds { get; set; } + + public Ratelimiter(RatelimitService svc) + { + _svc = svc; + } + + public CancellationTokenSource CancelSource { get; set; } = new CancellationTokenSource(); + + public ConcurrentDictionary Users { get; set; } = new ConcurrentDictionary(); + + public bool CheckUserRatelimit(ulong id, ulong guildId, SocketGuildUser optUser) + { + if ((_svc.IgnoredUsers.TryGetValue(guildId, out HashSet ignoreUsers) && ignoreUsers.Contains(id)) || + (optUser != null && _svc.IgnoredRoles.TryGetValue(guildId, out HashSet ignoreRoles) && optUser.Roles.Any(x => ignoreRoles.Contains(x.Id)))) + return false; + + var usr = Users.GetOrAdd(id, (key) => new RatelimitedUser() { UserId = id }); + if (usr.MessageCount >= MaxMessages) + { + return true; + } + usr.MessageCount++; + var _ = Task.Run(async () => + { + try + { + await Task.Delay(PerSeconds * 1000, CancelSource.Token); + } + catch (OperationCanceledException) { } + usr.MessageCount--; + }); + return false; + } + } +} diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs index d36afc94..734beab2 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -12,8 +12,7 @@ namespace NadekoBot.Services.Administration { public volatile bool ForwardDMs; public volatile bool ForwardDMsToAllOwners; - - private readonly Logger _log; + private readonly NadekoBot _bot; private readonly CommandHandler _cmdHandler; private readonly DbHandler _db; diff --git a/src/NadekoBot/Services/Administration/UserSpamStats.cs b/src/NadekoBot/Services/Administration/UserSpamStats.cs new file mode 100644 index 00000000..401aa180 --- /dev/null +++ b/src/NadekoBot/Services/Administration/UserSpamStats.cs @@ -0,0 +1,53 @@ +using Discord; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class UserSpamStats : IDisposable + { + public int Count => timers.Count; + public string LastMessage { get; set; } + + private ConcurrentQueue timers { get; } + + public UserSpamStats(IUserMessage msg) + { + LastMessage = msg.Content.ToUpperInvariant(); + timers = new ConcurrentQueue(); + + ApplyNextMessage(msg); + } + + private readonly object applyLock = new object(); + public void ApplyNextMessage(IUserMessage message) + { + lock (applyLock) + { + var upperMsg = message.Content.ToUpperInvariant(); + if (upperMsg != LastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any())) + { + LastMessage = upperMsg; + while (timers.TryDequeue(out var old)) + old.Change(Timeout.Infinite, Timeout.Infinite); + } + var t = new Timer((_) => { + if (timers.TryDequeue(out var old)) + old.Change(Timeout.Infinite, Timeout.Infinite); + }, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30)); + timers.Enqueue(t); + } + } + + public void Dispose() + { + while (timers.TryDequeue(out var old)) + old.Change(Timeout.Infinite, Timeout.Infinite); + } + } +} diff --git a/src/NadekoBot/Modules/Permissions/Pokemon/PokeStats.cs b/src/NadekoBot/Services/Pokemon/PokeStats.cs similarity index 85% rename from src/NadekoBot/Modules/Permissions/Pokemon/PokeStats.cs rename to src/NadekoBot/Services/Pokemon/PokeStats.cs index b9795043..069cfd72 100644 --- a/src/NadekoBot/Modules/Permissions/Pokemon/PokeStats.cs +++ b/src/NadekoBot/Services/Pokemon/PokeStats.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -namespace NadekoBot.Modules.Pokemon +namespace NadekoBot.Services.Pokemon { - class PokeStats + public class PokeStats { //Health left public int Hp { get; set; } = 500; diff --git a/src/NadekoBot/Services/Pokemon/PokemonService.cs b/src/NadekoBot/Services/Pokemon/PokemonService.cs new file mode 100644 index 00000000..d2160865 --- /dev/null +++ b/src/NadekoBot/Services/Pokemon/PokemonService.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using NLog; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; + +namespace NadekoBot.Services.Pokemon +{ + public class PokemonService + { + public readonly List PokemonTypes = new List(); + public readonly ConcurrentDictionary Stats = new ConcurrentDictionary(); + + public const string PokemonTypesFile = "data/pokemon_types.json"; + + private Logger _log { get; } + + public PokemonService() + { + _log = LogManager.GetCurrentClassLogger(); + if (File.Exists(PokemonTypesFile)) + { + PokemonTypes = JsonConvert.DeserializeObject>(File.ReadAllText(PokemonTypesFile)); + } + else + { + PokemonTypes = new List(); + _log.Warn(PokemonTypesFile + " is missing. Pokemon types not loaded."); + } + } + } +} diff --git a/src/NadekoBot/Modules/Permissions/Pokemon/PokemonType.cs b/src/NadekoBot/Services/Pokemon/PokemonType.cs similarity index 95% rename from src/NadekoBot/Modules/Permissions/Pokemon/PokemonType.cs rename to src/NadekoBot/Services/Pokemon/PokemonType.cs index 20d47df0..1b2bae80 100644 --- a/src/NadekoBot/Modules/Permissions/Pokemon/PokemonType.cs +++ b/src/NadekoBot/Services/Pokemon/PokemonType.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NadekoBot.Modules.Pokemon +namespace NadekoBot.Services.Pokemon { public class PokemonType { diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 1b4519b8..13df5a43 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -18,6 +18,9 @@ namespace NadekoBot.Extensions { public static class Extensions { + public static bool IsAuthor(this IMessage msg, IDiscordClient client) => + msg.Author?.Id == client.CurrentUser.Id; + private static readonly IEmote arrow_left = Emote.Parse("⬅"); private static readonly IEmote arrow_right = Emote.Parse("➡");