From 96eed1a820c353e8e80f2cfd1726f40c0034d32a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Dec 2023 08:46:40 +0300
Subject: [PATCH 01/11] Bump DiffPlex from 1.7.1 to 1.7.2 (#227)

Bumps [DiffPlex](https://github.com/mmanela/diffplex) from 1.7.1 to
1.7.2.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/mmanela/diffplex/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=DiffPlex&package-manager=nuget&previous-version=1.7.1&new-version=1.7.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Octobot.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Octobot.csproj b/Octobot.csproj
index 5258c89..e8f0dfa 100644
--- a/Octobot.csproj
+++ b/Octobot.csproj
@@ -20,7 +20,7 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="DiffPlex" Version="1.7.1" />
+        <PackageReference Include="DiffPlex" Version="1.7.2" />
         <PackageReference Include="Humanizer.Core.ru" Version="2.14.1" />
         <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
         <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />

From c0b43c6a184dcb118fccb2b5101afd3b63cc96a9 Mon Sep 17 00:00:00 2001
From: Macintxsh <95250141+mctaylors@users.noreply.github.com>
Date: Mon, 18 Dec 2023 12:26:08 +0300
Subject: [PATCH 02/11] /about: Show link to GitHub profile if Discord member
 wasn't found (#226)

In this PR, the behavior of the developer list display in /about has
been changed. Now, if a developer is not on the same server where the
/about command was executed, their username will have a link to their
GitHub profile.

---------

Signed-off-by: mctaylors <cantsendmails@mctaylors.ru>
---
 src/Commands/AboutCommandGroup.cs    |  4 +++-
 src/Extensions/MarkdownExtensions.cs | 13 +++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs
index 2c1e770..a12c070 100644
--- a/src/Commands/AboutCommandGroup.cs
+++ b/src/Commands/AboutCommandGroup.cs
@@ -88,7 +88,9 @@ public class AboutCommandGroup : CommandGroup
         {
             var guildMemberResult = await _guildApi.GetGuildMemberAsync(
                 guildId, dev.Id, ct);
-            var tag = guildMemberResult.IsSuccess ? $"<@{dev.Id}>" : $"@{dev.Username}";
+            var tag = guildMemberResult.IsSuccess
+                ? $"<@{dev.Id}>"
+                : MarkdownExtensions.Hyperlink($"@{dev.Username}", $"https://github.com/{dev.Username}");
 
             builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}");
         }
diff --git a/src/Extensions/MarkdownExtensions.cs b/src/Extensions/MarkdownExtensions.cs
index 7b7f780..95cd344 100644
--- a/src/Extensions/MarkdownExtensions.cs
+++ b/src/Extensions/MarkdownExtensions.cs
@@ -13,4 +13,17 @@ public static class MarkdownExtensions
     {
         return $"- {text}";
     }
+
+    /// <summary>
+    /// Formats a string to use Markdown Hyperlink formatting.
+    /// </summary>
+    /// <param name="text">The input text to format.</param>
+    /// <param name="url">The URL to use in formatting.</param>
+    /// <returns>
+    /// A markdown-formatted Hyperlink string.
+    /// </returns>
+    public static string Hyperlink(string text, string url)
+    {
+        return $"[{text}]({url})";
+    }
 }

From f79968fdc2dcc1317be87a394a5ef5311450f82a Mon Sep 17 00:00:00 2001
From: Macintxsh <95250141+mctaylors@users.noreply.github.com>
Date: Tue, 19 Dec 2023 13:32:11 +0300
Subject: [PATCH 03/11] /about: Use Markdown.Hyperlink instead of custom
 extension (#229)

Signed-off-by: mctaylors <cantsendmails@mctaylors.ru>
---
 src/Commands/AboutCommandGroup.cs    |  3 ++-
 src/Extensions/MarkdownExtensions.cs | 13 -------------
 2 files changed, 2 insertions(+), 14 deletions(-)

diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs
index a12c070..eec1f99 100644
--- a/src/Commands/AboutCommandGroup.cs
+++ b/src/Commands/AboutCommandGroup.cs
@@ -15,6 +15,7 @@ using Remora.Discord.Commands.Contexts;
 using Remora.Discord.Commands.Feedback.Messages;
 using Remora.Discord.Commands.Feedback.Services;
 using Remora.Discord.Extensions.Embeds;
+using Remora.Discord.Extensions.Formatting;
 using Remora.Rest.Core;
 using Remora.Results;
 
@@ -90,7 +91,7 @@ public class AboutCommandGroup : CommandGroup
                 guildId, dev.Id, ct);
             var tag = guildMemberResult.IsSuccess
                 ? $"<@{dev.Id}>"
-                : MarkdownExtensions.Hyperlink($"@{dev.Username}", $"https://github.com/{dev.Username}");
+                : Markdown.Hyperlink($"@{dev.Username}", $"https://github.com/{dev.Username}");
 
             builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}");
         }
diff --git a/src/Extensions/MarkdownExtensions.cs b/src/Extensions/MarkdownExtensions.cs
index 95cd344..7b7f780 100644
--- a/src/Extensions/MarkdownExtensions.cs
+++ b/src/Extensions/MarkdownExtensions.cs
@@ -13,17 +13,4 @@ public static class MarkdownExtensions
     {
         return $"- {text}";
     }
-
-    /// <summary>
-    /// Formats a string to use Markdown Hyperlink formatting.
-    /// </summary>
-    /// <param name="text">The input text to format.</param>
-    /// <param name="url">The URL to use in formatting.</param>
-    /// <returns>
-    /// A markdown-formatted Hyperlink string.
-    /// </returns>
-    public static string Hyperlink(string text, string url)
-    {
-        return $"[{text}]({url})";
-    }
 }

From 74e32dee9b245d1afe6d32b642b149fbfabf3e2c Mon Sep 17 00:00:00 2001
From: Octol1ttle <l1ttleofficial@outlook.com>
Date: Wed, 20 Dec 2023 21:23:37 +0500
Subject: [PATCH 04/11] Use collection expressions in more places (#238)

ReSharper inspections have been updated, causing new warnings to appear
in the codebase. This time, the "Use collection expressions" inspection
has been enabled for usecases where the collection is not empty. This PR
fixes the check failures caused by this inspection.
---
 src/Commands/AboutCommandGroup.cs          | 4 ++--
 src/Commands/SettingsCommandGroup.cs       | 4 ++--
 src/Commands/ToolsCommandGroup.cs          | 4 ++--
 src/Services/Update/MemberUpdateService.cs | 4 ++--
 src/Services/Update/SongUpdateService.cs   | 4 ++--
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs
index eec1f99..1c2656b 100644
--- a/src/Commands/AboutCommandGroup.cs
+++ b/src/Commands/AboutCommandGroup.cs
@@ -28,11 +28,11 @@ namespace Octobot.Commands;
 public class AboutCommandGroup : CommandGroup
 {
     private static readonly (string Username, Snowflake Id)[] Developers =
-    {
+    [
         ("Octol1ttle", new Snowflake(504343489664909322)),
         ("mctaylors", new Snowflake(326642240229474304)),
         ("neroduckale", new Snowflake(474943797063843851))
-    };
+    ];
 
     private readonly ICommandContext _context;
     private readonly IFeedbackService _feedback;
diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs
index a8891bd..15fd514 100644
--- a/src/Commands/SettingsCommandGroup.cs
+++ b/src/Commands/SettingsCommandGroup.cs
@@ -36,7 +36,7 @@ public class SettingsCommandGroup : CommandGroup
     ///     that the orders match.
     /// </remarks>
     private static readonly IOption[] AllOptions =
-    {
+    [
         GuildSettings.Language,
         GuildSettings.WelcomeMessage,
         GuildSettings.ReceiveStartupMessages,
@@ -51,7 +51,7 @@ public class SettingsCommandGroup : CommandGroup
         GuildSettings.MuteRole,
         GuildSettings.EventNotificationRole,
         GuildSettings.EventEarlyNotificationOffset
-    };
+    ];
 
     private readonly ICommandContext _context;
     private readonly IFeedbackService _feedback;
diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs
index 78058cb..f04ddf6 100644
--- a/src/Commands/ToolsCommandGroup.cs
+++ b/src/Commands/ToolsCommandGroup.cs
@@ -393,7 +393,7 @@ public class ToolsCommandGroup : CommandGroup
     }
 
     private static readonly TimestampStyle[] AllStyles =
-    {
+    [
         TimestampStyle.ShortDate,
         TimestampStyle.LongDate,
         TimestampStyle.ShortTime,
@@ -401,7 +401,7 @@ public class ToolsCommandGroup : CommandGroup
         TimestampStyle.ShortDateTime,
         TimestampStyle.LongDateTime,
         TimestampStyle.RelativeTime
-    };
+    ];
 
     /// <summary>
     ///     A slash command that shows the current timestamp with an optional offset in all styles supported by Discord.
diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs
index e7860ae..8937833 100644
--- a/src/Services/Update/MemberUpdateService.cs
+++ b/src/Services/Update/MemberUpdateService.cs
@@ -16,7 +16,7 @@ namespace Octobot.Services.Update;
 public sealed partial class MemberUpdateService : BackgroundService
 {
     private static readonly string[] GenericNicknames =
-    {
+    [
         "Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary",
         "Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck",
         "Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane",
@@ -24,7 +24,7 @@ public sealed partial class MemberUpdateService : BackgroundService
         "Nautilus", "Ostrich", "Octopus", "Pelican", "Puffin", "Pyramid", "Rattle", "Robin", "Rose",
         "Salmon", "Seal", "Shark", "Sheep", "Snake", "Sonar", "Stump", "Sparrow", "Toaster", "Toucan",
         "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag"
-    };
+    ];
 
     private readonly IDiscordRestChannelAPI _channelApi;
     private readonly IDiscordRestGuildAPI _guildApi;
diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs
index 391c416..53cc59b 100644
--- a/src/Services/Update/SongUpdateService.cs
+++ b/src/Services/Update/SongUpdateService.cs
@@ -9,7 +9,7 @@ namespace Octobot.Services.Update;
 public sealed class SongUpdateService : BackgroundService
 {
     private static readonly (string Author, string Name, TimeSpan Duration)[] SongList =
-    {
+    [
         ("Yoko & the Gold Bazookas", "Rockagilly Blues", new TimeSpan(0, 2, 52)),
         ("Deep Cut", "Big Betrayal", new TimeSpan(0, 5, 55)),
         ("Squid Sisters", "Tomorrow's Nostalgia Today", new TimeSpan(0, 3, 7)),
@@ -30,7 +30,7 @@ public sealed class SongUpdateService : BackgroundService
         ("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)),
         ("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)),
         ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5))
-    };
+    ];
 
     private readonly List<Activity> _activityList = [new Activity("with Remora.Discord", ActivityType.Game)];
 

From 6688481093ab217a0e8b036dc00122b82acc71e0 Mon Sep 17 00:00:00 2001
From: Macintxsh <95250141+mctaylors@users.noreply.github.com>
Date: Wed, 20 Dec 2023 19:25:13 +0300
Subject: [PATCH 05/11] /about: Show Discord bot username instead of hardcoded
 one (#230)

In this PR, I made it so that in the Author field instead of the
hardcoded name was the name of the Discord bot. This was done to match
the icon next to it in the same field.

Replaces #224

---------

Signed-off-by: mctaylors <cantsendmails@mctaylors.ru>
---
 locale/Messages.resx              | 2 +-
 locale/Messages.ru.resx           | 2 +-
 locale/Messages.tt-ru.resx        | 2 +-
 src/Commands/AboutCommandGroup.cs | 3 ++-
 4 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/locale/Messages.resx b/locale/Messages.resx
index 743dd93..60e6e07 100644
--- a/locale/Messages.resx
+++ b/locale/Messages.resx
@@ -400,7 +400,7 @@
     <value>Octobot's source code</value>
   </data>
   <data name="AboutBot" xml:space="preserve">
-    <value>About Octobot</value>
+      <value>About {0}</value>
   </data>
   <data name="AboutDeveloper@mctaylors" xml:space="preserve">
     <value>developer &amp; designer, Octobot's Wiki creator</value>
diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx
index 67a1d29..4b9492c 100644
--- a/locale/Messages.ru.resx
+++ b/locale/Messages.ru.resx
@@ -400,7 +400,7 @@
     <value>Исходный код Octobot</value>
   </data>
   <data name="AboutBot" xml:space="preserve">
-    <value>Об Octobot</value>
+      <value>О боте {0}</value>
   </data>
   <data name="AboutDeveloper@neroduckale" xml:space="preserve">
     <value>разработчик</value>
diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx
index 4050d43..de1f39f 100644
--- a/locale/Messages.tt-ru.resx
+++ b/locale/Messages.tt-ru.resx
@@ -400,7 +400,7 @@
     <value>репа Octobot (тык)</value>
   </data>
   <data name="AboutBot" xml:space="preserve">
-    <value>немного об Octobot</value>
+      <value>немного об {0}</value>
   </data>
   <data name="AboutDeveloper@mctaylors" xml:space="preserve">
     <value>скучный девелопер + дизайнер создавший Octobot's Wiki</value>
diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs
index 1c2656b..4c396d9 100644
--- a/src/Commands/AboutCommandGroup.cs
+++ b/src/Commands/AboutCommandGroup.cs
@@ -96,7 +96,8 @@ public class AboutCommandGroup : CommandGroup
             builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}");
         }
 
-        var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, bot)
+        var embed = new EmbedBuilder()
+            .WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot)
             .WithDescription(builder.ToString())
             .WithColour(ColorsList.Cyan)
             .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png")

From bd4c5b26da95d7a1fd3e00ce5ce0e90757018f8c Mon Sep 17 00:00:00 2001
From: Octol1ttle <l1ttleofficial@outlook.com>
Date: Wed, 20 Dec 2023 21:33:52 +0500
Subject: [PATCH 06/11] Remove "extends IHostedService" from classes where it's
 not required (#236)

Originally, these classes were services because I thought that all
DI-resolvable classes need to be services. However, this is not true, so
we can make these classes (notably Utility and GuildDataService) not
extend anything. `UtilityService` was renamed to `Utility` for
simplicity

---------

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
---
 src/Commands/BanCommandGroup.cs                   |  4 ++--
 src/Commands/ClearCommandGroup.cs                 |  4 ++--
 src/Commands/KickCommandGroup.cs                  |  4 ++--
 src/Commands/MuteCommandGroup.cs                  |  4 ++--
 src/Commands/SettingsCommandGroup.cs              |  4 ++--
 src/Octobot.cs                                    |  2 +-
 src/Responders/GuildLoadedResponder.cs            |  4 ++--
 src/Services/GuildDataService.cs                  | 12 +-----------
 src/Services/Update/MemberUpdateService.cs        |  4 ++--
 .../Update/ScheduledEventUpdateService.cs         |  4 ++--
 src/Services/{UtilityService.cs => Utility.cs}    | 15 ++-------------
 11 files changed, 20 insertions(+), 41 deletions(-)
 rename src/Services/{UtilityService.cs => Utility.cs} (97%)

diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs
index f0da978..bbcf459 100644
--- a/src/Commands/BanCommandGroup.cs
+++ b/src/Commands/BanCommandGroup.cs
@@ -33,12 +33,12 @@ public class BanCommandGroup : CommandGroup
     private readonly IDiscordRestGuildAPI _guildApi;
     private readonly GuildDataService _guildData;
     private readonly IDiscordRestUserAPI _userApi;
-    private readonly UtilityService _utility;
+    private readonly Utility _utility;
 
     public BanCommandGroup(
         ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
         IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
-        UtilityService utility)
+        Utility utility)
     {
         _context = context;
         _channelApi = channelApi;
diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs
index 7ebd4ea..1d0ad64 100644
--- a/src/Commands/ClearCommandGroup.cs
+++ b/src/Commands/ClearCommandGroup.cs
@@ -30,11 +30,11 @@ public class ClearCommandGroup : CommandGroup
     private readonly IFeedbackService _feedback;
     private readonly GuildDataService _guildData;
     private readonly IDiscordRestUserAPI _userApi;
-    private readonly UtilityService _utility;
+    private readonly Utility _utility;
 
     public ClearCommandGroup(
         IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService guildData,
-        IFeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility)
+        IFeedbackService feedback, IDiscordRestUserAPI userApi, Utility utility)
     {
         _channelApi = channelApi;
         _context = context;
diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs
index cad8ea9..ee94b93 100644
--- a/src/Commands/KickCommandGroup.cs
+++ b/src/Commands/KickCommandGroup.cs
@@ -30,12 +30,12 @@ public class KickCommandGroup : CommandGroup
     private readonly IDiscordRestGuildAPI _guildApi;
     private readonly GuildDataService _guildData;
     private readonly IDiscordRestUserAPI _userApi;
-    private readonly UtilityService _utility;
+    private readonly Utility _utility;
 
     public KickCommandGroup(
         ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
         IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
-        UtilityService utility)
+        Utility utility)
     {
         _context = context;
         _channelApi = channelApi;
diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs
index 6a28f38..522c7f7 100644
--- a/src/Commands/MuteCommandGroup.cs
+++ b/src/Commands/MuteCommandGroup.cs
@@ -32,11 +32,11 @@ public class MuteCommandGroup : CommandGroup
     private readonly IDiscordRestGuildAPI _guildApi;
     private readonly GuildDataService _guildData;
     private readonly IDiscordRestUserAPI _userApi;
-    private readonly UtilityService _utility;
+    private readonly Utility _utility;
 
     public MuteCommandGroup(
         ICommandContext context, GuildDataService guildData, IFeedbackService feedback,
-        IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility)
+        IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, Utility utility)
     {
         _context = context;
         _guildData = guildData;
diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs
index 15fd514..ce7472f 100644
--- a/src/Commands/SettingsCommandGroup.cs
+++ b/src/Commands/SettingsCommandGroup.cs
@@ -57,11 +57,11 @@ public class SettingsCommandGroup : CommandGroup
     private readonly IFeedbackService _feedback;
     private readonly GuildDataService _guildData;
     private readonly IDiscordRestUserAPI _userApi;
-    private readonly UtilityService _utility;
+    private readonly Utility _utility;
 
     public SettingsCommandGroup(
         ICommandContext context, GuildDataService guildData,
-        IFeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility)
+        IFeedbackService feedback, IDiscordRestUserAPI userApi, Utility utility)
     {
         _context = context;
         _guildData = guildData;
diff --git a/src/Octobot.cs b/src/Octobot.cs
index 1806330..5cffd70 100644
--- a/src/Octobot.cs
+++ b/src/Octobot.cs
@@ -87,7 +87,7 @@ public sealed class Octobot
                         .AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
                         // Services
                         .AddSingleton<GuildDataService>()
-                        .AddSingleton<UtilityService>()
+                        .AddSingleton<Utility>()
                         .AddHostedService<MemberUpdateService>()
                         .AddHostedService<ScheduledEventUpdateService>()
                         .AddHostedService<SongUpdateService>()
diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs
index 2d66a3b..a1e7d16 100644
--- a/src/Responders/GuildLoadedResponder.cs
+++ b/src/Responders/GuildLoadedResponder.cs
@@ -25,11 +25,11 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
     private readonly GuildDataService _guildData;
     private readonly ILogger<GuildLoadedResponder> _logger;
     private readonly IDiscordRestUserAPI _userApi;
-    private readonly UtilityService _utility;
+    private readonly Utility _utility;
 
     public GuildLoadedResponder(
         IDiscordRestChannelAPI channelApi, GuildDataService guildData, ILogger<GuildLoadedResponder> logger,
-        IDiscordRestUserAPI userApi, UtilityService utility)
+        IDiscordRestUserAPI userApi, Utility utility)
     {
         _channelApi = channelApi;
         _guildData = guildData;
diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs
index 3cc8cea..961c8f9 100644
--- a/src/Services/GuildDataService.cs
+++ b/src/Services/GuildDataService.cs
@@ -11,7 +11,7 @@ namespace Octobot.Services;
 /// <summary>
 ///     Handles saving, loading, initializing and providing <see cref="GuildData" />.
 /// </summary>
-public sealed class GuildDataService : IHostedService
+public sealed class GuildDataService
 {
     private readonly ConcurrentDictionary<Snowflake, GuildData> _datas = new();
     private readonly ILogger<GuildDataService> _logger;
@@ -24,16 +24,6 @@ public sealed class GuildDataService : IHostedService
         lifetime.ApplicationStopping.Register(ApplicationStopping);
     }
 
-    public Task StartAsync(CancellationToken ct)
-    {
-        return Task.CompletedTask;
-    }
-
-    public Task StopAsync(CancellationToken ct)
-    {
-        return Task.CompletedTask;
-    }
-
     private void ApplicationStopping()
     {
         SaveAsync(CancellationToken.None).GetAwaiter().GetResult();
diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs
index 8937833..06e531f 100644
--- a/src/Services/Update/MemberUpdateService.cs
+++ b/src/Services/Update/MemberUpdateService.cs
@@ -30,10 +30,10 @@ public sealed partial class MemberUpdateService : BackgroundService
     private readonly IDiscordRestGuildAPI _guildApi;
     private readonly GuildDataService _guildData;
     private readonly ILogger<MemberUpdateService> _logger;
-    private readonly UtilityService _utility;
+    private readonly Utility _utility;
 
     public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi,
-        GuildDataService guildData, ILogger<MemberUpdateService> logger, UtilityService utility)
+        GuildDataService guildData, ILogger<MemberUpdateService> logger, Utility utility)
     {
         _channelApi = channelApi;
         _guildApi = guildApi;
diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs
index dd9be0d..ac5c109 100644
--- a/src/Services/Update/ScheduledEventUpdateService.cs
+++ b/src/Services/Update/ScheduledEventUpdateService.cs
@@ -19,10 +19,10 @@ public sealed class ScheduledEventUpdateService : BackgroundService
     private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
     private readonly GuildDataService _guildData;
     private readonly ILogger<ScheduledEventUpdateService> _logger;
-    private readonly UtilityService _utility;
+    private readonly Utility _utility;
 
     public ScheduledEventUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi,
-        GuildDataService guildData, ILogger<ScheduledEventUpdateService> logger, UtilityService utility)
+        GuildDataService guildData, ILogger<ScheduledEventUpdateService> logger, Utility utility)
     {
         _channelApi = channelApi;
         _eventApi = eventApi;
diff --git a/src/Services/UtilityService.cs b/src/Services/Utility.cs
similarity index 97%
rename from src/Services/UtilityService.cs
rename to src/Services/Utility.cs
index 9ac481b..401b067 100644
--- a/src/Services/UtilityService.cs
+++ b/src/Services/Utility.cs
@@ -1,7 +1,6 @@
 using System.Drawing;
 using System.Text;
 using System.Text.Json.Nodes;
-using Microsoft.Extensions.Hosting;
 using Octobot.Data;
 using Octobot.Extensions;
 using Remora.Discord.API.Abstractions.Objects;
@@ -17,14 +16,14 @@ namespace Octobot.Services;
 ///     Provides utility methods that cannot be transformed to extension methods because they require usage
 ///     of some Discord APIs.
 /// </summary>
-public sealed class UtilityService : IHostedService
+public sealed class Utility
 {
     private readonly IDiscordRestChannelAPI _channelApi;
     private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
     private readonly IDiscordRestGuildAPI _guildApi;
     private readonly IDiscordRestUserAPI _userApi;
 
-    public UtilityService(
+    public Utility(
         IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi,
         IDiscordRestUserAPI userApi)
     {
@@ -34,16 +33,6 @@ public sealed class UtilityService : IHostedService
         _userApi = userApi;
     }
 
-    public Task StartAsync(CancellationToken ct)
-    {
-        return Task.CompletedTask;
-    }
-
-    public Task StopAsync(CancellationToken ct)
-    {
-        return Task.CompletedTask;
-    }
-
     /// <summary>
     ///     Checks whether or not a member can interact with another member
     /// </summary>

From 21f200c9884ca0fc9d4129fdc5c364bc0874547c Mon Sep 17 00:00:00 2001
From: Octol1ttle <l1ttleofficial@outlook.com>
Date: Wed, 20 Dec 2023 22:08:56 +0500
Subject: [PATCH 07/11] Merge BackgroundGuildDataSaverService into
 GuildDataService (#239)

Title. idk why I didn't think of this before.
Also, GuildDataService is now properly registered as an IHostedService,
so it receives start & shutdown events. So this PR gets rid of the
workaround that was needed for save-on-shutdown to function

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
---
 src/Octobot.cs                                | 10 ++++----
 .../BackgroundGuildDataSaverService.cs        | 23 ------------------
 src/Services/GuildDataService.cs              | 24 ++++++++++++-------
 3 files changed, 21 insertions(+), 36 deletions(-)
 delete mode 100644 src/Services/BackgroundGuildDataSaverService.cs

diff --git a/src/Octobot.cs b/src/Octobot.cs
index 5cffd70..2648338 100644
--- a/src/Octobot.cs
+++ b/src/Octobot.cs
@@ -24,12 +24,12 @@ namespace Octobot;
 
 public sealed class Octobot
 {
-    public static readonly AllowedMentions NoMentions = new(
-        Array.Empty<MentionType>(), Array.Empty<Snowflake>(), Array.Empty<Snowflake>());
-
     public const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot";
     public const string IssuesUrl = $"{RepositoryUrl}/issues";
 
+    public static readonly AllowedMentions NoMentions = new(
+        Array.Empty<MentionType>(), Array.Empty<Snowflake>(), Array.Empty<Snowflake>());
+
     public static async Task Main(string[] args)
     {
         var host = CreateHostBuilder(args).UseConsoleLifetime().Build();
@@ -86,12 +86,12 @@ public sealed class Octobot
                         .AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
                         .AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
                         // Services
-                        .AddSingleton<GuildDataService>()
                         .AddSingleton<Utility>()
+                        .AddSingleton<GuildDataService>()
+                        .AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
                         .AddHostedService<MemberUpdateService>()
                         .AddHostedService<ScheduledEventUpdateService>()
                         .AddHostedService<SongUpdateService>()
-                        .AddHostedService<BackgroundGuildDataSaverService>()
                         // Slash commands
                         .AddCommandTree()
                         .WithCommandGroup<AboutCommandGroup>()
diff --git a/src/Services/BackgroundGuildDataSaverService.cs b/src/Services/BackgroundGuildDataSaverService.cs
deleted file mode 100644
index 766ffe0..0000000
--- a/src/Services/BackgroundGuildDataSaverService.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.Extensions.Hosting;
-
-namespace Octobot.Services;
-
-public sealed class BackgroundGuildDataSaverService : BackgroundService
-{
-    private readonly GuildDataService _guildData;
-
-    public BackgroundGuildDataSaverService(GuildDataService guildData)
-    {
-        _guildData = guildData;
-    }
-
-    protected override async Task ExecuteAsync(CancellationToken ct)
-    {
-        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
-
-        while (await timer.WaitForNextTickAsync(ct))
-        {
-            await _guildData.SaveAsync(ct);
-        }
-    }
-}
diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs
index 961c8f9..c9458a0 100644
--- a/src/Services/GuildDataService.cs
+++ b/src/Services/GuildDataService.cs
@@ -11,25 +11,23 @@ namespace Octobot.Services;
 /// <summary>
 ///     Handles saving, loading, initializing and providing <see cref="GuildData" />.
 /// </summary>
-public sealed class GuildDataService
+public sealed class GuildDataService : BackgroundService
 {
     private readonly ConcurrentDictionary<Snowflake, GuildData> _datas = new();
     private readonly ILogger<GuildDataService> _logger;
 
-    // https://github.com/dotnet/aspnetcore/issues/39139
-    public GuildDataService(
-        IHostApplicationLifetime lifetime, ILogger<GuildDataService> logger)
+    public GuildDataService(ILogger<GuildDataService> logger)
     {
         _logger = logger;
-        lifetime.ApplicationStopping.Register(ApplicationStopping);
     }
 
-    private void ApplicationStopping()
+    public override Task StopAsync(CancellationToken ct)
     {
-        SaveAsync(CancellationToken.None).GetAwaiter().GetResult();
+        base.StopAsync(ct);
+        return SaveAsync(ct);
     }
 
-    public Task SaveAsync(CancellationToken ct)
+    private Task SaveAsync(CancellationToken ct)
     {
         var tasks = new List<Task>();
         var datas = _datas.Values.ToArray();
@@ -58,6 +56,16 @@ public sealed class GuildDataService
         File.Delete(tempFilePath);
     }
 
+    protected override async Task ExecuteAsync(CancellationToken ct)
+    {
+        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
+
+        while (await timer.WaitForNextTickAsync(ct))
+        {
+            await SaveAsync(ct);
+        }
+    }
+
     public async Task<GuildData> GetData(Snowflake guildId, CancellationToken ct = default)
     {
         return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct);

From d4871bb23da59cd0018a93744cacb68cf5e1a08c Mon Sep 17 00:00:00 2001
From: Octol1ttle <l1ttleofficial@outlook.com>
Date: Wed, 20 Dec 2023 22:48:32 +0500
Subject: [PATCH 08/11] Use AddFromAssembly for responders and command groups
 (#240)

now we don't have to explicitly type out command groups woooo

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
---
 src/Octobot.cs | 26 ++++----------------------
 1 file changed, 4 insertions(+), 22 deletions(-)

diff --git a/src/Octobot.cs b/src/Octobot.cs
index 2648338..2e810ed 100644
--- a/src/Octobot.cs
+++ b/src/Octobot.cs
@@ -2,11 +2,9 @@ using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
-using Octobot.Commands;
 using Octobot.Commands.Events;
 using Octobot.Services;
 using Octobot.Services.Update;
-using Remora.Commands.Extensions;
 using Remora.Discord.API.Abstractions.Gateway.Commands;
 using Remora.Discord.API.Abstractions.Objects;
 using Remora.Discord.API.Objects;
@@ -14,8 +12,8 @@ using Remora.Discord.Caching.Extensions;
 using Remora.Discord.Caching.Services;
 using Remora.Discord.Commands.Extensions;
 using Remora.Discord.Commands.Services;
+using Remora.Discord.Extensions.Extensions;
 using Remora.Discord.Gateway;
-using Remora.Discord.Gateway.Extensions;
 using Remora.Discord.Hosting.Extensions;
 using Remora.Rest.Core;
 using Serilog.Extensions.Logging;
@@ -82,6 +80,8 @@ public sealed class Octobot
                         // Init
                         .AddDiscordCaching()
                         .AddDiscordCommands(true, false)
+                        .AddRespondersFromAssembly(typeof(Octobot).Assembly)
+                        .AddCommandGroupsFromAssembly(typeof(Octobot).Assembly)
                         // Slash command event handlers
                         .AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
                         .AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
@@ -91,25 +91,7 @@ public sealed class Octobot
                         .AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
                         .AddHostedService<MemberUpdateService>()
                         .AddHostedService<ScheduledEventUpdateService>()
-                        .AddHostedService<SongUpdateService>()
-                        // Slash commands
-                        .AddCommandTree()
-                        .WithCommandGroup<AboutCommandGroup>()
-                        .WithCommandGroup<BanCommandGroup>()
-                        .WithCommandGroup<ClearCommandGroup>()
-                        .WithCommandGroup<KickCommandGroup>()
-                        .WithCommandGroup<MuteCommandGroup>()
-                        .WithCommandGroup<PingCommandGroup>()
-                        .WithCommandGroup<RemindCommandGroup>()
-                        .WithCommandGroup<SettingsCommandGroup>()
-                        .WithCommandGroup<ToolsCommandGroup>();
-                    var responderTypes = typeof(Octobot).Assembly
-                        .GetExportedTypes()
-                        .Where(t => t.IsResponder());
-                    foreach (var responderType in responderTypes)
-                    {
-                        services.AddResponder(responderType);
-                    }
+                        .AddHostedService<SongUpdateService>();
                 }
             ).ConfigureLogging(
                 c => c.AddConsole()

From 7d9a85d8156a15953f71fbe11bc13e6fa81f1bed Mon Sep 17 00:00:00 2001
From: Octol1ttle <l1ttleofficial@outlook.com>
Date: Wed, 20 Dec 2023 22:59:17 +0500
Subject: [PATCH 09/11] Add profiler base (#235)

This PR adds the base classes required for profiling code inside of
Octobot. The implementation of the profiler is similar to Minecraft,
however it is more detailed and provides per-event logs for each event.
This PR does not change any code to be profiled and this is intentional.
Changes required for profiling will come as separate PRs, one for
commands, one for responders, and one for background services.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
---
 src/Octobot.cs                           |   3 +
 src/Services/Profiler/Profiler.cs        | 114 +++++++++++++++++++++++
 src/Services/Profiler/ProfilerEvent.cs   |   9 ++
 src/Services/Profiler/ProfilerFactory.cs |  27 ++++++
 4 files changed, 153 insertions(+)
 create mode 100644 src/Services/Profiler/Profiler.cs
 create mode 100644 src/Services/Profiler/ProfilerEvent.cs
 create mode 100644 src/Services/Profiler/ProfilerFactory.cs

diff --git a/src/Octobot.cs b/src/Octobot.cs
index 2e810ed..063bd14 100644
--- a/src/Octobot.cs
+++ b/src/Octobot.cs
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Octobot.Commands.Events;
 using Octobot.Services;
+using Octobot.Services.Profiler;
 using Octobot.Services.Update;
 using Remora.Discord.API.Abstractions.Gateway.Commands;
 using Remora.Discord.API.Abstractions.Objects;
@@ -86,6 +87,8 @@ public sealed class Octobot
                         .AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
                         .AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
                         // Services
+                        .AddTransient<Profiler>()
+                        .AddSingleton<ProfilerFactory>()
                         .AddSingleton<Utility>()
                         .AddSingleton<GuildDataService>()
                         .AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
diff --git a/src/Services/Profiler/Profiler.cs b/src/Services/Profiler/Profiler.cs
new file mode 100644
index 0000000..8d4ca98
--- /dev/null
+++ b/src/Services/Profiler/Profiler.cs
@@ -0,0 +1,114 @@
+using System.Diagnostics;
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Remora.Results;
+
+// TODO: remove in future profiler PRs
+// ReSharper disable All
+
+namespace Octobot.Services.Profiler;
+
+/// <summary>
+/// Provides the ability to profile how long certain parts of code take to complete using <see cref="Stopwatch"/>es.
+/// </summary>
+/// <remarks>Resolve <see cref="ProfilerFactory"/> instead in singletons.</remarks>
+public sealed class Profiler
+{
+    private const int MaxProfilerTime = 1000; // milliseconds
+    private readonly List<ProfilerEvent> _events = [];
+    private readonly ILogger<Profiler> _logger;
+
+    public Profiler(ILogger<Profiler> logger)
+    {
+        _logger = logger;
+    }
+
+    /// <summary>
+    /// Pushes an event to the profiler.
+    /// </summary>
+    /// <param name="id">The ID of the event.</param>
+    public void Push(string id)
+    {
+        _events.Add(new ProfilerEvent
+        {
+            Id = id,
+            Stopwatch = Stopwatch.StartNew()
+        });
+    }
+
+    /// <summary>
+    /// Pops the last pushed event from the profiler.
+    /// </summary>
+    /// <exception cref="InvalidOperationException">Thrown if the profiler contains no events.</exception>
+    public void Pop()
+    {
+        if (_events.Count is 0)
+        {
+            throw new InvalidOperationException("Nothing to pop");
+        }
+
+        _events.Last().Stopwatch.Stop();
+    }
+
+    /// <summary>
+    /// If the profiler took too long to execute, this will log a warning with per-event time usage
+    /// </summary>
+    /// <exception cref="InvalidOperationException"></exception>
+    private void Report()
+    {
+        var main = _events[0];
+        if (main.Stopwatch.ElapsedMilliseconds < MaxProfilerTime)
+        {
+            return;
+        }
+
+        var unprofiled = main.Stopwatch.ElapsedMilliseconds;
+        var builder = new StringBuilder().AppendLine();
+        for (var i = 1; i < _events.Count; i++)
+        {
+            var profilerEvent = _events[i];
+            if (profilerEvent.Stopwatch.IsRunning)
+            {
+                throw new InvalidOperationException(
+                    $"Tried to report on a profiler with running stopwatches: {profilerEvent.Id}");
+            }
+
+            builder.AppendLine($"{profilerEvent.Id}: {profilerEvent.Stopwatch.ElapsedMilliseconds}ms");
+            unprofiled -= profilerEvent.Stopwatch.ElapsedMilliseconds;
+        }
+
+        builder.AppendLine($"<unprofiled>: {unprofiled}ms");
+
+        _logger.LogWarning("Profiler {ID} took {Elapsed} milliseconds to execute (max: {Max}ms):{Events}", main.Id,
+            main.Stopwatch.ElapsedMilliseconds, MaxProfilerTime, builder.ToString());
+    }
+
+    /// <summary>
+    /// <see cref="Pop"/> the profiler and <see cref="Report"/> on it afterwards.
+    /// </summary>
+    public void PopAndReport()
+    {
+        Pop();
+        Report();
+    }
+
+    /// <summary>
+    /// <see cref="PopAndReport"/> on the profiler and return a <see cref="Result{TEntity}"/>.
+    /// </summary>
+    /// <param name="result"></param>
+    /// <returns></returns>
+    public Result ReportWithResult(Result result)
+    {
+        PopAndReport();
+        return result;
+    }
+
+    /// <summary>
+    /// Calls <see cref="ReportWithResult"/> with <see cref="Result.FromSuccess"/>
+    /// </summary>
+    /// <returns>A successful result.</returns>
+    public Result ReportWithSuccess()
+    {
+        return ReportWithResult(Result.FromSuccess());
+    }
+}
diff --git a/src/Services/Profiler/ProfilerEvent.cs b/src/Services/Profiler/ProfilerEvent.cs
new file mode 100644
index 0000000..f655fc4
--- /dev/null
+++ b/src/Services/Profiler/ProfilerEvent.cs
@@ -0,0 +1,9 @@
+using System.Diagnostics;
+
+namespace Octobot.Services.Profiler;
+
+public struct ProfilerEvent
+{
+    public string Id { get; init; }
+    public Stopwatch Stopwatch { get; init; }
+}
diff --git a/src/Services/Profiler/ProfilerFactory.cs b/src/Services/Profiler/ProfilerFactory.cs
new file mode 100644
index 0000000..0135771
--- /dev/null
+++ b/src/Services/Profiler/ProfilerFactory.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Octobot.Services.Profiler;
+
+/// <summary>
+/// Provides a method to create a <see cref="Profiler"/>. Useful in singletons.
+/// </summary>
+public sealed class ProfilerFactory
+{
+    private readonly IServiceScopeFactory _scopeFactory;
+
+    public ProfilerFactory(IServiceScopeFactory scopeFactory)
+    {
+        _scopeFactory = scopeFactory;
+    }
+
+    /// <summary>
+    /// Creates a new <see cref="Profiler"/>.
+    /// </summary>
+    /// <returns>A new <see cref="Profiler"/>.</returns>
+    // TODO: remove in future profiler PRs
+    // ReSharper disable once UnusedMember.Global
+    public Profiler Create()
+    {
+        return _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<Profiler>();
+    }
+}

From 285763d50dd02f1f6e0d8a82e7aabfe74a2fabd2 Mon Sep 17 00:00:00 2001
From: Macintxsh <95250141+mctaylors@users.noreply.github.com>
Date: Thu, 21 Dec 2023 18:35:10 +0300
Subject: [PATCH 10/11] /userinfo: Show if the user was kicked (#242)

Closes #241

Updates:
- Show if the user was kicked by [adding "Kicked" parameter to
MemberData](https://github.com/LabsDevelopment/Octobot/issues/241)
- Change `mctaylors-ru`'s `UserInfoBannedPermanently` string to be
different from `UserInfoBanned`
- Finally add `AppendPunishmentsInformation` method to avoid Cognitive
Complexity
- Use MemberData to check if the user was banned
- Rename variable `isMuted` to `wasMuted` to be consistent with other
variable names

---------

Signed-off-by: mctaylors <cantsendmails@mctaylors.ru>
---
 locale/Messages.resx                         |  3 ++
 locale/Messages.ru.resx                      |  3 ++
 locale/Messages.tt-ru.resx                   |  5 +-
 src/Commands/KickCommandGroup.cs             |  4 +-
 src/Commands/MuteCommandGroup.cs             |  4 +-
 src/Commands/ToolsCommandGroup.cs            | 52 ++++++++++++--------
 src/Data/MemberData.cs                       |  1 +
 src/Messages.Designer.cs                     |  8 +++
 src/Responders/GuildMemberJoinedResponder.cs |  2 +
 9 files changed, 58 insertions(+), 24 deletions(-)

diff --git a/locale/Messages.resx b/locale/Messages.resx
index 60e6e07..1387edf 100644
--- a/locale/Messages.resx
+++ b/locale/Messages.resx
@@ -585,4 +585,7 @@
   <data name="ButtonReportIssue" xml:space="preserve">
       <value>Report an issue</value>
   </data>
+  <data name="UserInfoKicked" xml:space="preserve">
+      <value>Kicked</value>
+  </data>
 </root>
diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx
index 4b9492c..572c0b2 100644
--- a/locale/Messages.ru.resx
+++ b/locale/Messages.ru.resx
@@ -585,4 +585,7 @@
   <data name="ButtonReportIssue" xml:space="preserve">
       <value>Сообщить о проблеме</value>
   </data>
+  <data name="UserInfoKicked" xml:space="preserve">
+      <value>Выгнан</value>
+  </data>
 </root>
diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx
index de1f39f..d20c358 100644
--- a/locale/Messages.tt-ru.resx
+++ b/locale/Messages.tt-ru.resx
@@ -502,7 +502,7 @@
     <value>приколы полученные по заслугам</value>
   </data>
   <data name="UserInfoBannedPermanently" xml:space="preserve">
-    <value>забанен</value>
+    <value>пермабан</value>
   </data>
   <data name="UserInfoNotOnGuild" xml:space="preserve">
     <value>вышел из сервера</value>
@@ -585,4 +585,7 @@
   <data name="ButtonReportIssue" xml:space="preserve">
       <value>зарепортить баг</value>
   </data>
+  <data name="UserInfoKicked" xml:space="preserve">
+      <value>кикнут</value>
+  </data>
 </root>
diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs
index ee94b93..a278fb4 100644
--- a/src/Commands/KickCommandGroup.cs
+++ b/src/Commands/KickCommandGroup.cs
@@ -151,7 +151,9 @@ public class KickCommandGroup : CommandGroup
             return Result.FromError(kickResult.Error);
         }
 
-        data.GetOrCreateMemberData(target.ID).Roles.Clear();
+        var memberData = data.GetOrCreateMemberData(target.ID);
+        memberData.Roles.Clear();
+        memberData.Kicked = true;
 
         var title = string.Format(Messages.UserKicked, target.GetTag());
         var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));
diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs
index 522c7f7..c7b21f6 100644
--- a/src/Commands/MuteCommandGroup.cs
+++ b/src/Commands/MuteCommandGroup.cs
@@ -300,9 +300,9 @@ public class MuteCommandGroup : CommandGroup
         }
 
         var memberData = data.GetOrCreateMemberData(target.ID);
-        var isMuted = memberData.MutedUntil is not null || communicationDisabledUntil is not null;
+        var wasMuted = memberData.MutedUntil is not null || communicationDisabledUntil is not null;
 
-        if (!isMuted)
+        if (!wasMuted)
         {
             var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, bot)
                 .WithColour(ColorsList.Red).Build();
diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs
index f04ddf6..1dbf72d 100644
--- a/src/Commands/ToolsCommandGroup.cs
+++ b/src/Commands/ToolsCommandGroup.cs
@@ -122,32 +122,21 @@ public class ToolsCommandGroup : CommandGroup
             embedColor = AppendGuildInformation(embedColor, guildMember, builder);
         }
 
-        var isMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) ||
-                      communicationDisabledUntil is not null;
+        var wasMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) ||
+                       communicationDisabledUntil is not null;
+        var wasBanned = memberData.BannedUntil is not null;
+        var wasKicked = memberData.Kicked;
 
-        var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct);
-
-        if (isMuted || existingBanResult.IsDefined())
+        if (wasMuted || wasBanned || wasKicked)
         {
             builder.Append("### ")
                 .AppendLine(Markdown.Bold(Messages.UserInfoPunishments));
+
+            embedColor = AppendPunishmentsInformation(wasMuted, wasKicked, wasBanned, memberData,
+                builder, embedColor, communicationDisabledUntil);
         }
 
-        if (isMuted)
-        {
-            AppendMuteInformation(memberData, communicationDisabledUntil, builder);
-
-            embedColor = ColorsList.Red;
-        }
-
-        if (existingBanResult.IsDefined())
-        {
-            AppendBanInformation(memberData, builder);
-
-            embedColor = ColorsList.Black;
-        }
-
-        if (!guildMemberResult.IsSuccess && !existingBanResult.IsDefined())
+        if (!guildMemberResult.IsSuccess && !wasBanned)
         {
             builder.Append("### ")
                 .AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild));
@@ -166,6 +155,29 @@ public class ToolsCommandGroup : CommandGroup
         return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
     }
 
+    private static Color AppendPunishmentsInformation(bool wasMuted, bool wasKicked, bool wasBanned,
+        MemberData memberData, StringBuilder builder, Color embedColor, DateTimeOffset? communicationDisabledUntil)
+    {
+        if (wasMuted)
+        {
+            AppendMuteInformation(memberData, communicationDisabledUntil, builder);
+            embedColor = ColorsList.Red;
+        }
+
+        if (wasKicked)
+        {
+            builder.AppendBulletPointLine(Messages.UserInfoKicked);
+        }
+
+        if (wasBanned)
+        {
+            AppendBanInformation(memberData, builder);
+            embedColor = ColorsList.Black;
+        }
+
+        return embedColor;
+    }
+
     private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder)
     {
         if (guildMember.Nickname.IsDefined(out var nickname))
diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs
index 0b0cfb2..8e23e54 100644
--- a/src/Data/MemberData.cs
+++ b/src/Data/MemberData.cs
@@ -18,6 +18,7 @@ public sealed class MemberData
     public ulong Id { get; }
     public DateTimeOffset? BannedUntil { get; set; }
     public DateTimeOffset? MutedUntil { get; set; }
+    public bool Kicked { get; set; }
     public List<ulong> Roles { get; set; } = [];
     public List<Reminder> Reminders { get; } = [];
 }
diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs
index 767bd5b..8dad3dc 100644
--- a/src/Messages.Designer.cs
+++ b/src/Messages.Designer.cs
@@ -1036,5 +1036,13 @@ namespace Octobot {
                 return ResourceManager.GetString("ButtonReportIssue", resourceCulture);
             }
         }
+
+        internal static string UserInfoKicked
+        {
+            get
+            {
+                return ResourceManager.GetString("UserInfoKicked", resourceCulture);
+            }
+        }
     }
 }
diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs
index 66faa28..eee93b6 100644
--- a/src/Responders/GuildMemberJoinedResponder.cs
+++ b/src/Responders/GuildMemberJoinedResponder.cs
@@ -43,6 +43,8 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
         var cfg = data.Settings;
         var memberData = data.GetOrCreateMemberData(user.ID);
 
+        memberData.Kicked = false;
+
         var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct);
         if (!returnRolesResult.IsSuccess)
         {

From 3134c3575110e6c693f83466eb82d8f627e0585a Mon Sep 17 00:00:00 2001
From: Octol1ttle <l1ttleofficial@outlook.com>
Date: Thu, 21 Dec 2023 22:21:20 +0500
Subject: [PATCH 11/11] Ban usages of Thread#Sleep (#243)

Using Thread.Sleep blocks the _entire_ thread from doing *anything*,
while Task.Delay allows the thread to execute other tasks while the
delay is passing. The inability to cancel Thread.Sleep may also cause
longer shutdowns

tl;dr Thread.Sleep bad, Task.Delay good
made because of
https://github.com/LabsDevelopment/Octobot/pull/234/commits/578c03871de0dd042f2fe0918df296f0a8123e00

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
---
 CodeAnalysis/BannedSymbols.txt | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 0a1ec81..bf444a9 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -18,3 +18,5 @@ P:System.DateTime.Now;Use System.DateTime.UtcNow instead.
 P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead.
 P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead.
 M:System.IO.File.OpenWrite(System.String);File.OpenWrite(string) does not clear the file before writing to it. Use File.Create(string) instead.
+M:System.Threading.Thread.Sleep(System.Int32);Use Task.Delay(int, CancellationToken) instead.
+M:System.Threading.Thread.Sleep(System.TimeSpan);Use Task.Delay(TimeSpan, CancellationToken) instead.