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 @@ // //------------------------------------------------------------------------------ -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( - settings => { - settings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); - settings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); - settings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); - settings.SetSlidingExpiration(TimeSpan.FromDays(7)); + cSettings => { + cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); + cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); + cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); + cSettings.SetSlidingExpiration(TimeSpan.FromDays(7)); }); services.AddTransient() 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(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 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. /// 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 { } /// - /// A slash command that lists current per-guild settings. + /// A slash command that lists current per-guild GuildSettings. /// /// /// 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)) { - var list = (something as List); - 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 { } /// - /// A slash command that modifies per-guild settings. + /// A slash command that modifies per-guild GuildSettings. /// /// The setting to modify. /// The new value of the setting. @@ -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; - -/// -/// Stores per-guild settings that can be set by a member -/// with using the /settings command -/// -public class GuildConfiguration { - /// - /// Represents a scheduled event notification receiver. - /// - /// - /// Used to selectively mention guild members when a scheduled event has started or is about to start. - /// - public enum NotificationReceiver { - Interested, - Role - } - - public static readonly Dictionary CultureInfoCache = new() { - { "en", new CultureInfo("en-US") }, - { "ru", new CultureInfo("ru-RU") }, - { "mctaylors-ru", new CultureInfo("tt-RU") } - }; - - public string Language { get; set; } = "en"; - - /// - /// Controls what message should be sent in when a new member joins the server. - /// - /// - /// - /// No message will be sent if set to "off", "disable" or "disabled". - /// will be sent if set to "default" or "reset" - /// - /// - /// - public string WelcomeMessage { get; set; } = "default"; - - /// - /// Controls whether or not the message should be sent - /// in on startup. - /// - /// - public bool ReceiveStartupMessages { get; set; } - - public bool RemoveRolesOnMute { get; set; } - - /// - /// Controls whether or not a guild member's roles are returned if he/she leaves and then joins back. - /// - /// Roles will not be returned if the member left the guild because of /ban or /kick. - public bool ReturnRolesOnRejoin { get; set; } - - public bool AutoStartEvents { get; set; } - - /// - /// Controls what channel should all public messages be sent to. - /// - public ulong PublicFeedbackChannel { get; set; } - - /// - /// Controls what channel should all private, moderator-only messages be sent to. - /// - 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; } - - /// - /// Controls what guild members should be mentioned when a scheduled event has started or is about to start. - /// - /// - public List EventStartedReceivers { get; set; } - = new() { NotificationReceiver.Interested, NotificationReceiver.Role }; - - /// - /// Controls the amount of time before a scheduled event to send a reminder in . - /// - 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; /// /// This information is stored on disk as a JSON file. public class GuildData { - public readonly GuildConfiguration Configuration; - public readonly string ConfigurationPath; - public readonly Dictionary MemberData; public readonly string MemberDataPath; public readonly Dictionary ScheduledEvents; public readonly string ScheduledEventsPath; + public readonly JsonNode Settings; + public readonly string SettingsPath; public GuildData( - GuildConfiguration configuration, string configurationPath, + JsonNode settings, string settingsPath, Dictionary scheduledEvents, string scheduledEventsPath, Dictionary 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; + +/// +/// Contains all per-guild settings that can be set by a member +/// with using the /settings command +/// +public static class GuildSettings { + public static readonly LanguageOption Language = new("Language", "en"); + + /// + /// Controls what message should be sent in when a new member joins the server. + /// + /// + /// + /// No message will be sent if set to "off", "disable" or "disabled". + /// will be sent if set to "default" or "reset" + /// + /// + /// + public static readonly Option WelcomeMessage = new("WelcomeMessage", "default"); + + /// + /// Controls whether or not the message should be sent + /// in on startup. + /// + /// + public static readonly BoolOption ReceiveStartupMessages = new("ReceiveStartupMessages", false); + + public static readonly BoolOption RemoveRolesOnMute = new("RemoveRolesOnMute", false); + + /// + /// Controls whether or not a guild member's roles are returned if he/she leaves and then joins back. + /// + /// Roles will not be returned if the member left the guild because of /ban or /kick. + public static readonly BoolOption ReturnRolesOnRejoin = new("ReturnRolesOnRejoin", false); + + public static readonly BoolOption AutoStartEvents = new("AutoStartEvents", false); + + /// + /// Controls what channel should all public messages be sent to. + /// + public static readonly SnowflakeOption PublicFeedbackChannel = new("PublicFeedbackChannel"); + + /// + /// Controls what channel should all private, moderator-only messages be sent to. + /// + 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"); + + /// + /// Controls the amount of time before a scheduled event to send a reminder in . + /// + 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; /// @@ -13,6 +11,6 @@ public class MemberData { public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } - public List Roles { get; set; } = new(); + public List Roles { get; set; } = new(); public List 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 { + 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; + +/// +public class LanguageOption : Option { + private static readonly Dictionary 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]) { } + + /// + public override CultureInfo Get(JsonNode settings) { + var property = settings[Name]; + return property != null ? CultureInfoCache[property.GetValue()] : DefaultValue; + } + + /// + 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; + +/// +/// Represents an per-guild option. +/// +/// The type of the option. +public class Option : 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)!; + } + + /// + /// Sets the value of the option from a to the provided JsonNode. + /// + /// The to set the value to. + /// The string from which the new value of the option will be parsed. + /// A value setting result which may or may not have succeeded. + public virtual Result Set(JsonNode settings, string from) { + settings[Name] = from; + return Result.FromSuccess(); + } + + /// + /// Gets the value of the option from the provided . + /// + /// The to get the value from. + /// The value of the option. + public virtual T Get(JsonNode settings) { + var property = settings[Name]; + return property != null ? property.GetValue() : 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 { + public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { } + + public override Snowflake Get(JsonNode settings) { + var property = settings[Name]; + return property != null ? property.GetValue().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 { + 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; /// /// Handles sending a message to a guild that has just initialized if that guild -/// has enabled +/// has enabled /// public class GuildCreateResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; @@ -42,16 +43,16 @@ public class GuildCreateResponder : IResponder { 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 { 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); } } /// /// Handles logging the contents of a deleted message and the user who deleted the message -/// to a guild's if one is set. +/// to a guild's if one is set. /// public class MessageDeletedResponder : IResponder { private readonly IDiscordRestAuditLogAPI _auditLogApi; @@ -89,8 +90,8 @@ public class MessageDeletedResponder : IResponder { public async Task 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 { 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 { 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); } } /// /// Handles logging the difference between an edited message's old and new content -/// to a guild's if one is set. +/// to a guild's if one is set. /// public class MessageEditedResponder : IResponder { private readonly CacheService _cacheService; @@ -154,8 +155,8 @@ public class MessageEditedResponder : IResponder { public async Task 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 { 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 { 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); } } /// -/// Handles sending a guild's if one is set. -/// If is enabled, roles will be returned. +/// Handles sending a guild's if one is set. +/// If is enabled, roles will be returned. /// -/// +/// public class GuildMemberAddResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; private readonly GuildDataService _dataService; @@ -227,19 +228,21 @@ public class GuildMemberAddResponder : IResponder { 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 { 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); } } /// /// Handles sending a notification when a scheduled event has been cancelled -/// in a guild's if one is set. +/// in a guild's if one is set. /// public class GuildScheduledEventDeleteResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; @@ -275,7 +278,7 @@ public class GuildScheduledEventDeleteResponder : IResponder { public async Task 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( this IEnumerable source, Func 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(); 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 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( - 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(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(), scheduledEventsPath, memberData, memberDataPath); while (!_datas.ContainsKey(guildId)) _datas.TryAdd(guildId, finalData); return finalData; } - public async Task GetConfiguration(Snowflake guildId, CancellationToken ct = default) { - return (await GetData(guildId, ct)).Configuration; + public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) { + return (await GetData(guildId, ct)).Settings; } public async Task 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: /// /// Automatically unbans users once their ban period has expired. - /// Automatically grants members the guild's if one is set. + /// Automatically grants members the guild's if one is set. /// Sends reminders about an upcoming scheduled event. - /// Automatically starts scheduled events if is enabled. + /// Automatically starts scheduled events if is enabled. /// Sends scheduled event start notifications. /// Sends scheduled event completion notifications. /// Sends reminders to members. @@ -114,15 +116,15 @@ public class GuildUpdateService : BackgroundService { /// The cancellation token for this operation. 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 { } /// - /// Handles sending a notification, mentioning the if one is + /// Handles sending a notification, mentioning the if one is /// set, /// when a scheduled event is created - /// in a guild's if one is set. + /// in a guild's if one is set. /// /// The scheduled event that has just been created. - /// The configuration of the guild containing the scheduled event. + /// The settings of the guild containing the scheduled event. /// The cancellation token for this operation. /// A notification sending result which may or may not have succeeded. private async Task 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); } /// - /// Handles sending a notification, mentioning the s, + /// Handles sending a notification, mentioning the s, /// when a scheduled event is about to start, has started or completed - /// in a guild's if one is set. + /// in a guild's if one is set. /// /// The scheduled event that is about to start, has started or completed. /// The data for the guild containing the scheduled event. @@ -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), 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 { } /// - /// Gets the string mentioning all s related to a scheduled + /// Gets the string mentioning all s related to a scheduled /// event. /// /// - /// If the guild configuration enables , then the - /// will also be mentioned. + /// If the guild settings enables , then the + /// will also be mentioned. /// /// - /// The scheduled event whose subscribers will be mentioned if the guild configuration enables - /// . + /// The scheduled event whose subscribers will be mentioned if the guild settings enables + /// . /// - /// The configuration of the guild containing the scheduled event + /// The settings of the guild containing the scheduled event /// The cancellation token for this operation. /// A result containing the string which may or may not have succeeded. public async Task> 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.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(); } }