From cca296520587f73f1cef46fbc9365fc8cd90d851 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 30 May 2023 18:42:57 +0500 Subject: [PATCH] Fix issues reported by ReSharper, implement GuildData (only GuildConfiguration) Signed-off-by: Octol1ttle --- .github/workflows/resharper.yml | 3 - Boyfriend.cs | 40 +++++------- Boyfriend.csproj | 4 +- Data/GuildConfiguration.cs | 32 ++++++++++ Data/GuildData.cs | 11 ++++ Data/NotificationReceiver.cs | 6 ++ Data/Services/GuildDataService.cs | 49 ++++++++++++++ EventResponders.cs | 103 ++++++++++++++++++------------ Extensions.cs | 34 ++-------- Messages.Designer.cs | 35 +++++----- 10 files changed, 201 insertions(+), 116 deletions(-) create mode 100644 Data/GuildConfiguration.cs create mode 100644 Data/GuildData.cs create mode 100644 Data/NotificationReceiver.cs create mode 100644 Data/Services/GuildDataService.cs diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index a9cb03c..452bd51 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -18,9 +18,6 @@ jobs: contents: read security-events: write - strategy: - fail-fast: false - steps: - name: Checkout repository uses: actions/checkout@v3 diff --git a/Boyfriend.cs b/Boyfriend.cs index f36889e..3824fa6 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -17,20 +18,11 @@ using Remora.Rest.Core; namespace Boyfriend; public class Boyfriend { - public static ILogger Logger = null!; - public static IConfiguration GuildConfiguration = null!; - public static readonly AllowedMentions NoMentions = new( Array.Empty(), Array.Empty(), Array.Empty()); public static async Task Main(string[] args) { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); - - var services = host.Services; - Logger = services.GetRequiredService>(); - GuildConfiguration = services.GetRequiredService().AddJsonFile("guild_configs.json") - .Build(); - await host.RunAsync(); } @@ -47,12 +39,10 @@ public class Boyfriend { } ).ConfigureServices( (_, services) => { - var responderTypes = typeof(Boyfriend).Assembly - .GetExportedTypes() - .Where(t => t.IsResponder()); - foreach (var responderType in responderTypes) services.AddResponder(responderType); - - services.AddDiscordCaching(); + services.Configure( + options => options.Intents |= GatewayIntents.MessageContents + | GatewayIntents.GuildMembers + | GatewayIntents.GuildScheduledEvents); services.Configure( settings => { settings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); @@ -61,16 +51,16 @@ public class Boyfriend { settings.SetSlidingExpiration(TimeSpan.FromDays(7)); }); - services.AddTransient(); - - services.Configure( - options => options.Intents |= GatewayIntents.MessageContents - | GatewayIntents.GuildMembers - | GatewayIntents.GuildScheduledEvents); - - services.AddDiscordCommands(); - services.AddInteractivity(); - services.AddInteractionGroup(); + services.AddTransient() + .AddDiscordCaching() + .AddDiscordCommands() + .AddInteractivity() + .AddInteractionGroup() + .AddSingleton(); + var responderTypes = typeof(Boyfriend).Assembly + .GetExportedTypes() + .Where(t => t.IsResponder()); + foreach (var responderType in responderTypes) services.AddResponder(responderType); } ).ConfigureLogging( c => c.AddConsole() diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 0d6969a..444f4ab 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -5,7 +5,7 @@ net7.0 enable enable - 1.0.0 + 2.0.0 Boyfriend Octol1ttle, mctaylors AGPLv3 @@ -21,7 +21,7 @@ - + diff --git a/Data/GuildConfiguration.cs b/Data/GuildConfiguration.cs new file mode 100644 index 0000000..4d0c9c3 --- /dev/null +++ b/Data/GuildConfiguration.cs @@ -0,0 +1,32 @@ +using System.Globalization; + +namespace Boyfriend.Data; + +public class GuildConfiguration { + private static readonly Dictionary CultureInfoCache = new() { + { "en", new CultureInfo("en-US") }, + { "ru", new CultureInfo("ru-RU") }, + { "mctaylors-ru", new CultureInfo("tt-RU") } + }; + + public string Prefix { get; set; } = "!"; + public string Language { get; set; } = "en"; + public string? WelcomeMessage { get; set; } + public bool ReceiveStartupMessages { get; set; } + public bool RemoveRolesOnMute { get; set; } + public bool ReturnRolesOnRejoin { get; set; } + public bool AutoStartEvents { get; set; } + public ulong? PublicFeedbackChannel { get; set; } + public ulong? PrivateFeedbackChannel { get; set; } + public ulong? EventNotificationChannel { get; set; } + public ulong? StarterRole { get; set; } + public ulong? MuteRole { get; set; } + public ulong? EventNotificationRole { get; set; } + + public List EventStartedReceivers { get; set; } + = new() { NotificationReceiver.Interested, NotificationReceiver.Role }; + + public TimeSpan EventEarlyNotificationOffset { get; set; } = TimeSpan.Zero; + + public CultureInfo Culture => CultureInfoCache[Language]; +} diff --git a/Data/GuildData.cs b/Data/GuildData.cs new file mode 100644 index 0000000..fcacfd8 --- /dev/null +++ b/Data/GuildData.cs @@ -0,0 +1,11 @@ +namespace Boyfriend.Data; + +public class GuildData { + public readonly GuildConfiguration Configuration; + public readonly string ConfigurationPath; + + public GuildData(GuildConfiguration configuration, string configurationPath) { + Configuration = configuration; + ConfigurationPath = configurationPath; + } +} diff --git a/Data/NotificationReceiver.cs b/Data/NotificationReceiver.cs new file mode 100644 index 0000000..4d64f5d --- /dev/null +++ b/Data/NotificationReceiver.cs @@ -0,0 +1,6 @@ +namespace Boyfriend.Data; + +public enum NotificationReceiver { + Interested, + Role +} diff --git a/Data/Services/GuildDataService.cs b/Data/Services/GuildDataService.cs new file mode 100644 index 0000000..da9a54e --- /dev/null +++ b/Data/Services/GuildDataService.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using Microsoft.Extensions.Hosting; +using Remora.Rest.Core; + +namespace Boyfriend.Data.Services; + +public class GuildDataService : IHostedService { + private readonly Dictionary _datas = new(); + + public Task StartAsync(CancellationToken ct) { + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken ct) { + var tasks = new List(); + foreach (var data in _datas.Values) { + await using var stream = File.OpenWrite(data.ConfigurationPath); + tasks.Add(JsonSerializer.SerializeAsync(stream, data.Configuration, cancellationToken: ct)); + } + + await Task.WhenAll(tasks); + } + + private async Task GetData(Snowflake guildId, CancellationToken ct = default) { + return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); + } + + private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) { + var idString = $"{guildId}"; + var memberDataDir = $"{guildId}/MemberData"; + var configurationPath = $"{guildId}/Configuration.json"; + if (!Directory.Exists(idString)) Directory.CreateDirectory(idString); + if (!Directory.Exists(memberDataDir)) Directory.CreateDirectory(memberDataDir); + if (!File.Exists(configurationPath)) await File.WriteAllTextAsync(configurationPath, "{}", ct); + + await using var stream = File.OpenRead(configurationPath); + var configuration + = JsonSerializer.DeserializeAsync( + stream, cancellationToken: ct); + + var data = new GuildData(await configuration ?? new GuildConfiguration(), configurationPath); + _datas.Add(guildId, data); + return data; + } + + public async Task GetConfiguration(Snowflake guildId, CancellationToken ct = default) { + return (await GetData(guildId, ct)).Configuration; + } +} diff --git a/EventResponders.cs b/EventResponders.cs index 1518520..0a2aa2a 100644 --- a/EventResponders.cs +++ b/EventResponders.cs @@ -1,4 +1,5 @@ using System.Drawing; +using Boyfriend.Data.Services; using DiffPlex; using DiffPlex.DiffBuilder; using Microsoft.Extensions.Logging; @@ -21,29 +22,35 @@ namespace Boyfriend; public class GuildCreateResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly ILogger _logger; private readonly IDiscordRestUserAPI _userApi; - public GuildCreateResponder(IDiscordRestChannelAPI channelApi, IDiscordRestUserAPI userApi) { + public GuildCreateResponder( + IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestUserAPI userApi, + ILogger logger) { _channelApi = channelApi; + _dataService = dataService; _userApi = userApi; + _logger = logger; } public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // is IAvailableGuild var guild = gatewayEvent.Guild.AsT0; - Boyfriend.Logger.LogInformation("Joined guild \"{Name}\"", guild.Name); + _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); - var channelResult = guild.ID.GetConfigChannel("PrivateFeedbackChannel"); - if (!channelResult.IsDefined(out var channel)) return Result.FromSuccess(); + var guildConfig = await _dataService.GetConfiguration(guild.ID, ct); + if (!guildConfig.ReceiveStartupMessages) + return Result.FromSuccess(); + if (guildConfig.PrivateFeedbackChannel is null) + return Result.FromSuccess(); var currentUserResult = await _userApi.GetCurrentUserAsync(ct); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - if (!guild.GetConfigBool("ReceiveStartupMessages").IsDefined(out var shouldSendStartupMessage) - || !shouldSendStartupMessage) return Result.FromSuccess(); - - Messages.Culture = guild.ID.GetGuildCulture(); + Messages.Culture = guildConfig.Culture; var i = Random.Shared.Next(1, 4); var embed = new EmbedBuilder() @@ -56,7 +63,7 @@ public class GuildCreateResponder : IResponder { if (!embed.IsDefined(out var built)) return Result.FromError(embed); return (Result)await _channelApi.CreateMessageAsync( - channel, embeds: new[] { built }, ct: ct); + guildConfig.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, ct: ct); } } @@ -64,22 +71,24 @@ public class MessageDeletedResponder : IResponder { private readonly IDiscordRestAuditLogAPI _auditLogApi; private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; private readonly IDiscordRestUserAPI _userApi; public MessageDeletedResponder( - IDiscordRestAuditLogAPI auditLogApi, CacheService cacheService, IDiscordRestChannelAPI channelApi, - IDiscordRestUserAPI userApi) { + IDiscordRestAuditLogAPI auditLogApi, CacheService cacheService, IDiscordRestChannelAPI channelApi, + GuildDataService dataService, IDiscordRestUserAPI userApi) { _auditLogApi = auditLogApi; _cacheService = cacheService; _channelApi = channelApi; + _dataService = dataService; _userApi = userApi; } public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); - var channelResult = guildId.GetConfigChannel("PrivateFeedbackChannel"); - if (!channelResult.IsDefined(out var logChannel)) return Result.FromSuccess(); + var guildConfiguration = await _dataService.GetConfiguration(guildId, ct); + if (guildConfiguration.PrivateFeedbackChannel is null) return Result.FromSuccess(); var messageResult = await _cacheService.TryGetValueAsync( new KeyHelpers.MessageCacheKey(gatewayEvent.ChannelID, gatewayEvent.ID), ct); @@ -101,7 +110,7 @@ public class MessageDeletedResponder : IResponder { if (!userResult.IsDefined(out user)) return Result.FromError(userResult); } - Messages.Culture = guildId.GetGuildCulture(); + Messages.Culture = guildConfiguration.Culture; var embed = new EmbedBuilder() .WithSmallTitle( @@ -118,22 +127,29 @@ public class MessageDeletedResponder : IResponder { if (!embed.IsDefined(out var built)) return Result.FromError(embed); return (Result)await _channelApi.CreateMessageAsync( - logChannel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct); + guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); } } public class MessageEditedResponder : IResponder { private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; - public MessageEditedResponder(CacheService cacheService, IDiscordRestChannelAPI channelApi) { + public MessageEditedResponder( + CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService) { _cacheService = cacheService; _channelApi = channelApi; + _dataService = dataService; } 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 null) + return Result.FromSuccess(); if (!gatewayEvent.Content.IsDefined(out var newContent)) return Result.FromSuccess(); @@ -148,18 +164,12 @@ public class MessageEditedResponder : IResponder { var messageResult = await _cacheService.TryGetValueAsync( cacheKey, ct); if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); - if (string.IsNullOrWhiteSpace(message.Content) - || string.IsNullOrWhiteSpace(newContent) - || message.Content == newContent) return Result.FromSuccess(); + if (message.Content == newContent) return Result.FromSuccess(); await _cacheService.EvictAsync(cacheKey, ct); var newMessageResult = await _channelApi.GetChannelMessageAsync(channelId, messageId, ct); if (!newMessageResult.IsDefined(out var newMessage)) return Result.FromError(newMessageResult); - // No need to await the recache since we don't depend on it - _ = _cacheService.CacheAsync(cacheKey, newMessage, ct); - - var logChannelResult = guildId.GetConfigChannel("PrivateFeedbackChannel"); - if (!logChannelResult.IsDefined(out var logChannel)) return Result.FromSuccess(); + await _cacheService.CacheAsync(cacheKey, newMessage, ct); var currentUserResult = await _cacheService.TryGetValueAsync( new KeyHelpers.CurrentUserCacheKey(), ct); @@ -167,7 +177,7 @@ public class MessageEditedResponder : IResponder { var diff = new SideBySideDiffBuilder(Differ.Instance).BuildDiffModel(message.Content, newContent, true, true); - Messages.Culture = guildId.GetGuildCulture(); + Messages.Culture = guildConfiguration.Culture; var embed = new EmbedBuilder() .WithSmallTitle( @@ -181,30 +191,35 @@ public class MessageEditedResponder : IResponder { if (!embed.IsDefined(out var built)) return Result.FromError(embed); return (Result)await _channelApi.CreateMessageAsync( - logChannel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct); + guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); } } public class GuildMemberAddResponder : IResponder { private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; - public GuildMemberAddResponder(CacheService cacheService, IDiscordRestChannelAPI channelApi) { + public GuildMemberAddResponder( + CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService) { _cacheService = cacheService; _channelApi = channelApi; + _dataService = dataService; } public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.GuildID.GetConfigString("WelcomeMessage").IsDefined(out var welcomeMessage) - || welcomeMessage is "off" or "disable" or "disabled") + var guildConfiguration = await _dataService.GetConfiguration(gatewayEvent.GuildID, ct); + if (guildConfiguration.PublicFeedbackChannel is null) + return Result.FromSuccess(); + if (guildConfiguration.WelcomeMessage is null or "off" or "disable" or "disabled") return Result.FromSuccess(); - if (welcomeMessage is "default" or "reset") { - Messages.Culture = gatewayEvent.GuildID.GetGuildCulture(); - welcomeMessage = Messages.DefaultWelcomeMessage; - } - if (!gatewayEvent.GuildID.GetConfigChannel("PublicFeedbackChannel").IsDefined(out var channel)) - return Result.FromSuccess(); + Messages.Culture = guildConfiguration.Culture; + var welcomeMessage = guildConfiguration.WelcomeMessage is "default" or "reset" + ? Messages.DefaultWelcomeMessage + : guildConfiguration.WelcomeMessage; + if (!gatewayEvent.User.IsDefined(out var user)) return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User))); @@ -221,25 +236,30 @@ public class GuildMemberAddResponder : IResponder { if (!embed.IsDefined(out var built)) return Result.FromError(embed); return (Result)await _channelApi.CreateMessageAsync( - channel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct); + guildConfiguration.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); } } public class GuildScheduledEventCreateResponder : IResponder { private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; private readonly IDiscordRestUserAPI _userApi; public GuildScheduledEventCreateResponder( - CacheService cacheService, IDiscordRestChannelAPI channelApi, IDiscordRestUserAPI userApi) { + CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService, + IDiscordRestUserAPI userApi) { _cacheService = cacheService; _channelApi = channelApi; + _dataService = dataService; _userApi = userApi; } public async Task RespondAsync(IGuildScheduledEventCreate gatewayEvent, CancellationToken ct = default) { - var channelResult = gatewayEvent.GuildID.GetConfigChannel("EventNotificationChannel"); - if (!channelResult.IsDefined(out var channel)) return Result.FromSuccess(); + var guildConfiguration = await _dataService.GetConfiguration(gatewayEvent.GuildID, ct); + if (guildConfiguration.EventNotificationChannel is null) + return Result.FromSuccess(); var currentUserResult = await _cacheService.TryGetValueAsync( new KeyHelpers.CurrentUserCacheKey(), ct); @@ -250,7 +270,7 @@ public class GuildScheduledEventCreateResponder : IResponder CultureInfoCache = new() { - { "en", new CultureInfo("en-US") }, - { "ru", new CultureInfo("ru-RU") }, - { "mctaylors-ru", new CultureInfo("tt-RU") } - }; - - public static Result GetConfigBool(this IGuild guild, string key) { - var value = Boyfriend.GuildConfiguration.GetValue($"GuildConfigs:{guild.ID}:{key}"); - return value is not null ? Result.FromSuccess(value.Value) : Result.FromError(new NotFoundError()); - } - - public static Result GetConfigChannel(this Snowflake guildId, string key) { - var value = Boyfriend.GuildConfiguration.GetValue($"GuildConfigs:{guildId}:{key}"); - return value is not null - ? Result.FromSuccess(DiscordSnowflake.New(value.Value)) - : Result.FromError(new NotFoundError()); - } - - public static Result GetConfigString(this Snowflake guildId, string key) { - var value = Boyfriend.GuildConfiguration.GetValue($"GuildConfigs:{guildId}:{key}"); - return value is not null ? Result.FromSuccess(value) : Result.FromError(new NotFoundError()); - } - - public static CultureInfo GetGuildCulture(this Snowflake guildId) { - var value = Boyfriend.GuildConfiguration.GetValue($"GuildConfigs:{guildId}:Language"); - return value is not null ? CultureInfoCache[value] : CultureInfoCache["en"]; - } - public static async Task> TryGetUserAsync( this Snowflake userId, CacheService cacheService, IDiscordRestUserAPI userApi, CancellationToken ct) { var cachedUserResult = await cacheService.TryGetValueAsync( @@ -118,4 +88,8 @@ public static class Extensions { public static string GetTag(this IUser user) { return $"{user.Username}#{user.Discriminator:0000}"; } + + public static Snowflake ToDiscordSnowflake(this ulong? id) { + return DiscordSnowflake.New(id ?? 0); + } } diff --git a/Messages.Designer.cs b/Messages.Designer.cs index b018355..f08c2e5 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -7,36 +7,41 @@ // //------------------------------------------------------------------------------ +using System.CodeDom.Compiler; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Resources; +using System.Runtime.CompilerServices; + namespace Boyfriend { - using System; - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [GeneratedCode("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [DebuggerNonUserCode()] + [CompilerGenerated()] internal class Messages { - private static System.Resources.ResourceManager resourceMan; + private static ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + [EditorBrowsable(EditorBrowsableState.Advanced)] + internal static ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + if (Equals(null, resourceMan)) { + ResourceManager temp = new ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + [EditorBrowsable(EditorBrowsableState.Advanced)] + internal static CultureInfo Culture { get { return resourceCulture; }