From bbddb3790ad82f1cbecd2fa8911f44d7ffbf75fc Mon Sep 17 00:00:00 2001
From: Octol1ttle <l1ttleofficial@outlook.com>
Date: Tue, 11 Jul 2023 00:28:49 +0500
Subject: [PATCH] Made guild settings code 10x better

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
---
 locale/Messages.Designer.cs          |  5 +-
 src/Boyfriend.cs                     | 10 ++--
 src/Commands/AboutCommandGroup.cs    |  6 +-
 src/Commands/BanCommandGroup.cs      | 42 +++++++------
 src/Commands/ClearCommandGroup.cs    | 13 ++--
 src/Commands/KickCommandGroup.cs     | 22 ++++---
 src/Commands/MuteCommandGroup.cs     | 42 +++++++------
 src/Commands/PingCommandGroup.cs     |  6 +-
 src/Commands/RemindCommandGroup.cs   |  5 +-
 src/Commands/SettingsCommandGroup.cs | 80 +++++++++++--------------
 src/Data/GuildConfiguration.cs       | 90 ----------------------------
 src/Data/GuildData.cs                | 15 ++---
 src/Data/GuildSettings.cs            | 63 +++++++++++++++++++
 src/Data/MemberData.cs               |  4 +-
 src/Data/Options/BoolOption.cs       | 31 ++++++++++
 src/Data/Options/IOption.cs          | 10 ++++
 src/Data/Options/LanguageOption.cs   | 31 ++++++++++
 src/Data/Options/Option.cs           | 45 ++++++++++++++
 src/Data/Options/SnowflakeOption.cs  | 23 +++++++
 src/Data/Options/TimeSpanOption.cs   | 20 +++++++
 src/Data/Reminder.cs                 |  6 +-
 src/EventResponders.cs               | 65 ++++++++++----------
 src/Extensions.cs                    |  8 ++-
 src/Services/GuildDataService.cs     | 26 ++++----
 src/Services/GuildUpdateService.cs   | 53 ++++++++--------
 src/Services/UtilityService.cs       | 34 +++++------
 26 files changed, 452 insertions(+), 303 deletions(-)
 delete mode 100644 src/Data/GuildConfiguration.cs
 create mode 100644 src/Data/GuildSettings.cs
 create mode 100644 src/Data/Options/BoolOption.cs
 create mode 100644 src/Data/Options/IOption.cs
 create mode 100644 src/Data/Options/LanguageOption.cs
 create mode 100644 src/Data/Options/Option.cs
 create mode 100644 src/Data/Options/SnowflakeOption.cs
 create mode 100644 src/Data/Options/TimeSpanOption.cs

diff --git a/locale/Messages.Designer.cs b/locale/Messages.Designer.cs
index de6955b..8ab20f8 100644
--- a/locale/Messages.Designer.cs
+++ b/locale/Messages.Designer.cs
@@ -7,10 +7,7 @@
 // </auto-generated>
 //------------------------------------------------------------------------------
 
-namespace Boyfriend {
-    using System;
-
-
+namespace Boyfriend.locale {
     [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
     [System.Diagnostics.DebuggerNonUserCodeAttribute()]
     [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs
index 6af4326..d285213 100644
--- a/src/Boyfriend.cs
+++ b/src/Boyfriend.cs
@@ -56,11 +56,11 @@ public class Boyfriend {
                                                       | GatewayIntents.GuildMembers
                                                       | GatewayIntents.GuildScheduledEvents);
                     services.Configure<CacheSettings>(
-                        settings => {
-                            settings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1));
-                            settings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30));
-                            settings.SetAbsoluteExpiration<IMessage>(TimeSpan.FromDays(7));
-                            settings.SetSlidingExpiration<IMessage>(TimeSpan.FromDays(7));
+                        cSettings => {
+                            cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1));
+                            cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30));
+                            cSettings.SetAbsoluteExpiration<IMessage>(TimeSpan.FromDays(7));
+                            cSettings.SetSlidingExpiration<IMessage>(TimeSpan.FromDays(7));
                         });
 
                     services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>()
diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs
index 5ff757e..32ce07d 100644
--- a/src/Commands/AboutCommandGroup.cs
+++ b/src/Commands/AboutCommandGroup.cs
@@ -1,5 +1,7 @@
 using System.ComponentModel;
 using System.Text;
+using Boyfriend.Data;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using Remora.Commands.Attributes;
 using Remora.Commands.Groups;
@@ -51,8 +53,8 @@ public class AboutCommandGroup : CommandGroup {
         if (!currentUserResult.IsDefined(out var currentUser))
             return Result.FromError(currentUserResult);
 
-        var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
-        Messages.Culture = cfg.GetCulture();
+        var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers));
         foreach (var dev in Developers)
diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs
index 02e0fa2..b209683 100644
--- a/src/Commands/BanCommandGroup.cs
+++ b/src/Commands/BanCommandGroup.cs
@@ -1,5 +1,7 @@
 using System.ComponentModel;
 using System.Text;
+using Boyfriend.Data;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using Remora.Commands.Attributes;
 using Remora.Commands.Groups;
@@ -76,8 +78,8 @@ public class BanCommandGroup : CommandGroup {
             return Result.FromError(currentUserResult);
 
         var data = await _dataService.GetData(guildId.Value, CancellationToken);
-        var cfg = data.Configuration;
-        Messages.Culture = data.Culture;
+        var cfg = data.Settings;
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken);
         if (existingBanResult.IsDefined()) {
@@ -145,8 +147,10 @@ public class BanCommandGroup : CommandGroup {
                     string.Format(Messages.UserBanned, target.GetTag()), target)
                 .WithColour(ColorsList.Green).Build();
 
-            if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
-                || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
+            if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
+                 && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
+                || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
+                    && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
                 var logEmbed = new EmbedBuilder().WithSmallTitle(
                         string.Format(Messages.UserBanned, target.GetTag()), target)
                     .WithDescription(description)
@@ -160,14 +164,14 @@ public class BanCommandGroup : CommandGroup {
 
                 var builtArray = new[] { logBuilt };
                 // Not awaiting to reduce response time
-                if (cfg.PublicFeedbackChannel != channelId.Value)
+                if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
                     _ = _channelApi.CreateMessageAsync(
-                        cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                        GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
                         ct: CancellationToken);
-                if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
-                    && cfg.PrivateFeedbackChannel != channelId.Value)
+                if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
+                    && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
                     _ = _channelApi.CreateMessageAsync(
-                        cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                        GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
                         ct: CancellationToken);
             }
         }
@@ -209,8 +213,8 @@ public class BanCommandGroup : CommandGroup {
         if (!currentUserResult.IsDefined(out var currentUser))
             return Result.FromError(currentUserResult);
 
-        var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
-        Messages.Culture = cfg.GetCulture();
+        var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken);
         if (!existingBanResult.IsDefined()) {
@@ -238,8 +242,10 @@ public class BanCommandGroup : CommandGroup {
                 string.Format(Messages.UserUnbanned, target.GetTag()), target)
             .WithColour(ColorsList.Green).Build();
 
-        if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
-            || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
+        if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
+             && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
+            || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
+                && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
             var logEmbed = new EmbedBuilder().WithSmallTitle(
                     string.Format(Messages.UserUnbanned, target.GetTag()), target)
                 .WithDescription(string.Format(Messages.DescriptionActionReason, reason))
@@ -254,14 +260,14 @@ public class BanCommandGroup : CommandGroup {
             var builtArray = new[] { logBuilt };
 
             // Not awaiting to reduce response time
-            if (cfg.PublicFeedbackChannel != channelId.Value)
+            if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
                 _ = _channelApi.CreateMessageAsync(
-                    cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                    GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
                     ct: CancellationToken);
-            if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
-                && cfg.PrivateFeedbackChannel != channelId.Value)
+            if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
+                && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
                 _ = _channelApi.CreateMessageAsync(
-                    cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                    GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
                     ct: CancellationToken);
         }
 
diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs
index de44fbb..dd396c3 100644
--- a/src/Commands/ClearCommandGroup.cs
+++ b/src/Commands/ClearCommandGroup.cs
@@ -1,5 +1,7 @@
 using System.ComponentModel;
 using System.Text;
+using Boyfriend.Data;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using Remora.Commands.Attributes;
 using Remora.Commands.Groups;
@@ -64,8 +66,8 @@ public class ClearCommandGroup : CommandGroup {
         if (!messagesResult.IsDefined(out var messages))
             return Result.FromError(messagesResult);
 
-        var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
-        Messages.Culture = cfg.GetCulture();
+        var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var idList = new List<Snowflake>(messages.Count);
         var builder = new StringBuilder().AppendLine(Mention.Channel(channelId.Value)).AppendLine();
@@ -93,7 +95,8 @@ public class ClearCommandGroup : CommandGroup {
             return Result.FromError(currentUserResult);
 
         var title = string.Format(Messages.MessagesCleared, amount.ToString());
-        if (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value) {
+        if (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
+            && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) {
             var logEmbed = new EmbedBuilder().WithSmallTitle(title, currentUser)
                 .WithDescription(description)
                 .WithActionFooter(user)
@@ -105,9 +108,9 @@ public class ClearCommandGroup : CommandGroup {
                 return Result.FromError(logEmbed);
 
             // Not awaiting to reduce response time
-            if (cfg.PrivateFeedbackChannel != channelId.Value)
+            if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
                 _ = _channelApi.CreateMessageAsync(
-                    cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { logBuilt },
+                    GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { logBuilt },
                     ct: CancellationToken);
         }
 
diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs
index da7b2c5..a095c47 100644
--- a/src/Commands/KickCommandGroup.cs
+++ b/src/Commands/KickCommandGroup.cs
@@ -1,4 +1,6 @@
 using System.ComponentModel;
+using Boyfriend.Data;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using Remora.Commands.Attributes;
 using Remora.Commands.Groups;
@@ -71,8 +73,8 @@ public class KickCommandGroup : CommandGroup {
             return Result.FromError(currentUserResult);
 
         var data = await _dataService.GetData(guildId.Value, CancellationToken);
-        var cfg = data.Configuration;
-        Messages.Culture = cfg.GetCulture();
+        var cfg = data.Settings;
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
         if (!memberResult.IsSuccess) {
@@ -129,8 +131,10 @@ public class KickCommandGroup : CommandGroup {
                     string.Format(Messages.UserKicked, target.GetTag()), target)
                 .WithColour(ColorsList.Green).Build();
 
-            if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
-                || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
+            if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
+                 && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
+                || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
+                    && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
                 var logEmbed = new EmbedBuilder().WithSmallTitle(
                         string.Format(Messages.UserKicked, target.GetTag()), target)
                     .WithDescription(string.Format(Messages.DescriptionActionReason, reason))
@@ -144,14 +148,14 @@ public class KickCommandGroup : CommandGroup {
 
                 var builtArray = new[] { logBuilt };
                 // Not awaiting to reduce response time
-                if (cfg.PublicFeedbackChannel != channelId.Value)
+                if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
                     _ = _channelApi.CreateMessageAsync(
-                        cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                        GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
                         ct: CancellationToken);
-                if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
-                    && cfg.PrivateFeedbackChannel != channelId.Value)
+                if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
+                    && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
                     _ = _channelApi.CreateMessageAsync(
-                        cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                        GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
                         ct: CancellationToken);
             }
         }
diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs
index 764b4f4..791519d 100644
--- a/src/Commands/MuteCommandGroup.cs
+++ b/src/Commands/MuteCommandGroup.cs
@@ -1,5 +1,7 @@
 using System.ComponentModel;
 using System.Text;
+using Boyfriend.Data;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using Remora.Commands.Attributes;
 using Remora.Commands.Groups;
@@ -93,8 +95,8 @@ public class MuteCommandGroup : CommandGroup {
             return Result.FromError(interactionResult);
 
         var data = await _dataService.GetData(guildId.Value, CancellationToken);
-        var cfg = data.Configuration;
-        Messages.Culture = data.Culture;
+        var cfg = data.Settings;
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         Result<Embed> responseEmbed;
         if (interactionResult.Entity is not null) {
@@ -116,8 +118,10 @@ public class MuteCommandGroup : CommandGroup {
                     string.Format(Messages.UserMuted, target.GetTag()), target)
                 .WithColour(ColorsList.Green).Build();
 
-            if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
-                || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
+            if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
+                 && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
+                || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
+                    && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
                 var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason))
                     .Append(
                         string.Format(
@@ -136,14 +140,14 @@ public class MuteCommandGroup : CommandGroup {
 
                 var builtArray = new[] { logBuilt };
                 // Not awaiting to reduce response time
-                if (cfg.PublicFeedbackChannel != channelId.Value)
+                if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
                     _ = _channelApi.CreateMessageAsync(
-                        cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                        GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
                         ct: CancellationToken);
-                if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
-                    && cfg.PrivateFeedbackChannel != channelId.Value)
+                if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
+                    && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
                     _ = _channelApi.CreateMessageAsync(
-                        cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                        GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
                         ct: CancellationToken);
             }
         }
@@ -185,8 +189,8 @@ public class MuteCommandGroup : CommandGroup {
         if (!currentUserResult.IsDefined(out var currentUser))
             return Result.FromError(currentUserResult);
 
-        var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
-        Messages.Culture = cfg.GetCulture();
+        var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
         if (!memberResult.IsSuccess) {
@@ -220,8 +224,10 @@ public class MuteCommandGroup : CommandGroup {
                 string.Format(Messages.UserUnmuted, target.GetTag()), target)
             .WithColour(ColorsList.Green).Build();
 
-        if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
-            || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
+        if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
+             && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
+            || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
+                && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
             var logEmbed = new EmbedBuilder().WithSmallTitle(
                     string.Format(Messages.UserUnmuted, target.GetTag()), target)
                 .WithDescription(string.Format(Messages.DescriptionActionReason, reason))
@@ -236,14 +242,14 @@ public class MuteCommandGroup : CommandGroup {
             var builtArray = new[] { logBuilt };
 
             // Not awaiting to reduce response time
-            if (cfg.PublicFeedbackChannel != channelId.Value)
+            if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
                 _ = _channelApi.CreateMessageAsync(
-                    cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                    GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
                     ct: CancellationToken);
-            if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
-                && cfg.PrivateFeedbackChannel != channelId.Value)
+            if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
+                && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
                 _ = _channelApi.CreateMessageAsync(
-                    cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
+                    GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
                     ct: CancellationToken);
         }
 
diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs
index 45d27e2..f25b435 100644
--- a/src/Commands/PingCommandGroup.cs
+++ b/src/Commands/PingCommandGroup.cs
@@ -1,4 +1,6 @@
 using System.ComponentModel;
+using Boyfriend.Data;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using Remora.Commands.Attributes;
 using Remora.Commands.Groups;
@@ -53,8 +55,8 @@ public class PingCommandGroup : CommandGroup {
         if (!currentUserResult.IsDefined(out var currentUser))
             return Result.FromError(currentUserResult);
 
-        var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
-        Messages.Culture = cfg.GetCulture();
+        var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var latency = _client.Latency.TotalMilliseconds;
         if (latency is 0) {
diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs
index d1c4519..8305ef5 100644
--- a/src/Commands/RemindCommandGroup.cs
+++ b/src/Commands/RemindCommandGroup.cs
@@ -1,5 +1,6 @@
 using System.ComponentModel;
 using Boyfriend.Data;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using Remora.Commands.Attributes;
 using Remora.Commands.Groups;
@@ -57,8 +58,8 @@ public class RemindCommandGroup : CommandGroup {
 
         (await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add(
             new Reminder {
-                RemindAt = remindAt,
-                Channel = channelId.Value,
+                At = remindAt,
+                Channel = channelId.Value.Value,
                 Text = message
             });
 
diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs
index e519d37..61c5786 100644
--- a/src/Commands/SettingsCommandGroup.cs
+++ b/src/Commands/SettingsCommandGroup.cs
@@ -1,7 +1,8 @@
 using System.ComponentModel;
-using System.Reflection;
 using System.Text;
 using Boyfriend.Data;
+using Boyfriend.Data.Options;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using Remora.Commands.Attributes;
 using Remora.Commands.Groups;
@@ -21,6 +22,22 @@ namespace Boyfriend.Commands;
 ///     Handles the commands to list and modify per-guild settings: /settings and /settings list.
 /// </summary>
 public class SettingsCommandGroup : CommandGroup {
+    private static readonly IOption[] AllOptions = {
+        GuildSettings.Language,
+        GuildSettings.WelcomeMessage,
+        GuildSettings.ReceiveStartupMessages,
+        GuildSettings.RemoveRolesOnMute,
+        GuildSettings.ReturnRolesOnRejoin,
+        GuildSettings.AutoStartEvents,
+        GuildSettings.PublicFeedbackChannel,
+        GuildSettings.PrivateFeedbackChannel,
+        GuildSettings.EventNotificationChannel,
+        GuildSettings.DefaultRole,
+        GuildSettings.MuteRole,
+        GuildSettings.EventNotificationRole,
+        GuildSettings.EventEarlyNotificationOffset
+    };
+
     private readonly ICommandContext     _context;
     private readonly GuildDataService    _dataService;
     private readonly FeedbackService     _feedbackService;
@@ -36,7 +53,7 @@ public class SettingsCommandGroup : CommandGroup {
     }
 
     /// <summary>
-    ///     A slash command that lists current per-guild settings.
+    ///     A slash command that lists current per-guild GuildSettings.
     /// </summary>
     /// <returns>
     ///     A feedback sending result which may or may not have succeeded.
@@ -52,19 +69,16 @@ public class SettingsCommandGroup : CommandGroup {
         if (!currentUserResult.IsDefined(out var currentUser))
             return Result.FromError(currentUserResult);
 
-        var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
-        Messages.Culture = cfg.GetCulture();
+        var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var builder = new StringBuilder();
 
-        foreach (var setting in typeof(GuildConfiguration).GetProperties()) {
-            builder.Append(Markdown.InlineCode(setting.Name))
+        foreach (var option in AllOptions) {
+            builder.Append(Markdown.InlineCode(option.Name))
                 .Append(": ");
-            var something = setting.GetValue(cfg);
-            if (something!.GetType() == typeof(List<GuildConfiguration.NotificationReceiver>)) {
-                var list = (something as List<GuildConfiguration.NotificationReceiver>);
-                builder.AppendLine(string.Join(", ", list!.Select(v => Markdown.InlineCode(v.ToString()))));
-            } else { builder.AppendLine(Markdown.InlineCode(something.ToString()!)); }
+            var something = option.GetAsObject(cfg);
+            builder.AppendLine(Markdown.InlineCode(something.ToString()!));
         }
 
         var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser)
@@ -77,7 +91,7 @@ public class SettingsCommandGroup : CommandGroup {
     }
 
     /// <summary>
-    /// A slash command that modifies per-guild settings.
+    /// A slash command that modifies per-guild GuildSettings.
     /// </summary>
     /// <param name="setting">The setting to modify.</param>
     /// <param name="value">The new value of the setting.</param>
@@ -96,40 +110,16 @@ public class SettingsCommandGroup : CommandGroup {
         if (!currentUserResult.IsDefined(out var currentUser))
             return Result.FromError(currentUserResult);
 
-        var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
-        Messages.Culture = cfg.GetCulture();
+        var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
-        PropertyInfo? property = null;
+        var option = AllOptions.Single(
+            o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase));
 
-        try {
-            foreach (var prop in typeof(GuildConfiguration).GetProperties())
-                if (string.Equals(setting, prop.Name, StringComparison.CurrentCultureIgnoreCase))
-                    property = prop;
-            if (property == null || !property.CanWrite)
-                throw new ApplicationException(Messages.SettingDoesntExist);
-            var type = property.PropertyType;
-
-            if (value is "reset" or "default") { property.SetValue(cfg, null); } else if (type == typeof(string)) {
-                if (setting == "language" && value is not ("ru" or "en" or "mctaylors-ru"))
-                    throw new ApplicationException(Messages.LanguageNotSupported);
-                property.SetValue(cfg, value);
-            } else {
-                try {
-                    if (type == typeof(bool))
-                        property.SetValue(cfg, Convert.ToBoolean(value));
-
-                    if (type == typeof(ulong)) {
-                        var id = Convert.ToUInt64(value);
-
-                        property.SetValue(cfg, id);
-                    }
-                } catch (Exception e) when (e is FormatException or OverflowException) {
-                    throw new ApplicationException(Messages.InvalidSettingValue);
-                }
-            }
-        } catch (Exception e) {
+        var setResult = option.Set(cfg, value);
+        if (!setResult.IsSuccess) {
             var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser)
-                .WithDescription(e.Message)
+                .WithDescription(setResult.Error.Message)
                 .WithColour(ColorsList.Red)
                 .Build();
             if (!failedEmbed.IsDefined(out var failedBuilt)) return Result.FromError(failedEmbed);
@@ -139,9 +129,9 @@ public class SettingsCommandGroup : CommandGroup {
 
         var builder = new StringBuilder();
 
-        builder.Append(Markdown.InlineCode(setting))
+        builder.Append(Markdown.InlineCode(option.Name))
             .Append($" {Messages.SettingIsNow} ")
-            .Append(Markdown.InlineCode(value));
+            .Append(Markdown.InlineCode(option.GetAsObject(cfg).ToString()!));
 
         var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser)
             .WithDescription(builder.ToString())
diff --git a/src/Data/GuildConfiguration.cs b/src/Data/GuildConfiguration.cs
deleted file mode 100644
index 440e2b7..0000000
--- a/src/Data/GuildConfiguration.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System.Globalization;
-using Remora.Discord.API.Abstractions.Objects;
-
-namespace Boyfriend.Data;
-
-/// <summary>
-///     Stores per-guild settings that can be set by a member
-///     with <see cref="DiscordPermission.ManageGuild" /> using the /settings command
-/// </summary>
-public class GuildConfiguration {
-    /// <summary>
-    ///     Represents a scheduled event notification receiver.
-    /// </summary>
-    /// <remarks>
-    ///     Used to selectively mention guild members when a scheduled event has started or is about to start.
-    /// </remarks>
-    public enum NotificationReceiver {
-        Interested,
-        Role
-    }
-
-    public static readonly Dictionary<string, CultureInfo> CultureInfoCache = new() {
-        { "en", new CultureInfo("en-US") },
-        { "ru", new CultureInfo("ru-RU") },
-        { "mctaylors-ru", new CultureInfo("tt-RU") }
-    };
-
-    public string Language { get; set; } = "en";
-
-    /// <summary>
-    ///     Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the server.
-    /// </summary>
-    /// <remarks>
-    ///     <list type="bullet">
-    ///         <item>No message will be sent if set to "off", "disable" or "disabled".</item>
-    ///         <item><see cref="Messages.DefaultWelcomeMessage" /> will be sent if set to "default" or "reset"</item>
-    ///     </list>
-    /// </remarks>
-    /// <seealso cref="GuildMemberAddResponder" />
-    public string WelcomeMessage { get; set; } = "default";
-
-    /// <summary>
-    ///     Controls whether or not the <see cref="Messages.Ready" /> message should be sent
-    ///     in <see cref="PrivateFeedbackChannel" /> on startup.
-    /// </summary>
-    /// <seealso cref="GuildCreateResponder" />
-    public bool ReceiveStartupMessages { get; set; }
-
-    public bool RemoveRolesOnMute { get; set; }
-
-    /// <summary>
-    ///     Controls whether or not a guild member's roles are returned if he/she leaves and then joins back.
-    /// </summary>
-    /// <remarks>Roles will not be returned if the member left the guild because of /ban or /kick.</remarks>
-    public bool ReturnRolesOnRejoin { get; set; }
-
-    public bool AutoStartEvents { get; set; }
-
-    /// <summary>
-    ///     Controls what channel should all public messages be sent to.
-    /// </summary>
-    public ulong PublicFeedbackChannel { get; set; }
-
-    /// <summary>
-    ///     Controls what channel should all private, moderator-only messages be sent to.
-    /// </summary>
-    public ulong PrivateFeedbackChannel { get; set; }
-
-    public ulong EventNotificationChannel { get; set; }
-    public ulong DefaultRole              { get; set; }
-    public ulong MuteRole                 { get; set; }
-    public ulong EventNotificationRole    { get; set; }
-
-    /// <summary>
-    ///     Controls what guild members should be mentioned when a scheduled event has started or is about to start.
-    /// </summary>
-    /// <seealso cref="NotificationReceiver" />
-    public List<NotificationReceiver> EventStartedReceivers { get; set; }
-        = new() { NotificationReceiver.Interested, NotificationReceiver.Role };
-
-    /// <summary>
-    ///     Controls the amount of time before a scheduled event to send a reminder in <see cref="EventNotificationChannel" />.
-    /// </summary>
-    public TimeSpan EventEarlyNotificationOffset { get; set; } = TimeSpan.Zero;
-
-    // Do not convert this to a property, else serialization will be attempted
-    public CultureInfo GetCulture() {
-        return CultureInfoCache[Language];
-    }
-}
diff --git a/src/Data/GuildData.cs b/src/Data/GuildData.cs
index 992adc0..7c81364 100644
--- a/src/Data/GuildData.cs
+++ b/src/Data/GuildData.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Text.Json.Nodes;
 using Remora.Rest.Core;
 
 namespace Boyfriend.Data;
@@ -8,29 +8,26 @@ namespace Boyfriend.Data;
 /// </summary>
 /// <remarks>This information is stored on disk as a JSON file.</remarks>
 public class GuildData {
-    public readonly GuildConfiguration Configuration;
-    public readonly string             ConfigurationPath;
-
     public readonly Dictionary<ulong, MemberData> MemberData;
     public readonly string                        MemberDataPath;
 
     public readonly Dictionary<ulong, ScheduledEventData> ScheduledEvents;
     public readonly string                                ScheduledEventsPath;
+    public readonly JsonNode                              Settings;
+    public readonly string                                SettingsPath;
 
     public GuildData(
-        GuildConfiguration                    configuration,   string configurationPath,
+        JsonNode                              settings,        string settingsPath,
         Dictionary<ulong, ScheduledEventData> scheduledEvents, string scheduledEventsPath,
         Dictionary<ulong, MemberData>         memberData,      string memberDataPath) {
-        Configuration = configuration;
-        ConfigurationPath = configurationPath;
+        Settings = settings;
+        SettingsPath = settingsPath;
         ScheduledEvents = scheduledEvents;
         ScheduledEventsPath = scheduledEventsPath;
         MemberData = memberData;
         MemberDataPath = memberDataPath;
     }
 
-    public CultureInfo Culture => Configuration.GetCulture();
-
     public MemberData GetMemberData(Snowflake userId) {
         if (MemberData.TryGetValue(userId.Value, out var existing)) return existing;
 
diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs
new file mode 100644
index 0000000..fc76f21
--- /dev/null
+++ b/src/Data/GuildSettings.cs
@@ -0,0 +1,63 @@
+using Boyfriend.Data.Options;
+using Boyfriend.locale;
+using Remora.Discord.API.Abstractions.Objects;
+
+namespace Boyfriend.Data;
+
+/// <summary>
+///     Contains all per-guild settings that can be set by a member
+///     with <see cref="DiscordPermission.ManageGuild" /> using the /settings command
+/// </summary>
+public static class GuildSettings {
+    public static readonly LanguageOption Language = new("Language", "en");
+
+    /// <summary>
+    ///     Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the server.
+    /// </summary>
+    /// <remarks>
+    ///     <list type="bullet">
+    ///         <item>No message will be sent if set to "off", "disable" or "disabled".</item>
+    ///         <item><see cref="Messages.DefaultWelcomeMessage" /> will be sent if set to "default" or "reset"</item>
+    ///     </list>
+    /// </remarks>
+    /// <seealso cref="GuildMemberAddResponder" />
+    public static readonly Option<string> WelcomeMessage = new("WelcomeMessage", "default");
+
+    /// <summary>
+    ///     Controls whether or not the <see cref="Messages.Ready" /> message should be sent
+    ///     in <see cref="PrivateFeedbackChannel" /> on startup.
+    /// </summary>
+    /// <seealso cref="GuildCreateResponder" />
+    public static readonly BoolOption ReceiveStartupMessages = new("ReceiveStartupMessages", false);
+
+    public static readonly BoolOption RemoveRolesOnMute = new("RemoveRolesOnMute", false);
+
+    /// <summary>
+    ///     Controls whether or not a guild member's roles are returned if he/she leaves and then joins back.
+    /// </summary>
+    /// <remarks>Roles will not be returned if the member left the guild because of /ban or /kick.</remarks>
+    public static readonly BoolOption ReturnRolesOnRejoin = new("ReturnRolesOnRejoin", false);
+
+    public static readonly BoolOption AutoStartEvents = new("AutoStartEvents", false);
+
+    /// <summary>
+    ///     Controls what channel should all public messages be sent to.
+    /// </summary>
+    public static readonly SnowflakeOption PublicFeedbackChannel = new("PublicFeedbackChannel");
+
+    /// <summary>
+    ///     Controls what channel should all private, moderator-only messages be sent to.
+    /// </summary>
+    public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel");
+
+    public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel");
+    public static readonly SnowflakeOption DefaultRole              = new("DefaultRole");
+    public static readonly SnowflakeOption MuteRole                 = new("MuteRole");
+    public static readonly SnowflakeOption EventNotificationRole    = new("EventNotificationRole");
+
+    /// <summary>
+    ///     Controls the amount of time before a scheduled event to send a reminder in <see cref="EventNotificationChannel" />.
+    /// </summary>
+    public static readonly TimeSpanOption EventEarlyNotificationOffset = new(
+        "EventEarlyNotificationOffset", TimeSpan.Zero);
+}
diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs
index 72cbdec..7d49ec7 100644
--- a/src/Data/MemberData.cs
+++ b/src/Data/MemberData.cs
@@ -1,5 +1,3 @@
-using Remora.Rest.Core;
-
 namespace Boyfriend.Data;
 
 /// <summary>
@@ -13,6 +11,6 @@ public class MemberData {
 
     public ulong           Id          { get; }
     public DateTimeOffset? BannedUntil { get; set; }
-    public List<Snowflake> Roles       { get; set; } = new();
+    public List<ulong>     Roles       { get; set; } = new();
     public List<Reminder>  Reminders   { get; }      = new();
 }
diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs
new file mode 100644
index 0000000..0a3992c
--- /dev/null
+++ b/src/Data/Options/BoolOption.cs
@@ -0,0 +1,31 @@
+using System.Text.Json.Nodes;
+using Boyfriend.locale;
+using Remora.Results;
+
+namespace Boyfriend.Data.Options;
+
+public class BoolOption : Option<bool> {
+    public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { }
+
+    public override Result Set(JsonNode settings, string from) {
+        if (!TryParseBool(from, out var value))
+            return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue));
+
+        settings[Name] = value;
+        return Result.FromSuccess();
+    }
+
+    private static bool TryParseBool(string from, out bool value) {
+        value = false;
+        switch (from) {
+            case "1" or "y" or "yes" or "д" or "да":
+                value = true;
+                return true;
+            case "0" or "n" or "no" or "н" or "не" or "нет":
+                value = false;
+                return true;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/src/Data/Options/IOption.cs b/src/Data/Options/IOption.cs
new file mode 100644
index 0000000..f6acf9a
--- /dev/null
+++ b/src/Data/Options/IOption.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Nodes;
+using Remora.Results;
+
+namespace Boyfriend.Data.Options;
+
+public interface IOption {
+    string Name { get; init; }
+    object GetAsObject(JsonNode settings);
+    Result Set(JsonNode         settings, string from);
+}
diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs
new file mode 100644
index 0000000..7e741b2
--- /dev/null
+++ b/src/Data/Options/LanguageOption.cs
@@ -0,0 +1,31 @@
+using System.Globalization;
+using System.Text.Json.Nodes;
+using Boyfriend.locale;
+using Remora.Results;
+
+namespace Boyfriend.Data.Options;
+
+/// <inheritdoc />
+public class LanguageOption : Option<CultureInfo> {
+    private static readonly Dictionary<string, CultureInfo> CultureInfoCache = new() {
+        { "en", new CultureInfo("en-US") },
+        { "ru", new CultureInfo("ru-RU") },
+        { "mctaylors-ru", new CultureInfo("tt-RU") }
+    };
+
+    public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { }
+
+    /// <inheritdoc />
+    public override CultureInfo Get(JsonNode settings) {
+        var property = settings[Name];
+        return property != null ? CultureInfoCache[property.GetValue<string>()] : DefaultValue;
+    }
+
+    /// <inheritdoc />
+    public override Result Set(JsonNode settings, string from) {
+        if (!CultureInfoCache.ContainsKey(from.ToLowerInvariant()))
+            return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported));
+
+        return base.Set(settings, from.ToLowerInvariant());
+    }
+}
diff --git a/src/Data/Options/Option.cs b/src/Data/Options/Option.cs
new file mode 100644
index 0000000..a08b75e
--- /dev/null
+++ b/src/Data/Options/Option.cs
@@ -0,0 +1,45 @@
+using System.Text.Json.Nodes;
+using Remora.Results;
+
+namespace Boyfriend.Data.Options;
+
+/// <summary>
+///     Represents an per-guild option.
+/// </summary>
+/// <typeparam name="T">The type of the option.</typeparam>
+public class Option<T> : IOption {
+    internal readonly T DefaultValue;
+
+    public Option(string name, T defaultValue) {
+        Name = name;
+        DefaultValue = defaultValue;
+    }
+
+    public Type   Type { get; set; } = typeof(T);
+    public string Name { get; init; }
+
+    public object GetAsObject(JsonNode settings) {
+        return Get(settings)!;
+    }
+
+    /// <summary>
+    ///     Sets the value of the option from a <see cref="string" /> to the provided JsonNode.
+    /// </summary>
+    /// <param name="settings">The <see cref="JsonNode" /> to set the value to.</param>
+    /// <param name="from">The string from which the new value of the option will be parsed.</param>
+    /// <returns>A value setting result which may or may not have succeeded.</returns>
+    public virtual Result Set(JsonNode settings, string from) {
+        settings[Name] = from;
+        return Result.FromSuccess();
+    }
+
+    /// <summary>
+    ///     Gets the value of the option from the provided <paramref name="settings" />.
+    /// </summary>
+    /// <param name="settings">The <see cref="JsonNode" /> to get the value from.</param>
+    /// <returns>The value of the option.</returns>
+    public virtual T Get(JsonNode settings) {
+        var property = settings[Name];
+        return property != null ? property.GetValue<T>() : DefaultValue;
+    }
+}
diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs
new file mode 100644
index 0000000..d50f833
--- /dev/null
+++ b/src/Data/Options/SnowflakeOption.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Nodes;
+using Boyfriend.locale;
+using Remora.Rest.Core;
+using Remora.Results;
+
+namespace Boyfriend.Data.Options;
+
+public class SnowflakeOption : Option<Snowflake> {
+    public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { }
+
+    public override Snowflake Get(JsonNode settings) {
+        var property = settings[Name];
+        return property != null ? property.GetValue<ulong>().ToSnowflake() : DefaultValue;
+    }
+
+    public override Result Set(JsonNode settings, string from) {
+        if (!ulong.TryParse(from, out var parsed))
+            return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue));
+
+        settings[Name] = parsed;
+        return Result.FromSuccess();
+    }
+}
diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs
new file mode 100644
index 0000000..aa3c8d4
--- /dev/null
+++ b/src/Data/Options/TimeSpanOption.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Nodes;
+using Boyfriend.locale;
+using Remora.Commands.Parsers;
+using Remora.Results;
+
+namespace Boyfriend.Data.Options;
+
+public class TimeSpanOption : Option<TimeSpan> {
+    private static readonly TimeSpanParser Parser = new();
+
+    public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { }
+
+    public override Result Set(JsonNode settings, string from) {
+        if (!Parser.TryParseAsync(from).Result.IsDefined(out var span))
+            return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue));
+
+        settings[Name] = span.ToString();
+        return Result.FromSuccess();
+    }
+}
diff --git a/src/Data/Reminder.cs b/src/Data/Reminder.cs
index 1d0410c..2246b5e 100644
--- a/src/Data/Reminder.cs
+++ b/src/Data/Reminder.cs
@@ -1,9 +1,7 @@
-using Remora.Rest.Core;
-
 namespace Boyfriend.Data;
 
 public struct Reminder {
-    public DateTimeOffset RemindAt;
+    public DateTimeOffset At;
     public string         Text;
-    public Snowflake      Channel;
+    public ulong          Channel;
 }
diff --git a/src/EventResponders.cs b/src/EventResponders.cs
index 708bbc1..19b498d 100644
--- a/src/EventResponders.cs
+++ b/src/EventResponders.cs
@@ -1,4 +1,5 @@
 using Boyfriend.Data;
+using Boyfriend.locale;
 using Boyfriend.Services;
 using DiffPlex.DiffBuilder;
 using Microsoft.Extensions.Logging;
@@ -19,7 +20,7 @@ namespace Boyfriend;
 
 /// <summary>
 ///     Handles sending a <see cref="Messages.Ready" /> message to a guild that has just initialized if that guild
-///     has <see cref="GuildConfiguration.ReceiveStartupMessages" /> enabled
+///     has <see cref="GuildSettings.ReceiveStartupMessages" /> enabled
 /// </summary>
 public class GuildCreateResponder : IResponder<IGuildCreate> {
     private readonly IDiscordRestChannelAPI        _channelApi;
@@ -42,16 +43,16 @@ public class GuildCreateResponder : IResponder<IGuildCreate> {
         var guild = gatewayEvent.Guild.AsT0;
         _logger.LogInformation("Joined guild \"{Name}\"", guild.Name);
 
-        var guildConfig = await _dataService.GetConfiguration(guild.ID, ct);
-        if (!guildConfig.ReceiveStartupMessages)
+        var cfg = await _dataService.GetSettings(guild.ID, ct);
+        if (!GuildSettings.ReceiveStartupMessages.Get(cfg))
             return Result.FromSuccess();
-        if (guildConfig.PrivateFeedbackChannel is 0)
+        if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
             return Result.FromSuccess();
 
         var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
         if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
 
-        Messages.Culture = guildConfig.GetCulture();
+        Messages.Culture = GuildSettings.Language.Get(cfg);
         var i = Random.Shared.Next(1, 4);
 
         var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser)
@@ -63,13 +64,13 @@ public class GuildCreateResponder : IResponder<IGuildCreate> {
         if (!embed.IsDefined(out var built)) return Result.FromError(embed);
 
         return (Result)await _channelApi.CreateMessageAsync(
-            guildConfig.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, ct: ct);
+            GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct);
     }
 }
 
 /// <summary>
 ///     Handles logging the contents of a deleted message and the user who deleted the message
-///     to a guild's <see cref="GuildConfiguration.PrivateFeedbackChannel" /> if one is set.
+///     to a guild's <see cref="GuildSettings.PrivateFeedbackChannel" /> if one is set.
 /// </summary>
 public class MessageDeletedResponder : IResponder<IMessageDelete> {
     private readonly IDiscordRestAuditLogAPI _auditLogApi;
@@ -89,8 +90,8 @@ public class MessageDeletedResponder : IResponder<IMessageDelete> {
     public async Task<Result> RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) {
         if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess();
 
-        var guildConfiguration = await _dataService.GetConfiguration(guildId, ct);
-        if (guildConfiguration.PrivateFeedbackChannel is 0) return Result.FromSuccess();
+        var cfg = await _dataService.GetSettings(guildId, ct);
+        if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) return Result.FromSuccess();
 
         var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct);
         if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult);
@@ -111,7 +112,7 @@ public class MessageDeletedResponder : IResponder<IMessageDelete> {
             if (!userResult.IsDefined(out user)) return Result.FromError(userResult);
         }
 
-        Messages.Culture = guildConfiguration.GetCulture();
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var embed = new EmbedBuilder()
             .WithSmallTitle(
@@ -127,14 +128,14 @@ public class MessageDeletedResponder : IResponder<IMessageDelete> {
         if (!embed.IsDefined(out var built)) return Result.FromError(embed);
 
         return (Result)await _channelApi.CreateMessageAsync(
-            guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built },
+            GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built },
             allowedMentions: Boyfriend.NoMentions, ct: ct);
     }
 }
 
 /// <summary>
 ///     Handles logging the difference between an edited message's old and new content
-///     to a guild's <see cref="GuildConfiguration.PrivateFeedbackChannel" /> if one is set.
+///     to a guild's <see cref="GuildSettings.PrivateFeedbackChannel" /> if one is set.
 /// </summary>
 public class MessageEditedResponder : IResponder<IMessageUpdate> {
     private readonly CacheService           _cacheService;
@@ -154,8 +155,8 @@ public class MessageEditedResponder : IResponder<IMessageUpdate> {
     public async Task<Result> RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) {
         if (!gatewayEvent.GuildID.IsDefined(out var guildId))
             return Result.FromSuccess();
-        var guildConfiguration = await _dataService.GetConfiguration(guildId, ct);
-        if (guildConfiguration.PrivateFeedbackChannel is 0)
+        var cfg = await _dataService.GetSettings(guildId, ct);
+        if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
             return Result.FromSuccess();
         if (!gatewayEvent.Content.IsDefined(out var newContent))
             return Result.FromSuccess();
@@ -189,7 +190,7 @@ public class MessageEditedResponder : IResponder<IMessageUpdate> {
 
         var diff = InlineDiffBuilder.Diff(message.Content, newContent);
 
-        Messages.Culture = guildConfiguration.GetCulture();
+        Messages.Culture = GuildSettings.Language.Get(cfg);
 
         var embed = new EmbedBuilder()
             .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author)
@@ -201,16 +202,16 @@ public class MessageEditedResponder : IResponder<IMessageUpdate> {
         if (!embed.IsDefined(out var built)) return Result.FromError(embed);
 
         return (Result)await _channelApi.CreateMessageAsync(
-            guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built },
+            GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built },
             allowedMentions: Boyfriend.NoMentions, ct: ct);
     }
 }
 
 /// <summary>
-///     Handles sending a guild's <see cref="GuildConfiguration.WelcomeMessage" /> if one is set.
-///     If <see cref="GuildConfiguration.ReturnRolesOnRejoin"/> is enabled, roles will be returned.
+///     Handles sending a guild's <see cref="GuildSettings.WelcomeMessage" /> if one is set.
+///     If <see cref="GuildSettings.ReturnRolesOnRejoin"/> is enabled, roles will be returned.
 /// </summary>
-/// <seealso cref="GuildConfiguration.WelcomeMessage" />
+/// <seealso cref="GuildSettings.WelcomeMessage" />
 public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> {
     private readonly IDiscordRestChannelAPI _channelApi;
     private readonly GuildDataService       _dataService;
@@ -227,19 +228,21 @@ public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> {
         if (!gatewayEvent.User.IsDefined(out var user))
             return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User)));
         var data = await _dataService.GetData(gatewayEvent.GuildID, ct);
-        var cfg = data.Configuration;
-        if (cfg.PublicFeedbackChannel is 0 || cfg.WelcomeMessage is "off" or "disable" or "disabled")
+        var cfg = data.Settings;
+        if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
+            || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled")
             return Result.FromSuccess();
-        if (cfg.ReturnRolesOnRejoin) {
+        if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) {
             var result = await _guildApi.ModifyGuildMemberAsync(
-                gatewayEvent.GuildID, user.ID, roles: data.GetMemberData(user.ID).Roles, ct: ct);
+                gatewayEvent.GuildID, user.ID,
+                roles: data.GetMemberData(user.ID).Roles.ConvertAll(r => r.ToSnowflake()), ct: ct);
             if (!result.IsSuccess) return Result.FromError(result.Error);
         }
 
-        Messages.Culture = data.Culture;
-        var welcomeMessage = cfg.WelcomeMessage is "default" or "reset"
+        Messages.Culture = GuildSettings.Language.Get(cfg);
+        var welcomeMessage = GuildSettings.WelcomeMessage.Get(cfg) is "default" or "reset"
             ? Messages.DefaultWelcomeMessage
-            : cfg.WelcomeMessage;
+            : GuildSettings.WelcomeMessage.Get(cfg);
 
         var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct);
         if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult);
@@ -253,14 +256,14 @@ public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> {
         if (!embed.IsDefined(out var built)) return Result.FromError(embed);
 
         return (Result)await _channelApi.CreateMessageAsync(
-            cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built },
+            GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built },
             allowedMentions: Boyfriend.NoMentions, ct: ct);
     }
 }
 
 /// <summary>
 ///     Handles sending a notification when a scheduled event has been cancelled
-///     in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
+///     in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
 /// </summary>
 public class GuildScheduledEventDeleteResponder : IResponder<IGuildScheduledEventDelete> {
     private readonly IDiscordRestChannelAPI _channelApi;
@@ -275,7 +278,7 @@ public class GuildScheduledEventDeleteResponder : IResponder<IGuildScheduledEven
         var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct);
         guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value);
 
-        if (guildData.Configuration.EventNotificationChannel is 0)
+        if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty())
             return Result.FromSuccess();
 
         var embed = new EmbedBuilder()
@@ -288,7 +291,7 @@ public class GuildScheduledEventDeleteResponder : IResponder<IGuildScheduledEven
         if (!embed.IsDefined(out var built)) return Result.FromError(embed);
 
         return (Result)await _channelApi.CreateMessageAsync(
-            guildData.Configuration.EventNotificationChannel.ToDiscordSnowflake(), embeds: new[] { built }, ct: ct);
+            GuildSettings.EventNotificationChannel.Get(guildData.Settings), embeds: new[] { built }, ct: ct);
     }
 }
 
@@ -304,7 +307,7 @@ public class GuildMemberUpdateResponder : IResponder<IGuildMemberUpdate> {
 
     public async Task<Result> RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) {
         var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct);
-        memberData.Roles = gatewayEvent.Roles.ToList();
+        memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value);
         return Result.FromSuccess();
     }
 }
diff --git a/src/Extensions.cs b/src/Extensions.cs
index 95500ac..0f9a786 100644
--- a/src/Extensions.cs
+++ b/src/Extensions.cs
@@ -1,6 +1,7 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Net;
 using System.Text;
+using Boyfriend.locale;
 using DiffPlex.DiffBuilder.Model;
 using Remora.Discord.API;
 using Remora.Discord.API.Abstractions.Objects;
@@ -170,10 +171,11 @@ public static class Extensions {
         return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}";
     }
 
-    public static Snowflake ToDiscordSnowflake(this ulong id) {
+    public static Snowflake ToSnowflake(this ulong id) {
         return DiscordSnowflake.New(id);
     }
 
+
     public static TResult? MaxOrDefault<TSource, TResult>(
         this IEnumerable<TSource> source, Func<TSource, TResult> selector) {
         var list = source.ToList();
@@ -190,4 +192,8 @@ public static class Extensions {
                && context.TryGetChannelID(out channelId)
                && context.TryGetUserID(out userId);
     }
+
+    public static bool Empty(this Snowflake snowflake) {
+        return snowflake.Value is 0;
+    }
 }
diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs
index be873f1..990e731 100644
--- a/src/Services/GuildDataService.cs
+++ b/src/Services/GuildDataService.cs
@@ -1,5 +1,6 @@
 using System.Collections.Concurrent;
 using System.Text.Json;
+using System.Text.Json.Nodes;
 using Boyfriend.Data;
 using Microsoft.Extensions.Hosting;
 using Remora.Discord.API.Abstractions.Rest;
@@ -36,8 +37,8 @@ public class GuildDataService : IHostedService {
     private async Task SaveAsync(CancellationToken ct) {
         var tasks = new List<Task>();
         foreach (var data in _datas.Values) {
-            await using var configStream = File.OpenWrite(data.ConfigurationPath);
-            tasks.Add(JsonSerializer.SerializeAsync(configStream, data.Configuration, cancellationToken: ct));
+            await using var settingsStream = File.OpenWrite(data.SettingsPath);
+            tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct));
 
             await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath);
             tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct));
@@ -58,17 +59,16 @@ public class GuildDataService : IHostedService {
     private async Task<GuildData> InitializeData(Snowflake guildId, CancellationToken ct = default) {
         var idString = $"{guildId}";
         var memberDataPath = $"{guildId}/MemberData";
-        var configurationPath = $"{guildId}/Configuration.json";
+        var settingsPath = $"{guildId}/Settings.json";
         var scheduledEventsPath = $"{guildId}/ScheduledEvents.json";
         if (!Directory.Exists(idString)) Directory.CreateDirectory(idString);
         if (!Directory.Exists(memberDataPath)) Directory.CreateDirectory(memberDataPath);
-        if (!File.Exists(configurationPath)) await File.WriteAllTextAsync(configurationPath, "{}", ct);
+        if (!File.Exists(settingsPath)) await File.WriteAllTextAsync(settingsPath, "{}", ct);
         if (!File.Exists(scheduledEventsPath)) await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct);
 
-        await using var configurationStream = File.OpenRead(configurationPath);
-        var configuration
-            = JsonSerializer.DeserializeAsync<GuildConfiguration>(
-                configurationStream, cancellationToken: ct);
+        await using var settingsStream = File.OpenRead(settingsPath);
+        var jsonSettings
+            = JsonNode.Parse(settingsStream);
 
         await using var eventsStream = File.OpenRead(scheduledEventsPath);
         var events
@@ -80,23 +80,23 @@ public class GuildDataService : IHostedService {
             await using var dataStream = File.OpenRead(dataPath);
             var data = await JsonSerializer.DeserializeAsync<MemberData>(dataStream, cancellationToken: ct);
             if (data is null) continue;
-            var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToDiscordSnowflake(), ct);
+            var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct);
             if (memberResult.IsSuccess)
-                data.Roles = memberResult.Entity.Roles.ToList();
+                data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value);
 
             memberData.Add(data.Id, data);
         }
 
         var finalData = new GuildData(
-            await configuration ?? new GuildConfiguration(), configurationPath,
+            jsonSettings ?? new JsonObject(), settingsPath,
             await events ?? new Dictionary<ulong, ScheduledEventData>(), scheduledEventsPath,
             memberData, memberDataPath);
         while (!_datas.ContainsKey(guildId)) _datas.TryAdd(guildId, finalData);
         return finalData;
     }
 
-    public async Task<GuildConfiguration> GetConfiguration(Snowflake guildId, CancellationToken ct = default) {
-        return (await GetData(guildId, ct)).Configuration;
+    public async Task<JsonNode> GetSettings(Snowflake guildId, CancellationToken ct = default) {
+        return (await GetData(guildId, ct)).Settings;
     }
 
     public async Task<MemberData> GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) {
diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs
index 3d55f07..69ccc93 100644
--- a/src/Services/GuildUpdateService.cs
+++ b/src/Services/GuildUpdateService.cs
@@ -1,4 +1,6 @@
+using System.Text.Json.Nodes;
 using Boyfriend.Data;
+using Boyfriend.locale;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Remora.Discord.API.Abstractions.Objects;
@@ -94,9 +96,9 @@ public class GuildUpdateService : BackgroundService {
     ///     This method does the following:
     ///     <list type="bullet">
     ///         <item>Automatically unbans users once their ban period has expired.</item>
-    ///         <item>Automatically grants members the guild's <see cref="GuildConfiguration.DefaultRole"/> if one is set.</item>
+    ///         <item>Automatically grants members the guild's <see cref="GuildSettings.DefaultRole"/> if one is set.</item>
     ///         <item>Sends reminders about an upcoming scheduled event.</item>
-    ///         <item>Automatically starts scheduled events if <see cref="GuildConfiguration.AutoStartEvents"/> is enabled.</item>
+    ///         <item>Automatically starts scheduled events if <see cref="GuildSettings.AutoStartEvents"/> is enabled.</item>
     ///         <item>Sends scheduled event start notifications.</item>
     ///         <item>Sends scheduled event completion notifications.</item>
     ///         <item>Sends reminders to members.</item>
@@ -114,15 +116,15 @@ public class GuildUpdateService : BackgroundService {
     /// <param name="ct">The cancellation token for this operation.</param>
     private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) {
         var data = await _dataService.GetData(guildId, ct);
-        Messages.Culture = data.Culture;
-        var defaultRoleSnowflake = data.Configuration.DefaultRole.ToDiscordSnowflake();
+        Messages.Culture = GuildSettings.Language.Get(data.Settings);
+        var defaultRole = GuildSettings.DefaultRole.Get(data.Settings);
 
         foreach (var memberData in data.MemberData.Values) {
-            var userId = memberData.Id.ToDiscordSnowflake();
+            var userId = memberData.Id.ToSnowflake();
 
-            if (defaultRoleSnowflake.Value is not 0 && !memberData.Roles.Contains(defaultRoleSnowflake))
+            if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value))
                 _ = _guildApi.AddGuildMemberRoleAsync(
-                    guildId, userId, defaultRoleSnowflake, ct: ct);
+                    guildId, userId, defaultRole, ct: ct);
 
             if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
                 var unbanResult = await _guildApi.RemoveGuildBanAsync(
@@ -139,7 +141,7 @@ public class GuildUpdateService : BackgroundService {
 
             for (var i = memberData.Reminders.Count - 1; i >= 0; i--) {
                 var reminder = memberData.Reminders[i];
-                if (DateTimeOffset.UtcNow < reminder.RemindAt) continue;
+                if (DateTimeOffset.UtcNow < reminder.At) continue;
 
                 var embed = new EmbedBuilder().WithSmallTitle(
                         string.Format(Messages.Reminder, user.GetTag()), user)
@@ -151,7 +153,7 @@ public class GuildUpdateService : BackgroundService {
                 if (!embed.IsDefined(out var built)) continue;
 
                 var messageResult = await _channelApi.CreateMessageAsync(
-                    reminder.Channel, Mention.User(user), embeds: new[] { built }, ct: ct);
+                    reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct);
                 if (!messageResult.IsSuccess)
                     _logger.LogWarning(
                         "Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message);
@@ -163,7 +165,7 @@ public class GuildUpdateService : BackgroundService {
         var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
         if (!eventsResult.IsDefined(out var events)) return;
 
-        if (data.Configuration.EventNotificationChannel is 0) return;
+        if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) return;
 
         foreach (var scheduledEvent in events) {
             if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) {
@@ -172,7 +174,7 @@ public class GuildUpdateService : BackgroundService {
                 var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value];
                 if (storedEvent.Status == scheduledEvent.Status) {
                     if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) {
-                        if (data.Configuration.AutoStartEvents
+                        if (GuildSettings.AutoStartEvents.Get(data.Settings)
                             && scheduledEvent.Status is not GuildScheduledEventStatus.Active) {
                             var startResult = await _eventApi.ModifyGuildScheduledEventAsync(
                                 guildId, scheduledEvent.ID,
@@ -182,10 +184,11 @@ public class GuildUpdateService : BackgroundService {
                                     "Error in automatic scheduled event start request.\n{ErrorMessage}",
                                     startResult.Error.Message);
                         }
-                    } else if (data.Configuration.EventEarlyNotificationOffset != TimeSpan.Zero
+                    } else if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) != TimeSpan.Zero
                                && !storedEvent.EarlyNotificationSent
                                && DateTimeOffset.UtcNow
-                               >= scheduledEvent.ScheduledStartTime - data.Configuration.EventEarlyNotificationOffset) {
+                               >= scheduledEvent.ScheduledStartTime
+                               - GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) {
                         var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct);
                         if (earlyResult.IsSuccess)
                             storedEvent.EarlyNotificationSent = true;
@@ -203,7 +206,7 @@ public class GuildUpdateService : BackgroundService {
 
             var result = scheduledEvent.Status switch {
                 GuildScheduledEventStatus.Scheduled =>
-                    await SendScheduledEventCreatedMessage(scheduledEvent, data.Configuration, ct),
+                    await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct),
                 GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed =>
                     await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct),
                 _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)))
@@ -215,17 +218,17 @@ public class GuildUpdateService : BackgroundService {
     }
 
     /// <summary>
-    ///     Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventNotificationRole" /> if one is
+    ///     Handles sending a notification, mentioning the <see cref="GuildSettings.EventNotificationRole" /> if one is
     ///     set,
     ///     when a scheduled event is created
-    ///     in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
+    ///     in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
     /// </summary>
     /// <param name="scheduledEvent">The scheduled event that has just been created.</param>
-    /// <param name="config">The configuration of the guild containing the scheduled event.</param>
+    /// <param name="settings">The settings of the guild containing the scheduled event.</param>
     /// <param name="ct">The cancellation token for this operation.</param>
     /// <returns>A notification sending result which may or may not have succeeded.</returns>
     private async Task<Result> SendScheduledEventCreatedMessage(
-        IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) {
+        IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) {
         var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
         if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
 
@@ -281,8 +284,8 @@ public class GuildUpdateService : BackgroundService {
             .Build();
         if (!embed.IsDefined(out var built)) return Result.FromError(embed);
 
-        var roleMention = config.EventNotificationRole is not 0
-            ? Mention.Role(config.EventNotificationRole.ToDiscordSnowflake())
+        var roleMention = !GuildSettings.EventNotificationRole.Get(settings).Empty()
+            ? Mention.Role(GuildSettings.EventNotificationRole.Get(settings))
             : string.Empty;
 
         var button = new ButtonComponent(
@@ -294,14 +297,14 @@ public class GuildUpdateService : BackgroundService {
         );
 
         return (Result)await _channelApi.CreateMessageAsync(
-            config.EventNotificationChannel.ToDiscordSnowflake(), roleMention, embeds: new[] { built },
+            GuildSettings.EventNotificationChannel.Get(settings), roleMention, embeds: new[] { built },
             components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct);
     }
 
     /// <summary>
-    ///     Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventStartedReceivers" />s,
+    ///     Handles sending a notification, mentioning the <see cref="GuildSettings.EventStartedReceivers" />s,
     ///     when a scheduled event is about to start, has started or completed
-    ///     in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
+    ///     in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
     /// </summary>
     /// <param name="scheduledEvent">The scheduled event that is about to start, has started or completed.</param>
     /// <param name="data">The data for the guild containing the scheduled event.</param>
@@ -353,7 +356,7 @@ public class GuildUpdateService : BackgroundService {
                     }
 
                     var contentResult = await _utility.GetEventNotificationMentions(
-                        scheduledEvent, data.Configuration, ct);
+                        scheduledEvent, data.Settings, ct);
                     if (!contentResult.IsDefined(out content))
                         return Result.FromError(contentResult);
 
@@ -383,7 +386,7 @@ public class GuildUpdateService : BackgroundService {
         if (!result.IsDefined(out var built)) return Result.FromError(result);
 
         return (Result)await _channelApi.CreateMessageAsync(
-            data.Configuration.EventNotificationChannel.ToDiscordSnowflake(),
+            GuildSettings.EventNotificationChannel.Get(data.Settings),
             content ?? default(Optional<string>), embeds: new[] { built }, ct: ct);
     }
 }
diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs
index b4ff6fb..6279ae9 100644
--- a/src/Services/UtilityService.cs
+++ b/src/Services/UtilityService.cs
@@ -1,4 +1,5 @@
 using System.Text;
+using System.Text.Json.Nodes;
 using Boyfriend.Data;
 using Microsoft.Extensions.Hosting;
 using Remora.Discord.API.Abstractions.Objects;
@@ -103,38 +104,37 @@ public class UtilityService : IHostedService {
     }
 
     /// <summary>
-    ///     Gets the string mentioning all <see cref="GuildConfiguration.NotificationReceiver" />s related to a scheduled
+    ///     Gets the string mentioning all <see cref="GuildSettings.NotificationReceiver" />s related to a scheduled
     ///     event.
     /// </summary>
     /// <remarks>
-    ///     If the guild configuration enables <see cref="GuildConfiguration.NotificationReceiver.Role" />, then the
-    ///     <see cref="GuildConfiguration.EventNotificationRole" /> will also be mentioned.
+    ///     If the guild settings enables <see cref="GuildSettings.NotificationReceiver.Role" />, then the
+    ///     <see cref="GuildSettings.EventNotificationRole" /> will also be mentioned.
     /// </remarks>
     /// <param name="scheduledEvent">
-    ///     The scheduled event whose subscribers will be mentioned if the guild configuration enables
-    ///     <see cref="GuildConfiguration.NotificationReceiver.Interested" />.
+    ///     The scheduled event whose subscribers will be mentioned if the guild settings enables
+    ///     <see cref="GuildSettings.NotificationReceiver.Interested" />.
     /// </param>
-    /// <param name="config">The configuration of the guild containing the scheduled event</param>
+    /// <param name="settings">The settings of the guild containing the scheduled event</param>
     /// <param name="ct">The cancellation token for this operation.</param>
     /// <returns>A result containing the string which may or may not have succeeded.</returns>
     public async Task<Result<string>> GetEventNotificationMentions(
-        IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) {
+        IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) {
         var builder = new StringBuilder();
-        var receivers = config.EventStartedReceivers;
-        var role = config.EventNotificationRole.ToDiscordSnowflake();
+        var role = GuildSettings.EventNotificationRole.Get(settings);
         var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync(
             scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct);
         if (!usersResult.IsDefined(out var users)) return Result<string>.FromError(usersResult);
 
-        if (receivers.Contains(GuildConfiguration.NotificationReceiver.Role) && role.Value is not 0)
+        if (role.Value is not 0)
             builder.Append($"{Mention.Role(role)} ");
-        if (receivers.Contains(GuildConfiguration.NotificationReceiver.Interested))
-            builder = users.Where(
-                    user => {
-                        if (!user.GuildMember.IsDefined(out var member)) return true;
-                        return !member.Roles.Contains(role);
-                    })
-                .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} "));
+
+        builder = users.Where(
+                user => {
+                    if (!user.GuildMember.IsDefined(out var member)) return true;
+                    return !member.Roles.Contains(role);
+                })
+            .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} "));
         return builder.ToString();
     }
 }