From 9a6cbfd101c4aabfda1fae795850ee469fe2e4d9 Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sat, 20 Jan 2024 13:05:16 +1000 Subject: [PATCH 01/13] Attribute: Ignores Bots who try to use the command --- DiscordBot/Attributes/IgnoreBotsAttribute.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 DiscordBot/Attributes/IgnoreBotsAttribute.cs diff --git a/DiscordBot/Attributes/IgnoreBotsAttribute.cs b/DiscordBot/Attributes/IgnoreBotsAttribute.cs new file mode 100644 index 00000000..0c5b1c3c --- /dev/null +++ b/DiscordBot/Attributes/IgnoreBotsAttribute.cs @@ -0,0 +1,20 @@ +using Discord.Commands; + +namespace DiscordBot.Attributes; + +/// +/// Simple attribute, if the command is used by a bot, it escapes early and doesn't run the command. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class IgnoreBotsAttribute : PreconditionAttribute +{ + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + if (context.Message.Author.IsBot) + { + return Task.FromResult(PreconditionResult.FromError(string.Empty)); + } + + return Task.FromResult(PreconditionResult.FromSuccess()); + } +} \ No newline at end of file From 7e76ea96a21b4aa1ee0104c80342b21d74bfd399 Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sat, 20 Jan 2024 13:10:21 +1000 Subject: [PATCH 02/13] Split `preconditions` and move to `Attributes` --- .../Attributes/BotCommandChannelAttribute.cs | 22 ++++++++++ DiscordBot/Attributes/RoleAttributes.cs | 31 +++++++++++++ .../ThreadAttributes.cs} | 44 +------------------ DiscordBot/Modules/EmbedModule.cs | 1 + DiscordBot/Modules/TicketModule.cs | 1 + DiscordBot/Modules/UserModule.cs | 18 +++----- 6 files changed, 63 insertions(+), 54 deletions(-) create mode 100644 DiscordBot/Attributes/BotCommandChannelAttribute.cs create mode 100644 DiscordBot/Attributes/RoleAttributes.cs rename DiscordBot/{Preconditions.cs => Attributes/ThreadAttributes.cs} (68%) diff --git a/DiscordBot/Attributes/BotCommandChannelAttribute.cs b/DiscordBot/Attributes/BotCommandChannelAttribute.cs new file mode 100644 index 00000000..d8eada5d --- /dev/null +++ b/DiscordBot/Attributes/BotCommandChannelAttribute.cs @@ -0,0 +1,22 @@ +using Discord.Commands; +using DiscordBot.Settings; +using Microsoft.Extensions.DependencyInjection; + +namespace DiscordBot.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class BotCommandChannelAttribute : PreconditionAttribute +{ + public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + var settings = services.GetRequiredService(); + + if (context.Channel.Id == settings.BotCommandsChannel.Id) + { + return await Task.FromResult(PreconditionResult.FromSuccess()); + } + + Task task = context.Message.DeleteAfterSeconds(seconds: 10); + return await Task.FromResult(PreconditionResult.FromError($"This command can only be used in <#{settings.BotCommandsChannel.Id.ToString()}>.")); + } +} \ No newline at end of file diff --git a/DiscordBot/Attributes/RoleAttributes.cs b/DiscordBot/Attributes/RoleAttributes.cs new file mode 100644 index 00000000..d4e337ce --- /dev/null +++ b/DiscordBot/Attributes/RoleAttributes.cs @@ -0,0 +1,31 @@ +using Discord.Commands; +using Discord.WebSocket; +using DiscordBot.Settings; +using Microsoft.Extensions.DependencyInjection; + +namespace DiscordBot.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class RequireAdminAttribute : PreconditionAttribute +{ + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + var user = (SocketGuildUser)context.Message.Author; + + if (user.Roles.Any(x => x.Permissions.Administrator)) return Task.FromResult(PreconditionResult.FromSuccess()); + return Task.FromResult(PreconditionResult.FromError(user + " attempted to use admin only command!")); + } +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class RequireModeratorAttribute : PreconditionAttribute +{ + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + var user = (SocketGuildUser)context.Message.Author; + var settings = services.GetRequiredService(); + + if (user.Roles.Any(x => x.Id == settings.ModeratorRoleId)) return Task.FromResult(PreconditionResult.FromSuccess()); + return Task.FromResult(PreconditionResult.FromError(user + " attempted to use a moderator command!")); + } +} \ No newline at end of file diff --git a/DiscordBot/Preconditions.cs b/DiscordBot/Attributes/ThreadAttributes.cs similarity index 68% rename from DiscordBot/Preconditions.cs rename to DiscordBot/Attributes/ThreadAttributes.cs index 938df8dc..8cd6583b 100644 --- a/DiscordBot/Preconditions.cs +++ b/DiscordBot/Attributes/ThreadAttributes.cs @@ -3,49 +3,7 @@ using DiscordBot.Settings; using Microsoft.Extensions.DependencyInjection; -namespace DiscordBot; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class RequireAdminAttribute : PreconditionAttribute -{ - public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - var user = (SocketGuildUser)context.Message.Author; - - if (user.Roles.Any(x => x.Permissions.Administrator)) return Task.FromResult(PreconditionResult.FromSuccess()); - return Task.FromResult(PreconditionResult.FromError(user + " attempted to use admin only command!")); - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class RequireModeratorAttribute : PreconditionAttribute -{ - public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - var user = (SocketGuildUser)context.Message.Author; - var settings = services.GetRequiredService(); - - if (user.Roles.Any(x => x.Id == settings.ModeratorRoleId)) return Task.FromResult(PreconditionResult.FromSuccess()); - return Task.FromResult(PreconditionResult.FromError(user + " attempted to use a moderator command!")); - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class BotChannelOnlyAttribute : PreconditionAttribute -{ - public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - var settings = services.GetRequiredService(); - - if (context.Channel.Id == settings.BotCommandsChannel.Id) - { - return await Task.FromResult(PreconditionResult.FromSuccess()); - } - - Task task = context.Message.DeleteAfterSeconds(seconds: 10); - return await Task.FromResult(PreconditionResult.FromError($"This command can only be used in <#{settings.BotCommandsChannel.Id.ToString()}>.")); - } -} +namespace DiscordBot.Attributes; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class RequireThreadAttribute : PreconditionAttribute diff --git a/DiscordBot/Modules/EmbedModule.cs b/DiscordBot/Modules/EmbedModule.cs index bc59f999..f46bd30a 100644 --- a/DiscordBot/Modules/EmbedModule.cs +++ b/DiscordBot/Modules/EmbedModule.cs @@ -1,6 +1,7 @@ using System.Net; using System.Text; using Discord.Commands; +using DiscordBot.Attributes; using Newtonsoft.Json; // ReSharper disable all UnusedMember.Local diff --git a/DiscordBot/Modules/TicketModule.cs b/DiscordBot/Modules/TicketModule.cs index bdd07d55..67fb8f71 100644 --- a/DiscordBot/Modules/TicketModule.cs +++ b/DiscordBot/Modules/TicketModule.cs @@ -1,4 +1,5 @@ using Discord.Commands; +using DiscordBot.Attributes; using DiscordBot.Services; using DiscordBot.Settings; diff --git a/DiscordBot/Modules/UserModule.cs b/DiscordBot/Modules/UserModule.cs index 2e0031a0..ebb72b57 100644 --- a/DiscordBot/Modules/UserModule.cs +++ b/DiscordBot/Modules/UserModule.cs @@ -255,7 +255,7 @@ public async Task UserCompleted(string message) await Context.Message.DeleteAsync(); } - [Group("Role"), BotChannelOnly] + [Group("Role"), BotCommandChannel] public class RoleModule : ModuleBase { public BotSettings Settings { get; set; } @@ -640,7 +640,7 @@ public async Task CoinFlip() #region Publisher - [Command("PInfo"), BotChannelOnly, Priority(11)] + [Command("PInfo"), BotCommandChannel, Priority(11)] [Summary("Information on how to get publisher role.")] [Alias("publisherinfo")] public async Task PublisherInfo() @@ -656,7 +656,7 @@ public async Task PublisherInfo() await Context.Message.DeleteAfterSeconds(seconds: 2); } - [Command("Publisher"), BotChannelOnly, HideFromHelp] + [Command("Publisher"), BotCommandChannel, HideFromHelp] [Summary("Get the Asset-Publisher role by verifying who you are. Syntax: !publisher publisherID")] public async Task Publisher(uint publisherId) { @@ -676,7 +676,7 @@ public async Task Publisher(uint publisherId) await Context.Message.DeleteAfterSeconds(seconds: 1); } - [Command("Verify"), BotChannelOnly, HideFromHelp] + [Command("Verify"), BotCommandChannel, HideFromHelp] [Summary("Verify a publisher with the code received by email. Syntax : !verify publisherId code")] public async Task VerifyPackage(uint packageId, string code) { @@ -1014,13 +1014,9 @@ private int ParseNumber(string s) public async Task Birthday() { // URL to cell C15/"Next birthday" cell from Corn's google sheet - var nextBirthday = - "https://docs.google.com/spreadsheets/d/10iGiKcrBl1fjoBNTzdtjEVYEgOfTveRXdI5cybRTnj4/gviz/tq?tqx=out:html&range=C15:C15"; - var doc = new HtmlWeb().Load(nextBirthday); - - // XPath to the table row - var row = doc.DocumentNode.SelectSingleNode("/html/body/table/tr[2]/td"); - var tableText = WebUtility.HtmlDecode(row.InnerText); + const string nextBirthday = "https://docs.google.com/spreadsheets/d/10iGiKcrBl1fjoBNTzdtjEVYEgOfTveRXdI5cybRTnj4/gviz/tq?tqx=out:html&range=C15:C15"; + + var tableText = await WebUtil.GetHtmlNodeInnerText(nextBirthday, "/html/body/table/tr[2]/td"); var message = $"**{tableText}**"; await ReplyAsync(message).DeleteAfterTime(minutes: 3); From a041648863f5e58a78b7a253593536ff434cd3f3 Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sat, 20 Jan 2024 13:10:45 +1000 Subject: [PATCH 03/13] Don't send a message if a pre-condition is empty --- DiscordBot/Services/CommandHandlingService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DiscordBot/Services/CommandHandlingService.cs b/DiscordBot/Services/CommandHandlingService.cs index 77206f63..8b0264aa 100644 --- a/DiscordBot/Services/CommandHandlingService.cs +++ b/DiscordBot/Services/CommandHandlingService.cs @@ -209,6 +209,10 @@ private async Task HandleCommand(SocketMessage messageParam) if (result is PreconditionGroupResult groupResult) { resultString = groupResult.PreconditionResults.First().ErrorReason; + + // Pre-condition doesn't have a reason, we don't respond. + if (resultString == string.Empty) + return; } await context.Channel.SendMessageAsync(resultString).DeleteAfterSeconds(10); } From 8736a806f574b6c74d16d54fb14fa5fd68b44341 Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sat, 20 Jan 2024 13:11:41 +1000 Subject: [PATCH 04/13] Attempt to avoid a race condition by updating database later Not even sure this helps at all, I don't think so. Problem seems to be somewhere with database opening and never closing before something else tries to open another query but I can't find it. --- DiscordBot/Services/UserService.cs | 36 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/DiscordBot/Services/UserService.cs b/DiscordBot/Services/UserService.cs index d4e6c521..b4bee628 100644 --- a/DiscordBot/Services/UserService.cs +++ b/DiscordBot/Services/UserService.cs @@ -222,29 +222,33 @@ public async Task UpdateXp(SocketMessage messageParam) if (_xpCooldown.HasUser(userId)) return; - var user = await _databaseService.GetOrAddUser((SocketGuildUser)messageParam.Author); - if (user == null) - return; + // Add Delay and delay action by 200ms to avoid some weird database collision? + _xpCooldown.AddCooldown(userId, waitTime); + Task.Run(async () => + { + var user = await _databaseService.GetOrAddUser((SocketGuildUser)messageParam.Author); + if (user == null) + return; - bonusXp += baseXp * (1f + user.Karma / 100f); + bonusXp += baseXp * (1f + user.Karma / 100f); - //Reduce XP for members with no role - if (((IGuildUser)messageParam.Author).RoleIds.Count < 2) - baseXp *= .9f; + //Reduce XP for members with no role + if (((IGuildUser)messageParam.Author).RoleIds.Count < 2) + baseXp *= .9f; - //Lower xp for difference between level and karma - var reduceXp = 1f; - if (user.Karma < user.Level) reduceXp = 1 - Math.Min(.9f, (user.Level - user.Karma) * .05f); + //Lower xp for difference between level and karma + var reduceXp = 1f; + if (user.Karma < user.Level) reduceXp = 1 - Math.Min(.9f, (user.Level - user.Karma) * .05f); - var xpGain = (int)Math.Round((baseXp + bonusXp) * reduceXp); - _xpCooldown.AddCooldown(userId, waitTime); + var xpGain = (int)Math.Round((baseXp + bonusXp) * reduceXp); - await _databaseService.Query.UpdateXp(userId.ToString(), user.Exp + (uint)xpGain); + await _databaseService.Query.UpdateXp(userId.ToString(), user.Exp + (uint)xpGain); - _loggingService.LogXp(messageParam.Channel.Name, messageParam.Author.Username, baseXp, bonusXp, reduceXp, - xpGain); + _loggingService.LogXp(messageParam.Channel.Name, messageParam.Author.Username, baseXp, bonusXp, reduceXp, + xpGain); - await LevelUp(messageParam, userId); + await LevelUp(messageParam, userId); + }); } /// From a56c947b9f9205e78e7f6d15f1d128ed37ed5a3f Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sat, 20 Jan 2024 13:14:40 +1000 Subject: [PATCH 05/13] Update bday so order of op is more clear --- DiscordBot/Modules/UserModule.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/DiscordBot/Modules/UserModule.cs b/DiscordBot/Modules/UserModule.cs index ebb72b57..cad0ce5e 100644 --- a/DiscordBot/Modules/UserModule.cs +++ b/DiscordBot/Modules/UserModule.cs @@ -1030,29 +1030,29 @@ public async Task Birthday(IUser user) { var searchName = user.Username; // URL to columns B to D of Corn's google sheet - var birthdayTable = - "https://docs.google.com/spreadsheets/d/10iGiKcrBl1fjoBNTzdtjEVYEgOfTveRXdI5cybRTnj4/gviz/tq?tqx=out:html&gid=318080247&range=B:D"; - var doc = new HtmlWeb().Load(birthdayTable); + const string birthdayTable = "https://docs.google.com/spreadsheets/d/10iGiKcrBl1fjoBNTzdtjEVYEgOfTveRXdI5cybRTnj4/gviz/tq?tqx=out:html&gid=318080247&range=B:D"; + var relevantNodes = await WebUtil.GetHtmlNodes(birthdayTable, "/html/body/table/tr"); + var birthdate = default(DateTime); HtmlNode matchedNode = null; var matchedLength = int.MaxValue; // XPath to each table row - foreach (var row in doc.DocumentNode.SelectNodes("/html/body/table/tr")) + foreach (var row in relevantNodes) { // XPath to the name column (C) var nameNode = row.SelectSingleNode("td[2]"); var name = nameNode.InnerText; - if (name.ToLower().Contains(searchName.ToLower())) - // Check for a "Closer" match - if (name.Length < matchedLength) - { - matchedNode = row; - matchedLength = name.Length; - // Nothing will match "Better" so we may as well break out - if (name.Length == searchName.Length) break; - } + + if (!name.ToLower().Contains(searchName.ToLower()) || name.Length >= matchedLength) + continue; + + // Check for a "Closer" match + matchedNode = row; + matchedLength = name.Length; + // Nothing will match "Better" so we may as well break out + if (name.Length == searchName.Length) break; } if (matchedNode != null) From bc0ed9ee14dcd39a57e34dd6611384b674f89620 Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sat, 20 Jan 2024 13:53:44 +1000 Subject: [PATCH 06/13] Update Weather to work without a user input (Default = Context.Author) --- DiscordBot/Modules/Weather/WeatherModule.cs | 12 ++++++++---- DiscordBot/Services/ModerationService.cs | 8 ++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/DiscordBot/Modules/Weather/WeatherModule.cs b/DiscordBot/Modules/Weather/WeatherModule.cs index f3bc4784..f00ccbd5 100644 --- a/DiscordBot/Modules/Weather/WeatherModule.cs +++ b/DiscordBot/Modules/Weather/WeatherModule.cs @@ -54,8 +54,9 @@ private async Task TemperatureEmbed(string city, string replaceCit [Command("Temperature"), HideFromHelp] [Summary("Attempts to provide the temperature of the user provided.")] [Alias("temp"), Priority(20)] - public async Task Temperature(IUser user) + public async Task Temperature(IUser user = null) { + user ??= Context.User; if (!await DoesUserHaveDefaultCity(user)) return; @@ -133,8 +134,9 @@ private async Task WeatherEmbed(string city, string replaceCityWit [Command("Weather"), HideFromHelp, Priority(20)] [Summary("Attempts to provide the weather of the user provided.")] - public async Task CurentWeather(IUser user) + public async Task CurentWeather(IUser user = null) { + user ??= Context.User; if (!await DoesUserHaveDefaultCity(user)) return; @@ -209,8 +211,9 @@ private async Task PollutionEmbed(string city, string replaceCityW [Command("Pollution"), HideFromHelp, Priority(21)] [Summary("Attempts to provide the pollution conditions of the user provided.")] - public async Task Pollution(IUser user) + public async Task Pollution(IUser user = null) { + user ??= Context.User; if (!await DoesUserHaveDefaultCity(user)) return; @@ -256,8 +259,9 @@ private async Task TimeEmbed(string city, string replaceCityWith = [Command("Time"), HideFromHelp, Priority(22)] [Summary("Attempts to provide the time of the user provided.")] - public async Task Time(IUser user) + public async Task Time(IUser user = null) { + user ??= Context.User; if (!await DoesUserHaveDefaultCity(user)) return; diff --git a/DiscordBot/Services/ModerationService.cs b/DiscordBot/Services/ModerationService.cs index eaf82150..d7dd5118 100644 --- a/DiscordBot/Services/ModerationService.cs +++ b/DiscordBot/Services/ModerationService.cs @@ -53,9 +53,7 @@ private async Task MessageDeleted(Cacheable message, Cacheable< // TimeStamp for the Footer - await _loggingService.LogAction( - $"User {user.GetPreferredAndUsername()} has " + - $"deleted the message\n{content}\n from channel #{(await channel.GetOrDownloadAsync()).Name}", ExtendedLogSeverity.Info, embed); + await _loggingService.Log(LogBehaviour.Channel, string.Empty, ExtendedLogSeverity.Info, embed); } private async Task MessageUpdated(Cacheable before, SocketMessage after, ISocketMessageChannel channel) @@ -91,8 +89,6 @@ private async Task MessageUpdated(Cacheable before, SocketMessa // TimeStamp for the Footer - await _loggingService.LogAction( - $"User {user.GetPreferredAndUsername()} has " + - $"updated the message\n{content}\n in channel #{channel.Name}", ExtendedLogSeverity.Info, embed); + await _loggingService.Log(LogBehaviour.Channel,string.Empty, ExtendedLogSeverity.Info, embed); } } \ No newline at end of file From 6171d5949dad481d8f88ade596be47e016055ee1 Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sat, 20 Jan 2024 13:56:09 +1000 Subject: [PATCH 07/13] Add some more embed overrides --- DiscordBot/Extensions/EmbedBuilderExtension.cs | 14 ++++++++++++++ DiscordBot/Services/ModerationService.cs | 11 ++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/DiscordBot/Extensions/EmbedBuilderExtension.cs b/DiscordBot/Extensions/EmbedBuilderExtension.cs index 88f86dd4..25296cc2 100644 --- a/DiscordBot/Extensions/EmbedBuilderExtension.cs +++ b/DiscordBot/Extensions/EmbedBuilderExtension.cs @@ -19,6 +19,13 @@ public static EmbedBuilder FooterQuoteBy(this EmbedBuilder builder, IUser reques return builder; } + public static EmbedBuilder FooterInChannel(this EmbedBuilder builder, IChannel channel) + { + builder.WithFooter( + $"In channel #{channel.Name}", null); + return builder; + } + public static EmbedBuilder AddAuthor(this EmbedBuilder builder, IUser user, bool includeAvatar = true) { builder.WithAuthor( @@ -27,5 +34,12 @@ public static EmbedBuilder AddAuthor(this EmbedBuilder builder, IUser user, bool return builder; } + public static EmbedBuilder AddAuthorWithAction(this EmbedBuilder builder, IUser user, string action, bool includeAvatar = true) + { + builder.WithAuthor( + $"{user.GetUserPreferredName()} - {action}", + includeAvatar ? user.GetAvatarUrl() : null); + return builder; + } } \ No newline at end of file diff --git a/DiscordBot/Services/ModerationService.cs b/DiscordBot/Services/ModerationService.cs index d7dd5118..97cd5105 100644 --- a/DiscordBot/Services/ModerationService.cs +++ b/DiscordBot/Services/ModerationService.cs @@ -44,14 +44,11 @@ private async Task MessageDeleted(Cacheable message, Cacheable< var builder = new EmbedBuilder() .WithColor(DeletedMessageColor) .WithTimestamp(message.Value.Timestamp) - .WithFooter($"In channel {message.Value.Channel.Name}") - .WithAuthor($"{user.GetPreferredAndUsername()} deleted a message") + .FooterInChannel(message.Value.Channel) + .AddAuthorWithAction(user, "Deleted a message", true) .AddField($"Deleted Message {(content.Length != message.Value.Content.Length ? "(truncated)" : "")}", content); var embed = builder.Build(); - - // TimeStamp for the Footer - await _loggingService.Log(LogBehaviour.Channel, string.Empty, ExtendedLogSeverity.Info, embed); } @@ -80,8 +77,8 @@ private async Task MessageUpdated(Cacheable before, SocketMessa var builder = new EmbedBuilder() .WithColor(EditedMessageColor) .WithTimestamp(after.Timestamp) - .WithFooter($"In channel {after.Channel.Name}") - .WithAuthor($"{user.GetPreferredAndUsername()} updated a message"); + .FooterInChannel(after.Channel) + .AddAuthorWithAction(user, "Updated a message", true); if (isCached) builder.AddField($"Previous message content {(isTruncated ? "(truncated)" : "")}", content); builder.WithDescription($"Message: [{after.Id}]({after.GetJumpUrl()})"); From ca3fdb893d5957b68905d8bed00021ee147b474b Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sun, 21 Jan 2024 17:49:03 +1000 Subject: [PATCH 08/13] Use LoggingService properly, attempt to continue feed even on failure --- DiscordBot/Services/UpdateService.cs | 42 +++++++++++++++++----------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/DiscordBot/Services/UpdateService.cs b/DiscordBot/Services/UpdateService.cs index 1470298a..62be1ea4 100644 --- a/DiscordBot/Services/UpdateService.cs +++ b/DiscordBot/Services/UpdateService.cs @@ -48,6 +48,8 @@ public FeedData() //TODO Download all avatars to cache them public class UpdateService { + private const string ServiceName = "UpdateService"; + private readonly ILoggingService _loggingService; private readonly FeedService _feedService; private readonly BotSettings _settings; private readonly CancellationToken _token; @@ -62,10 +64,11 @@ public class UpdateService private UserData _userData; public UpdateService(DiscordSocketClient client, - DatabaseService databaseService, BotSettings settings, FeedService feedService) + DatabaseService databaseService, BotSettings settings, FeedService feedService, ILoggingService loggingService) { _client = client; _feedService = feedService; + _loggingService = loggingService as LoggingService; _settings = settings; _token = new CancellationToken(); @@ -183,9 +186,9 @@ private async Task DownloadDocDatabase() _apiDatabase = ConvertJsToArray(apiInput, false); if (!SerializeUtil.SerializeFile($"{_settings.ServerRootPath}/unitymanual.json", _manualDatabase)) - LoggingService.LogToConsole("Failed to save unitymanual.json", LogSeverity.Warning); + await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile,$"{ServiceName}: Failed to save unitymanual.json", ExtendedLogSeverity.Warning); if (!SerializeUtil.SerializeFile($"{_settings.ServerRootPath}/unityapi.json", _apiDatabase)) - LoggingService.LogToConsole("Failed to save unityapi.json", LogSeverity.Warning); + await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile,$"{ServiceName}: Failed to save unityapi.json", ExtendedLogSeverity.Warning); string[][] ConvertJsToArray(string data, bool isManual) { @@ -214,7 +217,7 @@ string[][] ConvertJsToArray(string data, bool isManual) } catch (Exception e) { - LoggingService.LogToConsole($"Failed to download manual/api file\nEx:{e.ToString()}", LogSeverity.Error); + await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile,$"{ServiceName}: Failed to download manual/api file\nEx:{e.ToString()}", ExtendedLogSeverity.Warning); } } @@ -235,23 +238,30 @@ private async Task UpdateRssFeeds() await Task.Delay(TimeSpan.FromSeconds(30d), _token); while (true) { - if (_feedData != null) + try { - if (_feedData.LastUnityReleaseCheck < DateTime.Now - TimeSpan.FromMinutes(5)) + if (_feedData != null) { - _feedData.LastUnityReleaseCheck = DateTime.Now; + if (_feedData.LastUnityReleaseCheck < DateTime.Now - TimeSpan.FromMinutes(5)) + { + _feedData.LastUnityReleaseCheck = DateTime.Now; - await _feedService.CheckUnityBetasAsync(_feedData); - await _feedService.CheckUnityReleasesAsync(_feedData); - } + await _feedService.CheckUnityBetasAsync(_feedData); + await _feedService.CheckUnityReleasesAsync(_feedData); + } - if (_feedData.LastUnityBlogCheck < DateTime.Now - TimeSpan.FromMinutes(10)) - { - _feedData.LastUnityBlogCheck = DateTime.Now; + if (_feedData.LastUnityBlogCheck < DateTime.Now - TimeSpan.FromMinutes(10)) + { + _feedData.LastUnityBlogCheck = DateTime.Now; - await _feedService.CheckUnityBlogAsync(_feedData); + await _feedService.CheckUnityBlogAsync(_feedData); + } } } + catch (Exception e) + { + await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile,$"{ServiceName}: Failed to update RSS feeds, attempting to continue.", ExtendedLogSeverity.Error); + } await Task.Delay(TimeSpan.FromSeconds(30d), _token); } @@ -270,7 +280,7 @@ private async Task UpdateRssFeeds() } catch { - LoggingService.LogToConsole($"Wikipedia method failed loading URL: {wikiSearchUri}", LogSeverity.Warning); + await _loggingService.LogChannelAndFile($"{ServiceName}: Wikipedia method failed loading URL: {wikiSearchUri}", ExtendedLogSeverity.Warning); return (null, null, null); } @@ -313,7 +323,7 @@ private async Task UpdateRssFeeds() } catch (Exception e) { - LoggingService.LogToConsole($"Wikipedia method likely failed to parse JSON response from: {wikiSearchUri}.\nEx:{e.ToString()}"); + await _loggingService.LogChannelAndFile($"{ServiceName}: Wikipedia method likely failed to parse JSON response from: {wikiSearchUri}.\nEx:{e.ToString()}", ExtendedLogSeverity.Warning); } return (null, null, null); From 37302414d7831fa5d676de566468e8b1b3bc6ceb Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sun, 21 Jan 2024 17:49:32 +1000 Subject: [PATCH 09/13] Sanity method for mods to check if Help service is failing to keep up --- DiscordBot/Modules/UnityHelp/UnityHelpModule.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/DiscordBot/Modules/UnityHelp/UnityHelpModule.cs b/DiscordBot/Modules/UnityHelp/UnityHelpModule.cs index ece8fbc1..8d3a146d 100644 --- a/DiscordBot/Modules/UnityHelp/UnityHelpModule.cs +++ b/DiscordBot/Modules/UnityHelp/UnityHelpModule.cs @@ -1,5 +1,6 @@ using Discord.Commands; using Discord.WebSocket; +using DiscordBot.Attributes; using DiscordBot.Services; using DiscordBot.Settings; @@ -25,6 +26,20 @@ public async Task ResolveAsync() await Context.Message.DeleteAsync(); await HelpService.OnUserRequestChannelClose(Context.User, Context.Channel as SocketThreadChannel); } + + [Command("pending-questions")] + [Summary("Moderation only command, announces the number of pending questions in the help channel.")] + [RequireModerator, HideFromHelp, IgnoreBots] + public async Task PendingQuestionsAsync() + { + if (!BotSettings.UnityHelpBabySitterEnabled) + { + await ReplyAsync("UnityHelp Service currently disabled.").DeleteAfterSeconds(15); + return; + } + var pendingQuestions = HelpService.GetPendingQuestions(); + await ReplyAsync($"There are {pendingQuestions} pending questions in the help channel."); + } #region Utility From 736da8ae337e635ce8ee45d3fdf7eb723dc659cd Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sun, 21 Jan 2024 20:22:44 +1000 Subject: [PATCH 10/13] Tracked question count --- DiscordBot/Modules/UnityHelp/UnityHelpModule.cs | 4 ++-- DiscordBot/Services/UnityHelp/UnityHelpService.cs | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/DiscordBot/Modules/UnityHelp/UnityHelpModule.cs b/DiscordBot/Modules/UnityHelp/UnityHelpModule.cs index 8d3a146d..8e64f22a 100644 --- a/DiscordBot/Modules/UnityHelp/UnityHelpModule.cs +++ b/DiscordBot/Modules/UnityHelp/UnityHelpModule.cs @@ -37,8 +37,8 @@ public async Task PendingQuestionsAsync() await ReplyAsync("UnityHelp Service currently disabled.").DeleteAfterSeconds(15); return; } - var pendingQuestions = HelpService.GetPendingQuestions(); - await ReplyAsync($"There are {pendingQuestions} pending questions in the help channel."); + var trackedQuestionCount = HelpService.GetTrackedQuestionCount(); + await ReplyAsync($"There are {trackedQuestionCount} pending questions in the help channel."); } #region Utility diff --git a/DiscordBot/Services/UnityHelp/UnityHelpService.cs b/DiscordBot/Services/UnityHelp/UnityHelpService.cs index 7a4ccfc8..41fc9dfa 100644 --- a/DiscordBot/Services/UnityHelp/UnityHelpService.cs +++ b/DiscordBot/Services/UnityHelp/UnityHelpService.cs @@ -739,6 +739,11 @@ private bool IsValidAuthorUser(SocketGuildUser user, ulong authorId) return false; } + + public int GetTrackedQuestionCount() + { + return _activeThreads.Count; + } #endregion // Utility Methods From 66c882e504634eecc34970e9664f1086cd3a75d6 Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sun, 21 Jan 2024 20:27:18 +1000 Subject: [PATCH 11/13] Simplify Settings significantly by removing the excessive Channel types --- DiscordBot/Services/ReminderService.cs | 2 +- DiscordBot/Settings/Deserialized/Settings.cs | 61 ++++---------------- 2 files changed, 11 insertions(+), 52 deletions(-) diff --git a/DiscordBot/Services/ReminderService.cs b/DiscordBot/Services/ReminderService.cs index 994cb3a7..4256f1af 100644 --- a/DiscordBot/Services/ReminderService.cs +++ b/DiscordBot/Services/ReminderService.cs @@ -27,7 +27,7 @@ public class ReminderService private readonly ILoggingService _loggingService; private List _reminders = new List(); - private readonly BotCommandsChannel _botCommandsChannel; + private readonly ChannelInfo _botCommandsChannel; private bool _hasChangedSinceLastSave = false; private const int _maxUserReminders = 10; diff --git a/DiscordBot/Settings/Deserialized/Settings.cs b/DiscordBot/Settings/Deserialized/Settings.cs index 8fae9f1b..3f9390c3 100644 --- a/DiscordBot/Settings/Deserialized/Settings.cs +++ b/DiscordBot/Settings/Deserialized/Settings.cs @@ -54,19 +54,19 @@ public class BotSettings #region Channels - public GeneralChannel GeneralChannel { get; set; } - public GenericHelpChannel GenericHelpChannel { get; set; } + public ChannelInfo GeneralChannel { get; set; } + public ChannelInfo GenericHelpChannel { get; set; } - public BotAnnouncementChannel BotAnnouncementChannel { get; set; } - public AnnouncementsChannel AnnouncementsChannel { get; set; } - public BotCommandsChannel BotCommandsChannel { get; set; } - public UnityNewsChannel UnityNewsChannel { get; set; } - public UnityReleasesChannel UnityReleasesChannel { get; set; } - public RulesChannel RulesChannel { get; set; } + public ChannelInfo BotAnnouncementChannel { get; set; } + public ChannelInfo AnnouncementsChannel { get; set; } + public ChannelInfo BotCommandsChannel { get; set; } + public ChannelInfo UnityNewsChannel { get; set; } + public ChannelInfo UnityReleasesChannel { get; set; } + public ChannelInfo RulesChannel { get; set; } // Recruitment Channels - public RecruitmentChannel RecruitmentChannel { get; set; } + public ChannelInfo RecruitmentChannel { get; set; } public ChannelInfo ReportedMessageChannel { get; set; } @@ -90,7 +90,7 @@ public class BotSettings #region User Roles - public UserAssignableRoles UserAssignableRoles { get; set; } + public RoleGroup UserAssignableRoles { get; set; } public ulong MutedRoleId { get; set; } public ulong SubsReleasesRoleId { get; set; } public ulong SubsNewsRoleId { get; set; } @@ -145,10 +145,6 @@ public class RoleGroup public List Roles { get; set; } } -public class UserAssignableRoles : RoleGroup -{ -} - #endregion #region Channel Information @@ -191,41 +187,4 @@ public string GenerateFirstMessage(IUser author) } } -public class GeneralChannel : ChannelInfo -{ -} - -public class GenericHelpChannel : ChannelInfo -{ - -} - -public class BotAnnouncementChannel : ChannelInfo -{ -} - -public class AnnouncementsChannel : ChannelInfo -{ -} - -public class BotCommandsChannel : ChannelInfo -{ -} - -public class UnityNewsChannel : ChannelInfo -{ -} - -public class UnityReleasesChannel : ChannelInfo -{ -} - -public class RecruitmentChannel : ChannelInfo -{ -} - -public class RulesChannel : ChannelInfo -{ -} - #endregion \ No newline at end of file From 1833e9ea22777b4eecbc693c53b06f1efc8ea6ff Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sun, 21 Jan 2024 21:17:56 +1000 Subject: [PATCH 12/13] Add a basic IntroductionWatcher, just deletes messages if same user posts twice Not a huge improvement, but we do seem to have a couple people who post once a day, or once a week. Introductions is a pretty slow channel so this won't really add any additional cost of running. Maybe slightly more memory, but we only hold user id of past 1000 messages, which is tiny. --- DiscordBot/Extensions/UserExtensions.cs | 8 +++ DiscordBot/Program.cs | 4 ++ .../Moderation/IntroductionWatcherService.cs | 64 +++++++++++++++++++ DiscordBot/Settings/Deserialized/Settings.cs | 4 +- DiscordBot/Settings/Settings.example.json | 4 ++ 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 DiscordBot/Services/Moderation/IntroductionWatcherService.cs diff --git a/DiscordBot/Extensions/UserExtensions.cs b/DiscordBot/Extensions/UserExtensions.cs index e4ec119d..38ebc737 100644 --- a/DiscordBot/Extensions/UserExtensions.cs +++ b/DiscordBot/Extensions/UserExtensions.cs @@ -37,4 +37,12 @@ public static string GetPreferredAndUsername(this IUser user) return guildUser.DisplayName; return $"{guildUser.DisplayName} (aka {user.Username})"; } + + public static string GetUserLoggingString(this IUser user) + { + var guildUser = user as SocketGuildUser; + if (guildUser == null) + return $"{user.Username} `{user.Id}`"; + return $"{guildUser.GetPreferredAndUsername()} `{guildUser.Id}`"; + } } \ No newline at end of file diff --git a/DiscordBot/Program.cs b/DiscordBot/Program.cs index 0296ab9a..9884e18e 100644 --- a/DiscordBot/Program.cs +++ b/DiscordBot/Program.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Discord.Commands; using Discord.Interactions; using Discord.WebSocket; @@ -70,6 +71,8 @@ private async Task MainAsync() _unityHelpService = _services.GetRequiredService(); _recruitService = _services.GetRequiredService(); + _services.GetRequiredService(); + return Task.CompletedTask; }; @@ -88,6 +91,7 @@ private IServiceProvider ConfigureServices() => .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/DiscordBot/Services/Moderation/IntroductionWatcherService.cs b/DiscordBot/Services/Moderation/IntroductionWatcherService.cs new file mode 100644 index 00000000..58b98387 --- /dev/null +++ b/DiscordBot/Services/Moderation/IntroductionWatcherService.cs @@ -0,0 +1,64 @@ +using Discord.WebSocket; +using DiscordBot.Settings; + +namespace DiscordBot.Services; + +// Small service to watch users posting new messages in introductions, keeping track of the last 500 messages and deleting any from the same user +public class IntroductionWatcherService +{ + private const string ServiceName = "IntroductionWatcherService"; + + private readonly DiscordSocketClient _client; + private readonly ILoggingService _loggingService; + private readonly SocketChannel _introductionChannel; + + private readonly HashSet _uniqueUsers = new HashSet(MaxMessagesToTrack + 1); + private readonly Queue _orderedUsers = new Queue(MaxMessagesToTrack + 1); + + private const int MaxMessagesToTrack = 1000; + + public IntroductionWatcherService(DiscordSocketClient client, ILoggingService loggingService, BotSettings settings) + { + _client = client; + _loggingService = loggingService; + + if (!settings.IntroductionWatcherServiceEnabled) + { + LoggingService.LogServiceDisabled(ServiceName, nameof(settings.IntroductionWatcherServiceEnabled)); + return; + } + + _introductionChannel = client.GetChannel(settings.IntroductionChannel.Id); + if (_introductionChannel == null) + { + _loggingService.LogAction($"[{ServiceName}] Error: Could not find introduction channel.", ExtendedLogSeverity.Warning); + return; + } + + _client.MessageReceived += MessageReceived; + } + + private async Task MessageReceived(SocketMessage message) + { + // We only watch the introduction channel + if (_introductionChannel == null || message.Channel.Id != _introductionChannel.Id) + return; + + if (_uniqueUsers.Contains(message.Author.Id)) + { + await message.DeleteAsync(); + await _loggingService.LogChannelAndFile( + $"[{ServiceName}]: Duplicate introduction from {message.Author.GetUserLoggingString()} [Message deleted]"); + } + + _uniqueUsers.Add(message.Author.Id); + _orderedUsers.Enqueue(message.Author.Id); + if (_orderedUsers.Count > MaxMessagesToTrack) + { + var oldestUser = _orderedUsers.Dequeue(); + _uniqueUsers.Remove(oldestUser); + } + + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/DiscordBot/Settings/Deserialized/Settings.cs b/DiscordBot/Settings/Deserialized/Settings.cs index 3f9390c3..e25a1832 100644 --- a/DiscordBot/Settings/Deserialized/Settings.cs +++ b/DiscordBot/Settings/Deserialized/Settings.cs @@ -31,10 +31,9 @@ public class BotSettings // Used for enabling/disabling services in the bot public bool RecruitmentServiceEnabled { get; set; } = false; - public bool UnityHelpBabySitterEnabled { get; set; } = false; - public bool ReactRoleServiceEnabled { get; set; } = false; + public bool IntroductionWatcherServiceEnabled { get; set; } = false; #endregion // Service Enabling @@ -54,6 +53,7 @@ public class BotSettings #region Channels + public ChannelInfo IntroductionChannel { get; set; } public ChannelInfo GeneralChannel { get; set; } public ChannelInfo GenericHelpChannel { get; set; } diff --git a/DiscordBot/Settings/Settings.example.json b/DiscordBot/Settings/Settings.example.json index cb1b355f..6d5da3ec 100644 --- a/DiscordBot/Settings/Settings.example.json +++ b/DiscordBot/Settings/Settings.example.json @@ -40,6 +40,10 @@ "desc": "General-Chat Channel", "id": "0" }, + "IntroductionChannel": { // Introductions + "desc": "Introductions Channel", + "id": "0" + }, "botAnnouncementChannel": { // Most bot logs will go here "desc": "Bot-Announcement Channel", "id": "0" From 721e3b41534daf787e5044b6cd218b4d899ca32e Mon Sep 17 00:00:00 2001 From: James Kellie Date: Sun, 21 Jan 2024 22:32:20 +1000 Subject: [PATCH 13/13] Fix issue where welcome would be spammed while user typed --- DiscordBot/Services/UserService.cs | 35 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/DiscordBot/Services/UserService.cs b/DiscordBot/Services/UserService.cs index b4bee628..86da65db 100644 --- a/DiscordBot/Services/UserService.cs +++ b/DiscordBot/Services/UserService.cs @@ -622,7 +622,11 @@ private async Task UserIsTyping(Cacheable user, Cacheable u.id == user.Id)) + { + _welcomeNoticeUsers.RemoveAll(u => u.id == user.Id); + await ProcessWelcomeUser(user.Id, user.Value); + } } private async Task CheckForWelcomeMessage(SocketMessage messageParam) @@ -633,7 +637,12 @@ private async Task CheckForWelcomeMessage(SocketMessage messageParam) var user = messageParam.Author; if (user.IsBot) return; - await ProcessWelcomeUser(user.Id, user); + + if (_welcomeNoticeUsers.Exists(u => u.id == user.Id)) + { + _welcomeNoticeUsers.RemoveAll(u => u.id == user.Id); + await ProcessWelcomeUser(user.Id, user); + } } private async Task UserJoined(SocketGuildUser user) @@ -733,20 +742,18 @@ private async Task DelayedWelcomeService() private async Task ProcessWelcomeUser(ulong userID, IUser user = null) { if (_welcomeNoticeUsers.Exists(u => u.id == userID)) - { // If we didn't get the user passed in, we try grab it user ??= await _client.GetUserAsync(userID); - // if they're null, they've likely left, so we just remove them from the list. - if (user == null) - return; - - var offTopic = await _client.GetChannelAsync(_settings.GeneralChannel.Id) as SocketTextChannel; - if (user is not SocketGuildUser guildUser) - return; - var em = WelcomeMessage(guildUser); - if (offTopic != null && em != null) - await offTopic.SendMessageAsync(string.Empty, false, em); - } + // if they're null, they've likely left, so we just remove them from the list. + if (user == null) + return; + + var offTopic = await _client.GetChannelAsync(_settings.GeneralChannel.Id) as SocketTextChannel; + if (user is not SocketGuildUser guildUser) + return; + var em = WelcomeMessage(guildUser); + if (offTopic != null && em != null) + await offTopic.SendMessageAsync(string.Empty, false, em); }