From 7b8888dae3db6a0b2720ba6698cf8ebcede01c6b Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 18 Jan 2023 19:39:24 +0500 Subject: [PATCH] Refactor guild data storage (#13) Co-authored-by: mctaylors --- .github/dependabot.yml | 8 +- .github/workflows/resharper.yml | 2 +- Boyfriend/Boyfriend.cs | 157 ++++++++++-------- Boyfriend/CommandProcessor.cs | 66 +++++--- Boyfriend/Commands/BanCommand.cs | 29 ++-- Boyfriend/Commands/ClearCommand.cs | 8 +- Boyfriend/Commands/HelpCommand.cs | 3 +- Boyfriend/Commands/KickCommand.cs | 6 +- Boyfriend/Commands/MuteCommand.cs | 53 ++---- Boyfriend/Commands/PingCommand.cs | 2 +- Boyfriend/Commands/RemindCommand.cs | 23 +++ Boyfriend/Commands/SettingsCommand.cs | 40 +++-- Boyfriend/Commands/UnbanCommand.cs | 2 +- Boyfriend/Commands/UnmuteCommand.cs | 32 ++-- Boyfriend/Data/GuildData.cs | 142 ++++++++++++++++ Boyfriend/Data/MemberData.cs | 38 +++++ Boyfriend/Data/Reminder.cs | 7 + Boyfriend/EventHandler.cs | 93 +++++++---- Boyfriend/Messages.Designer.cs | 78 +++++---- Boyfriend/Messages.resx | 230 +++++++++++++------------- Boyfriend/Messages.ru.resx | 230 +++++++++++++------------- Boyfriend/Messages.tt-ru.resx | 230 +++++++++++++------------- Boyfriend/ReplyEmojis.cs | 1 - Boyfriend/Utils.cs | 122 +++++--------- 24 files changed, 941 insertions(+), 661 deletions(-) create mode 100644 Boyfriend/Commands/RemindCommand.cs create mode 100644 Boyfriend/Data/GuildData.cs create mode 100644 Boyfriend/Data/MemberData.cs create mode 100644 Boyfriend/Data/Reminder.cs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b3f8cdb..18ba8f9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,7 +13,9 @@ updates: # Allow both direct and indirect updates for all packages - dependency-type: "all" assignees: - - "l1ttleO" + - "Octol1ttle" + labels: + - "type: dependencies" - package-ecosystem: "nuget" # See documentation for possible values directory: "/Boyfriend" # Location of package manifests @@ -24,4 +26,6 @@ updates: - dependency-type: "all" # Add assignees assignees: - - "l1ttleO" + - "Octol1ttle" + labels: + - "type: dependencies" diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index 8c55f0f..9a91364 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -29,7 +29,7 @@ jobs: run: dotnet restore - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.6.0 + uses: muno92/resharper_inspectcode@1.6.6 with: solutionPath: ./Boyfriend-CSharp.sln ignoreIssueType: InvertIf diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index cdbbe7d..f22da61 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -1,8 +1,10 @@ -using System.Collections.ObjectModel; using System.Text; +using System.Timers; +using Boyfriend.Data; using Discord; +using Discord.Rest; using Discord.WebSocket; -using Newtonsoft.Json; +using Timer = System.Timers.Timer; namespace Boyfriend; @@ -20,7 +22,10 @@ public static class Boyfriend { LargeThreshold = 500 }; - private static readonly List> ActivityList = new() { + private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; + private static uint _nextSongIndex; + + private static readonly Tuple[] ActivityList = { Tuple.Create(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening), new TimeSpan(0, 3, 40)), Tuple.Create(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)), @@ -32,33 +37,13 @@ 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" } - }; + private static readonly List GuildTickTasks = new(); public static void Main() { - Init().GetAwaiter().GetResult(); + InitAsync().GetAwaiter().GetResult(); } - private static async Task Init() { + private static async Task InitAsync() { var token = (await File.ReadAllTextAsync("token.txt")).Trim(); Client.Log += Log; @@ -68,13 +53,35 @@ public static class Boyfriend { EventHandler.InitEvents(); - while (ActivityList.Count > 0) - foreach (var activity in ActivityList) { - await Client.SetActivityAsync(activity.Item1); - await Task.Delay(activity.Item2); + var timer = new Timer(); + timer.Interval = 1000; + timer.AutoReset = true; + timer.Elapsed += TickAllGuildsAsync; + if (ActivityList.Length is 0) timer.Dispose(); // CodeQL moment + timer.Start(); + + while (ActivityList.Length > 0) + if (DateTimeOffset.Now >= _nextSongAt) { + var nextSong = ActivityList[_nextSongIndex]; + await Client.SetActivityAsync(nextSong.Item1); + _nextSongAt = DateTimeOffset.Now.Add(nextSong.Item2); + _nextSongIndex++; + if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0; } } + private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) { + foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild)); + + try { Task.WaitAll(GuildTickTasks.ToArray()); } catch (AggregateException ex) { + foreach (var exc in ex.InnerExceptions) + await Log(new LogMessage(LogSeverity.Error, nameof(Boyfriend), + "Exception while ticking guilds", exc)); + } + + GuildTickTasks.Clear(); + } + public static Task Log(LogMessage msg) { switch (msg.Severity) { case LogSeverity.Critical: @@ -102,53 +109,63 @@ 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)); + private static async Task TickGuildAsync(SocketGuild guild) { + var data = GuildData.Get(guild); + var config = data.Preferences; + var saveData = false; + _ = int.TryParse(config["EventEarlyNotificationOffset"], out var offset); + foreach (var schEvent in guild.Events) + if (schEvent.Status is GuildScheduledEventStatus.Scheduled && config["AutoStartEvents"] is "true" && + DateTimeOffset.Now >= schEvent.StartTime) { await schEvent.StartAsync(); } else if + (!data.EarlyNotifications.Contains(schEvent.Id) && + DateTimeOffset.Now >= schEvent.StartTime.Subtract(new TimeSpan(0, offset, 0))) { + data.EarlyNotifications.Add(schEvent.Id); + var receivers = config["EventStartedReceivers"]; + var role = guild.GetRole(ulong.Parse(config["EventNotificationRole"])); + var mentions = StringBuilder; - if (RemovedRolesDictionary.TryGetValue(id, out var removedRoles)) - await File.WriteAllTextAsync($"removedroles_{id}.json", - JsonConvert.SerializeObject(removedRoles, Formatting.Indented)); - } + if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} "); + if (receivers.Contains("users") || receivers.Contains("interested")) + mentions = (await schEvent.GetUsersAsync(15)) + .Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id)) + .Aggregate(mentions, (current, user) => current.Append($"{user.Mention} ")); - public static Dictionary GetGuildConfig(ulong id) { - if (GuildConfigDictionary.TryGetValue(id, out var cfg)) return cfg; + await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync(string.Format( + Messages.EventEarlyNotification, + mentions, + Utils.Wrap(schEvent.Name), + schEvent.StartTime.ToUnixTimeSeconds().ToString()))!; + mentions.Clear(); + } - var path = $"config_{id}.json"; + foreach (var mData in data.MemberData.Values) { + if (DateTimeOffset.Now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id); - if (!File.Exists(path)) File.Create(path).Dispose(); + if (mData.IsInGuild) { + if (DateTimeOffset.Now >= mData.MutedUntil) { + await Utils.UnmuteMemberAsync(data, Client.CurrentUser.ToString(), guild.GetUser(mData.Id), + Messages.PunishmentExpired); + saveData = true; + } - var json = File.ReadAllText(path); - var config = JsonConvert.DeserializeObject>(json) - ?? new Dictionary(); + for (var i = mData.Reminders.Count - 1; i >= 0; i--) { + var reminder = mData.Reminders[i]; + if (DateTimeOffset.Now >= reminder.RemindAt) { + var channel = guild.GetTextChannel(reminder.ReminderChannel); + if (channel is null) { + await Utils.SendDirectMessage(Client.GetUser(mData.Id), reminder.ReminderText); + continue; + } - 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); + await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}"); + mData.Reminders.RemoveAt(i); + + saveData = true; + } + } + } } - GuildConfigDictionary.Add(id, config); - - return config; - } - - public static Dictionary> GetRemovedRoles(ulong id) { - if (RemovedRolesDictionary.TryGetValue(id, out var dict)) return dict; - var path = $"removedroles_{id}.json"; - - if (!File.Exists(path)) File.Create(path).Dispose(); - - var json = File.ReadAllText(path); - var removedRoles = JsonConvert.DeserializeObject>>(json) - ?? new Dictionary>(); - - RemovedRolesDictionary.Add(id, removedRoles); - - return removedRoles; + if (saveData) data.Save(true).Wait(); } } diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index ab077ef..c20603c 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -1,5 +1,6 @@ using System.Text; using Boyfriend.Commands; +using Boyfriend.Data; using Discord; using Discord.Commands; using Discord.WebSocket; @@ -12,7 +13,8 @@ public sealed class CommandProcessor { public static readonly ICommand[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), new KickCommand(), new MuteCommand(), new PingCommand(), - new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() + new SettingsCommand(), new UnbanCommand(), new UnmuteCommand(), + new RemindCommand() }; private readonly StringBuilder _stackedPrivateFeedback = new(); @@ -30,19 +32,18 @@ public sealed class CommandProcessor { public async Task HandleCommandAsync() { var guild = Context.Guild; - var config = Boyfriend.GetGuildConfig(guild.Id); - var muteRole = Utils.GetMuteRole(guild); - Utils.SetCurrentLanguage(guild.Id); + var data = GuildData.Get(guild); + Utils.SetCurrentLanguage(guild); - if (GetMember().Roles.Contains(muteRole)) { - _ = Context.Message.ReplyAsync(Messages.UserCannotUnmuteThemselves); + if (GetMember().Roles.Contains(data.MuteRole)) { + _ = Context.Message.DeleteAsync(); return; } var list = Context.Message.Content.Split("\n"); var cleanList = Context.Message.CleanContent.Split("\n"); for (var i = 0; i < list.Length; i++) - _tasks.Add(RunCommandOnLine(list[i], cleanList[i], config["Prefix"])); + _tasks.Add(RunCommandOnLine(list[i], cleanList[i], data.Preferences["Prefix"])); try { Task.WaitAll(_tasks.ToArray()); } catch (AggregateException e) { foreach (var ex in e.InnerExceptions) @@ -52,7 +53,7 @@ public sealed class CommandProcessor { _tasks.Clear(); - if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfigAsync(guild.Id); + if (ConfigWriteScheduled) await data.Save(true); SendFeedbacks(); } @@ -79,8 +80,9 @@ public sealed class CommandProcessor { public void Audit(string action, bool isPublic = true) { var format = $"*[{Context.User.Mention}: {action}]*"; - if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, Context.Guild.SystemChannel); - Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, Utils.GetBotLogChannel(Context.Guild.Id)); + var data = GuildData.Get(Context.Guild); + if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, data.PublicFeedbackChannel); + Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, data.PrivateFeedbackChannel); if (_tasks.Count is 0) SendFeedbacks(false); } @@ -88,8 +90,9 @@ public sealed class CommandProcessor { if (reply && _stackedReplyMessage.Length > 0) _ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None); - var adminChannel = Utils.GetBotLogChannel(Context.Guild.Id); - var systemChannel = Context.Guild.SystemChannel; + var data = GuildData.Get(Context.Guild); + var adminChannel = data.PublicFeedbackChannel; + var systemChannel = data.PrivateFeedbackChannel; if (_stackedPrivateFeedback.Length > 0 && adminChannel is not null && adminChannel.Id != Context.Message.Channel.Id) { _ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString()); @@ -111,19 +114,30 @@ public sealed class CommandProcessor { return null; } - public SocketUser? GetUser(string[] args, string[] cleanArgs, int index, string? argument) { + public Tuple? GetUser(string[] args, string[] cleanArgs, int index) { if (index >= args.Length) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", Context.Message); return null; } - var user = Boyfriend.Client.GetUser(Utils.ParseMention(args[index])); - if (user is null && argument is not null) + var mention = Utils.ParseMention(args[index]); + if (mention is 0) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}", Context.Message); - return user; + return null; + } + + var exists = Utils.UserExists(mention); + if (!exists) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{ReplyEmojis.Error} {string.Format(Messages.UserNotFound, Utils.Wrap(cleanArgs[index]))}", + Context.Message); + return null; + } + + return Tuple.Create(mention, Boyfriend.Client.GetUser(mention))!; } public bool HasPermission(GuildPermission permission) { @@ -134,7 +148,7 @@ public sealed class CommandProcessor { return false; } - if (!Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission) + if (!GetMember().GuildPermissions.Has(permission) && Context.Guild.OwnerId != Context.User.Id) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}", @@ -145,11 +159,15 @@ public sealed class CommandProcessor { return true; } - public SocketGuildUser? GetMember(SocketUser user) { - return Context.Guild.GetUser(user.Id); + private SocketGuildUser GetMember() { + return GetMember(Context.User.Id)!; } - public SocketGuildUser? GetMember(string[] args, string[] cleanArgs, int index, string? argument) { + public SocketGuildUser? GetMember(ulong id) { + return Context.Guild.GetUser(id); + } + + public SocketGuildUser? GetMember(string[] args, int index) { if (index >= args.Length) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingMember}", Context.Message); @@ -157,17 +175,13 @@ public sealed class CommandProcessor { } var member = Context.Guild.GetUser(Utils.ParseMention(args[index])); - if (member is null && argument is not null) + if (member is null) Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidMember, Utils.Wrap(cleanArgs[index]))}", + $"{ReplyEmojis.InvalidArgument} {Messages.InvalidMember}", Context.Message); return member; } - private SocketGuildUser GetMember() { - return Context.Guild.GetUser(Context.User.Id); - } - public ulong? GetBan(string[] args, int index) { if (index >= args.Length) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 561edb1..9c00656 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data; using Discord; using Discord.WebSocket; @@ -7,10 +8,10 @@ public sealed class BanCommand : ICommand { public string[] Aliases { get; } = { "ban", "бан" }; public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - var toBan = cmd.GetUser(args, cleanArgs, 0, "ToBan"); + var toBan = cmd.GetUser(args, cleanArgs, 0); if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return; - var memberToBan = cmd.GetMember(toBan); + var memberToBan = cmd.GetMember(toBan.Item1); if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return; var duration = CommandProcessor.GetTimeSpan(args, 1); @@ -18,21 +19,27 @@ public sealed class BanCommand : ICommand { if (reason is not null) await BanUserAsync(cmd, toBan, duration, reason); } - private static async Task BanUserAsync(CommandProcessor cmd, SocketUser toBan, TimeSpan duration, string reason) { + private static async Task BanUserAsync(CommandProcessor cmd, Tuple toBan, TimeSpan duration, + string reason) { var author = cmd.Context.User; var guild = cmd.Context.Guild; - await Utils.SendDirectMessage(toBan, - string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason))); + if (toBan.Item2 is not null) + await Utils.SendDirectMessage(toBan.Item2, + string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason))); var guildBanMessage = $"({author}) {reason}"; - await guild.AddBanAsync(toBan, 0, guildBanMessage); + await guild.AddBanAsync(toBan.Item1, 0, guildBanMessage); - var feedback = string.Format(Messages.FeedbackUserBanned, toBan.Mention, - Utils.GetHumanizedTimeOffset(duration), Utils.Wrap(reason)); + var memberData = GuildData.Get(guild).MemberData[toBan.Item1]; + memberData.BannedUntil + = duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.Now.Add(duration); + memberData.Roles.Clear(); + + cmd.ConfigWriteScheduled = true; + + var feedback = string.Format(Messages.FeedbackUserBanned, $"<@{toBan.Item1.ToString()}>", + Utils.GetHumanizedTimeSpan(duration), Utils.Wrap(reason)); cmd.Reply(feedback, ReplyEmojis.Banned); cmd.Audit(feedback); - - if (duration.TotalSeconds > 0) - await Task.FromResult(Utils.DelayedUnbanAsync(cmd, toBan.Id, Messages.PunishmentExpired, duration)); } } diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs index ad5408e..141cc9a 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; @@ -18,6 +19,7 @@ public sealed class ClearCommand : ICommand { var user = (SocketGuildUser)cmd.Context.User; await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(user.ToString()!)); - cmd.Audit(string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString())); + cmd.Audit(string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString(), + Utils.MentionChannel(channel.Id))); } } diff --git a/Boyfriend/Commands/HelpCommand.cs b/Boyfriend/Commands/HelpCommand.cs index 0fc91d8..72b788b 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.Get(cmd.Context.Guild).Preferences["Prefix"]; var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp); foreach (var command in CommandProcessor.Commands) diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index 9e4cfe4..6d5689b 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data; using Discord; using Discord.WebSocket; @@ -7,7 +8,7 @@ public sealed class KickCommand : ICommand { public string[] Aliases { get; } = { "kick", "кик", "выгнать" }; public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - var toKick = cmd.GetMember(args, cleanArgs, 0, "ToKick"); + var toKick = cmd.GetMember(args, 0); if (toKick is null || !cmd.HasPermission(GuildPermission.KickMembers)) return; if (cmd.CanInteractWith(toKick, "Kick")) @@ -22,6 +23,9 @@ public sealed class KickCommand : ICommand { string.Format(Messages.YouWereKicked, cmd.Context.User.Mention, cmd.Context.Guild.Name, Utils.Wrap(reason))); + GuildData.Get(cmd.Context.Guild).MemberData[toKick.Id].Roles.Clear(); + cmd.ConfigWriteScheduled = true; + await toKick.KickAsync(guildKickMessage); var format = string.Format(Messages.FeedbackMemberKicked, toKick.Mention, Utils.Wrap(reason)); cmd.Reply(format, ReplyEmojis.Kicked); diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index b01f1f1..1d2ebeb 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; @@ -8,64 +8,38 @@ public sealed class MuteCommand : ICommand { public string[] Aliases { get; } = { "mute", "timeout", "заглушить", "мут" }; public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - var toMute = cmd.GetMember(args, cleanArgs, 0, "ToMute"); + var toMute = cmd.GetMember(args, 0); if (toMute is null) return; 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.Get(cmd.Context.Guild); + var role = guildData.MuteRole; if ((role is not null && toMute.Roles.Contains(role)) || (toMute.TimedOutUntil is not null - && toMute.TimedOutUntil.Value.ToUnixTimeSeconds() - > DateTimeOffset.Now.ToUnixTimeSeconds())) { + && toMute.TimedOutUntil.Value + > DateTimeOffset.Now)) { cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error); 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, requestOptions); await toMute.AddRoleAsync(role, requestOptions); - - if (hasDuration) - await Task.FromResult(Utils.DelayedUnmuteAsync(cmd, toMute, Messages.PunishmentExpired, duration)); } else { if (!hasDuration || duration.TotalDays > 28) { cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error); @@ -80,8 +54,11 @@ public sealed class MuteCommand : ICommand { await toMute.SetTimeOutAsync(duration, requestOptions); } + data.MemberData[toMute.Id].MutedUntil = DateTimeOffset.Now.Add(duration); + cmd.ConfigWriteScheduled = true; + var feedback = string.Format(Messages.FeedbackMemberMuted, toMute.Mention, - Utils.GetHumanizedTimeOffset(duration), + Utils.GetHumanizedTimeSpan(duration), Utils.Wrap(reason)); cmd.Reply(feedback, ReplyEmojis.Muted); cmd.Audit(feedback); diff --git a/Boyfriend/Commands/PingCommand.cs b/Boyfriend/Commands/PingCommand.cs index 88034a5..67e0861 100644 --- a/Boyfriend/Commands/PingCommand.cs +++ b/Boyfriend/Commands/PingCommand.cs @@ -7,7 +7,7 @@ public sealed class PingCommand : ICommand { var builder = Boyfriend.StringBuilder; builder.Append(Utils.GetBeep()) - .Append(Math.Abs(DateTimeOffset.Now.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds)) + .Append(Math.Round(Math.Abs(DateTimeOffset.Now.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds))) .Append(Messages.Milliseconds); cmd.Reply(builder.ToString(), ReplyEmojis.Ping); diff --git a/Boyfriend/Commands/RemindCommand.cs b/Boyfriend/Commands/RemindCommand.cs new file mode 100644 index 0000000..24b1a08 --- /dev/null +++ b/Boyfriend/Commands/RemindCommand.cs @@ -0,0 +1,23 @@ +using Boyfriend.Data; + +namespace Boyfriend.Commands; + +public sealed class RemindCommand : ICommand { + public string[] Aliases { get; } = { "remind", "reminder", "remindme", "напомни", "напомнить", "напоминание" }; + + public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { + // TODO: actually make this good + var remindIn = CommandProcessor.GetTimeSpan(args, 0); + var reminderText = cmd.GetRemaining(cleanArgs, 1, "ReminderText"); + if (reminderText is not null) + GuildData.Get(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add(new Reminder { + RemindAt = DateTimeOffset.Now.Add(remindIn), + ReminderText = reminderText, + ReminderChannel = cmd.Context.Channel.Id + }); + + cmd.ConfigWriteScheduled = true; + + return Task.CompletedTask; + } +} diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index c996ba5..7ed012c 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data; using Discord; namespace Boyfriend.Commands; @@ -9,14 +10,17 @@ 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.Get(guild); + var config = data.Preferences; if (args.Length is 0) { var currentSettings = Boyfriend.StringBuilder.AppendLine(Messages.CurrentSettings); - foreach (var setting in Boyfriend.DefaultConfig) { + foreach (var setting in GuildData.DefaultPreferences) { 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}>"; @@ -41,10 +45,7 @@ public sealed class SettingsCommand : ICommand { var selectedSetting = args[0].ToLower(); var exists = false; - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - // Too many allocations - foreach (var setting in Boyfriend.DefaultConfig.Keys) { - if (selectedSetting != setting.ToLower()) continue; + foreach (var setting in GuildData.DefaultPreferences.Keys.Where(x => x.ToLower() == selectedSetting)) { selectedSetting = setting; exists = true; break; @@ -70,7 +71,7 @@ public sealed class SettingsCommand : ICommand { } } else { value = "reset"; } - if (IsBool(Boyfriend.DefaultConfig[selectedSetting]) && !IsBool(value)) { + if (IsBool(GuildData.DefaultPreferences[selectedSetting]) && !IsBool(value)) { value = value switch { "y" or "yes" or "д" or "да" => "true", "n" or "no" or "н" or "нет" => "false", @@ -95,14 +96,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.DefaultPreferences[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.DefaultPreferences[selectedSetting]; } else { if (value == config[selectedSetting]) { cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), @@ -129,13 +130,28 @@ public sealed class SettingsCommand : ICommand { return Task.CompletedTask; } - if (selectedSetting is "MuteRole") Utils.RemoveMuteRoleFromCache(ulong.Parse(config[selectedSetting])); + if (selectedSetting.EndsWith("Offset") && !int.TryParse(value, out _)) { + cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error); + return Task.CompletedTask; + } + + switch (selectedSetting) { + case "MuteRole": + data.MuteRole = guild.GetRole(mention); + break; + case "PublicFeedbackChannel": + data.PublicFeedbackChannel = guild.GetTextChannel(mention); + break; + case "PrivateFeedbackChannel": + data.PrivateFeedbackChannel = guild.GetTextChannel(mention); + break; + } config[selectedSetting] = value; } if (selectedSetting is "Lang") { - Utils.SetCurrentLanguage(guild.Id); + Utils.SetCurrentLanguage(guild); localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); } diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index f1eb9e6..70abfe1 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -14,7 +14,7 @@ public sealed class UnbanCommand : ICommand { if (reason is not null) await UnbanUserAsync(cmd, id.Value, reason); } - public static async Task UnbanUserAsync(CommandProcessor cmd, ulong id, string reason) { + private static async Task UnbanUserAsync(CommandProcessor cmd, ulong id, string reason) { var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); await cmd.Context.Guild.RemoveBanAsync(id, requestOptions); diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index f312231..6310c1d 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -1,3 +1,4 @@ +using Boyfriend.Data; using Discord; using Discord.WebSocket; @@ -9,38 +10,25 @@ public sealed class UnmuteCommand : ICommand { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return; - var toUnmute = cmd.GetMember(args, cleanArgs, 0, "ToUnmute"); + var toUnmute = cmd.GetMember(args, 0); if (toUnmute is null) return; var reason = cmd.GetRemaining(args, 1, "UnmuteReason"); if (reason is not null && cmd.CanInteractWith(toUnmute, "Unmute")) await UnmuteMemberAsync(cmd, toUnmute, reason); } - public static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute, + private 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 isMuted = await Utils.UnmuteMemberAsync(GuildData.Get(cmd.Context.Guild), cmd.Context.User.ToString(), + toUnmute, reason); - 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); - } else { - if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value.ToUnixTimeSeconds() < - DateTimeOffset.Now.ToUnixTimeSeconds()) { - cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error); - return; - } - - await toUnmute.RemoveTimeOutAsync(); + if (!isMuted) { + cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error); + return; } + cmd.ConfigWriteScheduled = true; + var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason)); cmd.Reply(feedback, ReplyEmojis.Unmuted); cmd.Audit(feedback); diff --git a/Boyfriend/Data/GuildData.cs b/Boyfriend/Data/GuildData.cs new file mode 100644 index 0000000..9d00525 --- /dev/null +++ b/Boyfriend/Data/GuildData.cs @@ -0,0 +1,142 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Discord.WebSocket; + +namespace Boyfriend.Data; + +public record GuildData { + public static readonly Dictionary DefaultPreferences = 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" }, + { "AutoStartEvents", "false" } + }; + + public static readonly ConcurrentDictionary GuildDataDictionary = new(); + + private static readonly JsonSerializerOptions Options = new() { + IncludeFields = true, + WriteIndented = true + }; + + private readonly string _configurationFile; + + private readonly ulong _id; + + public readonly List EarlyNotifications = new(); + + public readonly Dictionary MemberData; + + public readonly Dictionary Preferences; + + private SocketRole? _cachedMuteRole; + private SocketTextChannel? _cachedPrivateFeedbackChannel; + private SocketTextChannel? _cachedPublicFeedbackChannel; + + [SuppressMessage("Performance", "CA1853:Unnecessary call to \'Dictionary.ContainsKey(key)\'")] + // https://github.com/dotnet/roslyn-analyzers/issues/6377 + private GuildData(SocketGuild guild) { + _id = guild.Id; + var idString = $"{_id}"; + var memberDataDir = $"{_id}/MemberData"; + _configurationFile = $"{_id}/Configuration.json"; + if (!Directory.Exists(idString)) Directory.CreateDirectory(idString); + if (!Directory.Exists(memberDataDir)) Directory.CreateDirectory(memberDataDir); + if (!File.Exists(_configurationFile)) File.WriteAllText(_configurationFile, "{}"); + Preferences + = JsonSerializer.Deserialize>(File.ReadAllText(_configurationFile)) ?? + new Dictionary(); + + if (Preferences.Keys.Count < DefaultPreferences.Keys.Count) + foreach (var key in DefaultPreferences.Keys.Where(key => !Preferences.ContainsKey(key))) + Preferences.Add(key, DefaultPreferences[key]); + if (Preferences.Keys.Count > DefaultPreferences.Keys.Count) + foreach (var key in Preferences.Keys.Where(key => !DefaultPreferences.ContainsKey(key))) + Preferences.Remove(key); + Preferences.TrimExcess(); + + MemberData = new Dictionary(); + foreach (var data in Directory.GetFiles(memberDataDir)) { + var deserialised + = JsonSerializer.Deserialize(File.ReadAllText(data), Options); + MemberData.Add(deserialised!.Id, deserialised); + } + + guild.DownloadUsersAsync().Wait(); + foreach (var member in guild.Users.Where(user => !user.IsBot)) { + if (MemberData.TryGetValue(member.Id, out var memberData)) { + if (!memberData.IsInGuild && + DateTimeOffset.Now.ToUnixTimeSeconds() - + Math.Max(memberData.LeftAt.Last().ToUnixTimeSeconds(), + memberData.BannedUntil?.ToUnixTimeSeconds() ?? 0) > + 60 * 60 * 24 * 30) { + File.Delete($"{_id}/MemberData/{memberData.Id}.json"); + MemberData.Remove(memberData.Id); + } + + continue; + } + + MemberData.Add(member.Id, new MemberData(member)); + } + + MemberData.TrimExcess(); + } + + public SocketRole? MuteRole { + get { + if (Preferences["MuteRole"] is "0") return null; + return _cachedMuteRole ??= Boyfriend.Client.GetGuild(_id).Roles + .Single(x => x.Id == ulong.Parse(Preferences["MuteRole"])); + } + set => _cachedMuteRole = value; + } + + public SocketTextChannel? PublicFeedbackChannel { + get { + if (Preferences["PublicFeedbackChannel"] is "0") return null; + return _cachedPublicFeedbackChannel ??= Boyfriend.Client.GetGuild(_id).TextChannels + .Single(x => x.Id == ulong.Parse(Preferences["PublicFeedbackChannel"])); + } + set => _cachedPublicFeedbackChannel = value; + } + + public SocketTextChannel? PrivateFeedbackChannel { + get { + if (Preferences["PublicFeedbackChannel"] is "0") return null; + return _cachedPrivateFeedbackChannel ??= Boyfriend.Client.GetGuild(_id).TextChannels + .Single(x => x.Id == ulong.Parse(Preferences["PrivateFeedbackChannel"])); + } + set => _cachedPrivateFeedbackChannel = value; + } + + public static GuildData Get(SocketGuild guild) { + if (GuildDataDictionary.TryGetValue(guild.Id, out var stored)) return stored; + var newData = new GuildData(guild); + while (!GuildDataDictionary.ContainsKey(guild.Id)) GuildDataDictionary.TryAdd(guild.Id, newData); + return newData; + } + + public async Task Save(bool saveMemberData) { + Preferences.TrimExcess(); + await File.WriteAllTextAsync(_configurationFile, + JsonSerializer.Serialize(Preferences)); + if (saveMemberData) + foreach (var data in MemberData.Values) + await File.WriteAllTextAsync($"{_id}/MemberData/{data.Id}.json", + JsonSerializer.Serialize(data, Options)); + } +} diff --git a/Boyfriend/Data/MemberData.cs b/Boyfriend/Data/MemberData.cs new file mode 100644 index 0000000..137375a --- /dev/null +++ b/Boyfriend/Data/MemberData.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using Discord; + +namespace Boyfriend.Data; + +public record MemberData { + public DateTimeOffset? BannedUntil; + public ulong Id; + public bool IsInGuild; + public List JoinedAt; + public List LeftAt; + public DateTimeOffset? MutedUntil; + public List Reminders; + public List Roles; + + [JsonConstructor] + public MemberData(DateTimeOffset? bannedUntil, ulong id, bool isInGuild, List joinedAt, + List leftAt, DateTimeOffset? mutedUntil, List reminders, List roles) { + BannedUntil = bannedUntil; + Id = id; + IsInGuild = isInGuild; + JoinedAt = joinedAt; + LeftAt = leftAt; + MutedUntil = mutedUntil; + Reminders = reminders; + Roles = roles; + } + + public MemberData(IGuildUser user) { + Id = user.Id; + IsInGuild = true; + JoinedAt = new List { user.JoinedAt!.Value }; + LeftAt = new List(); + Roles = user.RoleIds.ToList(); + Roles.Remove(user.Guild.Id); + Reminders = new List(); + } +} diff --git a/Boyfriend/Data/Reminder.cs b/Boyfriend/Data/Reminder.cs new file mode 100644 index 0000000..c64ebbd --- /dev/null +++ b/Boyfriend/Data/Reminder.cs @@ -0,0 +1,7 @@ +namespace Boyfriend.Data; + +public struct Reminder { + public DateTimeOffset RemindAt; + public string ReminderText; + public ulong ReminderChannel; +} diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 9f5af5f..3e3bcfd 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; +using Boyfriend.Data; using Discord; using Discord.Rest; using Discord.WebSocket; @@ -14,20 +16,30 @@ public static class EventHandler { Client.MessageReceived += MessageReceivedEvent; Client.MessageUpdated += MessageUpdatedEvent; Client.UserJoined += UserJoinedEvent; + Client.UserLeft += UserLeftEvent; + Client.GuildMemberUpdated += RolesUpdatedEvent; Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; Client.GuildScheduledEventStarted += ScheduledEventStartedEvent; Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; } + private static Task RolesUpdatedEvent(Cacheable oldUser, SocketGuildUser newUser) { + var data = GuildData.Get(newUser.Guild).MemberData[newUser.Id]; + data.Roles = ((IGuildUser)newUser).RoleIds.ToList(); + data.Roles.Remove(newUser.Guild.Id); + return Task.CompletedTask; + } + private static Task ReadyEvent() { if (!_sendReadyMessages) return Task.CompletedTask; var i = Random.Shared.Next(3); foreach (var guild in Client.Guilds) { - var config = Boyfriend.GetGuildConfig(guild.Id); - var channel = guild.GetTextChannel(Utils.ParseMention(config["BotLogChannel"])); - Utils.SetCurrentLanguage(guild.Id); + var data = GuildData.Get(guild); + var config = data.Preferences; + var channel = data.PrivateFeedbackChannel; + Utils.SetCurrentLanguage(guild); if (config["ReceiveStartupMessages"] is not "true" || channel is null) continue; _ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); @@ -45,19 +57,20 @@ public static class EventHandler { var guild = gChannel.Guild; - Utils.SetCurrentLanguage(guild.Id); + Utils.SetCurrentLanguage(guild); var mention = msg.Author.Mention; await Task.Delay(500); var auditLogEntry = (await guild.GetAuditLogsAsync(1).FlattenAsync()).First(); - if (auditLogEntry.Data is MessageDeleteAuditLogData data && msg.Author.Id == data.Target.Id) + if (auditLogEntry.CreatedAt >= DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(1)) && + auditLogEntry.Data is MessageDeleteAuditLogData data && msg.Author.Id == data.Target.Id) mention = auditLogEntry.User.Mention; await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageDeleted, msg.Author.Mention, Utils.MentionChannel(channel.Id), - Utils.Wrap(msg.CleanContent)), guild.Id, mention); + Utils.Wrap(msg.CleanContent)), guild, mention); } private static Task MessageReceivedEvent(IDeletable messageParam) { @@ -67,7 +80,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; @@ -80,34 +94,60 @@ public static class EventHandler { msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return; var guild = gChannel.Guild; - Utils.SetCurrentLanguage(guild.Id); + Utils.SetCurrentLanguage(guild); var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940; await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id), Utils.Wrap(msg.CleanContent, isLimitedSpace), Utils.Wrap(messageSocket.CleanContent, isLimitedSpace)), - guild.Id, msg.Author.Mention); + guild, msg.Author.Mention); } private static async Task UserJoinedEvent(SocketGuildUser user) { + if (user.IsBot) return; var guild = user.Guild; - var config = Boyfriend.GetGuildConfig(guild.Id); - Utils.SetCurrentLanguage(guild.Id); + var data = GuildData.Get(guild); + var config = data.Preferences; + Utils.SetCurrentLanguage(guild); - if (config["SendWelcomeMessages"] is "true") - await Utils.SilentSendAsync(guild.SystemChannel, + if (config["SendWelcomeMessages"] is "true" && data.PublicFeedbackChannel is not null) + await Utils.SilentSendAsync(data.PublicFeedbackChannel, 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"])); + + if (!data.MemberData.ContainsKey(user.Id)) data.MemberData.Add(user.Id, new MemberData(user)); + var memberData = data.MemberData[user.Id]; + memberData.IsInGuild = true; + memberData.BannedUntil = null; + if (memberData.LeftAt.Count > 0) { + if (memberData.JoinedAt.Contains(user.JoinedAt!.Value)) + throw new UnreachableException(); + memberData.JoinedAt.Add(user.JoinedAt!.Value); + } + + if (memberData.MutedUntil < DateTimeOffset.Now) { + if (data.MuteRole is not null) + await user.AddRoleAsync(data.MuteRole); + if (config["RemoveRolesOnMute"] is "false" && config["ReturnRolesOnRejoin"] is "true") + await user.AddRolesAsync(memberData.Roles); + } else if (config["ReturnRolesOnRejoin"] is "true") { await user.AddRolesAsync(memberData.Roles); } + } + + private static Task UserLeftEvent(SocketGuild guild, SocketUser user) { + var data = GuildData.Get(guild).MemberData[user.Id]; + data.IsInGuild = false; + data.LeftAt.Add(DateTimeOffset.Now); + return Task.CompletedTask; } private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; - var eventConfig = Boyfriend.GetGuildConfig(guild.Id); + var eventConfig = GuildData.Get(guild).Preferences; var channel = Utils.GetEventNotificationChannel(guild); - Utils.SetCurrentLanguage(guild.Id); + Utils.SetCurrentLanguage(guild); if (channel is not null) { var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); @@ -125,17 +165,13 @@ public static class EventHandler { scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink), true); } - - if (eventConfig["EventEarlyNotificationOffset"] is not "0") - _ = Utils.SendEarlyEventStartNotificationAsync(channel, scheduledEvent, - int.Parse(eventConfig["EventEarlyNotificationOffset"])); } private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; - var eventConfig = Boyfriend.GetGuildConfig(guild.Id); + var eventConfig = GuildData.Get(guild).Preferences; var channel = Utils.GetEventNotificationChannel(guild); - Utils.SetCurrentLanguage(guild.Id); + Utils.SetCurrentLanguage(guild); if (channel is not null) await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name), eventConfig["FrowningFace"] is "true" ? $" {Messages.SettingsFrowningFace}" : "")); @@ -143,9 +179,9 @@ public static class EventHandler { private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; - var eventConfig = Boyfriend.GetGuildConfig(guild.Id); + var eventConfig = GuildData.Get(guild).Preferences; var channel = Utils.GetEventNotificationChannel(guild); - Utils.SetCurrentLanguage(guild.Id); + Utils.SetCurrentLanguage(guild); if (channel is not null) { var receivers = eventConfig["EventStartedReceivers"]; @@ -154,8 +190,9 @@ public static class EventHandler { 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} ")); + mentions = (await scheduledEvent.GetUsersAsync(15)) + .Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id)) + .Aggregate(mentions, (current, user) => current.Append($"{user.Mention} ")); await channel.SendMessageAsync(string.Format(Messages.EventStarted, mentions, Utils.Wrap(scheduledEvent.Name), @@ -167,9 +204,9 @@ public static class EventHandler { private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; var channel = Utils.GetEventNotificationChannel(guild); - Utils.SetCurrentLanguage(guild.Id); + Utils.SetCurrentLanguage(guild); if (channel is not null) await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), - Utils.GetHumanizedTimeOffset(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); + Utils.GetHumanizedTimeSpan(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); } -} \ No newline at end of file +} diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 9aea677..8f2be50 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -284,6 +284,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to Adds a reminder. + /// + internal static string CommandDescriptionRemind { + get { + return ResourceManager.GetString("CommandDescriptionRemind", resourceCulture); + } + } + /// /// Looks up a localized string similar to Allows you to change certain preferences for this guild. /// @@ -492,7 +501,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to You need to specify a guild member instead of {0}!. + /// Looks up a localized string similar to You did not specify a member of this guild!. /// internal static string InvalidMember { get { @@ -609,11 +618,11 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to You need to specify a setting to change!. + /// Looks up a localized string similar to You need to specify reminder text!. /// - internal static string MissingSetting { + internal static string MissingReminderText { get { - return ResourceManager.GetString("MissingSetting", resourceCulture); + return ResourceManager.GetString("MissingReminderText", resourceCulture); } } @@ -680,24 +689,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to I couldn't remove role {0} because of an error! {1}. - /// - internal static string RoleRemovalFailed { - get { - return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Someone removed the mute role manually! I added back all roles that I removed during the mute. - /// - internal static string RolesReturned { - get { - return ResourceManager.GetString("RolesReturned", resourceCulture); - } - } - /// /// Looks up a localized string similar to That setting doesn't exist!. /// @@ -717,11 +708,11 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Bot log channel. + /// Looks up a localized string similar to Automatically start scheduled events. /// - internal static string SettingsBotLogChannel { + internal static string SettingsAutoStartEvents { get { - return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); + return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); } } @@ -806,6 +797,24 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to Channel for private notifications. + /// + internal static string SettingsPrivateFeedbackChannel { + get { + return ResourceManager.GetString("SettingsPrivateFeedbackChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for public notifications. + /// + internal static string SettingsPublicFeedbackChannel { + get { + return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Receive startup messages. /// @@ -824,6 +833,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to Return roles on rejoin. + /// + internal static string SettingsReturnRolesOnRejoin { + get { + return ResourceManager.GetString("SettingsReturnRolesOnRejoin", resourceCulture); + } + } + /// /// Looks up a localized string similar to Send welcome messages. /// @@ -1050,11 +1068,11 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to The specified user is not a member of this server!. + /// Looks up a localized string similar to I could not find this user in any guild I'm a member of! Check if the ID is correct and that the user was on this server no longer than 30 days ago. /// - internal static string UserNotInGuild { + internal static string UserNotFound { get { - return ResourceManager.GetString("UserNotInGuild", resourceCulture); + return ResourceManager.GetString("UserNotFound", resourceCulture); } } @@ -1068,7 +1086,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. + /// Looks up a localized string similar to You were banned by {0} in guild `{1}` for {2}. /// internal static string YouWereBanned { get { @@ -1077,7 +1095,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. + /// Looks up a localized string similar to You were kicked by {0} in guild `{1}` for {2}. /// internal static string YouWereKicked { get { diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 56fea2d..e397d73 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -1,64 +1,64 @@  - + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, ... + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + this is my long stringthis is a comment + Blue + + [base64 mime encoded serialized .NET Framework object] + + + [base64 mime encoded string representing a byte array form of the .NET Framework object] + This is a comment + + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> @@ -135,43 +135,43 @@ Bop! - + Beep! - + I do not have permission to execute this command! - + You do not have permission to execute this command! - - You were banned by {0} in guild {1} for {2} - - + + You were banned by {0} in guild `{1}` for {2} + + Punishment expired - + You specified less than {0} messages! - + You specified more than {0} messages! - + Command help: - - You were kicked by {0} in guild {1} for {2} - - + + You were kicked by {0} in guild `{1}` for {2} + + ms - + Member is already muted! - + Not specified - + Not specified @@ -189,16 +189,10 @@ Send welcome messages - - Starter role - - + Mute role - - Bot log channel - - + Language not supported! Supported languages: @@ -213,10 +207,7 @@ Member not muted! - - Someone removed the mute role manually! I added back all roles that I removed during the mute - - + Welcome message @@ -225,9 +216,6 @@ Banned {0} for{1}: {2} - - The specified user is not a member of this server! - That setting doesn't exist! @@ -243,10 +231,7 @@ This channel does not exist! - - I couldn't remove role {0} because of an error! {1} - - + I cannot mute someone for more than 28 days using timeouts! Either specify a duration shorter than 28 days, or set a mute role in settings @@ -333,28 +318,28 @@ You need to specify an integer from {0} to {1}! - + You need to specify a user! - + You need to specify a user instead of {0}! - + You need to specify a guild member! - - You need to specify a guild member instead of {0}! - - + + You did not specify a member of this guild! + + You cannot ban users from this guild! - + You cannot manage messages in this guild! - + You cannot kick members from this guild! - + You cannot moderate members in this guild! @@ -390,10 +375,7 @@ You need to specify a reason for unmute this member! - - You need to specify a setting to change! - - + You cannot ban the owner of this guild! @@ -450,13 +432,37 @@ I cannot unmute this member! - + You cannot unmute this user! - + {0}Event {1} will start <t:{2}:R>! - + Early event start notification offset - \ No newline at end of file + + I could not find this user in any guild I'm a member of! Check if the ID is correct and that the user was on this server no longer than 30 days ago + + + Starter role + + + Adds a reminder + + + Channel for public notifications + + + Channel for private notifications + + + Return roles on rejoin + + + Automatically start scheduled events + + + You need to specify reminder text! + + diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index e6a8836..55b7fb2 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -1,64 +1,64 @@  - + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, ... + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + this is my long stringthis is a comment + Blue + + [base64 mime encoded serialized .NET Framework object] + + + [base64 mime encoded string representing a byte array form of the .NET Framework object] + This is a comment + + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> @@ -135,43 +135,43 @@ Боп! - + Бип! - + У меня недостаточно прав для выполнения этой команды! - + У тебя недостаточно прав для выполнения этой команды! - - Тебя забанил {0} на сервере {1} за {2} - - + + Тебя забанил {0} на сервере `{1}` за {2} + + Время наказания истекло - + Указано менее {0} сообщений! - + Указано более {0} сообщений! - + Справка по командам: - - Тебя кикнул {0} на сервере {1} за {2} - - + + Тебя кикнул {0} на сервере `{1}` за {2} + + мс - + Участник уже заглушен! - + Не указан - + Не указана @@ -192,10 +192,7 @@ Роль мута - - Канал бот-уведомлений - - + Язык не поддерживается! Поддерживаемые языки: @@ -210,10 +207,7 @@ Участник не заглушен! - - Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте - - + Приветствие @@ -222,9 +216,6 @@ Забанен {0} на{1}: {2} - - Указанный пользователь не является участником этого сервера! - Такая настройка не существует! @@ -240,19 +231,13 @@ Этот канал не существует! - - Я не смог забрать роль {0} в связи с ошибкой! {1} - - + Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках - - Начальная роль - - + {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4} @@ -333,28 +318,28 @@ Надо указать целое число от {0} до {1}! - + Надо указать пользователя! - + Надо указать пользователя вместо {0}! - + Надо указать участника сервера! - - Надо указать участника сервера вместо {0}! - - + + Тебе надо указать участника этого сервера! + + Ты не можешь банить пользователей на этом сервере! - + Ты не можешь управлять сообщениями этого сервера! - + Ты не можешь выгонять участников с этого сервера! - + Ты не можешь модерировать участников этого сервера! @@ -384,10 +369,7 @@ Надо указать причину для мута этого участника! - - Надо указать настройку, которую нужно изменить! - - + Надо указать причину для разбана этого пользователя! @@ -450,13 +432,37 @@ Ты не можешь вернуть из мута этого пользователя! - + Я не могу вернуть из мута этого пользователя! - + {0}Событие {1} начнется <t:{2}:R>! - + Офсет отправки преждевременного уведомления о начале события - \ No newline at end of file + + Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад + + + Начальная роль + + + Добавляет напоминание + + + Канал для публичных уведомлений + + + Канал для приватных уведомлений + + + Возвращать роли при перезаходе + + + Автоматически начинать события + + + Тебе нужно указать текст напоминания! + + diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index 8d57c67..c2b67bc 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -1,64 +1,64 @@ - + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, ... + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + this is my long stringthis is a comment + Blue + + [base64 mime encoded serialized .NET Framework object] + + + [base64 mime encoded string representing a byte array form of the .NET Framework object] + This is a comment + + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> @@ -135,43 +135,43 @@ брох! - + брух! - + у меня прав нету, сделай что нибудь. - + у тебя прав нету, твои проблемы. - - здарова, тебя крч забанил {0} на сервере {1} за {2} - - + + здарова, тебя крч забанил {0} на сервере `{1}` за {2} + + время бана закончиловсь - + ты выбрал менее {0} сообщений - + ты выбрал более {0} сообщений - + туториал по приколам: - - здарова, тебя крч кикнул {0} на сервере {1} за {2} - - + + здарова, тебя крч кикнул {0} на сервере `{1}` за {2} + + мс - + шизоид уже замучен! - + *тут ничего нет* - + *тут ничего нет* @@ -192,10 +192,7 @@ роль замученного - - канал бот-уведомлений - - + такого языка нету, ты шо, есть только такие: @@ -210,10 +207,7 @@ шизоид не замучен! - - кто-то решил поумничать и обошел роль мута. я ее вернул. - - + приветствие @@ -222,9 +216,6 @@ забанен {0} на{1}: {2} - - шизик не на этом сервере - такой прикол не существует @@ -240,19 +231,13 @@ этого канала нету, ты шо - - я не украл звание {0} в связи с ошибкой! {1} - - + ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим я не могу замутить ботов, сделай что нибудь - - базовое звание - - + {0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!{4} @@ -333,28 +318,28 @@ укажи целое число от {0} до {1} - + укажи самого шизика - + надо указать юзверя вместо {0}! - + укажи самого шизика - - укажи шизоида сервера вместо {0}! - - + + укажи шизоида сервера! + + бан - + тебе нельзя иметь власть над сообщениями шизоидов - + кик шизиков нельзя - + тебе нельзя управлять шизоидами @@ -384,10 +369,7 @@ укажи зачем мутить шизика - - укажи настройку которую менять нужно - - + укажи зачем раззабанивать шизика @@ -450,13 +432,37 @@ тебе нельзя раззамучивать - + я не могу его раззамутить... - + {0}квест {1} начнется <t:{2}:R>! - + заранее пнуть в минутах до начала квеста - \ No newline at end of file + + у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад) + + + базовое звание + + + крафтит напоминалку + + + канал для секретных уведомлений + + + канал для не секретных уведомлений + + + вернуть звания при переподключении в дурку + + + автоматом стартить квесты + + + для крафта напоминалки нужен текст + + diff --git a/Boyfriend/ReplyEmojis.cs b/Boyfriend/ReplyEmojis.cs index c26d420..a4e8c40 100644 --- a/Boyfriend/ReplyEmojis.cs +++ b/Boyfriend/ReplyEmojis.cs @@ -2,7 +2,6 @@ namespace Boyfriend; public static class ReplyEmojis { public const string Success = ":white_check_mark:"; - public const string Warning = ":warning:"; public const string Error = ":x:"; public const string MissingArgument = ":keyboard:"; public const string InvalidArgument = ":construction:"; diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index b08eca3..1417370 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -1,8 +1,9 @@ -using System.Globalization; +using System.Diagnostics; +using System.Globalization; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using Boyfriend.Commands; +using Boyfriend.Data; using Discord; using Discord.Net; using Discord.WebSocket; @@ -12,15 +13,13 @@ 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 MuteRoleCache = new(); + private static readonly Dictionary ReflectionMessageCache = new(); private static readonly AllowedMentions AllowRoles = new() { AllowedTypes = AllowedMentionTypes.Roles @@ -30,11 +29,6 @@ public static partial class Utils { return GetMessage($"Beep{(i < 0 ? Random.Shared.Next(3) + 1 : ++i)}"); } - public static SocketTextChannel? GetBotLogChannel(ulong id) { - return Boyfriend.Client.GetGuild(id) - .GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(id)["BotLogChannel"])); - } - public static string? Wrap(string? original, bool limitedSpace = false) { if (original is null) return null; var maxChars = limitedSpace ? 970 : 1940; @@ -57,26 +51,10 @@ 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) - 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) { @@ -109,22 +87,23 @@ public static partial class Utils { } public static async Task - SendFeedbackAsync(string feedback, ulong guildId, string mention, bool sendPublic = false) { - var adminChannel = GetBotLogChannel(guildId); - var systemChannel = Boyfriend.Client.GetGuild(guildId).SystemChannel; + SendFeedbackAsync(string feedback, SocketGuild guild, string mention, bool sendPublic = false) { + var data = GuildData.Get(guild); + var adminChannel = data.PrivateFeedbackChannel; + var systemChannel = data.PublicFeedbackChannel; var toSend = $"*[{mention}: {feedback}]*"; if (adminChannel is not null) await SilentSendAsync(adminChannel, toSend); if (sendPublic && systemChannel is not null) await SilentSendAsync(systemChannel, toSend); } - public static string GetHumanizedTimeOffset(TimeSpan span) { - return span.TotalSeconds > 0 - ? $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture.Name.Contains("RU") ? CultureInfoCache["ru"] : Messages.Culture)}" - : Messages.Ever; + public static string GetHumanizedTimeSpan(TimeSpan span) { + return span.TotalSeconds < 1 + ? Messages.Ever + : $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture.Name.Contains("RU") ? CultureInfoCache["ru"] : Messages.Culture)}"; } - public static void SetCurrentLanguage(ulong guildId) { - Messages.Culture = CultureInfoCache[Boyfriend.GetGuildConfig(guildId)["Lang"]]; + public static void SetCurrentLanguage(SocketGuild guild) { + Messages.Culture = CultureInfoCache[GuildData.Get(guild).Preferences["Lang"]]; } public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketTextChannel? channel) { @@ -146,48 +125,37 @@ 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"])); + return guild.GetTextChannel(ParseMention(GuildData.Get(guild) + .Preferences["EventNotificationChannel"])); + } + + public static bool UserExists(ulong id) { + return Boyfriend.Client.GetUser(id) is not null || UserInMemberData(id); + } + + private static bool UserInMemberData(ulong id) { + return GuildData.GuildDataDictionary.Values.Any(gData => gData.MemberData.Values.Any(mData => mData.Id == id)); + } + + public static async Task UnmuteMemberAsync(GuildData data, string modDiscrim, SocketGuildUser toUnmute, + string reason) { + var requestOptions = GetRequestOptions($"({modDiscrim}) {reason}"); + var role = data.MuteRole; + + if (role is not null) { + if (!toUnmute.Roles.Contains(role)) return false; + if (data.Preferences["RemoveRolesOnMute"] is "true") + await toUnmute.AddRolesAsync(data.MemberData[toUnmute.Id].Roles, requestOptions); + await toUnmute.RemoveRoleAsync(role, requestOptions); + data.MemberData[toUnmute.Id].MutedUntil = null; + } else { + if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value < DateTimeOffset.Now) return false; + + await toUnmute.RemoveTimeOutAsync(requestOptions); + } + + return true; } [GeneratedRegex("[^0-9]")]