From b79be8a876e5759e8599a29ef9bcb00faedcaf05 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 21 Dec 2022 21:59:21 +0500 Subject: [PATCH] Begin guild storage refactor --- Boyfriend/Boyfriend.cs | 63 +------------- Boyfriend/Commands/ClearCommand.cs | 6 +- Boyfriend/Commands/SettingsCommand.cs | 16 ++-- Boyfriend/Data/GuildData.cs | 121 ++++++++++++++++++++++++++ Boyfriend/Data/MemberData.cs | 25 ++++++ Boyfriend/Data/Reminder.cs | 6 ++ Boyfriend/EventHandler.cs | 20 ++++- Boyfriend/Utils.cs | 10 ++- 8 files changed, 192 insertions(+), 75 deletions(-) create mode 100644 Boyfriend/Data/GuildData.cs create mode 100644 Boyfriend/Data/MemberData.cs create mode 100644 Boyfriend/Data/Reminder.cs diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index cdbbe7d..c82a295 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -1,8 +1,6 @@ -using System.Collections.ObjectModel; using System.Text; using Discord; using Discord.WebSocket; -using Newtonsoft.Json; namespace Boyfriend; @@ -32,28 +30,6 @@ public static class Boyfriend { public static readonly DiscordSocketClient Client = new(Config); - private static readonly Dictionary> GuildConfigDictionary = new(); - - private static readonly Dictionary>> RemovedRolesDictionary = - new(); - - public static readonly Dictionary DefaultConfig = new() { - { "Prefix", "!" }, - { "Lang", "en" }, - { "ReceiveStartupMessages", "false" }, - { "WelcomeMessage", "default" }, - { "SendWelcomeMessages", "true" }, - { "BotLogChannel", "0" }, - { "StarterRole", "0" }, - { "MuteRole", "0" }, - { "RemoveRolesOnMute", "false" }, - { "FrowningFace", "true" }, - { "EventStartedReceivers", "interested,role" }, - { "EventNotificationRole", "0" }, - { "EventNotificationChannel", "0" }, - { "EventEarlyNotificationOffset", "0" } - }; - public static void Main() { Init().GetAwaiter().GetResult(); } @@ -102,42 +78,7 @@ public static class Boyfriend { return Task.CompletedTask; } - public static async Task WriteGuildConfigAsync(ulong id) { - await File.WriteAllTextAsync($"config_{id}.json", - JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented)); - - if (RemovedRolesDictionary.TryGetValue(id, out var removedRoles)) - await File.WriteAllTextAsync($"removedroles_{id}.json", - JsonConvert.SerializeObject(removedRoles, Formatting.Indented)); - } - - public static Dictionary GetGuildConfig(ulong id) { - if (GuildConfigDictionary.TryGetValue(id, out var cfg)) return cfg; - - var path = $"config_{id}.json"; - - if (!File.Exists(path)) File.Create(path).Dispose(); - - var json = File.ReadAllText(path); - var config = JsonConvert.DeserializeObject>(json) - ?? new Dictionary(); - - if (config.Keys.Count < DefaultConfig.Keys.Count) { - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - // Conversion will result in a lot of memory allocations - foreach (var key in DefaultConfig.Keys) - if (!config.ContainsKey(key)) - config.Add(key, DefaultConfig[key]); - } else if (config.Keys.Count > DefaultConfig.Keys.Count) { - foreach (var key in config.Keys.Where(key => !DefaultConfig.ContainsKey(key))) config.Remove(key); - } - - GuildConfigDictionary.Add(id, config); - - return config; - } - - public static Dictionary> GetRemovedRoles(ulong id) { + /*public static Dictionary> GetRemovedRoles(ulong id) { if (RemovedRolesDictionary.TryGetValue(id, out var dict)) return dict; var path = $"removedroles_{id}.json"; @@ -150,5 +91,5 @@ public static class Boyfriend { RemovedRolesDictionary.Add(id, removedRoles); return removedRoles; - } + }*/ } diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs index ad5408e..fbe17fe 100644 --- a/Boyfriend/Commands/ClearCommand.cs +++ b/Boyfriend/Commands/ClearCommand.cs @@ -1,4 +1,5 @@ -using Discord; +using System.Diagnostics; +using Discord; using Discord.WebSocket; namespace Boyfriend.Commands; @@ -7,7 +8,7 @@ public sealed class ClearCommand : ICommand { public string[] Aliases { get; } = { "clear", "purge", "очистить", "стереть" }; public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - if (cmd.Context.Channel is not SocketTextChannel channel) throw new Exception(); + if (cmd.Context.Channel is not SocketTextChannel channel) throw new UnreachableException(); if (!cmd.HasPermission(GuildPermission.ManageMessages)) return; @@ -21,3 +22,4 @@ public sealed class ClearCommand : ICommand { cmd.Audit(string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString())); } } + diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index c996ba5..7140906 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data; using Discord; namespace Boyfriend.Commands; @@ -14,9 +15,11 @@ public sealed class SettingsCommand : ICommand { if (args.Length is 0) { var currentSettings = Boyfriend.StringBuilder.AppendLine(Messages.CurrentSettings); - foreach (var setting in Boyfriend.DefaultConfig) { + foreach (var setting in GuildData.DefaultConfiguration) { var format = "{0}"; - var currentValue = config[setting.Key] is "default" ? Messages.DefaultWelcomeMessage : config[setting.Key]; + var currentValue = config[setting.Key] is "default" + ? Messages.DefaultWelcomeMessage + : config[setting.Key]; if (setting.Key.EndsWith("Channel")) { if (guild.GetTextChannel(ulong.Parse(currentValue)) is not null) format = "<#{0}>"; @@ -43,7 +46,7 @@ public sealed class SettingsCommand : ICommand { var exists = false; // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator // Too many allocations - foreach (var setting in Boyfriend.DefaultConfig.Keys) { + foreach (var setting in GuildData.DefaultConfiguration.Keys) { if (selectedSetting != setting.ToLower()) continue; selectedSetting = setting; exists = true; @@ -70,7 +73,7 @@ public sealed class SettingsCommand : ICommand { } } else { value = "reset"; } - if (IsBool(Boyfriend.DefaultConfig[selectedSetting]) && !IsBool(value)) { + if (IsBool(GuildData.DefaultConfiguration[selectedSetting]) && !IsBool(value)) { value = value switch { "y" or "yes" or "д" or "да" => "true", "n" or "no" or "н" or "нет" => "false", @@ -95,14 +98,14 @@ public sealed class SettingsCommand : ICommand { var formattedValue = selectedSetting switch { "WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage), - "EventStartedReceivers" => Utils.Wrap(Boyfriend.DefaultConfig[selectedSetting])!, + "EventStartedReceivers" => Utils.Wrap(GuildData.DefaultConfiguration[selectedSetting])!, _ => value is "reset" or "default" ? Messages.SettingNotDefined : IsBool(value) ? YesOrNo(value is "true") : string.Format(formatting, value) }; if (value is "reset" or "default") { - config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; + config[selectedSetting] = GuildData.DefaultConfiguration[selectedSetting]; } else { if (value == config[selectedSetting]) { cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), @@ -155,3 +158,4 @@ public sealed class SettingsCommand : ICommand { return value is "true" or "false"; } } + diff --git a/Boyfriend/Data/GuildData.cs b/Boyfriend/Data/GuildData.cs new file mode 100644 index 0000000..0ae635e --- /dev/null +++ b/Boyfriend/Data/GuildData.cs @@ -0,0 +1,121 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Discord.WebSocket; +using Newtonsoft.Json; + +namespace Boyfriend.Data; + +public struct GuildData { + public static readonly Dictionary DefaultConfiguration = new() { + { "Prefix", "!" }, + { "Lang", "en" }, + { "ReceiveStartupMessages", "false" }, + { "WelcomeMessage", "default" }, + { "SendWelcomeMessages", "true" }, + { "PublicFeedbackChannel", "0" }, + { "PrivateFeedbackChannel", "0" }, + { "StarterRole", "0" }, + { "MuteRole", "0" }, + { "RemoveRolesOnMute", "false" }, + { "ReturnRolesOnRejoin", "false" }, + { "EventStartedReceivers", "interested,role" }, + { "EventNotificationRole", "0" }, + { "EventNotificationChannel", "0" }, + { "EventEarlyNotificationOffset", "0" } + }; + + private static readonly Dictionary GuildDataDictionary = new(); + + public readonly Dictionary GuildConfiguration; + + public readonly Dictionary MemberData; + + /*public static Dictionary GetGuildConfig(ulong id) { + if (GuildConfigDictionary.TryGetValue(id, out var cfg)) return cfg; + + var path = $"config_{id}.json"; + + if (!File.Exists(path)) File.Create(path).Dispose(); + + var json = File.ReadAllText(path); + var config = JsonConvert.DeserializeObject>(json) + ?? new Dictionary(); + + if (config.Keys.Count < GuildData.DefaultConfiguration.Keys.Count) { + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + // Conversion will result in a lot of memory allocations + foreach (var key in GuildData.DefaultConfiguration.Keys) + if (!config.ContainsKey(key)) + config.Add(key, GuildData.DefaultConfiguration[key]); + } else if (config.Keys.Count > GuildData.DefaultConfiguration.Keys.Count) { + foreach (var key in config.Keys.Where(key => !GuildData.DefaultConfiguration.ContainsKey(key))) config.Remove(key); + } + + GuildConfigDictionary.Add(id, config); + + return config; + }*/ + + /*public static async Task WriteGuildConfigAsync(ulong id) { + await File.WriteAllTextAsync($"config_{id}.json", + JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented)); + + if (RemovedRolesDictionary.TryGetValue(id, out var removedRoles)) + await File.WriteAllTextAsync($"removedroles_{id}.json", + JsonConvert.SerializeObject(removedRoles, Formatting.Indented)); + }*/ + [SuppressMessage("Performance", "CA1853:Unnecessary call to \'Dictionary.ContainsKey(key)\'")] + // https://github.com/dotnet/roslyn-analyzers/issues/6377 + public GuildData(SocketGuild guild) { + var id = guild.Id; + if (GuildDataDictionary.TryGetValue(id, out var stored)) { + this = stored; + return; + } + + if (!Directory.Exists($"{id}")) Directory.CreateDirectory($"{id}"); + if (!Directory.Exists($"{id}/MemberData")) Directory.CreateDirectory($"{id}/MemberData"); + if (!File.Exists($"{id}/Configuration.json")) File.Create($"{id}/Configuration.json").Dispose(); + GuildConfiguration = JsonConvert.DeserializeObject>($"{id}/Configuration.json") ?? + new Dictionary(); + + // ReSharper disable twice ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + if (GuildConfiguration.Keys.Count < DefaultConfiguration.Keys.Count) + foreach (var key in DefaultConfiguration.Keys) + if (!GuildConfiguration.ContainsKey(key)) + GuildConfiguration.Add(key, DefaultConfiguration[key]); + if (GuildConfiguration.Keys.Count > DefaultConfiguration.Keys.Count) + foreach (var key in GuildConfiguration.Keys) + if (!DefaultConfiguration.ContainsKey(key)) + GuildConfiguration.Remove(key); + GuildConfiguration.TrimExcess(); + + MemberData = new Dictionary(); + foreach (var data in Directory.GetFiles($"{id}/MemberData")) { + var deserialised = JsonConvert.DeserializeObject($"{id}/MemberData/{data}.json") ?? + throw new UnreachableException(); + MemberData.Add(deserialised.Id, deserialised); + } + + if (guild.MemberCount > MemberData.Count) + foreach (var member in guild.Users) { + if (MemberData.TryGetValue(member.Id, out var memberData)) { + if (memberData is { IsInGuild: false, BannedUntil: > -1 } && + DateTimeOffset.Now.ToUnixTimeSeconds() - memberData.LeftAt.Last() > + 60 * 60 * 24 * 30) { + File.Delete($"{id}/MemberData/{memberData.Id}.json"); + MemberData.Remove(memberData.Id); + } + + continue; + } + + var data = new MemberData(member); + MemberData.Add(member.Id, data); + File.WriteAllText($"{id}/MemberData/{data.Id}.json", + JsonConvert.SerializeObject(data, Formatting.Indented)); + } + + GuildDataDictionary.Add(id, this); + } +} diff --git a/Boyfriend/Data/MemberData.cs b/Boyfriend/Data/MemberData.cs new file mode 100644 index 0000000..26fc349 --- /dev/null +++ b/Boyfriend/Data/MemberData.cs @@ -0,0 +1,25 @@ +using Discord; + +namespace Boyfriend.Data; + +public record MemberData { + public long BannedUntil; + public ulong Id; + public bool IsInGuild; + public List JoinedAt; + public List LeftAt; + public long MutedUntil; + public List Reminders; + public List Roles; + + public MemberData(IGuildUser user) { + Id = user.Id; + IsInGuild = true; + JoinedAt = new List { user.JoinedAt!.Value.ToUnixTimeSeconds() }; + LeftAt = new List(); + Roles = user.RoleIds.ToList(); + Reminders = new List(); + MutedUntil = 0; + BannedUntil = 0; + } +} diff --git a/Boyfriend/Data/Reminder.cs b/Boyfriend/Data/Reminder.cs new file mode 100644 index 0000000..9d3d034 --- /dev/null +++ b/Boyfriend/Data/Reminder.cs @@ -0,0 +1,6 @@ +namespace Boyfriend.Data; + +public struct Reminder { + public DateTimeOffset RemindAt; + public string ReminderText; +} diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 9f5af5f..04f11ce 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -14,6 +14,7 @@ public static class EventHandler { Client.MessageReceived += MessageReceivedEvent; Client.MessageUpdated += MessageUpdatedEvent; Client.UserJoined += UserJoinedEvent; + Client.UserLeft += UserLeftEvent; Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; Client.GuildScheduledEventStarted += ScheduledEventStartedEvent; @@ -67,7 +68,8 @@ public static class EventHandler { "whoami" => message.ReplyAsync("`nobody`"), "сука !!" => message.ReplyAsync("`root`"), "воооо" => message.ReplyAsync("`removing /...`"), - "op ??" => message.ReplyAsync("некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"), + "op ??" => message.ReplyAsync( + "некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"), _ => new CommandProcessor(message).HandleCommandAsync() }; return Task.CompletedTask; @@ -103,6 +105,20 @@ public static class EventHandler { if (config["StarterRole"] is not "0") await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); } + private static async Task UserLeftEvent(SocketGuildUser user) { + var guild = user.Guild; + var config = Boyfriend.GetGuildConfig(guild.Id); + Utils.SetCurrentLanguage(guild.Id); + + if (config["SendWelcomeMessages"] is "true") + await Utils.SilentSendAsync(guild.SystemChannel, + string.Format(config["WelcomeMessage"] is "default" + ? Messages.DefaultWelcomeMessage + : config["WelcomeMessage"], user.Mention, guild.Name)); + + if (config["StarterRole"] is not "0") await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); + } + private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); @@ -172,4 +188,4 @@ public static class EventHandler { await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), Utils.GetHumanizedTimeOffset(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); } -} \ No newline at end of file +} diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index b08eca3..d236628 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Diagnostics; +using System.Globalization; using System.Reflection; using System.Text; using System.Text.RegularExpressions; @@ -12,14 +13,14 @@ using Humanizer.Localisation; namespace Boyfriend; public static partial class Utils { - private static readonly Dictionary ReflectionMessageCache = new(); - public static readonly Dictionary CultureInfoCache = new() { { "ru", new CultureInfo("ru-RU") }, { "en", new CultureInfo("en-US") }, { "mctaylors-ru", new CultureInfo("tt-RU") } }; + private static readonly Dictionary ReflectionMessageCache = new(); + private static readonly Dictionary MuteRoleCache = new(); private static readonly AllowedMentions AllowRoles = new() { @@ -76,7 +77,7 @@ public static partial class Utils { public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) { try { if (channel is null || text.Length is 0 or > 2000) - throw new Exception($"Message length is out of range: {text.Length}"); + throw new UnreachableException($"Message length is out of range: {text.Length}"); await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); } catch (Exception e) { @@ -193,3 +194,4 @@ public static partial class Utils { [GeneratedRegex("[^0-9]")] private static partial Regex NumbersOnlyRegex(); } +