From 30e1521810e1256addd8a578f24d94004229229f Mon Sep 17 00:00:00 2001 From: Kwoth Date: Tue, 18 Oct 2016 12:02:15 +0200 Subject: [PATCH] Anti raid and anti spam commands added O.O --- .../Commands/AntiRaidCommands.cs | 287 ++++++++++++++++++ .../Resources/CommandStrings.Designer.cs | 72 ++++- src/NadekoBot/Resources/CommandStrings.resx | 20 +- src/NadekoBot/_Extensions/Extensions.cs | 8 + 4 files changed, 377 insertions(+), 10 deletions(-) create mode 100644 src/NadekoBot/Modules/Administration/Commands/AntiRaidCommands.cs diff --git a/src/NadekoBot/Modules/Administration/Commands/AntiRaidCommands.cs b/src/NadekoBot/Modules/Administration/Commands/AntiRaidCommands.cs new file mode 100644 index 00000000..b2767455 --- /dev/null +++ b/src/NadekoBot/Modules/Administration/Commands/AntiRaidCommands.cs @@ -0,0 +1,287 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database; +using NLog; +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.Modules.Administration +{ + public partial class Administration + { + public enum PunishmentAction + { + Mute, + Kick, + Ban, + } + + private class AntiRaidSetting + { + public int UserThreshold { get; set; } + public int Seconds { get; set; } + public PunishmentAction Action { get; set; } + public IRole MuteRole { get; set; } + public int UsersCount { get; set; } + public ConcurrentHashSet RaidUsers { get; set; } = new ConcurrentHashSet(); + } + + private class AntiSpamSetting + { + public PunishmentAction Action { get; set; } + public int MessageThreshold { get; set; } = 3; + public IRole MuteRole { get; set; } + public ConcurrentDictionary UserStats { get; set; } + = new ConcurrentDictionary(); + } + + private class UserSpamStats + { + public int Count { get; set; } + public string LastMessage { get; set; } + + public UserSpamStats(string msg) + { + Count = 1; + LastMessage = msg.ToUpperInvariant(); + } + + public void ApplyNextMessage(string message) + { + var upperMsg = message.ToUpperInvariant(); + if (upperMsg == LastMessage) + Count++; + else + { + LastMessage = upperMsg; + Count = 0; + } + } + } + + [Group] + public class AntiRaidCommands + { + private static ConcurrentDictionary antiRaidGuilds = + new ConcurrentDictionary(); + // guildId | (userId|messages) + private static ConcurrentDictionary antiSpamGuilds = + new ConcurrentDictionary(); + + private Logger _log { get; } + + public AntiRaidCommands(ShardedDiscordClient client) + { + _log = LogManager.GetCurrentClassLogger(); + + client.MessageReceived += (imsg) => + { + var msg = imsg as IUserMessage; + if (msg == null) + return Task.CompletedTask; + + var channel = msg.Channel as ITextChannel; + if (channel == null) + return Task.CompletedTask; + + var t = Task.Run(async () => + { + AntiSpamSetting spamSettings; + if (!antiSpamGuilds.TryGetValue(channel.Guild.Id, out spamSettings)) + return; + + var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, new UserSpamStats(msg.Content), + (id, old) => { old.ApplyNextMessage(msg.Content); return old; }); + + if (stats.Count >= spamSettings.MessageThreshold) + { + if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) + { + var log = await PunishUser((IGuildUser)msg.Author, spamSettings.Action, spamSettings.MuteRole) + .ConfigureAwait(false); + try { await channel.Guild.SendMessageToOwnerAsync(log).ConfigureAwait(false); } catch { } + } + } + + }); + return Task.CompletedTask; + }; + + client.UserJoined += (usr) => + { + if (usr.IsBot) + return Task.CompletedTask; + + AntiRaidSetting settings; + if (!antiRaidGuilds.TryGetValue(usr.Guild.Id, out settings)) + return Task.CompletedTask; + + var t = Task.Run(async () => + { + if (!settings.RaidUsers.Add(usr)) + return; + + ++settings.UsersCount; + + if (settings.UsersCount >= settings.UserThreshold) + { + var users = settings.RaidUsers.ToList(); + settings.RaidUsers.Clear(); + string msg = ""; + foreach (var gu in users) + { + msg += await PunishUser(gu, settings.Action, settings.MuteRole).ConfigureAwait(false); + } + try { await usr.Guild.SendMessageToOwnerAsync(msg).ConfigureAwait(false); } catch { } + } + + await Task.Delay(1000 * settings.Seconds).ConfigureAwait(false); + + settings.RaidUsers.TryRemove(usr); + --settings.UsersCount; + }); + + return Task.CompletedTask; + }; + } + + private async Task PunishUser(IGuildUser gu, PunishmentAction action, IRole muteRole) + { + switch (action) + { + case PunishmentAction.Mute: + try + { + await gu.AddRolesAsync(muteRole); + return $"{Format.Bold(gu.ToString())} was muted due to raiding protection.\n"; + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishement"); } + break; + case PunishmentAction.Kick: + try + { + await gu.Guild.AddBanAsync(gu, 7); + try + { + await gu.Guild.RemoveBanAsync(gu); + } + catch + { + await gu.Guild.RemoveBanAsync(gu); + // try it twice, really don't want to ban user if + // only kick has been specified as the punishement + } + return $"{Format.Bold(gu.ToString())} was kicked due to raiding protection.\n"; + + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } + break; + case PunishmentAction.Ban: + try + { + await gu.Guild.AddBanAsync(gu, 7); + return $"{Format.Bold(gu.ToString())} was banned due to raiding protection.\n"; + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } + break; + default: + break; + } + return String.Empty; + } + + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequirePermission(GuildPermission.Administrator)] + public async Task AntiRaid(IUserMessage imsg, int userThreshold, int seconds, PunishmentAction action) + { + var channel = (ITextChannel)imsg.Channel; + + if (userThreshold < 2 || userThreshold > 30) + { + await channel.SendMessageAsync("User threshold must be between 2 and 30").ConfigureAwait(false); + return; + } + + if (seconds < 2 || seconds > 300) + { + await channel.SendMessageAsync("Time must be between 2 and 300 seconds.").ConfigureAwait(false); + return; + } + + IRole muteRole; + try + { + muteRole = await GetMuteRole(channel.Guild).ConfigureAwait(false); + } + catch (Exception ex) + { + await channel.SendMessageAsync("Failed creating a mute role. Give me ManageRoles permission" + + "or create 'nadeko-mute' role with disabled SendMessages and try again.") + .ConfigureAwait(false); + _log.Warn(ex); + return; + } + + var setting = new AntiRaidSetting() + { + Action = action, + Seconds = seconds, + UserThreshold = userThreshold, + MuteRole = muteRole, + }; + antiRaidGuilds.AddOrUpdate(channel.Guild.Id, setting, (id, old) => setting); + + await channel.SendMessageAsync($"{imsg.Author.Mention} `If {userThreshold} or more users join within {seconds} seconds, I will {action} them.`") + .ConfigureAwait(false); + } + + private async Task GetMuteRole(IGuild guild) + { + var muteRole = guild.Roles.FirstOrDefault(r => r.Name == "nadeko-mute") ?? + await guild.CreateRoleAsync("nadeko-mute", GuildPermissions.None).ConfigureAwait(false); + foreach (var toOverwrite in guild.GetTextChannels()) + { + await toOverwrite.AddPermissionOverwriteAsync(muteRole, new OverwritePermissions(sendMessages: PermValue.Deny, attachFiles: PermValue.Deny)) + .ConfigureAwait(false); + await Task.Delay(200).ConfigureAwait(false); + } + return muteRole; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequirePermission(GuildPermission.Administrator)] + public async Task AntiSpam(IUserMessage imsg, PunishmentAction action = PunishmentAction.Mute) + { + var channel = (ITextChannel)imsg.Channel; + + AntiSpamSetting throwaway; + if (antiSpamGuilds.TryRemove(channel.Guild.Id, out throwaway)) + { + await channel.SendMessageAsync("`Anti-Spam feature disabled on this server.`").ConfigureAwait(false); + } + else + { + if (antiSpamGuilds.TryAdd(channel.Guild.Id, new AntiSpamSetting() + { + Action = action, + MuteRole = await GetMuteRole(channel.Guild).ConfigureAwait(false), + })) + await channel.SendMessageAsync("`Anti-Spam feature enabled on this server.`").ConfigureAwait(false); + } + + } + } + } +} diff --git a/src/NadekoBot/Resources/CommandStrings.Designer.cs b/src/NadekoBot/Resources/CommandStrings.Designer.cs index 48332af8..139e5980 100644 --- a/src/NadekoBot/Resources/CommandStrings.Designer.cs +++ b/src/NadekoBot/Resources/CommandStrings.Designer.cs @@ -356,6 +356,60 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to antiraid. + /// + public static string antiraid_cmd { + get { + return ResourceManager.GetString("antiraid_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets an anti-raid protection on the server. First argument is number of people which will trigger the protection. Second one is a time interval in which that number of people needs to join in order to trigger the protection, and third argument is punishment for those people (Kick, Ban, Mute). + /// + public static string antiraid_desc { + get { + return ResourceManager.GetString("antiraid_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `.antiraid 5 20 Kick`. + /// + public static string antiraid_usage { + get { + return ResourceManager.GetString("antiraid_usage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to antispam. + /// + public static string antispam_cmd { + get { + return ResourceManager.GetString("antispam_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stops people from repeating same message more than 2 times in a row. You can either specify to mute, kick or ban them if that happens.. + /// + public static string antispam_desc { + get { + return ResourceManager.GetString("antispam_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `.antispam Mute` or `.antispam Kick` or `.antispam Ban`. + /// + public static string antispam_usage { + get { + return ResourceManager.GetString("antispam_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to asar. /// @@ -528,7 +582,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Bans a user by id or name with an optional message.. + /// Looks up a localized string similar to Bans a user by ID or name with an optional message.. /// public static string ban_desc { get { @@ -1446,7 +1500,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Deletes all text channels ending in `-voice` for which voicechannels are not found. **Use at your own risk. Needs Manage Roles and Manage Channels Permissions.**. + /// Looks up a localized string similar to Deletes all text channels ending in `-voice` for which voicechannels are not found. Use at your own risk.. /// public static string cleanvplust_desc { get { @@ -1932,7 +1986,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Toggles the automatic deletion of user's successful command message to prevent chat flood. **Server Manager Only.**. + /// Looks up a localized string similar to Toggles the automatic deletion of user's successful command message to prevent chat flood.. /// public static string delmsgoncmd_desc { get { @@ -2040,7 +2094,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Works only for the owner. Shuts the bot down.. + /// Looks up a localized string similar to Shuts the bot down.. /// public static string die_desc { get { @@ -5631,7 +5685,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Toggles rotation of playing status of the dynamic strings you specified earlier.. + /// Looks up a localized string similar to Toggles rotation of playing status of the dynamic strings you previously specified.. /// public static string rotateplaying_desc { get { @@ -5982,7 +6036,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Changed the name of the current channel. . + /// Looks up a localized string similar to Changes the name of the current channel. . /// public static string setchanlname_desc { get { @@ -6063,7 +6117,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Give the bot a new name. . + /// Looks up a localized string similar to Gives the bot a new name. . /// public static string setname_desc { get { @@ -6333,7 +6387,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Bans and then unbans a user by id or name with an optional message. . + /// Looks up a localized string similar to Bans and then unbans a user by ID or name with an optional message. . /// public static string softban_desc { get { @@ -7440,7 +7494,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Toggles logging to this channel whenever someone joins or leaves a voice channel you are in right now. . + /// Looks up a localized string similar to Toggles logging to this channel whenever someone joins or leaves a voice channel you are currently in. . /// public static string voicepresence_desc { get { diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 2be6ac0c..cd10e0a4 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -2646,4 +2646,22 @@ `.resetperms` - + + antiraid + + + Sets an anti-raid protection on the server. First argument is number of people which will trigger the protection. Second one is a time interval in which that number of people needs to join in order to trigger the protection, and third argument is punishment for those people (Kick, Ban, Mute) + + + `.antiraid 5 20 Kick` + + + antispam + + + Stops people from repeating same message more than 2 times in a row. You can either specify to mute, kick or ban them if that happens. + + + `.antispam Mute` or `.antispam Kick` or `.antispam Ban` + + \ No newline at end of file diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 6fe6b13c..60aa3cf4 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -24,6 +24,14 @@ namespace NadekoBot.Extensions http.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); } + public static async Task SendMessageToOwnerAsync(this IGuild guild, string message) + { + var ownerPrivate = await (await guild.GetOwnerAsync().ConfigureAwait(false)).CreateDMChannelAsync() + .ConfigureAwait(false); + + return await ownerPrivate.SendMessageAsync(message).ConfigureAwait(false); + } + public static IEnumerable ForEach(this IEnumerable elems, Action exec) { foreach (var elem in elems)