diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 561edb1..48fa0ef 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data; using Discord; using Discord.WebSocket; @@ -32,7 +33,7 @@ public sealed class BanCommand : ICommand { cmd.Reply(feedback, ReplyEmojis.Banned); cmd.Audit(feedback); - if (duration.TotalSeconds > 0) - await Task.FromResult(Utils.DelayedUnbanAsync(cmd, toBan.Id, Messages.PunishmentExpired, duration)); + GuildData.FromSocketGuild(guild).MemberData[toBan.Id].BannedUntil + = DateTimeOffset.Now.Add(duration).ToUnixTimeSeconds(); } } diff --git a/Boyfriend/Commands/HelpCommand.cs b/Boyfriend/Commands/HelpCommand.cs index 0fc91d8..99df178 100644 --- a/Boyfriend/Commands/HelpCommand.cs +++ b/Boyfriend/Commands/HelpCommand.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data; using Humanizer; namespace Boyfriend.Commands; @@ -6,7 +7,7 @@ public sealed class HelpCommand : ICommand { public string[] Aliases { get; } = { "help", "помощь", "справка" }; public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - var prefix = Boyfriend.GetGuildConfig(cmd.Context.Guild.Id)["Prefix"]; + var prefix = GuildData.FromSocketGuild(cmd.Context.Guild).Preferences["Prefix"]; var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp); foreach (var command in CommandProcessor.Commands) diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index b01f1f1..c2d4b27 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -1,5 +1,5 @@ +using Boyfriend.Data; using Discord; -using Discord.Net; using Discord.WebSocket; namespace Boyfriend.Commands; @@ -14,7 +14,8 @@ public sealed class MuteCommand : ICommand { var duration = CommandProcessor.GetTimeSpan(args, 1); var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "MuteReason"); if (reason is null) return; - var role = Utils.GetMuteRole(cmd.Context.Guild); + var guildData = GuildData.FromSocketGuild(cmd.Context.Guild); + var role = guildData.MuteRole; if ((role is not null && toMute.Roles.Contains(role)) || (toMute.TimedOutUntil is not null @@ -24,48 +25,22 @@ public sealed class MuteCommand : ICommand { return; } - var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id); - - if (rolesRemoved.TryGetValue(toMute.Id, out var mutedRemovedRoles)) { - foreach (var roleId in mutedRemovedRoles) await toMute.AddRoleAsync(roleId); - rolesRemoved.Remove(toMute.Id); - cmd.ConfigWriteScheduled = true; - cmd.Reply(Messages.RolesReturned, ReplyEmojis.Warning); - } - if (cmd.HasPermission(GuildPermission.ModerateMembers) && cmd.CanInteractWith(toMute, "Mute")) - await MuteMemberAsync(cmd, toMute, duration, reason); + await MuteMemberAsync(cmd, toMute, duration, guildData, reason); } private static async Task MuteMemberAsync(CommandProcessor cmd, SocketGuildUser toMute, - TimeSpan duration, string reason) { - var guild = cmd.Context.Guild; - var config = Boyfriend.GetGuildConfig(guild.Id); + TimeSpan duration, GuildData data, string reason) { var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); - var role = Utils.GetMuteRole(guild); + var role = data.MuteRole; var hasDuration = duration.TotalSeconds > 0; if (role is not null) { - if (config["RemoveRolesOnMute"] is "true") { - var rolesRemoved = new List(); - foreach (var userRole in toMute.Roles) - try { - if (userRole == guild.EveryoneRole || userRole == role) continue; - await toMute.RemoveRoleAsync(role); - rolesRemoved.Add(userRole.Id); - } catch (HttpException e) { - cmd.Reply(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.Wrap(e.Reason)), - ReplyEmojis.Warning); - } - - Boyfriend.GetRemovedRoles(guild.Id).Add(toMute.Id, rolesRemoved.AsReadOnly()); - cmd.ConfigWriteScheduled = true; - } + if (data.Preferences["RemoveRolesOnMute"] is "true") await toMute.RemoveRolesAsync(toMute.Roles); await toMute.AddRoleAsync(role, requestOptions); - if (hasDuration) - await Task.FromResult(Utils.DelayedUnmuteAsync(cmd, toMute, Messages.PunishmentExpired, duration)); + data.MemberData[toMute.Id].MutedUntil = DateTimeOffset.Now.Add(duration).ToUnixTimeSeconds(); } else { if (!hasDuration || duration.TotalDays > 28) { cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error); diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 7140906..3424fb8 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -10,7 +10,8 @@ public sealed class SettingsCommand : ICommand { if (!cmd.HasPermission(GuildPermission.ManageGuild)) return Task.CompletedTask; var guild = cmd.Context.Guild; - var config = Boyfriend.GetGuildConfig(guild.Id); + var data = GuildData.FromSocketGuild(guild); + var config = data.Preferences; if (args.Length is 0) { var currentSettings = Boyfriend.StringBuilder.AppendLine(Messages.CurrentSettings); @@ -132,7 +133,7 @@ public sealed class SettingsCommand : ICommand { return Task.CompletedTask; } - if (selectedSetting is "MuteRole") Utils.RemoveMuteRoleFromCache(ulong.Parse(config[selectedSetting])); + if (selectedSetting is "MuteRole") data.MuteRole = guild.GetRole(mention); config[selectedSetting] = value; } @@ -158,4 +159,3 @@ public sealed class SettingsCommand : ICommand { return value is "true" or "false"; } } - diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index f312231..81729cb 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data; using Discord; using Discord.WebSocket; @@ -19,18 +20,10 @@ public sealed class UnmuteCommand : ICommand { public static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute, string reason) { var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); - var role = Utils.GetMuteRole(cmd.Context.Guild); + var role = GuildData.FromSocketGuild(cmd.Context.Guild).MuteRole; if (role is not null && toUnmute.Roles.Contains(role)) { - var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id); - - if (rolesRemoved.TryGetValue(toUnmute.Id, out var unmutedRemovedRoles)) { - await toUnmute.AddRolesAsync(unmutedRemovedRoles); - rolesRemoved.Remove(toUnmute.Id); - cmd.ConfigWriteScheduled = true; - } - - await toUnmute.RemoveRoleAsync(role, requestOptions); + // TODO: Return roles } else { if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) { diff --git a/Boyfriend/Data/GuildData.cs b/Boyfriend/Data/GuildData.cs index 8c6bb6a..5880110 100644 --- a/Boyfriend/Data/GuildData.cs +++ b/Boyfriend/Data/GuildData.cs @@ -1,11 +1,10 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using Discord.WebSocket; -using Newtonsoft.Json; namespace Boyfriend.Data; -public struct GuildData { +public record GuildData { public static readonly Dictionary DefaultConfiguration = new() { { "Prefix", "!" }, { "Lang", "en" }, @@ -26,81 +25,46 @@ public struct GuildData { 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; + public readonly Dictionary Preferences; - var path = $"config_{id}.json"; + private SocketRole? _cachedMuteRole; - if (!File.Exists(path)) File.Create(path).Dispose(); + private ulong _id; - 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) { + private 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(); + Preferences + = JsonSerializer.Deserialize>(File.ReadAllText($"{id}/Configuration.json")) ?? + new Dictionary(); // ReSharper disable twice ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - if (GuildConfiguration.Keys.Count < DefaultConfiguration.Keys.Count) + if (Preferences.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 (!Preferences.ContainsKey(key)) + Preferences.Add(key, DefaultConfiguration[key]); + if (Preferences.Keys.Count > DefaultConfiguration.Keys.Count) + foreach (var key in Preferences.Keys) if (!DefaultConfiguration.ContainsKey(key)) - GuildConfiguration.Remove(key); - GuildConfiguration.TrimExcess(); + Preferences.Remove(key); + Preferences.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); + var deserialised = JsonSerializer.Deserialize(File.ReadAllText($"{id}/MemberData/{data}.json")); + 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 } && + if (!memberData.IsInGuild && DateTimeOffset.Now.ToUnixTimeSeconds() - Math.Max(memberData.LeftAt.Last(), memberData.BannedUntil) > 60 * 60 * 24 * 30) { @@ -114,9 +78,24 @@ public struct GuildData { var data = new MemberData(member); MemberData.Add(member.Id, data); File.WriteAllText($"{id}/MemberData/{data.Id}.json", - JsonConvert.SerializeObject(data, Formatting.Indented)); + JsonSerializer.Serialize(data)); } GuildDataDictionary.Add(id, this); } + + public SocketRole? MuteRole { + get => _cachedMuteRole ??= Boyfriend.Client.GetGuild(_id).Roles + .Single(x => x.Id == ulong.Parse(Preferences["MuteRole"])); + set => _cachedMuteRole = value; + } + + public static GuildData FromSocketGuild(SocketGuild guild) { + if (GuildDataDictionary.TryGetValue(guild.Id, out var stored)) return stored; + var newData = new GuildData(guild) { + _id = guild.Id + }; + GuildDataDictionary.Add(guild.Id, newData); + return newData; + } } diff --git a/Boyfriend/Data/Reminder.cs b/Boyfriend/Data/Reminder.cs index 9d3d034..3b14c20 100644 --- a/Boyfriend/Data/Reminder.cs +++ b/Boyfriend/Data/Reminder.cs @@ -1,6 +1,6 @@ namespace Boyfriend.Data; public struct Reminder { - public DateTimeOffset RemindAt; + public long RemindAt; public string ReminderText; } diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index d236628..3bbf916 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using Boyfriend.Commands; using Discord; using Discord.Net; using Discord.WebSocket; @@ -21,8 +20,6 @@ public static partial class Utils { private static readonly Dictionary ReflectionMessageCache = new(); - private static readonly Dictionary MuteRoleCache = new(); - private static readonly AllowedMentions AllowRoles = new() { AllowedTypes = AllowedMentionTypes.Roles }; @@ -58,22 +55,6 @@ public static partial class Utils { } } - public static SocketRole? GetMuteRole(SocketGuild guild) { - var id = ulong.Parse(Boyfriend.GetGuildConfig(guild.Id)["MuteRole"]); - if (MuteRoleCache.TryGetValue(id, out var cachedMuteRole)) return cachedMuteRole; - foreach (var x in guild.Roles) { - if (x.Id != id) continue; - MuteRoleCache.Add(id, x); - return x; - } - - return null; - } - - public static void RemoveMuteRoleFromCache(ulong id) { - MuteRoleCache.Remove(id); - } - public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) { try { if (channel is null || text.Length is 0 or > 2000) @@ -147,46 +128,6 @@ public static partial class Utils { appendTo.AppendLine(appendWhat); } - public static async Task DelayedUnbanAsync(CommandProcessor cmd, ulong banned, string reason, TimeSpan duration) { - await Task.Delay(duration); - SetCurrentLanguage(cmd.Context.Guild.Id); - await UnbanCommand.UnbanUserAsync(cmd, banned, reason); - } - - public static async Task DelayedUnmuteAsync(CommandProcessor cmd, SocketGuildUser muted, string reason, - TimeSpan duration) { - await Task.Delay(duration); - SetCurrentLanguage(cmd.Context.Guild.Id); - await UnmuteCommand.UnmuteMemberAsync(cmd, muted, reason); - } - - public static async Task SendEarlyEventStartNotificationAsync(SocketTextChannel? channel, - SocketGuildEvent scheduledEvent, int minuteOffset) { - try { - await Task.Delay(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now) - .Subtract(TimeSpan.FromMinutes(minuteOffset))); - var guild = scheduledEvent.Guild; - if (guild.GetEvent(scheduledEvent.Id) is null) return; - var eventConfig = Boyfriend.GetGuildConfig(guild.Id); - SetCurrentLanguage(guild.Id); - - var receivers = eventConfig["EventStartedReceivers"]; - var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); - var mentions = Boyfriend.StringBuilder; - - if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} "); - if (receivers.Contains("users") || receivers.Contains("interested")) - mentions = (await scheduledEvent.GetUsersAsync(15)).Aggregate(mentions, - (current, user) => current.Append($"{user.Mention} ")); - await channel?.SendMessageAsync(string.Format(Messages.EventEarlyNotification, mentions, - Wrap(scheduledEvent.Name), scheduledEvent.StartTime.ToUnixTimeSeconds().ToString()))!; - mentions.Clear(); - } catch (Exception e) { - await Boyfriend.Log(new LogMessage(LogSeverity.Error, nameof(Utils), - "Exception while sending early event start notification", e)); - } - } - public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) { return guild.GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(guild.Id)["EventNotificationChannel"])); } @@ -194,4 +135,3 @@ public static partial class Utils { [GeneratedRegex("[^0-9]")] private static partial Regex NumbersOnlyRegex(); } -