diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs
index c0349078..8529d487 100644
--- a/src/Commands/AboutCommandGroup.cs
+++ b/src/Commands/AboutCommandGroup.cs
@@ -2,6 +2,7 @@
using System.Text;
using JetBrains.Annotations;
using Octobot.Data;
+using Octobot.Extensions;
using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs
index 010a9dad..8b62858a 100644
--- a/src/Commands/BanCommandGroup.cs
+++ b/src/Commands/BanCommandGroup.cs
@@ -2,6 +2,7 @@
using System.Text;
using JetBrains.Annotations;
using Octobot.Data;
+using Octobot.Extensions;
using Octobot.Services;
using Octobot.Services.Update;
using Remora.Commands.Attributes;
@@ -53,7 +54,7 @@ public BanCommandGroup(
/// The user to ban.
/// The duration for this ban. The user will be automatically unbanned after this duration.
///
- /// The reason for this ban. Must be encoded with when passed to
+ /// The reason for this ban. Must be encoded with when passed to
/// .
///
///
@@ -196,7 +197,7 @@ var interactionResult
///
/// The user to unban.
///
- /// The reason for this unban. Must be encoded with when passed to
+ /// The reason for this unban. Must be encoded with when passed to
/// .
///
///
diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs
index c963fdf9..a6ac188c 100644
--- a/src/Commands/ClearCommandGroup.cs
+++ b/src/Commands/ClearCommandGroup.cs
@@ -2,6 +2,7 @@
using System.Text;
using JetBrains.Annotations;
using Octobot.Data;
+using Octobot.Extensions;
using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs
index 90002678..d6a66ccc 100644
--- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs
+++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs
@@ -1,5 +1,6 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
+using Octobot.Extensions;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Services;
using Remora.Results;
diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs
index b3f94252..be48e74b 100644
--- a/src/Commands/Events/LoggingPreparationErrorEvent.cs
+++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs
@@ -1,5 +1,6 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
+using Octobot.Extensions;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Services;
using Remora.Results;
diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs
index 2ee99a94..05552a2d 100644
--- a/src/Commands/KickCommandGroup.cs
+++ b/src/Commands/KickCommandGroup.cs
@@ -1,6 +1,7 @@
using System.ComponentModel;
using JetBrains.Annotations;
using Octobot.Data;
+using Octobot.Extensions;
using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
@@ -49,7 +50,7 @@ public KickCommandGroup(
///
/// The member to kick.
///
- /// The reason for this kick. Must be encoded with when passed to
+ /// The reason for this kick. Must be encoded with when passed to
/// .
///
///
diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs
index 4ec4c6c9..50fe7a3f 100644
--- a/src/Commands/MuteCommandGroup.cs
+++ b/src/Commands/MuteCommandGroup.cs
@@ -2,6 +2,7 @@
using System.Text;
using JetBrains.Annotations;
using Octobot.Data;
+using Octobot.Extensions;
using Octobot.Services;
using Octobot.Services.Update;
using Remora.Commands.Attributes;
@@ -50,7 +51,7 @@ public MuteCommandGroup(
/// The member to mute.
/// The duration for this mute. The member will be automatically unmuted after this duration.
///
- /// The reason for this mute. Must be encoded with when passed to
+ /// The reason for this mute. Must be encoded with when passed to
/// .
///
///
@@ -213,7 +214,7 @@ private async Task TimeoutUserAsync(
///
/// The member to unmute.
///
- /// The reason for this unmute. Must be encoded with when passed to
+ /// The reason for this unmute. Must be encoded with when passed to
/// .
///
///
diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs
index a1b14bda..293fbffd 100644
--- a/src/Commands/PingCommandGroup.cs
+++ b/src/Commands/PingCommandGroup.cs
@@ -1,6 +1,7 @@
using System.ComponentModel;
using JetBrains.Annotations;
using Octobot.Data;
+using Octobot.Extensions;
using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs
index 966b3ec9..4a4f6a1f 100644
--- a/src/Commands/RemindCommandGroup.cs
+++ b/src/Commands/RemindCommandGroup.cs
@@ -2,6 +2,7 @@
using System.Text;
using JetBrains.Annotations;
using Octobot.Data;
+using Octobot.Extensions;
using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs
index fc4fbe74..317b5c86 100644
--- a/src/Commands/SettingsCommandGroup.cs
+++ b/src/Commands/SettingsCommandGroup.cs
@@ -4,6 +4,7 @@
using JetBrains.Annotations;
using Octobot.Data;
using Octobot.Data.Options;
+using Octobot.Extensions;
using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs
index 92f17857..6c70c01e 100644
--- a/src/Commands/ToolsCommandGroup.cs
+++ b/src/Commands/ToolsCommandGroup.cs
@@ -3,6 +3,7 @@
using System.Text;
using JetBrains.Annotations;
using Octobot.Data;
+using Octobot.Extensions;
using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs
index 2150725b..66ada96e 100644
--- a/src/Data/Options/SnowflakeOption.cs
+++ b/src/Data/Options/SnowflakeOption.cs
@@ -1,5 +1,6 @@
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
+using Octobot.Extensions;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
diff --git a/src/Extensions.cs b/src/Extensions.cs
deleted file mode 100644
index 00d3d364..00000000
--- a/src/Extensions.cs
+++ /dev/null
@@ -1,366 +0,0 @@
-using System.Net;
-using System.Text;
-using DiffPlex.DiffBuilder.Model;
-using Microsoft.Extensions.Logging;
-using Remora.Discord.API;
-using Remora.Discord.API.Abstractions.Objects;
-using Remora.Discord.API.Objects;
-using Remora.Discord.Commands.Contexts;
-using Remora.Discord.Commands.Extensions;
-using Remora.Discord.Commands.Feedback.Services;
-using Remora.Discord.Extensions.Embeds;
-using Remora.Discord.Extensions.Formatting;
-using Remora.Rest.Core;
-using Remora.Results;
-
-namespace Octobot;
-
-public static class Extensions
-{
- ///
- /// Adds a footer representing that an action was performed by a .
- ///
- /// The builder to add the footer to.
- /// The user that performed the action whose tag and avatar to use.
- /// The builder with the added footer.
- public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user)
- {
- var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256);
- var avatarUrl = avatarUrlResult.IsSuccess
- ? avatarUrlResult.Entity.AbsoluteUri
- : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri;
-
- return builder.WithFooter(
- new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl));
- }
-
- ///
- /// Adds a title using the author field, making it smaller than using the title field.
- ///
- /// The builder to add the small title to.
- /// The text of the small title.
- /// The user whose avatar to use in the small title.
- /// The builder with the added small title in the author field.
- public static EmbedBuilder WithSmallTitle(
- this EmbedBuilder builder, string text, IUser? avatarSource = null)
- {
- Uri? avatarUrl = null;
- if (avatarSource is not null)
- {
- var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256);
-
- avatarUrl = avatarUrlResult.IsSuccess
- ? avatarUrlResult.Entity
- : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity;
- }
-
- builder.Author = new EmbedAuthorBuilder(text, iconUrl: avatarUrl?.AbsoluteUri);
- return builder;
- }
-
- ///
- /// Adds a user avatar in the thumbnail field.
- ///
- /// The builder to add the thumbnail to.
- /// The user whose avatar to use in the thumbnail field.
- /// The builder with the added avatar in the thumbnail field.
- public static EmbedBuilder WithLargeUserAvatar(
- this EmbedBuilder builder, IUser avatarSource)
- {
- var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256);
- var avatarUrl = avatarUrlResult.IsSuccess
- ? avatarUrlResult.Entity
- : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity;
-
- return builder.WithThumbnailUrl(avatarUrl.AbsoluteUri);
- }
-
- ///
- /// Adds a guild icon in the thumbnail field.
- ///
- /// The builder to add the thumbnail to.
- /// The guild whose icon to use in the thumbnail field.
- /// The builder with the added icon in the thumbnail field.
- public static EmbedBuilder WithLargeGuildIcon(
- this EmbedBuilder builder, IGuild iconSource)
- {
- var iconUrlResult = CDN.GetGuildIconUrl(iconSource, imageSize: 256);
- return iconUrlResult.IsSuccess
- ? builder.WithThumbnailUrl(iconUrlResult.Entity.AbsoluteUri)
- : builder;
- }
-
- ///
- /// Adds a guild banner in the image field.
- ///
- /// The builder to add the image to.
- /// The guild whose banner to use in the image field.
- /// The builder with the added banner in the image field.
- public static EmbedBuilder WithGuildBanner(
- this EmbedBuilder builder, IGuild bannerSource)
- {
- return bannerSource.Banner is not null
- ? builder.WithImageUrl(CDN.GetGuildBannerUrl(bannerSource).Entity.AbsoluteUri)
- : builder;
- }
-
- ///
- /// Adds a footer representing that the action was performed in the .
- ///
- /// The builder to add the footer to.
- /// The guild whose name and icon to use.
- /// The builder with the added footer.
- public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild)
- {
- var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256);
- var iconUrl = iconUrlResult.IsSuccess
- ? iconUrlResult.Entity.AbsoluteUri
- : default(Optional);
-
- return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl));
- }
-
- ///
- /// Adds a title representing that the action happened in the .
- ///
- /// The builder to add the title to.
- /// The guild whose name and icon to use.
- /// The builder with the added title.
- public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild)
- {
- var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256);
- var iconUrl = iconUrlResult.IsSuccess
- ? iconUrlResult.Entity.AbsoluteUri
- : null;
-
- builder.Author = new EmbedAuthorBuilder(guild.Name, iconUrl: iconUrl);
- return builder;
- }
-
- ///
- /// Adds a scheduled event's cover image.
- ///
- /// The builder to add the image to.
- /// The ID of the scheduled event whose image to use.
- /// The Optional containing the image hash.
- /// The builder with the added cover image.
- public static EmbedBuilder WithEventCover(
- this EmbedBuilder builder, Snowflake eventId, Optional imageHashOptional)
- {
- if (!imageHashOptional.IsDefined(out var imageHash))
- {
- return builder;
- }
-
- var iconUrlResult = CDN.GetGuildScheduledEventCoverUrl(eventId, imageHash, imageSize: 1024);
- return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder;
- }
-
- ///
- /// Sanitizes a string for use in by inserting zero-width spaces in between
- /// symbols used to format the string with block code.
- ///
- /// The string to sanitize.
- /// The sanitized string that can be safely used in .
- private static string SanitizeForBlockCode(this string s)
- {
- return s.Replace("```", "```");
- }
-
- ///
- /// Sanitizes a string (see ) and formats the string to use Markdown Block Code
- /// formatting with a specified
- /// language for syntax highlighting.
- ///
- /// The string to sanitize and format.
- ///
- ///
- /// The sanitized string formatted to use Markdown Block Code with a specified
- /// language for syntax highlighting.
- ///
- public static string InBlockCode(this string s, string language = "")
- {
- s = s.SanitizeForBlockCode();
- return
- $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith("`", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(s) ? " " : "")}```";
- }
-
- public static string Localized(this string key)
- {
- return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key;
- }
-
- ///
- /// Encodes a string to allow its transmission in request headers.
- ///
- /// Used when encountering "Request headers must contain only ASCII characters".
- /// The string to encode.
- /// An encoded string with spaces kept intact.
- public static string EncodeHeader(this string s)
- {
- return WebUtility.UrlEncode(s).Replace('+', ' ');
- }
-
- public static string AsMarkdown(this DiffPaneModel model)
- {
- var builder = new StringBuilder();
- foreach (var line in model.Lines)
- {
- if (line.Type is ChangeType.Deleted)
- {
- builder.Append("-- ");
- }
-
- if (line.Type is ChangeType.Inserted)
- {
- builder.Append("++ ");
- }
-
- if (line.Type is not ChangeType.Imaginary)
- {
- builder.AppendLine(line.Text);
- }
- }
-
- return InBlockCode(builder.ToString(), "diff");
- }
-
- public static string GetTag(this IUser user)
- {
- return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}";
- }
-
- public static Snowflake ToSnowflake(this ulong id)
- {
- return DiscordSnowflake.New(id);
- }
-
- public static TResult? MaxOrDefault(
- this IEnumerable source, Func selector)
- {
- var list = source.ToList();
- return list.Any() ? list.Max(selector) : default;
- }
-
- public static bool TryGetContextIDs(
- this ICommandContext context, out Snowflake guildId,
- out Snowflake channelId, out Snowflake executorId)
- {
- channelId = default;
- executorId = default;
- return context.TryGetGuildID(out guildId)
- && context.TryGetChannelID(out channelId)
- && context.TryGetUserID(out executorId);
- }
-
- ///
- /// Checks whether this Snowflake has any value set.
- ///
- /// The Snowflake to check.
- /// true if the Snowflake has no value set or it's set to 0, false otherwise.
- public static bool Empty(this Snowflake snowflake)
- {
- return snowflake.Value is 0;
- }
-
- ///
- /// Checks whether this snowflake is empty (see ) or it's equal to
- ///
- ///
- /// The Snowflake to check for emptiness
- /// The Snowflake to check for equality with .
- ///
- /// true if is empty or is equal to , false
- /// otherwise.
- ///
- ///
- public static bool EmptyOrEqualTo(this Snowflake snowflake, Snowflake anotherSnowflake)
- {
- return snowflake.Empty() || snowflake == anotherSnowflake;
- }
-
- public static async Task SendContextualEmbedResultAsync(
- this FeedbackService feedback, Result