From db83e6abc2b3c6f4196458067d45ebfda0270bf7 Mon Sep 17 00:00:00 2001 From: Toastie Date: Tue, 16 Jul 2024 22:30:23 +1200 Subject: [PATCH 01/27] 'coins will no longer show double minus sign for negative changes --- src/EllieBot/Modules/Searches/Crypto/CryptoCommands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EllieBot/Modules/Searches/Crypto/CryptoCommands.cs b/src/EllieBot/Modules/Searches/Crypto/CryptoCommands.cs index 13112e1..1bb8910 100644 --- a/src/EllieBot/Modules/Searches/Crypto/CryptoCommands.cs +++ b/src/EllieBot/Modules/Searches/Crypto/CryptoCommands.cs @@ -238,6 +238,6 @@ public partial class Searches => value > 0 ? "▲" : "▼"; private static string GetSign(decimal value) - => value >= 0 ? "+" : "-"; + => value >= 0 ? "+" : ""; } } \ No newline at end of file From f18eb1bf4280b421eb60e28e65fbed91a8649b8c Mon Sep 17 00:00:00 2001 From: Toastie Date: Tue, 16 Jul 2024 22:35:14 +1200 Subject: [PATCH 02/27] 'exexport will now send you the file in DMs, to avoid incident. 'exexport will now have a timestamped name along with the server id --- src/EllieBot/Modules/Expressions/EllieExpressions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EllieBot/Modules/Expressions/EllieExpressions.cs b/src/EllieBot/Modules/Expressions/EllieExpressions.cs index 888901d..fab6775 100644 --- a/src/EllieBot/Modules/Expressions/EllieExpressions.cs +++ b/src/EllieBot/Modules/Expressions/EllieExpressions.cs @@ -397,7 +397,7 @@ public partial class EllieExpressions : EllieModule var serialized = _service.ExportExpressions(ctx.Guild?.Id); await using var stream = await serialized.ToStream(); - await ctx.Channel.SendFileAsync(stream, "exprs-export.yml"); + await ctx.User.SendFileAsync(stream, $"exprs-export_{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}_{(ctx.Guild?.Id.ToString() ?? "global")}.yml"); } [Cmd] From 636ddac4d410a236719ae65aa1c3f0f869562289 Mon Sep 17 00:00:00 2001 From: Toastie Date: Wed, 17 Jul 2024 00:33:43 +1200 Subject: [PATCH 03/27] afk added This still needs some work but it is here --- .gitignore | 3 + .../Modules/Expressions/EllieExpressions.cs | 5 +- src/EllieBot/Modules/Utility/Utility.cs | 132 ++++++++++++++++-- src/EllieBot/data/aliases.yml | 4 +- .../data/strings/commands/commands.en-US.yml | 10 ++ .../strings/responses/responses.en-US.json | 3 +- 6 files changed, 141 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 3b99327..8330cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ src/EllieBot/credentials.json src/EllieBot/old_credentials.json src/EllieBot/credentials.json.bak src/EllieBot/data/EllieBot.db +build.ps1 +build.sh +test.ps1 # Created by https://www.gitignore.io/api/visualstudio,visualstudiocode,windows,linux,macos diff --git a/src/EllieBot/Modules/Expressions/EllieExpressions.cs b/src/EllieBot/Modules/Expressions/EllieExpressions.cs index fab6775..b79091d 100644 --- a/src/EllieBot/Modules/Expressions/EllieExpressions.cs +++ b/src/EllieBot/Modules/Expressions/EllieExpressions.cs @@ -401,11 +401,10 @@ public partial class EllieExpressions : EllieModule } [Cmd] -#if GLOBAL_ELLIE - [OwnerOnly] -#endif public async Task ExprsImport([Leftover] string input = null) { + // todo cooldown on public bot for 1 day, limit 100 + if (!AdminInGuildOrOwnerInDm()) { await Response().Error(strs.expr_insuff_perms).SendAsync(); diff --git a/src/EllieBot/Modules/Utility/Utility.cs b/src/EllieBot/Modules/Utility/Utility.cs index e79b39d..6ec28b5 100644 --- a/src/EllieBot/Modules/Utility/Utility.cs +++ b/src/EllieBot/Modules/Utility/Utility.cs @@ -1,4 +1,4 @@ -#nullable disable +using LinqToDB.Reflection; using EllieBot.Modules.Utility.Services; using Newtonsoft.Json; using System.Diagnostics; @@ -7,6 +7,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; +using EllieBot.Common.ModuleBehaviors; +using EllieBot.Modules.Games.Hangman; using EllieBot.Modules.Searches.Common; namespace EllieBot.Modules.Utility; @@ -41,6 +43,7 @@ public partial class Utility : EllieModule private readonly IHttpClientFactory _httpFactory; private readonly VerboseErrorsService _veService; private readonly IServiceProvider _services; + private readonly AfkService _afkService; public Utility( DiscordSocketClient client, @@ -50,7 +53,8 @@ public partial class Utility : EllieModule DownloadTracker tracker, IHttpClientFactory httpFactory, VerboseErrorsService veService, - IServiceProvider services) + IServiceProvider services, + AfkService afkService) { _client = client; _coord = coord; @@ -60,6 +64,7 @@ public partial class Utility : EllieModule _httpFactory = httpFactory; _veService = veService; _services = services; + _afkService = afkService; } [Cmd] @@ -99,7 +104,7 @@ public partial class Utility : EllieModule [Cmd] [RequireContext(ContextType.Guild)] - public async Task WhosPlaying([Leftover] string game) + public async Task WhosPlaying([Leftover] string? game) { game = game?.Trim().ToUpperInvariant(); if (string.IsNullOrWhiteSpace(game)) @@ -140,7 +145,7 @@ public partial class Utility : EllieModule [Cmd] [RequireContext(ContextType.Guild)] [Priority(0)] - public async Task InRole(int page, [Leftover] IRole role = null) + public async Task InRole(int page, [Leftover] IRole? role = null) { if (--page < 0) return; @@ -178,7 +183,7 @@ public partial class Utility : EllieModule [Cmd] [RequireContext(ContextType.Guild)] [Priority(1)] - public Task InRole([Leftover] IRole role = null) + public Task InRole([Leftover] IRole? role = null) => InRole(1, role); [Cmd] @@ -218,7 +223,7 @@ public partial class Utility : EllieModule [Cmd] [RequireContext(ContextType.Guild)] - public async Task UserId([Leftover] IGuildUser target = null) + public async Task UserId([Leftover] IGuildUser? target = null) { var usr = target ?? ctx.User; await Response() @@ -248,7 +253,7 @@ public partial class Utility : EllieModule [Cmd] [RequireContext(ContextType.Guild)] - public async Task Roles(IGuildUser target, int page = 1) + public async Task Roles(IGuildUser? target, int page = 1) { var guild = ctx.Guild; @@ -301,7 +306,7 @@ public partial class Utility : EllieModule [Cmd] [RequireContext(ContextType.Guild)] - public async Task ChannelTopic([Leftover] ITextChannel channel = null) + public async Task ChannelTopic([Leftover] ITextChannel? channel = null) { if (channel is null) channel = (ITextChannel)ctx.Channel; @@ -382,7 +387,7 @@ public partial class Utility : EllieModule [BotPerm(GuildPerm.ManageEmojisAndStickers)] [UserPerm(GuildPerm.ManageEmojisAndStickers)] [Priority(0)] - public async Task EmojiAdd(string name, string url = null) + public async Task EmojiAdd(string name, string? url = null) { name = name.Trim(':'); @@ -456,10 +461,10 @@ public partial class Utility : EllieModule [RequireContext(ContextType.Guild)] [BotPerm(GuildPerm.ManageEmojisAndStickers)] [UserPerm(GuildPerm.ManageEmojisAndStickers)] - public async Task StickerAdd(string name = null, string description = null, params string[] tags) + public async Task StickerAdd(string? name = null, string? description = null, params string[] tags) { string format; - Stream stream = null; + Stream? stream = null; try { @@ -696,6 +701,19 @@ public partial class Utility : EllieModule await Response().Confirm(strs.verbose_errors_disabled).SendAsync(); } + [Cmd] + public async Task Afk([Leftover] string text) + { + var succ = await _afkService.SetAfkAsync(ctx.User.Id, text); + + if (succ) + { + await Response() + .Confirm(strs.afk_set) + .SendAsync(); + } + } + [Cmd] [NoPublicBot] [OwnerOnly] @@ -759,4 +777,96 @@ public partial class Utility : EllieModule await Response().Error(ex.Message).SendAsync(); } } +} + +public sealed class AfkService : IEService, IReadyExecutor +{ + private readonly IBotCache _cache; + private readonly DiscordSocketClient _client; + private readonly MessageSenderService _mss; + + public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss) + { + _cache = cache; + _client = client; + _mss = mss; + } + + private static TypedKey GetKey(ulong userId) + => new($"afk:msg:{userId}"); + + public async Task SetAfkAsync(ulong userId, string text) + { + var added = await _cache.AddAsync(GetKey(userId), text, TimeSpan.FromHours(8), overwrite: true); + return added; + } + + public Task OnReadyAsync() + { + _client.MessageReceived += TryTriggerAfkMessage; + + return Task.CompletedTask; + } + + private Task TryTriggerAfkMessage(SocketMessage arg) + { + if (arg.Author.IsBot) + return Task.CompletedTask; + + if (arg.MentionedUsers.Count is 0 or > 2) + return Task.CompletedTask; + + if (arg is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc) + return Task.CompletedTask; + + + _ = Task.Run(async () => + { + var botUser = await tc.Guild.GetCurrentUserAsync(); + + var perms = botUser.GetPermissions(tc); + + if (!perms.SendMessages) + return; + + ulong mentionedUserId = 0; + foreach (var uid in uMsg.MentionedUserIds) + { + if (uid == arg.Author.Id) + continue; + + if (arg.Content.StartsWith($"<@{uid}>") || arg.Content.StartsWith($"<@!{uid}>")) + { + mentionedUserId = uid; + break; + } + } + + if (mentionedUserId == 0) + return; + + try + { + var result = await _cache.GetAsync(GetKey(mentionedUserId)); + if (result.TryPickT0(out var msg, out _)) + { + var st = SmartText.CreateFrom(msg); + + var toDelete = await _mss.Response(arg.Channel) + .Message(uMsg) + .Text(st) + .Sanitize(false) + .SendAsync(); + + toDelete.DeleteAfter(30); + } + } + catch (HttpException ex) + { + Log.Warning("Error in afk service: {Message}", ex.Message); + } + }); + + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/EllieBot/data/aliases.yml b/src/EllieBot/data/aliases.yml index 9140342..e8c0b4c 100644 --- a/src/EllieBot/data/aliases.yml +++ b/src/EllieBot/data/aliases.yml @@ -1410,4 +1410,6 @@ honeypot: coins: - coins - crypto - - cryptos \ No newline at end of file + - cryptos +afk: + - afk \ No newline at end of file diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index d137d44..42a69c5 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -4569,3 +4569,13 @@ coins: params: - page: desc: "Page number to show. Starts at 1." +afk: + desc: |- + Toggles AFK status for yourself with the specified message. + Anyone @ mentioning you in any server will receive the afk message. + This will only work if the other user's message starts with the mention. + ex: + - '' + params: + - msg: + desc: "The message to send when someone pings you." diff --git a/src/EllieBot/data/strings/responses/responses.en-US.json b/src/EllieBot/data/strings/responses/responses.en-US.json index 4c0893e..37231cf 100644 --- a/src/EllieBot/data/strings/responses/responses.en-US.json +++ b/src/EllieBot/data/strings/responses/responses.en-US.json @@ -1104,5 +1104,6 @@ "queue_search_results": "Type the number of the search result to queue up that track.", "overloads": "Overloads", "honeypot_on": "Honeypot enabled on this channel.", - "honeypot_off": "Honeypot disabled." + "honeypot_off": "Honeypot disabled.", + "afk_set": "AFK message set. Type a message in any channel to clear." } From afa00c8d4f83274b0cae4d175722d809c1c6f267 Mon Sep 17 00:00:00 2001 From: Toastie Date: Thu, 18 Jul 2024 21:54:45 +1200 Subject: [PATCH 04/27] Added 'afk ? command which sets an afk message which will trigger whenever someone pings a user. --- CHANGELOG.md | 7 + src/EllieBot/Modules/Utility/AfkService.cs | 148 ++++++++++++++++++ src/EllieBot/Modules/Utility/Utility.cs | 95 +---------- .../data/strings/commands/commands.en-US.yml | 3 +- 4 files changed, 158 insertions(+), 95 deletions(-) create mode 100644 src/EllieBot/Modules/Utility/AfkService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d2be481..c5722a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o +### Added + +- Added: Added a `'afk ?` command which sets an afk message which will trigger whenever someone pings you + - Message will when you type a message in any channel that the bot sees, or after 8 hours, whichever comes first + - The specified message will be prefixed with "The user is afk: " + - The afk message will disappear 30 seconds after being triggered + ## [5.1.4] - 15.07.2024 ### Added diff --git a/src/EllieBot/Modules/Utility/AfkService.cs b/src/EllieBot/Modules/Utility/AfkService.cs new file mode 100644 index 0000000..d41169b --- /dev/null +++ b/src/EllieBot/Modules/Utility/AfkService.cs @@ -0,0 +1,148 @@ +using EllieBot.Common.ModuleBehaviors; + +namespace EllieBot.Modules.Utility; + +public sealed class AfkService : IEService, IReadyExecutor +{ + private readonly IBotCache _cache; + private readonly DiscordSocketClient _client; + private readonly MessageSenderService _mss; + + private static readonly TimeSpan _maxAfkDuration = 8.Hours(); + public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss) + { + _cache = cache; + _client = client; + _mss = mss; + } + + private static TypedKey GetKey(ulong userId) + => new($"afk:msg:{userId}"); + + public async Task SetAfkAsync(ulong userId, string text) + { + var added = await _cache.AddAsync(GetKey(userId), text, _maxAfkDuration, overwrite: true); + + async Task StopAfk(SocketMessage socketMessage) + { + try + { + if (socketMessage.Author?.Id == userId) + { + await _cache.RemoveAsync(GetKey(userId)); + _client.MessageReceived -= StopAfk; + + // write the message saying afk status cleared + + if (socketMessage.Channel is ITextChannel tc) + { + _ = Task.Run(async () => + { + var msg = await _mss.Response(tc).Confirm("AFK message cleared!").SendAsync(); + + msg.DeleteAfter(5); + }); + } + + } + + } + catch (Exception ex) + { + Log.Warning("Unexpected error occurred while trying to stop afk: {Message}", ex.Message); + } + } + + _client.MessageReceived += StopAfk; + + + _ = Task.Run(async () => + { + await Task.Delay(_maxAfkDuration); + _client.MessageReceived -= StopAfk; + }); + + return added; + } + + public Task OnReadyAsync() + { + _client.MessageReceived += TryTriggerAfkMessage; + + return Task.CompletedTask; + } + + private Task TryTriggerAfkMessage(SocketMessage arg) + { + if (arg.Author.IsBot) + return Task.CompletedTask; + + if (arg is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc) + return Task.CompletedTask; + + if ((arg.MentionedUsers.Count is 0 or > 3) && uMsg.ReferencedMessage is null) + return Task.CompletedTask; + + _ = Task.Run(async () => + { + var botUser = await tc.Guild.GetCurrentUserAsync(); + + var perms = botUser.GetPermissions(tc); + + if (!perms.SendMessages) + return; + + ulong mentionedUserId = 0; + + if (arg.MentionedUsers.Count <= 3) + { + foreach (var uid in uMsg.MentionedUserIds) + { + if (uid == arg.Author.Id) + continue; + + if (arg.Content.StartsWith($"<@{uid}>") || arg.Content.StartsWith($"<@!{uid}>")) + { + mentionedUserId = uid; + break; + } + } + } + + if (mentionedUserId == 0) + { + if (uMsg.ReferencedMessage?.Author?.Id is not ulong repliedUserId) + { + return; + } + + mentionedUserId = repliedUserId; + } + + try + { + var result = await _cache.GetAsync(GetKey(mentionedUserId)); + if (result.TryPickT0(out var msg, out _)) + { + var st = SmartText.CreateFrom(msg); + + st = "The user is AFK: " + st; + + var toDelete = await _mss.Response(arg.Channel) + .Message(uMsg) + .Text(st) + .Sanitize(false) + .SendAsync(); + + toDelete.DeleteAfter(30); + } + } + catch (HttpException ex) + { + Log.Warning("Error in afk service: {Message}", ex.Message); + } + }); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Utility/Utility.cs b/src/EllieBot/Modules/Utility/Utility.cs index 6ec28b5..7afa03e 100644 --- a/src/EllieBot/Modules/Utility/Utility.cs +++ b/src/EllieBot/Modules/Utility/Utility.cs @@ -7,7 +7,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; -using EllieBot.Common.ModuleBehaviors; using EllieBot.Modules.Games.Hangman; using EllieBot.Modules.Searches.Common; @@ -702,7 +701,7 @@ public partial class Utility : EllieModule } [Cmd] - public async Task Afk([Leftover] string text) + public async Task Afk([Leftover] string text = "No reason specified.") { var succ = await _afkService.SetAfkAsync(ctx.User.Id, text); @@ -777,96 +776,4 @@ public partial class Utility : EllieModule await Response().Error(ex.Message).SendAsync(); } } -} - -public sealed class AfkService : IEService, IReadyExecutor -{ - private readonly IBotCache _cache; - private readonly DiscordSocketClient _client; - private readonly MessageSenderService _mss; - - public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss) - { - _cache = cache; - _client = client; - _mss = mss; - } - - private static TypedKey GetKey(ulong userId) - => new($"afk:msg:{userId}"); - - public async Task SetAfkAsync(ulong userId, string text) - { - var added = await _cache.AddAsync(GetKey(userId), text, TimeSpan.FromHours(8), overwrite: true); - return added; - } - - public Task OnReadyAsync() - { - _client.MessageReceived += TryTriggerAfkMessage; - - return Task.CompletedTask; - } - - private Task TryTriggerAfkMessage(SocketMessage arg) - { - if (arg.Author.IsBot) - return Task.CompletedTask; - - if (arg.MentionedUsers.Count is 0 or > 2) - return Task.CompletedTask; - - if (arg is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc) - return Task.CompletedTask; - - - _ = Task.Run(async () => - { - var botUser = await tc.Guild.GetCurrentUserAsync(); - - var perms = botUser.GetPermissions(tc); - - if (!perms.SendMessages) - return; - - ulong mentionedUserId = 0; - foreach (var uid in uMsg.MentionedUserIds) - { - if (uid == arg.Author.Id) - continue; - - if (arg.Content.StartsWith($"<@{uid}>") || arg.Content.StartsWith($"<@!{uid}>")) - { - mentionedUserId = uid; - break; - } - } - - if (mentionedUserId == 0) - return; - - try - { - var result = await _cache.GetAsync(GetKey(mentionedUserId)); - if (result.TryPickT0(out var msg, out _)) - { - var st = SmartText.CreateFrom(msg); - - var toDelete = await _mss.Response(arg.Channel) - .Message(uMsg) - .Text(st) - .Sanitize(false) - .SendAsync(); - - toDelete.DeleteAfter(30); - } - } - catch (HttpException ex) - { - Log.Warning("Error in afk service: {Message}", ex.Message); - } - }); - - return Task.CompletedTask; - } } \ No newline at end of file diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index 42a69c5..2ce37fb 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -4571,7 +4571,8 @@ coins: desc: "Page number to show. Starts at 1." afk: desc: |- - Toggles AFK status for yourself with the specified message. + Toggles AFK status for yourself with the specified message. + If you don't provide a message it default to a generic one. Anyone @ mentioning you in any server will receive the afk message. This will only work if the other user's message starts with the mention. ex: From 47c6f9ab03489b31457e05975e7eea50b9a7d734 Mon Sep 17 00:00:00 2001 From: Toastie Date: Fri, 19 Jul 2024 15:23:10 +1200 Subject: [PATCH 05/27] Possible fix for 'prune getting stuck after unsuccessful limit hit --- .../Modules/Administration/Prune/PruneService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/EllieBot/Modules/Administration/Prune/PruneService.cs b/src/EllieBot/Modules/Administration/Prune/PruneService.cs index 753b56b..cad3d01 100644 --- a/src/EllieBot/Modules/Administration/Prune/PruneService.cs +++ b/src/EllieBot/Modules/Administration/Prune/PruneService.cs @@ -26,21 +26,21 @@ public class PruneService : IEService ) { ArgumentNullException.ThrowIfNull(channel, nameof(channel)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); var originalAmount = amount; - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); using var cancelSource = new CancellationTokenSource(); if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource)) return PruneResult.AlreadyRunning; - - if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId)) - { - return PruneResult.FeatureLimit; - } try { + if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId)) + { + return PruneResult.FeatureLimit; + } + var now = DateTime.UtcNow; IMessage[] msgs; IMessage lastMessage = null; From 7d6a7f159bf0d96e548b9d135b02c78f9a51a04f Mon Sep 17 00:00:00 2001 From: Toastie Date: Fri, 19 Jul 2024 15:26:45 +1200 Subject: [PATCH 06/27] Show a message when 'prune fails due to already running error --- .../Administration/Prune/PruneCommands.cs | 39 +++++++++++-------- .../strings/responses/responses.en-US.json | 1 + 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs b/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs index 2317bf0..5ebafab 100644 --- a/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs +++ b/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs @@ -65,23 +65,6 @@ public partial class Administration await progressMsg.DeleteAsync(); } - private async Task SendResult(PruneResult result) - { - switch (result) - { - case PruneResult.Success: - break; - case PruneResult.AlreadyRunning: - break; - case PruneResult.FeatureLimit: - await Response().Pending(strs.feature_limit_reached_owner).SendAsync(); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(result), result, null); - } - } - // prune x [Cmd] [RequireContext(ContextType.Guild)] @@ -218,5 +201,27 @@ public partial class Administration await Response().Confirm(strs.prune_cancelled).SendAsync(); } + + + private async Task SendResult(PruneResult result) + { + switch (result) + { + case PruneResult.Success: + break; + case PruneResult.AlreadyRunning: + var msg = await Response().Pending(strs.prune_already_running).SendAsync(); + msg.DeleteAfter(5); + break; + case PruneResult.FeatureLimit: + var msg2 = await Response().Pending(strs.feature_limit_reached_owner).SendAsync(); + msg2.DeleteAfter(10); + break; + default: + Log.Error("Unhandled result received in prune: {Result}", result); + await Response().Error(strs.error_occured).SendAsync(); + break; + } + } } } \ No newline at end of file diff --git a/src/EllieBot/data/strings/responses/responses.en-US.json b/src/EllieBot/data/strings/responses/responses.en-US.json index 37231cf..c7d5b7a 100644 --- a/src/EllieBot/data/strings/responses/responses.en-US.json +++ b/src/EllieBot/data/strings/responses/responses.en-US.json @@ -38,6 +38,7 @@ "prune_cancelled": "Pruning was cancelled.", "prune_not_found": "No active prune was found on this server.", "prune_progress": "Pruning... {0}/{1} messages deleted.", + "prune_already_running": "A prune is already running on this server.", "timeoutdm": "You have been timed out in {0} server.\nReason: {1}", "timedout_user": "User Timed Out", "remove_roles_pl": "have had their roles removed", From 2e8e4daa25b81da7773ec788c0d9f41487c695a1 Mon Sep 17 00:00:00 2001 From: Toastie Date: Fri, 19 Jul 2024 15:29:31 +1200 Subject: [PATCH 07/27] 'cleverbot should be available on the public bot now --- .../Db/Extensions/GuildConfigExtensions.cs | 10 ----- .../Games/ChatterBot/ChatterBotCommands.cs | 19 ++------ .../Games/ChatterBot/ChatterbotService.cs | 44 ++++++++++++++++++- .../data/strings/commands/commands.en-US.yml | 6 +-- 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs b/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs index 7d16127..842fe4e 100644 --- a/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs +++ b/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs @@ -182,16 +182,6 @@ public static class GuildConfigExtensions .SelectMany(gc => gc.FollowedStreams) .ToList(); - public static void SetCleverbotEnabled(this DbSet configs, ulong id, bool cleverbotEnabled) - { - var conf = configs.FirstOrDefault(gc => gc.GuildId == id); - - if (conf is null) - return; - - conf.CleverbotEnabled = cleverbotEnabled; - } - public static XpSettings XpSettingsFor(this DbContext ctx, ulong guildId) { var gc = ctx.GuildConfigsForId(guildId, diff --git a/src/EllieBot/Modules/Games/ChatterBot/ChatterBotCommands.cs b/src/EllieBot/Modules/Games/ChatterBot/ChatterBotCommands.cs index 371c958..7f7f2cb 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/ChatterBotCommands.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/ChatterBotCommands.cs @@ -18,31 +18,18 @@ public partial class Games [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] - [NoPublicBot] public async Task CleverBot() { var channel = (ITextChannel)ctx.Channel; - if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _)) - { - await using (var uow = _db.GetDbContext()) - { - uow.Set().SetCleverbotEnabled(ctx.Guild.Id, false); - await uow.SaveChangesAsync(); - } + var newState = await _service.ToggleChatterBotAsync(ctx.Guild.Id); + if (!newState) + { await Response().Confirm(strs.chatbot_disabled).SendAsync(); return; } - _service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new(() => _service.CreateSession(), true)); - - await using (var uow = _db.GetDbContext()) - { - uow.Set().SetCleverbotEnabled(ctx.Guild.Id, true); - await uow.SaveChangesAsync(); - } - await Response().Confirm(strs.chatbot_enabled).SendAsync(); } } diff --git a/src/EllieBot/Modules/Games/ChatterBot/ChatterbotService.cs b/src/EllieBot/Modules/Games/ChatterBot/ChatterbotService.cs index 66decdc..e088319 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/ChatterbotService.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/ChatterbotService.cs @@ -1,5 +1,8 @@ #nullable disable +using LinqToDB; +using LinqToDB.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; +using EllieBot.Db.Models; using EllieBot.Modules.Games.Common; using EllieBot.Modules.Games.Common.ChatterBot; using EllieBot.Modules.Patronage; @@ -9,7 +12,7 @@ namespace EllieBot.Modules.Games.Services; public class ChatterBotService : IExecOnMessage { - public ConcurrentDictionary> ChatterBotGuilds { get; } + private ConcurrentDictionary> ChatterBotGuilds { get; } public int Priority => 1; @@ -20,6 +23,7 @@ public class ChatterBotService : IExecOnMessage private readonly IHttpClientFactory _httpFactory; private readonly GamesConfigService _gcs; private readonly IMessageSenderService _sender; + private readonly DbService _db; public readonly IPatronageService _ps; public ChatterBotService( @@ -30,12 +34,14 @@ public class ChatterBotService : IExecOnMessage IHttpClientFactory factory, IBotCredentials creds, GamesConfigService gcs, - IMessageSenderService sender) + IMessageSenderService sender, + DbService db) { _client = client; _perms = perms; _creds = creds; _sender = sender; + _db = db; _httpFactory = factory; _perms = perms; _gcs = gcs; @@ -196,4 +202,38 @@ public class ChatterBotService : IExecOnMessage return false; } + + public async Task ToggleChatterBotAsync(ulong guildId) + { + if (ChatterBotGuilds.TryRemove(guildId, out _)) + { + await using var uow = _db.GetDbContext(); + await uow.Set() + .ToLinqToDBTable() + .Where(x => x.GuildId == guildId) + .UpdateAsync((gc) => new GuildConfig() + { + CleverbotEnabled = false + }); + await uow.SaveChangesAsync(); + return false; + } + + ChatterBotGuilds.TryAdd(guildId, new(() => CreateSession(), true)); + + await using (var uow = _db.GetDbContext()) + { + await uow.Set() + .ToLinqToDBTable() + .Where(x => x.GuildId == guildId) + .UpdateAsync((gc) => new GuildConfig() + { + CleverbotEnabled = true + }); + + await uow.SaveChangesAsync(); + } + + return true; + } } \ No newline at end of file diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index 2ce37fb..7ad0299 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -1361,10 +1361,10 @@ flip: desc: "The number of times the coin is flipped." betflip: desc: |- - Bet to guess will the result be heads or tails. - Guessing awards you 1.95x the currency you've bet (rounded up). + Bet on the coin flip. + The result can be heads or tails. + Guessing correctly rewards you with 1.95x of the currency you've bet (rounded up). Multiplier can be changed by the bot owner. - ex: - 5 heads - 3 t From 8316b03c8c5ff73f5aec9900bb170dee03ca534b Mon Sep 17 00:00:00 2001 From: Toastie Date: Fri, 19 Jul 2024 15:31:09 +1200 Subject: [PATCH 08/27] Fixed some miscellaneous warnings in the build process And also updated games.yml and creds_example.yml --- CHANGELOG.md | 2 ++ .../Games/ChatterBot/_common/Choice.cs | 3 ++- .../Games/ChatterBot/_common/Message.cs | 3 ++- .../_common/OpenAiApi/OpenAiApiMessage.cs | 3 ++- .../_common/OpenAiApi/OpenAiApiRequest.cs | 3 ++- .../Modules/Searches/ReligiousCommands.cs | 3 ++- src/EllieBot/creds_example.yml | 2 +- src/EllieBot/data/games.yml | 22 +++++++++++++------ 8 files changed, 28 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5722a6..e842b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o +## Unreleased + ### Added - Added: Added a `'afk ?` command which sets an afk message which will trigger whenever someone pings you diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs index db71eee..0d3cbe8 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +#nullable disable +using System.Text.Json.Serialization; namespace EllieBot.Modules.Games.Common.ChatterBot; diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs index 04532f5..bd85a77 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +#nullable disable +using System.Text.Json.Serialization; namespace EllieBot.Modules.Games.Common.ChatterBot; diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs index 0fdaf71..efadfc0 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +#nullable disable +using System.Text.Json.Serialization; namespace EllieBot.Modules.Games.Common.ChatterBot; diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs index 1ea5d69..cf06ac1 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +#nullable disable +using System.Text.Json.Serialization; namespace EllieBot.Modules.Games.Common.ChatterBot; diff --git a/src/EllieBot/Modules/Searches/ReligiousCommands.cs b/src/EllieBot/Modules/Searches/ReligiousCommands.cs index a15bc74..97bd82a 100644 --- a/src/EllieBot/Modules/Searches/ReligiousCommands.cs +++ b/src/EllieBot/Modules/Searches/ReligiousCommands.cs @@ -1,4 +1,5 @@ -using EllieBot.Modules.Searches.Common; +#nullable disable +using EllieBot.Modules.Searches.Common; using System.Net.Http.Json; using System.Text.Json.Serialization; diff --git a/src/EllieBot/creds_example.yml b/src/EllieBot/creds_example.yml index c21a350..17e45e9 100644 --- a/src/EllieBot/creds_example.yml +++ b/src/EllieBot/creds_example.yml @@ -1,5 +1,5 @@ # DO NOT CHANGE -version: 7 +version: 9 # Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/ token: "" # List of Ids of the users who have bot owner permissions diff --git a/src/EllieBot/data/games.yml b/src/EllieBot/data/games.yml index b46f69e..b5fa9c1 100644 --- a/src/EllieBot/data/games.yml +++ b/src/EllieBot/data/games.yml @@ -57,18 +57,26 @@ raceAnimals: # Which chatbot API should bot use. # 'cleverbot' - bot will use Cleverbot API. # 'gpt' - bot will use GPT API -chatBot: Gpt +chatBot: OpenAi chatGpt: + # Url to any openai api compatible url. + # Make sure to modify the modelName appropriately + # DO NOT add /v1/chat/completions suffix to the url + apiUrl: https://api.openai.com # Which GPT Model should bot use. - # gpt35turbo - cheapest - # gpt4o - more expensive, higher quality + # gpt-3.5-turbo - cheapest + # gpt-4o - more expensive, higher quality # - modelName: Gpt35Turbo - # How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens) + # If you are using another openai compatible api, you may use any of the models supported by that api + modelName: gpt-3.5-turbo + # How should the chatbot behave, what's its personality? + # This will be sent as a system message. + # Usage of this counts towards the max tokens. personalityPrompt: You are a chat bot willing to have a conversation with anyone about anything. - # The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used) + # The maximum number of messages in a conversation that can be remembered. + # This will increase the number of tokens used. chatHistory: 5 - # The maximum number of tokens to use per GPT API call + # The maximum number of tokens to use per OpenAi API call maxTokens: 100 # The minimum number of tokens to use per GPT API call, such that chat history is removed to make room. minTokens: 30 From 13be30e82319acbd6b841185c4e14185c2dbf8a4 Mon Sep 17 00:00:00 2001 From: Toastie Date: Sat, 20 Jul 2024 16:44:29 +1200 Subject: [PATCH 09/27] You can once again disable cleverbot responses using fake 'cleverbot:response' module name in permission commands --- src/EllieBot/_common/CleverBotResponseStr.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EllieBot/_common/CleverBotResponseStr.cs b/src/EllieBot/_common/CleverBotResponseStr.cs index 6675a41..2f06a67 100644 --- a/src/EllieBot/_common/CleverBotResponseStr.cs +++ b/src/EllieBot/_common/CleverBotResponseStr.cs @@ -6,5 +6,5 @@ namespace EllieBot.Modules.Permissions; [StructLayout(LayoutKind.Sequential, Size = 1)] public readonly struct CleverBotResponseStr { - public const string CLEVERBOT_RESPONSE = "cleverbot:response"; + public const string CLEVERBOT_RESPONSE = "CLEVERBOT:RESPONSE"; } \ No newline at end of file From a51df649e288ee567b65df5fadddd4067dbf5fee Mon Sep 17 00:00:00 2001 From: Toastie Date: Sun, 21 Jul 2024 15:38:47 +1200 Subject: [PATCH 10/27] Updated some bet descriptions to include 'all' 'half' usage instructions --- .../data/strings/commands/commands.en-US.yml | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index 7ad0299..99db777 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -843,7 +843,12 @@ setservericon: - img: desc: "The URL of the image file to be displayed as the bot's banner." send: - desc: 'Sends a message to a channel or user. Channel or user can be ' + desc: |- + Sends a message to a channel or user. + You can write "channel" (literally word 'channel') first followed by the channel id or channel mention, or + You can write "user" (literally word 'user') first followed by the user id or user mention. + After either one of those, specify the message to be sent. + This command can only be used by the Bot Owner. ex: - channel 123123123132312 Stop spamming commands plz - user 1231231232132 I can see in the console what you're doing. @@ -1365,6 +1370,7 @@ betflip: The result can be heads or tails. Guessing correctly rewards you with 1.95x of the currency you've bet (rounded up). Multiplier can be changed by the bot owner. + You can type 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - 5 heads - 3 t @@ -1630,7 +1636,7 @@ rps: desc: |- Play a game of Rocket-Paperclip-Scissors with Ellie. You can bet on it. Multiplier is the same as on betflip. - You can type 'all', 'half' or 'X%' to bet that part of your current balance. + You can type 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - r 100 - scissors @@ -2755,7 +2761,7 @@ waifutransfer: newOwner: desc: "The user to whom ownership of the waifu is being transferred." waifugift: - desc: -| + desc: |- Gift an item to a waifu user. The waifu's value will be increased by the percentage of the gift's value. You can optionally prefix the gift with a multiplier to gift the item that many times. @@ -3765,7 +3771,11 @@ expredit: message: desc: "The text that will replace the original response in the expression's output." say: - desc: Bot will send the message you typed in the specified channel. If you omit the channel name, it will send the message in the current channel. Supports embeds. + desc: |- + Make the bot say something, or in other words, make the bot send the message. + You can optionally specify the channel where the bot will send the message. + If you omit the channel name, it will send the message in the current channel. + Supports embeds. ex: - hi - '#chat hi' @@ -4269,7 +4279,7 @@ bankbalance: Bot Owner can also check another user's bank balance. ex: - '' - - '@User' + - '@User' params: - {} banktake: @@ -4334,6 +4344,7 @@ betdraw: You can specify `r` or `b` for red or black, and `h` or `l` for high or low. You can specify only h/l or only r/b or both. Returns are high but **7 always loses**. + You can type 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - 50 r - 200 b l @@ -4579,4 +4590,4 @@ afk: - '' params: - msg: - desc: "The message to send when someone pings you." + desc: "The message to send when someone pings you." \ No newline at end of file From e4cc5f4e808bff7c3b0c7f8437d7a5307215f64f Mon Sep 17 00:00:00 2001 From: Toastie Date: Sun, 21 Jul 2024 15:43:34 +1200 Subject: [PATCH 11/27] Updated bot strings to clarify all half and x% usage --- .../data/strings/commands/commands.en-US.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index 99db777..f7a93b1 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -1370,7 +1370,7 @@ betflip: The result can be heads or tails. Guessing correctly rewards you with 1.95x of the currency you've bet (rounded up). Multiplier can be changed by the bot owner. - You can type 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. + You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - 5 heads - 3 t @@ -1519,7 +1519,7 @@ betroll: desc: |- Bets the specified amount of currency and rolls a dice. Rolling over 66 yields x2 of your currency, over 90 - x4 and 100 x10. - You can type 'all', 'half' or 'X%' to bet that part of your current balance. + You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - 5 params: @@ -1530,7 +1530,7 @@ luckyladder: Bets the specified amount of currency on the lucky ladder. You can stop on one of many different multipliers. The won amount is rounded down to the nearest whole number. - You can type 'all', 'half' or 'X%' to bet that part of your current balance. + You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - 10 params: @@ -1635,8 +1635,8 @@ choose: rps: desc: |- Play a game of Rocket-Paperclip-Scissors with Ellie. - You can bet on it. Multiplier is the same as on betflip. - You can type 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. + You can bet on it. Winning awards you 1.95x of the bet. + You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - r 100 - scissors @@ -2718,7 +2718,7 @@ betstats: slot: desc: |- Play Ellie slots by placing your bet. - You can type 'all', 'half' or 'X%' to bet that part of your current balance. + You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - 5 params: @@ -4344,7 +4344,7 @@ betdraw: You can specify `r` or `b` for red or black, and `h` or `l` for high or low. You can specify only h/l or only r/b or both. Returns are high but **7 always loses**. - You can type 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. + You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - 50 r - 200 b l From 9094b4e1444a8155981079e8bd4fc8c84ad960ce Mon Sep 17 00:00:00 2001 From: Toastie Date: Sat, 27 Jul 2024 19:13:39 +1200 Subject: [PATCH 12/27] Added admin-only .warndelete command --- .../UserPunish/UserPunishCommands.cs | 26 +++ .../UserPunish/UserPunishService.cs | 200 ++++++++++-------- src/EllieBot/data/aliases.yml | 4 + .../data/strings/commands/commands.en-US.yml | 58 ++--- .../strings/responses/responses.en-US.json | 2 + 5 files changed, 176 insertions(+), 114 deletions(-) diff --git a/src/EllieBot/Modules/Administration/UserPunish/UserPunishCommands.cs b/src/EllieBot/Modules/Administration/UserPunish/UserPunishCommands.cs index 27a3f12..3c43b1f 100644 --- a/src/EllieBot/Modules/Administration/UserPunish/UserPunishCommands.cs +++ b/src/EllieBot/Modules/Administration/UserPunish/UserPunishCommands.cs @@ -273,6 +273,31 @@ public partial class Administration .SendAsync(); } + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public Task WarnDelete(IGuildUser user, int index) + => WarnDelete(user.Id, index); + + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPermission.Administrator)] + public async Task WarnDelete(ulong userId, int index) + { + if (--index < 0) + return; + + var warn = await _service.WarnDelete(userId, index); + + if (warn is null) + { + await Response().Error(strs.warning_not_found).SendAsync(); + return; + } + + await Response().Confirm(strs.warning_deleted(Format.Bold(index.ToString()))).SendAsync(); + } + [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.BanMembers)] @@ -286,6 +311,7 @@ public partial class Administration { if (index < 0) return; + var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString()); var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString()); if (index == 0) diff --git a/src/EllieBot/Modules/Administration/UserPunish/UserPunishService.cs b/src/EllieBot/Modules/Administration/UserPunish/UserPunishService.cs index cdc9900..9ab0b73 100644 --- a/src/EllieBot/Modules/Administration/UserPunish/UserPunishService.cs +++ b/src/EllieBot/Modules/Administration/UserPunish/UserPunishService.cs @@ -89,9 +89,10 @@ public class UserPunishService : IEService, IReadyExecutor { ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; - previousCount = uow.Set().ForId(guildId, userId) - .Where(w => !w.Forgiven && w.UserId == userId) - .Sum(x => x.Weight); + previousCount = uow.Set() + .ForId(guildId, userId) + .Where(w => !w.Forgiven && w.UserId == userId) + .Sum(x => x.Weight); uow.Set().Add(warn); @@ -103,7 +104,7 @@ public class UserPunishService : IEService, IReadyExecutor var totalCount = previousCount + weight; var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount) - .MaxBy(x => x.Count); + .MaxBy(x => x.Count); if (p is not null) { @@ -244,33 +245,33 @@ public class UserPunishService : IEService, IReadyExecutor { await using var uow = _db.GetDbContext(); var cleared = await uow.Set() - .Where(x => uow.Set() - .Any(y => y.GuildId == x.GuildId - && y.WarnExpireHours > 0 - && y.WarnExpireAction == WarnExpireAction.Clear) - && x.Forgiven == false - && x.DateAdded - < DateTime.UtcNow.AddHours(-uow.Set() - .Where(y => x.GuildId == y.GuildId) - .Select(y => y.WarnExpireHours) - .First())) - .UpdateAsync(_ => new() - { - Forgiven = true, - ForgivenBy = "expiry" - }); + .Where(x => uow.Set() + .Any(y => y.GuildId == x.GuildId + && y.WarnExpireHours > 0 + && y.WarnExpireAction == WarnExpireAction.Clear) + && x.Forgiven == false + && x.DateAdded + < DateTime.UtcNow.AddHours(-uow.Set() + .Where(y => x.GuildId == y.GuildId) + .Select(y => y.WarnExpireHours) + .First())) + .UpdateAsync(_ => new() + { + Forgiven = true, + ForgivenBy = "expiry" + }); var deleted = await uow.Set() - .Where(x => uow.Set() - .Any(y => y.GuildId == x.GuildId - && y.WarnExpireHours > 0 - && y.WarnExpireAction == WarnExpireAction.Delete) - && x.DateAdded - < DateTime.UtcNow.AddHours(-uow.Set() - .Where(y => x.GuildId == y.GuildId) - .Select(y => y.WarnExpireHours) - .First())) - .DeleteAsync(); + .Where(x => uow.Set() + .Any(y => y.GuildId == x.GuildId + && y.WarnExpireHours > 0 + && y.WarnExpireAction == WarnExpireAction.Delete) + && x.DateAdded + < DateTime.UtcNow.AddHours(-uow.Set() + .Where(y => x.GuildId == y.GuildId) + .Select(y => y.WarnExpireHours) + .First())) + .DeleteAsync(); if (cleared > 0 || deleted > 0) { @@ -293,21 +294,21 @@ public class UserPunishService : IEService, IReadyExecutor if (config.WarnExpireAction == WarnExpireAction.Clear) { await uow.Set() - .Where(x => x.GuildId == guildId - && x.Forgiven == false - && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours)) - .UpdateAsync(_ => new() - { - Forgiven = true, - ForgivenBy = "expiry" - }); + .Where(x => x.GuildId == guildId + && x.Forgiven == false + && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours)) + .UpdateAsync(_ => new() + { + Forgiven = true, + ForgivenBy = "expiry" + }); } else if (config.WarnExpireAction == WarnExpireAction.Delete) { await uow.Set() - .Where(x => x.GuildId == guildId - && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours)) - .DeleteAsync(); + .Where(x => x.GuildId == guildId + && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours)) + .DeleteAsync(); } await uow.SaveChangesAsync(); @@ -425,8 +426,8 @@ public class UserPunishService : IEService, IReadyExecutor { using var uow = _db.GetDbContext(); return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments)) - .WarnPunishments.OrderBy(x => x.Count) - .ToArray(); + .WarnPunishments.OrderBy(x => x.Count) + .ToArray(); } public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill( @@ -436,20 +437,20 @@ public class UserPunishService : IEService, IReadyExecutor var gusers = guild.Users; //get user objects and reasons var bans = people.Split("\n") - .Select(x => - { - var split = x.Trim().Split(" "); + .Select(x => + { + var split = x.Trim().Split(" "); - var reason = string.Join(" ", split.Skip(1)); + var reason = string.Join(" ", split.Skip(1)); - if (ulong.TryParse(split[0], out var id)) - return (Original: split[0], Id: id, Reason: reason); + if (ulong.TryParse(split[0], out var id)) + return (Original: split[0], Id: id, Reason: reason); - return (Original: split[0], - gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id, - Reason: reason); - }) - .ToArray(); + return (Original: split[0], + gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id, + Reason: reason); + }) + .ToArray(); //if user is null, means that person couldn't be found var missing = bans.Count(x => !x.Id.HasValue); @@ -483,11 +484,12 @@ public class UserPunishService : IEService, IReadyExecutor } else if (template is null) { - uow.Set().Add(new() - { - GuildId = guildId, - Text = text - }); + uow.Set() + .Add(new() + { + GuildId = guildId, + Text = text + }); } else template.Text = text; @@ -499,31 +501,31 @@ public class UserPunishService : IEService, IReadyExecutor { await using var ctx = _db.GetDbContext(); await ctx.Set() - .ToLinqToDBTable() - .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - Text = null, - DateAdded = DateTime.UtcNow, - PruneDays = pruneDays - }, - old => new() - { - PruneDays = pruneDays - }, - () => new() - { - GuildId = guildId - }); + .ToLinqToDBTable() + .InsertOrUpdateAsync(() => new() + { + GuildId = guildId, + Text = null, + DateAdded = DateTime.UtcNow, + PruneDays = pruneDays + }, + old => new() + { + PruneDays = pruneDays + }, + () => new() + { + GuildId = guildId + }); } public async Task GetBanPruneAsync(ulong guildId) { await using var ctx = _db.GetDbContext(); return await ctx.Set() - .Where(x => x.GuildId == guildId) - .Select(x => x.PruneDays) - .FirstOrDefaultAsyncLinqToDB(); + .Where(x => x.GuildId == guildId) + .Select(x => x.PruneDays) + .FirstOrDefaultAsyncLinqToDB(); } public Task GetBanUserDmEmbed( @@ -554,18 +556,18 @@ public class UserPunishService : IEService, IReadyExecutor banReason = string.IsNullOrWhiteSpace(banReason) ? "-" : banReason; var repCtx = new ReplacementContext(client, guild) - .WithOverride("%ban.mod%", () => moderator.ToString()) - .WithOverride("%ban.mod.fullname%", () => moderator.ToString()) - .WithOverride("%ban.mod.name%", () => moderator.Username) - .WithOverride("%ban.mod.discrim%", () => moderator.Discriminator) - .WithOverride("%ban.user%", () => target.ToString()) - .WithOverride("%ban.user.fullname%", () => target.ToString()) - .WithOverride("%ban.user.name%", () => target.Username) - .WithOverride("%ban.user.discrim%", () => target.Discriminator) - .WithOverride("%reason%", () => banReason) - .WithOverride("%ban.reason%", () => banReason) - .WithOverride("%ban.duration%", - () => duration?.ToString(@"d\.hh\:mm") ?? "perma"); + .WithOverride("%ban.mod%", () => moderator.ToString()) + .WithOverride("%ban.mod.fullname%", () => moderator.ToString()) + .WithOverride("%ban.mod.name%", () => moderator.Username) + .WithOverride("%ban.mod.discrim%", () => moderator.Discriminator) + .WithOverride("%ban.user%", () => target.ToString()) + .WithOverride("%ban.user.fullname%", () => target.ToString()) + .WithOverride("%ban.user.name%", () => target.Username) + .WithOverride("%ban.user.discrim%", () => target.Discriminator) + .WithOverride("%reason%", () => banReason) + .WithOverride("%ban.reason%", () => banReason) + .WithOverride("%ban.duration%", + () => duration?.ToString(@"d\.hh\:mm") ?? "perma"); // if template isn't set, use the old message style @@ -594,4 +596,24 @@ public class UserPunishService : IEService, IReadyExecutor var output = SmartText.CreateFrom(template); return await _repSvc.ReplaceAsync(output, repCtx); } + + public async Task WarnDelete(ulong userId, int index) + { + await using var uow = _db.GetDbContext(); + + var warn = await uow.GetTable() + .Where(x => x.UserId == userId) + .OrderByDescending(x => x.DateAdded) + .Skip(index) + .FirstOrDefaultAsyncLinqToDB(); + + if (warn is not null) + { + await uow.GetTable() + .Where(x => x.Id == warn.Id) + .DeleteAsync(); + } + + return warn; + } } \ No newline at end of file diff --git a/src/EllieBot/data/aliases.yml b/src/EllieBot/data/aliases.yml index e8c0b4c..8e9ff14 100644 --- a/src/EllieBot/data/aliases.yml +++ b/src/EllieBot/data/aliases.yml @@ -947,6 +947,10 @@ warnexpire: warnclear: - warnclear - warnc +warndelete: + - warndelete + - warnrm + - warnd warnpunishlist: - warnpunishlist - warnpl diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index f7a93b1..f5d5a6d 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -576,7 +576,7 @@ deleterole: - Awesome Role params: - role: - desc: "The role being deleted, as identified by its unique identifier." + desc: "The role being deleted, as identified by its id." rolecolor: desc: Set a role's color using its hex value. Provide no color in order to see the hex value of the color of the specified role. The role you specify has to be lower in the role hierarchy than your highest role. ex: @@ -605,11 +605,11 @@ ban: - time: desc: "The duration of the temporary ban." userId: - desc: "The unique identifier of the user being banned." + desc: "The id of the user being banned." msg: desc: "The reason for the ban is provided in this message." - userId: - desc: "The unique identifier of the user being banned." + desc: "The id of the user being banned." msg: desc: "The reason for the ban is provided in this message." - user: @@ -627,7 +627,7 @@ softban: msg: desc: "The reason for the ban is described in this string." - userId: - desc: "The unique identifier for the user being banned and then unbanned." + desc: "The id of the user being banned and then unbanned." msg: desc: "The reason for the ban is described in this string." kick: @@ -1203,7 +1203,7 @@ userblacklist: - action: desc: "The type of operation to perform on the user, either adding or removing them from the blacklist." id: - desc: "The unique identifier of the user to be added, removed, or listed." + desc: "The id of the user to be added, removed, or listed." - action: desc: "The type of operation to perform on the user, either adding or removing them from the blacklist." usr: @@ -1223,7 +1223,7 @@ channelblacklist: - action: desc: "The type of operation to perform on the channel, either adding it to the blacklist or removing it from it." id: - desc: "The unique identifier of the channel being added, removed, or listed." + desc: "The id of the channel being added, removed, or listed." serverblacklist: desc: |- Either [add]s or [rem]oves a server, or servers specified by an ID from a blacklist. @@ -1239,7 +1239,7 @@ serverblacklist: - action: desc: "The type of operation to perform on the server(s). It can be either adding or removing them from the blacklist." id: - desc: "The unique identifier of the server being added, removed, or listed." + desc: "The id of the server being added, removed, or listed." - action: desc: "The type of operation to perform on the server(s). It can be either adding or removing them from the blacklist." guild: @@ -1296,7 +1296,7 @@ quoteshow: - 123 params: - quoteId: - desc: "The unique identifier for the quote being queried." + desc: "The id of the quote being queried." quotesearch: desc: 'Shows a random quote given a search query. Partially matches in several ways: 1) Only content of any quote, 2) only by author, 3) keyword and content, 3) or keyword and author' ex: @@ -1317,14 +1317,14 @@ quoteid: - 123456 params: - quoteId: - desc: "The unique identifier for the quote to be displayed." + desc: "The id of the quote to be displayed." quotedelete: desc: Deletes a quote with the specified ID. You have to either have the Manage Messages permission or be the creator of the quote to delete it. ex: - 123456 params: - quoteId: - desc: "The unique identifier for the quote being deleted." + desc: "The id of the quote being deleted." quotedeleteauthor: desc: Deletes all quotes by the specified author. If the author is not you, then ManageMessage server permission is required. ex: @@ -1822,7 +1822,7 @@ load: - 5 params: - id: - desc: "The unique identifier of the playlist to be loaded." + desc: "The id of the playlist to be loaded." playlists: desc: Lists all playlists. Paginated, 20 per page. ex: @@ -1836,7 +1836,7 @@ playlistshow: - 1 params: - id: - desc: "The unique identifier for the playlist to retrieve songs from." + desc: "The id of the playlist to retrieve songs from." page: desc: "The current page number for the pagination." deleteplaylist: @@ -2232,7 +2232,7 @@ currencytransaction: - 3yvd params: - id: - desc: "The unique identifier for the transaction being queried." + desc: "The id of the transaction being queried." listperms: desc: Lists whole permission chain with their indexes. You can specify an optional page number if there are a lot of permissions. ex: @@ -2721,6 +2721,7 @@ slot: You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. ex: - 5 + - 'all' params: - amount: desc: "The amount of currency to bet." @@ -3177,6 +3178,13 @@ warnclear: desc: "The ID of the user whose warnings are being cleared." index: desc: "The index of the warning to be cleared, or 0 to clear all warnings." +warndelete: + desc: Deletes a warning from a user by its index. + ex: + - 3 + params: + - index: + desc: "The index of the warning to be deleted." warnpunishlist: desc: Lists punishments for warnings. ex: @@ -3767,7 +3775,7 @@ expredit: - 123 I'm a magical girl params: - id: - desc: "The unique identifier for the expression being edited." + desc: "The id of the expression being edited." message: desc: "The text that will replace the original response in the expression's output." say: @@ -4080,7 +4088,7 @@ xpshopbuy: - type: desc: "The type of item to purchase, such as a skill or a cosmetic." key: - desc: "The unique identifier for the item being purchased." + desc: "The id of the item being purchased." xpshopuse: desc: Use a previously purchased item from the xp shop by specifying the type and the key of the item. ex: @@ -4090,7 +4098,7 @@ xpshopuse: - type: desc: "The type of item to be used, such as an experience point or a skill upgrade." key: - desc: "The unique identifier for the item in the XP shop that you want to use." + desc: "The id of the item in the XP shop that you want to use." bible: desc: Shows bible verse. You need to supply book name and chapter:verse ex: @@ -4118,13 +4126,13 @@ edit: - '#other-channel 771562360594628608 {{"description":"hello"}}' params: - messageId: - desc: "The unique identifier of the message being edited." + desc: "The id of the message being edited." text: desc: "The new text content of the edited message." - channel: desc: "The target channel where the edited message will be sent or updated in." messageId: - desc: "The unique identifier of the message being edited." + desc: "The id of the message being edited." text: desc: "The new text content of the edited message." delete: @@ -4135,13 +4143,13 @@ delete: - 771562360594628608 5m params: - messageId: - desc: "The unique identifier of a specific message within a channel, used to target the deletion operation." + desc: "The id of a specific message within a channel, used to target the deletion operation." time: desc: "The duration after which the message should be automatically deleted." - channel: desc: "The channel where the message is located or should be searched for." messageId: - desc: "The unique identifier of a specific message within a channel, used to target the deletion operation." + desc: "The id of a specific message within a channel, used to target the deletion operation." time: desc: "The duration after which the message should be automatically deleted." roleid: @@ -4294,7 +4302,7 @@ banktake: - amount: desc: "The total value of funds being withdrawn." userId: - desc: "The unique identifier for the user whose account is being accessed." + desc: "The id of the user whose account is being accessed." bankaward: desc: Award the specified amount of currency to a user's bank ex: @@ -4485,7 +4493,7 @@ todoedit: - abc This is an updated entry params: - todoId: - desc: "The unique identifier for the todo item being edited." + desc: "The id of the todo item being edited." newMessage: desc: "The text of a new task description or update to an existing one." todocomplete: @@ -4494,14 +4502,14 @@ todocomplete: - 4a params: - todoId: - desc: "The unique identifier for the todo item being marked as completed." + desc: "The id of the todo item being marked as completed." tododelete: desc: Deletes a todo with the specified ID. ex: - abc params: - todoId: - desc: "The unique identifier for the todo item being deleted." + desc: "The id of the todo item being deleted." todoclear: desc: Deletes all unarchived todos. ex: @@ -4535,7 +4543,7 @@ todoshow: - 4a params: - todoId: - desc: "The unique identifier for the todo item being displayed." + desc: "The id of the todo item being displayed." todoarchivedelete: desc: Deletes the archived todo list with the specified ID. ex: diff --git a/src/EllieBot/data/strings/responses/responses.en-US.json b/src/EllieBot/data/strings/responses/responses.en-US.json index c7d5b7a..523fb07 100644 --- a/src/EllieBot/data/strings/responses/responses.en-US.json +++ b/src/EllieBot/data/strings/responses/responses.en-US.json @@ -702,6 +702,7 @@ "warn_count": "{0} current, {1} total", "warnlog_for": "Warnlog for {0}", "warnpl_none": "No punishments set.", + "warning_not_found": "Warning not found.", "warn_expire_set_delete": "Warnings will be deleted after {0} days.", "warn_expire_set_clear": "Warnings will be cleared after {0} days.", "warn_expire_reset": "Warnings will no longer expire.", @@ -712,6 +713,7 @@ "warn_punish_rem": "Having {0} warnings will no longer trigger a punishment.", "warn_punish_set": "I will apply {0} punishment to users with {1} warnings.", "warn_punish_set_timed": "I will apply {0} punishment for {2} to users with {1} warnings.", + "warning_deleted": "Warning {0} has been deleted.", "time_new": "Time", "timezone": "Timezone", "timezone_db_api_key": "You need to activate your TimezoneDB API key. You can do so by clicking on the link you've received in the email with your API key.", From ddbf8fd3de524a7a0827168ed0b404247afa8886 Mon Sep 17 00:00:00 2001 From: Toastie Date: Sat, 27 Jul 2024 19:16:25 +1200 Subject: [PATCH 13/27] Updated imagesharp package --- src/EllieBot/EllieBot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EllieBot/EllieBot.csproj b/src/EllieBot/EllieBot.csproj index de5f076..dfb91a8 100644 --- a/src/EllieBot/EllieBot.csproj +++ b/src/EllieBot/EllieBot.csproj @@ -70,7 +70,7 @@ - + From 13fa7bd17b1911cb0bae54df888c975404016b89 Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 00:51:04 +1200 Subject: [PATCH 14/27] Added .keep command which will add the current guild to the list of keptguilds. This is needed for the future database purge. --- .../DangerousCommands/CleanupCommands.cs | 10 +++ .../DangerousCommands/CleanupService.cs | 62 ++++++++++++++----- .../_common/ICleanupService.cs | 1 + src/EllieBot/data/aliases.yml | 4 +- .../data/strings/commands/commands.en-US.yml | 9 ++- 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupCommands.cs b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupCommands.cs index 8fee8a3..e4e8e76 100644 --- a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupCommands.cs +++ b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupCommands.cs @@ -27,5 +27,15 @@ public partial class Administration .Confirm($"{result.GuildCount} guilds' data remain in the database.") .SendAsync(); } + + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task Keep() + { + var result = await _svc.KeepGuild(Context.Guild.Id); + + await Response().Text("This guild's bot data will be saved.").SendAsync(); + } } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs index b2dbf46..92feac4 100644 --- a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs +++ b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs @@ -1,6 +1,7 @@ using LinqToDB; using LinqToDB.Data; using LinqToDB.EntityFrameworkCore; +using LinqToDB.Mapping; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; @@ -66,67 +67,88 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService .Where(x => !tempTable.Select(x => x.GuildId) .Contains(x.GuildId)) .DeleteAsync(); - + // delete guild xp await ctx.GetTable() .Where(x => !tempTable.Select(x => x.GuildId) .Contains(x.GuildId)) .DeleteAsync(); - + // delete expressions await ctx.GetTable() - .Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId) - .Contains(x.GuildId.Value)) + .Where(x => x.GuildId != null + && !tempTable.Select(x => x.GuildId) + .Contains(x.GuildId.Value)) .DeleteAsync(); - + // delete quotes await ctx.GetTable() .Where(x => !tempTable.Select(x => x.GuildId) .Contains(x.GuildId)) .DeleteAsync(); - + // delete planted currencies await ctx.GetTable() .Where(x => !tempTable.Select(x => x.GuildId) .Contains(x.GuildId)) .DeleteAsync(); - + // delete image only channels await ctx.GetTable() .Where(x => !tempTable.Select(x => x.GuildId) .Contains(x.GuildId)) .DeleteAsync(); - + // delete reaction roles await ctx.GetTable() .Where(x => !tempTable.Select(x => x.GuildId) .Contains(x.GuildId)) .DeleteAsync(); - + // delete ignored users await ctx.GetTable() - .Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId) - .Contains(x.GuildId.Value)) + .Where(x => x.GuildId != null + && !tempTable.Select(x => x.GuildId) + .Contains(x.GuildId.Value)) .DeleteAsync(); - + // delete perm overrides await ctx.GetTable() - .Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId) - .Contains(x.GuildId.Value)) + .Where(x => x.GuildId != null + && !tempTable.Select(x => x.GuildId) + .Contains(x.GuildId.Value)) .DeleteAsync(); - + // delete repeaters await ctx.GetTable() .Where(x => !tempTable.Select(x => x.GuildId) .Contains(x.GuildId)) .DeleteAsync(); - + return new() { GuildCount = guildIds.Keys.Count, }; } - + + public async Task KeepGuild(ulong guildId) + { + await using var db = _db.GetDbContext(); + await using var ctx = db.CreateLinqToDBContext(); + + var table = ctx.CreateTable(tableOptions: TableOptions.CheckExistence); + + if (await table.AnyAsyncLinqToDB(x => x.GuildId == guildId)) + return false; + + await table.InsertAsync(() => new() + { + GuildId = guildId + }); + + return true; + } + private ValueTask OnKeepReport(KeepReport report) { guildIds[report.ShardId] = report.GuildIds; @@ -152,4 +174,10 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService return default; } +} + +public class KeptGuilds +{ + [PrimaryKey] + public ulong GuildId { get; set; } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/DangerousCommands/_common/ICleanupService.cs b/src/EllieBot/Modules/Administration/DangerousCommands/_common/ICleanupService.cs index a396082..5d4a720 100644 --- a/src/EllieBot/Modules/Administration/DangerousCommands/_common/ICleanupService.cs +++ b/src/EllieBot/Modules/Administration/DangerousCommands/_common/ICleanupService.cs @@ -3,4 +3,5 @@ public interface ICleanupService { Task DeleteMissingGuildDataAsync(); + Task KeepGuild(ulong guildId); } \ No newline at end of file diff --git a/src/EllieBot/data/aliases.yml b/src/EllieBot/data/aliases.yml index 8e9ff14..a6b5e16 100644 --- a/src/EllieBot/data/aliases.yml +++ b/src/EllieBot/data/aliases.yml @@ -1416,4 +1416,6 @@ coins: - crypto - cryptos afk: - - afk \ No newline at end of file + - afk +keep: + - keep \ No newline at end of file diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index f5d5a6d..2ba8bc7 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -4598,4 +4598,11 @@ afk: - '' params: - msg: - desc: "The message to send when someone pings you." \ No newline at end of file + desc: "The message to send when someone pings you." +keep: + desc: |- + The current serve, won't be deleted from Ellie's database during the purge. + ex: + - '' + params: + - {} \ No newline at end of file From 86b015115a43a03006c96479b7c4718daf62900e Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 18:26:18 +1200 Subject: [PATCH 15/27] Brough .wiki command to 2018 standards --- src/EllieBot/Modules/Searches/Searches.cs | 108 ++++++++++-------- .../Modules/Searches/SearchesService.cs | 73 ++++++++++++ 2 files changed, 135 insertions(+), 46 deletions(-) diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index b085ea6..6de2b01 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -136,11 +136,11 @@ public partial class Searches : EllieModule } var eb = _sender.CreateEmbed() - .WithOkColor() - .WithTitle(GetText(strs.time_new)) - .WithDescription(Format.Code(data.Time.ToString(Culture))) - .AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true) - .AddField(GetText(strs.timezone), data.TimeZoneName, true); + .WithOkColor() + .WithTitle(GetText(strs.time_new)) + .WithDescription(Format.Code(data.Time.ToString(Culture))) + .AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true) + .AddField(GetText(strs.timezone), data.TimeZoneName, true); await Response().Embed(eb).SendAsync(); } @@ -162,14 +162,16 @@ public partial class Searches : EllieModule await Response() .Embed(_sender.CreateEmbed() - .WithOkColor() - .WithTitle(movie.Title) - .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/") - .WithDescription(movie.Plot.TrimTo(1000)) - .AddField("Rating", movie.ImdbRating, true) - .AddField("Genre", movie.Genre, true) - .AddField("Year", movie.Year, true) - .WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) ? movie.Poster : null)) + .WithOkColor() + .WithTitle(movie.Title) + .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/") + .WithDescription(movie.Plot.TrimTo(1000)) + .AddField("Rating", movie.ImdbRating, true) + .AddField("Genre", movie.Genre, true) + .AddField("Year", movie.Year, true) + .WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) + ? movie.Poster + : null)) .SendAsync(); } @@ -244,9 +246,9 @@ public partial class Searches : EllieModule await Response() .Embed(_sender.CreateEmbed() - .WithOkColor() - .AddField(GetText(strs.original_url), $"<{query}>") - .AddField(GetText(strs.short_url), $"<{shortLink}>")) + .WithOkColor() + .AddField(GetText(strs.original_url), $"<{query}>") + .AddField(GetText(strs.short_url), $"<{shortLink}>")) .SendAsync(); } @@ -266,13 +268,13 @@ public partial class Searches : EllieModule } var embed = _sender.CreateEmbed() - .WithOkColor() - .WithTitle(card.Name) - .WithDescription(card.Description) - .WithImageUrl(card.ImageUrl) - .AddField(GetText(strs.store_url), card.StoreUrl, true) - .AddField(GetText(strs.cost), card.ManaCost, true) - .AddField(GetText(strs.types), card.Types, true); + .WithOkColor() + .WithTitle(card.Name) + .WithDescription(card.Description) + .WithImageUrl(card.ImageUrl) + .AddField(GetText(strs.store_url), card.StoreUrl, true) + .AddField(GetText(strs.cost), card.ManaCost, true) + .AddField(GetText(strs.types), card.Types, true); await Response().Embed(embed).SendAsync(); } @@ -331,10 +333,10 @@ public partial class Searches : EllieModule { var item = items[0]; return _sender.CreateEmbed() - .WithOkColor() - .WithUrl(item.Permalink) - .WithTitle(item.Word) - .WithDescription(item.Definition); + .WithOkColor() + .WithUrl(item.Permalink) + .WithTitle(item.Word) + .WithDescription(item.Definition); }) .SendAsync(); return; @@ -402,11 +404,11 @@ public partial class Searches : EllieModule { var model = items.First(); var embed = _sender.CreateEmbed() - .WithDescription(ctx.User.Mention) - .AddField(GetText(strs.word), model.Word, true) - .AddField(GetText(strs._class), model.WordType, true) - .AddField(GetText(strs.definition), model.Definition) - .WithOkColor(); + .WithDescription(ctx.User.Mention) + .AddField(GetText(strs.word), model.Word, true) + .AddField(GetText(strs._class), model.WordType, true) + .AddField(GetText(strs.definition), model.Definition) + .WithOkColor(); if (!string.IsNullOrWhiteSpace(model.Example)) embed.AddField(GetText(strs.example), model.Example); @@ -432,22 +434,36 @@ public partial class Searches : EllieModule } [Cmd] - public async Task Wiki([Leftover] string query = null) + public async Task Wiki([Leftover] string query) { query = query?.Trim(); if (!await ValidateQuery(query)) return; - using var http = _httpFactory.CreateClient(); - var result = await http.GetStringAsync( - "https://en.wikipedia.org//w/api.php?action=query&format=json&prop=info&redirects=1&formatversion=2&inprop=url&titles=" - + Uri.EscapeDataString(query)); - var data = JsonConvert.DeserializeObject(result); - if (data.Query.Pages[0].Missing || string.IsNullOrWhiteSpace(data.Query.Pages[0].FullUrl)) - await Response().Error(strs.wiki_page_not_found).SendAsync(); - else - await Response().Text(data.Query.Pages[0].FullUrl).SendAsync(); + var maybeRes = await _service.GetWikipediaPageAsync(query); + if (!maybeRes.TryPickT0(out var res, out var error)) + { + await HandleErrorAsync(error); + return; + } + + var data = res.Data; + await Response().Text(data.Url).SendAsync(); + } + + public Task HandleErrorAsync(ErrorType error) + { + var errorKey = error switch + { + ErrorType.ApiKeyMissing => strs.api_key_missing, + ErrorType.InvalidInput => strs.invalid_input, + ErrorType.NotFound => strs.not_found, + ErrorType.Unknown => strs.error_occured, + _ => strs.error_occured, + }; + + return Response().Error(errorKey).SendAsync(); } [Cmd] @@ -481,10 +497,10 @@ public partial class Searches : EllieModule await Response() .Embed( _sender.CreateEmbed() - .WithOkColor() - .AddField("Username", usr.ToString()) - .AddField("Avatar Url", avatarUrl) - .WithThumbnailUrl(avatarUrl.ToString())) + .WithOkColor() + .AddField("Username", usr.ToString()) + .AddField("Avatar Url", avatarUrl) + .WithThumbnailUrl(avatarUrl.ToString())) .SendAsync(); } diff --git a/src/EllieBot/Modules/Searches/SearchesService.cs b/src/EllieBot/Modules/Searches/SearchesService.cs index f5e3be4..742f71c 100644 --- a/src/EllieBot/Modules/Searches/SearchesService.cs +++ b/src/EllieBot/Modules/Searches/SearchesService.cs @@ -2,6 +2,7 @@ using EllieBot.Modules.Searches.Common; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OneOf.Types; using SixLabors.Fonts; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; @@ -454,4 +455,76 @@ public class SearchesService : IEService return gamesMap[key]; } + + public async Task> GetWikipediaPageAsync(string query) + { + query = query.Trim(); + if (string.IsNullOrEmpty(query)) + { + return ErrorType.InvalidInput; + } + + try + { + var result = await _c.GetOrAddAsync($"wikipedia_{query}", + async _ => + { + using var http = _httpFactory.CreateClient(); + http.DefaultRequestHeaders.Clear(); + + return await http.GetStringAsync( + "https://en.wikipedia.org/w/api.php?action=query" + + "&format=json" + + "&prop=info" + + "&redirects=1" + + "&formatversion=2" + + "&inprop=url" + + "&titles=" + + Uri.EscapeDataString(query)); + }, + TimeSpan.FromHours(1)) + .ConfigureAwait(false); + + var data = JsonConvert.DeserializeObject(result); + + if (data.Query.Pages is null || !data.Query.Pages.Any() || data.Query.Pages.First().Missing) + { + return ErrorType.NotFound; + } + + Log.Information("Sending wikipedia url for: {Query}", query); + + return new WikipediaReply + { + Data = new() + { + Url = data.Query.Pages[0].FullUrl, + } + }; + } + catch (Exception ex) + { + Log.Error(ex, "Error retrieving wikipedia data for: '{Query}'", query); + + return ErrorType.Unknown; + } + } +} + +public enum ErrorType +{ + InvalidInput, + NotFound, + Unknown, + ApiKeyMissing +} + +public class WikipediaReply +{ + public class Info + { + public required string Url { get; init; } + } + + public required Info Data { get; init; } } \ No newline at end of file From 20e5bbac8996926692efe22221c23668ec400f59 Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 18:32:34 +1200 Subject: [PATCH 16/27] Removed .rip command --- src/EllieBot/Modules/Searches/Searches.cs | 10 --- .../Modules/Searches/SearchesService.cs | 63 ++----------------- src/EllieBot/Services/Impl/ImageCache.cs | 6 -- src/EllieBot/_common/ImageUrls.cs | 10 +-- .../_common/Services/Impl/FontProvider.cs | 6 -- .../_common/Services/Impl/IImageCache.cs | 2 - src/EllieBot/data/aliases.yml | 2 - .../data/strings/commands/commands.en-US.yml | 7 --- 8 files changed, 7 insertions(+), 99 deletions(-) diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index 6de2b01..b9e189b 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -39,16 +39,6 @@ public partial class Searches : EllieModule _tzSvc = tzSvc; } - [Cmd] - public async Task Rip([Leftover] IGuildUser usr) - { - var av = usr.RealAvatarUrl(); - await using var picStream = await _service.GetRipPictureAsync(usr.Nickname ?? usr.Username, av); - await ctx.Channel.SendFileAsync(picStream, - "rip.png", - $"Rip {Format.Bold(usr.ToString())} \n\t- " + Format.Italics(ctx.User.ToString())); - } - [Cmd] public async Task Weather([Leftover] string query) { diff --git a/src/EllieBot/Modules/Searches/SearchesService.cs b/src/EllieBot/Modules/Searches/SearchesService.cs index 742f71c..4cdd5e8 100644 --- a/src/EllieBot/Modules/Searches/SearchesService.cs +++ b/src/EllieBot/Modules/Searches/SearchesService.cs @@ -73,56 +73,6 @@ public class SearchesService : IEService } } - public async Task GetRipPictureAsync(string text, Uri imgUrl) - => (await GetRipPictureFactory(text, imgUrl)).ToStream(); - - private void DrawAvatar(Image bg, Image avatarImage) - => bg.Mutate(x => x.Grayscale().DrawImage(avatarImage, new(83, 139), new GraphicsOptions())); - - public async Task GetRipPictureFactory(string text, Uri avatarUrl) - { - using var bg = Image.Load(await _imgs.GetRipBgAsync()); - var result = await _c.GetImageDataAsync(avatarUrl); - if (!result.TryPickT0(out var data, out _)) - { - using var http = _httpFactory.CreateClient(); - data = await http.GetByteArrayAsync(avatarUrl); - using (var avatarImg = Image.Load(data)) - { - avatarImg.Mutate(x => x.Resize(85, 85).ApplyRoundedCorners(42)); - await using var avStream = await avatarImg.ToStreamAsync(); - data = avStream.ToArray(); - DrawAvatar(bg, avatarImg); - } - - await _c.SetImageDataAsync(avatarUrl, data); - } - else - { - using var avatarImg = Image.Load(data); - DrawAvatar(bg, avatarImg); - } - - bg.Mutate(x => x.DrawText( - new TextOptions(_fonts.RipFont) - { - HorizontalAlignment = HorizontalAlignment.Center, - FallbackFontFamilies = _fonts.FallBackFonts, - Origin = new(bg.Width / 2, 225), - }, - text, - Color.Black)); - - //flowa - using (var flowers = Image.Load(await _imgs.GetRipOverlayAsync())) - { - bg.Mutate(x => x.DrawImage(flowers, new(0, 0), new GraphicsOptions())); - } - - await using var stream = bg.ToStream(); - return stream.ToArray(); - } - public async Task GetWeatherDataAsync(string query) { query = query.Trim().ToLowerInvariant(); @@ -396,12 +346,11 @@ public class SearchesService : IEService private async Task GetMovieDataFactory(string name) { using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync(string.Format("https://omdbapi.nadeko.bot/" - + "?t={0}" - + "&y=" - + "&plot=full" - + "&r=json", - name.Trim().Replace(' ', '+'))); + var res = await http.GetStringAsync("https://omdbapi.nadeko.bot/" + + $"?t={name.Trim().Replace(' ', '+')}" + + "&y=" + + "&plot=full" + + "&r=json"); var movie = JsonConvert.DeserializeObject(res); if (movie?.Title is null) return null; @@ -467,7 +416,7 @@ public class SearchesService : IEService try { var result = await _c.GetOrAddAsync($"wikipedia_{query}", - async _ => + async () => { using var http = _httpFactory.CreateClient(); http.DefaultRequestHeaders.Clear(); diff --git a/src/EllieBot/Services/Impl/ImageCache.cs b/src/EllieBot/Services/Impl/ImageCache.cs index 12ec051..4fc4e72 100644 --- a/src/EllieBot/Services/Impl/ImageCache.cs +++ b/src/EllieBot/Services/Impl/ImageCache.cs @@ -74,10 +74,4 @@ public sealed class ImageCache : IImageCache, IEService public Task GetSlotBgAsync() => GetImageDataAsync(_ic.Data.Slots.Bg); - - public Task GetRipBgAsync() - => GetImageDataAsync(_ic.Data.Rip.Bg); - - public Task GetRipOverlayAsync() - => GetImageDataAsync(_ic.Data.Rip.Overlay); } \ No newline at end of file diff --git a/src/EllieBot/_common/ImageUrls.cs b/src/EllieBot/_common/ImageUrls.cs index 449043f..5dc9557 100644 --- a/src/EllieBot/_common/ImageUrls.cs +++ b/src/EllieBot/_common/ImageUrls.cs @@ -8,7 +8,7 @@ namespace EllieBot.Common; public partial class ImageUrls : ICloneable { [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 3; + public int Version { get; set; } = 4; public CoinData Coins { get; set; } public Uri[] Currency { get; set; } @@ -16,16 +16,8 @@ public partial class ImageUrls : ICloneable public RategirlData Rategirl { get; set; } public XpData Xp { get; set; } - //new - public RipData Rip { get; set; } public SlotData Slots { get; set; } - public class RipData - { - public Uri Bg { get; set; } - public Uri Overlay { get; set; } - } - public class SlotData { public Uri[] Emojis { get; set; } diff --git a/src/EllieBot/_common/Services/Impl/FontProvider.cs b/src/EllieBot/_common/Services/Impl/FontProvider.cs index 543f23c..2bead90 100644 --- a/src/EllieBot/_common/Services/Impl/FontProvider.cs +++ b/src/EllieBot/_common/Services/Impl/FontProvider.cs @@ -12,11 +12,6 @@ public class FontProvider : IEService public FontFamily NotoSans { get; } //public FontFamily Emojis { get; } - /// - /// Font used for .rip command - /// - public Font RipFont { get; } - public List FallBackFonts { get; } private readonly FontCollection _fonts; @@ -54,7 +49,6 @@ public class FontProvider : IEService FallBackFonts.AddRange(_fonts.AddCollection(font)); } - RipFont = NotoSans.CreateFont(20, FontStyle.Bold); DottyFont = FallBackFonts.First(x => x.Name == "dotty"); } } \ No newline at end of file diff --git a/src/EllieBot/_common/Services/Impl/IImageCache.cs b/src/EllieBot/_common/Services/Impl/IImageCache.cs index 8c8ff32..890eeea 100644 --- a/src/EllieBot/_common/Services/Impl/IImageCache.cs +++ b/src/EllieBot/_common/Services/Impl/IImageCache.cs @@ -11,7 +11,5 @@ public interface IImageCache Task GetDiceAsync(int num); Task GetSlotEmojiAsync(int number); Task GetSlotBgAsync(); - Task GetRipBgAsync(); - Task GetRipOverlayAsync(); Task GetImageDataAsync(Uri url); } \ No newline at end of file diff --git a/src/EllieBot/data/aliases.yml b/src/EllieBot/data/aliases.yml index a6b5e16..d8c13df 100644 --- a/src/EllieBot/data/aliases.yml +++ b/src/EllieBot/data/aliases.yml @@ -1150,8 +1150,6 @@ discordpermoverridereset: - dpor rafflecur: - rafflecur -rip: - - rip timelyset: - timelyset timely: diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index 2ba8bc7..ce0f56f 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -3885,13 +3885,6 @@ rafflecur: desc: "The minimum or maximum amount of currency that can be used for betting." mixed: desc: "The parameter determines whether the raffle operates in \"fixed\" or \"proportional\" mode." -rip: - desc: Shows the inevitable fate of someone. - ex: - - '@Someone' - params: - - usr: - desc: "The user whose fate is being revealed." autodisconnect: desc: Toggles whether the bot should disconnect from the voice channel once it's done playing all of the songs and queue repeat option is set to `none`. ex: From 586f5ba4b01e5552b35616798b24ea1d8c442d0f Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 18:38:07 +1200 Subject: [PATCH 17/27] slightly updated .time --- src/EllieBot/Modules/Searches/Searches.cs | 22 ++----------------- .../Modules/Searches/SearchesService.cs | 12 +++++----- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index b9e189b..dfd1a8a 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -98,24 +98,7 @@ public partial class Searches : EllieModule var (data, err) = await _service.GetTimeDataAsync(query); if (err is not null) { - LocStr errorKey; - switch (err) - { - case TimeErrors.ApiKeyMissing: - errorKey = strs.api_key_missing; - break; - case TimeErrors.InvalidInput: - errorKey = strs.invalid_input; - break; - case TimeErrors.NotFound: - errorKey = strs.not_found; - break; - default: - errorKey = strs.error_occured; - break; - } - - await Response().Error(errorKey).SendAsync(); + await HandleErrorAsync(err.Value); return; } @@ -479,8 +462,7 @@ public partial class Searches : EllieModule [RequireContext(ContextType.Guild)] public async Task Avatar([Leftover] IGuildUser usr = null) { - if (usr is null) - usr = (IGuildUser)ctx.User; + usr ??= (IGuildUser)ctx.User; var avatarUrl = usr.RealAvatarUrl(2048); diff --git a/src/EllieBot/Modules/Searches/SearchesService.cs b/src/EllieBot/Modules/Searches/SearchesService.cs index 4cdd5e8..f7406cf 100644 --- a/src/EllieBot/Modules/Searches/SearchesService.cs +++ b/src/EllieBot/Modules/Searches/SearchesService.cs @@ -104,26 +104,26 @@ public class SearchesService : IEService } } - public Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataAsync(string arg) + public Task<((string Address, DateTime Time, string TimeZoneName), ErrorType?)> GetTimeDataAsync(string arg) => GetTimeDataFactory(arg); //return _cache.GetOrAddCachedDataAsync($"ellie_time_{arg}", // GetTimeDataFactory, // arg, // TimeSpan.FromMinutes(1)); - private async Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataFactory( + private async Task<((string Address, DateTime Time, string TimeZoneName), ErrorType?)> GetTimeDataFactory( string query) { query = query.Trim(); if (string.IsNullOrEmpty(query)) - return (default, TimeErrors.InvalidInput); + return (default, ErrorType.InvalidInput); var locIqKey = _creds.GetCreds().LocationIqApiKey; var tzDbKey = _creds.GetCreds().TimezoneDbApiKey; if (string.IsNullOrWhiteSpace(locIqKey) || string.IsNullOrWhiteSpace(tzDbKey)) - return (default, TimeErrors.ApiKeyMissing); + return (default, ErrorType.ApiKeyMissing); try { @@ -147,7 +147,7 @@ public class SearchesService : IEService if (responses is null || responses.Length == 0) { Log.Warning("Geocode lookup failed for: {Query}", query); - return (default, TimeErrors.NotFound); + return (default, ErrorType.NotFound); } var geoData = responses[0]; @@ -171,7 +171,7 @@ public class SearchesService : IEService catch (Exception ex) { Log.Error(ex, "Weather error: {Message}", ex.Message); - return (default, TimeErrors.NotFound); + return (default, ErrorType.NotFound); } } From 3a597a49ea5830a8094b2daf1a57b12e4a24b32c Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 18:49:57 +1200 Subject: [PATCH 18/27] v3 .catfact --- src/EllieBot/Modules/Searches/Searches.cs | 10 +++++--- .../Modules/Searches/SearchesService.cs | 25 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index dfd1a8a..21eb45d 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -399,10 +399,14 @@ public partial class Searches : EllieModule [Cmd] public async Task Catfact() { - using var http = _httpFactory.CreateClient(); - var response = await http.GetStringAsync("https://catfact.ninja/fact"); + var maybeFact = await _service.GetCatFactAsync(); + + if (!maybeFact.TryPickT0(out var fact, out var error)) + { + await HandleErrorAsync(error); + return; + } - var fact = JObject.Parse(response)["fact"].ToString(); await Response().Confirm("🐈" + GetText(strs.catfact), fact).SendAsync(); } diff --git a/src/EllieBot/Modules/Searches/SearchesService.cs b/src/EllieBot/Modules/Searches/SearchesService.cs index f7406cf..4ff9358 100644 --- a/src/EllieBot/Modules/Searches/SearchesService.cs +++ b/src/EllieBot/Modules/Searches/SearchesService.cs @@ -2,14 +2,8 @@ using EllieBot.Modules.Searches.Common; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OneOf.Types; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using Color = SixLabors.ImageSharp.Color; -using Image = SixLabors.ImageSharp.Image; +using System.Text.Json; +using OneOf; namespace EllieBot.Modules.Searches.Services; @@ -458,6 +452,21 @@ public class SearchesService : IEService return ErrorType.Unknown; } } + + public async Task> GetCatFactAsync() + { + using var http = _httpFactory.CreateClient(); + var response = await http.GetStringAsync("https://catfact.ninja/fact").ConfigureAwait(false); + + var doc = JsonDocument.Parse(response); + + if (!doc.RootElement.TryGetProperty("fact", out var factElement)) + { + return ErrorType.Unknown; + } + + return factElement.ToString(); + } } public enum ErrorType From e3a4c4bd4349e71fdbd7a6b8156327aa10e53ad6 Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 18:54:10 +1200 Subject: [PATCH 19/27] slight update to lmgtfy command's strings --- src/EllieBot/Modules/Searches/Searches.cs | 8 +++----- src/EllieBot/data/strings/commands/commands.en-US.yml | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index 21eb45d..b6a7140 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Caching.Memory; using EllieBot.Modules.Searches.Common; using EllieBot.Modules.Searches.Services; -using EllieBot.Modules.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SixLabors.ImageSharp; @@ -11,7 +10,6 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Net.Http.Json; using Color = SixLabors.ImageSharp.Color; namespace EllieBot.Modules.Searches; @@ -171,12 +169,12 @@ public partial class Searches : EllieModule } [Cmd] - public async Task Lmgtfy([Leftover] string ffs = null) + public async Task Lmgtfy([Leftover] string smh = null) { - if (!await ValidateQuery(ffs)) + if (!await ValidateQuery(smh)) return; - var shortenedUrl = await _google.ShortenUrl($"https://letmegooglethat.com/?q={Uri.EscapeDataString(ffs)}"); + var shortenedUrl = await _google.ShortenUrl($"https://letmegooglethat.com/?q={Uri.EscapeDataString(smh)}"); await Response().Confirm($"<{shortenedUrl}>").SendAsync(); } diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index ce0f56f..8e5ff9d 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -2078,11 +2078,11 @@ image: - query: desc: "The search term used to retrieve the desired image." lmgtfy: - desc: Google something for an idiot. + desc: Google something for a baka. ex: - - query + - How to eat a banana params: - - ffs: + - smh: desc: "The search query to be entered into the search engine." google: desc: Get a Google search link for some terms. From 1df8f092fe92cf8cc95d84783adaa5a1a93db709 Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 18:57:59 +1200 Subject: [PATCH 20/27] .keep will also automatically trigger for any new server the bot joins --- .../Administration/DangerousCommands/CleanupService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs index 92feac4..7bf7128 100644 --- a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs +++ b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs @@ -159,10 +159,17 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService { await _pubSub.Sub(_keepTriggerKey, OnKeepTrigger); + _client.JoinedGuild += ClientOnJoinedGuild; + if (_client.ShardId == 0) await _pubSub.Sub(_keepReportKey, OnKeepReport); } + private async Task ClientOnJoinedGuild(SocketGuild arg) + { + await KeepGuild(arg.Id); + } + private ValueTask OnKeepTrigger(bool arg) { _pubSub.Pub(_keepReportKey, From 4c436ccd8f90d23e3912de05dd9c74d0650904c0 Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 19:00:25 +1200 Subject: [PATCH 21/27] .reroadd error message improved --- src/EllieBot/data/strings/responses/responses.en-US.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EllieBot/data/strings/responses/responses.en-US.json b/src/EllieBot/data/strings/responses/responses.en-US.json index 523fb07..b55d8ad 100644 --- a/src/EllieBot/data/strings/responses/responses.en-US.json +++ b/src/EllieBot/data/strings/responses/responses.en-US.json @@ -1108,5 +1108,6 @@ "overloads": "Overloads", "honeypot_on": "Honeypot enabled on this channel.", "honeypot_off": "Honeypot disabled.", - "afk_set": "AFK message set. Type a message in any channel to clear." + "afk_set": "AFK message set. Type a message in any channel to clear.", + "rero_message_not_found": "The specified message wasn't found. Make sure you've specified the message from this channel." } From efa71c0233b4edee3509c57e73e6cf6e49057710 Mon Sep 17 00:00:00 2001 From: Toastie Date: Mon, 29 Jul 2024 19:00:25 +1200 Subject: [PATCH 22/27] .reroadd error message improved --- .../Modules/Administration/Role/ReactionRoleCommands.cs | 2 +- src/EllieBot/data/strings/responses/responses.en-US.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs b/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs index 3a728ed..cfcad28 100644 --- a/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs +++ b/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs @@ -33,7 +33,7 @@ public partial class Administration var msg = await ctx.Channel.GetMessageAsync(messageId); if (msg is null) { - await Response().Error(strs.not_found).SendAsync(); + await Response().Error(strs.rero_message_not_found).SendAsync(); return; } diff --git a/src/EllieBot/data/strings/responses/responses.en-US.json b/src/EllieBot/data/strings/responses/responses.en-US.json index 523fb07..b55d8ad 100644 --- a/src/EllieBot/data/strings/responses/responses.en-US.json +++ b/src/EllieBot/data/strings/responses/responses.en-US.json @@ -1108,5 +1108,6 @@ "overloads": "Overloads", "honeypot_on": "Honeypot enabled on this channel.", "honeypot_off": "Honeypot disabled.", - "afk_set": "AFK message set. Type a message in any channel to clear." + "afk_set": "AFK message set. Type a message in any channel to clear.", + "rero_message_not_found": "The specified message wasn't found. Make sure you've specified the message from this channel." } From 219c122f1cd2ac95fddacda96e201eb43f3e1884 Mon Sep 17 00:00:00 2001 From: Toastie Date: Wed, 31 Jul 2024 19:49:47 +1200 Subject: [PATCH 23/27] refactored .bible and .quran, moved to their own folder and created ReligiousApiService for their logic --- src/EllieBot/.editorconfig | 1 - .../Searches/Religious/Common/BibleVerse.cs | 13 +++ .../Searches/Religious/Common/BibleVerses.cs | 7 ++ .../Searches/Religious/Common/QuranAyah.cs | 20 ++++ .../Religious/Common/QuranResponse.cs | 15 +++ .../Searches/Religious/ReligiousApiService.cs | 63 +++++++++++ .../Searches/Religious/ReligiousCommands.cs | 60 ++++++++++ .../Modules/Searches/ReligiousCommands.cs | 103 ------------------ .../Modules/Searches/_common/BibleVerses.cs | 20 ---- 9 files changed, 178 insertions(+), 124 deletions(-) create mode 100644 src/EllieBot/Modules/Searches/Religious/Common/BibleVerse.cs create mode 100644 src/EllieBot/Modules/Searches/Religious/Common/BibleVerses.cs create mode 100644 src/EllieBot/Modules/Searches/Religious/Common/QuranAyah.cs create mode 100644 src/EllieBot/Modules/Searches/Religious/Common/QuranResponse.cs create mode 100644 src/EllieBot/Modules/Searches/Religious/ReligiousApiService.cs create mode 100644 src/EllieBot/Modules/Searches/Religious/ReligiousCommands.cs delete mode 100644 src/EllieBot/Modules/Searches/ReligiousCommands.cs delete mode 100644 src/EllieBot/Modules/Searches/_common/BibleVerses.cs diff --git a/src/EllieBot/.editorconfig b/src/EllieBot/.editorconfig index bc6d4d4..304861d 100644 --- a/src/EllieBot/.editorconfig +++ b/src/EllieBot/.editorconfig @@ -77,7 +77,6 @@ csharp_style_var_when_type_is_apparent = true:suggestion # Expression-bodied members csharp_style_expression_bodied_accessors = true:suggestion -csharp_style_expression_bodied_constructors = when_on_single_line:suggestion csharp_style_expression_bodied_indexers = true:suggestion csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_local_functions = true:suggestion diff --git a/src/EllieBot/Modules/Searches/Religious/Common/BibleVerse.cs b/src/EllieBot/Modules/Searches/Religious/Common/BibleVerse.cs new file mode 100644 index 0000000..8edcd1d --- /dev/null +++ b/src/EllieBot/Modules/Searches/Religious/Common/BibleVerse.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Searches; + +public class BibleVerse +{ + [JsonPropertyName("book_name")] + public required string BookName { get; set; } + + public required int Chapter { get; set; } + public required int Verse { get; set; } + public required string Text { get; set; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Religious/Common/BibleVerses.cs b/src/EllieBot/Modules/Searches/Religious/Common/BibleVerses.cs new file mode 100644 index 0000000..e31ee04 --- /dev/null +++ b/src/EllieBot/Modules/Searches/Religious/Common/BibleVerses.cs @@ -0,0 +1,7 @@ +namespace EllieBot.Modules.Searches; + +public class BibleVerses +{ + public string? Error { get; set; } + public BibleVerse[]? Verses { get; set; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Religious/Common/QuranAyah.cs b/src/EllieBot/Modules/Searches/Religious/Common/QuranAyah.cs new file mode 100644 index 0000000..b299242 --- /dev/null +++ b/src/EllieBot/Modules/Searches/Religious/Common/QuranAyah.cs @@ -0,0 +1,20 @@ +#nullable disable +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Searches; + +public sealed class QuranAyah +{ + [JsonPropertyName("number")] + public int Number { get; set; } + + [JsonPropertyName("audio")] + public string Audio { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } + +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Religious/Common/QuranResponse.cs b/src/EllieBot/Modules/Searches/Religious/Common/QuranResponse.cs new file mode 100644 index 0000000..86bee95 --- /dev/null +++ b/src/EllieBot/Modules/Searches/Religious/Common/QuranResponse.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Searches; + +public sealed class QuranResponse +{ + [JsonPropertyName("code")] + public required int Code { get; set; } + + [JsonPropertyName("status")] + public required string Status { get; set; } + + [JsonPropertyName("data")] + public required T[] Data { get; set; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Religious/ReligiousApiService.cs b/src/EllieBot/Modules/Searches/Religious/ReligiousApiService.cs new file mode 100644 index 0000000..07fbfa3 --- /dev/null +++ b/src/EllieBot/Modules/Searches/Religious/ReligiousApiService.cs @@ -0,0 +1,63 @@ +using EllieBot.Modules.Searches.Common; +using OneOf; +using OneOf.Types; +using System.Net; +using System.Net.Http.Json; + +namespace EllieBot.Modules.Searches; + +public sealed class ReligiousApiService : IEService +{ + private readonly IHttpClientFactory _httpFactory; + + public ReligiousApiService(IHttpClientFactory httpFactory) + { + _httpFactory = httpFactory; + } + + public async Task>> GetBibleVerseAsync(string book, string chapterAndVerse) + { + if (string.IsNullOrWhiteSpace(book) || string.IsNullOrWhiteSpace(chapterAndVerse)) + return new Error("Invalid input."); + + + book = Uri.EscapeDataString(book); + chapterAndVerse = Uri.EscapeDataString(chapterAndVerse); + + using var http = _httpFactory.CreateClient(); + try + { + var res = await http.GetFromJsonAsync($"https://bible-api.com/{book} {chapterAndVerse}"); + + if (res is null || res.Error is not null || res.Verses is null || res.Verses.Length == 0) + { + return new Error(res?.Error ?? "No verse found."); + } + + return res.Verses[0]; + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + return new Error("No verse found."); + } + } + + public async Task, Error>> GetQuranVerseAsync(string ayah) + { + if (string.IsNullOrWhiteSpace(ayah)) + return new Error(strs.invalid_input); + + ayah = Uri.EscapeDataString(ayah); + + using var http = _httpFactory.CreateClient(); + var res = await http.GetFromJsonAsync>( + $"https://api.alquran.cloud/v1/ayah/{ayah}/editions/en.asad,ar.alafasy"); + + if (res is null or not { Code: 200 }) + { + return new Error(strs.not_found); + } + + return res; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Religious/ReligiousCommands.cs b/src/EllieBot/Modules/Searches/Religious/ReligiousCommands.cs new file mode 100644 index 0000000..3bb7899 --- /dev/null +++ b/src/EllieBot/Modules/Searches/Religious/ReligiousCommands.cs @@ -0,0 +1,60 @@ +namespace EllieBot.Modules.Searches; + +public partial class Searches +{ + public partial class ReligiousCommands : EllieModule + { + private readonly IHttpClientFactory _httpFactory; + + public ReligiousCommands(IHttpClientFactory httpFactory) + => _httpFactory = httpFactory; + + [Cmd] + [RequireContext(ContextType.Guild)] + public async Task Bible(string book, string chapterAndVerse) + { + var res = await _service.GetBibleVerseAsync(book, chapterAndVerse); + + if (!res.TryPickT0(out var verse, out var error)) + { + await Response().Error(error.Value).SendAsync(); + return; + } + + await Response() + .Embed(_sender.CreateEmbed() + .WithOkColor() + .WithTitle($"{verse.BookName} {verse.Chapter}:{verse.Verse}") + .WithDescription(verse.Text)) + .SendAsync(); + } + + [Cmd] + [RequireContext(ContextType.Guild)] + public async Task Quran(string ayah) + { + var res = await _service.GetQuranVerseAsync(ayah); + + if (!res.TryPickT0(out var qr, out var error)) + { + await Response().Error(error.Value).SendAsync(); + return; + } + + var english = qr.Data[0]; + var arabic = qr.Data[1]; + + using var http = _httpFactory.CreateClient(); + await using var audio = await http.GetStreamAsync(arabic.Audio); + + await Response() + .Embed(_sender.CreateEmbed() + .WithOkColor() + .AddField("Arabic", arabic.Text) + .AddField("English", english.Text) + .WithFooter(arabic.Number.ToString())) + .File(audio, Uri.EscapeDataString(ayah) + ".mp3") + .SendAsync(); + } + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/ReligiousCommands.cs b/src/EllieBot/Modules/Searches/ReligiousCommands.cs deleted file mode 100644 index 97bd82a..0000000 --- a/src/EllieBot/Modules/Searches/ReligiousCommands.cs +++ /dev/null @@ -1,103 +0,0 @@ -#nullable disable -using EllieBot.Modules.Searches.Common; -using System.Net.Http.Json; -using System.Text.Json.Serialization; - -namespace EllieBot.Modules.Searches; - -public partial class Searches -{ - public partial class ReligiousCommands : EllieModule - { - private readonly IHttpClientFactory _httpFactory; - - public ReligiousCommands(IHttpClientFactory httpFactory) - { - _httpFactory = httpFactory; - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Bible(string book, string chapterAndVerse) - { - var obj = new BibleVerses(); - try - { - using var http = _httpFactory.CreateClient(); - obj = await http.GetFromJsonAsync($"https://bible-api.com/{book} {chapterAndVerse}"); - } - catch - { - } - - if (obj.Error is not null || obj.Verses is null || obj.Verses.Length == 0) - await Response().Error(obj.Error ?? "No verse found.").SendAsync(); - else - { - var v = obj.Verses[0]; - await Response() - .Embed(_sender.CreateEmbed() - .WithOkColor() - .WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}") - .WithDescription(v.Text)) - .SendAsync(); - } - } - - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task Quran(string ayah) - { - using var http = _httpFactory.CreateClient(); - - var obj = await http.GetFromJsonAsync>($"https://api.alquran.cloud/v1/ayah/{Uri.EscapeDataString(ayah)}/editions/en.asad,ar.alafasy"); - if(obj is null or not { Code: 200 }) - { - await Response().Error("No verse found.").SendAsync(); - return; - } - - var english = obj.Data[0]; - var arabic = obj.Data[1]; - - await using var audio = await http.GetStreamAsync(arabic.Audio); - - await Response() - .Embed(_sender.CreateEmbed() - .WithOkColor() - .AddField("Arabic", arabic.Text) - .AddField("English", english.Text) - .WithFooter(arabic.Number.ToString())) - .File(audio, Uri.EscapeDataString(ayah) + ".mp3") - .SendAsync(); - } - } -} - -public sealed class QuranResponse -{ - [JsonPropertyName("code")] - public int Code { get; set; } - - [JsonPropertyName("status")] - public string Status { get; set; } - - [JsonPropertyName("data")] - public T[] Data { get; set; } -} - -public sealed class QuranAyah -{ - [JsonPropertyName("number")] - public int Number { get; set; } - - [JsonPropertyName("audio")] - public string Audio { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } - -} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/_common/BibleVerses.cs b/src/EllieBot/Modules/Searches/_common/BibleVerses.cs deleted file mode 100644 index 30dd045..0000000 --- a/src/EllieBot/Modules/Searches/_common/BibleVerses.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace EllieBot.Modules.Searches.Common; - -public class BibleVerses -{ - public string Error { get; set; } - public BibleVerse[] Verses { get; set; } -} - -public class BibleVerse -{ - [JsonPropertyName("book_name")] - public string BookName { get; set; } - - public int Chapter { get; set; } - public int Verse { get; set; } - public string Text { get; set; } -} \ No newline at end of file From fe5c8622dd028764318e9ed786278913b33d3a5e Mon Sep 17 00:00:00 2001 From: Toastie Date: Thu, 1 Aug 2024 01:03:08 +1200 Subject: [PATCH 24/27] .wikia slightly changed and refactored --- src/EllieBot/Modules/Searches/Searches.cs | 33 +++------- .../Modules/Searches/SearchesService.cs | 61 ++++++++++++++----- .../Modules/Searches/_common/ErrorType.cs | 9 +++ .../Modules/Searches/_common/WikiaResponse.cs | 7 +++ .../Searches/_common/WikipediaReply.cs | 11 ++++ 5 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 src/EllieBot/Modules/Searches/_common/ErrorType.cs create mode 100644 src/EllieBot/Modules/Searches/_common/WikiaResponse.cs create mode 100644 src/EllieBot/Modules/Searches/_common/WikipediaReply.cs diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index b6a7140..9159ac6 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -487,35 +487,16 @@ public partial class Searches : EllieModule return; } - await ctx.Channel.TriggerTypingAsync(); - using var http = _httpFactory.CreateClient(); - http.DefaultRequestHeaders.Clear(); - try - { - var res = await http.GetStringAsync($"https://{Uri.EscapeDataString(target)}.fandom.com/api.php" - + "?action=query" - + "&format=json" - + "&list=search" - + $"&srsearch={Uri.EscapeDataString(query)}" - + "&srlimit=1"); - var items = JObject.Parse(res); - var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString(); + var maybeRes = await _service.GetWikiaPageAsync(target, query); - if (string.IsNullOrWhiteSpace(title)) - { - await Response().Error(strs.wikia_error).SendAsync(); - return; - } - - var url = Uri.EscapeDataString($"https://{target}.fandom.com/wiki/{title}"); - var response = $@"`{GetText(strs.title)}` {title.SanitizeMentions()} -`{GetText(strs.url)}:` {url}"; - await Response().Text(response).SendAsync(); - } - catch + if (!maybeRes.TryPickT0(out var res, out var error)) { - await Response().Error(strs.wikia_error).SendAsync(); + await HandleErrorAsync(error); + return; } + + var response = $"### {res.Title}\n{res.Url}"; + await Response().Text(response).Sanitize().SendAsync(); } [Cmd] diff --git a/src/EllieBot/Modules/Searches/SearchesService.cs b/src/EllieBot/Modules/Searches/SearchesService.cs index 4ff9358..c336b72 100644 --- a/src/EllieBot/Modules/Searches/SearchesService.cs +++ b/src/EllieBot/Modules/Searches/SearchesService.cs @@ -460,6 +460,7 @@ public class SearchesService : IEService var doc = JsonDocument.Parse(response); + if (!doc.RootElement.TryGetProperty("fact", out var factElement)) { return ErrorType.Unknown; @@ -467,22 +468,52 @@ public class SearchesService : IEService return factElement.ToString(); } -} -public enum ErrorType -{ - InvalidInput, - NotFound, - Unknown, - ApiKeyMissing -} - -public class WikipediaReply -{ - public class Info + public async Task> GetWikiaPageAsync(string target, string query) { - public required string Url { get; init; } - } + if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(query)) + { + return ErrorType.InvalidInput; + } - public required Info Data { get; init; } + query = Uri.EscapeDataString(query.Trim()); + target = Uri.EscapeDataString(target.Trim()); + + if (string.IsNullOrEmpty(query)) + { + return ErrorType.InvalidInput; + } + + using var http = _httpFactory.CreateClient(); + http.DefaultRequestHeaders.Clear(); + try + { + var res = await http.GetStringAsync($"https://{Uri.EscapeDataString(target)}.fandom.com/api.php" + + "?action=query" + + "&format=json" + + "&list=search" + + $"&srsearch={Uri.EscapeDataString(query)}" + + "&srlimit=1"); + var items = JObject.Parse(res); + var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString(); + + if (string.IsNullOrWhiteSpace(title)) + { + return ErrorType.NotFound; + } + + var url = $"https://{target}.fandom.com/wiki/{title}"; + + return new WikiaResponse() + { + Url = url, + Title = title, + }; + } + catch (Exception ex) + { + Log.Warning(ex, "Error getting wikia page: {Message}", ex.Message); + return ErrorType.Unknown; + } + } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/_common/ErrorType.cs b/src/EllieBot/Modules/Searches/_common/ErrorType.cs new file mode 100644 index 0000000..0daeea5 --- /dev/null +++ b/src/EllieBot/Modules/Searches/_common/ErrorType.cs @@ -0,0 +1,9 @@ +namespace EllieBot.Modules.Searches.Services; + +public enum ErrorType +{ + InvalidInput, + NotFound, + Unknown, + ApiKeyMissing +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/_common/WikiaResponse.cs b/src/EllieBot/Modules/Searches/_common/WikiaResponse.cs new file mode 100644 index 0000000..d0b3960 --- /dev/null +++ b/src/EllieBot/Modules/Searches/_common/WikiaResponse.cs @@ -0,0 +1,7 @@ +namespace EllieBot.Modules.Searches.Services; + +public sealed class WikiaResponse +{ + public required string Url { get; init; } + public required string Title { get; init; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/_common/WikipediaReply.cs b/src/EllieBot/Modules/Searches/_common/WikipediaReply.cs new file mode 100644 index 0000000..3969090 --- /dev/null +++ b/src/EllieBot/Modules/Searches/_common/WikipediaReply.cs @@ -0,0 +1,11 @@ +namespace EllieBot.Modules.Searches.Services; + +public class WikipediaReply +{ + public class Info + { + public required string Url { get; init; } + } + + public required Info Data { get; init; } +} \ No newline at end of file From 3e73dc8ba5b50db9d2e2bf783d8a4e19b50a46fc Mon Sep 17 00:00:00 2001 From: Toastie Date: Thu, 1 Aug 2024 13:41:30 +1200 Subject: [PATCH 25/27] .define slightly improved and refactored --- src/EllieBot/Modules/Searches/Searches.cs | 90 ++++++------------- .../Modules/Searches/SearchesService.cs | 64 ++++++++++++- .../Modules/Searches/_common/DefineData.cs | 10 +++ 3 files changed, 100 insertions(+), 64 deletions(-) create mode 100644 src/EllieBot/Modules/Searches/_common/DefineData.cs diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index 9159ac6..b3cc561 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -327,71 +327,35 @@ public partial class Searches : EllieModule if (!await ValidateQuery(word)) return; - using var http = _httpFactory.CreateClient(); - string res; - try + + var maybeItems = await _service.GetDefinitionsAsync(word); + + if (!maybeItems.TryPickT0(out var defs, out var error)) { - res = await _cache.GetOrCreateAsync($"define_{word}", - e => - { - e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12); - return http.GetStringAsync("https://api.pearson.com/v2/dictionaries/entries?headword=" - + WebUtility.UrlEncode(word)); - }); - - var responseModel = JsonConvert.DeserializeObject(res); - - var data = responseModel.Results - .Where(x => x.Senses is not null - && x.Senses.Count > 0 - && x.Senses[0].Definition is not null) - .Select(x => (Sense: x.Senses[0], x.PartOfSpeech)) - .ToList(); - - if (!data.Any()) - { - Log.Warning("Definition not found: {Word}", word); - await Response().Error(strs.define_unknown).SendAsync(); - } - - - var col = data.Select(x => ( - Definition: x.Sense.Definition is string - ? x.Sense.Definition.ToString() - : ((JArray)JToken.Parse(x.Sense.Definition.ToString())).First.ToString(), - Example: x.Sense.Examples is null || x.Sense.Examples.Count == 0 - ? string.Empty - : x.Sense.Examples[0].Text, Word: word, - WordType: string.IsNullOrWhiteSpace(x.PartOfSpeech) ? "-" : x.PartOfSpeech)) - .ToList(); - - Log.Information("Sending {Count} definition for: {Word}", col.Count, word); - - await Response() - .Paginated() - .Items(col) - .PageSize(1) - .Page((items, _) => - { - var model = items.First(); - var embed = _sender.CreateEmbed() - .WithDescription(ctx.User.Mention) - .AddField(GetText(strs.word), model.Word, true) - .AddField(GetText(strs._class), model.WordType, true) - .AddField(GetText(strs.definition), model.Definition) - .WithOkColor(); - - if (!string.IsNullOrWhiteSpace(model.Example)) - embed.AddField(GetText(strs.example), model.Example); - - return embed; - }) - .SendAsync(); - } - catch (Exception ex) - { - Log.Error(ex, "Error retrieving definition data for: {Word}", word); + await HandleErrorAsync(error); + return; } + + await Response() + .Paginated() + .Items(defs) + .PageSize(1) + .Page((items, _) => + { + var model = items.First(); + var embed = _sender.CreateEmbed() + .WithDescription(ctx.User.Mention) + .AddField(GetText(strs.word), model.Word, true) + .AddField(GetText(strs._class), model.WordType, true) + .AddField(GetText(strs.definition), model.Definition) + .WithOkColor(); + + if (!string.IsNullOrWhiteSpace(model.Example)) + embed.AddField(GetText(strs.example), model.Example); + + return embed; + }) + .SendAsync(); } [Cmd] diff --git a/src/EllieBot/Modules/Searches/SearchesService.cs b/src/EllieBot/Modules/Searches/SearchesService.cs index c336b72..3f01fc1 100644 --- a/src/EllieBot/Modules/Searches/SearchesService.cs +++ b/src/EllieBot/Modules/Searches/SearchesService.cs @@ -399,7 +399,7 @@ public class SearchesService : IEService return gamesMap[key]; } - public async Task> GetWikipediaPageAsync(string query) + public async Task> GetWikipediaPageAsync(string query) { query = query.Trim(); if (string.IsNullOrEmpty(query)) @@ -516,4 +516,66 @@ public class SearchesService : IEService return ErrorType.Unknown; } } + + private static TypedKey GetDefineKey(string query) + => new TypedKey($"define_{query}"); + + public async Task, ErrorType>> GetDefinitionsAsync(string query) + { + if (string.IsNullOrEmpty(query)) + { + return ErrorType.InvalidInput; + } + + query = Uri.EscapeDataString(query); + + using var http = _httpFactory.CreateClient(); + string res; + try + { + res = await _c.GetOrAddAsync(GetDefineKey(query), + async () => await http.GetStringAsync( + $"https://api.pearson.com/v2/dictionaries/entries?headword={query}"), + TimeSpan.FromHours(12)); + + var responseModel = JsonConvert.DeserializeObject(res); + + var data = responseModel.Results + .Where(x => x.Senses is not null + && x.Senses.Count > 0 + && x.Senses[0].Definition is not null) + .Select(x => (Sense: x.Senses[0], x.PartOfSpeech)) + .ToList(); + + if (!data.Any()) + { + Log.Warning("Definition not found: {Word}", query); + return ErrorType.NotFound; + } + + var items = new List(); + + foreach (var d in data) + { + items.Add(new DefineData + { + Definition = d.Sense.Definition is JArray { Count: > 0 } defs + ? defs[0].ToString() + : d.Sense.Definition.ToString(), + Example = d.Sense.Examples is null || d.Sense.Examples.Count == 0 + ? string.Empty + : d.Sense.Examples[0].Text, + WordType = string.IsNullOrWhiteSpace(d.PartOfSpeech) ? "-" : d.PartOfSpeech, + Word = query, + }); + } + + return items.OrderByDescending(x => !string.IsNullOrWhiteSpace(x.Example)).ToList(); + } + catch (Exception ex) + { + Log.Error(ex, "Error retrieving definition data for: {Word}", query); + return ErrorType.Unknown; + } + } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/_common/DefineData.cs b/src/EllieBot/Modules/Searches/_common/DefineData.cs new file mode 100644 index 0000000..2698d50 --- /dev/null +++ b/src/EllieBot/Modules/Searches/_common/DefineData.cs @@ -0,0 +1,10 @@ +#nullable disable +namespace EllieBot.Modules.Searches.Services; + +public sealed class DefineData +{ + public required string Definition { get; init; } + public required string Example { get; init; } + public required string WordType { get; init; } + public required string Word { get; init; } +} \ No newline at end of file From 67224663cd0dd6d82a3d4169f9192e03e17898eb Mon Sep 17 00:00:00 2001 From: Toastie Date: Thu, 1 Aug 2024 23:29:04 +1200 Subject: [PATCH 26/27] small cleanup --- src/EllieBot/Modules/Searches/Searches.cs | 12 ------------ src/EllieBot/_common/Interaction/EllieInteraction.cs | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index b3cc561..845e2ad 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -3,13 +3,11 @@ using Microsoft.Extensions.Caching.Memory; using EllieBot.Modules.Searches.Common; using EllieBot.Modules.Searches.Services; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.Diagnostics.CodeAnalysis; -using System.Net; using Color = SixLabors.ImageSharp.Color; namespace EllieBot.Modules.Searches; @@ -478,16 +476,6 @@ public partial class Searches : EllieModule return; } - //var embed = _sender.CreateEmbed() - // .WithOkColor() - // .WithDescription(gameData.ShortDescription) - // .WithTitle(gameData.Name) - // .WithUrl(gameData.Link) - // .WithImageUrl(gameData.HeaderImage) - // .AddField(GetText(strs.genres), gameData.TotalEpisodes.ToString(), true) - // .AddField(GetText(strs.price), gameData.IsFree ? GetText(strs.FREE) : game, true) - // .AddField(GetText(strs.links), gameData.GetGenresString(), true) - // .WithFooter(GetText(strs.recommendations(gameData.TotalRecommendations))); await Response().Text($"https://store.steampowered.com/app/{appId}").SendAsync(); } diff --git a/src/EllieBot/_common/Interaction/EllieInteraction.cs b/src/EllieBot/_common/Interaction/EllieInteraction.cs index 05b258f..3a24470 100644 --- a/src/EllieBot/_common/Interaction/EllieInteraction.cs +++ b/src/EllieBot/_common/Interaction/EllieInteraction.cs @@ -70,6 +70,7 @@ public abstract class EllieInteractionBase { if (_singleUse) _interactionCompletedSource.TrySetResult(true); + await ExecuteOnActionAsync(smc); if (!smc.HasResponded) From 0397ea09b0bcf2c81f6dff26df3a450406491c21 Mon Sep 17 00:00:00 2001 From: Toastie Date: Fri, 2 Aug 2024 00:06:28 +1200 Subject: [PATCH 27/27] using official version of discord.net upped version to 5.1.5 removed nuget.config as we are no longer using our fork of discord.net Fixed some build warnings --- EllieBot.sln | 5 +- NuGet.Config | 6 - src/Ellie.Marmalade/Ellie.Marmalade.csproj | 2 +- src/EllieBot/EllieBot.csproj | 4 +- .../Games/ChatterBot/_common/Choice.cs | 5 +- .../Games/ChatterBot/_common/Message.cs | 5 +- .../_common/OpenAiApi/OpenAiApiMessage.cs | 9 +- .../_common/OpenAiApi/OpenAiApiRequest.cs | 11 +- src/EllieBot/_common/DoAsUserMessage.cs | 131 +++++++++++++----- 9 files changed, 114 insertions(+), 64 deletions(-) delete mode 100644 NuGet.Config diff --git a/EllieBot.sln b/EllieBot.sln index 4a3f164..f34ad17 100644 --- a/EllieBot.sln +++ b/EllieBot.sln @@ -8,11 +8,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6C633450-E6C2-47ED-A7AA-7367232F703A}" ProjectSection(SolutionItems) = preProject CHANGELOG.md = CHANGELOG.md - LICENSE = LICENSE - README.md = README.md Dockerfile = Dockerfile - NuGet.Config = NuGet.Config + LICENSE = LICENSE migrate.ps1 = migrate.ps1 + README.md = README.md remove-migrations.ps1 = remove-migrations.ps1 TODO.md = TODO.md EndProjectSection diff --git a/NuGet.Config b/NuGet.Config deleted file mode 100644 index 7e64704..0000000 --- a/NuGet.Config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Ellie.Marmalade/Ellie.Marmalade.csproj b/src/Ellie.Marmalade/Ellie.Marmalade.csproj index db33025..8371f74 100644 --- a/src/Ellie.Marmalade/Ellie.Marmalade.csproj +++ b/src/Ellie.Marmalade/Ellie.Marmalade.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/EllieBot/EllieBot.csproj b/src/EllieBot/EllieBot.csproj index dfb91a8..1f0a2f0 100644 --- a/src/EllieBot/EllieBot.csproj +++ b/src/EllieBot/EllieBot.csproj @@ -4,7 +4,7 @@ enable true en - 5.1.4 + 5.1.5 $(MSBuildProjectDirectory) @@ -29,7 +29,7 @@ - + diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs index 0d3cbe8..c1290dd 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs @@ -1,10 +1,9 @@ -#nullable disable -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace EllieBot.Modules.Games.Common.ChatterBot; public class Choice { [JsonPropertyName("message")] - public Message Message { get; init; } + public required Message Message { get; init; } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs index bd85a77..df26315 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs @@ -1,10 +1,9 @@ -#nullable disable -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace EllieBot.Modules.Games.Common.ChatterBot; public class Message { [JsonPropertyName("content")] - public string Content { get; init; } + public required string Content { get; init; } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs index efadfc0..20ee90d 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs @@ -1,16 +1,15 @@ -#nullable disable -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace EllieBot.Modules.Games.Common.ChatterBot; public class OpenAiApiMessage { [JsonPropertyName("role")] - public string Role { get; init; } + public required string Role { get; init; } [JsonPropertyName("content")] - public string Content { get; init; } + public required string Content { get; init; } [JsonPropertyName("name")] - public string Name { get; init; } + public required string Name { get; init; } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs index cf06ac1..3ab7d68 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs @@ -1,19 +1,18 @@ -#nullable disable -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace EllieBot.Modules.Games.Common.ChatterBot; public class OpenAiApiRequest { [JsonPropertyName("model")] - public string Model { get; init; } + public required string Model { get; init; } [JsonPropertyName("messages")] - public List Messages { get; init; } + public required List Messages { get; init; } [JsonPropertyName("temperature")] - public int Temperature { get; init; } + public required int Temperature { get; init; } [JsonPropertyName("max_tokens")] - public int MaxTokens { get; init; } + public required int MaxTokens { get; init; } } \ No newline at end of file diff --git a/src/EllieBot/_common/DoAsUserMessage.cs b/src/EllieBot/_common/DoAsUserMessage.cs index f7f2980..68a9188 100644 --- a/src/EllieBot/_common/DoAsUserMessage.cs +++ b/src/EllieBot/_common/DoAsUserMessage.cs @@ -15,9 +15,11 @@ public sealed class DoAsUserMessage : IUserMessage _message = message; } - public ulong Id => _msg.Id; + public ulong Id + => _msg.Id; - public DateTimeOffset CreatedAt => _msg.CreatedAt; + public DateTimeOffset CreatedAt + => _msg.CreatedAt; public Task DeleteAsync(RequestOptions? options = null) { @@ -56,67 +58,104 @@ public sealed class DoAsUserMessage : IUserMessage ReactionType type = ReactionType.Normal) => _msg.GetReactionUsersAsync(emoji, limit, options, type); - public IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, + public IAsyncEnumerable> GetReactionUsersAsync( + IEmote emoji, + int limit, RequestOptions? options = null) { return _msg.GetReactionUsersAsync(emoji, limit, options); } - public MessageType Type => _msg.Type; + public MessageType Type + => _msg.Type; - public MessageSource Source => _msg.Source; + public MessageSource Source + => _msg.Source; - public bool IsTTS => _msg.IsTTS; + public bool IsTTS + => _msg.IsTTS; - public bool IsPinned => _msg.IsPinned; + public bool IsPinned + => _msg.IsPinned; - public bool IsSuppressed => _msg.IsSuppressed; + public bool IsSuppressed + => _msg.IsSuppressed; - public bool MentionedEveryone => _msg.MentionedEveryone; + public bool MentionedEveryone + => _msg.MentionedEveryone; - public string Content => _message; + public string Content + => _message; - public string CleanContent => _msg.CleanContent; + public string CleanContent + => _msg.CleanContent; - public DateTimeOffset Timestamp => _msg.Timestamp; + public DateTimeOffset Timestamp + => _msg.Timestamp; - public DateTimeOffset? EditedTimestamp => _msg.EditedTimestamp; + public DateTimeOffset? EditedTimestamp + => _msg.EditedTimestamp; - public IMessageChannel Channel => _msg.Channel; + public IMessageChannel Channel + => _msg.Channel; - public IUser Author => _user; + public IUser Author + => _user; - public IThreadChannel Thread => _msg.Thread; + public IThreadChannel Thread + => _msg.Thread; - public IReadOnlyCollection Attachments => _msg.Attachments; + public IReadOnlyCollection Attachments + => _msg.Attachments; - public IReadOnlyCollection Embeds => _msg.Embeds; + public IReadOnlyCollection Embeds + => _msg.Embeds; - public IReadOnlyCollection Tags => _msg.Tags; + public IReadOnlyCollection Tags + => _msg.Tags; - public IReadOnlyCollection MentionedChannelIds => _msg.MentionedChannelIds; + public IReadOnlyCollection MentionedChannelIds + => _msg.MentionedChannelIds; - public IReadOnlyCollection MentionedRoleIds => _msg.MentionedRoleIds; + public IReadOnlyCollection MentionedRoleIds + => _msg.MentionedRoleIds; - public IReadOnlyCollection MentionedUserIds => _msg.MentionedUserIds; + public IReadOnlyCollection MentionedUserIds + => _msg.MentionedUserIds; - public MessageActivity Activity => _msg.Activity; + public MessageActivity Activity + => _msg.Activity; - public MessageApplication Application => _msg.Application; + public MessageApplication Application + => _msg.Application; - public MessageReference Reference => _msg.Reference; + public MessageReference Reference + => _msg.Reference; - public IReadOnlyDictionary Reactions => _msg.Reactions; + public IReadOnlyDictionary Reactions + => _msg.Reactions; - public IReadOnlyCollection Components => _msg.Components; + public IReadOnlyCollection Components + => _msg.Components; - public IReadOnlyCollection Stickers => _msg.Stickers; + public IReadOnlyCollection Stickers + => _msg.Stickers; - public MessageFlags? Flags => _msg.Flags; + public MessageFlags? Flags + => _msg.Flags; [Obsolete("Obsolete in favor of InteractionMetadata")] - public IMessageInteraction Interaction => _msg.Interaction; - public MessageRoleSubscriptionData RoleSubscriptionData => _msg.RoleSubscriptionData; + public IMessageInteraction Interaction + => _msg.Interaction; + + public MessageRoleSubscriptionData RoleSubscriptionData + => _msg.RoleSubscriptionData; + + public PurchaseNotification PurchaseNotification + => _msg.PurchaseNotification; + + public MessageCallData? CallData + => _msg.CallData; public Task ModifyAsync(Action func, RequestOptions? options = null) { @@ -138,17 +177,39 @@ public sealed class DoAsUserMessage : IUserMessage return _msg.CrosspostAsync(options); } - public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + public string Resolve( + TagHandling userHandling = TagHandling.Name, + TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, - TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + TagHandling everyoneHandling = TagHandling.Ignore, + TagHandling emojiHandling = TagHandling.Name) { return _msg.Resolve(userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); } - public MessageResolvedData ResolvedData => _msg.ResolvedData; + public Task EndPollAsync(RequestOptions options) + => _msg.EndPollAsync(options); - public IUserMessage ReferencedMessage => _msg.ReferencedMessage; + public IAsyncEnumerable> GetPollAnswerVotersAsync( + uint answerId, + int? limit = null, + ulong? afterId = null, + RequestOptions? options = null) + => _msg.GetPollAnswerVotersAsync( + answerId, + limit, + afterId, + options); + + public MessageResolvedData ResolvedData + => _msg.ResolvedData; + + public IUserMessage ReferencedMessage + => _msg.ReferencedMessage; public IMessageInteractionMetadata InteractionMetadata => _msg.InteractionMetadata; + + public Poll? Poll + => _msg.Poll; } \ No newline at end of file