From 5c93460379192282a6f54bda9c19fcc5139dc52f Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Fri, 15 Jan 2021 13:29:34 -0800 Subject: [PATCH 01/42] Implements Unit Tests and a couple basic tests --- BotCatMaxy.sln | 6 +++++ .../Components/Filter/FilterCommands.cs | 7 +---- BotCatMaxy/Models/BadWord.cs | 12 ++++----- Tests/FilterTests.cs | 27 +++++++++++++++++++ Tests/ParseTests.cs | 23 ++++++++++++++++ Tests/Tests.csproj | 26 ++++++++++++++++++ 6 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 Tests/FilterTests.cs create mode 100644 Tests/ParseTests.cs create mode 100644 Tests/Tests.csproj diff --git a/BotCatMaxy.sln b/BotCatMaxy.sln index 5555bb1..b822043 100644 --- a/BotCatMaxy.sln +++ b/BotCatMaxy.sln @@ -10,6 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{B96578E1-28D5-467C-A133-897DE1406A47}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,6 +22,10 @@ Global {BA3618A8-03A9-4C2D-8901-417FE90F0D85}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA3618A8-03A9-4C2D-8901-417FE90F0D85}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA3618A8-03A9-4C2D-8901-417FE90F0D85}.Release|Any CPU.Build.0 = Release|Any CPU + {B96578E1-28D5-467C-A133-897DE1406A47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B96578E1-28D5-467C-A133-897DE1406A47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B96578E1-28D5-467C-A133-897DE1406A47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B96578E1-28D5-467C-A133-897DE1406A47}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index d476017..adb6f29 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -510,12 +510,7 @@ public async Task RemoveBadWord(string word) [HasAdmin] public async Task AddBadWord(string word, string euphemism = null, float size = 0.5f) { - BadWord badWord = new BadWord - { - Word = word, - Euphemism = euphemism, - Size = size - }; + BadWord badWord = new BadWord(word, euphemism, size); BadWordList badWordsClass = Context.Guild.LoadFromFile(true); badWordsClass.badWords.Add(badWord); badWordsClass.SaveToFile(); diff --git a/BotCatMaxy/Models/BadWord.cs b/BotCatMaxy/Models/BadWord.cs index b949740..96ddf23 100644 --- a/BotCatMaxy/Models/BadWord.cs +++ b/BotCatMaxy/Models/BadWord.cs @@ -8,13 +8,11 @@ namespace BotCatMaxy.Models { [BsonIgnoreExtraElements] - public record BadWord - { - public string Word { get; init; } - public string Euphemism { get; init; } - public float Size { get; init; } = 0.5f; - public bool PartOfWord { get; init; } = true; - } + public record BadWord( + string Word = null, + string Euphemism = null, + float Size = 0.5f, + bool PartOfWord = true); public class BadWordList : DataObject { diff --git a/Tests/FilterTests.cs b/Tests/FilterTests.cs new file mode 100644 index 0000000..40edde3 --- /dev/null +++ b/Tests/FilterTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BotCatMaxy.Components.Filter; +using BotCatMaxy; +using Xunit; +using BotCatMaxy.Models; + +namespace Tests +{ + public class FilterTests + { + public BadWord[] badWords = { new BadWord("Calzone"), new BadWord("Something") { PartOfWord = false }, new BadWord("Substitution") }; + + [Theory] + [InlineData("We like calzones", "calzone")] + [InlineData("Somethings is here", null)] + [InlineData("$ubst1tuti0n", "Substitution")] + public void BadWordTheory(string input, string expected) + { + var result = input.CheckForBadWords(badWords); + Assert.Equal(expected, result?.Word, ignoreCase: true); + } + } +} diff --git a/Tests/ParseTests.cs b/Tests/ParseTests.cs new file mode 100644 index 0000000..c0497e3 --- /dev/null +++ b/Tests/ParseTests.cs @@ -0,0 +1,23 @@ +using BotCatMaxy; +using System; +using Xunit; + +namespace Tests +{ + public class ParseTests + { + [Theory] + [InlineData("hey", null)] + [InlineData("30m", 30)] + [InlineData("1h", 60)] + [InlineData("30m1h", 90)] + [InlineData("1h30m", 90)] + public void TimeSpanTheory(string input, double? minutes) + { + double? result = null; + var time = input.ToTime(); + if (time.HasValue) result = time.Value.TotalMinutes; + Assert.Equal(minutes, result); + } + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj new file mode 100644 index 0000000..fe0344d --- /dev/null +++ b/Tests/Tests.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From 89130ce823d84f7a6030ccbb9ccc8b7cc719102b Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Thu, 21 Jan 2021 14:05:10 -0800 Subject: [PATCH 02/42] Updates packages and replaces old interactivity addon --- BotCatMaxy/BotCatMaxy.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BotCatMaxy/BotCatMaxy.csproj b/BotCatMaxy/BotCatMaxy.csproj index 0eab279..f91e780 100644 --- a/BotCatMaxy/BotCatMaxy.csproj +++ b/BotCatMaxy/BotCatMaxy.csproj @@ -20,11 +20,11 @@ - - - - + + + + @@ -33,7 +33,7 @@ - + From e71c330c374641210ee027e58571017df05b8e6d Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Thu, 21 Jan 2021 15:49:39 -0800 Subject: [PATCH 03/42] Makes the new Interactive package work and changes some prompts --- .../Components/Filter/FilterCommands.cs | 9 ++- .../Moderation/ModerationCommands.cs | 62 ++++++++++--------- BotCatMaxy/Components/Moderation/Settings.cs | 4 +- BotCatMaxy/Components/ReportModule.cs | 23 ++++--- BotCatMaxy/Startup/Command Handler.cs | 4 +- 5 files changed, 58 insertions(+), 44 deletions(-) diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index adb6f29..5529c8b 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -1,10 +1,10 @@ using BotCatMaxy.Data; using BotCatMaxy.Models; using Discord; -using Discord.Addons.Interactive; using Discord.Commands; using Discord.WebSocket; using System; +using Interactivity; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -16,8 +16,10 @@ namespace BotCatMaxy.Components.Filter [Group("automod")] [Summary("Manages the automoderator.")] [Alias("automod", "auto -mod", "filter")] - public class FilterCommands : InteractiveBase + public class FilterCommands : ModuleBase { + public InteractivityService Interactivity { get; set; } + [Command("list")] [Summary("View filter information.")] [Alias("info")] @@ -37,7 +39,8 @@ public async Task ListAutoMod(string extension = "") SocketGuild guild; while (true) { - SocketMessage reply = await NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); + var result = await Interactivity.NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); + var reply = result.Value; if (reply == null || reply.Content == "cancel") { await ReplyAsync("You have timed out or canceled"); diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index c195a9f..d040a41 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -4,7 +4,7 @@ using BotCatMaxy.Models; using BotCatMaxy.Moderation; using Discord; -using Discord.Addons.Interactive; +using Interactivity; using Discord.Commands; using Discord.WebSocket; using Humanizer; @@ -12,12 +12,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Interactivity.Confirmation; namespace BotCatMaxy { [Name("Moderation")] - public class ModerationCommands : InteractiveBase + public class ModerationCommands : ModuleBase { + public InteractivityService Interactivity { get; set; } + [RequireContext(ContextType.Guild)] [Command("warn")] [Summary("Warn a user with an option reason.")] @@ -87,7 +90,8 @@ public async Task DMUserWarnsAsync(UserRef userRef = null, int amount = 50) SocketGuild guild; while (true) { - SocketMessage message = await NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); + var result = await Interactivity.NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); + var message = result.Value; if (message == null || message.Content == "cancel") { await ReplyAsync("You have timed out or canceled"); @@ -220,20 +224,18 @@ public async Task TempBanUser([RequireHierarchy] UserRef userRef, TimeSpan time, await ReplyAsync($"{Context.User.Mention} please contact your admin(s) in order to shorten length of a punishment"); return; } - IUserMessage query = await ReplyAsync( - $"{userRef.Name(true)} is already temp-banned for {oldAct.Length.LimitedHumanize()} ({(oldAct.Length - (DateTime.UtcNow - oldAct.DateBanned)).LimitedHumanize()} left), reply with !confirm within 2 minutes to confirm you want to change the length"); - SocketMessage nextMessage = await NextMessageAsync(timeout: TimeSpan.FromMinutes(2)); - if (nextMessage?.Content?.ToLower() == "!confirm") + var text = $"{userRef.Name(true)} is already temp-banned for {oldAct.Length.LimitedHumanize()} ({(oldAct.Length - (DateTime.UtcNow - oldAct.DateBanned)).LimitedHumanize()} left), are you sure you want to change the length?"; + var request = new ConfirmationBuilder() + .WithContent(new PageBuilder().WithText(text)) + .Build(); + var result = await Interactivity.SendConfirmationAsync(request, Context.Channel, TimeSpan.FromMinutes(2)); + if (result.Value) { - _ = query.DeleteAsync(); - _ = nextMessage.DeleteAsync(); actions.tempBans.Remove(oldAct); actions.SaveToFile(); } else { - _ = query.DeleteAsync(); - if (nextMessage != null) _ = nextMessage.DeleteAsync(); await ReplyAsync("Command canceled"); return; } @@ -344,20 +346,18 @@ public async Task TempMuteUser([RequireHierarchy] UserRef userRef, TimeSpan time await ReplyAsync($"{Context.User.Mention} please contact your admin(s) in order to shorten length of a punishment"); return; } - IUserMessage query = await ReplyAsync( - $"{userRef.Name()} is already temp-muted for {oldAct.Length.LimitedHumanize()} ({(oldAct.Length - (DateTime.UtcNow - oldAct.DateBanned)).LimitedHumanize()} left), reply with !confirm within 2 minutes to confirm you want to change the length"); - SocketMessage nextMessage = await NextMessageAsync(timeout: TimeSpan.FromMinutes(2)); - if (nextMessage?.Content?.ToLower() == "!confirm") + string text = $"{userRef.Name()} is already temp-muted for {oldAct.Length.LimitedHumanize()} ({(oldAct.Length - (DateTime.UtcNow - oldAct.DateBanned)).LimitedHumanize()} left), are you sure you want to change the length?"; + var request = new ConfirmationBuilder() + .WithContent(new PageBuilder().WithText(text)) + .Build(); + var result = await Interactivity.SendConfirmationAsync(request, Context.Channel, TimeSpan.FromMinutes(2)); + if (result.Value) { - _ = query.DeleteAsync(); - _ = nextMessage.DeleteAsync(); actions.tempMutes.Remove(oldAct); actions.SaveToFile(); } else { - _ = query.DeleteAsync(); - if (nextMessage != null) _ = nextMessage.DeleteAsync(); await ReplyAsync("Command canceled"); return; } @@ -454,12 +454,14 @@ public async Task Ban([RequireHierarchy] UserRef userRef, [Remainder] string rea { if (reason.Split(' ').First().ToTime() != null) { - var query = await ReplyAsync("Are you sure you don't mean to use !tempban? Reply with !confirm if you want to override"); - var reply = await NextMessageAsync(timeout: TimeSpan.FromMinutes(2)); - if (reply?.Content?.ToLower() != "!confirm") + var query = "Are you sure you don't mean to use !tempban?"; + var request = new ConfirmationBuilder() + .WithContent(new PageBuilder().WithText(query)) + .Build(); + var result = await Interactivity.SendConfirmationAsync(request, Context.Channel, TimeSpan.FromMinutes(1)); + if (result.Value) { - Context.Channel.DeleteMessageAsync(query); - ReplyAsync("Command Canceled"); + await ReplyAsync("Command Canceled"); return; } } @@ -467,12 +469,14 @@ public async Task Ban([RequireHierarchy] UserRef userRef, [Remainder] string rea TempActionList actions = Context.Guild.LoadFromFile(false); if (actions?.tempBans?.Any(tempBan => tempBan.User == userRef.ID) ?? false) { - var query = await ReplyAsync("User is already tempbanned. Reply with !confirm if you want to override"); - var reply = await NextMessageAsync(timeout: TimeSpan.FromMinutes(2)); - if (reply?.Content?.ToLower() != "!confirm") + var query = "User is already tempbanned, are you use you want to ban?"; + var request = new ConfirmationBuilder() + .WithContent(new PageBuilder().WithText(query)) + .Build(); + var result = await Interactivity.SendConfirmationAsync(request, Context.Channel, TimeSpan.FromMinutes(2)); + if (result.Value) { - Context.Channel.DeleteMessageAsync(query); - ReplyAsync("Command Canceled"); + await ReplyAsync("Command Canceled"); return; } actions.tempBans.Remove(actions.tempBans.First(tempban => tempban.User == userRef.ID)); diff --git a/BotCatMaxy/Components/Moderation/Settings.cs b/BotCatMaxy/Components/Moderation/Settings.cs index 4f800c2..31e626d 100644 --- a/BotCatMaxy/Components/Moderation/Settings.cs +++ b/BotCatMaxy/Components/Moderation/Settings.cs @@ -1,7 +1,6 @@ using BotCatMaxy.Data; using BotCatMaxy.Models; using Discord; -using Discord.Addons.Interactive; using Discord.Commands; using Discord.WebSocket; using System; @@ -11,8 +10,9 @@ namespace BotCatMaxy { //I want to move away from vague files like settings since conflicts are annoying [Name("Settings")] - public class SettingsModule : InteractiveBase + public class SettingsModule : ModuleBase { + [Command("Settings Info")] [Summary("View settings.")] [RequireContext(ContextType.Guild)] diff --git a/BotCatMaxy/Components/ReportModule.cs b/BotCatMaxy/Components/ReportModule.cs index e9faa6e..4da05dc 100644 --- a/BotCatMaxy/Components/ReportModule.cs +++ b/BotCatMaxy/Components/ReportModule.cs @@ -2,17 +2,19 @@ using BotCatMaxy.Data; using BotCatMaxy.Models; using Discord; -using Discord.Addons.Interactive; using Discord.Commands; using Discord.WebSocket; +using Interactivity; using Humanizer; using System; using System.Linq; using System.Threading.Tasks; [Name("Report")] -public class ReportModule : InteractiveBase +public class ReportModule : ModuleBase { + public InteractivityService Interactivity { get; set; } + [Command("report", RunMode = RunMode.Async)] [Summary("Create a new report.")] [RequireContext(ContextType.DM)] @@ -31,7 +33,8 @@ public async Task Report() SocketGuild guild; while (true) { - SocketMessage message = await NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); + var result = await Interactivity.NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); + var message = result.Value; if (message == null || message.Content == "cancel") { await ReplyAsync("You have timed out or canceled"); @@ -89,18 +92,22 @@ public async Task Report() } await ReplyAsync("Please reply with what you want to report"); - var reportMsg = await NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); - if (reportMsg == null) ReplyAsync("Report aborted"); + var reportMsg = await Interactivity.NextMessageAsync(timeout: TimeSpan.FromMinutes(5)); + if (!reportMsg.IsSuccess) + { + await ReplyAsync("Report aborted"); + return; + } var embed = new EmbedBuilder(); embed.WithAuthor(Context.Message.Author); embed.WithTitle("Report"); - embed.WithDescription(reportMsg.Content); + embed.WithDescription(reportMsg.Value.Content); embed.WithFooter("User ID: " + Context.Message.Author.Id); embed.WithCurrentTimestamp(); string links = ""; - if (reportMsg.Attachments?.Count is not null or 0) - links = reportMsg.Attachments.Select(attachment => attachment.ProxyUrl).ListItems(" "); + if (reportMsg.Value.Attachments?.Count is not null or 0) + links = reportMsg.Value.Attachments.Select(attachment => attachment.ProxyUrl).ListItems(" "); var channel = guild.GetTextChannel(settings.channelID.Value); await channel.SendMessageAsync(embed: embed.Build()); if (!string.IsNullOrEmpty(links)) diff --git a/BotCatMaxy/Startup/Command Handler.cs b/BotCatMaxy/Startup/Command Handler.cs index 8ee134f..f77f308 100644 --- a/BotCatMaxy/Startup/Command Handler.cs +++ b/BotCatMaxy/Startup/Command Handler.cs @@ -2,7 +2,7 @@ using BotCatMaxy.Models; using BotCatMaxy.TypeReaders; using Discord; -using Discord.Addons.Interactive; +using Interactivity; using Discord.Commands; using Discord.Rest; using Discord.WebSocket; @@ -34,7 +34,7 @@ public CommandHandler(DiscordSocketClient client, CommandService commands) _client = client; services = new ServiceCollection() .AddSingleton(_client) - .AddSingleton(new InteractiveService(client)) + .AddSingleton(new InteractivityService(client, TimeSpan.FromMinutes(3))) .BuildServiceProvider(); _ = InstallCommandsAsync(); } From b38ac1b0021d20fdf4a78a0712547f580447c2cb Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Fri, 22 Jan 2021 17:59:08 -0800 Subject: [PATCH 04/42] Starts mockings Discord.NET types for unit testing --- Tests/Mocks/MockMessage.cs | 103 ++++++++++++++++++++++++++++++ Tests/Mocks/MockMessageChannel.cs | 102 +++++++++++++++++++++++++++++ Tests/Mocks/UserMockMessage.cs | 46 +++++++++++++ Tests/MocksTest.cs | 27 ++++++++ 4 files changed, 278 insertions(+) create mode 100644 Tests/Mocks/MockMessage.cs create mode 100644 Tests/Mocks/MockMessageChannel.cs create mode 100644 Tests/Mocks/UserMockMessage.cs create mode 100644 Tests/MocksTest.cs diff --git a/Tests/Mocks/MockMessage.cs b/Tests/Mocks/MockMessage.cs new file mode 100644 index 0000000..0d34d09 --- /dev/null +++ b/Tests/Mocks/MockMessage.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; + +namespace Tests.Mocks +{ + public class MockMessage : IMessage + { + public MockMessage(string content, IMessageChannel channel) + { + Content = content; + Channel = channel; + var random = new Random(); + Id = (ulong)random.Next(0, int.MaxValue); + } + + public MessageType Type => MessageType.Default; + + public MessageSource Source => MessageSource.User; + + public bool IsTTS => throw new NotImplementedException(); + + public bool IsPinned => throw new NotImplementedException(); + + public bool IsSuppressed => throw new NotImplementedException(); + + public bool MentionedEveryone => throw new NotImplementedException(); + + public string Content { get; init; } + + public DateTimeOffset Timestamp => throw new NotImplementedException(); + + public DateTimeOffset? EditedTimestamp => throw new NotImplementedException(); + + public IMessageChannel Channel { get; init; } + + public IUser Author => throw new NotImplementedException(); + + public IReadOnlyCollection Attachments => throw new NotImplementedException(); + + public IReadOnlyCollection Embeds => throw new NotImplementedException(); + + public IReadOnlyCollection Tags => throw new NotImplementedException(); + + public IReadOnlyCollection MentionedChannelIds => throw new NotImplementedException(); + + public IReadOnlyCollection MentionedRoleIds => throw new NotImplementedException(); + + public IReadOnlyCollection MentionedUserIds => throw new NotImplementedException(); + + public MessageActivity Activity => throw new NotImplementedException(); + + public MessageApplication Application => throw new NotImplementedException(); + + public MessageReference Reference => throw new NotImplementedException(); + + public IReadOnlyDictionary Reactions => throw new NotImplementedException(); + + public MessageFlags? Flags => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id { get; init; } + + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemoveAllReactionsAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Mocks/MockMessageChannel.cs b/Tests/Mocks/MockMessageChannel.cs new file mode 100644 index 0000000..fef79a6 --- /dev/null +++ b/Tests/Mocks/MockMessageChannel.cs @@ -0,0 +1,102 @@ +using Discord; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Linq; + +namespace Tests.Mocks +{ + public class MockMessageChannel : IMessageChannel + { + public MockMessageChannel() + { + var random = new Random(); + Id = (ulong)random.Next(0, int.MaxValue); + } + + public string Name => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id { get; init; } + + public List messages = new(8); + + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + int index = messages.FindIndex(message => message.Id == messageId); + messages.RemoveAt(index); + return Task.CompletedTask; + } + + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + int index = messages.FindIndex(msg => message.Id == msg.Id); + messages.RemoveAt(index); + return Task.CompletedTask; + } + + public IDisposable EnterTypingState(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + return Task.FromResult(messages.FirstOrDefault(message => message.Id == id) as IMessage); + } + + public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null) + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null) + { + throw new NotImplementedException(); + } + + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null) + { + var message = new UserMockMessage(text, this); + messages.Add(message); + return Task.FromResult(message as IUserMessage); + } + + public Task TriggerTypingAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Mocks/UserMockMessage.cs b/Tests/Mocks/UserMockMessage.cs new file mode 100644 index 0000000..f9e15e2 --- /dev/null +++ b/Tests/Mocks/UserMockMessage.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; + +namespace Tests.Mocks +{ + public class UserMockMessage : MockMessage, IUserMessage + { + public UserMockMessage(string content, IMessageChannel channel) : base(content, channel) { } + + public IUserMessage ReferencedMessage => throw new NotImplementedException(); + + public Task CrosspostAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task PinAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + { + throw new NotImplementedException(); + } + + public Task UnpinAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/MocksTest.cs b/Tests/MocksTest.cs new file mode 100644 index 0000000..2271a5d --- /dev/null +++ b/Tests/MocksTest.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using BotCatMaxy.Models; +using Tests.Mocks; + +namespace Tests +{ + public class MocksTest + { + [Fact] + public async Task MessageTest() + { + var channel = new MockMessageChannel(); + var message = await channel.SendMessageAsync("Test"); + Assert.Equal("Test", message.Content); + message = await channel.GetMessageAsync(message.Id) as UserMockMessage; + Assert.Equal("Test", message?.Content); + await channel.DeleteMessageAsync(message.Id); + message = await channel.GetMessageAsync(message.Id) as UserMockMessage; + Assert.Null(message); + } + } +} From 450a782642441ebc07c2a7d52c5a1747df5ee660 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sat, 23 Jan 2021 21:56:20 -0800 Subject: [PATCH 05/42] More work on mocking channels --- Tests/Mocks/MockMessageChannel.cs | 21 ++++++++++++++------ Tests/MocksTest.cs | 33 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/Tests/Mocks/MockMessageChannel.cs b/Tests/Mocks/MockMessageChannel.cs index fef79a6..39df76a 100644 --- a/Tests/Mocks/MockMessageChannel.cs +++ b/Tests/Mocks/MockMessageChannel.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading.Tasks; using System.Linq; +using System.Collections.ObjectModel; namespace Tests.Mocks { @@ -49,18 +50,26 @@ public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.Allow public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { - throw new NotImplementedException(); + if (limit > messages.Count) limit = messages.Count; + var range = messages.GetRange(0, limit).Select(message => (IMessage)message).ToList(); + IReadOnlyCollection[] collections = { new ReadOnlyCollection(range) }; + return collections.ToAsyncEnumerable(); } public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { - throw new NotImplementedException(); + if (dir is Direction.Around or Direction.Before) throw new NotImplementedException(); + if (!messages.Any(message => message.Id == fromMessageId)) return null; + var index = messages.FindIndex(message => message.Id == fromMessageId); + if (limit > messages.Count) limit = messages.Count - index; + + var range = messages.GetRange(index, limit).Select(message => (IMessage)message).ToList(); + IReadOnlyCollection[] collections = { new ReadOnlyCollection(range) }; + return collections.ToAsyncEnumerable(); } public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } + => GetMessagesAsync(fromMessage.Id, dir, limit, mode, options); public Task> GetPinnedMessagesAsync(RequestOptions options = null) { @@ -90,7 +99,7 @@ public Task SendFileAsync(Stream stream, string filename, string t public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null) { var message = new UserMockMessage(text, this); - messages.Add(message); + messages.Insert(0, message); return Task.FromResult(message as IUserMessage); } diff --git a/Tests/MocksTest.cs b/Tests/MocksTest.cs index 2271a5d..bb5f913 100644 --- a/Tests/MocksTest.cs +++ b/Tests/MocksTest.cs @@ -23,5 +23,38 @@ public async Task MessageTest() message = await channel.GetMessageAsync(message.Id) as UserMockMessage; Assert.Null(message); } + + [Fact] + public async Task MultipleMessageTest() + { + var channel = new MockMessageChannel(); + var message1 = await channel.SendMessageAsync("Test1"); + var message2 = await channel.SendMessageAsync("Test2"); + var messages = await channel.GetMessagesAsync(2).ToArrayAsync(); + var strings = messages[0].Select(message => message.Content); + CheckMessages(strings); + messages = await channel.GetMessagesAsync(message1.Id, Discord.Direction.After).ToArrayAsync(); + CheckMessages(strings); + messages = await channel.GetMessagesAsync(message1, Discord.Direction.After).ToArrayAsync(); + CheckMessages(strings); + messages = await channel.GetMessagesAsync(message2.Id, Discord.Direction.After).ToArrayAsync(); + CheckMessages(strings); + + } + + internal void CheckMessages(IEnumerable messages, bool containsFirst = true) + { + if (containsFirst) + { + Assert.Equal(2, messages.Count()); + Assert.Contains("Test1", messages); + } + else + { + Assert.Single(messages); + Assert.DoesNotContain("Test1", messages); + } + Assert.Contains("Test2", messages); + } } } From 41fead49d6ee42a64717efae5aa148cdc852df8a Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sun, 24 Jan 2021 14:17:59 -0800 Subject: [PATCH 06/42] Mocks basic guild functionality --- Tests/Mocks/Guild/MockBan.cs | 22 ++ Tests/Mocks/Guild/MockGuild.cs | 396 +++++++++++++++++++++++++++ Tests/Mocks/Guild/MockGuildUser.cs | 86 ++++++ Tests/Mocks/Guild/MockTextChannel.cs | 136 +++++++++ Tests/Mocks/MockMessage.cs | 9 +- Tests/Mocks/MockMessageChannel.cs | 2 +- Tests/Mocks/MockUser.cs | 69 +++++ Tests/Mocks/UserMockMessage.cs | 2 +- Tests/MocksTest.cs | 24 +- 9 files changed, 737 insertions(+), 9 deletions(-) create mode 100644 Tests/Mocks/Guild/MockBan.cs create mode 100644 Tests/Mocks/Guild/MockGuild.cs create mode 100644 Tests/Mocks/Guild/MockGuildUser.cs create mode 100644 Tests/Mocks/Guild/MockTextChannel.cs create mode 100644 Tests/Mocks/MockUser.cs diff --git a/Tests/Mocks/Guild/MockBan.cs b/Tests/Mocks/Guild/MockBan.cs new file mode 100644 index 0000000..984b421 --- /dev/null +++ b/Tests/Mocks/Guild/MockBan.cs @@ -0,0 +1,22 @@ +using Discord; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tests.Mocks.Guild +{ + public class MockBan : IBan + { + public MockBan(IUser user, string reason) + { + User = user; + Reason = reason; + } + + public IUser User { get; init; } + + public string Reason { get; init; } + } +} diff --git a/Tests/Mocks/Guild/MockGuild.cs b/Tests/Mocks/Guild/MockGuild.cs new file mode 100644 index 0000000..e1d8386 --- /dev/null +++ b/Tests/Mocks/Guild/MockGuild.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; +using Discord.Audio; + +namespace Tests.Mocks.Guild +{ + public class MockGuild : IGuild + { + public MockGuild() + { + var random = new Random(); + Id = (ulong)random.Next(0, int.MaxValue); + } + + protected List userList = new(8); + protected List channels = new(4); + protected List bans = new(4); + + public string Name => throw new NotImplementedException(); + + public int AFKTimeout => throw new NotImplementedException(); + + public bool IsEmbeddable => throw new NotImplementedException(); + + public bool IsWidgetEnabled => throw new NotImplementedException(); + + public DefaultMessageNotifications DefaultMessageNotifications => throw new NotImplementedException(); + + public MfaLevel MfaLevel => throw new NotImplementedException(); + + public VerificationLevel VerificationLevel => throw new NotImplementedException(); + + public ExplicitContentFilterLevel ExplicitContentFilter => throw new NotImplementedException(); + + public string IconId => throw new NotImplementedException(); + + public string IconUrl => throw new NotImplementedException(); + + public string SplashId => throw new NotImplementedException(); + + public string SplashUrl => throw new NotImplementedException(); + + public string DiscoverySplashId => throw new NotImplementedException(); + + public string DiscoverySplashUrl => throw new NotImplementedException(); + + public bool Available => true; + + public ulong? AFKChannelId => throw new NotImplementedException(); + + public ulong DefaultChannelId => throw new NotImplementedException(); + + public ulong? EmbedChannelId => throw new NotImplementedException(); + + public ulong? WidgetChannelId => throw new NotImplementedException(); + + public ulong? SystemChannelId => throw new NotImplementedException(); + + public ulong? RulesChannelId => throw new NotImplementedException(); + + public ulong? PublicUpdatesChannelId => throw new NotImplementedException(); + + public ulong OwnerId { get; internal set; } + + public ulong? ApplicationId => throw new NotImplementedException(); + + public string VoiceRegionId => throw new NotImplementedException(); + + public IAudioClient AudioClient => throw new NotImplementedException(); + + public IRole EveryoneRole => throw new NotImplementedException(); + + public IReadOnlyCollection Emotes => throw new NotImplementedException(); + + public IReadOnlyCollection Features => throw new NotImplementedException(); + + public IReadOnlyCollection Roles => throw new NotImplementedException(); + + public PremiumTier PremiumTier => throw new NotImplementedException(); + + public string BannerId => throw new NotImplementedException(); + + public string BannerUrl => throw new NotImplementedException(); + + public string VanityURLCode => throw new NotImplementedException(); + + public SystemChannelMessageDeny SystemChannelFlags => throw new NotImplementedException(); + + public string Description => throw new NotImplementedException(); + + public int PremiumSubscriptionCount => throw new NotImplementedException(); + + public int? MaxPresences => throw new NotImplementedException(); + + public int? MaxMembers => throw new NotImplementedException(); + + public int? MaxVideoChannelUsers => throw new NotImplementedException(); + + public int? ApproximateMemberCount => userList.Count; + + public int? ApproximatePresenceCount => throw new NotImplementedException(); + + public string PreferredLocale => throw new NotImplementedException(); + + public CultureInfo PreferredCulture => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id { get; set; } + + public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + { + if (user is not MockGuildUser gUser) return null; + var ban = new MockBan(user, reason); + bans.Add(ban); + userList.Remove(gUser); + return Task.CompletedTask; + } + + public async Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) + { + var user = await GetUserAsync(userId); + await AddBanAsync(user, 0, reason, options); + } + + public Task AddGuildUserAsync(ulong userId, string accessToken, Action func = null, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateCategoryAsync(string name, Action func = null, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateEmoteAsync(string name, Image image, Optional> roles = default, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) + { + var channel = new MockTextChannel(this); + channels.Add(channel); + return Task.FromResult(channel as ITextChannel); + } + + public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DownloadUsersAsync() + { + userList = new(4); + userList.Add(new MockGuildUser("Bot", this, true)); + var owner = new MockGuildUser("Owner", this); + OwnerId = owner.Id; + userList.Add(owner); + userList.Add(new MockGuildUser("Tester", this)); + userList.Add(new MockGuildUser("Testee", this)); + return Task.CompletedTask; + } + + public Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetAuditLogsAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, ActionType? actionType = null) + { + throw new NotImplementedException(); + } + + public Task GetBanAsync(IUser user, RequestOptions options = null) + => Task.FromResult(bans.FirstOrDefault(ban => ban.User.Id == user.Id) as IBan); + + public Task GetBanAsync(ulong userId, RequestOptions options = null) + => Task.FromResult(bans.FirstOrDefault(ban => ban.User.Id == userId) as IBan); + + public Task> GetBansAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + return Task.FromResult(channels.FirstOrDefault(channel => channel.Id == id) as IGuildChannel); + } + + public Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + => Task.FromResult(userList.First(user => user.IsBot) as IGuildUser); + + public Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetEmoteAsync(ulong id, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetIntegrationsAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetInvitesAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetPublicUpdatesChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IRole GetRole(ulong id) + { + throw new NotImplementedException(); + } + + public Task GetRulesChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + => Task.FromResult(channels.FirstOrDefault(channel => channel.Id == id) as ITextChannel); + + public Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + => Task.FromResult(userList.FirstOrDefault(user => user.Id == id) as IGuildUser); + + public Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetVanityInviteAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetWebhooksAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetWidgetChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task LeaveAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyEmbedAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyWidgetAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable includeRoleIds = null) + { + throw new NotImplementedException(); + } + + public Task RemoveBanAsync(IUser user, RequestOptions options = null) + => RemoveBanAsync(user.Id, options); + + public Task RemoveBanAsync(ulong userId, RequestOptions options = null) + { + var index = bans.FindIndex(ban => ban.User.Id == userId); + bans.RemoveAt(index); + return Task.CompletedTask; + } + + public Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> SearchUsersAsync(string query, int limit = 1000, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public ulong AddUser(MockGuildUser user) + { + userList.Add(user); + return user.Id; + } + } +} diff --git a/Tests/Mocks/Guild/MockGuildUser.cs b/Tests/Mocks/Guild/MockGuildUser.cs new file mode 100644 index 0000000..099422d --- /dev/null +++ b/Tests/Mocks/Guild/MockGuildUser.cs @@ -0,0 +1,86 @@ +using Discord; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tests.Mocks.Guild +{ + public class MockGuildUser : MockUser, IGuildUser + { + public MockGuildUser(string username, IGuild guild, bool isSelf = false) : base(username, isSelf) + { + Guild = guild; + } + + public DateTimeOffset? JoinedAt => throw new NotImplementedException(); + + public string Nickname => throw new NotImplementedException(); + + public GuildPermissions GuildPermissions => throw new NotImplementedException(); + + public IGuild Guild { get; set; } + + public ulong GuildId => Guild.Id; + + public DateTimeOffset? PremiumSince => throw new NotImplementedException(); + + public IReadOnlyCollection RoleIds => throw new NotImplementedException(); + + public bool? IsPending => throw new NotImplementedException(); + + public bool IsDeafened => throw new NotImplementedException(); + + public bool IsMuted => throw new NotImplementedException(); + + public bool IsSelfDeafened => throw new NotImplementedException(); + + public bool IsSelfMuted => throw new NotImplementedException(); + + public bool IsSuppressed => throw new NotImplementedException(); + + public IVoiceChannel VoiceChannel => throw new NotImplementedException(); + + public string VoiceSessionId => throw new NotImplementedException(); + + public bool IsStreaming => throw new NotImplementedException(); + + public Task AddRoleAsync(IRole role, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public ChannelPermissions GetPermissions(IGuildChannel channel) + { + throw new NotImplementedException(); + } + + public Task KickAsync(string reason = null, RequestOptions options = null) + { + Guild.AddBanAsync(this, reason: reason, options: options); + Guild.RemoveBanAsync(this, options); + return Task.CompletedTask; + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Mocks/Guild/MockTextChannel.cs b/Tests/Mocks/Guild/MockTextChannel.cs new file mode 100644 index 0000000..9b4c3ec --- /dev/null +++ b/Tests/Mocks/Guild/MockTextChannel.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; + +namespace Tests.Mocks.Guild +{ + public class MockTextChannel : MockMessageChannel, ITextChannel + { + public MockTextChannel(IGuild guild) : base() + { + Guild = guild; + } + + public bool IsNsfw => throw new NotImplementedException(); + + public string Topic => throw new NotImplementedException(); + + public int SlowModeInterval => throw new NotImplementedException(); + + public string Mention => throw new NotImplementedException(); + + public ulong? CategoryId => null; + + public int Position => throw new NotImplementedException(); + + public IGuild Guild { get; init; } + + public ulong GuildId => Guild.Id; + + public IReadOnlyCollection PermissionOverwrites => throw new NotImplementedException(); + + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetInvitesAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public OverwritePermissions? GetPermissionOverwrite(IRole role) + { + throw new NotImplementedException(); + } + + public OverwritePermissions? GetPermissionOverwrite(IUser user) + { + throw new NotImplementedException(); + } + + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetWebhooksAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SyncPermissionsAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotImplementedException(); + } + + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Mocks/MockMessage.cs b/Tests/Mocks/MockMessage.cs index 0d34d09..7fb9102 100644 --- a/Tests/Mocks/MockMessage.cs +++ b/Tests/Mocks/MockMessage.cs @@ -9,10 +9,11 @@ namespace Tests.Mocks { public class MockMessage : IMessage { - public MockMessage(string content, IMessageChannel channel) + public MockMessage(string content, IMessageChannel channel, IUser author) { Content = content; Channel = channel; + Author = author; var random = new Random(); Id = (ulong)random.Next(0, int.MaxValue); } @@ -37,7 +38,7 @@ public MockMessage(string content, IMessageChannel channel) public IMessageChannel Channel { get; init; } - public IUser Author => throw new NotImplementedException(); + public IUser Author { get; init; } public IReadOnlyCollection Attachments => throw new NotImplementedException(); @@ -71,9 +72,7 @@ public Task AddReactionAsync(IEmote emote, RequestOptions options = null) } public Task DeleteAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } + => Channel.DeleteMessageAsync(Id, options); public IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null) { diff --git a/Tests/Mocks/MockMessageChannel.cs b/Tests/Mocks/MockMessageChannel.cs index 39df76a..0e7b6e9 100644 --- a/Tests/Mocks/MockMessageChannel.cs +++ b/Tests/Mocks/MockMessageChannel.cs @@ -98,7 +98,7 @@ public Task SendFileAsync(Stream stream, string filename, string t public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null) { - var message = new UserMockMessage(text, this); + var message = new UserMockMessage(text, this, null); messages.Insert(0, message); return Task.FromResult(message as IUserMessage); } diff --git a/Tests/Mocks/MockUser.cs b/Tests/Mocks/MockUser.cs new file mode 100644 index 0000000..29811a2 --- /dev/null +++ b/Tests/Mocks/MockUser.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; + +namespace Tests.Mocks +{ + public class MockUser : IUser + { + public MockUser(string username, bool isSelf = false) + { + IsBot = isSelf; + if (isSelf) + { + Username = "BotCatMaxy"; + } + else + Username = username; + var random = new Random(); + Id = (ulong)random.Next(0, int.MaxValue); + } + + public string AvatarId => throw new NotImplementedException(); + + public string Discriminator => $"#{DiscriminatorValue}"; + + public ushort DiscriminatorValue => 1234; + + public bool IsBot { get; init; } + + public bool IsWebhook => false; + + public string Username { get; init; } + + public UserProperties? PublicFlags => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id { get; init; } + + public string Mention => $"@{Username}"; + + public IActivity Activity => throw new NotImplementedException(); + + public UserStatus Status => throw new NotImplementedException(); + + public IImmutableSet ActiveClients => throw new NotImplementedException(); + + public IImmutableList Activities => throw new NotImplementedException(); + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + { + throw new NotImplementedException(); + } + + public string GetDefaultAvatarUrl() + { + throw new NotImplementedException(); + } + + public Task GetOrCreateDMChannelAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Mocks/UserMockMessage.cs b/Tests/Mocks/UserMockMessage.cs index f9e15e2..9e7baf6 100644 --- a/Tests/Mocks/UserMockMessage.cs +++ b/Tests/Mocks/UserMockMessage.cs @@ -9,7 +9,7 @@ namespace Tests.Mocks { public class UserMockMessage : MockMessage, IUserMessage { - public UserMockMessage(string content, IMessageChannel channel) : base(content, channel) { } + public UserMockMessage(string content, IMessageChannel channel, IUser auther) : base(content, channel, auther) { } public IUserMessage ReferencedMessage => throw new NotImplementedException(); diff --git a/Tests/MocksTest.cs b/Tests/MocksTest.cs index bb5f913..333fcb3 100644 --- a/Tests/MocksTest.cs +++ b/Tests/MocksTest.cs @@ -6,15 +6,20 @@ using Xunit; using BotCatMaxy.Models; using Tests.Mocks; +using Tests.Mocks.Guild; +using Discord; namespace Tests { public class MocksTest { [Fact] - public async Task MessageTest() + public async Task GuildTest() { - var channel = new MockMessageChannel(); + var guild = new MockGuild(); + var channel = await guild.CreateTextChannelAsync("Channel"); + //Messages + Assert.Equal(channel.Id, (await guild.GetTextChannelAsync(channel.Id))?.Id); var message = await channel.SendMessageAsync("Test"); Assert.Equal("Test", message.Content); message = await channel.GetMessageAsync(message.Id) as UserMockMessage; @@ -22,6 +27,21 @@ public async Task MessageTest() await channel.DeleteMessageAsync(message.Id); message = await channel.GetMessageAsync(message.Id) as UserMockMessage; Assert.Null(message); + + //Users + await guild.DownloadUsersAsync(); + var userID = guild.AddUser(new MockGuildUser("Someone", guild)); + var users = guild.ApproximateMemberCount; + + var user = await guild.GetUserAsync(userID); + Assert.Equal(userID, user.Id); + await user.KickAsync("Test"); + Assert.Null(await guild.GetBanAsync(user)); + Assert.NotEqual(users, guild.ApproximateMemberCount); + + guild.AddUser(user as MockGuildUser); + await guild.AddBanAsync(user, reason: "Test"); + Assert.NotNull(await guild.GetBanAsync(user)); } [Fact] From 9d8d5b16c85d6354abe34271e16e8f40553d6808 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sat, 30 Jan 2021 13:09:35 -0800 Subject: [PATCH 07/42] Adds data unit testing --- BotCatMaxy/Startup/Main.cs | 2 +- Tests/DataTests.cs | 85 ++++++++++++++++++++++++++++++++++++++ Tests/Tests.csproj | 1 + 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 Tests/DataTests.cs diff --git a/BotCatMaxy/Startup/Main.cs b/BotCatMaxy/Startup/Main.cs index 0d5cd53..3b8229a 100644 --- a/BotCatMaxy/Startup/Main.cs +++ b/BotCatMaxy/Startup/Main.cs @@ -21,7 +21,7 @@ namespace BotCatMaxy public class MainClass { private static DiscordSocketClient _client; - public static MongoClient dbClient; + public static IMongoClient dbClient; public static async Task Main(string[] args) { var baseDir = AppDomain.CurrentDomain.BaseDirectory; diff --git a/Tests/DataTests.cs b/Tests/DataTests.cs new file mode 100644 index 0000000..a2259d8 --- /dev/null +++ b/Tests/DataTests.cs @@ -0,0 +1,85 @@ +using BotCatMaxy; +using BotCatMaxy.Data; +using BotCatMaxy.Models; +using Mongo2Go; +using MongoDB.Bson; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tests.Mocks.Guild; +using Xunit; + +namespace Tests +{ + public class DataTests : IDisposable + { + protected MongoDbRunner runner; + protected MongoCollectionBase collection; + protected IMongoDatabase settings; + + public DataTests() + { + runner = MongoDbRunner.Start(); + + MongoClient client = new MongoClient(runner.ConnectionString); + MainClass.dbClient = client; + settings = client.GetDatabase("Settings"); + var database = client.GetDatabase("IntegrationTest"); + collection = (MongoCollectionBase)database.GetCollection("TestCollection"); + } + + public void Dispose() + { + runner.Dispose(); + } + + [Fact] + public void TestBasic() + { + Assert.False(collection.FindSync((Builders.Filter.Eq("_id", 1234))).Any()); + var infractions = new UserInfractions() + { + ID = 1234, + infractions = new List + { + new Infraction() { Reason = "Test", Time = DateTime.UtcNow } + } + }; + collection.InsertOne(infractions.ToBsonDocument()); + + var document = collection.FindSync((Builders.Filter.Eq("_id", 1234))).FirstOrDefault(); + Assert.NotNull(document); + Assert.False(collection.FindSync((Builders.Filter.Eq("_id", 123))).Any()); + } + + [Fact] + public void TestCollections() + { + var guild = new MockGuild(); + Assert.Null(guild.GetCollection(false)); + var collection = guild.GetCollection(true); + Assert.NotNull(collection); + Assert.Equal(guild.Id.ToString(), collection.CollectionNamespace.CollectionName); + + var ownerCollection = settings.GetCollection(guild.OwnerId.ToString()); + ownerCollection.InsertOne(new BsonDocument()); + collection = guild.GetCollection(false); + Assert.NotNull(collection); + Assert.Equal(guild.OwnerId.ToString(), collection.CollectionNamespace.CollectionName); + } + + [Fact] + public void TestInfractions() + { + var guild = new MockGuild(); + ulong userID = 12345; + var infractions = userID.LoadInfractions(guild, false); + Assert.Null(infractions); + infractions = userID.LoadInfractions(guild, true); + Assert.NotNull(infractions); + } + } +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index fe0344d..a10dcbf 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -8,6 +8,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive From 7cb2f811b6ffe4e25ce34c0ac420c43f3f9e8ce0 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sun, 7 Feb 2021 14:59:23 -0800 Subject: [PATCH 08/42] Implements basic command testing --- .../Components/Filter/FilterCommands.cs | 3 + .../Moderation/ModerationCommands.cs | 3 + BotCatMaxy/Components/Moderation/Settings.cs | 4 +- BotCatMaxy/Components/ReportModule.cs | 3 + BotCatMaxy/Startup/Command Handler.cs | 34 +++-- BotCatMaxy/Utilities/PermissionUtilities.cs | 12 +- Tests/CommandTests.cs | 57 ++++++++ Tests/Mocks/Guild/MockGuild.cs | 10 +- Tests/Mocks/Guild/MockGuildUser.cs | 3 +- Tests/Mocks/Guild/MockTextChannel.cs | 4 +- Tests/Mocks/MockCommandContext.cs | 33 +++++ Tests/Mocks/MockDiscordClient.cs | 132 ++++++++++++++++++ Tests/Mocks/MockMessageChannel.cs | 20 ++- Tests/Mocks/MockSelfUser.cs | 34 +++++ Tests/Mocks/MockUser.cs | 11 +- ...{UserMockMessage.cs => MockUserMessage.cs} | 4 +- Tests/MocksTest.cs | 6 +- Tests/Tests.csproj | 6 + 18 files changed, 332 insertions(+), 47 deletions(-) create mode 100644 Tests/CommandTests.cs create mode 100644 Tests/Mocks/MockCommandContext.cs create mode 100644 Tests/Mocks/MockDiscordClient.cs create mode 100644 Tests/Mocks/MockSelfUser.cs rename Tests/Mocks/{UserMockMessage.cs => MockUserMessage.cs} (91%) diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index 5529c8b..d03b4f6 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -18,6 +18,9 @@ namespace BotCatMaxy.Components.Filter [Alias("automod", "auto -mod", "filter")] public class FilterCommands : ModuleBase { +#if !TEST + [DontInject] +#endif public InteractivityService Interactivity { get; set; } [Command("list")] diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index d040a41..e8afbc6 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -19,6 +19,9 @@ namespace BotCatMaxy [Name("Moderation")] public class ModerationCommands : ModuleBase { +#if !TEST + [DontInject] +#endif public InteractivityService Interactivity { get; set; } [RequireContext(ContextType.Guild)] diff --git a/BotCatMaxy/Components/Moderation/Settings.cs b/BotCatMaxy/Components/Moderation/Settings.cs index 31e626d..0306176 100644 --- a/BotCatMaxy/Components/Moderation/Settings.cs +++ b/BotCatMaxy/Components/Moderation/Settings.cs @@ -10,7 +10,7 @@ namespace BotCatMaxy { //I want to move away from vague files like settings since conflicts are annoying [Name("Settings")] - public class SettingsModule : ModuleBase + public class SettingsModule : ModuleBase { [Command("Settings Info")] @@ -36,7 +36,7 @@ public async Task SettingsInfo() await ReplyAsync(embed: embed.Build()); } - [Command("toggleserverstorage", RunMode = RunMode.Async)] + [Command("toggleserverstorage")] [Summary("Legacy feature. Run for instruction on how to enable.")] [HasAdmin] public async Task ToggleServerIDUse() diff --git a/BotCatMaxy/Components/ReportModule.cs b/BotCatMaxy/Components/ReportModule.cs index 4da05dc..554ec94 100644 --- a/BotCatMaxy/Components/ReportModule.cs +++ b/BotCatMaxy/Components/ReportModule.cs @@ -13,6 +13,9 @@ [Name("Report")] public class ReportModule : ModuleBase { +#if !TEST + [DontInject] +#endif public InteractivityService Interactivity { get; set; } [Command("report", RunMode = RunMode.Async)] diff --git a/BotCatMaxy/Startup/Command Handler.cs b/BotCatMaxy/Startup/Command Handler.cs index f77f308..66158f1 100644 --- a/BotCatMaxy/Startup/Command Handler.cs +++ b/BotCatMaxy/Startup/Command Handler.cs @@ -24,27 +24,29 @@ public class CommandHandler "User requires guild permission KickMembers.", "Bot requires guild permission ManageRoles.", "Command can only be run by the owner of the bot.", "You don't have the permissions to use this.", "User requires channel permission ManageMessages.", "Failed to parse UInt32." };*/ - private readonly DiscordSocketClient _client; + private readonly IDiscordClient _client; private readonly CommandService _commands; public readonly IServiceProvider services; - public CommandHandler(DiscordSocketClient client, CommandService commands) + public CommandHandler(IDiscordClient client, CommandService commands) { _commands = commands; _client = client; - services = new ServiceCollection() - .AddSingleton(_client) - .AddSingleton(new InteractivityService(client, TimeSpan.FromMinutes(3))) - .BuildServiceProvider(); + var serviceBuilder = new ServiceCollection() + .AddSingleton(_client); + if (client is DiscordSocketClient socketClient) + serviceBuilder.AddSingleton(new InteractivityService(socketClient, TimeSpan.FromMinutes(3))); + services = serviceBuilder.BuildServiceProvider(); _ = InstallCommandsAsync(); } - public async Task InstallCommandsAsync() + private async Task InstallCommandsAsync() { try { - // Hook the MessageReceived event into our command handler - _client.MessageReceived += HandleCommandAsync; + if (_client is DiscordSocketClient socketClient) + // Hook the MessageReceived event into our command handler + socketClient.MessageReceived += HandleCommandAsync; //Exception and Post Execution handling _commands.Log += ExceptionLogging.Log; @@ -57,7 +59,7 @@ public async Task InstallCommandsAsync() _commands.AddTypeReader(typeof(TimeSpan), new TimeSpanTypeReader(), true); // See Dependency Injection guide for more information. - await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(), + await _commands.AddModulesAsync(assembly: Assembly.GetAssembly(typeof(MainClass)), services: services); await new LogMessage(LogSeverity.Info, "CMDs", "Commands set up").Log(); } @@ -68,10 +70,12 @@ await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(), } private async Task HandleCommandAsync(SocketMessage messageParam) + => await ExecuteCommand(messageParam); + + public async Task ExecuteCommand(IMessage messageParam, ICommandContext context = null) { // Don't process the command if it was a system message - SocketUserMessage message = messageParam as SocketUserMessage; - if (message == null) + if (messageParam is not IUserMessage message) return; // Create a number to track where the prefix ends and the command begins @@ -83,8 +87,8 @@ private async Task HandleCommandAsync(SocketMessage messageParam) message.Author.IsBot) return; - // Create a WebSocket-based command context based on the message - var context = new SocketCommandContext(_client, message); + // Create a WebSocket-based command context based on the message and assume if no mock context then use Socket + context ??= new SocketCommandContext((DiscordSocketClient)_client, (SocketUserMessage)message); //var res = filter.CheckMessage(message, context); @@ -171,7 +175,7 @@ public class HasAdminAttribute : PreconditionAttribute public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { //Makes sure it's in a server - if (context.User is SocketGuildUser gUser) + if (context.User is IGuildUser gUser) { // If this command was executed by a user with administrator permission, return a success if (gUser.HasAdmin()) diff --git a/BotCatMaxy/Utilities/PermissionUtilities.cs b/BotCatMaxy/Utilities/PermissionUtilities.cs index 1d0dbea..4fabfab 100644 --- a/BotCatMaxy/Utilities/PermissionUtilities.cs +++ b/BotCatMaxy/Utilities/PermissionUtilities.cs @@ -1,5 +1,6 @@ using BotCatMaxy.Data; using BotCatMaxy.Models; +using Discord; using Discord.WebSocket; using System; using System.Collections.Generic; @@ -11,20 +12,19 @@ namespace BotCatMaxy { public static class PermissionUtilities { - public static bool HasAdmin(this SocketGuildUser user) + public static bool HasAdmin(this IGuildUser user) { if (user == null) return false; - if (user.Guild.Owner.Id == user.Id) + if (user.Guild.OwnerId == user.Id) { return true; } - foreach (SocketRole role in (user).Roles) + foreach (ulong id in user.RoleIds) { - if (role.Permissions.Administrator) - { + if (user.Guild.Roles.First(role => role.Id == id) + .Permissions.Administrator) return true; - } } return false; } diff --git a/Tests/CommandTests.cs b/Tests/CommandTests.cs new file mode 100644 index 0000000..5b2618f --- /dev/null +++ b/Tests/CommandTests.cs @@ -0,0 +1,57 @@ +using BotCatMaxy.Startup; +using Discord; +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tests.Mocks; +using Tests.Mocks.Guild; +using Xunit; + +namespace Tests +{ + public class CommandTests + { + MockDiscordClient client = new(); + MockGuild guild = new(); + CommandService service; + CommandHandler handler; + Task channelTask; + + public CommandTests() + { + client.guilds.Add(guild); + channelTask = guild.CreateTextChannelAsync("TestChannel"); + service = new CommandService(); + handler = new CommandHandler(client, service); + service.CommandExecuted += CommandExecuted; + } + + [Fact] + public async Task BasicCommandCheck() + { + var channel = await channelTask as MockMessageChannel; + var users = await guild.GetUsersAsync(); + var owner = users.First(user => user.Username == "Owner"); + var message = channel.SendMessageAsOther("!toggleserverstorage", owner); + MockCommandContext context = new(client, message); + Assert.True(context.Channel is IGuildChannel); + Assert.True(context.User is IGuildUser); + await handler.ExecuteCommand(message, context); + var messages = await channel.GetMessagesAsync().FlattenAsync(); + Assert.Equal(2, messages.Count()); + var response = messages.First(); + var expected = "This is a legacy feature, if you want this done now contact blackcatmaxy@gmail.com with your guild invite and your username so I can get back to you"; + Assert.Equal(expected, response.Content); + } + + private Task CommandExecuted(Optional arg1, ICommandContext arg2, IResult result) + { + if (result.Error == CommandError.Exception) throw ((ExecuteResult)result).Exception; + if (!result.IsSuccess) throw new Exception(result.ErrorReason); + return Task.CompletedTask; + } + } +} diff --git a/Tests/Mocks/Guild/MockGuild.cs b/Tests/Mocks/Guild/MockGuild.cs index e1d8386..edd0ff9 100644 --- a/Tests/Mocks/Guild/MockGuild.cs +++ b/Tests/Mocks/Guild/MockGuild.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Text; @@ -160,7 +161,7 @@ public Task CreateRoleAsync(string name, GuildPermissions? permissions = public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) { - var channel = new MockTextChannel(this); + var channel = new MockTextChannel(new MockSelfUser(), this, name); channels.Add(channel); return Task.FromResult(channel as ITextChannel); } @@ -183,7 +184,7 @@ public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) public Task DownloadUsersAsync() { userList = new(4); - userList.Add(new MockGuildUser("Bot", this, true)); + userList.Add(new MockGuildUser("BotCatMaxy", this, true)); var owner = new MockGuildUser("Owner", this); OwnerId = owner.Id; userList.Add(owner); @@ -292,9 +293,10 @@ public Task> GetTextChannelsAsync(CacheMode mo public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => Task.FromResult(userList.FirstOrDefault(user => user.Id == id) as IGuildUser); - public Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + public async Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { - throw new NotImplementedException(); + if (userList.Count == 0) await DownloadUsersAsync(); + return new ReadOnlyCollection(userList.Select(user => user as IGuildUser).ToList()); } public Task GetVanityInviteAsync(RequestOptions options = null) diff --git a/Tests/Mocks/Guild/MockGuildUser.cs b/Tests/Mocks/Guild/MockGuildUser.cs index 099422d..deec744 100644 --- a/Tests/Mocks/Guild/MockGuildUser.cs +++ b/Tests/Mocks/Guild/MockGuildUser.cs @@ -9,9 +9,10 @@ namespace Tests.Mocks.Guild { public class MockGuildUser : MockUser, IGuildUser { - public MockGuildUser(string username, IGuild guild, bool isSelf = false) : base(username, isSelf) + public MockGuildUser(string username, IGuild guild, bool isBot = false) : base(username) { Guild = guild; + IsBot = isBot; } public DateTimeOffset? JoinedAt => throw new NotImplementedException(); diff --git a/Tests/Mocks/Guild/MockTextChannel.cs b/Tests/Mocks/Guild/MockTextChannel.cs index 9b4c3ec..0ce7a10 100644 --- a/Tests/Mocks/Guild/MockTextChannel.cs +++ b/Tests/Mocks/Guild/MockTextChannel.cs @@ -8,9 +8,9 @@ namespace Tests.Mocks.Guild { - public class MockTextChannel : MockMessageChannel, ITextChannel + public class MockTextChannel : MockMessageChannel, ITextChannel, IGuildChannel { - public MockTextChannel(IGuild guild) : base() + public MockTextChannel(ISelfUser user, IGuild guild, string name) : base(user, name) { Guild = guild; } diff --git a/Tests/Mocks/MockCommandContext.cs b/Tests/Mocks/MockCommandContext.cs new file mode 100644 index 0000000..5bc3cfa --- /dev/null +++ b/Tests/Mocks/MockCommandContext.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; +using Discord.Commands; +using Tests.Mocks.Guild; + +namespace Tests.Mocks +{ + public class MockCommandContext : ICommandContext + { + public MockCommandContext(MockDiscordClient client, IUserMessage message) + { + Client = client; + Message = message; + User = message.Author; + Channel = message.Channel; + Guild = (message.Channel as MockTextChannel)?.Guild; + } + + public IDiscordClient Client { get; init; } + + public IGuild Guild { get; init; } + + public IMessageChannel Channel { get; init; } + + public IUser User { get; init; } + + public IUserMessage Message { get; init; } + } +} diff --git a/Tests/Mocks/MockDiscordClient.cs b/Tests/Mocks/MockDiscordClient.cs new file mode 100644 index 0000000..1964fb2 --- /dev/null +++ b/Tests/Mocks/MockDiscordClient.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; +using Discord.Rest; +using Discord.WebSocket; +using Tests.Mocks.Guild; + +namespace Tests.Mocks +{ + public class MockDiscordClient : IDiscordClient, IDisposable + { + public MockDiscordClient() + { + + } + + public List guilds = new List(); + + public ConnectionState ConnectionState => throw new NotImplementedException(); + + public ISelfUser CurrentUser => new MockSelfUser(); + + public TokenType TokenType => throw new NotImplementedException(); + + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + public Task GetApplicationInfoAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetBotGatewayAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetConnectionsAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + var guild = guilds.FirstOrDefault(g => g.Id == id); + return Task.FromResult(guild as IGuild); + } + + public Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + IReadOnlyCollection collection = new ReadOnlyCollection(guilds); + return Task.FromResult(collection); + } + + public Task GetInviteAsync(string inviteId, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetRecommendedShardCountAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(string username, string discriminator, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetVoiceRegionAsync(string id, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task StartAsync() + { + throw new NotImplementedException(); + } + + public Task StopAsync() + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Mocks/MockMessageChannel.cs b/Tests/Mocks/MockMessageChannel.cs index 0e7b6e9..64ee694 100644 --- a/Tests/Mocks/MockMessageChannel.cs +++ b/Tests/Mocks/MockMessageChannel.cs @@ -10,13 +10,17 @@ namespace Tests.Mocks { public class MockMessageChannel : IMessageChannel { - public MockMessageChannel() + public MockMessageChannel(ISelfUser bot, string name = null) { + Bot = bot; var random = new Random(); Id = (ulong)random.Next(0, int.MaxValue); + Name = name; } - public string Name => throw new NotImplementedException(); + protected ISelfUser Bot { get; init; } + + public string Name { get; init; } public DateTimeOffset CreatedAt => throw new NotImplementedException(); @@ -51,7 +55,8 @@ public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.Allow public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { if (limit > messages.Count) limit = messages.Count; - var range = messages.GetRange(0, limit).Select(message => (IMessage)message).ToList(); + var range = messages.GetRange(0, limit).Select(message => (IMessage)message) + .ToList(); IReadOnlyCollection[] collections = { new ReadOnlyCollection(range) }; return collections.ToAsyncEnumerable(); } @@ -98,11 +103,18 @@ public Task SendFileAsync(Stream stream, string filename, string t public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null) { - var message = new UserMockMessage(text, this, null); + var message = new MockUserMessage(text, this, Bot); messages.Insert(0, message); return Task.FromResult(message as IUserMessage); } + public IUserMessage SendMessageAsOther(string text, IUser user = null) + { + var message = new MockUserMessage(text, this, user); + messages.Insert(0, message); + return message; + } + public Task TriggerTypingAsync(RequestOptions options = null) { throw new NotImplementedException(); diff --git a/Tests/Mocks/MockSelfUser.cs b/Tests/Mocks/MockSelfUser.cs new file mode 100644 index 0000000..d7fccab --- /dev/null +++ b/Tests/Mocks/MockSelfUser.cs @@ -0,0 +1,34 @@ +using Discord; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tests.Mocks +{ + public class MockSelfUser : MockUser, ISelfUser + { + public MockSelfUser() : base("BotCatMaxy") + { + Username = "BotCatMaxy"; + IsBot = true; + } + public string Email => throw new NotImplementedException(); + + public bool IsVerified => throw new NotImplementedException(); + + public bool IsMfaEnabled => throw new NotImplementedException(); + + public UserProperties Flags => throw new NotImplementedException(); + + public PremiumType PremiumType => throw new NotImplementedException(); + + public string Locale => throw new NotImplementedException(); + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Mocks/MockUser.cs b/Tests/Mocks/MockUser.cs index 29811a2..d7b3a35 100644 --- a/Tests/Mocks/MockUser.cs +++ b/Tests/Mocks/MockUser.cs @@ -10,15 +10,10 @@ namespace Tests.Mocks { public class MockUser : IUser { - public MockUser(string username, bool isSelf = false) + public MockUser(string username) { - IsBot = isSelf; - if (isSelf) - { - Username = "BotCatMaxy"; - } - else - Username = username; + Username = username; + IsBot = false; var random = new Random(); Id = (ulong)random.Next(0, int.MaxValue); } diff --git a/Tests/Mocks/UserMockMessage.cs b/Tests/Mocks/MockUserMessage.cs similarity index 91% rename from Tests/Mocks/UserMockMessage.cs rename to Tests/Mocks/MockUserMessage.cs index 9e7baf6..e2cbca3 100644 --- a/Tests/Mocks/UserMockMessage.cs +++ b/Tests/Mocks/MockUserMessage.cs @@ -7,9 +7,9 @@ namespace Tests.Mocks { - public class UserMockMessage : MockMessage, IUserMessage + public class MockUserMessage : MockMessage, IUserMessage { - public UserMockMessage(string content, IMessageChannel channel, IUser auther) : base(content, channel, auther) { } + public MockUserMessage(string content, IMessageChannel channel, IUser auther) : base(content, channel, auther) { } public IUserMessage ReferencedMessage => throw new NotImplementedException(); diff --git a/Tests/MocksTest.cs b/Tests/MocksTest.cs index 333fcb3..3c3c0d3 100644 --- a/Tests/MocksTest.cs +++ b/Tests/MocksTest.cs @@ -22,10 +22,10 @@ public async Task GuildTest() Assert.Equal(channel.Id, (await guild.GetTextChannelAsync(channel.Id))?.Id); var message = await channel.SendMessageAsync("Test"); Assert.Equal("Test", message.Content); - message = await channel.GetMessageAsync(message.Id) as UserMockMessage; + message = await channel.GetMessageAsync(message.Id) as MockUserMessage; Assert.Equal("Test", message?.Content); await channel.DeleteMessageAsync(message.Id); - message = await channel.GetMessageAsync(message.Id) as UserMockMessage; + message = await channel.GetMessageAsync(message.Id) as MockUserMessage; Assert.Null(message); //Users @@ -47,7 +47,7 @@ public async Task GuildTest() [Fact] public async Task MultipleMessageTest() { - var channel = new MockMessageChannel(); + var channel = new MockMessageChannel(new MockSelfUser()); var message1 = await channel.SendMessageAsync("Test1"); var message2 = await channel.SendMessageAsync("Test2"); var messages = await channel.GetMessagesAsync(2).ToArrayAsync(); diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index a10dcbf..08ad7d7 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -4,6 +4,12 @@ net5.0 false + TEST; + + + + DEBUG;TRACE; + 2 From 4462d8847a96e37e8c449b38e8e14a0c3885ad7f Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sun, 7 Feb 2021 15:10:01 -0800 Subject: [PATCH 09/42] Updates and downgrades some NuGet libraries to stable versions --- BotCatMaxy/BotCatMaxy.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BotCatMaxy/BotCatMaxy.csproj b/BotCatMaxy/BotCatMaxy.csproj index f91e780..96cdf5c 100644 --- a/BotCatMaxy/BotCatMaxy.csproj +++ b/BotCatMaxy/BotCatMaxy.csproj @@ -22,16 +22,16 @@ - - - + + + - + - + From 982be279cb792789eb0c32058aa6cc43935273c4 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sun, 7 Feb 2021 15:13:06 -0800 Subject: [PATCH 10/42] Updates test project NuGet libraries --- Tests/Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 08ad7d7..b585054 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -13,14 +13,14 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 744eb954c0bcd7ddebb795b5f6764c0af07afab8 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sun, 14 Feb 2021 12:12:44 -0800 Subject: [PATCH 11/42] Adds filter testing --- BotCatMaxy/BotCatMaxy.csproj | 1 + BotCatMaxy/Components/Data/DataManipulator.cs | 2 +- BotCatMaxy/Components/Filter/FilterHandler.cs | 54 +++++++++-------- .../Components/Filter/FilterUtilities.cs | 10 ++-- .../Components/Moderation/PunishFunctions.cs | 10 ++-- BotCatMaxy/Utilities/GeneralUtilities.cs | 1 + BotCatMaxy/Utilities/PermissionUtilities.cs | 31 ++++++---- Tests/DataTests.cs | 24 ++++++-- Tests/FilterTests.cs | 59 ++++++++++++++++++- Tests/Mocks/Guild/MockGuild.cs | 2 +- Tests/Mocks/Guild/MockGuildUser.cs | 5 +- Tests/Mocks/Guild/MockTextChannel.cs | 10 +++- Tests/Tests.csproj | 2 +- 13 files changed, 152 insertions(+), 59 deletions(-) diff --git a/BotCatMaxy/BotCatMaxy.csproj b/BotCatMaxy/BotCatMaxy.csproj index 96cdf5c..ccb5110 100644 --- a/BotCatMaxy/BotCatMaxy.csproj +++ b/BotCatMaxy/BotCatMaxy.csproj @@ -11,6 +11,7 @@ true false + DEBUG;TRACE true diff --git a/BotCatMaxy/Components/Data/DataManipulator.cs b/BotCatMaxy/Components/Data/DataManipulator.cs index a2f1fbc..2c251a8 100644 --- a/BotCatMaxy/Components/Data/DataManipulator.cs +++ b/BotCatMaxy/Components/Data/DataManipulator.cs @@ -159,7 +159,7 @@ public static IMongoCollection GetActHistoryCollection(this IGuild return null; } - public static List LoadInfractions(this SocketGuildUser user, bool createDir = false) => + public static List LoadInfractions(this IGuildUser user, bool createDir = false) => user?.Id.LoadInfractions(user.Guild, createDir); diff --git a/BotCatMaxy/Components/Filter/FilterHandler.cs b/BotCatMaxy/Components/Filter/FilterHandler.cs index a48c08c..f419376 100644 --- a/BotCatMaxy/Components/Filter/FilterHandler.cs +++ b/BotCatMaxy/Components/Filter/FilterHandler.cs @@ -16,24 +16,28 @@ namespace BotCatMaxy.Startup { public class FilterHandler { - const string inviteRegex = @"(?:http|https?:\/\/)?(?:www\.)?(?:discord\.(?:gg|io|me|li|com)|discord(?:app)?\.com\/invite)\/(\S+)"; - RegexOptions regexOptions; + private const string inviteRegex = @"(?:http|https?:\/\/)?(?:www\.)?(?:discord\.(?:gg|io|me|li|com)|discord(?:app)?\.com\/invite)\/(\S+)"; + private const RegexOptions regexOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; - readonly DiscordSocketClient client; - public FilterHandler(DiscordSocketClient client) + private readonly IDiscordClient client; + + public FilterHandler(IDiscordClient client) { this.client = client; - client.MessageReceived += CheckMessage; - client.MessageUpdated += HandleEdit; - client.ReactionAdded += HandleReaction; - client.UserJoined += HandleUserJoin; - client.UserUpdated += HandleUserChange; - client.GuildMemberUpdated += HandleUserChange; - regexOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + if (client is BaseSocketClient socketClient) + { + socketClient.MessageReceived += HandleMessage; + socketClient.MessageUpdated += HandleEdit; + socketClient.ReactionAdded += HandleReaction; + socketClient.UserJoined += HandleUserJoin; + socketClient.UserUpdated += HandleUserChange; + socketClient.GuildMemberUpdated += HandleUserChange; + } + new LogMessage(LogSeverity.Info, "Filter", "Filter is active").Log(); } - public async Task HandleEdit(Cacheable oldMessage, SocketMessage editedMessage, ISocketMessageChannel channel) + private async Task HandleEdit(Cacheable oldMessage, SocketMessage editedMessage, ISocketMessageChannel channel) => await Task.Run(() => CheckMessage(editedMessage)).ConfigureAwait(false); public async Task HandleReaction(Cacheable cachedMessage, ISocketMessageChannel channel, SocketReaction reaction) @@ -60,19 +64,20 @@ public async Task HandleUserChange(SocketUser old, SocketUser updated) } public async Task HandleMessage(SocketMessage message) - => await Task.Run(() => CheckMessage(message)).ConfigureAwait(false); + => await Task.Run(() => CheckMessage(message)); - public async Task CheckNameInGuild(IUser user, string name, SocketGuild guild) + public async Task CheckNameInGuild(IUser user, string name, IGuild guild) { try { - if (!guild.CurrentUser.GuildPermissions.KickMembers) return; + var currentUser = await guild.GetCurrentUserAsync(); + if (!currentUser.GuildPermissions.KickMembers) return; ModerationSettings settings = guild.LoadFromFile(false); //Has to check if not equal to true since it's nullable if (settings?.moderateNames != true) return; - SocketGuildUser gUser = user as SocketGuildUser ?? guild.GetUser(user.Id); - if (gUser.CantBeWarned() || !gUser.CanActOn(guild.CurrentUser)) + IGuildUser gUser = user as IGuildUser ?? await guild.GetUserAsync(user.Id); + if (gUser.CantBeWarned() || !gUser.CanActOn(currentUser)) return; BadWord detectedBadWord = name.CheckForBadWords(guild.LoadFromFile(false)?.badWords.ToArray()); @@ -117,7 +122,7 @@ public async Task CheckNameInGuild(IUser user, string name, SocketGuild guild) public async Task CheckReaction(Cacheable cachedMessage, ISocketMessageChannel channel, SocketReaction reaction) { - if ((reaction.User.IsSpecified && reaction.User.Value.IsBot) || !(channel is SocketGuildChannel)) + if ((reaction.User.IsSpecified && reaction.User.Value.IsBot) || !(channel is IGuildChannel)) { return; //Makes sure it's not logging a message from a bot and that it's in a discord server } @@ -148,21 +153,20 @@ public async Task CheckReaction(Cacheable cachedMessage, IS } } - public async Task CheckMessage(SocketMessage message) + public async Task CheckMessage(IMessage message, ICommandContext context = null) { - if (message.Author.IsBot || !(message.Channel is SocketGuildChannel) || !(message is SocketUserMessage) || string.IsNullOrWhiteSpace(message.Content)) + if (message.Author.IsBot || message.Channel is not IGuildChannel chnl || message is not IUserMessage userMessage || string.IsNullOrWhiteSpace(message.Content)) { return; //Makes sure it's not logging a message from a bot and that it's in a discord server } - SocketCommandContext context = new SocketCommandContext(client, message as SocketUserMessage); - SocketGuildChannel chnl = message.Channel as SocketGuildChannel; - if (chnl?.Guild == null || (message.Author as SocketGuildUser).CantBeWarned()) return; + context ??= new SocketCommandContext((DiscordSocketClient)client, (SocketUserMessage)message); + IGuildUser gUser = message.Author as IGuildUser; + if (chnl?.Guild == null || gUser.CantBeWarned()) return; var guild = chnl.Guild; try { ModerationSettings modSettings = guild.LoadFromFile(); - SocketGuildUser gUser = message.Author as SocketGuildUser; List badWords = guild.LoadFromFile()?.badWords; string msgContent = message.Content; @@ -218,7 +222,7 @@ public async Task CheckMessage(SocketMessage message) if (msgContent.Equals(match.Value, StringComparison.InvariantCultureIgnoreCase)) return; msgContent = msgContent.Replace(match.Value, "", StringComparison.InvariantCultureIgnoreCase); //Checks for links - if ((modSettings.allowedLinks != null && modSettings.allowedLinks.Count > 0) && (modSettings.allowedToLink == null || !gUser.RoleIDs().Intersect(modSettings.allowedToLink).Any())) + if ((modSettings.allowedLinks != null && modSettings.allowedLinks.Count > 0) && (modSettings.allowedToLink == null || !gUser.RoleIds.Intersect(modSettings.allowedToLink).Any())) { if (!modSettings.allowedLinks.Any(s => match.ToString().ToLower().Contains(s.ToLower()))) { diff --git a/BotCatMaxy/Components/Filter/FilterUtilities.cs b/BotCatMaxy/Components/Filter/FilterUtilities.cs index 909cf03..a90dc91 100644 --- a/BotCatMaxy/Components/Filter/FilterUtilities.cs +++ b/BotCatMaxy/Components/Filter/FilterUtilities.cs @@ -85,15 +85,15 @@ public static BadWord CheckForBadWords(this string message, BadWord[] badWords) return null; } - public static async Task FilterPunish(this SocketCommandContext context, string reason, ModerationSettings settings, float warnSize = 0.5f) + public static async Task FilterPunish(this ICommandContext context, string reason, ModerationSettings settings, float warnSize = 0.5f) { - await context.FilterPunish(context.User as SocketGuildUser, reason, settings, delete: true, warnSize: warnSize); + await context.FilterPunish(context.User as IGuildUser, reason, settings, delete: true, warnSize: warnSize); } - public static async Task FilterPunish(this ICommandContext context, SocketGuildUser user, string reason, ModerationSettings settings, bool delete = true, float warnSize = 0.5f, string explicitInfo = "") + public static async Task FilterPunish(this ICommandContext context, IGuildUser user, string reason, ModerationSettings settings, bool delete = true, float warnSize = 0.5f, string explicitInfo = "") { string jumpLink = await DiscordLogging.LogMessage(reason, context.Message, context.Guild, color: Color.Gold, authorOveride: user); - await user.Warn(warnSize, reason, context.Channel as SocketTextChannel, logLink: jumpLink); + await user.Warn(warnSize, reason, context.Channel as ITextChannel, logLink: jumpLink); if (settings?.anouncementChannels?.Contains(context.Channel.Id) ?? false) //If this channel is an anouncement channel return; @@ -117,7 +117,7 @@ public static async Task FilterPunish(this ICommandContext context, SocketGuildU public const string notifyInfoRegex = @"<@!?(\d+)> has been given their (\d+)\w+-?(?:\d+\w+)? infraction because of (.+)"; - public static async Task> NotifyPunish(ICommandContext context, SocketGuildUser user, string reason, ModerationSettings settings) + public static async Task> NotifyPunish(ICommandContext context, IGuildUser user, string reason, ModerationSettings settings) { Task warnMessage = null; LogSettings logSettings = context.Guild.LoadFromFile(false); diff --git a/BotCatMaxy/Components/Moderation/PunishFunctions.cs b/BotCatMaxy/Components/Moderation/PunishFunctions.cs index f9ce095..7c662b9 100644 --- a/BotCatMaxy/Components/Moderation/PunishFunctions.cs +++ b/BotCatMaxy/Components/Moderation/PunishFunctions.cs @@ -37,7 +37,7 @@ public WarnResult(string error) public static class PunishFunctions { - public static async Task Warn(this UserRef userRef, float size, string reason, SocketTextChannel channel, string logLink = null) + public static async Task Warn(this UserRef userRef, float size, string reason, ITextChannel channel, string logLink = null) { Contract.Requires(userRef != null); if (userRef.GuildUser != null) @@ -46,7 +46,7 @@ public static async Task Warn(this UserRef userRef, float size, stri return await userRef.ID.Warn(size, reason, channel, userRef.User, logLink); } - public static async Task Warn(this SocketGuildUser user, float size, string reason, SocketTextChannel channel, string logLink = null) + public static async Task Warn(this IGuildUser user, float size, string reason, ITextChannel channel, string logLink = null) { if (user.CantBeWarned()) @@ -58,7 +58,7 @@ public static async Task Warn(this SocketGuildUser user, float size, } - public static async Task Warn(this ulong userID, float size, string reason, SocketTextChannel channel, IUser warnee = null, string logLink = null) + public static async Task Warn(this ulong userID, float size, string reason, ITextChannel channel, IUser warnee = null, string logLink = null) { if (size > 999 || size < 0.01) { @@ -77,9 +77,9 @@ public static async Task Warn(this ulong userID, float size, string LogSettings logSettings = channel.Guild.LoadFromFile(false); IUser[] users = null; if (logSettings?.pubLogChannel != null && channel.Guild.TryGetChannel(logSettings.pubLogChannel.Value, out IGuildChannel logChannel)) - users = await (logChannel as ISocketMessageChannel).GetUsersAsync().Flatten().ToArrayAsync(); + users = await (logChannel as IMessageChannel).GetUsersAsync().Flatten().ToArrayAsync(); else - users = await (channel as ISocketMessageChannel).GetUsersAsync().Flatten().ToArrayAsync(); + users = await (channel as IMessageChannel).GetUsersAsync().Flatten().ToArrayAsync(); if (!users.Any(xUser => xUser.Id == userID)) { warnee.TryNotify($"You have been warned in {channel.Guild.Name} discord for \"{reason}\" in a channel you can't view"); diff --git a/BotCatMaxy/Utilities/GeneralUtilities.cs b/BotCatMaxy/Utilities/GeneralUtilities.cs index 14a030d..0cff8bd 100644 --- a/BotCatMaxy/Utilities/GeneralUtilities.cs +++ b/BotCatMaxy/Utilities/GeneralUtilities.cs @@ -2,6 +2,7 @@ using BotCatMaxy.Data; using BotCatMaxy.Models; using Discord; +using Discord.Commands; using Discord.Net; using Discord.Rest; using Discord.WebSocket; diff --git a/BotCatMaxy/Utilities/PermissionUtilities.cs b/BotCatMaxy/Utilities/PermissionUtilities.cs index 4fabfab..e7b7adf 100644 --- a/BotCatMaxy/Utilities/PermissionUtilities.cs +++ b/BotCatMaxy/Utilities/PermissionUtilities.cs @@ -54,26 +54,37 @@ public static bool CanWarn(this SocketGuildUser user) return false; } - public static bool CantBeWarned(this SocketGuildUser user) + //Naming violation, should be WarnImmune + public static bool CantBeWarned(this IGuildUser user) { if (user == null) return false; if (user.HasAdmin()) return true; - ModerationSettings settings = user.Guild.LoadFromFile(); - if (settings != null) - { - List rolesUnableToBeWarned = new List(); - foreach (ulong roleID in settings.cantBeWarned) rolesUnableToBeWarned.Add(user.Guild.GetRole(roleID)); - if (user.Roles.Intersect(rolesUnableToBeWarned).Any()) return true; - } + ModerationSettings settings = user.Guild.LoadFromFile(false); + if (settings != null && user.RoleIds.Intersect(settings.cantBeWarned).Any()) + return true; return false; } - public static bool CanActOn(this SocketGuildUser focus, SocketGuildUser comparer) + public static bool CanActOn(this IGuildUser focus, IGuildUser comparer) { - if (focus.Roles.Select(role => role.Position).Max() > comparer.Roles.Select(role => role.Position).Max()) + var focusPositions = focus.GetRoles() + .Select(role => role.Position); + var comparerPositions = comparer.GetRoles() + .Select(role => role.Position); + + if (focusPositions.Max() > comparerPositions.Max()) return true; return false; } + + public static IEnumerable GetRoles(this IGuildUser user) + { + if (user is SocketGuildUser gUser) + return gUser.Roles; + + return user.RoleIds.Select(id => + user.Guild.Roles.First(role => role.Id == id)); + } } } diff --git a/Tests/DataTests.cs b/Tests/DataTests.cs index a2259d8..c84930d 100644 --- a/Tests/DataTests.cs +++ b/Tests/DataTests.cs @@ -1,6 +1,7 @@ using BotCatMaxy; using BotCatMaxy.Data; using BotCatMaxy.Models; +using BotCatMaxy.Moderation; using Mongo2Go; using MongoDB.Bson; using MongoDB.Driver; @@ -14,19 +15,22 @@ namespace Tests { - public class DataTests : IDisposable + //Forces all tests that inherit from this to run serialized + //In theory it should work in parallel but seems unstable + [Collection("Data")] + public class BaseDataTests : IDisposable { protected MongoDbRunner runner; protected MongoCollectionBase collection; - protected IMongoDatabase settings; + protected IMongoDatabase settingsDB; - public DataTests() + public BaseDataTests() { runner = MongoDbRunner.Start(); MongoClient client = new MongoClient(runner.ConnectionString); MainClass.dbClient = client; - settings = client.GetDatabase("Settings"); + settingsDB = client.GetDatabase("Settings"); var database = client.GetDatabase("IntegrationTest"); collection = (MongoCollectionBase)database.GetCollection("TestCollection"); } @@ -35,7 +39,10 @@ public void Dispose() { runner.Dispose(); } + } + public class DataTests : BaseDataTests, IDisposable + { [Fact] public void TestBasic() { @@ -64,8 +71,8 @@ public void TestCollections() Assert.NotNull(collection); Assert.Equal(guild.Id.ToString(), collection.CollectionNamespace.CollectionName); - var ownerCollection = settings.GetCollection(guild.OwnerId.ToString()); - ownerCollection.InsertOne(new BsonDocument()); + var ownerCollection = settingsDB.GetCollection(guild.OwnerId.ToString()); + ownerCollection.InsertOne(new BsonDocument("Test", "Value")); collection = guild.GetCollection(false); Assert.NotNull(collection); Assert.Equal(guild.OwnerId.ToString(), collection.CollectionNamespace.CollectionName); @@ -80,6 +87,11 @@ public void TestInfractions() Assert.Null(infractions); infractions = userID.LoadInfractions(guild, true); Assert.NotNull(infractions); + Assert.Empty(infractions); + userID.AddWarn(1, "Test", guild, "link"); + infractions = userID.LoadInfractions(guild, false); + Assert.NotNull(infractions); + Assert.NotEmpty(infractions); } } } \ No newline at end of file diff --git a/Tests/FilterTests.cs b/Tests/FilterTests.cs index 40edde3..ac4455f 100644 --- a/Tests/FilterTests.cs +++ b/Tests/FilterTests.cs @@ -7,21 +7,74 @@ using BotCatMaxy; using Xunit; using BotCatMaxy.Models; +using Tests.Mocks; +using BotCatMaxy.Startup; +using Tests.Mocks.Guild; +using Discord; +using BotCatMaxy.Data; namespace Tests { - public class FilterTests + public class FilterTests : BaseDataTests { - public BadWord[] badWords = { new BadWord("Calzone"), new BadWord("Something") { PartOfWord = false }, new BadWord("Substitution") }; + private readonly BadWord[] badWords = { new BadWord("Calzone"), new BadWord("Something") { PartOfWord = false }, new BadWord("Substitution") }; + private readonly MockDiscordClient client = new(); + private readonly MockGuild guild = new(); + private readonly FilterHandler filter; + private ModerationSettings settings; + private Task channelTask; + + public FilterTests() + { + filter = new(client); + client.guilds.Add(guild); + channelTask = guild.CreateTextChannelAsync("TestChannel"); + ModerationSettings settings = new() + { + guild = guild, + moderateNames = true, + maxNewLines = 5 + }; + BadWordList badWordList = new BadWordList() { badWords = badWords.ToList(), guild = guild }; + badWordList.SaveToFile(); + settings.SaveToFile(); + } + + [Fact] + public async Task PunishTest() + { + var channel = (MockTextChannel)await channelTask; + var users = await guild.GetUsersAsync(); + var testee = users.First(user => user.Username == "Testee"); + var message = channel.SendMessageAsOther("", testee); + var context = new MockCommandContext(client, message); + await context.FilterPunish("Testing Punish", settings); + var infractons = testee.LoadInfractions(true); + Assert.NotNull(infractons); + Assert.NotEmpty(infractons); + } [Theory] [InlineData("We like calzones", "calzone")] [InlineData("Somethings is here", null)] [InlineData("$ubst1tuti0n", "Substitution")] - public void BadWordTheory(string input, string expected) + public async Task BadWordTheory(string input, string expected) { var result = input.CheckForBadWords(badWords); Assert.Equal(expected, result?.Word, ignoreCase: true); + + var channel = (MockTextChannel)await channelTask; + var users = await guild.GetUsersAsync(); + var testee = users.First(user => user.Username == "Testee"); + var message = channel.SendMessageAsOther(input, testee); + var context = new MockCommandContext(client, message); + await filter.CheckMessage(message, context); + if (expected != null) + { + var infractons = testee.LoadInfractions(false); + Assert.NotNull(infractons); + Assert.NotEmpty(infractons); + } } } } diff --git a/Tests/Mocks/Guild/MockGuild.cs b/Tests/Mocks/Guild/MockGuild.cs index edd0ff9..8482840 100644 --- a/Tests/Mocks/Guild/MockGuild.cs +++ b/Tests/Mocks/Guild/MockGuild.cs @@ -22,7 +22,7 @@ public MockGuild() protected List channels = new(4); protected List bans = new(4); - public string Name => throw new NotImplementedException(); + public string Name => "TestName"; public int AFKTimeout => throw new NotImplementedException(); diff --git a/Tests/Mocks/Guild/MockGuildUser.cs b/Tests/Mocks/Guild/MockGuildUser.cs index deec744..5af177d 100644 --- a/Tests/Mocks/Guild/MockGuildUser.cs +++ b/Tests/Mocks/Guild/MockGuildUser.cs @@ -1,6 +1,7 @@ using Discord; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -15,6 +16,8 @@ public MockGuildUser(string username, IGuild guild, bool isBot = false) : base(u IsBot = isBot; } + List roles = new(); + public DateTimeOffset? JoinedAt => throw new NotImplementedException(); public string Nickname => throw new NotImplementedException(); @@ -27,7 +30,7 @@ public MockGuildUser(string username, IGuild guild, bool isBot = false) : base(u public DateTimeOffset? PremiumSince => throw new NotImplementedException(); - public IReadOnlyCollection RoleIds => throw new NotImplementedException(); + public IReadOnlyCollection RoleIds => new ReadOnlyCollection(roles.Select(role => role.Id).ToList()); public bool? IsPending => throw new NotImplementedException(); diff --git a/Tests/Mocks/Guild/MockTextChannel.cs b/Tests/Mocks/Guild/MockTextChannel.cs index 0ce7a10..90e36ce 100644 --- a/Tests/Mocks/Guild/MockTextChannel.cs +++ b/Tests/Mocks/Guild/MockTextChannel.cs @@ -15,6 +15,8 @@ public MockTextChannel(ISelfUser user, IGuild guild, string name) : base(user, n Guild = guild; } + IReadOnlyCollection users; + public bool IsNsfw => throw new NotImplementedException(); public string Topic => throw new NotImplementedException(); @@ -128,9 +130,15 @@ Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOpt throw new NotImplementedException(); } + public async Task DownloadUsers() + { + users = await Guild.GetUsersAsync(); + } + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { - throw new NotImplementedException(); + IReadOnlyCollection[] collection = { users }; + return collection.ToAsyncEnumerable(); } } } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index b585054..1f0c198 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -8,7 +8,7 @@ - DEBUG;TRACE; + DEBUG;TRACE;TESTING; 2 From e13906651675c22652efb6b74dcd5906b0af85de Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sun, 14 Feb 2021 12:14:27 -0800 Subject: [PATCH 12/42] Removes unneeded Azure WebJobs package --- BotCatMaxy/BotCatMaxy.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/BotCatMaxy/BotCatMaxy.csproj b/BotCatMaxy/BotCatMaxy.csproj index ccb5110..18496cc 100644 --- a/BotCatMaxy/BotCatMaxy.csproj +++ b/BotCatMaxy/BotCatMaxy.csproj @@ -29,7 +29,6 @@ - From 19ab76654057a7278d4aef0e8e1dfc2ae7740ef7 Mon Sep 17 00:00:00 2001 From: Daniel Bereza Date: Mon, 15 Feb 2021 12:18:50 -0800 Subject: [PATCH 13/42] Changes UserRef to use interface types instead of just Socket types --- .../Components/Moderation/ModerationCommands.cs | 4 ++-- BotCatMaxy/Models/UserRef.cs | 17 +++++------------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index e8afbc6..e58cbbb 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -119,7 +119,7 @@ public async Task DMUserWarnsAsync(UserRef userRef = null, int amount = 50) await ReplyAsync(message); return; } - userRef = new UserRef(userRef, guild); + userRef = userRef with { GuildUser = guild.GetUser(userRef.ID) }; await ReplyAsync($"Here are {userRef.Mention()}'s {((amount < infractions.Count) ? $"last {amount} out of " : "")}{"infraction".ToQuantity(infractions.Count)}", embed: infractions.GetEmbed(userRef, amount: amount)); } @@ -506,7 +506,7 @@ public async Task DeleteMany(uint number, UserRef user = null) await ReplyAsync("Invalid number"); return; } - if (user?.GuildUser != null && user.GuildUser.Hierarchy >= ((SocketGuildUser)Context.User).Hierarchy) + if (user?.GuildUser != null && ((SocketGuildUser)user.GuildUser).Hierarchy >= ((SocketGuildUser)Context.User).Hierarchy) { await ReplyAsync("Can't target deleted messages belonging to people with higher hierarchy"); return; diff --git a/BotCatMaxy/Models/UserRef.cs b/BotCatMaxy/Models/UserRef.cs index cb9d6c3..0a01da3 100644 --- a/BotCatMaxy/Models/UserRef.cs +++ b/BotCatMaxy/Models/UserRef.cs @@ -1,33 +1,26 @@ -using Discord.WebSocket; +using Discord; namespace BotCatMaxy.Models { public record UserRef { - public SocketGuildUser GuildUser { get; init; } - public SocketUser User { get; init; } + public IGuildUser GuildUser { get; init; } + public IUser User { get; init; } public ulong ID { get; init; } - public UserRef(SocketGuildUser gUser) + public UserRef(IGuildUser gUser) { GuildUser = gUser; User = gUser; ID = gUser.Id; } - public UserRef(SocketUser user) + public UserRef(IUser user) { User = user; ID = user.Id; } public UserRef(ulong ID) => this.ID = ID; - - public UserRef(UserRef userRef, SocketGuild guild) - { - User = userRef.User; - ID = userRef.ID; - GuildUser = guild.GetUser(ID); - } } } From 0e6791391bff746765f39ccd594949ee3aaf1cde Mon Sep 17 00:00:00 2001 From: Daniel Bereza Date: Mon, 15 Feb 2021 12:27:20 -0800 Subject: [PATCH 14/42] Makes warn checks not require SocketGuildUser --- BotCatMaxy/Startup/Command Handler.cs | 2 +- BotCatMaxy/Utilities/PermissionUtilities.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BotCatMaxy/Startup/Command Handler.cs b/BotCatMaxy/Startup/Command Handler.cs index 66158f1..b38de7e 100644 --- a/BotCatMaxy/Startup/Command Handler.cs +++ b/BotCatMaxy/Startup/Command Handler.cs @@ -157,7 +157,7 @@ public class CanWarnAttribute : PreconditionAttribute public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { //Makes sure it's in a server - if (context.User is SocketGuildUser gUser) + if (context.User is IGuildUser gUser) { // If this command was executed by a user with the appropriate role, return a success if (gUser.CanWarn()) diff --git a/BotCatMaxy/Utilities/PermissionUtilities.cs b/BotCatMaxy/Utilities/PermissionUtilities.cs index e7b7adf..44c5460 100644 --- a/BotCatMaxy/Utilities/PermissionUtilities.cs +++ b/BotCatMaxy/Utilities/PermissionUtilities.cs @@ -29,14 +29,14 @@ public static bool HasAdmin(this IGuildUser user) return false; } - public static bool CanWarn(this SocketGuildUser user) + public static bool CanWarn(this IGuildUser user) { if (HasAdmin(user)) { return true; } - foreach (SocketRole role in user.Roles) + foreach (IRole role in user.GetRoles()) { if (role.Permissions.KickMembers) { @@ -46,7 +46,7 @@ public static bool CanWarn(this SocketGuildUser user) ModerationSettings settings = user.Guild.LoadFromFile(); if (settings != null && settings.ableToWarn != null && settings.ableToWarn.Count > 0) { - if (user.RoleIDs().Intersect(settings.ableToWarn).Any()) + if (user.RoleIds.Intersect(settings.ableToWarn).Any()) { return true; } From 3a9175017664ec33751a4dcaa3976c32431cd758 Mon Sep 17 00:00:00 2001 From: Daniel Bereza Date: Mon, 15 Feb 2021 13:41:02 -0800 Subject: [PATCH 15/42] Makes hierarchy attribute not require SocketGuildUser --- BotCatMaxy/Startup/Command Handler.cs | 15 +++++++-------- BotCatMaxy/Utilities/PermissionUtilities.cs | 11 +++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/BotCatMaxy/Startup/Command Handler.cs b/BotCatMaxy/Startup/Command Handler.cs index b38de7e..0fe3ad5 100644 --- a/BotCatMaxy/Startup/Command Handler.cs +++ b/BotCatMaxy/Startup/Command Handler.cs @@ -193,14 +193,13 @@ public class RequireHierarchyAttribute : ParameterPreconditionAttribute public override async Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services) { - // Hierarchy is only available under the socket variant of the user. - if (!(context.User is SocketGuildUser guildUser)) + if (context.User is not IGuildUser guildUser) return PreconditionResult.FromError("This command cannot be used outside of a guild"); var targetUser = value switch { - UserRef userRef => userRef.GuildUser as SocketGuildUser, - SocketGuildUser targetGuildUser => targetGuildUser, - ulong userId => await context.Guild.GetUserAsync(userId).ConfigureAwait(false) as SocketGuildUser, + UserRef userRef => userRef.GuildUser, + IGuildUser targetGuildUser => targetGuildUser, + ulong userId => await context.Guild.GetUserAsync(userId), _ => throw new ArgumentOutOfRangeException("Unknown Type used in parameter that requires hierarchy"), }; if (targetUser == null) @@ -209,11 +208,11 @@ public override async Task CheckPermissionsAsync(ICommandCon else return PreconditionResult.FromError("Target user not found"); - if (guildUser.Hierarchy <= targetUser.Hierarchy) + if (guildUser.GetHierarchy() <= targetUser.GetHierarchy()) return PreconditionResult.FromError("You cannot target anyone else whose roles are higher than yours"); - var currentUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false) as SocketGuildUser; - if (currentUser?.Hierarchy < targetUser.Hierarchy) + var currentUser = await context.Guild.GetCurrentUserAsync(); + if (currentUser?.GetHierarchy() < targetUser.GetHierarchy()) return PreconditionResult.FromError("The bot's role is lower than the targeted user."); return PreconditionResult.FromSuccess(); diff --git a/BotCatMaxy/Utilities/PermissionUtilities.cs b/BotCatMaxy/Utilities/PermissionUtilities.cs index 44c5460..f5596c7 100644 --- a/BotCatMaxy/Utilities/PermissionUtilities.cs +++ b/BotCatMaxy/Utilities/PermissionUtilities.cs @@ -86,5 +86,16 @@ public static IEnumerable GetRoles(this IGuildUser user) return user.RoleIds.Select(id => user.Guild.Roles.First(role => role.Id == id)); } + + public static int GetHierarchy(this IGuildUser user) + { + if (user is SocketGuildUser socketUser) + return socketUser.Hierarchy; + + if (user.Guild.OwnerId == user.Id) + return int.MaxValue; + + return user.RoleIds.Max(role => user.Guild.GetRole(role).Position); + } } } From 052d1e084a831c9a4bec8bc5dbe66f23095b7c01 Mon Sep 17 00:00:00 2001 From: Daniel Bereza Date: Sun, 21 Feb 2021 14:23:27 -0800 Subject: [PATCH 16/42] Tries to set up a Unit Test for !warn --- BotCatMaxy/Components/Data/SettingsCache.cs | 11 ++-- .../Moderation/ModerationCommands.cs | 51 +++++++++---------- .../Components/Moderation/PunishFunctions.cs | 6 +-- BotCatMaxy/Startup/Command Handler.cs | 2 +- BotCatMaxy/TypeReaders/UserRefTypeReader.cs | 12 ++--- BotCatMaxy/Utilities/GeneralUtilities.cs | 2 +- BotCatMaxy/Utilities/PermissionUtilities.cs | 14 +++++ Tests/CommandTests.cs | 49 +++++++++++++++--- Tests/DataTests.cs | 3 +- 9 files changed, 100 insertions(+), 50 deletions(-) diff --git a/BotCatMaxy/Components/Data/SettingsCache.cs b/BotCatMaxy/Components/Data/SettingsCache.cs index f29e8b4..2929f2a 100644 --- a/BotCatMaxy/Components/Data/SettingsCache.cs +++ b/BotCatMaxy/Components/Data/SettingsCache.cs @@ -9,17 +9,16 @@ namespace BotCatMaxy.Cache public class SettingsCache { public static HashSet guildSettings = new HashSet(); - private DiscordSocketClient client; - public SettingsCache(DiscordSocketClient client) - { - this.client = client; - this.client.LeftGuild += RemoveGuild; + public SettingsCache(IDiscordClient client) { + if (client is BaseSocketClient socketClient) + socketClient.LeftGuild += RemoveGuild; } - public async Task RemoveGuild(SocketGuild guild) + public Task RemoveGuild(SocketGuild guild) { guildSettings.RemoveWhere(g => g.ID == guild.Id); + return Task.CompletedTask; } } diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index e58cbbb..328adc2 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -17,21 +17,19 @@ namespace BotCatMaxy { [Name("Moderation")] - public class ModerationCommands : ModuleBase + public class ModerationCommands : ModuleBase { #if !TEST [DontInject] #endif public InteractivityService Interactivity { get; set; } - [RequireContext(ContextType.Guild)] [Command("warn")] [Summary("Warn a user with an option reason.")] - [CanWarn()] public async Task WarnUserAsync([RequireHierarchy] UserRef userRef, [Remainder] string reason) { IUserMessage logMessage = await DiscordLogging.LogWarn(Context.Guild, Context.Message.Author, userRef.ID, reason, Context.Message.GetJumpUrl()); - WarnResult result = await userRef.Warn(1, reason, Context.Channel as SocketTextChannel, logLink: logMessage?.GetJumpUrl()); + WarnResult result = await userRef.Warn(1, reason, Context.Channel as ITextChannel, logLink: logMessage?.GetJumpUrl()); if (result.success) Context.Message.DeleteOrRespond($"{userRef.Mention()} has gotten their {result.warnsAmount.Suffix()} infraction for {reason}", Context.Guild); @@ -53,7 +51,7 @@ public async Task WarnUserAsync([RequireHierarchy] UserRef userRef, [Remainder] public async Task WarnWithSizeUserAsync([RequireHierarchy] UserRef userRef, float size, [Remainder] string reason) { IUserMessage logMessage = await DiscordLogging.LogWarn(Context.Guild, Context.Message.Author, userRef.ID, reason, Context.Message.GetJumpUrl()); - WarnResult result = await userRef.Warn(size, reason, Context.Channel as SocketTextChannel, logLink: logMessage?.GetJumpUrl()); + WarnResult result = await userRef.Warn(size, reason, Context.Channel as ITextChannel, logLink: logMessage?.GetJumpUrl()); if (result.success) Context.Message.DeleteOrRespond($"{userRef.Mention()} has gotten their {result.warnsAmount.Suffix()} infraction for {reason}", Context.Guild); else @@ -78,7 +76,8 @@ public async Task DMUserWarnsAsync(UserRef userRef = null, int amount = 50) await ReplyAsync("Why would you want to see that many infractions?"); return; } - var mutualGuilds = Context.Message.Author.MutualGuilds.ToArray(); + + var mutualGuilds = (await Context.Message.Author.GetMutualGuildsAsync(Context.Client)).ToArray(); if (userRef == null) userRef = new UserRef(Context.Message.Author); @@ -90,7 +89,7 @@ public async Task DMUserWarnsAsync(UserRef userRef = null, int amount = 50) guildsEmbed.AddField($"[{i + 1}] {mutualGuilds[i].Name}", mutualGuilds[i].Id); } await ReplyAsync(embed: guildsEmbed.Build()); - SocketGuild guild; + IGuild guild; while (true) { var result = await Interactivity.NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); @@ -119,7 +118,7 @@ public async Task DMUserWarnsAsync(UserRef userRef = null, int amount = 50) await ReplyAsync(message); return; } - userRef = userRef with { GuildUser = guild.GetUser(userRef.ID) }; + userRef = userRef with { GuildUser = await guild.GetUserAsync(userRef.ID) }; await ReplyAsync($"Here are {userRef.Mention()}'s {((amount < infractions.Count) ? $"last {amount} out of " : "")}{"infraction".ToQuantity(infractions.Count)}", embed: infractions.GetEmbed(userRef, amount: amount)); } @@ -131,7 +130,7 @@ await ReplyAsync($"Here are {userRef.Mention()}'s {((amount < infractions.Count) [Alias("infractions", "warnings")] public async Task CheckUserWarnsAsync(UserRef userRef = null, int amount = 5) { - userRef ??= new UserRef(Context.User as SocketGuildUser); + userRef ??= new UserRef(Context.User as IGuildUser); List infractions = userRef.LoadInfractions(Context.Guild, false); if (infractions?.Count is null or 0) { @@ -173,7 +172,7 @@ public async Task RemoveWarnAsync([RequireHierarchy] UserRef userRef, int index) [RequireUserPermission(GuildPermission.KickMembers)] public async Task KickAndWarn([RequireHierarchy] SocketGuildUser user, [Remainder] string reason = "Unspecified") { - await user.Warn(1, reason, Context.Channel as SocketTextChannel, "Discord"); + await user.Warn(1, reason, Context.Channel as ITextChannel, "Discord"); await DiscordLogging.LogWarn(Context.Guild, Context.Message.Author, user.Id, reason, Context.Message.GetJumpUrl(), "kick"); _ = user.Notify("kicked", reason, Context.Guild, Context.Message.Author); @@ -188,7 +187,7 @@ public async Task KickAndWarn([RequireHierarchy] SocketGuildUser user, [Remainde [RequireUserPermission(GuildPermission.KickMembers)] public async Task KickAndWarn([RequireHierarchy] SocketGuildUser user, float size, [Remainder] string reason = "Unspecified") { - await user.Warn(size, reason, Context.Channel as SocketTextChannel, "Discord"); + await user.Warn(size, reason, Context.Channel as ITextChannel, "Discord"); await DiscordLogging.LogWarn(Context.Guild, Context.Message.Author, user.Id, reason, Context.Message.GetJumpUrl(), "kick"); _ = user.Notify("kicked", reason, Context.Guild, Context.Message.Author); @@ -209,7 +208,7 @@ public async Task TempBanUser([RequireHierarchy] UserRef userRef, TimeSpan time, await ReplyAsync("Can't temp-ban for less than a minute"); return; } - if (!(Context.Message.Author as SocketGuildUser).HasAdmin()) + if (!(Context.Message.Author as IGuildUser).HasAdmin()) { ModerationSettings settings = Context.Guild.LoadFromFile(false); if (settings?.maxTempAction != null && time > settings.maxTempAction) @@ -222,7 +221,7 @@ public async Task TempBanUser([RequireHierarchy] UserRef userRef, TimeSpan time, TempAct oldAct = actions.tempBans.FirstOrDefault(tempMute => tempMute.User == userRef.ID); if (oldAct != null) { - if (!(Context.Message.Author as SocketGuildUser).HasAdmin() && (oldAct.Length - (DateTime.UtcNow - oldAct.DateBanned)) >= time) + if (!(Context.Message.Author as IGuildUser).HasAdmin() && (oldAct.Length - (DateTime.UtcNow - oldAct.DateBanned)) >= time) { await ReplyAsync($"{Context.User.Mention} please contact your admin(s) in order to shorten length of a punishment"); return; @@ -260,7 +259,7 @@ public async Task TempBanWarnUser([RequireHierarchy] UserRef userRef, TimeSpan t await ReplyAsync("Can't temp-ban for less than a minute"); return; } - if (!(Context.Message.Author as SocketGuildUser).HasAdmin()) + if (!(Context.Message.Author as IGuildUser).HasAdmin()) { ModerationSettings settings = Context.Guild.LoadFromFile(false); if (settings?.maxTempAction != null && time > settings.maxTempAction) @@ -269,7 +268,7 @@ public async Task TempBanWarnUser([RequireHierarchy] UserRef userRef, TimeSpan t return; } } - await userRef.Warn(1, reason, Context.Channel as SocketTextChannel, "Discord"); + await userRef.Warn(1, reason, Context.Channel as ITextChannel, "Discord"); TempActionList actions = Context.Guild.LoadFromFile(true); if (actions.tempBans.Any(tempBan => tempBan.User == userRef.ID)) { @@ -293,7 +292,7 @@ public async Task TempBanWarnUser([RequireHierarchy] UserRef userRef, TimeSpan t await ReplyAsync("Can't temp-ban for less than a minute"); return; } - if (!(Context.Message.Author as SocketGuildUser).HasAdmin()) + if (!(Context.Message.Author as IGuildUser).HasAdmin()) { ModerationSettings settings = Context.Guild.LoadFromFile(false); if (settings?.maxTempAction != null && time > settings.maxTempAction) @@ -302,7 +301,7 @@ public async Task TempBanWarnUser([RequireHierarchy] UserRef userRef, TimeSpan t return; } } - await userRef.Warn(size, reason, Context.Channel as SocketTextChannel, "Discord"); + await userRef.Warn(size, reason, Context.Channel as ITextChannel, "Discord"); TempActionList actions = Context.Guild.LoadFromFile(true); if (actions.tempBans.Any(tempBan => tempBan.User == userRef.ID)) { @@ -327,7 +326,7 @@ public async Task TempMuteUser([RequireHierarchy] UserRef userRef, TimeSpan time return; } ModerationSettings settings = Context.Guild.LoadFromFile(); - if (!(Context.Message.Author as SocketGuildUser).HasAdmin()) + if (!(Context.Message.Author as IGuildUser).HasAdmin()) { if (settings?.maxTempAction != null && time > settings.maxTempAction) { @@ -344,7 +343,7 @@ public async Task TempMuteUser([RequireHierarchy] UserRef userRef, TimeSpan time TempAct oldAct = actions.tempMutes.FirstOrDefault(tempMute => tempMute.User == userRef.ID); if (oldAct != null) { - if (!(Context.Message.Author as SocketGuildUser).HasAdmin() && (oldAct.Length - (DateTime.UtcNow - oldAct.DateBanned)) >= time) + if (!(Context.Message.Author as IGuildUser).HasAdmin() && (oldAct.Length - (DateTime.UtcNow - oldAct.DateBanned)) >= time) { await ReplyAsync($"{Context.User.Mention} please contact your admin(s) in order to shorten length of a punishment"); return; @@ -384,7 +383,7 @@ public async Task TempMuteWarnUser([RequireHierarchy] UserRef userRef, TimeSpan return; } ModerationSettings settings = Context.Guild.LoadFromFile(); - if (!(Context.Message.Author as SocketGuildUser).HasAdmin()) + if (!(Context.Message.Author as IGuildUser).HasAdmin()) { if (settings?.maxTempAction != null && time > settings.maxTempAction) { @@ -397,7 +396,7 @@ public async Task TempMuteWarnUser([RequireHierarchy] UserRef userRef, TimeSpan await ReplyAsync("Muted role is null or invalid"); return; } - await userRef.Warn(1, reason, Context.Channel as SocketTextChannel, Context.Message.GetJumpUrl()); + await userRef.Warn(1, reason, Context.Channel as ITextChannel, Context.Message.GetJumpUrl()); TempActionList actions = Context.Guild.LoadFromFile(true); if (actions.tempMutes.Any(tempMute => tempMute.User == userRef.ID)) { @@ -423,7 +422,7 @@ public async Task TempMuteWarnUser([RequireHierarchy] UserRef userRef, TimeSpan return; } ModerationSettings settings = Context.Guild.LoadFromFile(); - if (!(Context.Message.Author as SocketGuildUser).HasAdmin()) + if (!(Context.Message.Author as IGuildUser).HasAdmin()) { if (settings?.maxTempAction != null && time > settings.maxTempAction) { @@ -436,7 +435,7 @@ public async Task TempMuteWarnUser([RequireHierarchy] UserRef userRef, TimeSpan await ReplyAsync("Muted role is null or invalid"); return; } - await userRef.Warn(size, reason, Context.Channel as SocketTextChannel, "Discord"); + await userRef.Warn(size, reason, Context.Channel as ITextChannel, "Discord"); TempActionList actions = Context.Guild.LoadFromFile(true); if (actions.tempMutes.Any(tempMute => tempMute.User == userRef.ID)) { @@ -506,7 +505,7 @@ public async Task DeleteMany(uint number, UserRef user = null) await ReplyAsync("Invalid number"); return; } - if (user?.GuildUser != null && ((SocketGuildUser)user.GuildUser).Hierarchy >= ((SocketGuildUser)Context.User).Hierarchy) + if (user?.GuildUser != null && user.GuildUser.GetHierarchy() >= ((IGuildUser)Context.User).GetHierarchy()) { await ReplyAsync("Can't target deleted messages belonging to people with higher hierarchy"); return; @@ -549,7 +548,7 @@ public async Task DeleteMany(uint number, UserRef user = null) await ExceptionLogging.AssertAsync(messages.Count <= number); //No need to delete messages or log if no actual messages deleted - await (Context.Channel as SocketTextChannel).DeleteMessagesAsync(messages); + await (Context.Channel as ITextChannel).DeleteMessagesAsync(messages); LogSettings logSettings = Context.Guild.LoadFromFile(false); if (Context.Guild.TryGetChannel(logSettings?.logChannel ?? 0, out IGuildChannel logChannel)) { @@ -560,7 +559,7 @@ public async Task DeleteMany(uint number, UserRef user = null) embed.WithTitle("Mass message deletion"); embed.AddField("Messages searched", $"{searchedMessages} messages", true); embed.AddField("Messages deleted", $"{messages.Count} messages", true); - embed.AddField("Channel", ((SocketTextChannel)Context.Channel).Mention, true); + embed.AddField("Channel", ((ITextChannel)Context.Channel).Mention, true); await ((ISocketMessageChannel)logChannel).SendMessageAsync(embed: embed.Build()); } } diff --git a/BotCatMaxy/Components/Moderation/PunishFunctions.cs b/BotCatMaxy/Components/Moderation/PunishFunctions.cs index 7c662b9..fd9796f 100644 --- a/BotCatMaxy/Components/Moderation/PunishFunctions.cs +++ b/BotCatMaxy/Components/Moderation/PunishFunctions.cs @@ -222,7 +222,7 @@ public static Embed GetEmbed(this List infractions, UserRef userRef, return embed.Build(); } - public static async Task TempBan(this UserRef userRef, TimeSpan time, string reason, SocketCommandContext context, TempActionList actions = null) + public static async Task TempBan(this UserRef userRef, TimeSpan time, string reason, ICommandContext context, TempActionList actions = null) { TempAct tempBan = new TempAct(userRef, time, reason); if (actions == null) actions = context.Guild.LoadFromFile(true); @@ -244,7 +244,7 @@ public static async Task TempBan(this UserRef userRef, TimeSpan time, string rea userRef.ID.RecordAct(context.Guild, tempBan, "tempban", context.Message.GetJumpUrl()); } - public static async Task TempMute(this UserRef userRef, TimeSpan time, string reason, SocketCommandContext context, ModerationSettings settings, TempActionList actions = null) + public static async Task TempMute(this UserRef userRef, TimeSpan time, string reason, ICommandContext context, ModerationSettings settings, TempActionList actions = null) { TempAct tempMute = new TempAct(userRef.ID, time, reason); if (actions == null) actions = context.Guild.LoadFromFile(true); @@ -266,7 +266,7 @@ public static async Task TempMute(this UserRef userRef, TimeSpan time, string re userRef.ID.RecordAct(context.Guild, tempMute, "tempmute", context.Message.GetJumpUrl()); } - public static async Task Notify(this IUser user, string action, string reason, IGuild guild, SocketUser author = null, string article = "from", Color color = default) + public static async Task Notify(this IUser user, string action, string reason, IGuild guild, IUser author = null, string article = "from", Color color = default) { if (color == default) color = Color.LightGrey; var embed = new EmbedBuilder(); diff --git a/BotCatMaxy/Startup/Command Handler.cs b/BotCatMaxy/Startup/Command Handler.cs index 0fe3ad5..f3af495 100644 --- a/BotCatMaxy/Startup/Command Handler.cs +++ b/BotCatMaxy/Startup/Command Handler.cs @@ -61,7 +61,7 @@ private async Task InstallCommandsAsync() // See Dependency Injection guide for more information. await _commands.AddModulesAsync(assembly: Assembly.GetAssembly(typeof(MainClass)), services: services); - await new LogMessage(LogSeverity.Info, "CMDs", "Commands set up").Log(); + //await new LogMessage(LogSeverity.Info, "CMDs", "Commands set up").Log(); } catch (Exception e) { diff --git a/BotCatMaxy/TypeReaders/UserRefTypeReader.cs b/BotCatMaxy/TypeReaders/UserRefTypeReader.cs index e1218b1..d7b3ea5 100644 --- a/BotCatMaxy/TypeReaders/UserRefTypeReader.cs +++ b/BotCatMaxy/TypeReaders/UserRefTypeReader.cs @@ -14,18 +14,18 @@ public class UserRefTypeReader : TypeReader { public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - SocketGuildUser gUserResult = null; - SocketUser userResult; + IGuildUser gUserResult = null; + IUser userResult; //By Mention (1.0) if (MentionUtils.TryParseUser(input, out var id)) { if (context.Guild != null) - gUserResult = await context.Guild.GetUserAsync(id, CacheMode.AllowDownload) as SocketGuildUser; + gUserResult = await context.Guild.GetUserAsync(id, CacheMode.AllowDownload); if (gUserResult != null) return TypeReaderResult.FromSuccess(new UserRef(gUserResult)); else - userResult = await context.Client.GetUserAsync(id, CacheMode.AllowDownload) as SocketUser; + userResult = await context.Client.GetUserAsync(id, CacheMode.AllowDownload); if (userResult != null) return TypeReaderResult.FromSuccess(new UserRef(userResult)); else @@ -36,11 +36,11 @@ public override async Task ReadAsync(ICommandContext context, if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { if (context.Guild != null) - gUserResult = await context.Guild.GetUserAsync(id, CacheMode.AllowDownload) as SocketGuildUser; + gUserResult = await context.Guild.GetUserAsync(id, CacheMode.AllowDownload); if (gUserResult != null) return TypeReaderResult.FromSuccess(new UserRef(gUserResult)); else - userResult = await context.Client.GetUserAsync(id, CacheMode.AllowDownload) as SocketUser; + userResult = await context.Client.GetUserAsync(id, CacheMode.AllowDownload); if (userResult != null) return TypeReaderResult.FromSuccess(new UserRef(userResult)); else diff --git a/BotCatMaxy/Utilities/GeneralUtilities.cs b/BotCatMaxy/Utilities/GeneralUtilities.cs index 0cff8bd..320dc2f 100644 --- a/BotCatMaxy/Utilities/GeneralUtilities.cs +++ b/BotCatMaxy/Utilities/GeneralUtilities.cs @@ -96,7 +96,7 @@ public static string Pluralize(this string s, float num) else return s.Pluralize(); } - public static void DeleteOrRespond(this SocketMessage message, string toSay, IGuild guild, LogSettings settings = null) + public static void DeleteOrRespond(this IMessage message, string toSay, IGuild guild, LogSettings settings = null) { if (settings == null) settings = guild.LoadFromFile(false); if (guild.GetChannelAsync(settings?.pubLogChannel ?? 0).Result == null) message.Channel.SendMessageAsync(toSay); diff --git a/BotCatMaxy/Utilities/PermissionUtilities.cs b/BotCatMaxy/Utilities/PermissionUtilities.cs index f5596c7..5dcdcf9 100644 --- a/BotCatMaxy/Utilities/PermissionUtilities.cs +++ b/BotCatMaxy/Utilities/PermissionUtilities.cs @@ -4,6 +4,7 @@ using Discord.WebSocket; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -97,5 +98,18 @@ public static int GetHierarchy(this IGuildUser user) return user.RoleIds.Max(role => user.Guild.GetRole(role).Position); } + + public static async Task> GetMutualGuildsAsync(this IUser user, IDiscordClient client) + { + if (user is SocketUser socketUser) + return socketUser.MutualGuilds; + + var guilds = await client.GetGuildsAsync(); + var result = new List(1); + foreach (var guild in guilds) + if (await guild.GetUserAsync(user.Id) != null) + result.Add(guild); + return result.ToImmutableArray(); + } } } diff --git a/Tests/CommandTests.cs b/Tests/CommandTests.cs index 5b2618f..06defc8 100644 --- a/Tests/CommandTests.cs +++ b/Tests/CommandTests.cs @@ -1,4 +1,8 @@ -using BotCatMaxy.Startup; +using BotCatMaxy.Cache; +using BotCatMaxy.Data; +using BotCatMaxy.Models; +using BotCatMaxy.Startup; +using BotCatMaxy.TypeReaders; using Discord; using Discord.Commands; using System; @@ -12,18 +16,17 @@ namespace Tests { - public class CommandTests + public class CommandTests : BaseDataTests { MockDiscordClient client = new(); MockGuild guild = new(); CommandService service; CommandHandler handler; - Task channelTask; - public CommandTests() + public CommandTests() : base() { + cache = new SettingsCache(client); client.guilds.Add(guild); - channelTask = guild.CreateTextChannelAsync("TestChannel"); service = new CommandService(); handler = new CommandHandler(client, service); service.CommandExecuted += CommandExecuted; @@ -32,7 +35,7 @@ public CommandTests() [Fact] public async Task BasicCommandCheck() { - var channel = await channelTask as MockMessageChannel; + var channel = await guild.CreateTextChannelAsync("BasicChannel") as MockTextChannel; var users = await guild.GetUsersAsync(); var owner = users.First(user => user.Username == "Owner"); var message = channel.SendMessageAsOther("!toggleserverstorage", owner); @@ -47,6 +50,40 @@ public async Task BasicCommandCheck() Assert.Equal(expected, response.Content); } + [Fact] + public async Task TypeReaderTest() + { + var channel = await guild.CreateTextChannelAsync("TypeReaderChannel") as MockTextChannel; + var users = await guild.GetUsersAsync(); + var owner = users.First(user => user.Username == "Owner"); + var testee = users.First(user => user.Username == "Testee"); + var message = channel.SendMessageAsOther($"!warn {testee.Id} test", owner); + MockCommandContext context = new(client, message); + var userRefReader = new UserRefTypeReader(); + var result = await userRefReader.ReadAsync(context, testee.Id.ToString(), handler.services); + Assert.True(result.IsSuccess); + var match = result.BestMatch as UserRef; + Assert.NotNull(match); + + } + + [Fact] + public async Task WarnCommandTest() + { + var channel = await guild.CreateTextChannelAsync("WarnChannel") as MockTextChannel; + var users = await guild.GetUsersAsync(); + var owner = users.First(user => user.Username == "Owner"); + var testee = users.First(user => user.Username == "Testee"); + var message = channel.SendMessageAsOther($"!warn {testee.Id} test", owner); + MockCommandContext context = new(client, message); + await handler.ExecuteCommand(message, context); + var messages = await channel.GetMessagesAsync().FlattenAsync(); + Assert.Equal(2, messages.Count()); + var infractions = testee.LoadInfractions(false); + Assert.NotNull(infractions); + Assert.NotEmpty(infractions); + } + private Task CommandExecuted(Optional arg1, ICommandContext arg2, IResult result) { if (result.Error == CommandError.Exception) throw ((ExecuteResult)result).Exception; diff --git a/Tests/DataTests.cs b/Tests/DataTests.cs index c84930d..63a0b97 100644 --- a/Tests/DataTests.cs +++ b/Tests/DataTests.cs @@ -1,4 +1,5 @@ using BotCatMaxy; +using BotCatMaxy.Cache; using BotCatMaxy.Data; using BotCatMaxy.Models; using BotCatMaxy.Moderation; @@ -23,11 +24,11 @@ public class BaseDataTests : IDisposable protected MongoDbRunner runner; protected MongoCollectionBase collection; protected IMongoDatabase settingsDB; + protected SettingsCache cache; public BaseDataTests() { runner = MongoDbRunner.Start(); - MongoClient client = new MongoClient(runner.ConnectionString); MainClass.dbClient = client; settingsDB = client.GetDatabase("Settings"); From 5c42ccd3b77ab691472a33a58da159a0ad99e892 Mon Sep 17 00:00:00 2001 From: unixerval Date: Wed, 24 Feb 2021 15:42:50 -0800 Subject: [PATCH 17/42] Add emoji name to reaction filter reason --- BotCatMaxy/Components/Filter/FilterHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BotCatMaxy/Components/Filter/FilterHandler.cs b/BotCatMaxy/Components/Filter/FilterHandler.cs index a48c08c..500a74c 100644 --- a/BotCatMaxy/Components/Filter/FilterHandler.cs +++ b/BotCatMaxy/Components/Filter/FilterHandler.cs @@ -139,7 +139,7 @@ public async Task CheckReaction(Cacheable cachedMessage, IS if (settings.badUEmojis.Contains(reaction.Emote.Name)) { await message.RemoveAllReactionsForEmoteAsync(reaction.Emote); - await context.FilterPunish(gUser, "bad reaction used", settings, delete: false, warnSize: 1); + await context.FilterPunish(gUser, "bad reaction used (" + reaction.Emote.Name + ")", settings, delete: false, warnSize: 1); } } catch (Exception e) From c3ff82fd87fb72c6f75a29929548b00c21d4bb3f Mon Sep 17 00:00:00 2001 From: Markus Washington <75017384+clerically@users.noreply.github.com> Date: Thu, 25 Feb 2021 08:14:00 -0800 Subject: [PATCH 18/42] Update FilterHandler.cs --- BotCatMaxy/Components/Filter/FilterHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BotCatMaxy/Components/Filter/FilterHandler.cs b/BotCatMaxy/Components/Filter/FilterHandler.cs index 500a74c..de928fe 100644 --- a/BotCatMaxy/Components/Filter/FilterHandler.cs +++ b/BotCatMaxy/Components/Filter/FilterHandler.cs @@ -139,7 +139,7 @@ public async Task CheckReaction(Cacheable cachedMessage, IS if (settings.badUEmojis.Contains(reaction.Emote.Name)) { await message.RemoveAllReactionsForEmoteAsync(reaction.Emote); - await context.FilterPunish(gUser, "bad reaction used (" + reaction.Emote.Name + ")", settings, delete: false, warnSize: 1); + await context.FilterPunish(gUser, $"bad reaction used ({reaction.Emote.Name})", settings, delete: false, warnSize: 1); } } catch (Exception e) From 2bea239ad8800b55e1d32514131d3eeefefd4e48 Mon Sep 17 00:00:00 2001 From: Daniel Bereza Date: Sat, 6 Mar 2021 14:59:19 -0800 Subject: [PATCH 19/42] Removes ; as a splitter character for the filter as it is too easy to mistype --- BotCatMaxy/Components/Filter/FilterUtilities.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BotCatMaxy/Components/Filter/FilterUtilities.cs b/BotCatMaxy/Components/Filter/FilterUtilities.cs index a90dc91..d8370a2 100644 --- a/BotCatMaxy/Components/Filter/FilterUtilities.cs +++ b/BotCatMaxy/Components/Filter/FilterUtilities.cs @@ -17,7 +17,7 @@ namespace BotCatMaxy.Components.Filter { public static class FilterUtilities { - readonly static char[] splitters = @"#.,;/\|=_- ".ToCharArray(); + readonly static char[] splitters = @"#.,/\|=_- ".ToCharArray(); public static BadWord CheckForBadWords(this string message, BadWord[] badWords) { From 866601f10ad88b07e4cb20d7d385c963223f2241 Mon Sep 17 00:00:00 2001 From: Daniel Bereza Date: Sat, 6 Mar 2021 15:34:49 -0800 Subject: [PATCH 20/42] Fixes WarnCommandTest and restores !warn permission attributes that were removed for debugging and included in a commit --- BotCatMaxy/Components/Moderation/ModerationCommands.cs | 2 +- BotCatMaxy/Utilities/PermissionUtilities.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index 328adc2..a9791d1 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -26,6 +26,7 @@ public class ModerationCommands : ModuleBase [Command("warn")] [Summary("Warn a user with an option reason.")] + [CanWarn()] public async Task WarnUserAsync([RequireHierarchy] UserRef userRef, [Remainder] string reason) { IUserMessage logMessage = await DiscordLogging.LogWarn(Context.Guild, Context.Message.Author, userRef.ID, reason, Context.Message.GetJumpUrl()); @@ -44,7 +45,6 @@ public async Task WarnUserAsync([RequireHierarchy] UserRef userRef, [Remainder] } } - [RequireContext(ContextType.Guild)] [Command("warn")] [Summary("Warn a user with a specific size, along with an option reason.")] [CanWarn()] diff --git a/BotCatMaxy/Utilities/PermissionUtilities.cs b/BotCatMaxy/Utilities/PermissionUtilities.cs index 5dcdcf9..8b6a4d3 100644 --- a/BotCatMaxy/Utilities/PermissionUtilities.cs +++ b/BotCatMaxy/Utilities/PermissionUtilities.cs @@ -96,6 +96,7 @@ public static int GetHierarchy(this IGuildUser user) if (user.Guild.OwnerId == user.Id) return int.MaxValue; + if (user.RoleIds.Count == 0) return 0; return user.RoleIds.Max(role => user.Guild.GetRole(role).Position); } From 784e092b1235f207b7bcffdce531bf27de5fa110 Mon Sep 17 00:00:00 2001 From: Daniel Bereza Date: Sat, 6 Mar 2021 19:56:52 -0800 Subject: [PATCH 21/42] Cleans up the help commands --- BotCatMaxy/Misc Commands.cs | 61 +++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/BotCatMaxy/Misc Commands.cs b/BotCatMaxy/Misc Commands.cs index e1f5240..c1441a5 100644 --- a/BotCatMaxy/Misc Commands.cs +++ b/BotCatMaxy/Misc Commands.cs @@ -18,7 +18,7 @@ namespace BotCatMaxy { public class MiscCommands : ModuleBase { - public object TempActs { get; private set; } + private const string GITHUB = "https://github.com/Blackcatmaxy/Botcatmaxy"; private readonly CommandService _service; public MiscCommands(CommandService service) @@ -32,10 +32,10 @@ public async Task Help() { var embed = new EmbedBuilder { - Description = "Say !dmhelp in the bot's dms for a full list of commands you can use." + Description = "Say !dmhelp in the bot's direct messages for a full list of commands you can use." }; - embed.AddField("To see commands", "[Click here](https://github.com/Blackcatmaxy/Botcatmaxy/wiki)", true); - embed.AddField("Report issues and contribute at", "[Click here for GitHub link](http://bot.blackcatmaxy.com)", true); + embed.AddField("To see commands", $"[Click Here]({GITHUB}/wiki)", true); + embed.AddField("Report issues and contribute at", $"[Click Here]({GITHUB})", true); await ReplyAsync(embed: embed.Build()); } @@ -47,24 +47,31 @@ private EmbedFieldBuilder MakeCommandField(CommandInfo command) args += $"[{param.Name}] "; } - const string guildMessage = "in guilds only"; - RequireContextAttribute contextAttribute = command.Preconditions.FirstOrDefault(attribute => attribute is RequireContextAttribute) as RequireContextAttribute; - string context = contextAttribute?.Contexts switch - { - ContextType.Guild => guildMessage, - ContextType.DM => "in DMs only", - _ => "anywhere", - }; + const string GUILDMESSSAGE = "in guilds only"; + string context = null; if (command.Preconditions.Any(attribute => attribute is HasAdminAttribute)) - context = $"{guildMessage} \n**Requires administrator permission**"; + context = $"{GUILDMESSSAGE} \n**Requires administrator permission**"; else if (command.Preconditions.Any(attribute => attribute is CanWarnAttribute)) - context = $"{guildMessage} \n**Requires ability to warn**"; + context = $"{GUILDMESSSAGE} \n**Requires ability to warn**"; else { - RequireUserPermissionAttribute permissionAttribute = command.Preconditions.FirstOrDefault(attribute => attribute is RequireUserPermissionAttribute) as RequireUserPermissionAttribute; + var permissionAttribute = command.Preconditions + .FirstOrDefault(attribute => attribute is RequireUserPermissionAttribute) as RequireUserPermissionAttribute; if (permissionAttribute?.GuildPermission != null) - context = $"{guildMessage} \n**Requires {permissionAttribute.GuildPermission.Value.Humanize(LetterCasing.LowerCase)} permission**"; + context = $"{GUILDMESSSAGE} \n**Requires {permissionAttribute.GuildPermission.Value.Humanize(LetterCasing.LowerCase)} permission**"; + } + + if (context == null) + { + var contextAttribute = command.Preconditions + .FirstOrDefault(attribute => attribute is RequireContextAttribute) as RequireContextAttribute; + context = contextAttribute?.Contexts switch + { + ContextType.Guild => GUILDMESSSAGE, + ContextType.DM => "in DMs only", + _ => "anywhere", + }; } string description = command.Summary ?? "*No description.*"; @@ -78,40 +85,34 @@ private EmbedFieldBuilder MakeCommandField(CommandInfo command) } [Command("dmhelp"), Alias("dmbotinfo", "dmcommands", "commandlist", "listcommands")] - [Summary("DM's a list of commands you can use.")] + [Summary("Direct messagess a full list of commands you can use.")] [RequireContext(ContextType.DM)] public async Task DMHelp() { EmbedBuilder extraHelpEmbed = new EmbedBuilder(); - extraHelpEmbed.AddField("Wiki", "[Click Here](https://github.com/Blackcatmaxy/Botcatmaxy/wiki)", true); - extraHelpEmbed.AddField("Submit bugs, enhancements, and contribute", "[Click Here](http://bot.blackcatmaxy.com)", true); + extraHelpEmbed.AddField("Wiki", $"[Click Here]({GITHUB}/wiki)", true); + extraHelpEmbed.AddField("Submit bugs, enhancements, and contribute", $"[Click Here]({GITHUB})", true); await Context.User.SendMessageAsync(embed: extraHelpEmbed.Build()); IUserMessage msg = await Context.User.SendMessageAsync("Fetching commands..."); - ICollection contexts = new List(); - contexts.Add(Context); - + List contexts = new() { Context }; foreach (SocketGuild guild in Context.User.MutualGuilds) { - IMessageChannel channel = guild.Channels.First(channel => channel is IMessageChannel) as IMessageChannel; - if (channel == null) - continue; + var channel = guild.Channels.First(channel => channel is IMessageChannel) as IMessageChannel; - WriteableCommandContext tmpCtx = new WriteableCommandContext + contexts.Add(new WriteableCommandContext { Client = Context.Client, Message = Context.Message, Guild = guild, Channel = channel, User = guild.GetUser(Context.User.Id) - }; - - contexts.Add(tmpCtx); + }); } foreach (ModuleInfo module in _service.Modules) { - EmbedBuilder embed = new EmbedBuilder + EmbedBuilder embed = new() { Title = module.Name }; From 721ed41325fd401b4ae56ba5f8a7d36c52db89ac Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Mon, 8 Mar 2021 21:13:03 -0800 Subject: [PATCH 22/42] Moves around CommandTests so the base behavior can be inherited and split up into several classes Also the Tests project got turned into UTF-8 encoding instead of Windows which sounds fine --- Tests/{ => Commands}/CommandTests.cs | 40 +++++++++--------------- Tests/Commands/ModerationCommandTests.cs | 33 +++++++++++++++++++ Tests/Tests.csproj | 2 +- 3 files changed, 48 insertions(+), 27 deletions(-) rename Tests/{ => Commands}/CommandTests.cs (74%) create mode 100644 Tests/Commands/ModerationCommandTests.cs diff --git a/Tests/CommandTests.cs b/Tests/Commands/CommandTests.cs similarity index 74% rename from Tests/CommandTests.cs rename to Tests/Commands/CommandTests.cs index 06defc8..4c57e11 100644 --- a/Tests/CommandTests.cs +++ b/Tests/Commands/CommandTests.cs @@ -18,10 +18,10 @@ namespace Tests { public class CommandTests : BaseDataTests { - MockDiscordClient client = new(); - MockGuild guild = new(); - CommandService service; - CommandHandler handler; + protected readonly MockDiscordClient client = new(); + protected readonly MockGuild guild = new(); + protected readonly CommandService service; + protected readonly CommandHandler handler; public CommandTests() : base() { @@ -32,6 +32,16 @@ public CommandTests() : base() service.CommandExecuted += CommandExecuted; } + private Task CommandExecuted(Optional arg1, ICommandContext arg2, IResult result) + { + if (result.Error == CommandError.Exception) throw ((ExecuteResult)result).Exception; + if (!result.IsSuccess) throw new Exception(result.ErrorReason); + return Task.CompletedTask; + } + } + + public class BasicCommandTests : CommandTests + { [Fact] public async Task BasicCommandCheck() { @@ -67,28 +77,6 @@ public async Task TypeReaderTest() } - [Fact] - public async Task WarnCommandTest() - { - var channel = await guild.CreateTextChannelAsync("WarnChannel") as MockTextChannel; - var users = await guild.GetUsersAsync(); - var owner = users.First(user => user.Username == "Owner"); - var testee = users.First(user => user.Username == "Testee"); - var message = channel.SendMessageAsOther($"!warn {testee.Id} test", owner); - MockCommandContext context = new(client, message); - await handler.ExecuteCommand(message, context); - var messages = await channel.GetMessagesAsync().FlattenAsync(); - Assert.Equal(2, messages.Count()); - var infractions = testee.LoadInfractions(false); - Assert.NotNull(infractions); - Assert.NotEmpty(infractions); - } - private Task CommandExecuted(Optional arg1, ICommandContext arg2, IResult result) - { - if (result.Error == CommandError.Exception) throw ((ExecuteResult)result).Exception; - if (!result.IsSuccess) throw new Exception(result.ErrorReason); - return Task.CompletedTask; - } } } diff --git a/Tests/Commands/ModerationCommandTests.cs b/Tests/Commands/ModerationCommandTests.cs new file mode 100644 index 0000000..90c1c0c --- /dev/null +++ b/Tests/Commands/ModerationCommandTests.cs @@ -0,0 +1,33 @@ +using BotCatMaxy.Data; +using Discord; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tests.Mocks; +using Tests.Mocks.Guild; +using Xunit; + +namespace Tests.Commands +{ + public class ModerationCommandTests : CommandTests + { + [Fact] + public async Task WarnCommandTest() + { + var channel = await guild.CreateTextChannelAsync("WarnChannel") as MockTextChannel; + var users = await guild.GetUsersAsync(); + var owner = users.First(user => user.Username == "Owner"); + var testee = users.First(user => user.Username == "Testee"); + var message = channel.SendMessageAsOther($"!warn {testee.Id} test", owner); + MockCommandContext context = new(client, message); + await handler.ExecuteCommand(message, context); + var messages = await channel.GetMessagesAsync().FlattenAsync(); + Assert.Equal(2, messages.Count()); + var infractions = testee.LoadInfractions(false); + Assert.NotNull(infractions); + Assert.NotEmpty(infractions); + } + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 1f0c198..6bb320f 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,4 +1,4 @@ - + net5.0 From e1a6cdb27e2f1dbe5c6dea573b2acb52a6e7a9bb Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Mon, 8 Mar 2021 21:48:40 -0800 Subject: [PATCH 23/42] Adds a test for RequireHierarchyAttribute --- Tests/Commands/PreconditionTests.cs | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 Tests/Commands/PreconditionTests.cs diff --git a/Tests/Commands/PreconditionTests.cs b/Tests/Commands/PreconditionTests.cs new file mode 100644 index 0000000..ad09a91 --- /dev/null +++ b/Tests/Commands/PreconditionTests.cs @@ -0,0 +1,53 @@ +using BotCatMaxy.Models; +using Discord; +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tests.Mocks; +using Tests.Mocks.Guild; +using Xunit; + +namespace Tests.Commands +{ + public class PreconditionTests : CommandTests + { + //TODO: Once roles are properly mocked need to add new cases for "Tester" user with position > 1 acting on "Testee" + [Fact] + public async Task CheckHierarchyAttribute() + { + var instance = new RequireHierarchyAttribute(); + + //Set up channel and context + var channel = await guild.CreateTextChannelAsync("HierarchyChannel") as MockTextChannel; + var users = await guild.GetUsersAsync(); + var owner = users.First(user => user.Username == "Owner"); + var testee = users.First(user => user.Username == "Testee"); + var message = channel.SendMessageAsOther($"!warn {testee.Id} test", owner); + MockCommandContext context = new(client, message); + + //Simple function so we don't need to this out for all the cases + async Task CheckPermissions(IGuildUser user) + { + var tasks = new Task[3]; + tasks[0] = instance.CheckPermissionsAsync(context, null, user, null); + tasks[1] = instance.CheckPermissionsAsync(context, null, new UserRef(user), null); + tasks[2] = instance.CheckPermissionsAsync(context, null, user.Id, null); + var results = await Task.WhenAll(tasks); + return results.All(result => result.IsSuccess); //Should always equal even if expecting false + } + + //Check if Owner can do stuff to a user, should always be true + Assert.True(await CheckPermissions(testee)); + + //Check if a user can do stuff to an Owner, should always be false + message = channel.SendMessageAsOther($"!warn {owner.Id} test", testee); //NOTE: The author of the message is what actually matters here + context = new MockCommandContext(client, message); + Assert.False(await CheckPermissions(owner)); + //Check if a user can do stuff to themselves, should always be false unless the behavior is changed in which case the test needs to be changed + Assert.False(await CheckPermissions(testee)); + } + } +} From fe270b94a8e950046f1e0df499796a7ad1046f82 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Tue, 9 Mar 2021 10:00:42 -0800 Subject: [PATCH 24/42] Adds a CommandResult class that wraps RuntimeResult to allow more hooks in tests --- .../CommandHandling/CommandHandler.cs} | 6 +++++ .../CommandHandling/CommandResult.cs | 27 +++++++++++++++++++ .../Moderation/ModerationCommands.cs | 14 +++++----- Tests/Commands/CommandTests.cs | 2 -- 4 files changed, 39 insertions(+), 10 deletions(-) rename BotCatMaxy/{Startup/Command Handler.cs => Components/CommandHandling/CommandHandler.cs} (98%) create mode 100644 BotCatMaxy/Components/CommandHandling/CommandResult.cs diff --git a/BotCatMaxy/Startup/Command Handler.cs b/BotCatMaxy/Components/CommandHandling/CommandHandler.cs similarity index 98% rename from BotCatMaxy/Startup/Command Handler.cs rename to BotCatMaxy/Components/CommandHandling/CommandHandler.cs index f3af495..3e95bbb 100644 --- a/BotCatMaxy/Startup/Command Handler.cs +++ b/BotCatMaxy/Components/CommandHandling/CommandHandler.cs @@ -107,6 +107,12 @@ await _commands.ExecuteAsync( private async Task CommandExecuted(Optional command, ICommandContext context, IResult result) { + if (result is CommandResult) + { + await context.Channel.SendMessageAsync(result.ErrorReason); + return; + } + if (result.IsSuccess || result.Error == CommandError.UnknownCommand) { return; diff --git a/BotCatMaxy/Components/CommandHandling/CommandResult.cs b/BotCatMaxy/Components/CommandHandling/CommandResult.cs new file mode 100644 index 0000000..74053c7 --- /dev/null +++ b/BotCatMaxy/Components/CommandHandling/CommandResult.cs @@ -0,0 +1,27 @@ +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BotCatMaxy.Startup; + +namespace Discord.Commands +{ + /// + /// A wrapper of for communicating the result of a command in the + /// + public class CommandResult : RuntimeResult + { + public CommandResult(CommandError? error, string reason) : base(error, reason) + { + + } + + public static CommandResult FromError(string reason) + => new(CommandError.Unsuccessful, reason); + + public static CommandResult FromSuccess(string reason) + => new(null, reason); + } +} diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index a9791d1..9e29384 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -20,7 +20,7 @@ namespace BotCatMaxy public class ModerationCommands : ModuleBase { #if !TEST - [DontInject] + [DontInject] #endif public InteractivityService Interactivity { get; set; } @@ -76,7 +76,7 @@ public async Task DMUserWarnsAsync(UserRef userRef = null, int amount = 50) await ReplyAsync("Why would you want to see that many infractions?"); return; } - + var mutualGuilds = (await Context.Message.Author.GetMutualGuildsAsync(Context.Client)).ToArray(); if (userRef == null) userRef = new UserRef(Context.Message.Author); @@ -144,25 +144,23 @@ public async Task CheckUserWarnsAsync(UserRef userRef = null, int amount = 5) [Summary("Removes a warn from a user.")] [Alias("warnremove", "removewarning")] [HasAdmin()] - public async Task RemoveWarnAsync([RequireHierarchy] UserRef userRef, int index) + public async Task RemoveWarnAsync([RequireHierarchy] UserRef userRef, int index) { List infractions = userRef.LoadInfractions(Context.Guild, false); if (infractions?.Count is null or 0) { - await ReplyAsync("Infractions are null"); - return; + return CommandResult.FromError("Infractions are null"); } if (infractions.Count < index || index <= 0) { - await ReplyAsync("Invalid infraction number"); - return; + return CommandResult.FromError("Invalid infraction number"); } string reason = infractions[index - 1].Reason; infractions.RemoveAt(index - 1); userRef.SaveInfractions(infractions, Context.Guild); await userRef.User?.TryNotify($"Your {index.Ordinalize()} warning in {Context.Guild.Name} discord for {reason} has been removed"); - await ReplyAsync("Removed " + userRef.Mention() + "'s warning for " + reason); + return CommandResult.FromSuccess($"Removed {userRef.Mention()}'s warning for {reason}"); } [Command("kickwarn")] diff --git a/Tests/Commands/CommandTests.cs b/Tests/Commands/CommandTests.cs index 4c57e11..0cfbf2b 100644 --- a/Tests/Commands/CommandTests.cs +++ b/Tests/Commands/CommandTests.cs @@ -76,7 +76,5 @@ public async Task TypeReaderTest() Assert.NotNull(match); } - - } } From 624f11d8db2e068af9125fe7ab5cced3eb655e43 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Fri, 12 Mar 2021 12:49:37 -0800 Subject: [PATCH 25/42] Adds test for removing warns and mocks DMChannel --- .../CommandHandling/CommandHandler.cs | 3 +- .../Moderation/ModerationCommands.cs | 10 +++-- BotCatMaxy/Components/Moderation/Settings.cs | 5 ++- Tests/Commands/CommandTests.cs | 45 +++++++++++++++---- Tests/Commands/ModerationCommandTests.cs | 26 ++++++++--- Tests/Mocks/MockDMChannel.cs | 28 ++++++++++++ Tests/Mocks/MockDiscordClient.cs | 2 +- Tests/Mocks/MockUser.cs | 4 +- 8 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 Tests/Mocks/MockDMChannel.cs diff --git a/BotCatMaxy/Components/CommandHandling/CommandHandler.cs b/BotCatMaxy/Components/CommandHandling/CommandHandler.cs index 3e95bbb..2cf6a11 100644 --- a/BotCatMaxy/Components/CommandHandling/CommandHandler.cs +++ b/BotCatMaxy/Components/CommandHandling/CommandHandler.cs @@ -109,7 +109,8 @@ private async Task CommandExecuted(Optional command, ICommandContex { if (result is CommandResult) { - await context.Channel.SendMessageAsync(result.ErrorReason); + if (!string.IsNullOrEmpty(result.ErrorReason)) + await context.Channel.SendMessageAsync(result.ErrorReason); return; } diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index 9e29384..2a5488e 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -27,13 +27,16 @@ public class ModerationCommands : ModuleBase [Command("warn")] [Summary("Warn a user with an option reason.")] [CanWarn()] - public async Task WarnUserAsync([RequireHierarchy] UserRef userRef, [Remainder] string reason) + public async Task WarnUserAsync([RequireHierarchy] UserRef userRef, [Remainder] string reason) { IUserMessage logMessage = await DiscordLogging.LogWarn(Context.Guild, Context.Message.Author, userRef.ID, reason, Context.Message.GetJumpUrl()); WarnResult result = await userRef.Warn(1, reason, Context.Channel as ITextChannel, logLink: logMessage?.GetJumpUrl()); if (result.success) + { Context.Message.DeleteOrRespond($"{userRef.Mention()} has gotten their {result.warnsAmount.Suffix()} infraction for {reason}", Context.Guild); + return CommandResult.FromSuccess(null); + } else { if (logMessage != null) @@ -41,7 +44,7 @@ public async Task WarnUserAsync([RequireHierarchy] UserRef userRef, [Remainder] DiscordLogging.deletedMessagesCache.Enqueue(logMessage.Id); await logMessage.DeleteAsync(); } - await ReplyAsync(result.description.Truncate(1500)); + return CommandResult.FromError(result.description.Truncate(1500)); } } @@ -159,7 +162,8 @@ public async Task RemoveWarnAsync([RequireHierarchy] UserRef user infractions.RemoveAt(index - 1); userRef.SaveInfractions(infractions, Context.Guild); - await userRef.User?.TryNotify($"Your {index.Ordinalize()} warning in {Context.Guild.Name} discord for {reason} has been removed"); + if (userRef.User != null) //Can't use null propagation on awaited tasks since it would be awaiting null + await userRef.User.TryNotify($"Your {index.Ordinalize()} warning in {Context.Guild.Name} discord for {reason} has been removed"); return CommandResult.FromSuccess($"Removed {userRef.Mention()}'s warning for {reason}"); } diff --git a/BotCatMaxy/Components/Moderation/Settings.cs b/BotCatMaxy/Components/Moderation/Settings.cs index 0306176..f77d8ce 100644 --- a/BotCatMaxy/Components/Moderation/Settings.cs +++ b/BotCatMaxy/Components/Moderation/Settings.cs @@ -39,9 +39,10 @@ public async Task SettingsInfo() [Command("toggleserverstorage")] [Summary("Legacy feature. Run for instruction on how to enable.")] [HasAdmin] - public async Task ToggleServerIDUse() + public async Task ToggleServerIDUse() { - await ReplyAsync("This is a legacy feature, if you want this done now contact blackcatmaxy@gmail.com with your guild invite and your username so I can get back to you"); + return CommandResult.FromSuccess( + "This is a legacy feature, if you want this done now contact blackcatmaxy@gmail.com with your guild invite and your username so I can get back to you"); } [Command("allowwarn"), Alias("allowtowarn")] diff --git a/Tests/Commands/CommandTests.cs b/Tests/Commands/CommandTests.cs index 0cfbf2b..2c719a2 100644 --- a/Tests/Commands/CommandTests.cs +++ b/Tests/Commands/CommandTests.cs @@ -22,6 +22,8 @@ public class CommandTests : BaseDataTests protected readonly MockGuild guild = new(); protected readonly CommandService service; protected readonly CommandHandler handler; + protected CommandResult commandResult; + protected TaskCompletionSource completionSource; public CommandTests() : base() { @@ -32,28 +34,53 @@ public CommandTests() : base() service.CommandExecuted += CommandExecuted; } - private Task CommandExecuted(Optional arg1, ICommandContext arg2, IResult result) + public async Task TryExecuteCommand(string text, IUser user, MockTextChannel channel) { - if (result.Error == CommandError.Exception) throw ((ExecuteResult)result).Exception; - if (!result.IsSuccess) throw new Exception(result.ErrorReason); + Assert.NotNull(channel); + Assert.NotNull(user); + var message = channel.SendMessageAsOther(text, user); + var context = new MockCommandContext(client, message); + completionSource = new TaskCompletionSource(); + await handler.ExecuteCommand(message, context); + return await completionSource.Task; + } + + /// + /// Executes after command is finished with full info 's CommandExecuted + /// + private Task CommandExecuted(Optional arg1, ICommandContext context, IResult result) + { + if (result is CommandResult commandResult) + { + this.commandResult = commandResult; + completionSource.SetResult(commandResult); + } + else if (result.Error == CommandError.Exception) completionSource.SetException(((ExecuteResult)result).Exception); + else if (!result.IsSuccess) completionSource.SetException(new Exception(result.ErrorReason)); + else completionSource.SetResult(new CommandResult(null, "Test")); + return Task.CompletedTask; } } + public class CommandTestException : Exception + { + public CommandTestException(IResult result) : base(result.ErrorReason) { } + } + public class BasicCommandTests : CommandTests { [Fact] public async Task BasicCommandCheck() { var channel = await guild.CreateTextChannelAsync("BasicChannel") as MockTextChannel; + var messages = await channel.GetMessagesAsync().FlattenAsync(); + Assert.Empty(messages); var users = await guild.GetUsersAsync(); var owner = users.First(user => user.Username == "Owner"); - var message = channel.SendMessageAsOther("!toggleserverstorage", owner); - MockCommandContext context = new(client, message); - Assert.True(context.Channel is IGuildChannel); - Assert.True(context.User is IGuildUser); - await handler.ExecuteCommand(message, context); - var messages = await channel.GetMessagesAsync().FlattenAsync(); + var result = await TryExecuteCommand("!toggleserverstorage", owner, channel); + messages = await channel.GetMessagesAsync().FlattenAsync(); + Assert.True(result.IsSuccess); Assert.Equal(2, messages.Count()); var response = messages.First(); var expected = "This is a legacy feature, if you want this done now contact blackcatmaxy@gmail.com with your guild invite and your username so I can get back to you"; diff --git a/Tests/Commands/ModerationCommandTests.cs b/Tests/Commands/ModerationCommandTests.cs index 90c1c0c..c280e95 100644 --- a/Tests/Commands/ModerationCommandTests.cs +++ b/Tests/Commands/ModerationCommandTests.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Tests.Mocks; +using BotCatMaxy.Moderation; using Tests.Mocks.Guild; using Xunit; @@ -14,20 +15,31 @@ namespace Tests.Commands public class ModerationCommandTests : CommandTests { [Fact] - public async Task WarnCommandTest() + public async Task WarnCommandAndRemoveTest() { var channel = await guild.CreateTextChannelAsync("WarnChannel") as MockTextChannel; var users = await guild.GetUsersAsync(); var owner = users.First(user => user.Username == "Owner"); var testee = users.First(user => user.Username == "Testee"); - var message = channel.SendMessageAsOther($"!warn {testee.Id} test", owner); - MockCommandContext context = new(client, message); - await handler.ExecuteCommand(message, context); - var messages = await channel.GetMessagesAsync().FlattenAsync(); - Assert.Equal(2, messages.Count()); + + var result = await TryExecuteCommand($"!removewarn {testee.Id} 1", owner, channel); + Assert.False(result.IsSuccess); //No infractions currently + result = await TryExecuteCommand($"!warn {testee.Id} test", owner, channel); + Assert.True(result.IsSuccess); var infractions = testee.LoadInfractions(false); Assert.NotNull(infractions); - Assert.NotEmpty(infractions); + Assert.Single(infractions); + result = await TryExecuteCommand($"!warn {testee.Id} test", owner, channel); + Assert.True(result.IsSuccess); + infractions = testee.LoadInfractions(false); + Assert.Equal(2, infractions.Count); + + result = await TryExecuteCommand($"!removewarn {testee.Id} 1", owner, channel); + Assert.True(result.IsSuccess); + result = await TryExecuteCommand($"!removewarn {testee.Id} 2", owner, channel); + Assert.False(result.IsSuccess); //Should be out of bounds and fail now since we removed 1 out of 2 of the infractions + infractions = testee.LoadInfractions(false); + Assert.Single(infractions); } } } diff --git a/Tests/Mocks/MockDMChannel.cs b/Tests/Mocks/MockDMChannel.cs new file mode 100644 index 0000000..27ca5e6 --- /dev/null +++ b/Tests/Mocks/MockDMChannel.cs @@ -0,0 +1,28 @@ +using Discord; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tests.Mocks +{ + public class MockDMChannel : MockMessageChannel, IDMChannel + { + public MockDMChannel(ISelfUser bot, IUser user) : base(bot, $"{user.Username}'s DM") + { + Recipients = new IUser[] { user, bot }.ToImmutableArray(); + Recipient = user; + } + + public IUser Recipient { get; init; } + + public IReadOnlyCollection Recipients { get; init; } + + public Task CloseAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Mocks/MockDiscordClient.cs b/Tests/Mocks/MockDiscordClient.cs index 1964fb2..c47e553 100644 --- a/Tests/Mocks/MockDiscordClient.cs +++ b/Tests/Mocks/MockDiscordClient.cs @@ -96,7 +96,7 @@ public Task GetRecommendedShardCountAsync(RequestOptions options = null) public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { - throw new NotImplementedException(); + return Task.FromResult(null); } public Task GetUserAsync(string username, string discriminator, RequestOptions options = null) diff --git a/Tests/Mocks/MockUser.cs b/Tests/Mocks/MockUser.cs index d7b3a35..4f5c90e 100644 --- a/Tests/Mocks/MockUser.cs +++ b/Tests/Mocks/MockUser.cs @@ -18,6 +18,7 @@ public MockUser(string username) Id = (ulong)random.Next(0, int.MaxValue); } + MockDMChannel channel; public string AvatarId => throw new NotImplementedException(); public string Discriminator => $"#{DiscriminatorValue}"; @@ -58,7 +59,8 @@ public string GetDefaultAvatarUrl() public Task GetOrCreateDMChannelAsync(RequestOptions options = null) { - throw new NotImplementedException(); + channel ??= new MockDMChannel(new MockSelfUser(), this); + return Task.FromResult(channel); } } } From 0471e4544e23a15d6edfa56fa32624cf85c64977 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Fri, 12 Mar 2021 13:40:57 -0800 Subject: [PATCH 26/42] Adds info shields to README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57a6ae3..73f71ec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Botcatmaxy +# Botcatmaxy ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/blackcatmaxy/botcatmaxy/build) ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/blackcatmaxy/botcatmaxy) ![GitHub issues](https://img.shields.io/github/issues-raw/blackcatmaxy/botcatmaxy) ![GitHub issues by-label](https://img.shields.io/github/issues-raw/blackcatmaxy/botcatmaxy/bug) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/blackcatmaxy/botcatmaxy/development?label=last%20dev%20branch%20commit) Botcatmaxy is a discord bot focused on helping moderate discord servers. People who have used MEE6 will notice a similarity with the basic commands but Botcatmaxy has many changes on top of the basic "moderation bot" that you can see over in the wiki. It has a robust autofilter that does a lot of work to balance false flags with catching things that other bots wouldn't. Of course we also provide tons of customization in this aspect if you really would like to avoid false warnings. -If you're interested [click here to add the bot to your Discord server!](https://discord.com/api/oauth2/authorize?client_id=488796531512573953&permissions=403008582&scope=bot) +If you're interested [click here to add the bot to your Discord server](https://discord.com/api/oauth2/authorize?client_id=488796531512573953&permissions=403008582&scope=bot)! We also have very quick support and discussion over on [our Discord server](https://discord.gg/hgxynGZ). We are always looking for feedback and ways to improve, even when it's just focusing on one feature over another or as massive as a feature just not doing its job. There's a lot of bots out there, and what's best can differ from server to server, so we always value your input. From 6b5b67ee1c5b46688c2f93c81dcb4723aa3a7284 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Wed, 17 Mar 2021 16:26:28 -0700 Subject: [PATCH 27/42] Updates README.MD to make the shields clickable --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73f71ec..72ac173 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Botcatmaxy ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/blackcatmaxy/botcatmaxy/build) ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/blackcatmaxy/botcatmaxy) ![GitHub issues](https://img.shields.io/github/issues-raw/blackcatmaxy/botcatmaxy) ![GitHub issues by-label](https://img.shields.io/github/issues-raw/blackcatmaxy/botcatmaxy/bug) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/blackcatmaxy/botcatmaxy/development?label=last%20dev%20branch%20commit) +# Botcatmaxy [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/blackcatmaxy/botcatmaxy/build)](https://github.com/Blackcatmaxy/Botcatmaxy/actions) [![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/blackcatmaxy/botcatmaxy)](https://github.com/Blackcatmaxy/Botcatmaxy/issues?q=is%3Aissue+is%3Aclosed) [![GitHub issues](https://img.shields.io/github/issues-raw/blackcatmaxy/botcatmaxy)](https://github.com/Blackcatmaxy/Botcatmaxy/issues) [![GitHub issues by-label](https://img.shields.io/github/issues-raw/blackcatmaxy/botcatmaxy/bug)](https://github.com/Blackcatmaxy/Botcatmaxy/labels/bug) [![GitHub last commit (branch)](https://img.shields.io/github/last-commit/blackcatmaxy/botcatmaxy/development?label=last%20dev%20branch%20commit)](https://github.com/Blackcatmaxy/Botcatmaxy/tree/development) Botcatmaxy is a discord bot focused on helping moderate discord servers. People who have used MEE6 will notice a similarity with the basic commands but Botcatmaxy has many changes on top of the basic "moderation bot" that you can see over in the wiki. It has a robust autofilter that does a lot of work to balance false flags with catching things that other bots wouldn't. Of course we also provide tons of customization in this aspect if you really would like to avoid false warnings. If you're interested [click here to add the bot to your Discord server](https://discord.com/api/oauth2/authorize?client_id=488796531512573953&permissions=403008582&scope=bot)! From 16117ef6cffe98e869ab5b55de7b4eba47d5288c Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Tue, 30 Mar 2021 15:07:29 -0700 Subject: [PATCH 28/42] Makes InteractiveService not ignored by the DI when it's needed --- BotCatMaxy/BotCatMaxy.csproj | 6 ++--- .../CommandHandling/InteractiveModule.cs | 23 +++++++++++++++++++ .../Components/Filter/FilterCommands.cs | 11 ++++----- .../Components/Logging/LoggingCommands.cs | 18 ++++++++++----- .../Moderation/ModerationCommands.cs | 9 ++++---- BotCatMaxy/Components/ReportModule.cs | 14 +++++------ .../Components/Slowmode/SlowmodeCommands.cs | 2 +- 7 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 BotCatMaxy/Components/CommandHandling/InteractiveModule.cs diff --git a/BotCatMaxy/BotCatMaxy.csproj b/BotCatMaxy/BotCatMaxy.csproj index 18496cc..83f7d04 100644 --- a/BotCatMaxy/BotCatMaxy.csproj +++ b/BotCatMaxy/BotCatMaxy.csproj @@ -23,9 +23,9 @@ - - - + + + diff --git a/BotCatMaxy/Components/CommandHandling/InteractiveModule.cs b/BotCatMaxy/Components/CommandHandling/InteractiveModule.cs new file mode 100644 index 0000000..de1acae --- /dev/null +++ b/BotCatMaxy/Components/CommandHandling/InteractiveModule.cs @@ -0,0 +1,23 @@ +using Interactivity; +using System; + +namespace Discord.Commands +{ + /// + /// Wrapper around with T as + /// and including an property + /// + public class InteractiveModule : ModuleBase + { + /// + /// This constructor is required for DI to work in both test environment and release without + /// mocking of + /// + public InteractiveModule(IServiceProvider service) : base() + { + Interactivity = (InteractivityService)service.GetService(typeof(InteractivityService)); + } + + protected InteractivityService Interactivity { get; init; } + } +} \ No newline at end of file diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index d03b4f6..ca409f7 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -16,12 +16,11 @@ namespace BotCatMaxy.Components.Filter [Group("automod")] [Summary("Manages the automoderator.")] [Alias("automod", "auto -mod", "filter")] - public class FilterCommands : ModuleBase + public class FilterCommands : InteractiveModule { -#if !TEST - [DontInject] -#endif - public InteractivityService Interactivity { get; set; } + public FilterCommands(IServiceProvider service) : base(service) + { + } [Command("list")] [Summary("View filter information.")] @@ -29,7 +28,7 @@ public class FilterCommands : ModuleBase [RequireContext(ContextType.DM, ErrorMessage = "This command now only works in the bot's DMs")] public async Task ListAutoMod(string extension = "") { - var mutualGuilds = Context.Message.Author.MutualGuilds.ToArray(); + var mutualGuilds = (Context.Message.Author as SocketUser).MutualGuilds.ToArray(); var guildsEmbed = new EmbedBuilder(); guildsEmbed.WithTitle("Reply with the the number next to the guild you want to check the filter info from"); diff --git a/BotCatMaxy/Components/Logging/LoggingCommands.cs b/BotCatMaxy/Components/Logging/LoggingCommands.cs index 61f3fc0..12e3bac 100644 --- a/BotCatMaxy/Components/Logging/LoggingCommands.cs +++ b/BotCatMaxy/Components/Logging/LoggingCommands.cs @@ -3,6 +3,7 @@ using Discord; using Discord.Commands; using Discord.WebSocket; +using System; using System.Threading.Tasks; namespace BotCatMaxy.Components.Logging @@ -11,8 +12,12 @@ namespace BotCatMaxy.Components.Logging [Group("logs"), Alias("logs")] [Summary("Manages logging.")] [RequireContext(ContextType.Guild)] - public class LoggingCommands : ModuleBase + public class LoggingCommands : InteractiveModule { + public LoggingCommands(IServiceProvider service) : base(service) + { + } + [Command("setchannel"), Alias("sethere")] [Summary("Sets the logging channel to the current channel.")] [HasAdmin] @@ -27,7 +32,7 @@ public async Task SetLogChannel() return; } - if (Context.Client.GetChannel(settings.logChannel ?? 0) == Context.Channel) + if (Context.Client.GetChannelAsync(settings.logChannel ?? 0) == Context.Channel) { await message.ModifyAsync(msg => msg.Content = "This channel already is the logging channel"); return; @@ -56,7 +61,7 @@ public async Task SetPubLogChannel(string setNull = null) await message.ModifyAsync(msg => msg.Content = "Set public log channel to null"); return; } - if (Context.Client.GetChannel(settings.pubLogChannel ?? 0) == Context.Channel) + if (Context.Client.GetChannelAsync(settings.pubLogChannel ?? 0) == Context.Channel) { await message.ModifyAsync(msg => msg.Content = "This channel already is the logging channel"); return; @@ -74,6 +79,7 @@ public async Task SetPubLogChannel(string setNull = null) [Summary("Views logging settings.")] public async Task DebugLogSettings() { + var socketContext = Context as SocketCommandContext; //Not ready for testing yet LogSettings settings = Context.Guild.LoadFromFile(); if (settings == null) @@ -84,7 +90,7 @@ public async Task DebugLogSettings() var embed = new EmbedBuilder(); - SocketTextChannel logChannel = Context.Guild.GetTextChannel(settings.logChannel ?? 0); + SocketTextChannel logChannel = socketContext.Guild.GetTextChannel(settings.logChannel ?? 0); if (logChannel == null) { _ = ReplyAsync("Logging channel is null"); @@ -95,7 +101,7 @@ public async Task DebugLogSettings() embed.AddField("Log deleted messages", settings.logDeletes, true); if (settings.pubLogChannel != null) { - var pubLogChannel = Context.Guild.GetTextChannel(settings.pubLogChannel.Value); + var pubLogChannel = socketContext.Guild.GetTextChannel(settings.pubLogChannel.Value); if (pubLogChannel == null) embed.AddField("Public Log Channel", "Improper value set", true); else embed.AddField("Public Log Channel", pubLogChannel.Mention, true); } @@ -162,7 +168,7 @@ public async Task SetBackupLogChannel(string setNull = null) await ReplyAsync("Set backup channel to null"); return; } - if (Context.Client.GetChannel(settings.backupChannel ?? 0) == Context.Channel) + if (await Context.Client.GetChannelAsync(settings.backupChannel ?? 0) == Context.Channel) { await ReplyAsync("This channel already is the backup channel"); return; diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index 2a5488e..ac7df6a 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -17,12 +17,11 @@ namespace BotCatMaxy { [Name("Moderation")] - public class ModerationCommands : ModuleBase + public class ModerationCommands : InteractiveModule { -#if !TEST - [DontInject] -#endif - public InteractivityService Interactivity { get; set; } + public ModerationCommands(IServiceProvider service) : base(service) + { + } [Command("warn")] [Summary("Warn a user with an option reason.")] diff --git a/BotCatMaxy/Components/ReportModule.cs b/BotCatMaxy/Components/ReportModule.cs index 554ec94..568766b 100644 --- a/BotCatMaxy/Components/ReportModule.cs +++ b/BotCatMaxy/Components/ReportModule.cs @@ -11,12 +11,11 @@ using System.Threading.Tasks; [Name("Report")] -public class ReportModule : ModuleBase +public class ReportModule : InteractiveModule { -#if !TEST - [DontInject] -#endif - public InteractivityService Interactivity { get; set; } + public ReportModule(IServiceProvider service) : base(service) + { + } [Command("report", RunMode = RunMode.Async)] [Summary("Create a new report.")] @@ -25,10 +24,11 @@ public async Task Report() { try { + var socketContext = Context as SocketCommandContext; var guildsEmbed = new EmbedBuilder(); guildsEmbed.WithTitle("Reply with the the number next to the guild you want to make the report in"); - var mutualGuilds = Context.User.MutualGuilds.ToArray(); - for (int i = 0; i < Context.User.MutualGuilds.Count; i++) + var mutualGuilds = socketContext.User.MutualGuilds.ToArray(); + for (int i = 0; i < mutualGuilds.Length; i++) { guildsEmbed.AddField($"[{i + 1}] {mutualGuilds[i].Name} discord", mutualGuilds[i].Id); } diff --git a/BotCatMaxy/Components/Slowmode/SlowmodeCommands.cs b/BotCatMaxy/Components/Slowmode/SlowmodeCommands.cs index f2487b8..9bb6c40 100644 --- a/BotCatMaxy/Components/Slowmode/SlowmodeCommands.cs +++ b/BotCatMaxy/Components/Slowmode/SlowmodeCommands.cs @@ -14,7 +14,7 @@ namespace BotCatMaxy.Components.Settings [Name("Slowmode")] [RequireUserPermission(ChannelPermission.ManageChannels)] [RequireBotPermission(ChannelPermission.ManageChannels)] - public class SlowmodeCommands : ModuleBase + public class SlowmodeCommands : ModuleBase { [Command("setslowmode"), Alias("setcooldown", "slowmodeset")] [Summary("Sets this channel's slowmode.")] From a35fa8c355f1c7457208fe64ee2931426840e49c Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Tue, 30 Mar 2021 15:17:33 -0700 Subject: [PATCH 29/42] Cleans up the csprojects --- BotCatMaxy/BotCatMaxy.csproj | 16 ++++++++-------- Tests/Tests.csproj | 6 +----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/BotCatMaxy/BotCatMaxy.csproj b/BotCatMaxy/BotCatMaxy.csproj index 83f7d04..0c627ae 100644 --- a/BotCatMaxy/BotCatMaxy.csproj +++ b/BotCatMaxy/BotCatMaxy.csproj @@ -5,21 +5,21 @@ net5.0 BotCatMaxy.MainClass BotCatMaxy - Blackcatmaxy + https://github.com/Blackcatmaxy/Botcatmaxy/ + https://github.com/Blackcatmaxy/Botcatmaxy/graphs/contributors true - - + TRACE true - false - DEBUG;TRACE - - true - AnyCPU + + + DEBUG + + diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 6bb320f..6247a43 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,14 +1,11 @@  - net5.0 - false - TEST; - DEBUG;TRACE;TESTING; + DEBUG;TRACE; 2 @@ -29,5 +26,4 @@ - From 53699721a07e0ed37da684a064c677ad7d5a00bd Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Tue, 30 Mar 2021 15:26:49 -0700 Subject: [PATCH 30/42] Updates some packages --- BotCatMaxy/BotCatMaxy.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BotCatMaxy/BotCatMaxy.csproj b/BotCatMaxy/BotCatMaxy.csproj index 0c627ae..be03994 100644 --- a/BotCatMaxy/BotCatMaxy.csproj +++ b/BotCatMaxy/BotCatMaxy.csproj @@ -26,10 +26,10 @@ - + - + From 2783d6e30b395c6ee8a4d2048c745472b1ca8fd1 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Tue, 30 Mar 2021 15:35:41 -0700 Subject: [PATCH 31/42] Fixes typos --- .../Components/Filter/FilterCommands.cs | 2 +- .../Moderation/ModerationCommands.cs | 24 +++++++++---------- BotCatMaxy/Components/ReportModule.cs | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index ca409f7..14de998 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -31,7 +31,7 @@ public async Task ListAutoMod(string extension = "") var mutualGuilds = (Context.Message.Author as SocketUser).MutualGuilds.ToArray(); var guildsEmbed = new EmbedBuilder(); - guildsEmbed.WithTitle("Reply with the the number next to the guild you want to check the filter info from"); + guildsEmbed.WithTitle("Reply with the number next to the guild you want to check the filter info from"); for (int i = 0; i < mutualGuilds.Length; i++) { diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index ac7df6a..c70ab91 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -24,7 +24,7 @@ public ModerationCommands(IServiceProvider service) : base(service) } [Command("warn")] - [Summary("Warn a user with an option reason.")] + [Summary("Warn a user with a reason.")] [CanWarn()] public async Task WarnUserAsync([RequireHierarchy] UserRef userRef, [Remainder] string reason) { @@ -48,7 +48,7 @@ public async Task WarnUserAsync([RequireHierarchy] UserRef userRe } [Command("warn")] - [Summary("Warn a user with a specific size, along with an option reason.")] + [Summary("Warn a user with a specific size, along with a reason.")] [CanWarn()] public async Task WarnWithSizeUserAsync([RequireHierarchy] UserRef userRef, float size, [Remainder] string reason) { @@ -167,7 +167,7 @@ public async Task RemoveWarnAsync([RequireHierarchy] UserRef user } [Command("kickwarn")] - [Summary("Kicks a user, and warns them with an option reason.")] + [Summary("Kicks a user, and warns them with an optional reason.")] [Alias("warnkick", "warnandkick", "kickandwarn")] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.KickMembers)] @@ -178,11 +178,11 @@ public async Task KickAndWarn([RequireHierarchy] SocketGuildUser user, [Remainde _ = user.Notify("kicked", reason, Context.Guild, Context.Message.Author); await user.KickAsync(reason); - Context.Message.DeleteOrRespond($"{user.Mention} has been kicked for {reason} ", Context.Guild); + Context.Message.DeleteOrRespond($"{user.Mention} has been kicked for {reason}", Context.Guild); } [Command("kickwarn")] - [Summary("Kicks a user, and warns them with a specific size along with an option reason.")] + [Summary("Kicks a user, and warns them with a specific size along with an optional reason.")] [Alias("warnkick", "warnandkick", "kickandwarn")] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.KickMembers)] @@ -193,7 +193,7 @@ public async Task KickAndWarn([RequireHierarchy] SocketGuildUser user, float siz _ = user.Notify("kicked", reason, Context.Guild, Context.Message.Author); await user.KickAsync(reason); - Context.Message.DeleteOrRespond($"{user.Mention} has been kicked for {reason} ", Context.Guild); + Context.Message.DeleteOrRespond($"{user.Mention} has been kicked for {reason}", Context.Guild); } [Command("tempban")] @@ -248,7 +248,7 @@ public async Task TempBanUser([RequireHierarchy] UserRef userRef, TimeSpan time, } [Command("tempbanwarn")] - [Summary("Temporarily bans a user, and warns them with an option reason.")] + [Summary("Temporarily bans a user, and warns them with a reason.")] [Alias("tbanwarn", "temp-banwarn", "tempbanandwarn", "tbw")] [RequireContext(ContextType.Guild)] [RequireBotPermission(GuildPermission.BanMembers)] @@ -282,7 +282,7 @@ public async Task TempBanWarnUser([RequireHierarchy] UserRef userRef, TimeSpan t [Command("tempbanwarn")] [Alias("tbanwarn", "temp-banwarn", "tempbanwarn", "warntempban", "tbw")] - [Summary("Temporarily bans a user, and warns them with a specific size along with an option reason.")] + [Summary("Temporarily bans a user, and warns them with a specific size along with a reason.")] [RequireContext(ContextType.Guild)] [RequireBotPermission(GuildPermission.BanMembers)] [RequireUserPermission(GuildPermission.KickMembers)] @@ -371,7 +371,7 @@ public async Task TempMuteUser([RequireHierarchy] UserRef userRef, TimeSpan time } [Command("tempmutewarn")] - [Summary("Temporarily mutes a user in text channels, and warns them with an option reason.")] + [Summary("Temporarily assigns a muted role to a user, and warns them with a reason.")] [Alias("tmutewarn", "temp-mutewarn", "warntmute", "tempmuteandwarn", "tmw")] [RequireContext(ContextType.Guild)] [RequireBotPermission(GuildPermission.ManageRoles)] @@ -410,7 +410,7 @@ public async Task TempMuteWarnUser([RequireHierarchy] UserRef userRef, TimeSpan } [Command("tempmutewarn")] - [Summary("Temporarily mutes a user in text channels, and warns them with a specific size along with an option reason.")] + [Summary("Temporarily assigns a muted role to a user, and warns them with a specific size along with a reason.")] [Alias("tmutewarn", "temp-mutewarn", "warntmute", "tempmuteandwarn", "tmw")] [RequireContext(ContextType.Guild)] [RequireBotPermission(GuildPermission.ManageRoles)] @@ -449,7 +449,7 @@ public async Task TempMuteWarnUser([RequireHierarchy] UserRef userRef, TimeSpan } [Command("ban", RunMode = RunMode.Async)] - [Summary("Bans a user with an option reason.")] + [Summary("Bans a user with a reason.")] [RequireContext(ContextType.Guild)] [RequireBotPermission(GuildPermission.BanMembers)] [RequireUserPermission(GuildPermission.BanMembers)] @@ -566,7 +566,7 @@ public async Task DeleteMany(uint number, UserRef user = null) } string extra = ""; if (searchedMessages != messages.Count) extra = $" out of {searchedMessages} searched messages"; - if (timeRanOut) extra = " (note, due to ratelimits and discord limitations, only messages in the last two weeks can be mass deleted)"; + if (timeRanOut) extra += " (reached limit due to ratelimits and Discord limitations, because only messages in the last two weeks can be mass deleted)"; Context.Message.DeleteOrRespond($"{Context.User.Mention} deleted {messages.Count} messages{extra}", Context.Guild); } } diff --git a/BotCatMaxy/Components/ReportModule.cs b/BotCatMaxy/Components/ReportModule.cs index 568766b..3ab11b3 100644 --- a/BotCatMaxy/Components/ReportModule.cs +++ b/BotCatMaxy/Components/ReportModule.cs @@ -26,7 +26,7 @@ public async Task Report() { var socketContext = Context as SocketCommandContext; var guildsEmbed = new EmbedBuilder(); - guildsEmbed.WithTitle("Reply with the the number next to the guild you want to make the report in"); + guildsEmbed.WithTitle("Reply with the number next to the guild you want to make the report in"); var mutualGuilds = socketContext.User.MutualGuilds.ToArray(); for (int i = 0; i < mutualGuilds.Length; i++) { @@ -168,7 +168,7 @@ public async Task SetReportCooldown(string time) TimeSpan? cooldown = time.ToTime(); if (cooldown == null) { - ReplyAsync("Time is invalid, if you intend to remove cooldon instead use ``none``"); + ReplyAsync("Time is invalid, if you intend to remove cooldown instead use ``none``"); return; } if (settings.cooldown == cooldown) ReplyAsync("Cooldown is already set to value"); From 9b46e462de0ee40160cbc9a21e3a55b7048d8d98 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Wed, 31 Mar 2021 22:05:13 -0700 Subject: [PATCH 32/42] Adds better multithreading stability (use of "volatile" keyword) --- BotCatMaxy/Components/Data/SettingsCache.cs | 18 ++++++++++++++---- .../Components/Logging/DiscordLogging.cs | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/BotCatMaxy/Components/Data/SettingsCache.cs b/BotCatMaxy/Components/Data/SettingsCache.cs index 2929f2a..90fc795 100644 --- a/BotCatMaxy/Components/Data/SettingsCache.cs +++ b/BotCatMaxy/Components/Data/SettingsCache.cs @@ -1,6 +1,7 @@ using BotCatMaxy.Models; using Discord; using Discord.WebSocket; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -8,13 +9,13 @@ namespace BotCatMaxy.Cache { public class SettingsCache { - public static HashSet guildSettings = new HashSet(); - public SettingsCache(IDiscordClient client) { + public static volatile HashSet guildSettings = new(); + + public SettingsCache(IDiscordClient client) { if (client is BaseSocketClient socketClient) socketClient.LeftGuild += RemoveGuild; } - public Task RemoveGuild(SocketGuild guild) { guildSettings.RemoveWhere(g => g.ID == guild.Id); @@ -22,7 +23,7 @@ public Task RemoveGuild(SocketGuild guild) } } - public class GuildSettings + public class GuildSettings : IEquatable { private readonly IGuild guild; public ulong ID => guild.Id; @@ -36,5 +37,14 @@ public GuildSettings(IGuild guild) { this.guild = guild; } + + public override bool Equals(object obj) + => Equals(obj as GuildSettings); + + public bool Equals(GuildSettings other) + => other?.ID == ID; + + public override int GetHashCode() + => HashCode.Combine(ID); } } diff --git a/BotCatMaxy/Components/Logging/DiscordLogging.cs b/BotCatMaxy/Components/Logging/DiscordLogging.cs index c222fcc..0df0b1d 100644 --- a/BotCatMaxy/Components/Logging/DiscordLogging.cs +++ b/BotCatMaxy/Components/Logging/DiscordLogging.cs @@ -11,7 +11,7 @@ namespace BotCatMaxy.Components.Logging { public static class DiscordLogging { - public static FixedSizedQueue deletedMessagesCache = new FixedSizedQueue(10); + public static volatile FixedSizedQueue deletedMessagesCache = new(10); public static async Task LogMessage(string reason, IMessage message, IGuild guild = null, bool addJumpLink = false, Color? color = null, IUser authorOveride = null) { From 9a50f1d8a78e5f4b1be62027c3d26a121e2dfe0e Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sat, 3 Apr 2021 15:27:16 -0700 Subject: [PATCH 33/42] Improves filter regex and adds tests as well as closes #123 --- BotCatMaxy/Components/Filter/FilterHandler.cs | 2 +- Tests/FilterTests.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/BotCatMaxy/Components/Filter/FilterHandler.cs b/BotCatMaxy/Components/Filter/FilterHandler.cs index 4fafa1f..e97e608 100644 --- a/BotCatMaxy/Components/Filter/FilterHandler.cs +++ b/BotCatMaxy/Components/Filter/FilterHandler.cs @@ -16,7 +16,7 @@ namespace BotCatMaxy.Startup { public class FilterHandler { - private const string inviteRegex = @"(?:http|https?:\/\/)?(?:www\.)?(?:discord\.(?:gg|io|me|li|com)|discord(?:app)?\.com\/invite)\/(\S+)"; + public const string inviteRegex = @"(?:https?:\/\/)?(?:\w+\.)?discord(?:(?:app)?\.com\/invite|\.gg)\/([A-Za-z0-9-]+)"; private const RegexOptions regexOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; private readonly IDiscordClient client; diff --git a/Tests/FilterTests.cs b/Tests/FilterTests.cs index ac4455f..d56626f 100644 --- a/Tests/FilterTests.cs +++ b/Tests/FilterTests.cs @@ -12,6 +12,7 @@ using Tests.Mocks.Guild; using Discord; using BotCatMaxy.Data; +using System.Text.RegularExpressions; namespace Tests { @@ -76,5 +77,17 @@ public async Task BadWordTheory(string input, string expected) Assert.NotEmpty(infractons); } } + + [Theory] + [InlineData("discord.gg/test", true)] + [InlineData("http://discord.gg/test", true)] + [InlineData("https://discord.gg/test", true)] + [InlineData("https://discord.com/invite/test", true)] + [InlineData("https://discord.com/test", false)] + [InlineData("https://discord.com/security", false)] + public void InviteRegexTheory(string input, bool expected) + { + Assert.True(Regex.IsMatch(input, FilterHandler.inviteRegex) == expected); + } } } From fc7a04d6fade0815b07d15326bebd39f2cb0714b Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sat, 3 Apr 2021 21:02:52 -0700 Subject: [PATCH 34/42] Adds highlighting when logging or DMing filter violations, which closes #107 --- BotCatMaxy/Components/Filter/FilterHandler.cs | 35 +++++++++--------- .../Components/Filter/FilterUtilities.cs | 37 ++++++++++++++++--- .../Components/Logging/DiscordLogging.cs | 4 +- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/BotCatMaxy/Components/Filter/FilterHandler.cs b/BotCatMaxy/Components/Filter/FilterHandler.cs index e97e608..e291d80 100644 --- a/BotCatMaxy/Components/Filter/FilterHandler.cs +++ b/BotCatMaxy/Components/Filter/FilterHandler.cs @@ -144,7 +144,7 @@ public async Task CheckReaction(Cacheable cachedMessage, IS if (settings.badUEmojis.Contains(reaction.Emote.Name)) { await message.RemoveAllReactionsForEmoteAsync(reaction.Emote); - await context.FilterPunish(gUser, $"bad reaction used ({reaction.Emote.Name})", settings, delete: false, warnSize: 1); + await context.FilterPunish(gUser, $"bad reaction used ({reaction.Emote.Name})", settings, null, delete: false, warnSize: 1); } } catch (Exception e) @@ -182,7 +182,7 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) int newLines = context.Message.Content.Count(c => c == '\n'); if (newLines > modSettings.maxNewLines.Value) { - await context.FilterPunish("too many newlines", modSettings, (newLines - modSettings.maxNewLines.Value) * 0.5f); + await context.FilterPunish("too many newlines", modSettings, null, (newLines - modSettings.maxNewLines.Value) * 0.5f); return; } } @@ -191,14 +191,15 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) if (!modSettings.invitesAllowed) { MatchCollection matches = Regex.Matches(message.Content, inviteRegex, regexOptions); - var invites = matches.Select(async match => await client.GetInviteAsync(match.Value)).Select(match => match.Result); - if (invites.Any()) - foreach (RestInviteMetadata invite in invites) - if (invite?.GuildId != null && !modSettings.whitelistedForInvite.Contains(invite.GuildId.Value)) - { - await context.FilterPunish("Posted Invite", modSettings); - return; - } + foreach (Match match in matches) + { + var invite = await client.GetInviteAsync(match.Value); + if (invite?.GuildId != null && !modSettings.whitelistedForInvite.Contains(invite.GuildId.Value)) + { + await context.FilterPunish("Posted Invite", modSettings, match.Value); + return; + } + } } //Checks if a message contains ugly, unwanted text t̨̠̱̭͓̠ͪ̈́͌ͪͮ̐͒h̲̱̯̀͂̔̆̌͊ͅà̸̻͌̍̍ͅt͕̖̦͂̎͂̂ͮ͜ ̲͈̥͒ͣ͗̚l̬͚̺͚͎̆͜ͅo͔̯̖͙ͩõ̲̗̎͆͜k̦̭̮̺ͮ͆̀ ͙̍̂͘l̡̮̱̤͍̜̲͙̓̌̐͐͂̓i͙̬ͫ̀̒͑̔͐k̯͇̀ͭe̎͋̓́ ̥͖̼̬ͪ̆ṫ͏͕̳̞̯h̛̼͔ͩ̑̿͑i͍̲̽ͮͪsͦ͋ͦ̌͗ͭ̋ @@ -209,7 +210,7 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) MatchCollection matches = Regex.Matches(message.Content, zalgoRegex, regexOptions); if (matches.Any()) { - await context.FilterPunish("zalgo usage", modSettings); + await context.FilterPunish("zalgo usage", modSettings, null); return; } } @@ -224,9 +225,9 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) //Checks for links if ((modSettings.allowedLinks != null && modSettings.allowedLinks.Count > 0) && (modSettings.allowedToLink == null || !gUser.RoleIds.Intersect(modSettings.allowedToLink).Any())) { - if (!modSettings.allowedLinks.Any(s => match.ToString().ToLower().Contains(s.ToLower()))) + if (!modSettings.allowedLinks.Any(s => match.Value.Contains(s, StringComparison.InvariantCultureIgnoreCase))) { - await context.FilterPunish("Using unauthorized links", modSettings, 1); + await context.FilterPunish("Using unauthorized links", modSettings, match.Value, 1); return; } } @@ -235,7 +236,7 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) //Check for emojis if (modSettings.badUEmojis?.Count is not null or 0 && modSettings.badUEmojis.Any(s => message.Content.Contains(s))) { - await context.FilterPunish("Bad emoji used", modSettings, 0.8f); + await context.FilterPunish("Bad emoji used", modSettings, null, 0.8f); return; } @@ -251,7 +252,7 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) } if (((amountCaps / (float)message.Content.Length) * 100) >= modSettings.allowedCaps) { - await context.FilterPunish("Excessive caps", modSettings, 0.3f); + await context.FilterPunish("Excessive caps", modSettings, null, 0.3f); return; } } @@ -262,12 +263,12 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) { if (!string.IsNullOrEmpty(detectedBadWord.Euphemism)) { - await context.FilterPunish("Bad word used (" + detectedBadWord.Euphemism + ")", modSettings, detectedBadWord.Size); + await context.FilterPunish($"Bad word used ({detectedBadWord.Euphemism})", modSettings, detectedBadWord.Word, detectedBadWord.Size); return; } else { - await context.FilterPunish("Bad word used", modSettings, detectedBadWord.Size); + await context.FilterPunish("Bad word used", modSettings, detectedBadWord.Word, detectedBadWord.Size); return; } } diff --git a/BotCatMaxy/Components/Filter/FilterUtilities.cs b/BotCatMaxy/Components/Filter/FilterUtilities.cs index d8370a2..eae987c 100644 --- a/BotCatMaxy/Components/Filter/FilterUtilities.cs +++ b/BotCatMaxy/Components/Filter/FilterUtilities.cs @@ -85,20 +85,38 @@ public static BadWord CheckForBadWords(this string message, BadWord[] badWords) return null; } - public static async Task FilterPunish(this ICommandContext context, string reason, ModerationSettings settings, float warnSize = 0.5f) + public static async Task FilterPunish(this ICommandContext context, string reason, ModerationSettings settings, string badText, float warnSize = 0.5f) { - await context.FilterPunish(context.User as IGuildUser, reason, settings, delete: true, warnSize: warnSize); + await context.FilterPunish(context.User as IGuildUser, reason, settings, badText, delete: true, warnSize: warnSize); } - public static async Task FilterPunish(this ICommandContext context, IGuildUser user, string reason, ModerationSettings settings, bool delete = true, float warnSize = 0.5f, string explicitInfo = "") + public static async Task FilterPunish(this ICommandContext context, IGuildUser user, string reason, ModerationSettings settings, string badText, bool delete = true, float warnSize = 0.5f) { - string jumpLink = await DiscordLogging.LogMessage(reason, context.Message, context.Guild, color: Color.Gold, authorOveride: user); + string content = context.Message.Content; + if (badText != null) //will be null in case of reaction warn where reason speaks for itself + { + if (badText == content) + { + content = $"**[{badText}]**"; + } + else + { + int badTextStart = content.IndexOf(badText); + int badTextEnd = badTextStart + badText.Length; + content = content.Insert(badTextStart, "**["); + content = content.Insert(badTextEnd + 3, "]**"); + } + } + else + content = null; + + string jumpLink = await DiscordLogging.LogMessage(reason, context.Message, context.Guild, color: Color.Gold, authorOveride: user, textOverride: content); await user.Warn(warnSize, reason, context.Channel as ITextChannel, logLink: jumpLink); if (settings?.anouncementChannels?.Contains(context.Channel.Id) ?? false) //If this channel is an anouncement channel return; - Task warnMessage = await NotifyPunish(context, user, reason, settings); + Task warnMessage = await NotifyPunish(context, user, reason, settings, content); if (delete) { @@ -117,13 +135,20 @@ public static async Task FilterPunish(this ICommandContext context, IGuildUser u public const string notifyInfoRegex = @"<@!?(\d+)> has been given their (\d+)\w+-?(?:\d+\w+)? infraction because of (.+)"; - public static async Task> NotifyPunish(ICommandContext context, IGuildUser user, string reason, ModerationSettings settings) + public static async Task> NotifyPunish(ICommandContext context, IGuildUser user, string reason, ModerationSettings settings, string highlight) { Task warnMessage = null; LogSettings logSettings = context.Guild.LoadFromFile(false); string infractionAmount = user.LoadInfractions().Count.Suffix(); + var embed = new EmbedBuilder() + .WithTitle($"Filter warning in {context.Guild.Name} for {reason.ToLower()}") + .WithColor(Color.Gold) + .WithCurrentTimestamp(); + if (highlight != null) embed.WithDescription(highlight); + await user.TryNotify(embed.Build()); + var messages = (await context.Channel.GetMessagesAsync(5).FlattenAsync()).Where(msg => msg.Author.Id == context.Client.CurrentUser.Id); //If need to go from "@person has been given their 3rd infractions because of fdsfd" to "@person has been given their 3rd-4th infractions because of fdsfd" if (messages.MatchInMessages(user.Id, out Match match, out IMessage message)) diff --git a/BotCatMaxy/Components/Logging/DiscordLogging.cs b/BotCatMaxy/Components/Logging/DiscordLogging.cs index 0df0b1d..bc7bdd6 100644 --- a/BotCatMaxy/Components/Logging/DiscordLogging.cs +++ b/BotCatMaxy/Components/Logging/DiscordLogging.cs @@ -13,7 +13,7 @@ public static class DiscordLogging { public static volatile FixedSizedQueue deletedMessagesCache = new(10); - public static async Task LogMessage(string reason, IMessage message, IGuild guild = null, bool addJumpLink = false, Color? color = null, IUser authorOveride = null) + public static async Task LogMessage(string reason, IMessage message, IGuild guild = null, bool addJumpLink = false, Color? color = null, IUser authorOveride = null, string textOverride = null) { try { @@ -43,7 +43,7 @@ public static async Task LogMessage(string reason, IMessage message, IGu "`This message had no text`", true); else embed.AddField(reason + " in #" + message.Channel.Name, - message.Content.Truncate(1020), true); + textOverride ?? message.Content.Truncate(1020), true); if (addJumpLink) { From 1f9d37b8a6a7e3e14d42fec9de2af4d9df47fd44 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sat, 3 Apr 2021 21:55:01 -0700 Subject: [PATCH 35/42] Updates embeds to include guild in author or footer where appropriate and overhauls Notify embed --- .../Components/Filter/FilterCommands.cs | 4 +-- .../Components/Filter/FilterUtilities.cs | 1 + .../Moderation/ModerationCommands.cs | 4 +-- .../Components/Moderation/PunishFunctions.cs | 27 ++++------------- BotCatMaxy/Startup/TempActions.cs | 2 +- BotCatMaxy/Utilities/NotifyUtilities.cs | 29 +++++++++++++++++++ 6 files changed, 41 insertions(+), 26 deletions(-) diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index 14de998..abd34b3 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -62,8 +62,8 @@ public async Task ListAutoMod(string extension = "") ModerationSettings settings = guild.LoadFromFile(false); BadWords badWords = new BadWords(guild); - var embed = new EmbedBuilder(); - embed.Author = new EmbedAuthorBuilder().WithName("Automod information for " + guild.Name + " discord"); + var embed = new EmbedBuilder() + .WithGuildAsAuthor(guild); string message = ""; bool useExplicit = false; diff --git a/BotCatMaxy/Components/Filter/FilterUtilities.cs b/BotCatMaxy/Components/Filter/FilterUtilities.cs index eae987c..17e4670 100644 --- a/BotCatMaxy/Components/Filter/FilterUtilities.cs +++ b/BotCatMaxy/Components/Filter/FilterUtilities.cs @@ -145,6 +145,7 @@ public static async Task> NotifyPunish(ICommandContext contex var embed = new EmbedBuilder() .WithTitle($"Filter warning in {context.Guild.Name} for {reason.ToLower()}") .WithColor(Color.Gold) + .WithGuildAsAuthor(context.Guild) .WithCurrentTimestamp(); if (highlight != null) embed.WithDescription(highlight); await user.TryNotify(embed.Build()); diff --git a/BotCatMaxy/Components/Moderation/ModerationCommands.cs b/BotCatMaxy/Components/Moderation/ModerationCommands.cs index c70ab91..c8e98f3 100644 --- a/BotCatMaxy/Components/Moderation/ModerationCommands.cs +++ b/BotCatMaxy/Components/Moderation/ModerationCommands.cs @@ -122,7 +122,7 @@ public async Task DMUserWarnsAsync(UserRef userRef = null, int amount = 50) } userRef = userRef with { GuildUser = await guild.GetUserAsync(userRef.ID) }; await ReplyAsync($"Here are {userRef.Mention()}'s {((amount < infractions.Count) ? $"last {amount} out of " : "")}{"infraction".ToQuantity(infractions.Count)}", - embed: infractions.GetEmbed(userRef, amount: amount)); + embed: infractions.GetEmbed(userRef, guild, amount: amount)); } @@ -139,7 +139,7 @@ public async Task CheckUserWarnsAsync(UserRef userRef = null, int amount = 5) await ReplyAsync($"{userRef.Name()} has no infractions"); return; } - await ReplyAsync(embed: infractions.GetEmbed(userRef, amount: amount, showLinks: true)); + await ReplyAsync(embed: infractions.GetEmbed(userRef, Context.Guild, amount: amount, showLinks: true)); } [Command("removewarn")] diff --git a/BotCatMaxy/Components/Moderation/PunishFunctions.cs b/BotCatMaxy/Components/Moderation/PunishFunctions.cs index fd9796f..d32ab4d 100644 --- a/BotCatMaxy/Components/Moderation/PunishFunctions.cs +++ b/BotCatMaxy/Components/Moderation/PunishFunctions.cs @@ -195,9 +195,9 @@ public InfractionInfo(List infractions, int amount = 5, bool showLin } } - public static Embed GetEmbed(this List infractions, UserRef userRef, int amount = 5, bool showLinks = false) + public static Embed GetEmbed(this List infractions, UserRef userRef, IGuild guild, int amount = 5, bool showLinks = false) { - InfractionInfo data = new InfractionInfo(infractions, amount, showLinks); + InfractionInfo data = new(infractions, amount, showLinks); //Builds infraction embed var embed = new EmbedBuilder(); @@ -211,13 +211,11 @@ public static Embed GetEmbed(this List infractions, UserRef userRef, data.infractionStrings[0]); data.infractionStrings.RemoveAt(0); foreach (string s in data.infractionStrings) - { embed.AddField("------------------------------------------------------------", s); - } - embed.WithAuthor(userRef); - embed.WithFooter("ID: " + userRef.ID) - .WithColor(Color.Blue) - .WithCurrentTimestamp(); + embed.WithAuthor(userRef) + .WithGuildAsFooter(guild, "ID: " + userRef.ID) + .WithColor(Color.Blue) + .WithCurrentTimestamp(); return embed.Build(); } @@ -265,18 +263,5 @@ public static async Task TempMute(this UserRef userRef, TimeSpan time, string re } userRef.ID.RecordAct(context.Guild, tempMute, "tempmute", context.Message.GetJumpUrl()); } - - public static async Task Notify(this IUser user, string action, string reason, IGuild guild, IUser author = null, string article = "from", Color color = default) - { - if (color == default) color = Color.LightGrey; - var embed = new EmbedBuilder(); - embed.WithTitle($"You have been {action} {article} a discord guild"); - embed.AddField("Reason", reason, true); - embed.AddField("Guild name", guild.Name, true); - embed.WithCurrentTimestamp(); - embed.WithColor(color); - if (author != null) embed.WithAuthor(author); - await user.TryNotify(embed.Build()); - } } } diff --git a/BotCatMaxy/Startup/TempActions.cs b/BotCatMaxy/Startup/TempActions.cs index abbebec..79fd9d1 100644 --- a/BotCatMaxy/Startup/TempActions.cs +++ b/BotCatMaxy/Startup/TempActions.cs @@ -154,7 +154,7 @@ public static async Task CheckTempActs(DiscordSocketClient client, bool debug = if (user != null) { // if possible to message, message and log DiscordLogging.LogEndTempAct(sockGuild, user, "mut", tempMute.Reason, tempMute.Length); - _ = user.Notify($"untemp-muted", tempMute.Reason, sockGuild); + _ = user.Notify("auto untempmuted", tempMute.Reason, sockGuild, client.CurrentUser); } editedMutes.Remove(tempMute); } diff --git a/BotCatMaxy/Utilities/NotifyUtilities.cs b/BotCatMaxy/Utilities/NotifyUtilities.cs index 1d9823b..1a4b373 100644 --- a/BotCatMaxy/Utilities/NotifyUtilities.cs +++ b/BotCatMaxy/Utilities/NotifyUtilities.cs @@ -1,4 +1,5 @@ using Discord; +using Discord.WebSocket; using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +10,18 @@ namespace BotCatMaxy { public static class NotifyUtilities { + public static async Task Notify(this IUser user, string action, string reason, IGuild guild, IUser author = null, Color color = default) + { + if (color == default) color = Color.LightGrey; + var embed = new EmbedBuilder() + .AddField($"You have been {action}", reason) + .WithCurrentTimestamp() + .WithGuildAsAuthor(guild) + .WithColor(color); + if (author != null) embed.WithFooter($"Done by {author.Username}#{author.Discriminator}", author.GetAvatarUrl()); + await user.TryNotify(embed.Build()); + } + public static async Task TryNotify(this IUser user, string message) { try @@ -36,5 +49,21 @@ public static async Task TryNotify(this IUser user, Embed embed) return false; } } + + /// + ///Sets the author field of the using the supplied guild info + /// + public static EmbedBuilder WithGuildAsAuthor(this EmbedBuilder embed, IGuild guild) + => embed.WithAuthor(guild.Name, guild.IconUrl); + + /// + ///Sets the footer field of the using the supplied guild info + /// + public static EmbedBuilder WithGuildAsFooter(this EmbedBuilder embed, IGuild guild, string extra = null) + { + string text = guild.Name; + if (extra != null) text += $" • {extra}"; + return embed.WithFooter(text, guild.IconUrl); + } } } From a3d40fb055251f71e5509d8354e3c4584c1340dc Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Sat, 3 Apr 2021 22:48:15 -0700 Subject: [PATCH 36/42] Fixes filter highlighting not working with bad word substitution, passing all tests --- BotCatMaxy/Components/Filter/FilterHandler.cs | 23 +++++++----- .../Components/Filter/FilterUtilities.cs | 35 ++++++++----------- Tests/FilterTests.cs | 6 ++-- Tests/Mocks/Guild/MockGuild.cs | 2 +- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/BotCatMaxy/Components/Filter/FilterHandler.cs b/BotCatMaxy/Components/Filter/FilterHandler.cs index e291d80..97edf92 100644 --- a/BotCatMaxy/Components/Filter/FilterHandler.cs +++ b/BotCatMaxy/Components/Filter/FilterHandler.cs @@ -80,7 +80,7 @@ public async Task CheckNameInGuild(IUser user, string name, IGuild guild) if (gUser.CantBeWarned() || !gUser.CanActOn(currentUser)) return; - BadWord detectedBadWord = name.CheckForBadWords(guild.LoadFromFile(false)?.badWords.ToArray()); + BadWord detectedBadWord = name.CheckForBadWords(guild.LoadFromFile(false)?.badWords.ToArray()).word; if (detectedBadWord == null) return; LogSettings logSettings = guild.LoadFromFile(false); @@ -182,7 +182,7 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) int newLines = context.Message.Content.Count(c => c == '\n'); if (newLines > modSettings.maxNewLines.Value) { - await context.FilterPunish("too many newlines", modSettings, null, (newLines - modSettings.maxNewLines.Value) * 0.5f); + await context.FilterPunish("too many newlines", modSettings, null, warnSize: (newLines - modSettings.maxNewLines.Value) * 0.5f); return; } } @@ -196,7 +196,7 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) var invite = await client.GetInviteAsync(match.Value); if (invite?.GuildId != null && !modSettings.whitelistedForInvite.Contains(invite.GuildId.Value)) { - await context.FilterPunish("Posted Invite", modSettings, match.Value); + await context.FilterPunish("Posted Invite", modSettings, match.Value, match.Index); return; } } @@ -227,7 +227,7 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) { if (!modSettings.allowedLinks.Any(s => match.Value.Contains(s, StringComparison.InvariantCultureIgnoreCase))) { - await context.FilterPunish("Using unauthorized links", modSettings, match.Value, 1); + await context.FilterPunish("Using unauthorized links", modSettings, match.Value, match.Index, warnSize: 1); return; } } @@ -236,7 +236,7 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) //Check for emojis if (modSettings.badUEmojis?.Count is not null or 0 && modSettings.badUEmojis.Any(s => message.Content.Contains(s))) { - await context.FilterPunish("Bad emoji used", modSettings, null, 0.8f); + await context.FilterPunish("Bad emoji used", modSettings, null, warnSize: 0.8f); return; } @@ -252,23 +252,24 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) } if (((amountCaps / (float)message.Content.Length) * 100) >= modSettings.allowedCaps) { - await context.FilterPunish("Excessive caps", modSettings, null, 0.3f); + await context.FilterPunish("Excessive caps", modSettings, null, warnSize: 0.3f); return; } } } //End of stuff from mod settings - BadWord detectedBadWord = msgContent.CheckForBadWords(badWords?.ToArray()); + var badWordResult = msgContent.CheckForBadWords(badWords?.ToArray()); + var detectedBadWord = badWordResult.word; if (detectedBadWord != null) { if (!string.IsNullOrEmpty(detectedBadWord.Euphemism)) { - await context.FilterPunish($"Bad word used ({detectedBadWord.Euphemism})", modSettings, detectedBadWord.Word, detectedBadWord.Size); + await context.FilterPunish($"Bad word used ({detectedBadWord.Euphemism})", modSettings, detectedBadWord.Word, badWordResult.index, detectedBadWord.Size); return; } else { - await context.FilterPunish("Bad word used", modSettings, detectedBadWord.Word, detectedBadWord.Size); + await context.FilterPunish("Bad word used", modSettings, detectedBadWord.Word, badWordResult.index, detectedBadWord.Size); return; } } @@ -276,7 +277,11 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) } catch (Exception e) { +#if DEBUG + throw; +#else await e.LogFilterError("message", guild); +#endif } } } diff --git a/BotCatMaxy/Components/Filter/FilterUtilities.cs b/BotCatMaxy/Components/Filter/FilterUtilities.cs index 17e4670..d5a808a 100644 --- a/BotCatMaxy/Components/Filter/FilterUtilities.cs +++ b/BotCatMaxy/Components/Filter/FilterUtilities.cs @@ -19,9 +19,9 @@ public static class FilterUtilities { readonly static char[] splitters = @"#.,/\|=_- ".ToCharArray(); - public static BadWord CheckForBadWords(this string message, BadWord[] badWords) + public static (BadWord word, int? index) CheckForBadWords(this string message, BadWord[] badWords) { - if (badWords?.Length is null or 0) return null; + if (badWords?.Length is null or 0) return (null, null); //Checks for bad words StringBuilder sb = new StringBuilder(); @@ -63,34 +63,26 @@ public static BadWord CheckForBadWords(this string message, BadWord[] badWords) string[] messageParts = message.Split(splitters, StringSplitOptions.RemoveEmptyEntries); foreach (BadWord badWord in badWords) - { if (badWord.PartOfWord) { - if (strippedMessage.Contains(badWord.Word, StringComparison.InvariantCultureIgnoreCase)) - { - return badWord; - } + int index = strippedMessage.IndexOf(badWord.Word, StringComparison.InvariantCultureIgnoreCase); + if (index > -1) + return (badWord, index); } else - { //If bad word is ignored inside of words + //If bad word is ignored inside of words foreach (string word in messageParts) - { if (word.Equals(badWord.Word, StringComparison.InvariantCultureIgnoreCase)) - { - return badWord; - } - } - } - } - return null; + return (badWord, null); + return (null, null); } - public static async Task FilterPunish(this ICommandContext context, string reason, ModerationSettings settings, string badText, float warnSize = 0.5f) + public static async Task FilterPunish(this ICommandContext context, string reason, ModerationSettings settings, string badText, int? index = null, float warnSize = 0.5f) { - await context.FilterPunish(context.User as IGuildUser, reason, settings, badText, delete: true, warnSize: warnSize); + await context.FilterPunish(context.User as IGuildUser, reason, settings, badText, index: index, delete: true, warnSize: warnSize); } - public static async Task FilterPunish(this ICommandContext context, IGuildUser user, string reason, ModerationSettings settings, string badText, bool delete = true, float warnSize = 0.5f) + public static async Task FilterPunish(this ICommandContext context, IGuildUser user, string reason, ModerationSettings settings, string badText, int? index = null, bool delete = true, float warnSize = 0.5f) { string content = context.Message.Content; if (badText != null) //will be null in case of reaction warn where reason speaks for itself @@ -101,7 +93,7 @@ public static async Task FilterPunish(this ICommandContext context, IGuildUser u } else { - int badTextStart = content.IndexOf(badText); + int badTextStart = index ?? content.IndexOf(badText); int badTextEnd = badTextStart + badText.Length; content = content.Insert(badTextStart, "**["); content = content.Insert(badTextEnd + 3, "]**"); @@ -157,7 +149,8 @@ public static async Task> NotifyPunish(ICommandContext contex int oldInfraction = int.Parse(match.Groups[2].Value); await (message as IUserMessage).ModifyAsync(msg => msg.Content = $"{user.Mention} has been given their {oldInfraction.Suffix()}-{infractionAmount} infraction because of {reason}"); return null; - } else + } + else { //Public channel nonsense if someone want a public log (don't think this has been used since the old Vesteria Discord but backward compat) string toSay = $"{user.Mention} has been given their {infractionAmount} infraction because of {reason}"; diff --git a/Tests/FilterTests.cs b/Tests/FilterTests.cs index d56626f..af28d43 100644 --- a/Tests/FilterTests.cs +++ b/Tests/FilterTests.cs @@ -47,9 +47,9 @@ public async Task PunishTest() var channel = (MockTextChannel)await channelTask; var users = await guild.GetUsersAsync(); var testee = users.First(user => user.Username == "Testee"); - var message = channel.SendMessageAsOther("", testee); + var message = channel.SendMessageAsOther("calzone", testee); var context = new MockCommandContext(client, message); - await context.FilterPunish("Testing Punish", settings); + await context.FilterPunish("Testing Punish", settings, "calzone"); var infractons = testee.LoadInfractions(true); Assert.NotNull(infractons); Assert.NotEmpty(infractons); @@ -61,7 +61,7 @@ public async Task PunishTest() [InlineData("$ubst1tuti0n", "Substitution")] public async Task BadWordTheory(string input, string expected) { - var result = input.CheckForBadWords(badWords); + var result = input.CheckForBadWords(badWords).word; Assert.Equal(expected, result?.Word, ignoreCase: true); var channel = (MockTextChannel)await channelTask; diff --git a/Tests/Mocks/Guild/MockGuild.cs b/Tests/Mocks/Guild/MockGuild.cs index 8482840..7177537 100644 --- a/Tests/Mocks/Guild/MockGuild.cs +++ b/Tests/Mocks/Guild/MockGuild.cs @@ -40,7 +40,7 @@ public MockGuild() public string IconId => throw new NotImplementedException(); - public string IconUrl => throw new NotImplementedException(); + public string IconUrl => null; public string SplashId => throw new NotImplementedException(); From 8e275ead2a169a5d9710dad06babba311ee0dff4 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Mon, 5 Apr 2021 15:45:31 -0700 Subject: [PATCH 37/42] Fixes event handling so it will generate less gateway issues and warnings --- BotCatMaxy/Components/Filter/FilterHandler.cs | 36 +++++++++++-------- .../Components/Logging/LoggingHandler.cs | 28 ++++++++++----- BotCatMaxy/Startup/TempActions.cs | 3 +- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/BotCatMaxy/Components/Filter/FilterHandler.cs b/BotCatMaxy/Components/Filter/FilterHandler.cs index 97edf92..df3837b 100644 --- a/BotCatMaxy/Components/Filter/FilterHandler.cs +++ b/BotCatMaxy/Components/Filter/FilterHandler.cs @@ -31,40 +31,46 @@ public FilterHandler(IDiscordClient client) socketClient.ReactionAdded += HandleReaction; socketClient.UserJoined += HandleUserJoin; socketClient.UserUpdated += HandleUserChange; - socketClient.GuildMemberUpdated += HandleUserChange; } new LogMessage(LogSeverity.Info, "Filter", "Filter is active").Log(); } - private async Task HandleEdit(Cacheable oldMessage, SocketMessage editedMessage, ISocketMessageChannel channel) - => await Task.Run(() => CheckMessage(editedMessage)).ConfigureAwait(false); - - public async Task HandleReaction(Cacheable cachedMessage, ISocketMessageChannel channel, SocketReaction reaction) - => await Task.Run(() => CheckReaction(cachedMessage, channel, reaction)).ConfigureAwait(false); + private Task HandleEdit(Cacheable oldMessage, SocketMessage editedMessage, ISocketMessageChannel channel) + { + Task.Run(() => CheckMessage(editedMessage)); + return Task.CompletedTask; + } - public async Task HandleUserJoin(SocketGuildUser user) - => await Task.Run(async () => CheckNameInGuild(user, user.Username, user.Guild)); + public Task HandleReaction(Cacheable cachedMessage, ISocketMessageChannel channel, SocketReaction reaction) + { + Task.Run(() => CheckReaction(cachedMessage, channel, reaction)); + return Task.CompletedTask; + } - public async Task HandleGuildUserChange(SocketGuildUser old, SocketGuildUser updated) + public Task HandleUserJoin(SocketGuildUser user) { - if (updated.Nickname != old.Nickname) - await Task.Run(async () => CheckNameInGuild(updated, updated.Nickname, updated.Guild)); + Task.Run(() => CheckNameInGuild(user, user.Username, user.Guild)); + return Task.CompletedTask; } - public async Task HandleUserChange(SocketUser old, SocketUser updated) + public Task HandleUserChange(SocketUser old, SocketUser updated) { if (updated.Username != old.Username) { foreach (SocketGuild guild in updated.MutualGuilds) { - await Task.Run(async () => CheckNameInGuild(updated, updated.Username, guild)); + Task.Run(() => CheckNameInGuild(updated, updated.Username, guild)); } } + return Task.CompletedTask; } - public async Task HandleMessage(SocketMessage message) - => await Task.Run(() => CheckMessage(message)); + public Task HandleMessage(SocketMessage message) + { + Task.Run(() => CheckMessage(message)); + return Task.CompletedTask; + } public async Task CheckNameInGuild(IUser user, string name, IGuild guild) { diff --git a/BotCatMaxy/Components/Logging/LoggingHandler.cs b/BotCatMaxy/Components/Logging/LoggingHandler.cs index 980be31..0dda3eb 100644 --- a/BotCatMaxy/Components/Logging/LoggingHandler.cs +++ b/BotCatMaxy/Components/Logging/LoggingHandler.cs @@ -29,25 +29,35 @@ public async Task SetUpAsync() await new LogMessage(LogSeverity.Info, "Logs", "Logging set up").Log(); } - public async Task HandleNew(IMessage message) - => await Task.Run(() => LogNew(message)).ConfigureAwait(false); + public Task HandleNew(IMessage message) + { + Task.Run(() => LogNew(message)); + return Task.CompletedTask; + } - private async Task HandleDelete(Cacheable message, ISocketMessageChannel channel) - => await Task.Run(() => LogDelete(message, channel)); + private Task HandleDelete(Cacheable message, ISocketMessageChannel channel) + { + Task.Run(() => LogDelete(message, channel)); + return Task.CompletedTask; + } - private async Task HandleEdit(Cacheable cachedMessage, SocketMessage newMessage, ISocketMessageChannel channel) - => await Task.Run(() => LogEdit(cachedMessage, newMessage, channel)); + private Task HandleEdit(Cacheable cachedMessage, SocketMessage newMessage, ISocketMessageChannel channel) + { + Task.Run(() => LogEdit(cachedMessage, newMessage, channel)); + return Task.CompletedTask; + } - public async Task LogNew(IMessage message) + public Task LogNew(IMessage message) { if (message.Channel as SocketGuildChannel != null && message.MentionedRoleIds != null && message.MentionedRoleIds.Count > 0) { SocketGuild guild = (message.Channel as SocketGuildChannel).Guild; - await DiscordLogging.LogMessage("Role ping", message, guild, true); + Task.Run(() => DiscordLogging.LogMessage("Role ping", message, guild, true)); } + return Task.CompletedTask; } - async Task LogEdit(Cacheable cachedMessage, SocketMessage newMessage, ISocketMessageChannel channel) + public async Task LogEdit(Cacheable cachedMessage, SocketMessage newMessage, ISocketMessageChannel channel) { try { diff --git a/BotCatMaxy/Startup/TempActions.cs b/BotCatMaxy/Startup/TempActions.cs index 79fd9d1..aeca9be 100644 --- a/BotCatMaxy/Startup/TempActions.cs +++ b/BotCatMaxy/Startup/TempActions.cs @@ -49,7 +49,8 @@ private async Task CheckNewUser(SocketGuildUser user) TempActionList actions = user.Guild?.LoadFromFile(); //Can be done better and cleaner if (settings == null || user.Guild?.GetRole(settings.mutedRole) == null || (actions?.tempMutes?.Count is null or 0)) return; - if (actions.tempMutes.Any(tempMute => tempMute.User == user.Id)) await user.AddRoleAsync(user.Guild.GetRole(settings.mutedRole)); + if (actions.tempMutes.Any(tempMute => tempMute.User == user.Id)) + await user.AddRoleAsync(user.Guild.GetRole(settings.mutedRole)); } public static async Task CheckTempActs(DiscordSocketClient client, bool debug = false, CancellationToken? ct = null) From 096e01d2965fd72e8808b7f5212532365ed6c97c Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Mon, 5 Apr 2021 21:49:10 -0700 Subject: [PATCH 38/42] Splits ModerationSettings into FilterSettings and ModerationSettings Also fixes mistake making filter immune roles !warn immune --- .../Components/Filter/FilterCommands.cs | 64 +++++++++---------- BotCatMaxy/Components/Filter/FilterHandler.cs | 63 +++++++++--------- .../Components/Filter/FilterUtilities.cs | 10 +-- BotCatMaxy/Models/FilterSettings.cs | 28 ++++++++ BotCatMaxy/Models/ModerationSettings.cs | 14 ---- BotCatMaxy/Utilities/PermissionUtilities.cs | 4 -- 6 files changed, 98 insertions(+), 85 deletions(-) create mode 100644 BotCatMaxy/Models/FilterSettings.cs diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index abd34b3..198311b 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -59,7 +59,7 @@ public async Task ListAutoMod(string extension = "") } } - ModerationSettings settings = guild.LoadFromFile(false); + var settings = guild.LoadFromFile(false); BadWords badWords = new BadWords(guild); var embed = new EmbedBuilder() @@ -166,7 +166,7 @@ public async Task ListAutoMod(string extension = "") [Summary("Set a number of max emojis a user may send in a single message.")] public async Task AllowEmojis(uint amount) { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); if (amount == settings.maxEmojis) { await ReplyAsync("The selected value is already set"); @@ -185,13 +185,13 @@ public async Task AllowEmojis(uint amount) [Summary("Set a number of max emojis a user may send in a single message.")] public async Task SetMaxEmojis(string amount) { - ModerationSettings settings; + FilterSettings settings; switch (amount.ToLower()) { case "null": case "none": case "disable": - settings = Context.Guild.LoadFromFile(false); + settings = Context.Guild.LoadFromFile(false); if (settings?.maxEmojis == null) await ReplyAsync("Emoji moderation is already disabled"); else @@ -202,7 +202,7 @@ public async Task SetMaxEmojis(string amount) } break; case "all": - settings = Context.Guild.LoadFromFile(true); + settings = Context.Guild.LoadFromFile(true); settings.maxEmojis = 0; settings.SaveToFile(); string extraInfo = ""; @@ -221,7 +221,7 @@ public async Task SetMaxEmojis(string amount) [RequireBotPermission(ChannelPermission.AddReactions)] public async Task BanEmoji(Emoji emoji) { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); if (settings.badUEmojis.Contains(emoji.Name)) { await ReplyAsync($"Emoji {emoji.Name} is already banned"); @@ -238,7 +238,7 @@ public async Task BanEmoji(Emoji emoji) [RequireBotPermission(ChannelPermission.AddReactions)] public async Task RemoveBannedEmoji(Emoji emoji) { - ModerationSettings settings = Context.Guild.LoadFromFile(false); + var settings = Context.Guild.LoadFromFile(false); if (settings == null || !settings.badUEmojis.Contains(emoji.Name)) { await ReplyAsync($"Emoji {emoji.Name} is not banned"); @@ -254,7 +254,7 @@ public async Task RemoveBannedEmoji(Emoji emoji) [Summary("Sets the maximum percentage of capital letters a user may send.")] public async Task SetCapFilter(ushort percent) { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); if (percent == settings.allowedCaps) { @@ -283,9 +283,9 @@ public async Task SetCapFilter(ushort percent) [Alias("addroleallowedtolink")] public async Task AddAllowedLinkRole(SocketRole role) { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); - if (settings.allowedToLink == null) settings.allowedToLink = new List(); + if (settings.allowedToLink == null) settings.allowedToLink = new HashSet(); if (settings.allowedToLink.Contains(role.Id)) { await ReplyAsync($"Role '{role.Name}' was already exempt from link filtering"); @@ -303,7 +303,7 @@ public async Task AddAllowedLinkRole(SocketRole role) [Alias("removeroleallowedtolink")] public async Task RemoveAllowedLinkRole(SocketRole role) { - ModerationSettings settings = Context.Guild.LoadFromFile(); + var settings = Context.Guild.LoadFromFile(); if (settings == null || settings.allowedToLink == null) { @@ -363,7 +363,7 @@ public async Task ToggleContainBadWord(string word) [HasAdmin] public async Task ToggleAutoMod() { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); if (settings.channelsWithoutAutoMod.Contains(Context.Channel.Id)) { @@ -384,14 +384,14 @@ public async Task ToggleAutoMod() [HasAdmin] public async Task AddWarnIgnoredRole(SocketRole role) { - ModerationSettings settings = Context.Guild.LoadFromFile(true); - if (settings.cantBeWarned == null) settings.cantBeWarned = new List(); - else if (settings.cantBeWarned.Contains(role.Id)) + var settings = Context.Guild.LoadFromFile(true); + if (settings.filterIgnored == null) settings.filterIgnored = new HashSet(); + else if (settings.filterIgnored.Contains(role.Id)) { await ReplyAsync($"Role '{role.Name}' is already not able to be warned"); return; } - settings.cantBeWarned.Add(role.Id); + settings.filterIgnored.Add(role.Id); settings.SaveToFile(); await ReplyAsync($"Role '{role.Name}' will not be able to be warned now"); } @@ -401,15 +401,15 @@ public async Task AddWarnIgnoredRole(SocketRole role) [HasAdmin] public async Task RemovedWarnIgnoredRole(SocketRole role) { - ModerationSettings settings = Context.Guild.LoadFromFile(); - if (settings == null || settings.cantBeWarned == null) settings.cantBeWarned = new List(); - else if (settings.cantBeWarned.Contains(role.Id)) + var settings = Context.Guild.LoadFromFile(); + if (settings == null || settings.filterIgnored == null) settings.filterIgnored = new HashSet(); + else if (settings.filterIgnored.Contains(role.Id)) { await ReplyAsync($"Role '{role.Name}' is already able to be warned"); } else { - settings.cantBeWarned.Add(role.Id); + settings.filterIgnored.Add(role.Id); settings.SaveToFile(); await ReplyAsync($"Role '{role.Name}' will not be able to be warned now"); } @@ -427,7 +427,7 @@ public async Task AddAllowedLink(string link) await ReplyAsync("Link is not valid"); return; } - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); if (settings.allowedLinks == null) settings.allowedLinks = new HashSet(); else if (settings.allowedLinks.Contains(link)) { @@ -444,7 +444,7 @@ public async Task AddAllowedLink(string link) [HasAdmin] public async Task RemoveAllowedLink(string link) { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); if (settings.allowedLinks == null || !settings.allowedLinks.Contains(link)) { await ReplyAsync("Link is already not allowed"); @@ -462,7 +462,7 @@ public async Task RemoveAllowedLink(string link) public async Task ToggleInviteWarn() { IUserMessage message = await ReplyAsync("Trying to toggle"); - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); settings.invitesAllowed = !settings.invitesAllowed; settings.SaveToFile(); @@ -475,7 +475,7 @@ public async Task ToggleInviteWarn() public async Task ToggleZalgoWarn() { IUserMessage message = await ReplyAsync("Trying to toggle"); - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); settings.zalgoAllowed = !settings.zalgoAllowed; settings.SaveToFile(); @@ -527,7 +527,7 @@ public async Task AddBadWord(string word, string euphemism = null, float size = [Summary("Sets this channel as an announcement channel.")] public async Task AddAnouncementChannel() { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); if (settings.anouncementChannels.Contains(Context.Channel.Id)) { await ReplyAsync("This is already an 'anouncement' channel"); @@ -542,7 +542,7 @@ public async Task AddAnouncementChannel() [Summary("Sets this channel as a regular channel.")] public async Task RemoveAnouncementChannel() { - ModerationSettings settings = Context.Guild.LoadFromFile(false); + var settings = Context.Guild.LoadFromFile(false); if (!settings?.anouncementChannels?.Contains(Context.Channel.Id) ?? true) { //Goes through various steps to check if 1. settings (for anouncement channels) exist 2. Current channel is in those settings @@ -559,7 +559,7 @@ public async Task RemoveAnouncementChannel() [HasAdmin] public async Task ToggleNameFilter() { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); settings.moderateNames = !settings.moderateNames; settings.SaveToFile(); await ReplyAsync("Set user name and nickname filtering to " + settings.moderateNames.ToString().ToLowerInvariant()); @@ -571,7 +571,7 @@ public async Task ToggleNameFilter() [HasAdmin] public async Task SetMaximumNewLines(byte? amount) { - ModerationSettings settings = Context.Guild.LoadFromFile(true); + var settings = Context.Guild.LoadFromFile(true); string reply; if (amount == null || amount < 0) { @@ -593,8 +593,8 @@ public async Task SetMaximumNewLines(byte? amount) [HasAdmin] public async Task AddInviteWhitelist(ulong guildID) { - ModerationSettings settings = Context.Guild.LoadFromFile(true); - if (settings?.whitelistedForInvite == null) settings.whitelistedForInvite = new List(); + var settings = Context.Guild.LoadFromFile(true); + if (settings?.whitelistedForInvite == null) settings.whitelistedForInvite = new HashSet(); else if (settings.whitelistedForInvite.Contains(guildID)) { await ReplyAsync("Selected guild is already whitelisted"); @@ -611,8 +611,8 @@ public async Task AddInviteWhitelist(ulong guildID) [HasAdmin] public async Task RemoveInviteWhitelist(ulong guildID) { - ModerationSettings settings = Context.Guild.LoadFromFile(); - if (settings?.whitelistedForInvite == null) settings.whitelistedForInvite = new List(); + var settings = Context.Guild.LoadFromFile(); + if (settings?.whitelistedForInvite == null) settings.whitelistedForInvite = new HashSet(); else if (settings.whitelistedForInvite.Contains(guildID)) { await ReplyAsync("Invites leading to selected server will already result in warns"); diff --git a/BotCatMaxy/Components/Filter/FilterHandler.cs b/BotCatMaxy/Components/Filter/FilterHandler.cs index df3837b..55d7c3a 100644 --- a/BotCatMaxy/Components/Filter/FilterHandler.cs +++ b/BotCatMaxy/Components/Filter/FilterHandler.cs @@ -78,7 +78,7 @@ public async Task CheckNameInGuild(IUser user, string name, IGuild guild) { var currentUser = await guild.GetCurrentUserAsync(); if (!currentUser.GuildPermissions.KickMembers) return; - ModerationSettings settings = guild.LoadFromFile(false); + var settings = guild.LoadFromFile(false); //Has to check if not equal to true since it's nullable if (settings?.moderateNames != true) return; @@ -140,7 +140,7 @@ public async Task CheckReaction(Cacheable cachedMessage, IS //Needed to do our own get instead of cachedMessage.GetOrDownloadAsync() because this can be ISystenMessage and not just IUserMessage IMessage message = await channel.GetMessageAsync(cachedMessage.Id); ReactionContext context = new ReactionContext(client, message); - ModerationSettings settings = guild.LoadFromFile(false); + var settings = guild.LoadFromFile(false); SocketGuildUser gUser = guild.GetUser(reaction.UserId); var Guild = chnl.Guild; if (settings?.badUEmojis?.Count == null || settings.badUEmojis.Count == 0 || (reaction.User.Value as SocketGuildUser).CantBeWarned() || reaction.User.Value.IsBot) @@ -150,7 +150,7 @@ public async Task CheckReaction(Cacheable cachedMessage, IS if (settings.badUEmojis.Contains(reaction.Emote.Name)) { await message.RemoveAllReactionsForEmoteAsync(reaction.Emote); - await context.FilterPunish(gUser, $"bad reaction used ({reaction.Emote.Name})", settings, null, delete: false, warnSize: 1); + await context.FilterPunish(gUser, $"bad reaction used ({reaction.Emote.Name})", guild.LoadFromFile(), settings, null, delete: false, warnSize: 1); } } catch (Exception e) @@ -173,36 +173,37 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) try { ModerationSettings modSettings = guild.LoadFromFile(); + var filterSettings = guild.LoadFromFile(); List badWords = guild.LoadFromFile()?.badWords; string msgContent = message.Content; - if (modSettings != null) + if (modSettings != null && filterSettings != null) { - if (modSettings.channelsWithoutAutoMod != null && modSettings.channelsWithoutAutoMod.Contains(chnl.Id)) + if (filterSettings.channelsWithoutAutoMod != null && filterSettings.channelsWithoutAutoMod.Contains(chnl.Id)) return; //Returns if channel is set as not using automod //Checks if a message contains too many "newlines" - if (modSettings.maxNewLines != null) + if (filterSettings.maxNewLines != null) { //Gets number of "newlines" int newLines = context.Message.Content.Count(c => c == '\n'); - if (newLines > modSettings.maxNewLines.Value) + if (newLines > filterSettings.maxNewLines.Value) { - await context.FilterPunish("too many newlines", modSettings, null, warnSize: (newLines - modSettings.maxNewLines.Value) * 0.5f); + await context.FilterPunish("too many newlines", modSettings, filterSettings, null, warnSize: (newLines - filterSettings.maxNewLines.Value) * 0.5f); return; } } //Checks if a message contains an invite - if (!modSettings.invitesAllowed) + if (!filterSettings.invitesAllowed) { MatchCollection matches = Regex.Matches(message.Content, inviteRegex, regexOptions); foreach (Match match in matches) { var invite = await client.GetInviteAsync(match.Value); - if (invite?.GuildId != null && !modSettings.whitelistedForInvite.Contains(invite.GuildId.Value)) + if (invite?.GuildId != null && !filterSettings.whitelistedForInvite.Contains(invite.GuildId.Value)) { - await context.FilterPunish("Posted Invite", modSettings, match.Value, match.Index); + await context.FilterPunish("Posted Invite", modSettings, filterSettings, match.Value, match.Index); return; } } @@ -211,42 +212,44 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) //Checks if a message contains ugly, unwanted text t̨̠̱̭͓̠ͪ̈́͌ͪͮ̐͒h̲̱̯̀͂̔̆̌͊ͅà̸̻͌̍̍ͅt͕̖̦͂̎͂̂ͮ͜ ̲͈̥͒ͣ͗̚l̬͚̺͚͎̆͜ͅo͔̯̖͙ͩõ̲̗̎͆͜k̦̭̮̺ͮ͆̀ ͙̍̂͘l̡̮̱̤͍̜̲͙̓̌̐͐͂̓i͙̬ͫ̀̒͑̔͐k̯͇̀ͭe̎͋̓́ ̥͖̼̬ͪ̆ṫ͏͕̳̞̯h̛̼͔ͩ̑̿͑i͍̲̽ͮͪsͦ͋ͦ̌͗ͭ̋ //Props to Mathias Bynens for the regex string const string zalgoRegex = @"([\0-\u02FF\u0370-\u1AAF\u1B00-\u1DBF\u1E00-\u20CF\u2100-\uD7FF\uE000-\uFE1F\uFE30-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])([\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]+)"; - if (modSettings.zalgoAllowed == false) + if (filterSettings.zalgoAllowed == false) { MatchCollection matches = Regex.Matches(message.Content, zalgoRegex, regexOptions); if (matches.Any()) { - await context.FilterPunish("zalgo usage", modSettings, null); + await context.FilterPunish("zalgo usage", modSettings, filterSettings, null); return; } } - const string linkRegex = @"((?:https?|steam):\/\/[^\s<]+[^<.,:;" + "\"\'\\]\\s])"; - MatchCollection linkMatches = Regex.Matches(message.Content, linkRegex, regexOptions); - //if (matches != null && matches.Count > 0) await new LogMessage(LogSeverity.Info, "Filter", "Link detected").Log(); - foreach (Match match in linkMatches) + //Check for links if setting enabled and user is not allowed to link + if (filterSettings.allowedLinks?.Count is not null or 0 && (filterSettings.allowedToLink == null || !gUser.RoleIds.Intersect(filterSettings.allowedToLink).Any())) { - if (msgContent.Equals(match.Value, StringComparison.InvariantCultureIgnoreCase)) return; - msgContent = msgContent.Replace(match.Value, "", StringComparison.InvariantCultureIgnoreCase); - //Checks for links - if ((modSettings.allowedLinks != null && modSettings.allowedLinks.Count > 0) && (modSettings.allowedToLink == null || !gUser.RoleIds.Intersect(modSettings.allowedToLink).Any())) + const string linkRegex = @"((?:https?|steam):\/\/[^\s<]+[^<.,:;" + "\"\'\\]\\s])"; + MatchCollection linkMatches = Regex.Matches(message.Content, linkRegex, regexOptions); + //if (matches != null && matches.Count > 0) await new LogMessage(LogSeverity.Info, "Filter", "Link detected").Log(); + foreach (Match match in linkMatches) { - if (!modSettings.allowedLinks.Any(s => match.Value.Contains(s, StringComparison.InvariantCultureIgnoreCase))) + if (msgContent.Equals(match.Value, StringComparison.InvariantCultureIgnoreCase)) return; + msgContent = msgContent.Replace(match.Value, "", StringComparison.InvariantCultureIgnoreCase); + //Checks for links + + if (!filterSettings.allowedLinks.Any(s => match.Value.Contains(s, StringComparison.InvariantCultureIgnoreCase))) { - await context.FilterPunish("Using unauthorized links", modSettings, match.Value, match.Index, warnSize: 1); + await context.FilterPunish("Using unauthorized links", modSettings, filterSettings, match.Value, match.Index, warnSize: 1); return; } } } //Check for emojis - if (modSettings.badUEmojis?.Count is not null or 0 && modSettings.badUEmojis.Any(s => message.Content.Contains(s))) + if (filterSettings.badUEmojis?.Count is not null or 0 && filterSettings.badUEmojis.Any(s => message.Content.Contains(s))) { - await context.FilterPunish("Bad emoji used", modSettings, null, warnSize: 0.8f); + await context.FilterPunish("Bad emoji used", modSettings, filterSettings, null, warnSize: 0.8f); return; } - if (modSettings.allowedCaps > 0 && message.Content.Length > 5) + if (filterSettings.allowedCaps > 0 && message.Content.Length > 5) { uint amountCaps = 0; foreach (char c in message.Content) @@ -256,9 +259,9 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) amountCaps++; } } - if (((amountCaps / (float)message.Content.Length) * 100) >= modSettings.allowedCaps) + if (((amountCaps / (float)message.Content.Length) * 100) >= filterSettings.allowedCaps) { - await context.FilterPunish("Excessive caps", modSettings, null, warnSize: 0.3f); + await context.FilterPunish("Excessive caps", modSettings, filterSettings, null, warnSize: 0.3f); return; } } @@ -270,12 +273,12 @@ public async Task CheckMessage(IMessage message, ICommandContext context = null) { if (!string.IsNullOrEmpty(detectedBadWord.Euphemism)) { - await context.FilterPunish($"Bad word used ({detectedBadWord.Euphemism})", modSettings, detectedBadWord.Word, badWordResult.index, detectedBadWord.Size); + await context.FilterPunish($"Bad word used ({detectedBadWord.Euphemism})", modSettings, filterSettings, detectedBadWord.Word, badWordResult.index, detectedBadWord.Size); return; } else { - await context.FilterPunish("Bad word used", modSettings, detectedBadWord.Word, badWordResult.index, detectedBadWord.Size); + await context.FilterPunish("Bad word used", modSettings, filterSettings, detectedBadWord.Word, badWordResult.index, detectedBadWord.Size); return; } } diff --git a/BotCatMaxy/Components/Filter/FilterUtilities.cs b/BotCatMaxy/Components/Filter/FilterUtilities.cs index d5a808a..7fa6a9b 100644 --- a/BotCatMaxy/Components/Filter/FilterUtilities.cs +++ b/BotCatMaxy/Components/Filter/FilterUtilities.cs @@ -77,12 +77,12 @@ public static (BadWord word, int? index) CheckForBadWords(this string message, B return (null, null); } - public static async Task FilterPunish(this ICommandContext context, string reason, ModerationSettings settings, string badText, int? index = null, float warnSize = 0.5f) + public static async Task FilterPunish(this ICommandContext context, string reason, ModerationSettings modSettings, FilterSettings filterSettings, string badText, int? index = null, float warnSize = 0.5f) { - await context.FilterPunish(context.User as IGuildUser, reason, settings, badText, index: index, delete: true, warnSize: warnSize); + await context.FilterPunish(context.User as IGuildUser, reason, modSettings, filterSettings, badText, index: index, delete: true, warnSize: warnSize); } - public static async Task FilterPunish(this ICommandContext context, IGuildUser user, string reason, ModerationSettings settings, string badText, int? index = null, bool delete = true, float warnSize = 0.5f) + public static async Task FilterPunish(this ICommandContext context, IGuildUser user, string reason, ModerationSettings modSettings, FilterSettings filterSettings, string badText, int? index = null, bool delete = true, float warnSize = 0.5f) { string content = context.Message.Content; if (badText != null) //will be null in case of reaction warn where reason speaks for itself @@ -105,10 +105,10 @@ public static async Task FilterPunish(this ICommandContext context, IGuildUser u string jumpLink = await DiscordLogging.LogMessage(reason, context.Message, context.Guild, color: Color.Gold, authorOveride: user, textOverride: content); await user.Warn(warnSize, reason, context.Channel as ITextChannel, logLink: jumpLink); - if (settings?.anouncementChannels?.Contains(context.Channel.Id) ?? false) //If this channel is an anouncement channel + if (filterSettings?.anouncementChannels?.Contains(context.Channel.Id) ?? false) //If this channel is an anouncement channel return; - Task warnMessage = await NotifyPunish(context, user, reason, settings, content); + Task warnMessage = await NotifyPunish(context, user, reason, modSettings, content); if (delete) { diff --git a/BotCatMaxy/Models/FilterSettings.cs b/BotCatMaxy/Models/FilterSettings.cs new file mode 100644 index 0000000..075ac11 --- /dev/null +++ b/BotCatMaxy/Models/FilterSettings.cs @@ -0,0 +1,28 @@ +using BotCatMaxy.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotCatMaxy.Models +{ + public class FilterSettings : DataObject + { + public HashSet channelsWithoutAutoMod = new(); + public HashSet whitelistedForInvite = new(); + public HashSet anouncementChannels = new(); + public HashSet allowedToLink = new(); + public HashSet filterIgnored = new(); + public HashSet allowedLinks = new(); + public HashSet badUEmojis = new(); + public HashSet badLinks = new(); + + public bool invitesAllowed = true; + public bool moderateNames = false; + public bool zalgoAllowed = true; + public ushort? maxNewLines = null; + public ushort allowedCaps = 0; + public uint? maxEmojis = null; + } +} diff --git a/BotCatMaxy/Models/ModerationSettings.cs b/BotCatMaxy/Models/ModerationSettings.cs index 57ee6db..01896de 100644 --- a/BotCatMaxy/Models/ModerationSettings.cs +++ b/BotCatMaxy/Models/ModerationSettings.cs @@ -7,24 +7,10 @@ namespace BotCatMaxy.Models public class ModerationSettings : DataObject { public List ableToWarn = new(); - public List cantBeWarned = new(); - public List channelsWithoutAutoMod = new(); - public HashSet allowedLinks = new(); - public List allowedToLink = new(); - public HashSet badUEmojis = new(); - public HashSet badLinks = new(); public List ableToBan = new(); - public List anouncementChannels = new(); public Dictionary dynamicSlowmode = new(); - public List whitelistedForInvite = new(); public TimeSpan? maxTempAction = null; public ulong mutedRole = 0; - public ushort allowedCaps = 0; public bool useOwnerID = false; - public bool invitesAllowed = true; - public bool zalgoAllowed = true; - public uint? maxEmojis = null; - public bool moderateNames = false; - public ushort? maxNewLines = null; } } \ No newline at end of file diff --git a/BotCatMaxy/Utilities/PermissionUtilities.cs b/BotCatMaxy/Utilities/PermissionUtilities.cs index 8b6a4d3..398257b 100644 --- a/BotCatMaxy/Utilities/PermissionUtilities.cs +++ b/BotCatMaxy/Utilities/PermissionUtilities.cs @@ -60,10 +60,6 @@ public static bool CantBeWarned(this IGuildUser user) { if (user == null) return false; if (user.HasAdmin()) return true; - - ModerationSettings settings = user.Guild.LoadFromFile(false); - if (settings != null && user.RoleIds.Intersect(settings.cantBeWarned).Any()) - return true; return false; } From 5c9a24894e0bc9dd1838e73854231e02f9f5db6a Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Mon, 5 Apr 2021 22:22:20 -0700 Subject: [PATCH 39/42] Updates Test dependencies --- Tests/Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 6247a43..8ba831b 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -10,14 +10,14 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 46e1fe6e22263140dc9556bb11c3ec304426827a Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Tue, 6 Apr 2021 11:15:04 -0700 Subject: [PATCH 40/42] Greatly optimizes data by using upsert requests instead of a delete and insert request as well as many smaller changes like adding FilterSettings to cache --- BotCatMaxy/Components/Data/DataManipulator.cs | 30 ++++++++----------- BotCatMaxy/Components/Data/SettingsCache.cs | 5 ++-- Tests/DataTests.cs | 1 + Tests/FilterTests.cs | 6 ++-- Tests/Tests.csproj | 2 +- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/BotCatMaxy/Components/Data/DataManipulator.cs b/BotCatMaxy/Components/Data/DataManipulator.cs index 2c251a8..cce358b 100644 --- a/BotCatMaxy/Components/Data/DataManipulator.cs +++ b/BotCatMaxy/Components/Data/DataManipulator.cs @@ -19,6 +19,7 @@ namespace BotCatMaxy.Data public static class DataManipulator { static readonly Type cacheType = typeof(GuildSettings); + static readonly ReplaceOptions replaceOptions = new() { IsUpsert = true }; public static async Task MapTypes() { @@ -43,9 +44,6 @@ public static async Task MapTypes() //checks and gets from cache if it's there public static T GetFromCache(this IGuild guild, out FieldInfo field, out GuildSettings gCache) where T : DataObject { - if (guild == null) - throw new ArgumentNullException("Guild is null"); - string tName = typeof(T).Name; tName = char.ToLower(tName[0], CultureInfo.InvariantCulture) + tName.Substring(1); field = cacheType.GetField(tName); @@ -88,9 +86,6 @@ public static void AddToCache(this T file, FieldInfo field = null, GuildSetti public static T LoadFromFile(this IGuild guild, bool createFile = false) where T : DataObject { - if (guild == null) - throw new ArgumentNullException(nameof(guild)); - T file = guild.GetFromCache(out FieldInfo field, out GuildSettings gCache); if (file != null) return file; @@ -103,11 +98,11 @@ public static T LoadFromFile(this IGuild guild, bool createFile = false) wher var doc = cursor?.FirstOrDefault(); if (doc != null) file = BsonSerializer.Deserialize(doc); } + if (createFile && file == null) { file = (T)Activator.CreateInstance(typeof(T)); file.guild = guild; - return file; } } @@ -126,10 +121,10 @@ public static void SaveToFile(this T file) where T : DataObject file.AddToCache(); var collection = file.guild.GetCollection(true); var name = typeof(T).Name; - collection.FindOneAndDelete(Builders.Filter.Eq("_id", name)); - var doc = file.ToBsonDocument(); - doc.Set("_id", name); - collection.InsertOne(doc); + var filter = Builders.Filter.Eq("_id", name); + var document = file.ToBsonDocument() + .Set("_id", name); + collection.ReplaceOne(filter, document, replaceOptions); } public static IMongoCollection GetInfractionsCollection(this IGuild guild, bool createDir = true) @@ -153,7 +148,7 @@ public static IMongoCollection GetActHistoryCollection(this IGuild var guildCollection = db.GetCollection(guild.Id.ToString()); if (guildCollection.CountDocuments(new BsonDocument()) > 0 || createDir) { - return guildCollection; + return guildCollection as MongoCollectionBase; } return null; @@ -162,7 +157,6 @@ public static IMongoCollection GetActHistoryCollection(this IGuild public static List LoadInfractions(this IGuildUser user, bool createDir = false) => user?.Id.LoadInfractions(user.Guild, createDir); - public static List LoadInfractions(this UserRef userRef, IGuild guild, bool createDir = false) => userRef?.ID.LoadInfractions(guild, createDir); @@ -198,8 +192,9 @@ public static List LoadActRecord(this ulong userID, IGuild guild, boo public static void SaveActRecord(this ulong userID, IGuild guild, List acts) { var collection = guild.GetActHistoryCollection(true); - collection.FindOneAndDelete(Builders.Filter.Eq("_id", userID)); - collection.InsertOne(new UserActs { ID = userID, acts = acts }.ToBsonDocument()); + var filter = Builders.Filter.Eq("_id", userID); + var document = new UserActs { ID = userID, acts = acts }.ToBsonDocument(); + collection.ReplaceOne(filter, document, replaceOptions); } public static void SaveInfractions(this SocketGuildUser user, List infractions) => @@ -210,8 +205,9 @@ public static void SaveInfractions(this UserRef userRef, List infrac public static void SaveInfractions(this ulong userID, IGuild guild, List infractions) { var collection = guild.GetInfractionsCollection(true); - collection.FindOneAndDelete(Builders.Filter.Eq("_id", userID)); - collection.InsertOne(new UserInfractions { ID = userID, infractions = infractions }.ToBsonDocument()); + var filter = Builders.Filter.Eq("_id", userID); + var document = new UserInfractions { ID = userID, infractions = infractions }.ToBsonDocument(); + collection.ReplaceOne(filter, document, replaceOptions); } } diff --git a/BotCatMaxy/Components/Data/SettingsCache.cs b/BotCatMaxy/Components/Data/SettingsCache.cs index 90fc795..a250e3f 100644 --- a/BotCatMaxy/Components/Data/SettingsCache.cs +++ b/BotCatMaxy/Components/Data/SettingsCache.cs @@ -28,10 +28,11 @@ public class GuildSettings : IEquatable private readonly IGuild guild; public ulong ID => guild.Id; public ModerationSettings moderationSettings; - public LogSettings logSettings; public TempActionList tempActionList; - public BadWordList badWordList; public ReportSettings reportSettings; + public FilterSettings filterSettings; + public BadWordList badWordList; + public LogSettings logSettings; public GuildSettings(IGuild guild) { diff --git a/Tests/DataTests.cs b/Tests/DataTests.cs index 63a0b97..03fb891 100644 --- a/Tests/DataTests.cs +++ b/Tests/DataTests.cs @@ -77,6 +77,7 @@ public void TestCollections() collection = guild.GetCollection(false); Assert.NotNull(collection); Assert.Equal(guild.OwnerId.ToString(), collection.CollectionNamespace.CollectionName); + Assert.True(ownerCollection is MongoCollectionBase); } [Fact] diff --git a/Tests/FilterTests.cs b/Tests/FilterTests.cs index af28d43..1c14f5f 100644 --- a/Tests/FilterTests.cs +++ b/Tests/FilterTests.cs @@ -22,7 +22,7 @@ public class FilterTests : BaseDataTests private readonly MockDiscordClient client = new(); private readonly MockGuild guild = new(); private readonly FilterHandler filter; - private ModerationSettings settings; + private FilterSettings settings; private Task channelTask; public FilterTests() @@ -30,7 +30,7 @@ public FilterTests() filter = new(client); client.guilds.Add(guild); channelTask = guild.CreateTextChannelAsync("TestChannel"); - ModerationSettings settings = new() + FilterSettings settings = new() { guild = guild, moderateNames = true, @@ -49,7 +49,7 @@ public async Task PunishTest() var testee = users.First(user => user.Username == "Testee"); var message = channel.SendMessageAsOther("calzone", testee); var context = new MockCommandContext(client, message); - await context.FilterPunish("Testing Punish", settings, "calzone"); + await context.FilterPunish("Testing Punish", null, settings, "calzone"); var infractons = testee.LoadInfractions(true); Assert.NotNull(infractons); Assert.NotEmpty(infractons); diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 8ba831b..2f41e00 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -11,7 +11,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive From cb7bdcfca17a3700bf6e18eac64f536ba5fac2a9 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Tue, 6 Apr 2021 11:26:52 -0700 Subject: [PATCH 41/42] Fixes a few typos in FilterCommands --- .../Components/Filter/FilterCommands.cs | 32 ++++++++++--------- .../Components/Filter/FilterUtilities.cs | 2 +- BotCatMaxy/Models/FilterSettings.cs | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index 198311b..5ddb32a 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -466,7 +466,7 @@ public async Task ToggleInviteWarn() settings.invitesAllowed = !settings.invitesAllowed; settings.SaveToFile(); - await message.ModifyAsync(msg => msg.Content = "set invites allowed to " + settings.invitesAllowed.ToString().ToLower()); + await message.ModifyAsync(msg => msg.Content = "Set invites allowed to " + settings.invitesAllowed.ToString().ToLower()); } [Command("togglezalgowarn"), Alias("togglezalgoallowed")] @@ -523,35 +523,37 @@ public async Task AddBadWord(string word, string euphemism = null, float size = await ReplyAsync($"Added {badWord.Word}{((badWord.Euphemism != null) ? $", also known as {badWord.Euphemism}" : "")} to bad word list"); } - [Command("addanouncementchannel"), HasAdmin] - [Summary("Sets this channel as an announcement channel.")] - public async Task AddAnouncementChannel() + [Command("addannouncementchannel"), HasAdmin] + [Alias("addanouncementchannel")] //Such a common misspelling this was the name for a long time with no reports + [Summary("Sets this channel as an 'announcement' channel.")] + public async Task AddAnnouncementChannel() { var settings = Context.Guild.LoadFromFile(true); - if (settings.anouncementChannels.Contains(Context.Channel.Id)) + if (settings.announcementChannels.Contains(Context.Channel.Id)) { - await ReplyAsync("This is already an 'anouncement' channel"); + await ReplyAsync("This is already an 'announcement' channel"); return; } - settings.anouncementChannels.Add(Context.Channel.Id); + settings.announcementChannels.Add(Context.Channel.Id); settings.SaveToFile(); - await ReplyAsync("This channel is now an 'anouncement' channel"); + await ReplyAsync("This channel is now an 'announcement' channel"); } - [Command("removeanouncementchannel"), HasAdmin] + [Command("removeannouncementchannel"), HasAdmin] + [Alias("removeanouncementchannel")] //Such a common misspelling this was the name for a long time with no reports [Summary("Sets this channel as a regular channel.")] - public async Task RemoveAnouncementChannel() + public async Task RemoveAnnouncementChannel() { var settings = Context.Guild.LoadFromFile(false); - if (!settings?.anouncementChannels?.Contains(Context.Channel.Id) ?? true) + if (!settings?.announcementChannels?.Contains(Context.Channel.Id) ?? true) { - //Goes through various steps to check if 1. settings (for anouncement channels) exist 2. Current channel is in those settings - await ReplyAsync("This already not an 'anouncement' channel"); + //Goes through various steps to check if 1. settings (for announcement channels) exist 2. Current channel is in those settings + await ReplyAsync("This already not an 'announcement' channel"); return; } - settings.anouncementChannels.Remove(Context.Channel.Id); + settings.announcementChannels.Remove(Context.Channel.Id); settings.SaveToFile(); - await ReplyAsync("This channel is now not an 'anouncement' channel"); + await ReplyAsync("This channel is now not an 'announcement' channel"); } [Command("togglenamefilter")] diff --git a/BotCatMaxy/Components/Filter/FilterUtilities.cs b/BotCatMaxy/Components/Filter/FilterUtilities.cs index 7fa6a9b..0cb70eb 100644 --- a/BotCatMaxy/Components/Filter/FilterUtilities.cs +++ b/BotCatMaxy/Components/Filter/FilterUtilities.cs @@ -105,7 +105,7 @@ public static async Task FilterPunish(this ICommandContext context, IGuildUser u string jumpLink = await DiscordLogging.LogMessage(reason, context.Message, context.Guild, color: Color.Gold, authorOveride: user, textOverride: content); await user.Warn(warnSize, reason, context.Channel as ITextChannel, logLink: jumpLink); - if (filterSettings?.anouncementChannels?.Contains(context.Channel.Id) ?? false) //If this channel is an anouncement channel + if (filterSettings?.announcementChannels?.Contains(context.Channel.Id) ?? false) //If this channel is an anouncement channel return; Task warnMessage = await NotifyPunish(context, user, reason, modSettings, content); diff --git a/BotCatMaxy/Models/FilterSettings.cs b/BotCatMaxy/Models/FilterSettings.cs index 075ac11..9d2a4b6 100644 --- a/BotCatMaxy/Models/FilterSettings.cs +++ b/BotCatMaxy/Models/FilterSettings.cs @@ -11,7 +11,7 @@ public class FilterSettings : DataObject { public HashSet channelsWithoutAutoMod = new(); public HashSet whitelistedForInvite = new(); - public HashSet anouncementChannels = new(); + public HashSet announcementChannels = new(); public HashSet allowedToLink = new(); public HashSet filterIgnored = new(); public HashSet allowedLinks = new(); From 862399eb0c120e48b9c65d823bf0d84da13aee06 Mon Sep 17 00:00:00 2001 From: Daniel Bereza <36429439+Blackcatmaxy@users.noreply.github.com> Date: Tue, 6 Apr 2021 12:16:28 -0700 Subject: [PATCH 42/42] Makes explicit filter info not restricted but still behind a confirmation --- .../Components/Filter/FilterCommands.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/BotCatMaxy/Components/Filter/FilterCommands.cs b/BotCatMaxy/Components/Filter/FilterCommands.cs index 5ddb32a..70dbe52 100644 --- a/BotCatMaxy/Components/Filter/FilterCommands.cs +++ b/BotCatMaxy/Components/Filter/FilterCommands.cs @@ -31,26 +31,25 @@ public async Task ListAutoMod(string extension = "") var mutualGuilds = (Context.Message.Author as SocketUser).MutualGuilds.ToArray(); var guildsEmbed = new EmbedBuilder(); - guildsEmbed.WithTitle("Reply with the number next to the guild you want to check the filter info from"); + guildsEmbed.WithTitle("Reply with the number corresponding to the guild you want to check the filter info from"); for (int i = 0; i < mutualGuilds.Length; i++) { - guildsEmbed.AddField($"[{i + 1}] {mutualGuilds[i].Name}", mutualGuilds[i].Id); + guildsEmbed.AddField($"[{i + 1}] {mutualGuilds[i].Name}", $"ID: {mutualGuilds[i].Id}"); } await ReplyAsync(embed: guildsEmbed.Build()); SocketGuild guild; while (true) { var result = await Interactivity.NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); - var reply = result.Value; - if (reply == null || reply.Content == "cancel") + if (result.Value?.Content is null or "cancel") { await ReplyAsync("You have timed out or canceled"); return; } try { - guild = mutualGuilds[ushort.Parse(reply.Content) - 1]; + guild = mutualGuilds[ushort.Parse(result.Value.Content) - 1]; break; } catch @@ -67,7 +66,8 @@ public async Task ListAutoMod(string extension = "") string message = ""; bool useExplicit = false; - if (extension != null && extension.ToLower() == "explicit" || extension.ToLower() == "e") + extension = extension?.ToLowerInvariant(); + if (extension == "explicit" || extension == "e") { if (guild.GetUser(Context.Message.Author.Id).CanWarn()) { @@ -75,14 +75,22 @@ public async Task ListAutoMod(string extension = "") } else { - await ReplyAsync("You lack the permissions for viewing explicit bad words"); - return; + await ReplyAsync("Are you sure you want to view the explicit filter? Reply with !confirm if you are sure."); + var result = await Interactivity.NextMessageAsync(timeout: TimeSpan.FromMinutes(1)); + if (result.Value?.Content.Equals("!confirm", StringComparison.InvariantCultureIgnoreCase) ?? false) + { + useExplicit = true; + } + else + { + useExplicit = false; + } } } if (settings == null) { - embed.AddField("Moderation settings", "Are null", true); + embed.AddField("Filter settings", "Are null", true); } else { @@ -155,7 +163,10 @@ public async Task ListAutoMod(string extension = "") } } message = words.ListItems("\n"); - embed.AddField("Badword euphemisms (not an exhaustive list)", message, false); + string listName; + if (useExplicit) listName = "Bad words with euphemisms"; + else listName = "Bad word euphemisms (not an exhaustive list)"; + embed.AddField(listName, message, false); } await ReplyAsync("The symbol '¤' next to a word means that you can be warned for a word that contains the bad word", embed: embed.Build());