From 3fa19a47943252cd8b15df70eab23d4085757ee4 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Tue, 7 Dec 2021 17:52:37 +0500 Subject: [PATCH 001/329] Initial commit --- .gitignore | 8 +++++++ Boyfriend-CSharp.sln | 16 ++++++++++++++ Boyfriend/Boyfriend.cs | 28 +++++++++++++++++++++++++ Boyfriend/Boyfriend.csproj | 20 ++++++++++++++++++ Boyfriend/Boyfriend.csproj.DotSettings | 3 +++ Boyfriend/CommandHandler.cs | 29 ++++++++++++++++++++++++++ Boyfriend/Commands/PingModule.cs | 12 +++++++++++ Boyfriend/Utils.cs | 8 +++++++ 8 files changed, 124 insertions(+) create mode 100644 .gitignore create mode 100644 Boyfriend-CSharp.sln create mode 100644 Boyfriend/Boyfriend.cs create mode 100644 Boyfriend/Boyfriend.csproj create mode 100644 Boyfriend/Boyfriend.csproj.DotSettings create mode 100644 Boyfriend/CommandHandler.cs create mode 100644 Boyfriend/Commands/PingModule.cs create mode 100644 Boyfriend/Utils.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2976bab --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea/ +*.user +token.txt +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/Boyfriend-CSharp.sln b/Boyfriend-CSharp.sln new file mode 100644 index 0000000..003c58b --- /dev/null +++ b/Boyfriend-CSharp.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Boyfriend", "Boyfriend\Boyfriend.csproj", "{21640A7A-75C2-4515-A1DF-CE8B6EEBD260}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs new file mode 100644 index 0000000..89853d8 --- /dev/null +++ b/Boyfriend/Boyfriend.cs @@ -0,0 +1,28 @@ +using Discord; +using Discord.WebSocket; + +namespace Boyfriend; + public class Boyfriend { + + public static void Main(string[] args) + => new Boyfriend().MainAsync().GetAwaiter().GetResult(); + + public static readonly DiscordSocketClient Client = new(); + + private async Task MainAsync() { + Client.Log += Log; + var token = File.ReadAllText("token.txt").Trim(); + + await Client.LoginAsync(TokenType.Bot, token); + await Client.StartAsync(); + + await new CommandHandler().InstallCommandsAsync(); + + await Task.Delay(-1); + } + + private static Task Log(LogMessage msg) { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + } + } \ No newline at end of file diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj new file mode 100644 index 0000000..9d8eac3 --- /dev/null +++ b/Boyfriend/Boyfriend.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + default + Boyfriend + l1ttle + https://github.com/l1ttleO/Boyfriend-CSharp + https://github.com/l1ttleO/Boyfriend-CSharp + git + + + + + + + diff --git a/Boyfriend/Boyfriend.csproj.DotSettings b/Boyfriend/Boyfriend.csproj.DotSettings new file mode 100644 index 0000000..25ce924 --- /dev/null +++ b/Boyfriend/Boyfriend.csproj.DotSettings @@ -0,0 +1,3 @@ + + Yes + \ No newline at end of file diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs new file mode 100644 index 0000000..5f000ba --- /dev/null +++ b/Boyfriend/CommandHandler.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using Discord.Commands; +using Discord.WebSocket; + +namespace Boyfriend; + +public class CommandHandler { + private readonly DiscordSocketClient _client = Boyfriend.Client; + private readonly CommandService _commands = new CommandService(); + + public async Task InstallCommandsAsync() { + _client.MessageReceived += HandleCommandAsync; + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), null); + } + + private async Task HandleCommandAsync(SocketMessage messageParam) { + if (messageParam is not SocketUserMessage message) return; + var argPos = 0; + + if (!(message.HasCharPrefix('!', ref argPos) || + message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || + message.Author.IsBot) + return; + + var context = new SocketCommandContext(_client, message); + + await _commands.ExecuteAsync(context, argPos, null); + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/PingModule.cs b/Boyfriend/Commands/PingModule.cs new file mode 100644 index 0000000..c818ee2 --- /dev/null +++ b/Boyfriend/Commands/PingModule.cs @@ -0,0 +1,12 @@ +using Discord.Commands; + +namespace Boyfriend.Commands; + +public class PingModule : ModuleBase { + + [Command("ping")] + [Summary("Измеряет время обработки REST-запроса")] + [Alias("пинг")] + public async Task Run() + => await ReplyAsync(Utils.GetBeep() + Boyfriend.Client.Latency + "мс"); +} \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs new file mode 100644 index 0000000..6b6524b --- /dev/null +++ b/Boyfriend/Utils.cs @@ -0,0 +1,8 @@ +namespace Boyfriend; + +public static class Utils { + public static string GetBeep() { + var letters = new[] { "а", "о", "и"}; + return "Б" + letters[new Random().Next(3)] + "п! "; + } +} \ No newline at end of file From 8644f70b14592d8ffd50235f6f172cfc964ee1b9 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Tue, 7 Dec 2021 23:27:27 +0500 Subject: [PATCH 002/329] tired --- Boyfriend/Boyfriend.cs | 17 ++++--- Boyfriend/CommandHandler.cs | 29 ------------ Boyfriend/Commands/BanModule.cs | 26 +++++++++++ Boyfriend/Commands/PingModule.cs | 2 + Boyfriend/Commands/Unban.cs | 23 ++++++++++ Boyfriend/EventHandler.cs | 79 ++++++++++++++++++++++++++++++++ Boyfriend/Utils.cs | 19 +++++++- 7 files changed, 158 insertions(+), 37 deletions(-) delete mode 100644 Boyfriend/CommandHandler.cs create mode 100644 Boyfriend/Commands/BanModule.cs create mode 100644 Boyfriend/Commands/Unban.cs create mode 100644 Boyfriend/EventHandler.cs diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 89853d8..cea4da0 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -2,21 +2,24 @@ using Discord.WebSocket; namespace Boyfriend; - public class Boyfriend { + public static class Boyfriend { - public static void Main(string[] args) - => new Boyfriend().MainAsync().GetAwaiter().GetResult(); + public static void Main() + => Init().GetAwaiter().GetResult(); - public static readonly DiscordSocketClient Client = new(); + private static readonly DiscordSocketConfig Config = new() { + MessageCacheSize = 250 + }; + public static readonly DiscordSocketClient Client = new(Config); - private async Task MainAsync() { + private static async Task Init() { Client.Log += Log; - var token = File.ReadAllText("token.txt").Trim(); + var token = (await File.ReadAllTextAsync("token.txt")).Trim(); await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); - await new CommandHandler().InstallCommandsAsync(); + await new EventHandler().InitEvents(); await Task.Delay(-1); } diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs deleted file mode 100644 index 5f000ba..0000000 --- a/Boyfriend/CommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -using Discord.Commands; -using Discord.WebSocket; - -namespace Boyfriend; - -public class CommandHandler { - private readonly DiscordSocketClient _client = Boyfriend.Client; - private readonly CommandService _commands = new CommandService(); - - public async Task InstallCommandsAsync() { - _client.MessageReceived += HandleCommandAsync; - await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), null); - } - - private async Task HandleCommandAsync(SocketMessage messageParam) { - if (messageParam is not SocketUserMessage message) return; - var argPos = 0; - - if (!(message.HasCharPrefix('!', ref argPos) || - message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || - message.Author.IsBot) - return; - - var context = new SocketCommandContext(_client, message); - - await _commands.ExecuteAsync(context, argPos, null); - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/BanModule.cs b/Boyfriend/Commands/BanModule.cs new file mode 100644 index 0000000..973059f --- /dev/null +++ b/Boyfriend/Commands/BanModule.cs @@ -0,0 +1,26 @@ +using Discord; +using Discord.Commands; +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +public class BanModule : ModuleBase { + + [Command("ban")] + [Summary("Банит пользователя")] + [Alias("бан")] + public async Task Run(IUser toBan, TimeSpan duration, [Remainder]string reason) + => await BanUser(Context.Guild, Context.User, toBan, duration, reason); + + public async void BanUser(IGuild guild, IUser author, IUser toBan, TimeSpan duration, string reason = "") { + var authorMention = author.Mention; + await toBan.SendMessageAsync("Тебя забанил " + authorMention + " за " + reason); + await guild.AddBanAsync(toBan, 0, reason); + await guild.GetSystemChannelAsync().Result.SendMessageAsync(authorMention + " банит " + toBan.Mention + " за " + + reason); + var banTimer = new System.Timers.Timer(duration.Milliseconds); + banTimer.Elapsed += UnbanModule.UnbanUser(guild, author, toBan, "Время наказания истекло").; + banTimer.Start(); + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/PingModule.cs b/Boyfriend/Commands/PingModule.cs index c818ee2..39d0ad7 100644 --- a/Boyfriend/Commands/PingModule.cs +++ b/Boyfriend/Commands/PingModule.cs @@ -1,4 +1,6 @@ using Discord.Commands; +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global namespace Boyfriend.Commands; diff --git a/Boyfriend/Commands/Unban.cs b/Boyfriend/Commands/Unban.cs new file mode 100644 index 0000000..0ef4361 --- /dev/null +++ b/Boyfriend/Commands/Unban.cs @@ -0,0 +1,23 @@ +using Discord; +using Discord.Commands; +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +public class UnbanModule : ModuleBase { + + [Command("unban")] + [Summary("Возвращает пользователя из бана")] + [Alias("разбан")] + public async Task Run(IUser toBan, TimeSpan duration, [Remainder]string reason) + => await UnbanUser(Context.Guild, Context.User, toBan, reason); + + public async Task UnbanUser(IGuild guild, IUser author, IUser toBan, string reason = "") { + var authorMention = author.Mention; + await toBan.SendMessageAsync("Тебя разбанил " + authorMention + " за " + reason); + await guild.RemoveBanAsync(toBan); + await guild.GetSystemChannelAsync().Result.SendMessageAsync(authorMention + " возвращает из бана " + + toBan.Mention + " за " + reason); + } +} \ No newline at end of file diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs new file mode 100644 index 0000000..95d970b --- /dev/null +++ b/Boyfriend/EventHandler.cs @@ -0,0 +1,79 @@ +using System.Reflection; +using Boyfriend.Commands; +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +namespace Boyfriend; + +public class EventHandler { + private readonly DiscordSocketClient _client = Boyfriend.Client; + private readonly CommandService _commands = new(); + + public async Task InitEvents() { + _client.Ready += ReadyEvent; + _client.MessageDeleted += MessageDeletedEvent; + _client.MessageReceived += MessageReceivedEvent; + _client.MessageUpdated += MessageUpdatedEvent; + _client.UserJoined += UserJoinedEvent; + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), null); + } + + [Obsolete("Stop hard-coding things!")] + private async Task ReadyEvent() { + if (_client.GetChannel(618044439939645444) is not IMessageChannel botLogChannel) + throw new ArgumentException("Invalid bot log channel"); + await botLogChannel.SendMessageAsync(Utils.GetBeep() + + "Я запустился! (C#)"); + } + + private static async Task MessageDeletedEvent(Cacheable message, ISocketMessageChannel channel) { + var msg = message.Value; + string toSend; + if (msg == null) + toSend = "Удалено сообщение в канале " + Utils.MentionChannel(channel.Id) + ", но я забыл что там было"; + else + toSend = "Удалено сообщение от " + msg.Author.Mention + " в канале " + Utils.MentionChannel(channel.Id) + + ": " + Utils.Wrap(msg.Content); + await Utils.GetAdminLogChannel().SendMessageAsync(toSend); + } + + private async Task MessageReceivedEvent(SocketMessage messageParam) { + if (messageParam is not SocketUserMessage {Author: IGuildUser user} message) return; + var argPos = 0; + var guild = user.Guild; + + if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) + && !user.GuildPermissions.MentionEveryone) + await new BanModule().BanUser(guild, guild.GetCurrentUserAsync().Result, user, TimeSpan.Zero, + "Более 3-ёх упоминаний в одном сообщении"); + + if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || + message.Author.IsBot) + return; + + var context = new SocketCommandContext(_client, message); + + await _commands.ExecuteAsync(context, argPos, null); + } + + private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, + ISocketMessageChannel channel) { + var msg = messageCached.Value; + string toSend; + if (msg == null) + toSend = "Отредактировано сообщение в канале " + + Utils.MentionChannel(channel.Id) + ", но я забыл что там было до редактирования: " + + Utils.Wrap(messageSocket.Content); + else + toSend = "Отредактировано сообщение от " + msg.Author.Mention + " в канале " + + Utils.MentionChannel(channel.Id) + ": " + Utils.Wrap(msg.Content) + + Utils.Wrap(messageSocket.Content); + await Utils.GetAdminLogChannel().SendMessageAsync(toSend); + } + + private static async Task UserJoinedEvent(SocketGuildUser user) { + await user.Guild.SystemChannel.SendMessageAsync(user.Mention + ", добро пожаловать на сервер " + + user.Guild.Name); + } +} \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 6b6524b..29dec57 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -1,8 +1,25 @@ -namespace Boyfriend; +using Discord; + +namespace Boyfriend; public static class Utils { public static string GetBeep() { var letters = new[] { "а", "о", "и"}; return "Б" + letters[new Random().Next(3)] + "п! "; } + + [Obsolete("Stop hard-coding things!")] + public static IMessageChannel GetAdminLogChannel() { + if (Boyfriend.Client.GetChannel(870929165141032971) is not IMessageChannel adminLogChannel) + throw new ArgumentException("Invalid admin log channel"); + return adminLogChannel; + } + + public static string Wrap(string original) { + return original.Trim().Equals("") ? "" : "```" + original.Replace("```", "​`​`​`​") + "```"; + } + + public static string MentionChannel(ulong id) { + return "<#" + id + ">"; + } } \ No newline at end of file From 382add19a3ce33efccf9a7524d34d702ee682002 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Fri, 10 Dec 2021 16:25:29 +0500 Subject: [PATCH 003/329] a ton of stuff --- Boyfriend/Boyfriend.cs | 4 +- Boyfriend/Boyfriend.csproj | 2 +- Boyfriend/Boyfriend.csproj.DotSettings | 2 +- Boyfriend/Commands/BanModule.cs | 30 ++++++---- Boyfriend/Commands/ClearModule.cs | 34 +++++++++++ Boyfriend/Commands/KickModule.cs | 34 +++++++++++ Boyfriend/Commands/MuteModule.cs | 35 +++++++++++ Boyfriend/Commands/PingModule.cs | 2 +- Boyfriend/Commands/Unban.cs | 23 -------- Boyfriend/Commands/UnbanModule.cs | 29 +++++++++ Boyfriend/Commands/UnmuteModule.cs | 29 +++++++++ Boyfriend/EventHandler.cs | 82 ++++++++++++++++++-------- Boyfriend/Utils.cs | 65 ++++++++++++++++++-- 13 files changed, 304 insertions(+), 67 deletions(-) create mode 100644 Boyfriend/Commands/ClearModule.cs create mode 100644 Boyfriend/Commands/KickModule.cs create mode 100644 Boyfriend/Commands/MuteModule.cs delete mode 100644 Boyfriend/Commands/Unban.cs create mode 100644 Boyfriend/Commands/UnbanModule.cs create mode 100644 Boyfriend/Commands/UnmuteModule.cs diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index cea4da0..9937d0e 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -8,7 +8,8 @@ namespace Boyfriend; => Init().GetAwaiter().GetResult(); private static readonly DiscordSocketConfig Config = new() { - MessageCacheSize = 250 + MessageCacheSize = 250, + GatewayIntents = GatewayIntents.All }; public static readonly DiscordSocketClient Client = new(Config); @@ -18,6 +19,7 @@ namespace Boyfriend; await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); + await Client.SetActivityAsync(new Game("Retrospecter - Electrospasm", ActivityType.Listening)); await new EventHandler().InitEvents(); diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index 9d8eac3..9b9566e 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -14,7 +14,7 @@ - + diff --git a/Boyfriend/Boyfriend.csproj.DotSettings b/Boyfriend/Boyfriend.csproj.DotSettings index 25ce924..47a5106 100644 --- a/Boyfriend/Boyfriend.csproj.DotSettings +++ b/Boyfriend/Boyfriend.csproj.DotSettings @@ -1,3 +1,3 @@  - Yes + No \ No newline at end of file diff --git a/Boyfriend/Commands/BanModule.cs b/Boyfriend/Commands/BanModule.cs index 973059f..049c203 100644 --- a/Boyfriend/Commands/BanModule.cs +++ b/Boyfriend/Commands/BanModule.cs @@ -1,7 +1,9 @@ using Discord; using Discord.Commands; + // ReSharper disable UnusedType.Global // ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global namespace Boyfriend.Commands; @@ -10,17 +12,25 @@ public class BanModule : ModuleBase { [Command("ban")] [Summary("Банит пользователя")] [Alias("бан")] - public async Task Run(IUser toBan, TimeSpan duration, [Remainder]string reason) - => await BanUser(Context.Guild, Context.User, toBan, duration, reason); + [RequireBotPermission(GuildPermission.BanMembers)] + [RequireUserPermission(GuildPermission.BanMembers)] + public Task Run(string user, TimeSpan duration, [Remainder]string reason) { + var toBan = Utils.ParseUser(user).Result; + BanUser(Context.Guild, Context.User, toBan, duration, reason); + return Task.CompletedTask; + } - public async void BanUser(IGuild guild, IUser author, IUser toBan, TimeSpan duration, string reason = "") { + public static async void BanUser(IGuild guild, IUser author, IUser toBan, TimeSpan duration, string reason) { var authorMention = author.Mention; - await toBan.SendMessageAsync("Тебя забанил " + authorMention + " за " + reason); - await guild.AddBanAsync(toBan, 0, reason); - await guild.GetSystemChannelAsync().Result.SendMessageAsync(authorMention + " банит " + toBan.Mention + " за " - + reason); - var banTimer = new System.Timers.Timer(duration.Milliseconds); - banTimer.Elapsed += UnbanModule.UnbanUser(guild, author, toBan, "Время наказания истекло").; - banTimer.Start(); + await Utils.SendDirectMessage(toBan, $"Тебя забанил {author.Mention} на сервере {guild.Name} за `{reason}`"); + + var guildBanMessage = $"({author.Username}#{author.Discriminator}) {reason}"; + await guild.AddBanAsync(toBan, 0, guildBanMessage); + var notification = $"{authorMention} банит {toBan.Mention} за {Utils.WrapInline(reason)}"; + await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); + await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + var task = new Task(() => UnbanModule.UnbanUser(guild, guild.GetCurrentUserAsync().Result, toBan, + "Время наказания истекло")); + await Utils.StartDelayed(task, duration, () => guild.GetBanAsync(toBan).Result != null); } } \ No newline at end of file diff --git a/Boyfriend/Commands/ClearModule.cs b/Boyfriend/Commands/ClearModule.cs new file mode 100644 index 0000000..f17dab0 --- /dev/null +++ b/Boyfriend/Commands/ClearModule.cs @@ -0,0 +1,34 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class ClearModule : ModuleBase { + + [Command("clear")] + [Summary("Удаляет указанное количество сообщений")] + [Alias("очистить")] + [RequireBotPermission(GuildPermission.ManageMessages)] + [RequireUserPermission(GuildPermission.ManageMessages)] + public async Task Run(int toDelete) { + if (Context.Channel is not ITextChannel channel) return; + switch (toDelete) { + case < 1: + throw new ArgumentException("toDelete is less than 1."); + case > 200: + throw new ArgumentException("toDelete is more than 200."); + default: { + var messages = await channel.GetMessagesAsync(toDelete + 1).FlattenAsync(); + await channel.DeleteMessagesAsync(messages); + await Utils.GetAdminLogChannel().SendMessageAsync( + $"{Context.User.Mention} удаляет {toDelete + 1} сообщений в канале " + + $"{Utils.MentionChannel(Context.Channel.Id)}"); + break; + } + } + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/KickModule.cs b/Boyfriend/Commands/KickModule.cs new file mode 100644 index 0000000..4989557 --- /dev/null +++ b/Boyfriend/Commands/KickModule.cs @@ -0,0 +1,34 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class KickModule : ModuleBase { + + [Command("kick")] + [Summary("Выгоняет пользователя")] + [Alias("кик")] + [RequireBotPermission(GuildPermission.KickMembers)] + [RequireUserPermission(GuildPermission.KickMembers)] + public Task Run(string user, [Remainder]string reason) { + var toKick = Utils.ParseMember(Context.Guild, user).Result; + KickMember(Context.Guild, Context.User, toKick, reason); + return Task.CompletedTask; + } + + private static async void KickMember(IGuild guild, IUser author, IGuildUser toKick, string reason) { + var authorMention = author.Mention; + await Utils.SendDirectMessage(toKick, $"Тебя кикнул {authorMention} на сервере {guild.Name} за " + + $"{Utils.WrapInline(reason)}"); + + var guildKickMessage = $"({author.Username}#{author.Discriminator}) {reason}"; + await toKick.KickAsync(guildKickMessage); + var notification = $"{authorMention} выгоняет {toKick.Mention} за {Utils.WrapInline(reason)}"; + await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); + await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/MuteModule.cs b/Boyfriend/Commands/MuteModule.cs new file mode 100644 index 0000000..f95cbd3 --- /dev/null +++ b/Boyfriend/Commands/MuteModule.cs @@ -0,0 +1,35 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class MuteModule : ModuleBase { + + [Command("mute")] + [Summary("Глушит пользователя")] + [Alias("мут")] + [RequireBotPermission(GuildPermission.ManageRoles)] + [RequireUserPermission(GuildPermission.ManageMessages)] + public Task Run(string user, TimeSpan duration, [Remainder]string reason) { + var toMute = Utils.ParseMember(Context.Guild, user).Result; + MuteMember(Context.Guild, Context.User, toMute, duration, reason); + return Task.CompletedTask; + } + + private static async void MuteMember(IGuild guild, IMentionable author, IGuildUser toMute, TimeSpan duration, + string reason) { + var authorMention = author.Mention; + var role = Utils.GetMuteRole(guild); + await toMute.AddRoleAsync(role); + var notification = $"{authorMention} глушит {toMute.Mention} за {Utils.WrapInline(reason)}"; + await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); + await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + var task = new Task(() => UnmuteModule.UnmuteMember(guild, guild.GetCurrentUserAsync().Result, toMute, + "Время наказания истекло")); + await Utils.StartDelayed(task, duration, () => toMute.RoleIds.Any(x => x == role.Id)); + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/PingModule.cs b/Boyfriend/Commands/PingModule.cs index 39d0ad7..6c86073 100644 --- a/Boyfriend/Commands/PingModule.cs +++ b/Boyfriend/Commands/PingModule.cs @@ -10,5 +10,5 @@ public class PingModule : ModuleBase { [Summary("Измеряет время обработки REST-запроса")] [Alias("пинг")] public async Task Run() - => await ReplyAsync(Utils.GetBeep() + Boyfriend.Client.Latency + "мс"); + => await ReplyAsync($"{Utils.GetBeep()}{Boyfriend.Client.Latency}мс"); } \ No newline at end of file diff --git a/Boyfriend/Commands/Unban.cs b/Boyfriend/Commands/Unban.cs deleted file mode 100644 index 0ef4361..0000000 --- a/Boyfriend/Commands/Unban.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Discord; -using Discord.Commands; -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global - -namespace Boyfriend.Commands; - -public class UnbanModule : ModuleBase { - - [Command("unban")] - [Summary("Возвращает пользователя из бана")] - [Alias("разбан")] - public async Task Run(IUser toBan, TimeSpan duration, [Remainder]string reason) - => await UnbanUser(Context.Guild, Context.User, toBan, reason); - - public async Task UnbanUser(IGuild guild, IUser author, IUser toBan, string reason = "") { - var authorMention = author.Mention; - await toBan.SendMessageAsync("Тебя разбанил " + authorMention + " за " + reason); - await guild.RemoveBanAsync(toBan); - await guild.GetSystemChannelAsync().Result.SendMessageAsync(authorMention + " возвращает из бана " - + toBan.Mention + " за " + reason); - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/UnbanModule.cs b/Boyfriend/Commands/UnbanModule.cs new file mode 100644 index 0000000..0a2e841 --- /dev/null +++ b/Boyfriend/Commands/UnbanModule.cs @@ -0,0 +1,29 @@ +using Discord; +using Discord.Commands; +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class UnbanModule : ModuleBase { + + [Command("unban")] + [Summary("Возвращает пользователя из бана")] + [Alias("разбан")] + [RequireBotPermission(GuildPermission.BanMembers)] + [RequireUserPermission(GuildPermission.BanMembers)] + public Task Run(string user, [Remainder] string reason) { + var toBan = Utils.ParseUser(user).Result; + UnbanUser(Context.Guild, Context.User, toBan, reason); + return Task.CompletedTask; + } + + public static async void UnbanUser(IGuild guild, IUser author, IUser toUnban, string reason) { + var authorMention = author.Mention; + var notification = $"{authorMention} возвращает из бана {toUnban.Mention} за {Utils.WrapInline(reason)}"; + await guild.RemoveBanAsync(toUnban); + await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); + await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/UnmuteModule.cs b/Boyfriend/Commands/UnmuteModule.cs new file mode 100644 index 0000000..6135909 --- /dev/null +++ b/Boyfriend/Commands/UnmuteModule.cs @@ -0,0 +1,29 @@ +using Discord; +using Discord.Commands; +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class UnmuteModule : ModuleBase { + + [Command("unmute")] + [Summary("Возвращает пользователя из мута")] + [Alias("размут")] + [RequireBotPermission(GuildPermission.ManageRoles)] + [RequireUserPermission(GuildPermission.ManageMessages)] + public Task Run(string user, [Remainder] string reason) { + var toUnmute = Utils.ParseMember(Context.Guild, user).Result; + UnmuteMember(Context.Guild, Context.User, toUnmute, reason); + return Task.CompletedTask; + } + + public static async void UnmuteMember(IGuild guild, IUser author, IGuildUser toUnmute, string reason) { + var authorMention = author.Mention; + var notification = $"{authorMention} возвращает из мута {toUnmute.Mention} за {Utils.WrapInline(reason)}"; + await toUnmute.RemoveRoleAsync(Utils.GetMuteRole(guild)); + await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); + await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + } +} \ No newline at end of file diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 95d970b..2c14e6d 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -19,23 +19,57 @@ public class EventHandler { await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), null); } + private static async Task HandleErrors(SocketCommandContext context, IResult result) { + var channel = context.Channel; + var reason = Utils.WrapInline(result.ErrorReason); + switch (result.Error) { + case CommandError.Exception: + await channel.SendMessageAsync($"Произошла непредвиденная ошибка при выполнении команды: {reason}"); + break; + case CommandError.Unsuccessful: + await channel.SendMessageAsync($"Выполнение команды завершилось неудачей: {reason}"); + break; + case CommandError.MultipleMatches: + await channel.SendMessageAsync($"Обнаружены повторяющиеся типы аргументов! {reason}"); + break; + case CommandError.ParseFailed: + await channel.SendMessageAsync($"Не удалось обработать команду: {reason}"); + break; + case CommandError.UnknownCommand: + await channel.SendMessageAsync($"Неизвестная команда! {reason}"); + break; + case CommandError.UnmetPrecondition: + await channel.SendMessageAsync($"У тебя недостаточно прав для выполнения этой команды! {reason}"); + break; + case CommandError.BadArgCount: + await channel.SendMessageAsync($"Неверное количество аргументов! {reason}"); + break; + case CommandError.ObjectNotFound: + await channel.SendMessageAsync($"Нету нужных аргументов! {reason}"); + break; + case null: + break; + default: + throw new ArgumentException("CommandError"); + + } + } + [Obsolete("Stop hard-coding things!")] private async Task ReadyEvent() { if (_client.GetChannel(618044439939645444) is not IMessageChannel botLogChannel) throw new ArgumentException("Invalid bot log channel"); - await botLogChannel.SendMessageAsync(Utils.GetBeep() + - "Я запустился! (C#)"); + await botLogChannel.SendMessageAsync($"{Utils.GetBeep()}Я запустился! (C#)"); } - private static async Task MessageDeletedEvent(Cacheable message, ISocketMessageChannel channel) { + private static async Task MessageDeletedEvent(Cacheable message, + Cacheable channel) { var msg = message.Value; - string toSend; - if (msg == null) - toSend = "Удалено сообщение в канале " + Utils.MentionChannel(channel.Id) + ", но я забыл что там было"; - else - toSend = "Удалено сообщение от " + msg.Author.Mention + " в канале " + Utils.MentionChannel(channel.Id) - + ": " + Utils.Wrap(msg.Content); - await Utils.GetAdminLogChannel().SendMessageAsync(toSend); + var toSend = msg == null + ? "Удалено сообщение в канале {Utils.MentionChannel(channel.Id)}, но я забыл что там было" + : $"Удалено сообщение от {msg.Author.Mention} в канале " + + $"{Utils.MentionChannel(channel.Id)}: {Environment.NewLine}{Utils.Wrap(msg.Content)}"; + await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), toSend); } private async Task MessageReceivedEvent(SocketMessage messageParam) { @@ -45,7 +79,7 @@ public class EventHandler { if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && !user.GuildPermissions.MentionEveryone) - await new BanModule().BanUser(guild, guild.GetCurrentUserAsync().Result, user, TimeSpan.Zero, + BanModule.BanUser(guild, guild.GetCurrentUserAsync().Result, user, TimeSpan.FromMilliseconds(-1), "Более 3-ёх упоминаний в одном сообщении"); if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || @@ -54,26 +88,26 @@ public class EventHandler { var context = new SocketCommandContext(_client, message); - await _commands.ExecuteAsync(context, argPos, null); + var result = await _commands.ExecuteAsync(context, argPos, null); + await HandleErrors(context, result); } private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, ISocketMessageChannel channel) { var msg = messageCached.Value; - string toSend; - if (msg == null) - toSend = "Отредактировано сообщение в канале " - + Utils.MentionChannel(channel.Id) + ", но я забыл что там было до редактирования: " - + Utils.Wrap(messageSocket.Content); - else - toSend = "Отредактировано сообщение от " + msg.Author.Mention + " в канале " - + Utils.MentionChannel(channel.Id) + ": " + Utils.Wrap(msg.Content) - + Utils.Wrap(messageSocket.Content); - await Utils.GetAdminLogChannel().SendMessageAsync(toSend); + var nl = Environment.NewLine; + var toSend = msg == null + ? $"Отредактировано сообщение от {messageSocket.Author.Mention} в канале" + + $" {Utils.MentionChannel(channel.Id)}," + $" но я забыл что там было до редактирования: " + + $"{Utils.Wrap(messageSocket.Content)}" + : $"Отредактировано сообщение от {msg.Author.Mention} " + + $"в канале {Utils.MentionChannel(channel.Id)}." + + $"{nl}До:{nl}{Utils.Wrap(msg.Content)}{nl}После:{nl}{Utils.Wrap(messageSocket.Content)}"; + await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), toSend); } private static async Task UserJoinedEvent(SocketGuildUser user) { - await user.Guild.SystemChannel.SendMessageAsync(user.Mention + ", добро пожаловать на сервер " - + user.Guild.Name); + var guild = user.Guild; + await guild.SystemChannel.SendMessageAsync($"{user.Mention}, добро пожаловать на сервер {guild.Name}"); } } \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 29dec57..9c0ced5 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -1,25 +1,78 @@ -using Discord; +using System.Text.RegularExpressions; +using Discord; +using Discord.Net; namespace Boyfriend; public static class Utils { public static string GetBeep() { var letters = new[] { "а", "о", "и"}; - return "Б" + letters[new Random().Next(3)] + "п! "; + return $"Б{letters[new Random().Next(3)]}п! "; } [Obsolete("Stop hard-coding things!")] - public static IMessageChannel GetAdminLogChannel() { - if (Boyfriend.Client.GetChannel(870929165141032971) is not IMessageChannel adminLogChannel) + public static ITextChannel GetAdminLogChannel() { + if (Boyfriend.Client.GetChannel(870929165141032971) is not ITextChannel adminLogChannel) throw new ArgumentException("Invalid admin log channel"); return adminLogChannel; } public static string Wrap(string original) { - return original.Trim().Equals("") ? "" : "```" + original.Replace("```", "​`​`​`​") + "```"; + var toReturn = original.Replace("```", "​`​`​`​"); + return $"```{toReturn}{(toReturn.EndsWith("`") || toReturn.Trim().Equals("") ? " " : "")}```"; + } + + public static string WrapInline(string original) { + return $"`{original}`"; } public static string MentionChannel(ulong id) { - return "<#" + id + ">"; + return $"<#{id}>"; + } + + public static async Task StartDelayed(Task toRun, TimeSpan delay, Func? condition = null) { + switch (delay.TotalMilliseconds) { + case < -1: + throw new ArgumentOutOfRangeException(nameof(delay), "Указана отрицательная продолжительность!"); + case > int.MaxValue: + throw new ArgumentOutOfRangeException(nameof(delay), "Указана слишком большая продолжительность!"); + } + + await Task.Delay(delay); + var conditionResult = condition?.Invoke() ?? true; + if (conditionResult) + toRun.Start(); + } + + private static ulong ParseMention(string mention) { + return Convert.ToUInt64(Regex.Replace(mention, "[^0-9]", "")); + } + + public static async Task ParseUser(string mention) { + var user = Boyfriend.Client.GetUserAsync(ParseMention(mention)); + return await user; + } + + public static async Task ParseMember(IGuild guild, string mention) { + return await guild.GetUserAsync(ParseMention(mention)); + } + + public static async Task SendDirectMessage(IUser user, string toSend) { + try { + await user.SendMessageAsync(toSend); + } catch (HttpException e) { + if (e.DiscordCode != DiscordErrorCode.CannotSendMessageToUser) + throw; + } + } + + public static IRole GetMuteRole(IGuild guild) { + var role = guild.Roles.FirstOrDefault(x => x.Name.ToLower() is "заключённый" or "muted"); + if (role == null) throw new Exception("Не удалось найти роль мута"); + return role; + } + + public static async Task SilentSendAsync(ITextChannel channel, string text) { + await channel.SendMessageAsync(text, false, null, null, AllowedMentions.None); } } \ No newline at end of file From 270fba5c3cc97758a5d9010406c2c75170bd7ff6 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Wed, 15 Dec 2021 11:19:14 +0500 Subject: [PATCH 004/329] more stuff. yeah --- Boyfriend/Boyfriend.cs | 73 ++++++++++++++++++--------- Boyfriend/CommandHandler.cs | 74 ++++++++++++++++++++++++++++ Boyfriend/Commands/BanModule.cs | 24 ++++++--- Boyfriend/Commands/ClearModule.cs | 7 ++- Boyfriend/Commands/HelpModule.cs | 25 ++++++++++ Boyfriend/Commands/KickModule.cs | 12 ++--- Boyfriend/Commands/MuteModule.cs | 29 ++++++++--- Boyfriend/Commands/SettingsModule.cs | 46 +++++++++++++++++ Boyfriend/Commands/UnbanModule.cs | 11 +++-- Boyfriend/Commands/UnmuteModule.cs | 15 +++--- Boyfriend/EventHandler.cs | 70 ++++++++------------------ Boyfriend/GuildConfig.cs | 22 +++++++++ Boyfriend/Utils.cs | 9 +--- 13 files changed, 301 insertions(+), 116 deletions(-) create mode 100644 Boyfriend/CommandHandler.cs create mode 100644 Boyfriend/Commands/HelpModule.cs create mode 100644 Boyfriend/Commands/SettingsModule.cs create mode 100644 Boyfriend/GuildConfig.cs diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 9937d0e..f643729 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -1,33 +1,62 @@ -using Discord; +using System.Text.Json; +using Discord; using Discord.WebSocket; namespace Boyfriend; - public static class Boyfriend { - public static void Main() - => Init().GetAwaiter().GetResult(); +public static class Boyfriend { - private static readonly DiscordSocketConfig Config = new() { - MessageCacheSize = 250, - GatewayIntents = GatewayIntents.All - }; - public static readonly DiscordSocketClient Client = new(Config); + public static void Main() + => Init().GetAwaiter().GetResult(); - private static async Task Init() { - Client.Log += Log; - var token = (await File.ReadAllTextAsync("token.txt")).Trim(); + private static readonly DiscordSocketConfig Config = new() { + MessageCacheSize = 250, + GatewayIntents = GatewayIntents.All + }; - await Client.LoginAsync(TokenType.Bot, token); - await Client.StartAsync(); - await Client.SetActivityAsync(new Game("Retrospecter - Electrospasm", ActivityType.Listening)); + public static readonly DiscordSocketClient Client = new(Config); - await new EventHandler().InitEvents(); + private static readonly Dictionary GuildConfigDictionary = new(); - await Task.Delay(-1); + private static async Task Init() { + Client.Log += Log; + var token = (await File.ReadAllTextAsync("token.txt")).Trim(); + + await Client.LoginAsync(TokenType.Bot, token); + await Client.StartAsync(); + await Client.SetActivityAsync(new Game("Retrospecter - Electrospasm", ActivityType.Listening)); + + await new EventHandler().InitEvents(); + + await Task.Delay(-1); + } + + private static Task Log(LogMessage msg) { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + } + + public static async Task SetupGuildConfigs() { + foreach (var guild in Client.Guilds) { + var path = "config_" + guild.Id + ".json"; + var openStream = !File.Exists(path) ? File.Create(path) : File.OpenRead(path); + + GuildConfig config; + try { + config = await JsonSerializer.DeserializeAsync(openStream) ?? throw new Exception(); + } catch (JsonException) { + config = new GuildConfig(guild.Id, "ru", "!", false); + } + GuildConfigDictionary.Add(guild.Id, config); } + } - private static Task Log(LogMessage msg) { - Console.WriteLine(msg.ToString()); - return Task.CompletedTask; - } - } \ No newline at end of file + public static GuildConfig GetGuildConfig(IGuild guild) { + GuildConfig toReturn; + toReturn = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] + : new GuildConfig(guild.Id, "ru", "!", false); + + if (toReturn.Id != guild.Id) throw new Exception(); + return toReturn; + } +} \ No newline at end of file diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs new file mode 100644 index 0000000..90026cc --- /dev/null +++ b/Boyfriend/CommandHandler.cs @@ -0,0 +1,74 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +namespace Boyfriend; + +public static class CommandHandler { + public static async Task HandleCommand(SocketUserMessage message, int argPos) { + var context = new SocketCommandContext(Boyfriend.Client, message); + var result = await EventHandler.Commands.ExecuteAsync(context, argPos, null); + + await HandleErrors(context, result); + } + private static async Task HandleErrors(SocketCommandContext context, IResult result) { + var channel = context.Channel; + var reason = Utils.WrapInline(result.ErrorReason); + switch (result.Error) { + case CommandError.Exception: + await channel.SendMessageAsync(reason); + break; + case CommandError.Unsuccessful: + await channel.SendMessageAsync($"Выполнение команды завершилось неудачей: {reason}"); + break; + case CommandError.MultipleMatches: + await channel.SendMessageAsync($"Обнаружены повторяющиеся типы аргументов! {reason}"); + break; + case CommandError.ParseFailed: + await channel.SendMessageAsync($"Не удалось обработать команду: {reason}"); + break; + case CommandError.UnknownCommand: + await channel.SendMessageAsync($"Неизвестная команда! {reason}"); + break; + case CommandError.UnmetPrecondition: + await channel.SendMessageAsync($"У тебя недостаточно прав для выполнения этой к: {reason}"); + break; + case CommandError.BadArgCount: + await channel.SendMessageAsync($"Неверное количество аргументов! {reason}"); + break; + case CommandError.ObjectNotFound: + await channel.SendMessageAsync($"Нету нужных аргументов! {reason}"); + break; + case null: + break; + default: + throw new Exception("CommandError"); + } + } + + public static async Task CheckPermissions(IGuildUser user, GuildPermission toCheck, + GuildPermission forBot = GuildPermission.StartEmbeddedActivities) { + if (forBot == GuildPermission.StartEmbeddedActivities) forBot = toCheck; + if (!(await user.Guild.GetCurrentUserAsync()).GuildPermissions.Has(forBot)) + throw new Exception("У меня недостаточно прав для выполнения этой команды!"); + if (!user.GuildPermissions.Has(toCheck)) + throw new Exception("У тебя недостаточно прав для выполнения этой команды!"); + } + + public static async Task CheckInteractions(IGuildUser actor, IGuildUser target) { + if (actor.Guild != target.Guild) + throw new Exception("Участники находятся в разных гильдиях!"); + var me = await target.Guild.GetCurrentUserAsync(); + if (actor.Id == actor.Guild.OwnerId) return; + if (target.Id == target.Guild.OwnerId) + throw new Exception("Ты не можешь взаимодействовать с владельцем сервера!"); + if (actor == target) + throw new Exception("Ты не можешь взаимодействовать с самим собой!"); + if (target == me) + throw new Exception("Ты не можешь со мной взаимодействовать!"); + if (actor.Hierarchy <= target.Hierarchy) + throw new Exception("Ты не можешь взаимодействовать с этим участником!"); + if (me.Hierarchy <= target.Hierarchy) + throw new Exception("Я не могу взаимодействовать с этим участником!"); + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/BanModule.cs b/Boyfriend/Commands/BanModule.cs index 049c203..a89c1d5 100644 --- a/Boyfriend/Commands/BanModule.cs +++ b/Boyfriend/Commands/BanModule.cs @@ -12,18 +12,26 @@ public class BanModule : ModuleBase { [Command("ban")] [Summary("Банит пользователя")] [Alias("бан")] - [RequireBotPermission(GuildPermission.BanMembers)] - [RequireUserPermission(GuildPermission.BanMembers)] - public Task Run(string user, TimeSpan duration, [Remainder]string reason) { - var toBan = Utils.ParseUser(user).Result; - BanUser(Context.Guild, Context.User, toBan, duration, reason); - return Task.CompletedTask; + public async Task Run(string user, string durationString, [Remainder]string reason) { + TimeSpan duration; + try { + duration = TimeSpan.Parse(durationString); + } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { + duration = TimeSpan.FromMilliseconds(-1); + reason = durationString + reason; + } + var author = Context.Guild.GetUser(Context.User.Id); + var toBan = await Utils.ParseUser(user); + await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); + var memberToBan = Context.Guild.GetUser(toBan.Id); + if (memberToBan != null) + await CommandHandler.CheckInteractions(author, memberToBan); + BanUser(Context.Guild, Context.Guild.GetUser(Context.User.Id), toBan, duration, reason); } - public static async void BanUser(IGuild guild, IUser author, IUser toBan, TimeSpan duration, string reason) { + public static async void BanUser(IGuild guild, IGuildUser author, IUser toBan, TimeSpan duration, string reason) { var authorMention = author.Mention; await Utils.SendDirectMessage(toBan, $"Тебя забанил {author.Mention} на сервере {guild.Name} за `{reason}`"); - var guildBanMessage = $"({author.Username}#{author.Discriminator}) {reason}"; await guild.AddBanAsync(toBan, 0, guildBanMessage); var notification = $"{authorMention} банит {toBan.Mention} за {Utils.WrapInline(reason)}"; diff --git a/Boyfriend/Commands/ClearModule.cs b/Boyfriend/Commands/ClearModule.cs index f17dab0..bd5f87f 100644 --- a/Boyfriend/Commands/ClearModule.cs +++ b/Boyfriend/Commands/ClearModule.cs @@ -12,15 +12,14 @@ public class ClearModule : ModuleBase { [Command("clear")] [Summary("Удаляет указанное количество сообщений")] [Alias("очистить")] - [RequireBotPermission(GuildPermission.ManageMessages)] - [RequireUserPermission(GuildPermission.ManageMessages)] public async Task Run(int toDelete) { if (Context.Channel is not ITextChannel channel) return; + await CommandHandler.CheckPermissions(Context.Guild.GetUser(Context.User.Id), GuildPermission.ManageMessages); switch (toDelete) { case < 1: - throw new ArgumentException("toDelete is less than 1."); + throw new Exception( "Указано отрицательное количество сообщений!"); case > 200: - throw new ArgumentException("toDelete is more than 200."); + throw new Exception("Указано слишком много сообщений!"); default: { var messages = await channel.GetMessagesAsync(toDelete + 1).FlattenAsync(); await channel.DeleteMessagesAsync(messages); diff --git a/Boyfriend/Commands/HelpModule.cs b/Boyfriend/Commands/HelpModule.cs new file mode 100644 index 0000000..cafea6d --- /dev/null +++ b/Boyfriend/Commands/HelpModule.cs @@ -0,0 +1,25 @@ +using Discord.Commands; +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +public class HelpModule : ModuleBase { + + [Command("help")] + [Summary("Показывает эту справку")] + [Alias("помощь", "справка")] + public Task Run() { + var nl = Environment.NewLine; + var toSend = $"Справка по командам:{nl}"; + var prefix = Boyfriend.GetGuildConfig(Context.Guild).Prefix; + foreach (var command in EventHandler.Commands.Commands) { + var aliases = command.Aliases.Aggregate("", (current, alias) => + current + (current == "" ? "" : $", {prefix}") + alias); + toSend += $"`{prefix}{aliases}`: {command.Summary}{nl}"; + } + + Context.Channel.SendMessageAsync(toSend); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/KickModule.cs b/Boyfriend/Commands/KickModule.cs index 4989557..f45f0d0 100644 --- a/Boyfriend/Commands/KickModule.cs +++ b/Boyfriend/Commands/KickModule.cs @@ -12,15 +12,15 @@ public class KickModule : ModuleBase { [Command("kick")] [Summary("Выгоняет пользователя")] [Alias("кик")] - [RequireBotPermission(GuildPermission.KickMembers)] - [RequireUserPermission(GuildPermission.KickMembers)] - public Task Run(string user, [Remainder]string reason) { + public async Task Run(string user, [Remainder]string reason) { + var author = Context.Guild.GetUser(Context.User.Id); var toKick = Utils.ParseMember(Context.Guild, user).Result; - KickMember(Context.Guild, Context.User, toKick, reason); - return Task.CompletedTask; + await CommandHandler.CheckPermissions(author, GuildPermission.KickMembers); + await CommandHandler.CheckInteractions(author, toKick); + KickMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toKick, reason); } - private static async void KickMember(IGuild guild, IUser author, IGuildUser toKick, string reason) { + private static async void KickMember(IGuild guild, IGuildUser author, IGuildUser toKick, string reason) { var authorMention = author.Mention; await Utils.SendDirectMessage(toKick, $"Тебя кикнул {authorMention} на сервере {guild.Name} за " + $"{Utils.WrapInline(reason)}"); diff --git a/Boyfriend/Commands/MuteModule.cs b/Boyfriend/Commands/MuteModule.cs index f95cbd3..9d383c8 100644 --- a/Boyfriend/Commands/MuteModule.cs +++ b/Boyfriend/Commands/MuteModule.cs @@ -12,18 +12,33 @@ public class MuteModule : ModuleBase { [Command("mute")] [Summary("Глушит пользователя")] [Alias("мут")] - [RequireBotPermission(GuildPermission.ManageRoles)] - [RequireUserPermission(GuildPermission.ManageMessages)] - public Task Run(string user, TimeSpan duration, [Remainder]string reason) { - var toMute = Utils.ParseMember(Context.Guild, user).Result; - MuteMember(Context.Guild, Context.User, toMute, duration, reason); - return Task.CompletedTask; + public async Task Run(string user, string durationString, [Remainder]string reason) { + TimeSpan duration; + try { + duration = TimeSpan.Parse(durationString); + } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { + duration = TimeSpan.FromMilliseconds(-1); + reason = durationString + reason; + } + var author = Context.Guild.GetUser(Context.User.Id); + var toMute = await Utils.ParseMember(Context.Guild, user); + await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); + await CommandHandler.CheckInteractions(author, toMute); + MuteMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toMute, duration, reason); } - private static async void MuteMember(IGuild guild, IMentionable author, IGuildUser toMute, TimeSpan duration, + private static async void MuteMember(IGuild guild, IGuildUser author, IGuildUser toMute, TimeSpan duration, string reason) { + await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); var authorMention = author.Mention; var role = Utils.GetMuteRole(guild); + if (Boyfriend.GetGuildConfig(guild).RemoveRolesOnMute) { + foreach (var roleId in toMute.RoleIds) { + await toMute.RemoveRoleAsync(roleId); + + } + } + await toMute.AddRoleAsync(role); var notification = $"{authorMention} глушит {toMute.Mention} за {Utils.WrapInline(reason)}"; await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); diff --git a/Boyfriend/Commands/SettingsModule.cs b/Boyfriend/Commands/SettingsModule.cs new file mode 100644 index 0000000..a528bbf --- /dev/null +++ b/Boyfriend/Commands/SettingsModule.cs @@ -0,0 +1,46 @@ +using Discord.Commands; +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +public class SettingsModule : ModuleBase { + + [Command("settings")] + [Summary("Настраивает бота")] + [Alias("config", "настройки", "конфиг")] + public async Task Run([Remainder] string s = "") { + var config = Boyfriend.GetGuildConfig(Context.Guild); + var sArray = s.Split(" "); + if (s == "") { + var nl = Environment.NewLine; + await Context.Channel.SendMessageAsync($"Текущие настройки:{nl}Язык: `{config.Lang}`" + + $"{nl}Префикс: `{config.Prefix}`" + + $"{nl}Удалять роли при муте: " + + $"{(config.RemoveRolesOnMute ? "Да" : "Нет")}"); + return; + } + + if (sArray[0].ToLower() == "lang") { + if (sArray[1].ToLower() != "ru") throw new Exception("Язык не поддерживается!"); + config.Lang = sArray[1].ToLower(); + } + + + if (sArray[0].ToLower() == "prefix") + config.Prefix = sArray[1]; + + if (sArray[0].ToLower() == "removerolesonmute") { + try { + config.RemoveRolesOnMute = bool.Parse(sArray[1].ToLower()); + } catch (FormatException) { + await Context.Channel.SendMessageAsync("Неверный параметр! Требуется `true` или `false`"); + return; + } + } + + config.Save(); + + await Context.Channel.SendMessageAsync("Настройки успешно обновлены!"); + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/UnbanModule.cs b/Boyfriend/Commands/UnbanModule.cs index 0a2e841..57039db 100644 --- a/Boyfriend/Commands/UnbanModule.cs +++ b/Boyfriend/Commands/UnbanModule.cs @@ -11,15 +11,16 @@ public class UnbanModule : ModuleBase { [Command("unban")] [Summary("Возвращает пользователя из бана")] [Alias("разбан")] - [RequireBotPermission(GuildPermission.BanMembers)] - [RequireUserPermission(GuildPermission.BanMembers)] public Task Run(string user, [Remainder] string reason) { - var toBan = Utils.ParseUser(user).Result; - UnbanUser(Context.Guild, Context.User, toBan, reason); + var toUnban = Utils.ParseUser(user).Result; + if (Context.Guild.GetBanAsync(toUnban.Id) == null) + throw new Exception("Пользователь не забанен!"); + UnbanUser(Context.Guild, Context.Guild.GetUser(Context.User.Id), toUnban, reason); return Task.CompletedTask; } - public static async void UnbanUser(IGuild guild, IUser author, IUser toUnban, string reason) { + public static async void UnbanUser(IGuild guild, IGuildUser author, IUser toUnban, string reason) { + await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); var authorMention = author.Mention; var notification = $"{authorMention} возвращает из бана {toUnban.Mention} за {Utils.WrapInline(reason)}"; await guild.RemoveBanAsync(toUnban); diff --git a/Boyfriend/Commands/UnmuteModule.cs b/Boyfriend/Commands/UnmuteModule.cs index 6135909..0e81805 100644 --- a/Boyfriend/Commands/UnmuteModule.cs +++ b/Boyfriend/Commands/UnmuteModule.cs @@ -11,15 +11,18 @@ public class UnmuteModule : ModuleBase { [Command("unmute")] [Summary("Возвращает пользователя из мута")] [Alias("размут")] - [RequireBotPermission(GuildPermission.ManageRoles)] - [RequireUserPermission(GuildPermission.ManageMessages)] - public Task Run(string user, [Remainder] string reason) { + public async Task Run(string user, [Remainder] string reason) { var toUnmute = Utils.ParseMember(Context.Guild, user).Result; - UnmuteMember(Context.Guild, Context.User, toUnmute, reason); - return Task.CompletedTask; + var author = Context.Guild.GetUser(Context.User.Id); + await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); + await CommandHandler.CheckInteractions(author, toUnmute); + if (toUnmute.RoleIds.All(x => x != Utils.GetMuteRole(Context.Guild).Id)) + throw new Exception("Пользователь не в муте!"); + UnmuteMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toUnmute, reason); } - public static async void UnmuteMember(IGuild guild, IUser author, IGuildUser toUnmute, string reason) { + public static async void UnmuteMember(IGuild guild, IGuildUser author, IGuildUser toUnmute, string reason) { + await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); var authorMention = author.Mention; var notification = $"{authorMention} возвращает из мута {toUnmute.Mention} за {Utils.WrapInline(reason)}"; await toUnmute.RemoveRoleAsync(Utils.GetMuteRole(guild)); diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 2c14e6d..1ab8dd8 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -8,7 +8,7 @@ namespace Boyfriend; public class EventHandler { private readonly DiscordSocketClient _client = Boyfriend.Client; - private readonly CommandService _commands = new(); + public static readonly CommandService Commands = new(); public async Task InitEvents() { _client.Ready += ReadyEvent; @@ -16,50 +16,16 @@ public class EventHandler { _client.MessageReceived += MessageReceivedEvent; _client.MessageUpdated += MessageUpdatedEvent; _client.UserJoined += UserJoinedEvent; - await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), null); - } - - private static async Task HandleErrors(SocketCommandContext context, IResult result) { - var channel = context.Channel; - var reason = Utils.WrapInline(result.ErrorReason); - switch (result.Error) { - case CommandError.Exception: - await channel.SendMessageAsync($"Произошла непредвиденная ошибка при выполнении команды: {reason}"); - break; - case CommandError.Unsuccessful: - await channel.SendMessageAsync($"Выполнение команды завершилось неудачей: {reason}"); - break; - case CommandError.MultipleMatches: - await channel.SendMessageAsync($"Обнаружены повторяющиеся типы аргументов! {reason}"); - break; - case CommandError.ParseFailed: - await channel.SendMessageAsync($"Не удалось обработать команду: {reason}"); - break; - case CommandError.UnknownCommand: - await channel.SendMessageAsync($"Неизвестная команда! {reason}"); - break; - case CommandError.UnmetPrecondition: - await channel.SendMessageAsync($"У тебя недостаточно прав для выполнения этой команды! {reason}"); - break; - case CommandError.BadArgCount: - await channel.SendMessageAsync($"Неверное количество аргументов! {reason}"); - break; - case CommandError.ObjectNotFound: - await channel.SendMessageAsync($"Нету нужных аргументов! {reason}"); - break; - case null: - break; - default: - throw new ArgumentException("CommandError"); - - } + await Commands.AddModulesAsync(Assembly.GetEntryAssembly(), null); } [Obsolete("Stop hard-coding things!")] private async Task ReadyEvent() { if (_client.GetChannel(618044439939645444) is not IMessageChannel botLogChannel) - throw new ArgumentException("Invalid bot log channel"); + throw new Exception("Invalid bot log channel"); await botLogChannel.SendMessageAsync($"{Utils.GetBeep()}Я запустился! (C#)"); + + await Boyfriend.SetupGuildConfigs(); } private static async Task MessageDeletedEvent(Cacheable message, @@ -72,34 +38,38 @@ public class EventHandler { await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), toSend); } - private async Task MessageReceivedEvent(SocketMessage messageParam) { - if (messageParam is not SocketUserMessage {Author: IGuildUser user} message) return; - var argPos = 0; + private static async Task MessageReceivedEvent(SocketMessage messageParam) { + if (messageParam is not SocketUserMessage message) return; + var user = (IGuildUser) message.Author; var guild = user.Guild; + var argPos = 0; if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && !user.GuildPermissions.MentionEveryone) BanModule.BanUser(guild, guild.GetCurrentUserAsync().Result, user, TimeSpan.FromMilliseconds(-1), "Более 3-ёх упоминаний в одном сообщении"); - if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || - message.Author.IsBot) + var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); + var prevsArray = prevs as IMessage[] ?? prevs.ToArray(); + var prev = prevsArray[1].Content; + var prevFailsafe = prevsArray[2].Content; + if (!(message.HasStringPrefix(Boyfriend.GetGuildConfig(guild).Prefix, ref argPos) + || message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) + || user.IsBot && message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)) return; - var context = new SocketCommandContext(_client, message); - - var result = await _commands.ExecuteAsync(context, argPos, null); - await HandleErrors(context, result); + await CommandHandler.HandleCommand(message, argPos); } private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, ISocketMessageChannel channel) { var msg = messageCached.Value; var nl = Environment.NewLine; + if (msg.Content == messageSocket.Content) return; var toSend = msg == null ? $"Отредактировано сообщение от {messageSocket.Author.Mention} в канале" + - $" {Utils.MentionChannel(channel.Id)}," + $" но я забыл что там было до редактирования: " + - $"{Utils.Wrap(messageSocket.Content)}" + $" {Utils.MentionChannel(channel.Id)}," + " но я забыл что там было до редактирования: " + + Utils.Wrap(messageSocket.Content) : $"Отредактировано сообщение от {msg.Author.Mention} " + $"в канале {Utils.MentionChannel(channel.Id)}." + $"{nl}До:{nl}{Utils.Wrap(msg.Content)}{nl}После:{nl}{Utils.Wrap(messageSocket.Content)}"; diff --git a/Boyfriend/GuildConfig.cs b/Boyfriend/GuildConfig.cs new file mode 100644 index 0000000..c9a6c26 --- /dev/null +++ b/Boyfriend/GuildConfig.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace Boyfriend; + +public class GuildConfig { + public ulong Id { get; } + public string Lang { get; set; } + public string Prefix { get; set; } + public bool RemoveRolesOnMute { get; set; } + + public GuildConfig(ulong id, string lang, string prefix, bool removeRolesOnMute) { + Id = id; + Lang = lang; + Prefix = prefix; + RemoveRolesOnMute = removeRolesOnMute; + } + + public async void Save() { + await using var stream = File.OpenWrite("config_" + Id + ".json"); + await JsonSerializer.SerializeAsync(stream, this); + } +} \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 9c0ced5..3ff7c4a 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -13,7 +13,7 @@ public static class Utils { [Obsolete("Stop hard-coding things!")] public static ITextChannel GetAdminLogChannel() { if (Boyfriend.Client.GetChannel(870929165141032971) is not ITextChannel adminLogChannel) - throw new ArgumentException("Invalid admin log channel"); + throw new Exception("Invalid admin log channel"); return adminLogChannel; } @@ -31,13 +31,6 @@ public static class Utils { } public static async Task StartDelayed(Task toRun, TimeSpan delay, Func? condition = null) { - switch (delay.TotalMilliseconds) { - case < -1: - throw new ArgumentOutOfRangeException(nameof(delay), "Указана отрицательная продолжительность!"); - case > int.MaxValue: - throw new ArgumentOutOfRangeException(nameof(delay), "Указана слишком большая продолжительность!"); - } - await Task.Delay(delay); var conditionResult = condition?.Invoke() ?? true; if (conditionResult) From 1c9caf6d75c5b9d486c291c740bf2ea3487c4395 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Thu, 16 Dec 2021 00:07:04 +0500 Subject: [PATCH 005/329] now fully configurable :D --- Boyfriend/Boyfriend.cs | 19 +++-- Boyfriend/Commands/BanModule.cs | 11 +-- Boyfriend/Commands/ClearModule.cs | 2 +- Boyfriend/Commands/KickModule.cs | 8 +-- Boyfriend/Commands/MuteModule.cs | 31 +++++--- Boyfriend/Commands/SettingsModule.cs | 103 ++++++++++++++++++++++----- Boyfriend/Commands/UnbanModule.cs | 9 ++- Boyfriend/Commands/UnmuteModule.cs | 24 +++++-- Boyfriend/EventHandler.cs | 29 +++++--- Boyfriend/GuildConfig.cs | 15 +++- Boyfriend/Utils.cs | 41 ++++++++--- 11 files changed, 221 insertions(+), 71 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index f643729..ef1c5ab 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -45,18 +45,29 @@ public static class Boyfriend { try { config = await JsonSerializer.DeserializeAsync(openStream) ?? throw new Exception(); } catch (JsonException) { - config = new GuildConfig(guild.Id, "ru", "!", false); + config = new GuildConfig(guild.Id, "ru", "!", false, true, true, 0, 0, 0); } GuildConfigDictionary.Add(guild.Id, config); } } public static GuildConfig GetGuildConfig(IGuild guild) { - GuildConfig toReturn; - toReturn = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] - : new GuildConfig(guild.Id, "ru", "!", false); + var toReturn = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] + : new GuildConfig(guild.Id, "ru", "!", false, true, true, 0, 0, 0); if (toReturn.Id != guild.Id) throw new Exception(); return toReturn; } + + public static IGuild FindGuild(ITextChannel channel) { + foreach (var guild in Client.Guilds) { + if (guild.Channels.Any(x => x == channel)) return guild; + } + + throw new Exception("Не удалось найти сервер по каналу!"); + } + + public static void ThrowFatal(Exception e) { + throw e; + } } \ No newline at end of file diff --git a/Boyfriend/Commands/BanModule.cs b/Boyfriend/Commands/BanModule.cs index a89c1d5..cd1295b 100644 --- a/Boyfriend/Commands/BanModule.cs +++ b/Boyfriend/Commands/BanModule.cs @@ -12,13 +12,14 @@ public class BanModule : ModuleBase { [Command("ban")] [Summary("Банит пользователя")] [Alias("бан")] - public async Task Run(string user, string durationString, [Remainder]string reason) { + public async Task Run(string user, [Remainder]string reason) { TimeSpan duration; try { - duration = TimeSpan.Parse(durationString); + var reasonArray = reason.Split(); + duration = Utils.GetTimeSpan(reasonArray[0]); + reason = string.Join(" ", reasonArray.Skip(1)); } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { duration = TimeSpan.FromMilliseconds(-1); - reason = durationString + reason; } var author = Context.Guild.GetUser(Context.User.Id); var toBan = await Utils.ParseUser(user); @@ -35,8 +36,8 @@ public class BanModule : ModuleBase { var guildBanMessage = $"({author.Username}#{author.Discriminator}) {reason}"; await guild.AddBanAsync(toBan, 0, guildBanMessage); var notification = $"{authorMention} банит {toBan.Mention} за {Utils.WrapInline(reason)}"; - await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); - await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); var task = new Task(() => UnbanModule.UnbanUser(guild, guild.GetCurrentUserAsync().Result, toBan, "Время наказания истекло")); await Utils.StartDelayed(task, duration, () => guild.GetBanAsync(toBan).Result != null); diff --git a/Boyfriend/Commands/ClearModule.cs b/Boyfriend/Commands/ClearModule.cs index bd5f87f..94eade6 100644 --- a/Boyfriend/Commands/ClearModule.cs +++ b/Boyfriend/Commands/ClearModule.cs @@ -23,7 +23,7 @@ public class ClearModule : ModuleBase { default: { var messages = await channel.GetMessagesAsync(toDelete + 1).FlattenAsync(); await channel.DeleteMessagesAsync(messages); - await Utils.GetAdminLogChannel().SendMessageAsync( + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Context.Guild), $"{Context.User.Mention} удаляет {toDelete + 1} сообщений в канале " + $"{Utils.MentionChannel(Context.Channel.Id)}"); break; diff --git a/Boyfriend/Commands/KickModule.cs b/Boyfriend/Commands/KickModule.cs index f45f0d0..67fb9fd 100644 --- a/Boyfriend/Commands/KickModule.cs +++ b/Boyfriend/Commands/KickModule.cs @@ -14,13 +14,13 @@ public class KickModule : ModuleBase { [Alias("кик")] public async Task Run(string user, [Remainder]string reason) { var author = Context.Guild.GetUser(Context.User.Id); - var toKick = Utils.ParseMember(Context.Guild, user).Result; + var toKick = await Utils.ParseMember(Context.Guild, user); await CommandHandler.CheckPermissions(author, GuildPermission.KickMembers); await CommandHandler.CheckInteractions(author, toKick); KickMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toKick, reason); } - private static async void KickMember(IGuild guild, IGuildUser author, IGuildUser toKick, string reason) { + private static async void KickMember(IGuild guild, IUser author, IGuildUser toKick, string reason) { var authorMention = author.Mention; await Utils.SendDirectMessage(toKick, $"Тебя кикнул {authorMention} на сервере {guild.Name} за " + $"{Utils.WrapInline(reason)}"); @@ -28,7 +28,7 @@ public class KickModule : ModuleBase { var guildKickMessage = $"({author.Username}#{author.Discriminator}) {reason}"; await toKick.KickAsync(guildKickMessage); var notification = $"{authorMention} выгоняет {toKick.Mention} за {Utils.WrapInline(reason)}"; - await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); - await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); } } \ No newline at end of file diff --git a/Boyfriend/Commands/MuteModule.cs b/Boyfriend/Commands/MuteModule.cs index 9d383c8..24a67d5 100644 --- a/Boyfriend/Commands/MuteModule.cs +++ b/Boyfriend/Commands/MuteModule.cs @@ -12,16 +12,21 @@ public class MuteModule : ModuleBase { [Command("mute")] [Summary("Глушит пользователя")] [Alias("мут")] - public async Task Run(string user, string durationString, [Remainder]string reason) { + public async Task Run(string user, [Remainder]string reason) { TimeSpan duration; try { - duration = TimeSpan.Parse(durationString); + var reasonArray = reason.Split(); + duration = Utils.GetTimeSpan(reasonArray[0]); + reason = string.Join(" ", reasonArray.Skip(1)); } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { duration = TimeSpan.FromMilliseconds(-1); - reason = durationString + reason; } var author = Context.Guild.GetUser(Context.User.Id); var toMute = await Utils.ParseMember(Context.Guild, user); + if (toMute.RoleIds.Any(x => x == Utils.GetMuteRole(Context.Guild).Id)) + throw new Exception("Участник уже заглушен!"); + if (Boyfriend.GetGuildConfig(Context.Guild).RolesRemovedOnMute.ContainsKey(toMute.Id)) + throw new Exception("Кто-то убрал роль мута самостоятельно!"); await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); await CommandHandler.CheckInteractions(author, toMute); MuteMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toMute, duration, reason); @@ -32,17 +37,25 @@ public class MuteModule : ModuleBase { await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); var authorMention = author.Mention; var role = Utils.GetMuteRole(guild); - if (Boyfriend.GetGuildConfig(guild).RemoveRolesOnMute) { - foreach (var roleId in toMute.RoleIds) { - await toMute.RemoveRoleAsync(roleId); + var config = Boyfriend.GetGuildConfig(guild); - } + if (config.RemoveRolesOnMute) { + var rolesRemoved = new List(); + try { + foreach (var roleId in toMute.RoleIds) { + if (roleId == guild.Id) continue; + await toMute.RemoveRoleAsync(roleId); + rolesRemoved.Add(roleId); + } + } catch (NullReferenceException) {} + config.RolesRemovedOnMute.Add(toMute.Id, rolesRemoved); + config.Save(); } await toMute.AddRoleAsync(role); var notification = $"{authorMention} глушит {toMute.Mention} за {Utils.WrapInline(reason)}"; - await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); - await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); var task = new Task(() => UnmuteModule.UnmuteMember(guild, guild.GetCurrentUserAsync().Result, toMute, "Время наказания истекло")); await Utils.StartDelayed(task, duration, () => toMute.RoleIds.Any(x => x == role.Id)); diff --git a/Boyfriend/Commands/SettingsModule.cs b/Boyfriend/Commands/SettingsModule.cs index a528bbf..dd25c56 100644 --- a/Boyfriend/Commands/SettingsModule.cs +++ b/Boyfriend/Commands/SettingsModule.cs @@ -1,4 +1,5 @@ -using Discord.Commands; +using Discord; +using Discord.Commands; // ReSharper disable UnusedType.Global // ReSharper disable UnusedMember.Global @@ -10,37 +11,105 @@ public class SettingsModule : ModuleBase { [Summary("Настраивает бота")] [Alias("config", "настройки", "конфиг")] public async Task Run([Remainder] string s = "") { + await CommandHandler.CheckPermissions(Context.Guild.GetUser(Context.User.Id), GuildPermission.ManageGuild); var config = Boyfriend.GetGuildConfig(Context.Guild); var sArray = s.Split(" "); + var guild = Context.Guild; if (s == "") { var nl = Environment.NewLine; - await Context.Channel.SendMessageAsync($"Текущие настройки:{nl}Язык: `{config.Lang}`" + - $"{nl}Префикс: `{config.Prefix}`" + - $"{nl}Удалять роли при муте: " + - $"{(config.RemoveRolesOnMute ? "Да" : "Нет")}"); + var adminLogChannel = guild.GetTextChannel(config.AdminLogChannel); + var admin = adminLogChannel == null ? "Не указан" : adminLogChannel.Mention; + var botLogChannel = guild.GetTextChannel(config.BotLogChannel); + var bot = botLogChannel == null ? "Не указан" : botLogChannel.Mention; + var muteRole = guild.GetRole(config.MuteRole); + var mute = muteRole == null ? "Не указана" : muteRole.Mention; + var toSend = $"Текущие настройки:{nl}" + + $"Язык (`lang`): `{config.Lang}`{nl}" + + $"Префикс (`prefix`): `{config.Prefix}`{nl}" + + $"Удалять роли при муте (`removeRolesOnMute`): {YesOrNo(config.RemoveRolesOnMute)}{nl}" + + "Использовать канал системных сообщений для уведомлений (`useSystemChannel`): " + + $"{YesOrNo(config.UseSystemChannel)}{nl}" + + $"Отправлять приветствия (`sendWelcomeMessages`): {YesOrNo(config.UseSystemChannel)}{nl}" + + $"Роль мута (`muteRole`): {mute}{nl}" + + $"Канал админ-уведомлений (`adminLogChannel`): " + + $"{admin}{nl}" + + $"Канал бот-уведомлений (`botLogChannel`): " + + $"{bot}"; + await Utils.SilentSendAsync(Context.Channel as ITextChannel ?? throw new Exception(), toSend); return; } - if (sArray[0].ToLower() == "lang") { - if (sArray[1].ToLower() != "ru") throw new Exception("Язык не поддерживается!"); - config.Lang = sArray[1].ToLower(); + var setting = sArray[0].ToLower(); + var value = sArray[1].ToLower(); + + ITextChannel? channel; + try { + channel = await Utils.ParseChannel(value) as ITextChannel; + } catch (FormatException) { + channel = null; } + IRole? role; + try { + role = Utils.ParseRole(guild, value); + } + catch (FormatException) { + role = null; + } - if (sArray[0].ToLower() == "prefix") - config.Prefix = sArray[1]; + var boolValue = ParseBool(sArray[1]); - if (sArray[0].ToLower() == "removerolesonmute") { - try { - config.RemoveRolesOnMute = bool.Parse(sArray[1].ToLower()); - } catch (FormatException) { - await Context.Channel.SendMessageAsync("Неверный параметр! Требуется `true` или `false`"); - return; - } + switch (setting) { + case "lang" when sArray[1].ToLower() != "ru": + throw new Exception("Язык не поддерживается!"); + case "lang": + config.Lang = value; + break; + case "prefix": + config.Prefix = value; + break; + case "removerolesonmute": + config.RemoveRolesOnMute = boolValue ?? + throw new Exception("Неверный параметр! Требуется `true` или `false"); + break; + case "usesystemchannel": + config.UseSystemChannel = boolValue ?? + throw new Exception("Неверный параметр! Требуется `true` или `false"); + break; + case "sendwelcomemessages": + config.SendWelcomeMessages = boolValue ?? + throw new Exception("Неверный параметр! Требуется `true` или `false"); + break; + case "adminlogchannel": + config.AdminLogChannel = Convert.ToUInt64((channel ?? + throw new Exception("Указан недействительный канал!")) + .Id); + break; + case "botlogchannel": + config.BotLogChannel = Convert.ToUInt64((channel ?? + throw new Exception("Указан недействительный канал!")) + .Id); + break; + case "muterole": + config.MuteRole = Convert.ToUInt64((role ?? throw new Exception("Указана недействительная роль!")) + .Id); + break; } config.Save(); await Context.Channel.SendMessageAsync("Настройки успешно обновлены!"); } + + private static bool? ParseBool(string toParse) { + try { + return bool.Parse(toParse.ToLower()); + } catch (FormatException) { + return null; + } + } + + private static string YesOrNo(bool isYes) { + return isYes ? "Да" : "Нет"; + } } \ No newline at end of file diff --git a/Boyfriend/Commands/UnbanModule.cs b/Boyfriend/Commands/UnbanModule.cs index 57039db..33b5902 100644 --- a/Boyfriend/Commands/UnbanModule.cs +++ b/Boyfriend/Commands/UnbanModule.cs @@ -11,12 +11,11 @@ public class UnbanModule : ModuleBase { [Command("unban")] [Summary("Возвращает пользователя из бана")] [Alias("разбан")] - public Task Run(string user, [Remainder] string reason) { - var toUnban = Utils.ParseUser(user).Result; + public async Task Run(string user, [Remainder] string reason) { + var toUnban = await Utils.ParseUser(user); if (Context.Guild.GetBanAsync(toUnban.Id) == null) throw new Exception("Пользователь не забанен!"); UnbanUser(Context.Guild, Context.Guild.GetUser(Context.User.Id), toUnban, reason); - return Task.CompletedTask; } public static async void UnbanUser(IGuild guild, IGuildUser author, IUser toUnban, string reason) { @@ -24,7 +23,7 @@ public class UnbanModule : ModuleBase { var authorMention = author.Mention; var notification = $"{authorMention} возвращает из бана {toUnban.Mention} за {Utils.WrapInline(reason)}"; await guild.RemoveBanAsync(toUnban); - await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); - await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); } } \ No newline at end of file diff --git a/Boyfriend/Commands/UnmuteModule.cs b/Boyfriend/Commands/UnmuteModule.cs index 0e81805..9baee78 100644 --- a/Boyfriend/Commands/UnmuteModule.cs +++ b/Boyfriend/Commands/UnmuteModule.cs @@ -12,12 +12,17 @@ public class UnmuteModule : ModuleBase { [Summary("Возвращает пользователя из мута")] [Alias("размут")] public async Task Run(string user, [Remainder] string reason) { - var toUnmute = Utils.ParseMember(Context.Guild, user).Result; + var toUnmute = await Utils.ParseMember(Context.Guild, user); var author = Context.Guild.GetUser(Context.User.Id); await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); await CommandHandler.CheckInteractions(author, toUnmute); - if (toUnmute.RoleIds.All(x => x != Utils.GetMuteRole(Context.Guild).Id)) - throw new Exception("Пользователь не в муте!"); + if (toUnmute.RoleIds.All(x => x != Utils.GetMuteRole(Context.Guild).Id)) { + var rolesRemoved = Boyfriend.GetGuildConfig(Context.Guild).RolesRemovedOnMute; + if (!rolesRemoved.ContainsKey(toUnmute.Id)) throw new Exception("Пользователь не в муте!"); + rolesRemoved.Remove(toUnmute.Id); + throw new Exception("Пользователь не в муте, но я нашёл и удалил запись о его удалённых ролях!"); + } + UnmuteMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toUnmute, reason); } @@ -26,7 +31,16 @@ public class UnmuteModule : ModuleBase { var authorMention = author.Mention; var notification = $"{authorMention} возвращает из мута {toUnmute.Mention} за {Utils.WrapInline(reason)}"; await toUnmute.RemoveRoleAsync(Utils.GetMuteRole(guild)); - await Utils.SilentSendAsync(guild.GetSystemChannelAsync().Result, notification); - await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), notification); + var config = Boyfriend.GetGuildConfig(guild); + + if (config.RolesRemovedOnMute.ContainsKey(toUnmute.Id)) { + foreach (var roleId in config.RolesRemovedOnMute[toUnmute.Id]) { + await toUnmute.AddRoleAsync(roleId); + } + config.RolesRemovedOnMute.Remove(toUnmute.Id); + } + + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); } } \ No newline at end of file diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 1ab8dd8..05d6fb2 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -19,23 +19,27 @@ public class EventHandler { await Commands.AddModulesAsync(Assembly.GetEntryAssembly(), null); } - [Obsolete("Stop hard-coding things!")] - private async Task ReadyEvent() { - if (_client.GetChannel(618044439939645444) is not IMessageChannel botLogChannel) - throw new Exception("Invalid bot log channel"); - await botLogChannel.SendMessageAsync($"{Utils.GetBeep()}Я запустился! (C#)"); - + private static async Task ReadyEvent() { await Boyfriend.SetupGuildConfigs(); + + foreach (var guild in Boyfriend.Client.Guilds) { + var channel = guild.GetTextChannel(Boyfriend.GetGuildConfig(guild).BotLogChannel); + if (channel == null) continue; + await channel.SendMessageAsync($"{Utils.GetBeep()}Я запустился! (C#)"); + } } private static async Task MessageDeletedEvent(Cacheable message, Cacheable channel) { var msg = message.Value; var toSend = msg == null - ? "Удалено сообщение в канале {Utils.MentionChannel(channel.Id)}, но я забыл что там было" + ? $"Удалено сообщение в канале {Utils.MentionChannel(channel.Id)}, но я забыл что там было" : $"Удалено сообщение от {msg.Author.Mention} в канале " + $"{Utils.MentionChannel(channel.Id)}: {Environment.NewLine}{Utils.Wrap(msg.Content)}"; - await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), toSend); + try { + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel( + Boyfriend.FindGuild(channel.Value as ITextChannel)), toSend); + } catch (ArgumentException) {} } private static async Task MessageReceivedEvent(SocketMessage messageParam) { @@ -46,15 +50,17 @@ public class EventHandler { if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && !user.GuildPermissions.MentionEveryone) - BanModule.BanUser(guild, guild.GetCurrentUserAsync().Result, user, TimeSpan.FromMilliseconds(-1), + BanModule.BanUser(guild, await guild.GetCurrentUserAsync(), user, TimeSpan.FromMilliseconds(-1), "Более 3-ёх упоминаний в одном сообщении"); var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); var prevsArray = prevs as IMessage[] ?? prevs.ToArray(); var prev = prevsArray[1].Content; var prevFailsafe = prevsArray[2].Content; + if (message.Channel is not ITextChannel channel) throw new Exception(); if (!(message.HasStringPrefix(Boyfriend.GetGuildConfig(guild).Prefix, ref argPos) || message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) + || user == await Boyfriend.FindGuild(channel).GetCurrentUserAsync() || user.IsBot && message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)) return; @@ -73,7 +79,10 @@ public class EventHandler { : $"Отредактировано сообщение от {msg.Author.Mention} " + $"в канале {Utils.MentionChannel(channel.Id)}." + $"{nl}До:{nl}{Utils.Wrap(msg.Content)}{nl}После:{nl}{Utils.Wrap(messageSocket.Content)}"; - await Utils.SilentSendAsync(Utils.GetAdminLogChannel(), toSend); + try { + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Boyfriend.FindGuild(channel as ITextChannel)), + toSend); + } catch (ArgumentException) {} } private static async Task UserJoinedEvent(SocketGuildUser user) { diff --git a/Boyfriend/GuildConfig.cs b/Boyfriend/GuildConfig.cs index c9a6c26..80490de 100644 --- a/Boyfriend/GuildConfig.cs +++ b/Boyfriend/GuildConfig.cs @@ -7,12 +7,25 @@ public class GuildConfig { public string Lang { get; set; } public string Prefix { get; set; } public bool RemoveRolesOnMute { get; set; } + public bool UseSystemChannel { get; set; } + public bool SendWelcomeMessages { get; set; } + public ulong MuteRole { get; set; } + public ulong AdminLogChannel { get; set; } + public ulong BotLogChannel { get; set; } + public Dictionary> RolesRemovedOnMute { get; set; } - public GuildConfig(ulong id, string lang, string prefix, bool removeRolesOnMute) { + public GuildConfig(ulong id, string lang, string prefix, bool removeRolesOnMute, bool useSystemChannel, + bool sendWelcomeMessages, ulong muteRole, ulong adminLogChannel, ulong botLogChannel) { Id = id; Lang = lang; Prefix = prefix; RemoveRolesOnMute = removeRolesOnMute; + UseSystemChannel = useSystemChannel; + SendWelcomeMessages = sendWelcomeMessages; + MuteRole = muteRole; + AdminLogChannel = adminLogChannel; + BotLogChannel = botLogChannel; + RolesRemovedOnMute = new Dictionary>(); } public async void Save() { diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 3ff7c4a..2c9ad4b 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Globalization; +using System.Text.RegularExpressions; using Discord; using Discord.Net; @@ -6,15 +7,16 @@ namespace Boyfriend; public static class Utils { public static string GetBeep() { - var letters = new[] { "а", "о", "и"}; + var letters = new[] {"а", "о", "и"}; return $"Б{letters[new Random().Next(3)]}п! "; } - [Obsolete("Stop hard-coding things!")] - public static ITextChannel GetAdminLogChannel() { - if (Boyfriend.Client.GetChannel(870929165141032971) is not ITextChannel adminLogChannel) - throw new Exception("Invalid admin log channel"); - return adminLogChannel; + public static async Task GetAdminLogChannel(IGuild guild) { + var adminLogChannel = await ParseChannel(Boyfriend.GetGuildConfig(guild).AdminLogChannel.ToString()); + if (adminLogChannel is ITextChannel channel) + return channel; + + throw new Exception("Неверный канал админ-логов для гильдии " + guild.Id); } public static string Wrap(string original) { @@ -50,6 +52,14 @@ public static class Utils { return await guild.GetUserAsync(ParseMention(mention)); } + public static async Task ParseChannel(string mention) { + return await Boyfriend.Client.GetChannelAsync(ParseMention(mention)); + } + + public static IRole ParseRole(IGuild guild, string mention) { + return guild.GetRole(ParseMention(mention)); + } + public static async Task SendDirectMessage(IUser user, string toSend) { try { await user.SendMessageAsync(toSend); @@ -60,12 +70,23 @@ public static class Utils { } public static IRole GetMuteRole(IGuild guild) { - var role = guild.Roles.FirstOrDefault(x => x.Name.ToLower() is "заключённый" or "muted"); - if (role == null) throw new Exception("Не удалось найти роль мута"); + var role = guild.Roles.FirstOrDefault(x => x.Id == Boyfriend.GetGuildConfig(guild).MuteRole); + if (role == null) throw new Exception("Требуется указать роль мута в настройках!"); return role; } public static async Task SilentSendAsync(ITextChannel channel, string text) { - await channel.SendMessageAsync(text, false, null, null, AllowedMentions.None); + try { + await channel.SendMessageAsync(text, false, null, null, AllowedMentions.None); + } catch (ArgumentException) {} + } + + private static readonly string[] Formats = { + "%d'd'%h'h'%m'm'%s's'", "%d'd'%h'h'%m'm'", "%d'd'%h'h'%s's'", "%d'd'%h'h'", "%d'd'%m'm'%s's'", "%d'd'%m'm'", + "%d'd'%s's'", "%d'd'", "%h'h'%m'm'%s's'", "%h'h'%m'm'", "%h'h'%s's'", "%h'h'", "%m'm'%s's'", "%m'm'", "%s's'" + }; + public static TimeSpan GetTimeSpan(string from) { + return TimeSpan.ParseExact(from.ToLowerInvariant(), Formats, + CultureInfo.InvariantCulture); } } \ No newline at end of file From f30485dd711f5346629abd3fcbe0cc1d8062081b Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Tue, 18 Jan 2022 22:38:15 +0500 Subject: [PATCH 006/329] ton of stuff - new command handler - multilanguage support - time out support - responses - a ton of stuff --- Boyfriend/Boyfriend.cs | 35 +- Boyfriend/Boyfriend.csproj | 18 +- Boyfriend/Boyfriend.csproj.DotSettings | 2 + Boyfriend/CommandHandler.cs | 89 +++-- Boyfriend/Commands/BanCommand.cs | 61 ++++ Boyfriend/Commands/BanModule.cs | 45 --- Boyfriend/Commands/ClearCommand.cs | 49 +++ Boyfriend/Commands/ClearModule.cs | 33 -- Boyfriend/Commands/Command.cs | 19 + Boyfriend/Commands/HelpCommand.cs | 30 ++ Boyfriend/Commands/HelpModule.cs | 25 -- Boyfriend/Commands/KickCommand.cs | 47 +++ Boyfriend/Commands/KickModule.cs | 34 -- Boyfriend/Commands/MuteCommand.cs | 92 +++++ Boyfriend/Commands/MuteModule.cs | 63 ---- Boyfriend/Commands/PingCommand.cs | 25 ++ Boyfriend/Commands/PingModule.cs | 14 - Boyfriend/Commands/SettingsCommand.cs | 125 +++++++ Boyfriend/Commands/SettingsModule.cs | 115 ------ Boyfriend/Commands/UnbanCommand.cs | 43 +++ Boyfriend/Commands/UnbanModule.cs | 29 -- Boyfriend/Commands/UnmuteCommand.cs | 61 ++++ Boyfriend/Commands/UnmuteModule.cs | 46 --- Boyfriend/EventHandler.cs | 68 ++-- Boyfriend/GuildConfig.cs | 9 +- Boyfriend/Messages.Designer.cs | 486 +++++++++++++++++++++++++ Boyfriend/Messages.resx | 246 +++++++++++++ Boyfriend/Messages.ru.resx | 237 ++++++++++++ Boyfriend/Utils.cs | 60 ++- 29 files changed, 1686 insertions(+), 520 deletions(-) create mode 100644 Boyfriend/Commands/BanCommand.cs delete mode 100644 Boyfriend/Commands/BanModule.cs create mode 100644 Boyfriend/Commands/ClearCommand.cs delete mode 100644 Boyfriend/Commands/ClearModule.cs create mode 100644 Boyfriend/Commands/Command.cs create mode 100644 Boyfriend/Commands/HelpCommand.cs delete mode 100644 Boyfriend/Commands/HelpModule.cs create mode 100644 Boyfriend/Commands/KickCommand.cs delete mode 100644 Boyfriend/Commands/KickModule.cs create mode 100644 Boyfriend/Commands/MuteCommand.cs delete mode 100644 Boyfriend/Commands/MuteModule.cs create mode 100644 Boyfriend/Commands/PingCommand.cs delete mode 100644 Boyfriend/Commands/PingModule.cs create mode 100644 Boyfriend/Commands/SettingsCommand.cs delete mode 100644 Boyfriend/Commands/SettingsModule.cs create mode 100644 Boyfriend/Commands/UnbanCommand.cs delete mode 100644 Boyfriend/Commands/UnbanModule.cs create mode 100644 Boyfriend/Commands/UnmuteCommand.cs delete mode 100644 Boyfriend/Commands/UnmuteModule.cs create mode 100644 Boyfriend/Messages.Designer.cs create mode 100644 Boyfriend/Messages.resx create mode 100644 Boyfriend/Messages.ru.resx diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index ef1c5ab..05d473b 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Globalization; +using System.Text.Json; using Discord; using Discord.WebSocket; @@ -6,9 +7,6 @@ namespace Boyfriend; public static class Boyfriend { - public static void Main() - => Init().GetAwaiter().GetResult(); - private static readonly DiscordSocketConfig Config = new() { MessageCacheSize = 250, GatewayIntents = GatewayIntents.All @@ -18,15 +16,19 @@ public static class Boyfriend { private static readonly Dictionary GuildConfigDictionary = new(); + public static void Main() { + Init().GetAwaiter().GetResult(); + } + private static async Task Init() { Client.Log += Log; var token = (await File.ReadAllTextAsync("token.txt")).Trim(); await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); - await Client.SetActivityAsync(new Game("Retrospecter - Electrospasm", ActivityType.Listening)); + await Client.SetActivityAsync(new Game("Retrospecter - Chiller", ActivityType.Listening)); - await new EventHandler().InitEvents(); + new EventHandler().InitEvents(); await Task.Delay(-1); } @@ -45,29 +47,28 @@ public static class Boyfriend { try { config = await JsonSerializer.DeserializeAsync(openStream) ?? throw new Exception(); } catch (JsonException) { - config = new GuildConfig(guild.Id, "ru", "!", false, true, true, 0, 0, 0); + Messages.Culture = new CultureInfo("ru"); + config = new GuildConfig(guild.Id, "ru", "!", false, true, + true, Messages.DefaultWelcomeMessage, 0, 0, 0, 0); } GuildConfigDictionary.Add(guild.Id, config); } } public static GuildConfig GetGuildConfig(IGuild guild) { + Messages.Culture = new CultureInfo("ru"); var toReturn = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] - : new GuildConfig(guild.Id, "ru", "!", false, true, true, 0, 0, 0); + : new GuildConfig(guild.Id, "ru", "!", false, true, true, Messages.DefaultWelcomeMessage, 0, 0, 0, 0); if (toReturn.Id != guild.Id) throw new Exception(); return toReturn; } - public static IGuild FindGuild(ITextChannel channel) { - foreach (var guild in Client.Guilds) { - if (guild.Channels.Any(x => x == channel)) return guild; - } + public static IGuild FindGuild(IMessageChannel channel) { + foreach (var guild in Client.Guilds) + if (guild.Channels.Any(x => x == channel)) + return guild; - throw new Exception("Не удалось найти сервер по каналу!"); - } - - public static void ThrowFatal(Exception e) { - throw e; + throw new Exception(Messages.CouldntFindGuildByChannel); } } \ No newline at end of file diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index 9b9566e..1563891 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -11,10 +11,26 @@ https://github.com/l1ttleO/Boyfriend-CSharp https://github.com/l1ttleO/Boyfriend-CSharp git + 1.0.1 - + + + + + + ResXFileCodeGenerator + Messages.Designer.cs + + + + + + True + True + Messages.resx + diff --git a/Boyfriend/Boyfriend.csproj.DotSettings b/Boyfriend/Boyfriend.csproj.DotSettings index 47a5106..b5da49f 100644 --- a/Boyfriend/Boyfriend.csproj.DotSettings +++ b/Boyfriend/Boyfriend.csproj.DotSettings @@ -1,3 +1,5 @@  No + + \ No newline at end of file diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs index 90026cc..0d79886 100644 --- a/Boyfriend/CommandHandler.cs +++ b/Boyfriend/CommandHandler.cs @@ -1,48 +1,43 @@ -using Discord; +using System.Text.RegularExpressions; +using Boyfriend.Commands; +using Discord; using Discord.Commands; using Discord.WebSocket; namespace Boyfriend; public static class CommandHandler { - public static async Task HandleCommand(SocketUserMessage message, int argPos) { - var context = new SocketCommandContext(Boyfriend.Client, message); - var result = await EventHandler.Commands.ExecuteAsync(context, argPos, null); + public static readonly Command[] Commands = { + new BanCommand(), new ClearCommand(), new HelpCommand(), + new KickCommand(), new MuteCommand() + }; - await HandleErrors(context, result); - } - private static async Task HandleErrors(SocketCommandContext context, IResult result) { - var channel = context.Channel; - var reason = Utils.WrapInline(result.ErrorReason); - switch (result.Error) { - case CommandError.Exception: - await channel.SendMessageAsync(reason); - break; - case CommandError.Unsuccessful: - await channel.SendMessageAsync($"Выполнение команды завершилось неудачей: {reason}"); - break; - case CommandError.MultipleMatches: - await channel.SendMessageAsync($"Обнаружены повторяющиеся типы аргументов! {reason}"); - break; - case CommandError.ParseFailed: - await channel.SendMessageAsync($"Не удалось обработать команду: {reason}"); - break; - case CommandError.UnknownCommand: - await channel.SendMessageAsync($"Неизвестная команда! {reason}"); - break; - case CommandError.UnmetPrecondition: - await channel.SendMessageAsync($"У тебя недостаточно прав для выполнения этой к: {reason}"); - break; - case CommandError.BadArgCount: - await channel.SendMessageAsync($"Неверное количество аргументов! {reason}"); - break; - case CommandError.ObjectNotFound: - await channel.SendMessageAsync($"Нету нужных аргументов! {reason}"); - break; - case null: - break; - default: - throw new Exception("CommandError"); + public static async Task HandleCommand(SocketUserMessage message) { + var context = new SocketCommandContext(Boyfriend.Client, message); + + foreach (var command in Commands) { + var regex = new Regex(Regex.Escape(Boyfriend.GetGuildConfig(context.Guild).Prefix)); + if (!command.GetAliases().Contains(regex.Replace(message.Content, "", 1).Split()[0])) continue; + + var args = message.Content.Split().Skip(1).ToArray(); + try { + if (command.GetArgumentsAmountRequired() > args.Length) + throw new ApplicationException(string.Format(Messages.NotEnoughArguments, + command.GetArgumentsAmountRequired(), args.Length)); + await command.Run(context, args); + } + catch (Exception e) { + var signature = e switch { + ApplicationException => ":x:", + UnauthorizedAccessException => ":no_entry_sign:", + _ => ":stop_sign:" + }; + await context.Channel.SendMessageAsync($"{signature} `{e.Message}`"); + if (e.StackTrace != null && e is not ApplicationException or UnauthorizedAccessException) + await context.Channel.SendMessageAsync(Utils.Wrap(e.StackTrace)); + } + + break; } } @@ -50,25 +45,25 @@ public static class CommandHandler { GuildPermission forBot = GuildPermission.StartEmbeddedActivities) { if (forBot == GuildPermission.StartEmbeddedActivities) forBot = toCheck; if (!(await user.Guild.GetCurrentUserAsync()).GuildPermissions.Has(forBot)) - throw new Exception("У меня недостаточно прав для выполнения этой команды!"); + throw new UnauthorizedAccessException(Messages.CommandNoPermissionBot); if (!user.GuildPermissions.Has(toCheck)) - throw new Exception("У тебя недостаточно прав для выполнения этой команды!"); + throw new UnauthorizedAccessException(Messages.CommandNoPermissionUser); } public static async Task CheckInteractions(IGuildUser actor, IGuildUser target) { if (actor.Guild != target.Guild) - throw new Exception("Участники находятся в разных гильдиях!"); + throw new UnauthorizedAccessException(Messages.InteractionsDifferentGuilds); var me = await target.Guild.GetCurrentUserAsync(); if (actor.Id == actor.Guild.OwnerId) return; if (target.Id == target.Guild.OwnerId) - throw new Exception("Ты не можешь взаимодействовать с владельцем сервера!"); + throw new UnauthorizedAccessException(Messages.InteractionsOwner); if (actor == target) - throw new Exception("Ты не можешь взаимодействовать с самим собой!"); + throw new UnauthorizedAccessException(Messages.InteractionsYourself); if (target == me) - throw new Exception("Ты не можешь со мной взаимодействовать!"); - if (actor.Hierarchy <= target.Hierarchy) - throw new Exception("Ты не можешь взаимодействовать с этим участником!"); + throw new UnauthorizedAccessException(Messages.InteractionsMe); if (me.Hierarchy <= target.Hierarchy) - throw new Exception("Я не могу взаимодействовать с этим участником!"); + throw new UnauthorizedAccessException(Messages.InteractionsFailedBot); + if (actor.Hierarchy <= target.Hierarchy) + throw new UnauthorizedAccessException(Messages.InteractionsFailedUser); } } \ No newline at end of file diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs new file mode 100644 index 0000000..dcd5a7b --- /dev/null +++ b/Boyfriend/Commands/BanCommand.cs @@ -0,0 +1,61 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class BanCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + var toBan = await Utils.ParseUser(args[0]); + var reason = Utils.JoinString(args, 1); + TimeSpan duration; + try { + duration = Utils.GetTimeSpan(args[1]); + reason = Utils.JoinString(args, 2); + } + catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { + duration = TimeSpan.FromMilliseconds(-1); + } + + var author = context.Guild.GetUser(context.User.Id); + + await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); + var memberToBan = context.Guild.GetUser(toBan.Id); + if (memberToBan != null) + await CommandHandler.CheckInteractions(author, memberToBan); + await BanUser(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), + toBan, duration, reason); + } + + public static async Task BanUser(IGuild guild, ITextChannel? channel, IGuildUser author, IUser toBan, + TimeSpan duration, string reason) { + var authorMention = author.Mention; + await Utils.SendDirectMessage(toBan, string.Format(Messages.YouWereBanned, author.Mention, guild.Name, + Utils.WrapInline(reason))); + var guildBanMessage = $"({author.Username}#{author.Discriminator}) {reason}"; + await guild.AddBanAsync(toBan, 0, guildBanMessage); + var notification = string.Format(Messages.UserBanned, authorMention, toBan.Mention, Utils.WrapInline(reason)); + await Utils.SilentSendAsync(channel, string.Format(Messages.BanResponse, toBan.Mention, + Utils.WrapInline(reason))); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); + var task = new Task(() => UnbanCommand.UnbanUser(guild, null, guild.GetCurrentUserAsync().Result, toBan, + Messages.PunishmentExpired)); + await Utils.StartDelayed(task, duration, () => guild.GetBanAsync(toBan).Result != null); + } + + public override List GetAliases() { + return new List {"ban", "бан"}; + } + + public override int GetArgumentsAmountRequired() { + return 2; + } + + public override string GetSummary() { + return "Банит пользователя"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/BanModule.cs b/Boyfriend/Commands/BanModule.cs deleted file mode 100644 index cd1295b..0000000 --- a/Boyfriend/Commands/BanModule.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Discord; -using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global - -namespace Boyfriend.Commands; - -public class BanModule : ModuleBase { - - [Command("ban")] - [Summary("Банит пользователя")] - [Alias("бан")] - public async Task Run(string user, [Remainder]string reason) { - TimeSpan duration; - try { - var reasonArray = reason.Split(); - duration = Utils.GetTimeSpan(reasonArray[0]); - reason = string.Join(" ", reasonArray.Skip(1)); - } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { - duration = TimeSpan.FromMilliseconds(-1); - } - var author = Context.Guild.GetUser(Context.User.Id); - var toBan = await Utils.ParseUser(user); - await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); - var memberToBan = Context.Guild.GetUser(toBan.Id); - if (memberToBan != null) - await CommandHandler.CheckInteractions(author, memberToBan); - BanUser(Context.Guild, Context.Guild.GetUser(Context.User.Id), toBan, duration, reason); - } - - public static async void BanUser(IGuild guild, IGuildUser author, IUser toBan, TimeSpan duration, string reason) { - var authorMention = author.Mention; - await Utils.SendDirectMessage(toBan, $"Тебя забанил {author.Mention} на сервере {guild.Name} за `{reason}`"); - var guildBanMessage = $"({author.Username}#{author.Discriminator}) {reason}"; - await guild.AddBanAsync(toBan, 0, guildBanMessage); - var notification = $"{authorMention} банит {toBan.Mention} за {Utils.WrapInline(reason)}"; - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - var task = new Task(() => UnbanModule.UnbanUser(guild, guild.GetCurrentUserAsync().Result, toBan, - "Время наказания истекло")); - await Utils.StartDelayed(task, duration, () => guild.GetBanAsync(toBan).Result != null); - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs new file mode 100644 index 0000000..fee031e --- /dev/null +++ b/Boyfriend/Commands/ClearCommand.cs @@ -0,0 +1,49 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class ClearCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + int toDelete; + try { + toDelete = Convert.ToInt32(args[0]); + } + catch (Exception e) when (e is FormatException or OverflowException) { + throw new ApplicationException(Messages.ClearInvalidAmountSpecified); + } + + if (context.Channel is not ITextChannel channel) return; + await CommandHandler.CheckPermissions(context.Guild.GetUser(context.User.Id), GuildPermission.ManageMessages); + switch (toDelete) { + case < 1: + throw new ApplicationException(Messages.ClearNegativeAmount); + case > 200: + throw new ApplicationException(Messages.ClearAmountTooLarge); + default: { + var messages = await channel.GetMessagesAsync(toDelete + 1).FlattenAsync(); + await channel.DeleteMessagesAsync(messages); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(context.Guild), + string.Format(Messages.MessagesDeleted, context.User.Mention, toDelete + 1, + Utils.MentionChannel(context.Channel.Id))); + break; + } + } + } + + public override List GetAliases() { + return new List {"clear", "purge", "очистить", "стереть"}; + } + + public override int GetArgumentsAmountRequired() { + return 1; + } + + public override string GetSummary() { + return "Очищает сообщения"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/ClearModule.cs b/Boyfriend/Commands/ClearModule.cs deleted file mode 100644 index 94eade6..0000000 --- a/Boyfriend/Commands/ClearModule.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Discord; -using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global - -namespace Boyfriend.Commands; - -public class ClearModule : ModuleBase { - - [Command("clear")] - [Summary("Удаляет указанное количество сообщений")] - [Alias("очистить")] - public async Task Run(int toDelete) { - if (Context.Channel is not ITextChannel channel) return; - await CommandHandler.CheckPermissions(Context.Guild.GetUser(Context.User.Id), GuildPermission.ManageMessages); - switch (toDelete) { - case < 1: - throw new Exception( "Указано отрицательное количество сообщений!"); - case > 200: - throw new Exception("Указано слишком много сообщений!"); - default: { - var messages = await channel.GetMessagesAsync(toDelete + 1).FlattenAsync(); - await channel.DeleteMessagesAsync(messages); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Context.Guild), - $"{Context.User.Mention} удаляет {toDelete + 1} сообщений в канале " + - $"{Utils.MentionChannel(Context.Channel.Id)}"); - break; - } - } - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/Command.cs b/Boyfriend/Commands/Command.cs new file mode 100644 index 0000000..b90d6b0 --- /dev/null +++ b/Boyfriend/Commands/Command.cs @@ -0,0 +1,19 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +namespace Boyfriend.Commands; + +public abstract class Command { + public abstract Task Run(SocketCommandContext context, string[] args); + + public abstract List GetAliases(); + + public abstract int GetArgumentsAmountRequired(); + + public abstract string GetSummary(); + + protected static async Task Warn(ISocketMessageChannel channel, string warning) { + await Utils.SilentSendAsync(channel as ITextChannel, ":warning: " + warning); + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/HelpCommand.cs b/Boyfriend/Commands/HelpCommand.cs new file mode 100644 index 0000000..b3eeed1 --- /dev/null +++ b/Boyfriend/Commands/HelpCommand.cs @@ -0,0 +1,30 @@ +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +public class HelpCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + var nl = Environment.NewLine; + var toSend = string.Format(Messages.CommandHelp, nl); + var prefix = Boyfriend.GetGuildConfig(context.Guild).Prefix; + toSend = CommandHandler.Commands.Aggregate(toSend, + (current, command) => current + $"`{prefix}{command.GetAliases()[0]}`: {command.GetSummary()}{nl}"); + + await context.Channel.SendMessageAsync(toSend); + } + + public override List GetAliases() { + return new List {"help", "помощь", "справка"}; + } + + public override int GetArgumentsAmountRequired() { + return 0; + } + + public override string GetSummary() { + return "Показывает эту справку"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/HelpModule.cs b/Boyfriend/Commands/HelpModule.cs deleted file mode 100644 index cafea6d..0000000 --- a/Boyfriend/Commands/HelpModule.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Discord.Commands; -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global - -namespace Boyfriend.Commands; - -public class HelpModule : ModuleBase { - - [Command("help")] - [Summary("Показывает эту справку")] - [Alias("помощь", "справка")] - public Task Run() { - var nl = Environment.NewLine; - var toSend = $"Справка по командам:{nl}"; - var prefix = Boyfriend.GetGuildConfig(Context.Guild).Prefix; - foreach (var command in EventHandler.Commands.Commands) { - var aliases = command.Aliases.Aggregate("", (current, alias) => - current + (current == "" ? "" : $", {prefix}") + alias); - toSend += $"`{prefix}{aliases}`: {command.Summary}{nl}"; - } - - Context.Channel.SendMessageAsync(toSend); - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs new file mode 100644 index 0000000..ac7038b --- /dev/null +++ b/Boyfriend/Commands/KickCommand.cs @@ -0,0 +1,47 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class KickCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + var reason = Utils.JoinString(args, 1); + var author = context.Guild.GetUser(context.User.Id); + var toKick = await Utils.ParseMember(context.Guild, args[0]); + await CommandHandler.CheckPermissions(author, GuildPermission.KickMembers); + await CommandHandler.CheckInteractions(author, toKick); + KickMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toKick, + reason); + } + + private static async void KickMember(IGuild guild, ITextChannel? channel, IUser author, IGuildUser toKick, + string reason) { + var authorMention = author.Mention; + await Utils.SendDirectMessage(toKick, string.Format(Messages.YouWereKicked, authorMention, guild.Name, + Utils.WrapInline(reason))); + var guildKickMessage = $"({author.Username}#{author.Discriminator}) {reason}"; + await toKick.KickAsync(guildKickMessage); + var notification = string.Format(Messages.MemberKicked, authorMention, toKick.Mention, + Utils.WrapInline(reason)); + await Utils.SilentSendAsync(channel, string.Format(Messages.KickResponse, toKick.Mention, + Utils.WrapInline(reason))); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); + } + + public override List GetAliases() { + return new List {"kick", "кик"}; + } + + public override int GetArgumentsAmountRequired() { + return 2; + } + + public override string GetSummary() { + return "Выгоняет участника"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/KickModule.cs b/Boyfriend/Commands/KickModule.cs deleted file mode 100644 index 67fb9fd..0000000 --- a/Boyfriend/Commands/KickModule.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Discord; -using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global - -namespace Boyfriend.Commands; - -public class KickModule : ModuleBase { - - [Command("kick")] - [Summary("Выгоняет пользователя")] - [Alias("кик")] - public async Task Run(string user, [Remainder]string reason) { - var author = Context.Guild.GetUser(Context.User.Id); - var toKick = await Utils.ParseMember(Context.Guild, user); - await CommandHandler.CheckPermissions(author, GuildPermission.KickMembers); - await CommandHandler.CheckInteractions(author, toKick); - KickMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toKick, reason); - } - - private static async void KickMember(IGuild guild, IUser author, IGuildUser toKick, string reason) { - var authorMention = author.Mention; - await Utils.SendDirectMessage(toKick, $"Тебя кикнул {authorMention} на сервере {guild.Name} за " + - $"{Utils.WrapInline(reason)}"); - - var guildKickMessage = $"({author.Username}#{author.Discriminator}) {reason}"; - await toKick.KickAsync(guildKickMessage); - var notification = $"{authorMention} выгоняет {toKick.Mention} за {Utils.WrapInline(reason)}"; - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs new file mode 100644 index 0000000..d9832fc --- /dev/null +++ b/Boyfriend/Commands/MuteCommand.cs @@ -0,0 +1,92 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class MuteCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + TimeSpan duration; + var reason = Utils.JoinString(args, 1); + try { + duration = Utils.GetTimeSpan(args[1]); + reason = Utils.JoinString(args, 2); + } + catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { + duration = TimeSpan.FromMilliseconds(-1); + } + + var author = context.Guild.GetUser(context.User.Id); + var toMute = await Utils.ParseMember(context.Guild, args[0]); + if (toMute == null) + throw new ApplicationException(Messages.UserNotInGuild); + var role = Utils.GetMuteRole(context.Guild); + if (role != null && toMute.RoleIds.Any(x => x == role.Id) || + toMute.TimedOutUntil != null && toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() + > DateTimeOffset.Now.ToUnixTimeMilliseconds()) + throw new ApplicationException(Messages.MemberAlreadyMuted); + var rolesRemoved = Boyfriend.GetGuildConfig(context.Guild).RolesRemovedOnMute; + if (rolesRemoved.ContainsKey(toMute.Id)) { + foreach (var roleId in rolesRemoved[toMute.Id]) await toMute.AddRoleAsync(roleId); + rolesRemoved.Remove(toMute.Id); + await Warn(context.Channel, Messages.RolesReturned); + return; + } + + await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); + await CommandHandler.CheckInteractions(author, toMute); + MuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toMute, + duration, reason); + } + + private static async void MuteMember(IGuild guild, ITextChannel? channel, IGuildUser author, IGuildUser toMute, + TimeSpan duration, string reason) { + await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); + var authorMention = author.Mention; + var role = Utils.GetMuteRole(guild); + var config = Boyfriend.GetGuildConfig(guild); + if (config.RemoveRolesOnMute && role != null) { + var rolesRemoved = new List(); + try { + foreach (var roleId in toMute.RoleIds) { + if (roleId == guild.Id) continue; + await toMute.RemoveRoleAsync(roleId); + rolesRemoved.Add(roleId); + } + } + catch (NullReferenceException) { } + + config.RolesRemovedOnMute.Add(toMute.Id, rolesRemoved); + await config.Save(); + } + + if (role != null) + await toMute.AddRoleAsync(role); + else + await toMute.SetTimeOutAsync(duration); + var notification = string.Format(Messages.MemberMuted, authorMention, toMute.Mention, Utils.WrapInline(reason)); + await Utils.SilentSendAsync(channel, string.Format(Messages.MuteResponse, toMute.Mention, + Utils.WrapInline(reason))); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); + var task = new Task(() => UnmuteCommand.UnmuteMember(guild, null, guild.GetCurrentUserAsync().Result, toMute, + Messages.PunishmentExpired)); + if (role != null) + await Utils.StartDelayed(task, duration, () => toMute.RoleIds.Any(x => x == role.Id)); + } + + public override List GetAliases() { + return new List {"mute", "мут", "мьют"}; + } + + public override int GetArgumentsAmountRequired() { + return 2; + } + + public override string GetSummary() { + return "Глушит участника"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/MuteModule.cs b/Boyfriend/Commands/MuteModule.cs deleted file mode 100644 index 24a67d5..0000000 --- a/Boyfriend/Commands/MuteModule.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Discord; -using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global - -namespace Boyfriend.Commands; - -public class MuteModule : ModuleBase { - - [Command("mute")] - [Summary("Глушит пользователя")] - [Alias("мут")] - public async Task Run(string user, [Remainder]string reason) { - TimeSpan duration; - try { - var reasonArray = reason.Split(); - duration = Utils.GetTimeSpan(reasonArray[0]); - reason = string.Join(" ", reasonArray.Skip(1)); - } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { - duration = TimeSpan.FromMilliseconds(-1); - } - var author = Context.Guild.GetUser(Context.User.Id); - var toMute = await Utils.ParseMember(Context.Guild, user); - if (toMute.RoleIds.Any(x => x == Utils.GetMuteRole(Context.Guild).Id)) - throw new Exception("Участник уже заглушен!"); - if (Boyfriend.GetGuildConfig(Context.Guild).RolesRemovedOnMute.ContainsKey(toMute.Id)) - throw new Exception("Кто-то убрал роль мута самостоятельно!"); - await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); - await CommandHandler.CheckInteractions(author, toMute); - MuteMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toMute, duration, reason); - } - - private static async void MuteMember(IGuild guild, IGuildUser author, IGuildUser toMute, TimeSpan duration, - string reason) { - await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); - var authorMention = author.Mention; - var role = Utils.GetMuteRole(guild); - var config = Boyfriend.GetGuildConfig(guild); - - if (config.RemoveRolesOnMute) { - var rolesRemoved = new List(); - try { - foreach (var roleId in toMute.RoleIds) { - if (roleId == guild.Id) continue; - await toMute.RemoveRoleAsync(roleId); - rolesRemoved.Add(roleId); - } - } catch (NullReferenceException) {} - config.RolesRemovedOnMute.Add(toMute.Id, rolesRemoved); - config.Save(); - } - - await toMute.AddRoleAsync(role); - var notification = $"{authorMention} глушит {toMute.Mention} за {Utils.WrapInline(reason)}"; - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - var task = new Task(() => UnmuteModule.UnmuteMember(guild, guild.GetCurrentUserAsync().Result, toMute, - "Время наказания истекло")); - await Utils.StartDelayed(task, duration, () => toMute.RoleIds.Any(x => x == role.Id)); - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/PingCommand.cs b/Boyfriend/Commands/PingCommand.cs new file mode 100644 index 0000000..ce8100a --- /dev/null +++ b/Boyfriend/Commands/PingCommand.cs @@ -0,0 +1,25 @@ +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +public class PingCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + await context.Channel.SendMessageAsync($"{Utils.GetBeep(Boyfriend.GetGuildConfig(context.Guild).Lang)}" + + $"{Boyfriend.Client.Latency}{Messages.Milliseconds}"); + } + + public override List GetAliases() { + return new List {"ping", "пинг", "задержка"}; + } + + public override int GetArgumentsAmountRequired() { + return 0; + } + + public override string GetSummary() { + return "Измеряет время обработки REST-запроса"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/PingModule.cs b/Boyfriend/Commands/PingModule.cs deleted file mode 100644 index 6c86073..0000000 --- a/Boyfriend/Commands/PingModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Discord.Commands; -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global - -namespace Boyfriend.Commands; - -public class PingModule : ModuleBase { - - [Command("ping")] - [Summary("Измеряет время обработки REST-запроса")] - [Alias("пинг")] - public async Task Run() - => await ReplyAsync($"{Utils.GetBeep()}{Boyfriend.Client.Latency}мс"); -} \ No newline at end of file diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs new file mode 100644 index 0000000..3902077 --- /dev/null +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -0,0 +1,125 @@ +using System.Globalization; +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +public class SettingsCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + await CommandHandler.CheckPermissions(context.Guild.GetUser(context.User.Id), GuildPermission.ManageGuild); + var config = Boyfriend.GetGuildConfig(context.Guild); + var guild = context.Guild; + if (args.Length == 0) { + var nl = Environment.NewLine; + var adminLogChannel = guild.GetTextChannel(config.AdminLogChannel); + var admin = adminLogChannel == null ? Messages.ChannelNotSpecified : adminLogChannel.Mention; + var botLogChannel = guild.GetTextChannel(config.BotLogChannel); + var bot = botLogChannel == null ? Messages.ChannelNotSpecified : botLogChannel.Mention; + var muteRole = guild.GetRole(config.MuteRole); + var mute = muteRole == null ? Messages.RoleNotSpecified : muteRole.Mention; + var defaultRole = guild.GetRole(config.DefaultRole); + var defaultr = muteRole == null ? Messages.RoleNotSpecified : defaultRole.Mention; + var toSend = string.Format(Messages.CurrentSettings, nl) + + string.Format(Messages.CurrentSettingsLang, config.Lang, nl) + + string.Format(Messages.CurrentSettingsPrefix, config.Prefix, nl) + + string.Format(Messages.CurrentSettingsRemoveRoles, YesOrNo(config.RemoveRolesOnMute), nl) + + string.Format(Messages.CurrentSettingsUseSystemChannel, YesOrNo(config.UseSystemChannel), nl) + + string.Format(Messages.CurrentSettingsSendWelcomeMessages, YesOrNo(config.UseSystemChannel), + nl) + + string.Format(Messages.CurrentSettingsWelcomeMessage, config.WelcomeMessage, nl) + + string.Format(Messages.CurrentSettingsDefaultRole, defaultr, nl) + + string.Format(Messages.CurrentSettingsMuteRole, mute, nl) + + string.Format(Messages.CurrentSettingsAdminLogChannel, admin, nl) + + string.Format(Messages.CurrentSettingsBotLogChannel, bot); + await Utils.SilentSendAsync(context.Channel as ITextChannel ?? throw new Exception(), toSend); + return; + } + + var setting = args[0].ToLower(); + var value = args[1].ToLower(); + + var boolValue = ParseBool(args[1]); + var channel = await Utils.ParseChannelNullable(value) as IGuildChannel; + var role = Utils.ParseRoleNullable(guild, value); + + switch (setting) { + case "lang" when value is not ("ru" or "en"): + throw new Exception(Messages.LanguageNotSupported); + case "lang": + config.Lang = value; + Messages.Culture = new CultureInfo(value); + break; + case "prefix": + config.Prefix = value; + break; + case "removerolesonmute": + config.RemoveRolesOnMute = GetBoolValue(boolValue); + break; + case "usesystemchannel": + config.UseSystemChannel = GetBoolValue(boolValue); + break; + case "sendwelcomemessages": + config.SendWelcomeMessages = GetBoolValue(boolValue); + break; + case "welcomemessage": + config.WelcomeMessage = value; + break; + case "defaultrole": + config.DefaultRole = GetRoleId(role); + break; + case "muterole": + config.MuteRole = GetRoleId(role); + break; + case "adminlogchannel": + config.AdminLogChannel = GetChannelId(channel); + break; + case "botlogchannel": + config.BotLogChannel = GetChannelId(channel); + break; + } + + await config.Save(); + + await context.Channel.SendMessageAsync(Messages.SettingsUpdated); + } + + private static bool? ParseBool(string toParse) { + try { + return bool.Parse(toParse.ToLower()); + } + catch (FormatException) { + return null; + } + } + + private static bool GetBoolValue(bool? from) { + return from ?? throw new Exception(Messages.InvalidBoolean); + } + + private static ulong GetRoleId(IRole? role) { + return (role ?? throw new Exception(Messages.InvalidRoleSpecified)).Id; + } + + private static ulong GetChannelId(IGuildChannel? channel) { + return (channel ?? throw new Exception(Messages.InvalidChannelSpecified)).Id; + } + + private static string YesOrNo(bool isYes) { + return isYes ? Messages.Yes : Messages.No; + } + + public override List GetAliases() { + return new List {"settings", "настройки", "config", "конфиг "}; + } + + public override int GetArgumentsAmountRequired() { + return 0; + } + + public override string GetSummary() { + return "Настраивает бота отдельно для этого сервера"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/SettingsModule.cs b/Boyfriend/Commands/SettingsModule.cs deleted file mode 100644 index dd25c56..0000000 --- a/Boyfriend/Commands/SettingsModule.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Discord; -using Discord.Commands; -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global - -namespace Boyfriend.Commands; - -public class SettingsModule : ModuleBase { - - [Command("settings")] - [Summary("Настраивает бота")] - [Alias("config", "настройки", "конфиг")] - public async Task Run([Remainder] string s = "") { - await CommandHandler.CheckPermissions(Context.Guild.GetUser(Context.User.Id), GuildPermission.ManageGuild); - var config = Boyfriend.GetGuildConfig(Context.Guild); - var sArray = s.Split(" "); - var guild = Context.Guild; - if (s == "") { - var nl = Environment.NewLine; - var adminLogChannel = guild.GetTextChannel(config.AdminLogChannel); - var admin = adminLogChannel == null ? "Не указан" : adminLogChannel.Mention; - var botLogChannel = guild.GetTextChannel(config.BotLogChannel); - var bot = botLogChannel == null ? "Не указан" : botLogChannel.Mention; - var muteRole = guild.GetRole(config.MuteRole); - var mute = muteRole == null ? "Не указана" : muteRole.Mention; - var toSend = $"Текущие настройки:{nl}" + - $"Язык (`lang`): `{config.Lang}`{nl}" + - $"Префикс (`prefix`): `{config.Prefix}`{nl}" + - $"Удалять роли при муте (`removeRolesOnMute`): {YesOrNo(config.RemoveRolesOnMute)}{nl}" + - "Использовать канал системных сообщений для уведомлений (`useSystemChannel`): " + - $"{YesOrNo(config.UseSystemChannel)}{nl}" + - $"Отправлять приветствия (`sendWelcomeMessages`): {YesOrNo(config.UseSystemChannel)}{nl}" + - $"Роль мута (`muteRole`): {mute}{nl}" + - $"Канал админ-уведомлений (`adminLogChannel`): " + - $"{admin}{nl}" + - $"Канал бот-уведомлений (`botLogChannel`): " + - $"{bot}"; - await Utils.SilentSendAsync(Context.Channel as ITextChannel ?? throw new Exception(), toSend); - return; - } - - var setting = sArray[0].ToLower(); - var value = sArray[1].ToLower(); - - ITextChannel? channel; - try { - channel = await Utils.ParseChannel(value) as ITextChannel; - } catch (FormatException) { - channel = null; - } - - IRole? role; - try { - role = Utils.ParseRole(guild, value); - } - catch (FormatException) { - role = null; - } - - var boolValue = ParseBool(sArray[1]); - - switch (setting) { - case "lang" when sArray[1].ToLower() != "ru": - throw new Exception("Язык не поддерживается!"); - case "lang": - config.Lang = value; - break; - case "prefix": - config.Prefix = value; - break; - case "removerolesonmute": - config.RemoveRolesOnMute = boolValue ?? - throw new Exception("Неверный параметр! Требуется `true` или `false"); - break; - case "usesystemchannel": - config.UseSystemChannel = boolValue ?? - throw new Exception("Неверный параметр! Требуется `true` или `false"); - break; - case "sendwelcomemessages": - config.SendWelcomeMessages = boolValue ?? - throw new Exception("Неверный параметр! Требуется `true` или `false"); - break; - case "adminlogchannel": - config.AdminLogChannel = Convert.ToUInt64((channel ?? - throw new Exception("Указан недействительный канал!")) - .Id); - break; - case "botlogchannel": - config.BotLogChannel = Convert.ToUInt64((channel ?? - throw new Exception("Указан недействительный канал!")) - .Id); - break; - case "muterole": - config.MuteRole = Convert.ToUInt64((role ?? throw new Exception("Указана недействительная роль!")) - .Id); - break; - } - - config.Save(); - - await Context.Channel.SendMessageAsync("Настройки успешно обновлены!"); - } - - private static bool? ParseBool(string toParse) { - try { - return bool.Parse(toParse.ToLower()); - } catch (FormatException) { - return null; - } - } - - private static string YesOrNo(bool isYes) { - return isYes ? "Да" : "Нет"; - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs new file mode 100644 index 0000000..c8b9fef --- /dev/null +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -0,0 +1,43 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class UnbanCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + var toUnban = await Utils.ParseUser(args[0]); + if (context.Guild.GetBanAsync(toUnban.Id) == null) + throw new Exception(Messages.UserNotBanned); + UnbanUser(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toUnban, + Utils.JoinString(args, 1)); + } + + public static async void UnbanUser(IGuild guild, ITextChannel? channel, IGuildUser author, IUser toUnban, + string reason) { + await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); + var authorMention = author.Mention; + var notification = string.Format(Messages.UserUnbanned, authorMention, toUnban.Mention, + Utils.WrapInline(reason)); + await guild.RemoveBanAsync(toUnban); + await Utils.SilentSendAsync(channel, string.Format(Messages.UnbanResponse, toUnban.Mention, + Utils.WrapInline(reason))); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); + } + + public override List GetAliases() { + return new List {"unban", "разбан"}; + } + + public override int GetArgumentsAmountRequired() { + return 2; + } + + public override string GetSummary() { + return "Возвращает пользователя из бана"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/UnbanModule.cs b/Boyfriend/Commands/UnbanModule.cs deleted file mode 100644 index 33b5902..0000000 --- a/Boyfriend/Commands/UnbanModule.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Discord; -using Discord.Commands; -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global - -namespace Boyfriend.Commands; - -public class UnbanModule : ModuleBase { - - [Command("unban")] - [Summary("Возвращает пользователя из бана")] - [Alias("разбан")] - public async Task Run(string user, [Remainder] string reason) { - var toUnban = await Utils.ParseUser(user); - if (Context.Guild.GetBanAsync(toUnban.Id) == null) - throw new Exception("Пользователь не забанен!"); - UnbanUser(Context.Guild, Context.Guild.GetUser(Context.User.Id), toUnban, reason); - } - - public static async void UnbanUser(IGuild guild, IGuildUser author, IUser toUnban, string reason) { - await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); - var authorMention = author.Mention; - var notification = $"{authorMention} возвращает из бана {toUnban.Mention} за {Utils.WrapInline(reason)}"; - await guild.RemoveBanAsync(toUnban); - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - } -} \ No newline at end of file diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs new file mode 100644 index 0000000..c416807 --- /dev/null +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -0,0 +1,61 @@ +using Discord; +using Discord.Commands; + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +public class UnmuteCommand : Command { + public override async Task Run(SocketCommandContext context, string[] args) { + var toUnmute = await Utils.ParseMember(context.Guild, args[0]); + var author = context.Guild.GetUser(context.User.Id); + await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); + await CommandHandler.CheckInteractions(author, toUnmute); + var role = Utils.GetMuteRole(context.Guild); + if (role != null) + if (toUnmute.RoleIds.All(x => x != role.Id)) { + var rolesRemoved = Boyfriend.GetGuildConfig(context.Guild).RolesRemovedOnMute; + + foreach (var roleId in rolesRemoved[toUnmute.Id]) await toUnmute.AddRoleAsync(roleId); + rolesRemoved.Remove(toUnmute.Id); + throw new ApplicationException(Messages.RolesReturned); + } + + UnmuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), + toUnmute, Utils.JoinString(args, 1)); + } + + public static async void UnmuteMember(IGuild guild, ITextChannel? channel, IGuildUser author, IGuildUser toUnmute, + string reason) { + await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); + var authorMention = author.Mention; + var notification = string.Format(Messages.MemberUnmuted, authorMention, toUnmute.Mention, + Utils.WrapInline(reason)); + await toUnmute.RemoveRoleAsync(Utils.GetMuteRole(guild)); + var config = Boyfriend.GetGuildConfig(guild); + + if (config.RolesRemovedOnMute.ContainsKey(toUnmute.Id)) { + foreach (var roleId in config.RolesRemovedOnMute[toUnmute.Id]) await toUnmute.AddRoleAsync(roleId); + config.RolesRemovedOnMute.Remove(toUnmute.Id); + } + + await Utils.SilentSendAsync(channel, string.Format(Messages.UnmuteResponse, toUnmute.Mention, + Utils.WrapInline(reason))); + await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); + } + + public override List GetAliases() { + return new List {"unmute", "размут"}; + } + + public override int GetArgumentsAmountRequired() { + return 2; + } + + public override string GetSummary() { + return "Снимает мут с участника"; + } +} \ No newline at end of file diff --git a/Boyfriend/Commands/UnmuteModule.cs b/Boyfriend/Commands/UnmuteModule.cs deleted file mode 100644 index 9baee78..0000000 --- a/Boyfriend/Commands/UnmuteModule.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Discord; -using Discord.Commands; -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global - -namespace Boyfriend.Commands; - -public class UnmuteModule : ModuleBase { - - [Command("unmute")] - [Summary("Возвращает пользователя из мута")] - [Alias("размут")] - public async Task Run(string user, [Remainder] string reason) { - var toUnmute = await Utils.ParseMember(Context.Guild, user); - var author = Context.Guild.GetUser(Context.User.Id); - await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); - await CommandHandler.CheckInteractions(author, toUnmute); - if (toUnmute.RoleIds.All(x => x != Utils.GetMuteRole(Context.Guild).Id)) { - var rolesRemoved = Boyfriend.GetGuildConfig(Context.Guild).RolesRemovedOnMute; - if (!rolesRemoved.ContainsKey(toUnmute.Id)) throw new Exception("Пользователь не в муте!"); - rolesRemoved.Remove(toUnmute.Id); - throw new Exception("Пользователь не в муте, но я нашёл и удалил запись о его удалённых ролях!"); - } - - UnmuteMember(Context.Guild, Context.Guild.GetUser(Context.User.Id), toUnmute, reason); - } - - public static async void UnmuteMember(IGuild guild, IGuildUser author, IGuildUser toUnmute, string reason) { - await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); - var authorMention = author.Mention; - var notification = $"{authorMention} возвращает из мута {toUnmute.Mention} за {Utils.WrapInline(reason)}"; - await toUnmute.RemoveRoleAsync(Utils.GetMuteRole(guild)); - var config = Boyfriend.GetGuildConfig(guild); - - if (config.RolesRemovedOnMute.ContainsKey(toUnmute.Id)) { - foreach (var roleId in config.RolesRemovedOnMute[toUnmute.Id]) { - await toUnmute.AddRoleAsync(roleId); - } - config.RolesRemovedOnMute.Remove(toUnmute.Id); - } - - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - } -} \ No newline at end of file diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 05d6fb2..fdae9d2 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Globalization; using Boyfriend.Commands; using Discord; using Discord.Commands; @@ -8,24 +8,25 @@ namespace Boyfriend; public class EventHandler { private readonly DiscordSocketClient _client = Boyfriend.Client; - public static readonly CommandService Commands = new(); - public async Task InitEvents() { + public void InitEvents() { _client.Ready += ReadyEvent; _client.MessageDeleted += MessageDeletedEvent; _client.MessageReceived += MessageReceivedEvent; _client.MessageUpdated += MessageUpdatedEvent; _client.UserJoined += UserJoinedEvent; - await Commands.AddModulesAsync(Assembly.GetEntryAssembly(), null); } private static async Task ReadyEvent() { await Boyfriend.SetupGuildConfigs(); + var i = new Random().Next(3); foreach (var guild in Boyfriend.Client.Guilds) { - var channel = guild.GetTextChannel(Boyfriend.GetGuildConfig(guild).BotLogChannel); + var config = Boyfriend.GetGuildConfig(guild); + Messages.Culture = new CultureInfo(config.Lang); + var channel = guild.GetTextChannel(config.BotLogChannel); if (channel == null) continue; - await channel.SendMessageAsync($"{Utils.GetBeep()}Я запустился! (C#)"); + await channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(config.Lang, i))); } } @@ -33,13 +34,11 @@ public class EventHandler { Cacheable channel) { var msg = message.Value; var toSend = msg == null - ? $"Удалено сообщение в канале {Utils.MentionChannel(channel.Id)}, но я забыл что там было" - : $"Удалено сообщение от {msg.Author.Mention} в канале " + + ? string.Format(Messages.UncachedMessageDeleted, Utils.MentionChannel(channel.Id)) + : string.Format(Messages.CachedMessageDeleted, msg.Author.Mention) + $"{Utils.MentionChannel(channel.Id)}: {Environment.NewLine}{Utils.Wrap(msg.Content)}"; - try { - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel( - Boyfriend.FindGuild(channel.Value as ITextChannel)), toSend); - } catch (ArgumentException) {} + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel( + Boyfriend.FindGuild(channel.Value)), toSend); } private static async Task MessageReceivedEvent(SocketMessage messageParam) { @@ -48,45 +47,54 @@ public class EventHandler { var guild = user.Guild; var argPos = 0; + var guildConfig = Boyfriend.GetGuildConfig(guild); + Messages.Culture = new CultureInfo(guildConfig.Lang); if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && !user.GuildPermissions.MentionEveryone) - BanModule.BanUser(guild, await guild.GetCurrentUserAsync(), user, TimeSpan.FromMilliseconds(-1), - "Более 3-ёх упоминаний в одном сообщении"); + await BanCommand.BanUser(guild, null, await guild.GetCurrentUserAsync(), user, + TimeSpan.FromMilliseconds(-1), Messages.AutobanReason); var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); var prevsArray = prevs as IMessage[] ?? prevs.ToArray(); - var prev = prevsArray[1].Content; - var prevFailsafe = prevsArray[2].Content; - if (message.Channel is not ITextChannel channel) throw new Exception(); - if (!(message.HasStringPrefix(Boyfriend.GetGuildConfig(guild).Prefix, ref argPos) + var prev = ""; + var prevFailsafe = ""; + try { + prev = prevsArray[1].Content; + prevFailsafe = prevsArray[2].Content; + } + catch (IndexOutOfRangeException) { } + + if (!(message.HasStringPrefix(guildConfig.Prefix, ref argPos) || message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) - || user == await Boyfriend.FindGuild(channel).GetCurrentUserAsync() + || user == await guild.GetCurrentUserAsync() || user.IsBot && message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)) return; - await CommandHandler.HandleCommand(message, argPos); + await CommandHandler.HandleCommand(message); } private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, ISocketMessageChannel channel) { var msg = messageCached.Value; var nl = Environment.NewLine; - if (msg.Content == messageSocket.Content) return; + if (msg != null && msg.Content == messageSocket.Content) return; var toSend = msg == null - ? $"Отредактировано сообщение от {messageSocket.Author.Mention} в канале" + - $" {Utils.MentionChannel(channel.Id)}," + " но я забыл что там было до редактирования: " + + ? string.Format(Messages.UncachedMessageEdited, messageSocket.Author.Mention, + Utils.MentionChannel(channel.Id)) + Utils.Wrap(messageSocket.Content) - : $"Отредактировано сообщение от {msg.Author.Mention} " + - $"в канале {Utils.MentionChannel(channel.Id)}." + - $"{nl}До:{nl}{Utils.Wrap(msg.Content)}{nl}После:{nl}{Utils.Wrap(messageSocket.Content)}"; - try { - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Boyfriend.FindGuild(channel as ITextChannel)), + : string.Format(Messages.CachedMessageEdited, msg.Author.Mention, Utils.MentionChannel(channel.Id), nl, nl, + Utils.Wrap(msg.Content), nl, nl, Utils.Wrap(messageSocket.Content)); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Boyfriend.FindGuild(channel)), toSend); - } catch (ArgumentException) {} } private static async Task UserJoinedEvent(SocketGuildUser user) { var guild = user.Guild; - await guild.SystemChannel.SendMessageAsync($"{user.Mention}, добро пожаловать на сервер {guild.Name}"); + var config = Boyfriend.GetGuildConfig(guild); + if (config.SendWelcomeMessages) + await Utils.SilentSendAsync(guild.SystemChannel, string.Format(config.WelcomeMessage, user.Mention, + guild.Name)); + if (config.DefaultRole != 0) + await user.AddRoleAsync(Utils.ParseRole(guild, config.DefaultRole.ToString())); } } \ No newline at end of file diff --git a/Boyfriend/GuildConfig.cs b/Boyfriend/GuildConfig.cs index 80490de..1238579 100644 --- a/Boyfriend/GuildConfig.cs +++ b/Boyfriend/GuildConfig.cs @@ -9,26 +9,31 @@ public class GuildConfig { public bool RemoveRolesOnMute { get; set; } public bool UseSystemChannel { get; set; } public bool SendWelcomeMessages { get; set; } + public string WelcomeMessage { get; set; } + public ulong DefaultRole { get; set; } public ulong MuteRole { get; set; } public ulong AdminLogChannel { get; set; } public ulong BotLogChannel { get; set; } public Dictionary> RolesRemovedOnMute { get; set; } public GuildConfig(ulong id, string lang, string prefix, bool removeRolesOnMute, bool useSystemChannel, - bool sendWelcomeMessages, ulong muteRole, ulong adminLogChannel, ulong botLogChannel) { + bool sendWelcomeMessages, string welcomeMessage, ulong defaultRole, ulong muteRole, ulong adminLogChannel, + ulong botLogChannel) { Id = id; Lang = lang; Prefix = prefix; RemoveRolesOnMute = removeRolesOnMute; UseSystemChannel = useSystemChannel; SendWelcomeMessages = sendWelcomeMessages; + WelcomeMessage = welcomeMessage; + DefaultRole = defaultRole; MuteRole = muteRole; AdminLogChannel = adminLogChannel; BotLogChannel = botLogChannel; RolesRemovedOnMute = new Dictionary>(); } - public async void Save() { + public async Task Save() { await using var stream = File.OpenWrite("config_" + Id + ".json"); await JsonSerializer.SerializeAsync(stream, this); } diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs new file mode 100644 index 0000000..6538678 --- /dev/null +++ b/Boyfriend/Messages.Designer.cs @@ -0,0 +1,486 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Boyfriend { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Messages { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Messages() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string CouldntFindGuildByChannel { + get { + return ResourceManager.GetString("CouldntFindGuildByChannel", resourceCulture); + } + } + + internal static string Ready { + get { + return ResourceManager.GetString("Ready", resourceCulture); + } + } + + internal static string UncachedMessageDeleted { + get { + return ResourceManager.GetString("UncachedMessageDeleted", resourceCulture); + } + } + + internal static string CachedMessageDeleted { + get { + return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); + } + } + + internal static string AutobanReason { + get { + return ResourceManager.GetString("AutobanReason", resourceCulture); + } + } + + internal static string UncachedMessageEdited { + get { + return ResourceManager.GetString("UncachedMessageEdited", resourceCulture); + } + } + + internal static string CachedMessageEdited { + get { + return ResourceManager.GetString("CachedMessageEdited", resourceCulture); + } + } + + internal static string DefaultWelcomeMessage { + get { + return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); + } + } + + internal static string Beep1 { + get { + return ResourceManager.GetString("Beep1", resourceCulture); + } + } + + internal static string Beep2 { + get { + return ResourceManager.GetString("Beep2", resourceCulture); + } + } + + internal static string Beep3 { + get { + return ResourceManager.GetString("Beep3", resourceCulture); + } + } + + internal static string InvalidAdminLogChannel { + get { + return ResourceManager.GetString("InvalidAdminLogChannel", resourceCulture); + } + } + + internal static string MuteRoleRequired { + get { + return ResourceManager.GetString("MuteRoleRequired", resourceCulture); + } + } + + internal static string CommandExecutionUnsuccessful { + get { + return ResourceManager.GetString("CommandExecutionUnsuccessful", resourceCulture); + } + } + + internal static string RepeatedArgumentsDetected { + get { + return ResourceManager.GetString("RepeatedArgumentsDetected", resourceCulture); + } + } + + internal static string CommandParseFailed { + get { + return ResourceManager.GetString("CommandParseFailed", resourceCulture); + } + } + + internal static string UnknownCommand { + get { + return ResourceManager.GetString("UnknownCommand", resourceCulture); + } + } + + internal static string BadArgumentCount { + get { + return ResourceManager.GetString("BadArgumentCount", resourceCulture); + } + } + + internal static string ArgumentNotPresent { + get { + return ResourceManager.GetString("ArgumentNotPresent", resourceCulture); + } + } + + internal static string CommandNoPermissionBot { + get { + return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); + } + } + + internal static string CommandNoPermissionUser { + get { + return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); + } + } + + internal static string InteractionsDifferentGuilds { + get { + return ResourceManager.GetString("InteractionsDifferentGuilds", resourceCulture); + } + } + + internal static string InteractionsOwner { + get { + return ResourceManager.GetString("InteractionsOwner", resourceCulture); + } + } + + internal static string InteractionsYourself { + get { + return ResourceManager.GetString("InteractionsYourself", resourceCulture); + } + } + + internal static string InteractionsMe { + get { + return ResourceManager.GetString("InteractionsMe", resourceCulture); + } + } + + internal static string InteractionsFailedUser { + get { + return ResourceManager.GetString("InteractionsFailedUser", resourceCulture); + } + } + + internal static string InteractionsFailedBot { + get { + return ResourceManager.GetString("InteractionsFailedBot", resourceCulture); + } + } + + internal static string YouWereBanned { + get { + return ResourceManager.GetString("YouWereBanned", resourceCulture); + } + } + + internal static string UserBanned { + get { + return ResourceManager.GetString("UserBanned", resourceCulture); + } + } + + internal static string PunishmentExpired { + get { + return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + + internal static string ClearNegativeAmount { + get { + return ResourceManager.GetString("ClearNegativeAmount", resourceCulture); + } + } + + internal static string ClearAmountTooLarge { + get { + return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); + } + } + + internal static string MessagesDeleted { + get { + return ResourceManager.GetString("MessagesDeleted", resourceCulture); + } + } + + internal static string CommandHelp { + get { + return ResourceManager.GetString("CommandHelp", resourceCulture); + } + } + + internal static string YouWereKicked { + get { + return ResourceManager.GetString("YouWereKicked", resourceCulture); + } + } + + internal static string MemberKicked { + get { + return ResourceManager.GetString("MemberKicked", resourceCulture); + } + } + + internal static string MemberMuted { + get { + return ResourceManager.GetString("MemberMuted", resourceCulture); + } + } + + internal static string Milliseconds { + get { + return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + + internal static string MemberAlreadyMuted { + get { + return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + + internal static string MuteRoleManuallyRemoved { + get { + return ResourceManager.GetString("MuteRoleManuallyRemoved", resourceCulture); + } + } + + internal static string ChannelNotSpecified { + get { + return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); + } + } + + internal static string RoleNotSpecified { + get { + return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + } + } + + internal static string CurrentSettings { + get { + return ResourceManager.GetString("CurrentSettings", resourceCulture); + } + } + + internal static string CurrentSettingsLang { + get { + return ResourceManager.GetString("CurrentSettingsLang", resourceCulture); + } + } + + internal static string CurrentSettingsPrefix { + get { + return ResourceManager.GetString("CurrentSettingsPrefix", resourceCulture); + } + } + + internal static string CurrentSettingsRemoveRoles { + get { + return ResourceManager.GetString("CurrentSettingsRemoveRoles", resourceCulture); + } + } + + internal static string CurrentSettingsUseSystemChannel { + get { + return ResourceManager.GetString("CurrentSettingsUseSystemChannel", resourceCulture); + } + } + + internal static string CurrentSettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("CurrentSettingsSendWelcomeMessages", resourceCulture); + } + } + + internal static string CurrentSettingsDefaultRole { + get { + return ResourceManager.GetString("CurrentSettingsDefaultRole", resourceCulture); + } + } + + internal static string CurrentSettingsMuteRole { + get { + return ResourceManager.GetString("CurrentSettingsMuteRole", resourceCulture); + } + } + + internal static string CurrentSettingsAdminLogChannel { + get { + return ResourceManager.GetString("CurrentSettingsAdminLogChannel", resourceCulture); + } + } + + internal static string CurrentSettingsBotLogChannel { + get { + return ResourceManager.GetString("CurrentSettingsBotLogChannel", resourceCulture); + } + } + + internal static string LanguageNotSupported { + get { + return ResourceManager.GetString("LanguageNotSupported", resourceCulture); + } + } + + internal static string SettingsUpdated { + get { + return ResourceManager.GetString("SettingsUpdated", resourceCulture); + } + } + + internal static string InvalidBoolean { + get { + return ResourceManager.GetString("InvalidBoolean", resourceCulture); + } + } + + internal static string InvalidRoleSpecified { + get { + return ResourceManager.GetString("InvalidRoleSpecified", resourceCulture); + } + } + + internal static string InvalidChannelSpecified { + get { + return ResourceManager.GetString("InvalidChannelSpecified", resourceCulture); + } + } + + internal static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } + + internal static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + + internal static string UserNotBanned { + get { + return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + + internal static string MemberNotMuted { + get { + return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + + internal static string RolesReturned { + get { + return ResourceManager.GetString("RolesReturned", resourceCulture); + } + } + + internal static string MemberUnmuted { + get { + return ResourceManager.GetString("MemberUnmuted", resourceCulture); + } + } + + internal static string UserUnbanned { + get { + return ResourceManager.GetString("UserUnbanned", resourceCulture); + } + } + + internal static string CurrentSettingsWelcomeMessage { + get { + return ResourceManager.GetString("CurrentSettingsWelcomeMessage", resourceCulture); + } + } + + internal static string NotEnoughArguments { + get { + return ResourceManager.GetString("NotEnoughArguments", resourceCulture); + } + } + + internal static string ClearInvalidAmountSpecified { + get { + return ResourceManager.GetString("ClearInvalidAmountSpecified", resourceCulture); + } + } + + internal static string BanResponse { + get { + return ResourceManager.GetString("BanResponse", resourceCulture); + } + } + + internal static string KickResponse { + get { + return ResourceManager.GetString("KickResponse", resourceCulture); + } + } + + internal static string UserNotInGuild { + get { + return ResourceManager.GetString("UserNotInGuild", resourceCulture); + } + } + + internal static string MuteResponse { + get { + return ResourceManager.GetString("MuteResponse", resourceCulture); + } + } + + internal static string UnbanResponse { + get { + return ResourceManager.GetString("UnbanResponse", resourceCulture); + } + } + + internal static string UnmuteResponse { + get { + return ResourceManager.GetString("UnmuteResponse", resourceCulture); + } + } + } +} diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx new file mode 100644 index 0000000..bc46f3c --- /dev/null +++ b/Boyfriend/Messages.resx @@ -0,0 +1,246 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + Couldn't find guild by message! + + + {0}I'm ready! (C#) + + + Deleted message in {0}, but I forgot what was there + + + Deleted message from {0} in channel + + + Too many mentions in 1 message + + + Message edited from {0} in channel {1}, but I forgot what was there before the edit + + + Message edited from {0} in channel {1}.{2}Before:{3}{4}{5}After:{6}{7} + + + {0}, welcome to {1} + + + Bah! + + + Bop! + + + Beep! + + + Invalid admin log channel for guild + + + You must set up a mute role in settings! + + + Command execution was unsuccessful: {0} + + + Repeated arguments detected! {0} + + + Command parsing failed: {0} + + + Unknown command! {0} + + + Invalid argument count! {0} + + + Arguments not present! {0} + + + I do not have permission to execute this command! + + + You do not have permission to execute this command! + + + Members are in different guilds! + + + You cannot interact with guild owner! + + + You cannot interact with yourself! + + + You cannot interact with me! + + + You cannot interact with this member! + + + I cannot interact with this member! + + + You were banned by {0} in guild {1} for {2} + + + {0} banned {1} for {2} + + + Punishment expired + + + Negative message amount specified! + + + Too many messages specified! + + + {0} deleted {1} messages in channel {2} + + + Command help:{0} + + + You were kicked by {0} in guild {1} for {2} + + + {0} kicked {1} for {2} + + + {0} muted {1} for {2} + + + ms + + + Member is already muted! + + + Someone removed the mute role manually! + + + Not specified + + + Not specified + + + Current settings:{0} + + + Language (`lang`): `{0}`{1} + + + Prefix (`prefix`): `{0}`{1} + + + Remove roles on mute (`removeRolesOnMute`): {0}{1} + + + Use system channel for notifications (`useSystemChannel`): {0}{1} + + + Send welcome messages (`sendWelcomeMessages`): {0}{1} + + + Default role (`defaultRole`): {0}{1} + + + Mute role (`muteRole`): {0}{1} + + + Admin log channel (`adminLogChannel`): {0}{1} + + + Bot log channel (`botLogChannel`): {0} + + + Language not supported! + + + Settings successfully updated + + + Invalid argument! 'true' or 'false' required! + + + Invalid role specified! + + + Invalid channel specified! + + + Yes + + + No + + + User not banned! + + + Member not muted! + + + Someone removed the mute role manually! I added back all roles that I removed during the mute + + + {0} unmuted {1} for {2} + + + {0} unbanned {1} for {2} + + + Welcome message: `{0}`{1} + + + Not enough arguments! Needed: {0}, provided: {1} + + + Invalid message amount specified! + + + :white_check_mark: Successfully banned {0} for {1} + + + :white_check_mark: Successfully kicked {0} for {1} + + + The specified user is not a member of this server! + + + :white_check_mark: Successfully muted {0} for {1} + + + :white_check_mark: Successfully unbanned {0} for {1} + + + :white_check_mark: Successfully unmuted {0} for {1} + + \ No newline at end of file diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx new file mode 100644 index 0000000..534cca1 --- /dev/null +++ b/Boyfriend/Messages.ru.resx @@ -0,0 +1,237 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + Не удалось найти сервер по каналу! + + + {0}Я запустился! (C#) + + + Удалено сообщение в канале {0}, но я забыл что там было + + + Удалено сообщение от {0} в канале + + + Слишком много упоминаний в одном сообщении + + + Отредактировано сообщение от {0} в канале {1}, но я забыл что там было до редактирования + + + Отредактировано сообщение от {0} в канале {1}.{2}До:{3}{4}{5}После:{6}{7} + + + {0}, добро пожаловать на сервер {1} + + + Бап! + + + Боп! + + + Бип! + + + Требуется указать роль мута в настройках! + + + Неверный канал админ-логов для гильдии + + + Выполнение команды завершилось неудачей: {0} + + + Обнаружены повторяющиеся типы аргументов! {0} + + + Не удалось обработать команду: {0} + + + Неизвестная команда! {0} + + + Неверное количество аргументов! {0} + + + Нету нужных аргументов! {0} + + + У меня недостаточно прав для выполнения этой команды! + + + У тебя недостаточно прав для выполнения этой команды! + + + Участники находятся в разных гильдиях! + + + Ты не можешь взаимодействовать с владельцем сервера! + + + Ты не можешь взаимодействовать с самим собой! + + + Ты не можешь со мной взаимодействовать! + + + Ты не можешь взаимодействовать с этим участником! + + + Я не могу взаимодействовать с этим участником! + + + Тебя забанил {0} на сервере {1} за {2} + + + {0} банит {1} за {2} + + + Время наказания истекло + + + Указано отрицательное количество сообщений! + + + Указано слишком много сообщений! + + + {0} удаляет {1} сообщений в канале {2} + + + Справка по командам:{0} + + + Тебя кикнул {0} на сервере {1} за {2} + + + {0} выгоняет {1} за {2} + + + {0} глушит {1} за {2} + + + мс + + + Участник уже заглушен! + + + Кто-то убрал роль мута самостоятельно! + + + Не указан + + + Не указана + + + Текущие настройки:{0} + + + Язык (`lang`): `{0}`{1} + + + Префикс (`prefix`): `{0}`{1} + + + Удалять роли при муте (`removeRolesOnMute`): {0}{1} + + + Использовать канал системных сообщений для уведомлений (`useSystemChannel`): {0}{1} + + + Отправлять приветствия (`sendWelcomeMessages`): {0}{1} + + + Стандартная роль (`defaultRole`): {0}{1} + + + Роль мута (`muteRole`): {0}{1} + + + Канал админ-уведомлений (`adminLogChannel`): {0}{1} + + + Канал бот-уведомлений (`botLogChannel`): {0} + + + Язык не поддерживается! + + + Настройки успешно обновлены! + + + Неверный параметр! Требуется 'true' или 'false' + + + Указана недействительная роль! + + + Указан недействильный канал! + + + Да + + + Нет + + + Пользователь не забанен! + + + Участник не заглушен! + + + Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте + + + {0} возвращает из мута {1} за {2} + + + {0} возвращает из бана {1} за {2} + + + Приветствие: `{0}`{1} + + + Недостаточно аргументов! Требуется: {0}, указано: {1} + + + Указано неверное количество сообщений! + + + :white_check_mark: Успешно забанен {0} за {1} + + + :white_check_mark: Успешно выгнан {0} за {1} + + + Указанный пользователь не является участником этого сервера! + + + :white_check_mark: Успешно заглушен {0} за {1} + + + :white_check_mark: Успешно возвращён из бана {0} за {1} + + + :white_check_mark: Успешно возвращён из мута {0} за {1} + + \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 2c9ad4b..b6b981f 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -6,17 +6,23 @@ using Discord.Net; namespace Boyfriend; public static class Utils { - public static string GetBeep() { - var letters = new[] {"а", "о", "и"}; - return $"Б{letters[new Random().Next(3)]}п! "; + private static readonly string[] Formats = { + "%d'd'%h'h'%m'm'%s's'", "%d'd'%h'h'%m'm'", "%d'd'%h'h'%s's'", "%d'd'%h'h'", "%d'd'%m'm'%s's'", "%d'd'%m'm'", + "%d'd'%s's'", "%d'd'", "%h'h'%m'm'%s's'", "%h'h'%m'm'", "%h'h'%s's'", "%h'h'", "%m'm'%s's'", "%m'm'", "%s's'", + + "%d'д'%h'ч'%m'м'%s'с'", "%d'д'%h'ч'%m'м'", "%d'д'%h'ч'%s'с'", "%d'д'%h'ч'", "%d'д'%m'м'%s'с'", "%d'д'%m'м'", + "%d'д'%s'с'", "%d'д'", "%h'ч'%m'м'%s'с'", "%h'ч'%m'м'", "%h'ч'%s'с'", "%h'ч'", "%m'м'%s'с'", "%m'м'", "%s'с'" + }; + + public static string GetBeep(string cultureInfo, int i = -1) { + Messages.Culture = new CultureInfo(cultureInfo); + var beeps = new[] {Messages.Beep1, Messages.Beep2, Messages.Beep3}; + return beeps[i < 0 ? new Random().Next(3) : i]; } - public static async Task GetAdminLogChannel(IGuild guild) { - var adminLogChannel = await ParseChannel(Boyfriend.GetGuildConfig(guild).AdminLogChannel.ToString()); - if (adminLogChannel is ITextChannel channel) - return channel; - - throw new Exception("Неверный канал админ-логов для гильдии " + guild.Id); + public static async Task GetAdminLogChannel(IGuild guild) { + var adminLogChannel = await ParseChannelNullable(Boyfriend.GetGuildConfig(guild).AdminLogChannel.ToString()); + return adminLogChannel as ITextChannel; } public static string Wrap(string original) { @@ -43,6 +49,15 @@ public static class Utils { return Convert.ToUInt64(Regex.Replace(mention, "[^0-9]", "")); } + private static ulong? ParseMentionNullable(string mention) { + try { + return ParseMention(mention) == 0 ? throw new FormatException() : ParseMention(mention); + } + catch (FormatException) { + return null; + } + } + public static async Task ParseUser(string mention) { var user = Boyfriend.Client.GetUserAsync(ParseMention(mention)); return await user; @@ -52,14 +67,22 @@ public static class Utils { return await guild.GetUserAsync(ParseMention(mention)); } - public static async Task ParseChannel(string mention) { + private static async Task ParseChannel(string mention) { return await Boyfriend.Client.GetChannelAsync(ParseMention(mention)); } - public static IRole ParseRole(IGuild guild, string mention) { + public static async Task ParseChannelNullable(string mention) { + return ParseMentionNullable(mention) == null ? null : await ParseChannel(mention); + } + + public static IRole? ParseRole(IGuild guild, string mention) { return guild.GetRole(ParseMention(mention)); } + public static IRole? ParseRoleNullable(IGuild guild, string mention) { + return ParseMentionNullable(mention) == null ? null : ParseRole(guild, mention); + } + public static async Task SendDirectMessage(IUser user, string toSend) { try { await user.SendMessageAsync(toSend); @@ -69,24 +92,23 @@ public static class Utils { } } - public static IRole GetMuteRole(IGuild guild) { + public static IRole? GetMuteRole(IGuild guild) { var role = guild.Roles.FirstOrDefault(x => x.Id == Boyfriend.GetGuildConfig(guild).MuteRole); - if (role == null) throw new Exception("Требуется указать роль мута в настройках!"); return role; } - public static async Task SilentSendAsync(ITextChannel channel, string text) { + public static async Task SilentSendAsync(ITextChannel? channel, string text) { + if (channel == null) return; try { await channel.SendMessageAsync(text, false, null, null, AllowedMentions.None); } catch (ArgumentException) {} } - - private static readonly string[] Formats = { - "%d'd'%h'h'%m'm'%s's'", "%d'd'%h'h'%m'm'", "%d'd'%h'h'%s's'", "%d'd'%h'h'", "%d'd'%m'm'%s's'", "%d'd'%m'm'", - "%d'd'%s's'", "%d'd'", "%h'h'%m'm'%s's'", "%h'h'%m'm'", "%h'h'%s's'", "%h'h'", "%m'm'%s's'", "%m'm'", "%s's'" - }; public static TimeSpan GetTimeSpan(string from) { return TimeSpan.ParseExact(from.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture); } + + public static string JoinString(string[] args, int startIndex) { + return string.Join(" ", args, startIndex, args.Length - startIndex); + } } \ No newline at end of file From 4d838e5af393a779f8d9483d45758cec50c7c14f Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Sun, 30 Jan 2022 13:43:15 +0500 Subject: [PATCH 007/329] every single file changed lulw --- Boyfriend/Boyfriend.cs | 36 +- Boyfriend/Boyfriend.csproj | 3 +- Boyfriend/CommandHandler.cs | 8 +- Boyfriend/Commands/MuteCommand.cs | 12 +- Boyfriend/Commands/PingCommand.cs | 4 +- Boyfriend/Commands/SettingsCommand.cs | 77 +- Boyfriend/Commands/UnbanCommand.cs | 4 +- Boyfriend/Commands/UnmuteCommand.cs | 32 +- Boyfriend/EventHandler.cs | 20 +- Boyfriend/GuildConfig.cs | 68 +- Boyfriend/Messages.Designer.cs | 1056 +++++++++++++++---------- Boyfriend/Messages.resx | 6 + Boyfriend/Messages.ru.resx | 6 + Boyfriend/Utils.cs | 4 +- 14 files changed, 825 insertions(+), 511 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 05d473b..7d3d94d 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -1,5 +1,5 @@ using System.Globalization; -using System.Text.Json; +using Newtonsoft.Json; using Discord; using Discord.WebSocket; @@ -26,7 +26,7 @@ public static class Boyfriend { await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); - await Client.SetActivityAsync(new Game("Retrospecter - Chiller", ActivityType.Listening)); + await Client.SetActivityAsync(new Game("Retrospecter - Expurgation", ActivityType.Listening)); new EventHandler().InitEvents(); @@ -41,27 +41,33 @@ public static class Boyfriend { public static async Task SetupGuildConfigs() { foreach (var guild in Client.Guilds) { var path = "config_" + guild.Id + ".json"; - var openStream = !File.Exists(path) ? File.Create(path) : File.OpenRead(path); + if (!File.Exists(path)) File.Create(path); - GuildConfig config; - try { - config = await JsonSerializer.DeserializeAsync(openStream) ?? throw new Exception(); - } catch (JsonException) { + var config = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(path)); + if (config == null) { Messages.Culture = new CultureInfo("ru"); - config = new GuildConfig(guild.Id, "ru", "!", false, true, - true, Messages.DefaultWelcomeMessage, 0, 0, 0, 0); + config = new GuildConfig(guild.Id); } - GuildConfigDictionary.Add(guild.Id, config); + config.Validate(); + + GuildConfigDictionary.Add(config.Id.GetValueOrDefault(0), config); } } + public static void ResetGuildConfig(IGuild guild) { + GuildConfigDictionary.Remove(guild.Id); + var config = new GuildConfig(guild.Id); + config.Validate(); + GuildConfigDictionary.Add(guild.Id, config); + } + public static GuildConfig GetGuildConfig(IGuild guild) { Messages.Culture = new CultureInfo("ru"); - var toReturn = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] - : new GuildConfig(guild.Id, "ru", "!", false, true, true, Messages.DefaultWelcomeMessage, 0, 0, 0, 0); + var config = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] : + new GuildConfig(guild.Id); + config.Validate(); - if (toReturn.Id != guild.Id) throw new Exception(); - return toReturn; + return config; } public static IGuild FindGuild(IMessageChannel channel) { @@ -71,4 +77,4 @@ public static class Boyfriend { throw new Exception(Messages.CouldntFindGuildByChannel); } -} \ No newline at end of file +} diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index 1563891..ada7a52 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -15,7 +15,8 @@ - + + diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs index 0d79886..b9a455c 100644 --- a/Boyfriend/CommandHandler.cs +++ b/Boyfriend/CommandHandler.cs @@ -9,14 +9,15 @@ namespace Boyfriend; public static class CommandHandler { public static readonly Command[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), - new KickCommand(), new MuteCommand() + new KickCommand(), new MuteCommand(), new PingCommand(), + new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() }; public static async Task HandleCommand(SocketUserMessage message) { var context = new SocketCommandContext(Boyfriend.Client, message); foreach (var command in Commands) { - var regex = new Regex(Regex.Escape(Boyfriend.GetGuildConfig(context.Guild).Prefix)); + var regex = new Regex(Regex.Escape(Boyfriend.GetGuildConfig(context.Guild).Prefix!)); if (!command.GetAliases().Contains(regex.Replace(message.Content, "", 1).Split()[0])) continue; var args = message.Content.Split().Skip(1).ToArray(); @@ -35,6 +36,7 @@ public static class CommandHandler { await context.Channel.SendMessageAsync($"{signature} `{e.Message}`"); if (e.StackTrace != null && e is not ApplicationException or UnauthorizedAccessException) await context.Channel.SendMessageAsync(Utils.Wrap(e.StackTrace)); + throw; } break; @@ -66,4 +68,4 @@ public static class CommandHandler { if (actor.Hierarchy <= target.Hierarchy) throw new UnauthorizedAccessException(Messages.InteractionsFailedUser); } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index d9832fc..2c3e0a9 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -28,15 +28,17 @@ public class MuteCommand : Command { toMute.TimedOutUntil != null && toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > DateTimeOffset.Now.ToUnixTimeMilliseconds()) throw new ApplicationException(Messages.MemberAlreadyMuted); - var rolesRemoved = Boyfriend.GetGuildConfig(context.Guild).RolesRemovedOnMute; + var config = Boyfriend.GetGuildConfig(context.Guild); + var rolesRemoved = config.RolesRemovedOnMute!; if (rolesRemoved.ContainsKey(toMute.Id)) { foreach (var roleId in rolesRemoved[toMute.Id]) await toMute.AddRoleAsync(roleId); rolesRemoved.Remove(toMute.Id); + await config.Save(); await Warn(context.Channel, Messages.RolesReturned); return; } - await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); + await CommandHandler.CheckPermissions(author, GuildPermission.ModerateMembers, GuildPermission.ManageRoles); await CommandHandler.CheckInteractions(author, toMute); MuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toMute, duration, reason); @@ -48,7 +50,7 @@ public class MuteCommand : Command { var authorMention = author.Mention; var role = Utils.GetMuteRole(guild); var config = Boyfriend.GetGuildConfig(guild); - if (config.RemoveRolesOnMute && role != null) { + if (config.RemoveRolesOnMute.GetValueOrDefault(false) && role != null) { var rolesRemoved = new List(); try { foreach (var roleId in toMute.RoleIds) { @@ -59,7 +61,7 @@ public class MuteCommand : Command { } catch (NullReferenceException) { } - config.RolesRemovedOnMute.Add(toMute.Id, rolesRemoved); + config.RolesRemovedOnMute!.Add(toMute.Id, rolesRemoved); await config.Save(); } @@ -89,4 +91,4 @@ public class MuteCommand : Command { public override string GetSummary() { return "Глушит участника"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/PingCommand.cs b/Boyfriend/Commands/PingCommand.cs index ce8100a..ae0c4af 100644 --- a/Boyfriend/Commands/PingCommand.cs +++ b/Boyfriend/Commands/PingCommand.cs @@ -7,7 +7,7 @@ namespace Boyfriend.Commands; public class PingCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { - await context.Channel.SendMessageAsync($"{Utils.GetBeep(Boyfriend.GetGuildConfig(context.Guild).Lang)}" + + await context.Channel.SendMessageAsync($"{Utils.GetBeep(Boyfriend.GetGuildConfig(context.Guild).Lang!)}" + $"{Boyfriend.Client.Latency}{Messages.Milliseconds}"); } @@ -22,4 +22,4 @@ public class PingCommand : Command { public override string GetSummary() { return "Измеряет время обработки REST-запроса"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 3902077..9cdfe6e 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -14,71 +14,89 @@ public class SettingsCommand : Command { var guild = context.Guild; if (args.Length == 0) { var nl = Environment.NewLine; - var adminLogChannel = guild.GetTextChannel(config.AdminLogChannel); + var adminLogChannel = guild.GetTextChannel(config.AdminLogChannel.GetValueOrDefault(0)); var admin = adminLogChannel == null ? Messages.ChannelNotSpecified : adminLogChannel.Mention; - var botLogChannel = guild.GetTextChannel(config.BotLogChannel); + var botLogChannel = guild.GetTextChannel(config.BotLogChannel.GetValueOrDefault(0)); var bot = botLogChannel == null ? Messages.ChannelNotSpecified : botLogChannel.Mention; - var muteRole = guild.GetRole(config.MuteRole); + var muteRole = guild.GetRole(config.MuteRole.GetValueOrDefault(0)); var mute = muteRole == null ? Messages.RoleNotSpecified : muteRole.Mention; - var defaultRole = guild.GetRole(config.DefaultRole); - var defaultr = muteRole == null ? Messages.RoleNotSpecified : defaultRole.Mention; + var defaultRole = guild.GetRole(config.DefaultRole.GetValueOrDefault(0)); + var defaultr = defaultRole == null ? Messages.RoleNotSpecified : defaultRole.Mention; var toSend = string.Format(Messages.CurrentSettings, nl) + string.Format(Messages.CurrentSettingsLang, config.Lang, nl) + string.Format(Messages.CurrentSettingsPrefix, config.Prefix, nl) + - string.Format(Messages.CurrentSettingsRemoveRoles, YesOrNo(config.RemoveRolesOnMute), nl) + - string.Format(Messages.CurrentSettingsUseSystemChannel, YesOrNo(config.UseSystemChannel), nl) + - string.Format(Messages.CurrentSettingsSendWelcomeMessages, YesOrNo(config.UseSystemChannel), - nl) + + string.Format(Messages.CurrentSettingsRemoveRoles, YesOrNo( + config.RemoveRolesOnMute.GetValueOrDefault(false)), nl) + + string.Format(Messages.CurrentSettingsUseSystemChannel, YesOrNo( + config.UseSystemChannel.GetValueOrDefault(true)), nl) + + string.Format(Messages.CurrentSettingsSendWelcomeMessages, YesOrNo( + config.SendWelcomeMessages.GetValueOrDefault(true)), nl) + + string.Format(Messages.CurrentSettingsReceiveStartupMessages, YesOrNo( + config.ReceiveStartupMessages.GetValueOrDefault(true)), nl) + string.Format(Messages.CurrentSettingsWelcomeMessage, config.WelcomeMessage, nl) + string.Format(Messages.CurrentSettingsDefaultRole, defaultr, nl) + string.Format(Messages.CurrentSettingsMuteRole, mute, nl) + string.Format(Messages.CurrentSettingsAdminLogChannel, admin, nl) + string.Format(Messages.CurrentSettingsBotLogChannel, bot); - await Utils.SilentSendAsync(context.Channel as ITextChannel ?? throw new Exception(), toSend); + await Utils.SilentSendAsync(context.Channel as ITextChannel ?? throw new ApplicationException(), toSend); return; } var setting = args[0].ToLower(); - var value = args[1].ToLower(); + var value = ""; + var shouldDefault = false; + if (args.Length >= 2) + value = args[1].ToLower(); + else + shouldDefault = true; - var boolValue = ParseBool(args[1]); + var boolValue = ParseBool(value); var channel = await Utils.ParseChannelNullable(value) as IGuildChannel; var role = Utils.ParseRoleNullable(guild, value); switch (setting) { + case "reset": + Boyfriend.ResetGuildConfig(guild); + break; case "lang" when value is not ("ru" or "en"): - throw new Exception(Messages.LanguageNotSupported); + throw new ApplicationException(Messages.LanguageNotSupported); case "lang": - config.Lang = value; - Messages.Culture = new CultureInfo(value); + config.Lang = shouldDefault ? "ru" : value; + Messages.Culture = new CultureInfo(shouldDefault ? "ru" : value); break; case "prefix": - config.Prefix = value; + config.Prefix = shouldDefault ? "!" : value; break; case "removerolesonmute": - config.RemoveRolesOnMute = GetBoolValue(boolValue); + config.RemoveRolesOnMute = !shouldDefault && GetBoolValue(boolValue); break; case "usesystemchannel": - config.UseSystemChannel = GetBoolValue(boolValue); + config.UseSystemChannel = shouldDefault || GetBoolValue(boolValue); break; case "sendwelcomemessages": - config.SendWelcomeMessages = GetBoolValue(boolValue); + config.SendWelcomeMessages = shouldDefault || GetBoolValue(boolValue); + break; + case "receivestartupmessages": + config.ReceiveStartupMessages = shouldDefault || GetBoolValue(boolValue); break; case "welcomemessage": - config.WelcomeMessage = value; + config.WelcomeMessage = shouldDefault ? Messages.DefaultWelcomeMessage : value; break; case "defaultrole": - config.DefaultRole = GetRoleId(role); + config.DefaultRole = shouldDefault ? 0 : GetRoleId(role); break; case "muterole": - config.MuteRole = GetRoleId(role); + config.MuteRole = shouldDefault ? 0 : GetRoleId(role); break; case "adminlogchannel": - config.AdminLogChannel = GetChannelId(channel); + config.AdminLogChannel = shouldDefault ? 0 : GetChannelId(channel); break; case "botlogchannel": - config.BotLogChannel = GetChannelId(channel); + config.BotLogChannel = shouldDefault ? 0 : GetChannelId(channel); break; + default: + await context.Channel.SendMessageAsync(Messages.SettingDoesntExist); + return; } await config.Save(); @@ -89,22 +107,21 @@ public class SettingsCommand : Command { private static bool? ParseBool(string toParse) { try { return bool.Parse(toParse.ToLower()); - } - catch (FormatException) { + } catch (FormatException) { return null; } } private static bool GetBoolValue(bool? from) { - return from ?? throw new Exception(Messages.InvalidBoolean); + return from ?? throw new ApplicationException(Messages.InvalidBoolean); } private static ulong GetRoleId(IRole? role) { - return (role ?? throw new Exception(Messages.InvalidRoleSpecified)).Id; + return (role ?? throw new ApplicationException(Messages.InvalidRoleSpecified)).Id; } private static ulong GetChannelId(IGuildChannel? channel) { - return (channel ?? throw new Exception(Messages.InvalidChannelSpecified)).Id; + return (channel ?? throw new ApplicationException(Messages.InvalidChannelSpecified)).Id; } private static string YesOrNo(bool isYes) { @@ -122,4 +139,4 @@ public class SettingsCommand : Command { public override string GetSummary() { return "Настраивает бота отдельно для этого сервера"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index c8b9fef..d2d223f 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -11,7 +11,7 @@ public class UnbanCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { var toUnban = await Utils.ParseUser(args[0]); if (context.Guild.GetBanAsync(toUnban.Id) == null) - throw new Exception(Messages.UserNotBanned); + throw new ApplicationException(Messages.UserNotBanned); UnbanUser(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toUnban, Utils.JoinString(args, 1)); } @@ -40,4 +40,4 @@ public class UnbanCommand : Command { public override string GetSummary() { return "Возвращает пользователя из бана"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index c416807..75eec78 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -14,14 +14,21 @@ public class UnmuteCommand : Command { await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); await CommandHandler.CheckInteractions(author, toUnmute); var role = Utils.GetMuteRole(context.Guild); - if (role != null) + if (role != null) { if (toUnmute.RoleIds.All(x => x != role.Id)) { - var rolesRemoved = Boyfriend.GetGuildConfig(context.Guild).RolesRemovedOnMute; + var config = Boyfriend.GetGuildConfig(context.Guild); + var rolesRemoved = config.RolesRemovedOnMute; - foreach (var roleId in rolesRemoved[toUnmute.Id]) await toUnmute.AddRoleAsync(roleId); + foreach (var roleId in rolesRemoved![toUnmute.Id]) await toUnmute.AddRoleAsync(roleId); rolesRemoved.Remove(toUnmute.Id); + await config.Save(); throw new ApplicationException(Messages.RolesReturned); } + } + if (role != null && toUnmute.RoleIds.All(x => x != role.Id) || + toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() + < DateTimeOffset.Now.ToUnixTimeMilliseconds()) + throw new ApplicationException(Messages.MemberNotMuted); UnmuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toUnmute, Utils.JoinString(args, 1)); @@ -33,12 +40,19 @@ public class UnmuteCommand : Command { var authorMention = author.Mention; var notification = string.Format(Messages.MemberUnmuted, authorMention, toUnmute.Mention, Utils.WrapInline(reason)); - await toUnmute.RemoveRoleAsync(Utils.GetMuteRole(guild)); - var config = Boyfriend.GetGuildConfig(guild); + var role = Utils.GetMuteRole(guild); - if (config.RolesRemovedOnMute.ContainsKey(toUnmute.Id)) { - foreach (var roleId in config.RolesRemovedOnMute[toUnmute.Id]) await toUnmute.AddRoleAsync(roleId); - config.RolesRemovedOnMute.Remove(toUnmute.Id); + if (role != null) { + await toUnmute.RemoveRoleAsync(role); + var config = Boyfriend.GetGuildConfig(guild); + + if (config.RolesRemovedOnMute!.ContainsKey(toUnmute.Id)) { + foreach (var roleId in config.RolesRemovedOnMute[toUnmute.Id]) await toUnmute.AddRoleAsync(roleId); + config.RolesRemovedOnMute.Remove(toUnmute.Id); + await config.Save(); + } + } else { + await toUnmute.RemoveTimeOutAsync(); } await Utils.SilentSendAsync(channel, string.Format(Messages.UnmuteResponse, toUnmute.Mention, @@ -58,4 +72,4 @@ public class UnmuteCommand : Command { public override string GetSummary() { return "Снимает мут с участника"; } -} \ No newline at end of file +} diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index fdae9d2..3ca9897 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -23,10 +23,10 @@ public class EventHandler { var i = new Random().Next(3); foreach (var guild in Boyfriend.Client.Guilds) { var config = Boyfriend.GetGuildConfig(guild); - Messages.Culture = new CultureInfo(config.Lang); - var channel = guild.GetTextChannel(config.BotLogChannel); - if (channel == null) continue; - await channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(config.Lang, i))); + Messages.Culture = new CultureInfo(config.Lang!); + var channel = guild.GetTextChannel(config.BotLogChannel.GetValueOrDefault(0)); + if (!config.ReceiveStartupMessages.GetValueOrDefault(true) || channel == null) continue; + await channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(config.Lang!, i))); } } @@ -48,7 +48,7 @@ public class EventHandler { var argPos = 0; var guildConfig = Boyfriend.GetGuildConfig(guild); - Messages.Culture = new CultureInfo(guildConfig.Lang); + Messages.Culture = new CultureInfo(guildConfig.Lang!); if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && !user.GuildPermissions.MentionEveryone) await BanCommand.BanUser(guild, null, await guild.GetCurrentUserAsync(), user, @@ -67,7 +67,7 @@ public class EventHandler { if (!(message.HasStringPrefix(guildConfig.Prefix, ref argPos) || message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) || user == await guild.GetCurrentUserAsync() - || user.IsBot && message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)) + || user.IsBot && (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe))) return; await CommandHandler.HandleCommand(message); @@ -91,10 +91,10 @@ public class EventHandler { private static async Task UserJoinedEvent(SocketGuildUser user) { var guild = user.Guild; var config = Boyfriend.GetGuildConfig(guild); - if (config.SendWelcomeMessages) - await Utils.SilentSendAsync(guild.SystemChannel, string.Format(config.WelcomeMessage, user.Mention, + if (config.SendWelcomeMessages.GetValueOrDefault(true)) + await Utils.SilentSendAsync(guild.SystemChannel, string.Format(config.WelcomeMessage!, user.Mention, guild.Name)); if (config.DefaultRole != 0) - await user.AddRoleAsync(Utils.ParseRole(guild, config.DefaultRole.ToString())); + await user.AddRoleAsync(Utils.ParseRole(guild, config.DefaultRole.ToString()!)); } -} \ No newline at end of file +} diff --git a/Boyfriend/GuildConfig.cs b/Boyfriend/GuildConfig.cs index 1238579..6b45aa7 100644 --- a/Boyfriend/GuildConfig.cs +++ b/Boyfriend/GuildConfig.cs @@ -1,40 +1,48 @@ -using System.Text.Json; +using System.Globalization; +using Newtonsoft.Json; namespace Boyfriend; public class GuildConfig { - public ulong Id { get; } - public string Lang { get; set; } - public string Prefix { get; set; } - public bool RemoveRolesOnMute { get; set; } - public bool UseSystemChannel { get; set; } - public bool SendWelcomeMessages { get; set; } - public string WelcomeMessage { get; set; } - public ulong DefaultRole { get; set; } - public ulong MuteRole { get; set; } - public ulong AdminLogChannel { get; set; } - public ulong BotLogChannel { get; set; } - public Dictionary> RolesRemovedOnMute { get; set; } + public ulong? Id { get; } + public string? Lang { get; set; } + public string? Prefix { get; set; } + public bool? RemoveRolesOnMute { get; set; } + public bool? UseSystemChannel { get; set; } + public bool? SendWelcomeMessages { get; set; } + public bool? ReceiveStartupMessages { get; set; } + public string? WelcomeMessage { get; set; } + public ulong? DefaultRole { get; set; } + public ulong? MuteRole { get; set; } + public ulong? AdminLogChannel { get; set; } + public ulong? BotLogChannel { get; set; } + public Dictionary>? RolesRemovedOnMute { get; private set; } - public GuildConfig(ulong id, string lang, string prefix, bool removeRolesOnMute, bool useSystemChannel, - bool sendWelcomeMessages, string welcomeMessage, ulong defaultRole, ulong muteRole, ulong adminLogChannel, - ulong botLogChannel) { + public GuildConfig(ulong id) { Id = id; - Lang = lang; - Prefix = prefix; - RemoveRolesOnMute = removeRolesOnMute; - UseSystemChannel = useSystemChannel; - SendWelcomeMessages = sendWelcomeMessages; - WelcomeMessage = welcomeMessage; - DefaultRole = defaultRole; - MuteRole = muteRole; - AdminLogChannel = adminLogChannel; - BotLogChannel = botLogChannel; - RolesRemovedOnMute = new Dictionary>(); + Validate(); + } + + public void Validate() { + if (Id == null) throw new Exception("Something went horribly, horribly wrong"); + Lang ??= "ru"; + Messages.Culture = new CultureInfo(Lang); + Prefix ??= "!"; + RemoveRolesOnMute ??= false; + UseSystemChannel ??= true; + SendWelcomeMessages ??= true; + ReceiveStartupMessages ??= true; + WelcomeMessage ??= Messages.DefaultWelcomeMessage; + DefaultRole ??= 0; + MuteRole ??= 0; + AdminLogChannel ??= 0; + BotLogChannel ??= 0; + RolesRemovedOnMute ??= new Dictionary>(); } public async Task Save() { - await using var stream = File.OpenWrite("config_" + Id + ".json"); - await JsonSerializer.SerializeAsync(stream, this); + Validate(); + RolesRemovedOnMute!.TrimExcess(); + await File.WriteAllTextAsync("config_" + Id + ".json", JsonConvert.SerializeObject(this)); } -} \ No newline at end of file +} diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 6538678..5e7fd44 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -11,32 +12,46 @@ namespace Boyfriend { using System; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -45,442 +60,679 @@ namespace Boyfriend { } } - internal static string CouldntFindGuildByChannel { - get { - return ResourceManager.GetString("CouldntFindGuildByChannel", resourceCulture); - } - } - - internal static string Ready { - get { - return ResourceManager.GetString("Ready", resourceCulture); - } - } - - internal static string UncachedMessageDeleted { - get { - return ResourceManager.GetString("UncachedMessageDeleted", resourceCulture); - } - } - - internal static string CachedMessageDeleted { - get { - return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); - } - } - - internal static string AutobanReason { - get { - return ResourceManager.GetString("AutobanReason", resourceCulture); - } - } - - internal static string UncachedMessageEdited { - get { - return ResourceManager.GetString("UncachedMessageEdited", resourceCulture); - } - } - - internal static string CachedMessageEdited { - get { - return ResourceManager.GetString("CachedMessageEdited", resourceCulture); - } - } - - internal static string DefaultWelcomeMessage { - get { - return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); - } - } - - internal static string Beep1 { - get { - return ResourceManager.GetString("Beep1", resourceCulture); - } - } - - internal static string Beep2 { - get { - return ResourceManager.GetString("Beep2", resourceCulture); - } - } - - internal static string Beep3 { - get { - return ResourceManager.GetString("Beep3", resourceCulture); - } - } - - internal static string InvalidAdminLogChannel { - get { - return ResourceManager.GetString("InvalidAdminLogChannel", resourceCulture); - } - } - - internal static string MuteRoleRequired { - get { - return ResourceManager.GetString("MuteRoleRequired", resourceCulture); - } - } - - internal static string CommandExecutionUnsuccessful { - get { - return ResourceManager.GetString("CommandExecutionUnsuccessful", resourceCulture); - } - } - - internal static string RepeatedArgumentsDetected { - get { - return ResourceManager.GetString("RepeatedArgumentsDetected", resourceCulture); - } - } - - internal static string CommandParseFailed { - get { - return ResourceManager.GetString("CommandParseFailed", resourceCulture); - } - } - - internal static string UnknownCommand { - get { - return ResourceManager.GetString("UnknownCommand", resourceCulture); - } - } - - internal static string BadArgumentCount { - get { - return ResourceManager.GetString("BadArgumentCount", resourceCulture); - } - } - + /// + /// Looks up a localized string similar to Arguments not present! {0}. + /// internal static string ArgumentNotPresent { get { return ResourceManager.GetString("ArgumentNotPresent", resourceCulture); } } - internal static string CommandNoPermissionBot { + /// + /// Looks up a localized string similar to Too many mentions in 1 message. + /// + internal static string AutobanReason { get { - return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); + return ResourceManager.GetString("AutobanReason", resourceCulture); } } - internal static string CommandNoPermissionUser { + /// + /// Looks up a localized string similar to Invalid argument count! {0}. + /// + internal static string BadArgumentCount { get { - return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); - } - } - - internal static string InteractionsDifferentGuilds { - get { - return ResourceManager.GetString("InteractionsDifferentGuilds", resourceCulture); - } - } - - internal static string InteractionsOwner { - get { - return ResourceManager.GetString("InteractionsOwner", resourceCulture); - } - } - - internal static string InteractionsYourself { - get { - return ResourceManager.GetString("InteractionsYourself", resourceCulture); - } - } - - internal static string InteractionsMe { - get { - return ResourceManager.GetString("InteractionsMe", resourceCulture); - } - } - - internal static string InteractionsFailedUser { - get { - return ResourceManager.GetString("InteractionsFailedUser", resourceCulture); - } - } - - internal static string InteractionsFailedBot { - get { - return ResourceManager.GetString("InteractionsFailedBot", resourceCulture); - } - } - - internal static string YouWereBanned { - get { - return ResourceManager.GetString("YouWereBanned", resourceCulture); - } - } - - internal static string UserBanned { - get { - return ResourceManager.GetString("UserBanned", resourceCulture); - } - } - - internal static string PunishmentExpired { - get { - return ResourceManager.GetString("PunishmentExpired", resourceCulture); - } - } - - internal static string ClearNegativeAmount { - get { - return ResourceManager.GetString("ClearNegativeAmount", resourceCulture); - } - } - - internal static string ClearAmountTooLarge { - get { - return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); - } - } - - internal static string MessagesDeleted { - get { - return ResourceManager.GetString("MessagesDeleted", resourceCulture); - } - } - - internal static string CommandHelp { - get { - return ResourceManager.GetString("CommandHelp", resourceCulture); - } - } - - internal static string YouWereKicked { - get { - return ResourceManager.GetString("YouWereKicked", resourceCulture); - } - } - - internal static string MemberKicked { - get { - return ResourceManager.GetString("MemberKicked", resourceCulture); - } - } - - internal static string MemberMuted { - get { - return ResourceManager.GetString("MemberMuted", resourceCulture); - } - } - - internal static string Milliseconds { - get { - return ResourceManager.GetString("Milliseconds", resourceCulture); - } - } - - internal static string MemberAlreadyMuted { - get { - return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - - internal static string MuteRoleManuallyRemoved { - get { - return ResourceManager.GetString("MuteRoleManuallyRemoved", resourceCulture); - } - } - - internal static string ChannelNotSpecified { - get { - return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); - } - } - - internal static string RoleNotSpecified { - get { - return ResourceManager.GetString("RoleNotSpecified", resourceCulture); - } - } - - internal static string CurrentSettings { - get { - return ResourceManager.GetString("CurrentSettings", resourceCulture); - } - } - - internal static string CurrentSettingsLang { - get { - return ResourceManager.GetString("CurrentSettingsLang", resourceCulture); - } - } - - internal static string CurrentSettingsPrefix { - get { - return ResourceManager.GetString("CurrentSettingsPrefix", resourceCulture); - } - } - - internal static string CurrentSettingsRemoveRoles { - get { - return ResourceManager.GetString("CurrentSettingsRemoveRoles", resourceCulture); - } - } - - internal static string CurrentSettingsUseSystemChannel { - get { - return ResourceManager.GetString("CurrentSettingsUseSystemChannel", resourceCulture); - } - } - - internal static string CurrentSettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("CurrentSettingsSendWelcomeMessages", resourceCulture); - } - } - - internal static string CurrentSettingsDefaultRole { - get { - return ResourceManager.GetString("CurrentSettingsDefaultRole", resourceCulture); - } - } - - internal static string CurrentSettingsMuteRole { - get { - return ResourceManager.GetString("CurrentSettingsMuteRole", resourceCulture); - } - } - - internal static string CurrentSettingsAdminLogChannel { - get { - return ResourceManager.GetString("CurrentSettingsAdminLogChannel", resourceCulture); - } - } - - internal static string CurrentSettingsBotLogChannel { - get { - return ResourceManager.GetString("CurrentSettingsBotLogChannel", resourceCulture); - } - } - - internal static string LanguageNotSupported { - get { - return ResourceManager.GetString("LanguageNotSupported", resourceCulture); - } - } - - internal static string SettingsUpdated { - get { - return ResourceManager.GetString("SettingsUpdated", resourceCulture); - } - } - - internal static string InvalidBoolean { - get { - return ResourceManager.GetString("InvalidBoolean", resourceCulture); - } - } - - internal static string InvalidRoleSpecified { - get { - return ResourceManager.GetString("InvalidRoleSpecified", resourceCulture); - } - } - - internal static string InvalidChannelSpecified { - get { - return ResourceManager.GetString("InvalidChannelSpecified", resourceCulture); - } - } - - internal static string Yes { - get { - return ResourceManager.GetString("Yes", resourceCulture); - } - } - - internal static string No { - get { - return ResourceManager.GetString("No", resourceCulture); - } - } - - internal static string UserNotBanned { - get { - return ResourceManager.GetString("UserNotBanned", resourceCulture); - } - } - - internal static string MemberNotMuted { - get { - return ResourceManager.GetString("MemberNotMuted", resourceCulture); - } - } - - internal static string RolesReturned { - get { - return ResourceManager.GetString("RolesReturned", resourceCulture); - } - } - - internal static string MemberUnmuted { - get { - return ResourceManager.GetString("MemberUnmuted", resourceCulture); - } - } - - internal static string UserUnbanned { - get { - return ResourceManager.GetString("UserUnbanned", resourceCulture); - } - } - - internal static string CurrentSettingsWelcomeMessage { - get { - return ResourceManager.GetString("CurrentSettingsWelcomeMessage", resourceCulture); - } - } - - internal static string NotEnoughArguments { - get { - return ResourceManager.GetString("NotEnoughArguments", resourceCulture); - } - } - - internal static string ClearInvalidAmountSpecified { - get { - return ResourceManager.GetString("ClearInvalidAmountSpecified", resourceCulture); + return ResourceManager.GetString("BadArgumentCount", resourceCulture); } } + /// + /// Looks up a localized string similar to :white_check_mark: Successfully banned {0} for {1}. + /// internal static string BanResponse { get { return ResourceManager.GetString("BanResponse", resourceCulture); } } + /// + /// Looks up a localized string similar to Bah! . + /// + internal static string Beep1 { + get { + return ResourceManager.GetString("Beep1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bop! . + /// + internal static string Beep2 { + get { + return ResourceManager.GetString("Beep2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Beep! . + /// + internal static string Beep3 { + get { + return ResourceManager.GetString("Beep3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleted message from {0} in channel . + /// + internal static string CachedMessageDeleted { + get { + return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message edited from {0} in channel {1}.{2}Before:{3}{4}{5}After:{6}{7}. + /// + internal static string CachedMessageEdited { + get { + return ResourceManager.GetString("CachedMessageEdited", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string ChannelNotSpecified { + get { + return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Too many messages specified!. + /// + internal static string ClearAmountTooLarge { + get { + return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid message amount specified!. + /// + internal static string ClearInvalidAmountSpecified { + get { + return ResourceManager.GetString("ClearInvalidAmountSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Negative message amount specified!. + /// + internal static string ClearNegativeAmount { + get { + return ResourceManager.GetString("ClearNegativeAmount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command execution was unsuccessful: {0}. + /// + internal static string CommandExecutionUnsuccessful { + get { + return ResourceManager.GetString("CommandExecutionUnsuccessful", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command help:{0}. + /// + internal static string CommandHelp { + get { + return ResourceManager.GetString("CommandHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I do not have permission to execute this command!. + /// + internal static string CommandNoPermissionBot { + get { + return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You do not have permission to execute this command!. + /// + internal static string CommandNoPermissionUser { + get { + return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command parsing failed: {0}. + /// + internal static string CommandParseFailed { + get { + return ResourceManager.GetString("CommandParseFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find guild by message!. + /// + internal static string CouldntFindGuildByChannel { + get { + return ResourceManager.GetString("CouldntFindGuildByChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current settings:{0}. + /// + internal static string CurrentSettings { + get { + return ResourceManager.GetString("CurrentSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Admin log channel (`adminLogChannel`): {0}{1}. + /// + internal static string CurrentSettingsAdminLogChannel { + get { + return ResourceManager.GetString("CurrentSettingsAdminLogChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bot log channel (`botLogChannel`): {0}. + /// + internal static string CurrentSettingsBotLogChannel { + get { + return ResourceManager.GetString("CurrentSettingsBotLogChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default role (`defaultRole`): {0}{1}. + /// + internal static string CurrentSettingsDefaultRole { + get { + return ResourceManager.GetString("CurrentSettingsDefaultRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language (`lang`): `{0}`{1}. + /// + internal static string CurrentSettingsLang { + get { + return ResourceManager.GetString("CurrentSettingsLang", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mute role (`muteRole`): {0}{1}. + /// + internal static string CurrentSettingsMuteRole { + get { + return ResourceManager.GetString("CurrentSettingsMuteRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix (`prefix`): `{0}`{1}. + /// + internal static string CurrentSettingsPrefix { + get { + return ResourceManager.GetString("CurrentSettingsPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Receive startup messages (`receiveStartupMessages`): {0}{1}. + /// + internal static string CurrentSettingsReceiveStartupMessages { + get { + return ResourceManager.GetString("CurrentSettingsReceiveStartupMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove roles on mute (`removeRolesOnMute`): {0}{1}. + /// + internal static string CurrentSettingsRemoveRoles { + get { + return ResourceManager.GetString("CurrentSettingsRemoveRoles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send welcome messages (`sendWelcomeMessages`): {0}{1}. + /// + internal static string CurrentSettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("CurrentSettingsSendWelcomeMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use system channel for notifications (`useSystemChannel`): {0}{1}. + /// + internal static string CurrentSettingsUseSystemChannel { + get { + return ResourceManager.GetString("CurrentSettingsUseSystemChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome message: `{0}`{1}. + /// + internal static string CurrentSettingsWelcomeMessage { + get { + return ResourceManager.GetString("CurrentSettingsWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}, welcome to {1}. + /// + internal static string DefaultWelcomeMessage { + get { + return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Members are in different guilds!. + /// + internal static string InteractionsDifferentGuilds { + get { + return ResourceManager.GetString("InteractionsDifferentGuilds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot interact with this member!. + /// + internal static string InteractionsFailedBot { + get { + return ResourceManager.GetString("InteractionsFailedBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot interact with this member!. + /// + internal static string InteractionsFailedUser { + get { + return ResourceManager.GetString("InteractionsFailedUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot interact with me!. + /// + internal static string InteractionsMe { + get { + return ResourceManager.GetString("InteractionsMe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot interact with guild owner!. + /// + internal static string InteractionsOwner { + get { + return ResourceManager.GetString("InteractionsOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot interact with yourself!. + /// + internal static string InteractionsYourself { + get { + return ResourceManager.GetString("InteractionsYourself", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid admin log channel for guild. + /// + internal static string InvalidAdminLogChannel { + get { + return ResourceManager.GetString("InvalidAdminLogChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid argument! 'true' or 'false' required!. + /// + internal static string InvalidBoolean { + get { + return ResourceManager.GetString("InvalidBoolean", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid channel specified!. + /// + internal static string InvalidChannelSpecified { + get { + return ResourceManager.GetString("InvalidChannelSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid role specified!. + /// + internal static string InvalidRoleSpecified { + get { + return ResourceManager.GetString("InvalidRoleSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to :white_check_mark: Successfully kicked {0} for {1}. + /// internal static string KickResponse { get { return ResourceManager.GetString("KickResponse", resourceCulture); } } - internal static string UserNotInGuild { + /// + /// Looks up a localized string similar to Language not supported!. + /// + internal static string LanguageNotSupported { get { - return ResourceManager.GetString("UserNotInGuild", resourceCulture); + return ResourceManager.GetString("LanguageNotSupported", resourceCulture); } } + /// + /// Looks up a localized string similar to Member is already muted!. + /// + internal static string MemberAlreadyMuted { + get { + return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} kicked {1} for {2}. + /// + internal static string MemberKicked { + get { + return ResourceManager.GetString("MemberKicked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} muted {1} for {2}. + /// + internal static string MemberMuted { + get { + return ResourceManager.GetString("MemberMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member not muted!. + /// + internal static string MemberNotMuted { + get { + return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} unmuted {1} for {2}. + /// + internal static string MemberUnmuted { + get { + return ResourceManager.GetString("MemberUnmuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} deleted {1} messages in channel {2}. + /// + internal static string MessagesDeleted { + get { + return ResourceManager.GetString("MessagesDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ms. + /// + internal static string Milliseconds { + get { + return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to :white_check_mark: Successfully muted {0} for {1}. + /// internal static string MuteResponse { get { return ResourceManager.GetString("MuteResponse", resourceCulture); } } + /// + /// Looks up a localized string similar to Someone removed the mute role manually!. + /// + internal static string MuteRoleManuallyRemoved { + get { + return ResourceManager.GetString("MuteRoleManuallyRemoved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You must set up a mute role in settings!. + /// + internal static string MuteRoleRequired { + get { + return ResourceManager.GetString("MuteRoleRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No. + /// + internal static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not enough arguments! Needed: {0}, provided: {1}. + /// + internal static string NotEnoughArguments { + get { + return ResourceManager.GetString("NotEnoughArguments", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Punishment expired. + /// + internal static string PunishmentExpired { + get { + return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}I'm ready! (C#). + /// + internal static string Ready { + get { + return ResourceManager.GetString("Ready", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Repeated arguments detected! {0}. + /// + internal static string RepeatedArgumentsDetected { + get { + return ResourceManager.GetString("RepeatedArgumentsDetected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string RoleNotSpecified { + get { + return ResourceManager.GetString("RoleNotSpecified", 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!. + /// + internal static string SettingDoesntExist { + get { + return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings successfully updated. + /// + internal static string SettingsUpdated { + get { + return ResourceManager.GetString("SettingsUpdated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to :white_check_mark: Successfully unbanned {0} for {1}. + /// internal static string UnbanResponse { get { return ResourceManager.GetString("UnbanResponse", resourceCulture); } } + /// + /// Looks up a localized string similar to Deleted message in {0}, but I forgot what was there. + /// + internal static string UncachedMessageDeleted { + get { + return ResourceManager.GetString("UncachedMessageDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message edited from {0} in channel {1}, but I forgot what was there before the edit. + /// + internal static string UncachedMessageEdited { + get { + return ResourceManager.GetString("UncachedMessageEdited", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown command! {0}. + /// + internal static string UnknownCommand { + get { + return ResourceManager.GetString("UnknownCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to :white_check_mark: Successfully unmuted {0} for {1}. + /// internal static string UnmuteResponse { get { return ResourceManager.GetString("UnmuteResponse", resourceCulture); } } + + /// + /// Looks up a localized string similar to {0} banned {1} for {2}. + /// + internal static string UserBanned { + get { + return ResourceManager.GetString("UserBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User not banned!. + /// + internal static string UserNotBanned { + get { + return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified user is not a member of this server!. + /// + internal static string UserNotInGuild { + get { + return ResourceManager.GetString("UserNotInGuild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} unbanned {1} for {2}. + /// + internal static string UserUnbanned { + get { + return ResourceManager.GetString("UserUnbanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yes. + /// + internal static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. + /// + internal static string YouWereBanned { + get { + return ResourceManager.GetString("YouWereBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. + /// + internal static string YouWereKicked { + get { + return ResourceManager.GetString("YouWereKicked", resourceCulture); + } + } } } diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index bc46f3c..75ee336 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -243,4 +243,10 @@ :white_check_mark: Successfully unmuted {0} for {1} + + That setting doesn't exist! + + + Receive startup messages (`receiveStartupMessages`): {0}{1} + \ No newline at end of file diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 534cca1..2790d0a 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -234,4 +234,10 @@ :white_check_mark: Успешно возвращён из мута {0} за {1} + + Такая настройка не существует! + + + Получать сообщения о запуске (`receiveStartupMessages`): {0}{1} + \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index b6b981f..eedeaf7 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -21,7 +21,7 @@ public static class Utils { } public static async Task GetAdminLogChannel(IGuild guild) { - var adminLogChannel = await ParseChannelNullable(Boyfriend.GetGuildConfig(guild).AdminLogChannel.ToString()); + var adminLogChannel = await ParseChannelNullable(Boyfriend.GetGuildConfig(guild).AdminLogChannel.ToString()!); return adminLogChannel as ITextChannel; } @@ -111,4 +111,4 @@ public static class Utils { public static string JoinString(string[] args, int startIndex) { return string.Join(" ", args, startIndex, args.Length - startIndex); } -} \ No newline at end of file +} From 04facc3de25563c06d1a9fcca38c60f64c449096 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Wed, 2 Feb 2022 18:14:26 +0500 Subject: [PATCH 008/329] general code refactor and bug fixes --- Boyfriend/Boyfriend.cs | 7 ++- Boyfriend/Boyfriend.csproj | 2 +- Boyfriend/Boyfriend.csproj.DotSettings | 5 -- Boyfriend/CommandHandler.cs | 34 ++++++++----- Boyfriend/Commands/BanCommand.cs | 39 ++++++++------ Boyfriend/Commands/ClearCommand.cs | 17 ++++--- Boyfriend/Commands/HelpCommand.cs | 6 +-- Boyfriend/Commands/KickCommand.cs | 21 ++++---- Boyfriend/Commands/MuteCommand.cs | 70 +++++++++++++++----------- Boyfriend/Commands/SettingsCommand.cs | 7 ++- Boyfriend/Commands/UnbanCommand.cs | 21 +++++--- Boyfriend/Commands/UnmuteCommand.cs | 50 +++++++++--------- Boyfriend/EventHandler.cs | 23 ++++++--- Boyfriend/GuildConfig.cs | 6 +++ Boyfriend/Utils.cs | 21 +++++--- 15 files changed, 197 insertions(+), 132 deletions(-) delete mode 100644 Boyfriend/Boyfriend.csproj.DotSettings diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 7d3d94d..1793c3e 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -21,9 +21,10 @@ public static class Boyfriend { } private static async Task Init() { - Client.Log += Log; var token = (await File.ReadAllTextAsync("token.txt")).Trim(); + Client.Log += Log; + await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); await Client.SetActivityAsync(new Game("Retrospecter - Expurgation", ActivityType.Listening)); @@ -35,6 +36,7 @@ public static class Boyfriend { private static Task Log(LogMessage msg) { Console.WriteLine(msg.ToString()); + return Task.CompletedTask; } @@ -56,13 +58,16 @@ public static class Boyfriend { public static void ResetGuildConfig(IGuild guild) { GuildConfigDictionary.Remove(guild.Id); + var config = new GuildConfig(guild.Id); config.Validate(); + GuildConfigDictionary.Add(guild.Id, config); } public static GuildConfig GetGuildConfig(IGuild guild) { Messages.Culture = new CultureInfo("ru"); + var config = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] : new GuildConfig(guild.Id); config.Validate(); diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index ada7a52..eaa313b 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -15,7 +15,7 @@ - + diff --git a/Boyfriend/Boyfriend.csproj.DotSettings b/Boyfriend/Boyfriend.csproj.DotSettings deleted file mode 100644 index b5da49f..0000000 --- a/Boyfriend/Boyfriend.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - No - - - \ No newline at end of file diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs index b9a455c..610b69a 100644 --- a/Boyfriend/CommandHandler.cs +++ b/Boyfriend/CommandHandler.cs @@ -18,7 +18,8 @@ public static class CommandHandler { foreach (var command in Commands) { var regex = new Regex(Regex.Escape(Boyfriend.GetGuildConfig(context.Guild).Prefix!)); - if (!command.GetAliases().Contains(regex.Replace(message.Content, "", 1).Split()[0])) continue; + if (!command.GetAliases().Contains(regex.Replace(message.Content, "", 1).Split()[0])) + continue; var args = message.Content.Split().Skip(1).ToArray(); try { @@ -26,16 +27,17 @@ public static class CommandHandler { throw new ApplicationException(string.Format(Messages.NotEnoughArguments, command.GetArgumentsAmountRequired(), args.Length)); await command.Run(context, args); - } - catch (Exception e) { + } catch (Exception e) { var signature = e switch { ApplicationException => ":x:", UnauthorizedAccessException => ":no_entry_sign:", _ => ":stop_sign:" }; - await context.Channel.SendMessageAsync($"{signature} `{e.Message}`"); - if (e.StackTrace != null && e is not ApplicationException or UnauthorizedAccessException) - await context.Channel.SendMessageAsync(Utils.Wrap(e.StackTrace)); + var stacktrace = e.StackTrace; + var toSend = $"{signature} `{e.Message}`"; + if (stacktrace != null && e is not ApplicationException && e is not UnauthorizedAccessException) + toSend += $"{Environment.NewLine}{Utils.Wrap(stacktrace)}"; + await context.Channel.SendMessageAsync(toSend); throw; } @@ -45,18 +47,26 @@ public static class CommandHandler { public static async Task CheckPermissions(IGuildUser user, GuildPermission toCheck, GuildPermission forBot = GuildPermission.StartEmbeddedActivities) { + var me = await user.Guild.GetCurrentUserAsync(); if (forBot == GuildPermission.StartEmbeddedActivities) forBot = toCheck; - if (!(await user.Guild.GetCurrentUserAsync()).GuildPermissions.Has(forBot)) - throw new UnauthorizedAccessException(Messages.CommandNoPermissionBot); - if (!user.GuildPermissions.Has(toCheck)) - throw new UnauthorizedAccessException(Messages.CommandNoPermissionUser); + + if (user.Id != user.Guild.OwnerId + && (!me.GuildPermissions.Has(GuildPermission.Administrator) + || !user.GuildPermissions.Has(GuildPermission.Administrator))) { + if (!me.GuildPermissions.Has(forBot)) + throw new UnauthorizedAccessException(Messages.CommandNoPermissionBot); + if (!user.GuildPermissions.Has(toCheck)) + throw new UnauthorizedAccessException(Messages.CommandNoPermissionUser); + } } public static async Task CheckInteractions(IGuildUser actor, IGuildUser target) { - if (actor.Guild != target.Guild) - throw new UnauthorizedAccessException(Messages.InteractionsDifferentGuilds); var me = await target.Guild.GetCurrentUserAsync(); + + if (actor.Guild != target.Guild) + throw new Exception(Messages.InteractionsDifferentGuilds); if (actor.Id == actor.Guild.OwnerId) return; + if (target.Id == target.Guild.OwnerId) throw new UnauthorizedAccessException(Messages.InteractionsOwner); if (actor == target) diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index dcd5a7b..145316a 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -9,42 +9,49 @@ namespace Boyfriend.Commands; public class BanCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { - var toBan = await Utils.ParseUser(args[0]); var reason = Utils.JoinString(args, 1); + TimeSpan duration; try { duration = Utils.GetTimeSpan(args[1]); reason = Utils.JoinString(args, 2); - } - catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { + } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { duration = TimeSpan.FromMilliseconds(-1); } - var author = context.Guild.GetUser(context.User.Id); - - await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); - var memberToBan = context.Guild.GetUser(toBan.Id); - if (memberToBan != null) - await CommandHandler.CheckInteractions(author, memberToBan); await BanUser(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), - toBan, duration, reason); + await Utils.ParseUser(args[0]), duration, reason); } public static async Task BanUser(IGuild guild, ITextChannel? channel, IGuildUser author, IUser toBan, TimeSpan duration, string reason) { var authorMention = author.Mention; + var guildBanMessage = $"({Utils.GetNameAndDiscrim(author)}) {reason}"; + var memberToBan = await guild.GetUserAsync(toBan.Id); + var notification = string.Format(Messages.UserBanned, authorMention, toBan.Mention, Utils.WrapInline(reason)); + + await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); + if (memberToBan != null) + await CommandHandler.CheckInteractions(author, memberToBan); + await Utils.SendDirectMessage(toBan, string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.WrapInline(reason))); - var guildBanMessage = $"({author.Username}#{author.Discriminator}) {reason}"; + await guild.AddBanAsync(toBan, 0, guildBanMessage); - var notification = string.Format(Messages.UserBanned, authorMention, toBan.Mention, Utils.WrapInline(reason)); + await Utils.SilentSendAsync(channel, string.Format(Messages.BanResponse, toBan.Mention, Utils.WrapInline(reason))); await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - var task = new Task(() => UnbanCommand.UnbanUser(guild, null, guild.GetCurrentUserAsync().Result, toBan, - Messages.PunishmentExpired)); - await Utils.StartDelayed(task, duration, () => guild.GetBanAsync(toBan).Result != null); + + async void UnbanWhenExpires() { + try { + await UnbanCommand.UnbanUser(guild, null, await guild.GetCurrentUserAsync(), toBan, + Messages.PunishmentExpired); + } catch (ApplicationException) {} + } + + await Utils.StartDelayed(new Task(UnbanWhenExpires), duration); } public override List GetAliases() { @@ -58,4 +65,4 @@ public class BanCommand : Command { public override string GetSummary() { return "Банит пользователя"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs index fee031e..473d8d6 100644 --- a/Boyfriend/Commands/ClearCommand.cs +++ b/Boyfriend/Commands/ClearCommand.cs @@ -9,16 +9,19 @@ namespace Boyfriend.Commands; public class ClearCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { + var user = context.User; + int toDelete; try { toDelete = Convert.ToInt32(args[0]); - } - catch (Exception e) when (e is FormatException or OverflowException) { + } catch (Exception e) when (e is FormatException or OverflowException) { throw new ApplicationException(Messages.ClearInvalidAmountSpecified); } if (context.Channel is not ITextChannel channel) return; - await CommandHandler.CheckPermissions(context.Guild.GetUser(context.User.Id), GuildPermission.ManageMessages); + + await CommandHandler.CheckPermissions(context.Guild.GetUser(user.Id), GuildPermission.ManageMessages); + switch (toDelete) { case < 1: throw new ApplicationException(Messages.ClearNegativeAmount); @@ -26,9 +29,11 @@ public class ClearCommand : Command { throw new ApplicationException(Messages.ClearAmountTooLarge); default: { var messages = await channel.GetMessagesAsync(toDelete + 1).FlattenAsync(); - await channel.DeleteMessagesAsync(messages); + + await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(Utils.GetNameAndDiscrim(user))); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(context.Guild), - string.Format(Messages.MessagesDeleted, context.User.Mention, toDelete + 1, + string.Format(Messages.MessagesDeleted, user.Mention, toDelete + 1, Utils.MentionChannel(context.Channel.Id))); break; } @@ -46,4 +51,4 @@ public class ClearCommand : Command { public override string GetSummary() { return "Очищает сообщения"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/HelpCommand.cs b/Boyfriend/Commands/HelpCommand.cs index b3eeed1..50b9818 100644 --- a/Boyfriend/Commands/HelpCommand.cs +++ b/Boyfriend/Commands/HelpCommand.cs @@ -8,11 +8,11 @@ namespace Boyfriend.Commands; public class HelpCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { var nl = Environment.NewLine; - var toSend = string.Format(Messages.CommandHelp, nl); var prefix = Boyfriend.GetGuildConfig(context.Guild).Prefix; + var toSend = string.Format(Messages.CommandHelp, nl); + toSend = CommandHandler.Commands.Aggregate(toSend, (current, command) => current + $"`{prefix}{command.GetAliases()[0]}`: {command.GetSummary()}{nl}"); - await context.Channel.SendMessageAsync(toSend); } @@ -27,4 +27,4 @@ public class HelpCommand : Command { public override string GetSummary() { return "Показывает эту справку"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index ac7038b..0145ebc 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -9,24 +9,27 @@ namespace Boyfriend.Commands; public class KickCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { - var reason = Utils.JoinString(args, 1); var author = context.Guild.GetUser(context.User.Id); var toKick = await Utils.ParseMember(context.Guild, args[0]); + await CommandHandler.CheckPermissions(author, GuildPermission.KickMembers); await CommandHandler.CheckInteractions(author, toKick); - KickMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toKick, - reason); + + await KickMember(context.Guild, context.Channel as ITextChannel, author, toKick, Utils.JoinString(args, 1)); } - private static async void KickMember(IGuild guild, ITextChannel? channel, IUser author, IGuildUser toKick, + private static async Task KickMember(IGuild guild, ITextChannel? channel, IUser author, IGuildUser toKick, string reason) { var authorMention = author.Mention; - await Utils.SendDirectMessage(toKick, string.Format(Messages.YouWereKicked, authorMention, guild.Name, - Utils.WrapInline(reason))); - var guildKickMessage = $"({author.Username}#{author.Discriminator}) {reason}"; - await toKick.KickAsync(guildKickMessage); + var guildKickMessage = $"({Utils.GetNameAndDiscrim(author)}) {reason}"; var notification = string.Format(Messages.MemberKicked, authorMention, toKick.Mention, Utils.WrapInline(reason)); + + await Utils.SendDirectMessage(toKick, string.Format(Messages.YouWereKicked, authorMention, guild.Name, + Utils.WrapInline(reason))); + + await toKick.KickAsync(guildKickMessage); + await Utils.SilentSendAsync(channel, string.Format(Messages.KickResponse, toKick.Mention, Utils.WrapInline(reason))); await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); @@ -44,4 +47,4 @@ public class KickCommand : Command { public override string GetSummary() { return "Выгоняет участника"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index 2c3e0a9..aece45d 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -1,5 +1,6 @@ using Discord; using Discord.Commands; +using Discord.Net; // ReSharper disable UnusedType.Global // ReSharper disable UnusedMember.Global @@ -9,27 +10,29 @@ namespace Boyfriend.Commands; public class MuteCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { - TimeSpan duration; + var author = context.Guild.GetUser(context.User.Id); + var config = Boyfriend.GetGuildConfig(context.Guild); var reason = Utils.JoinString(args, 1); + var role = Utils.GetMuteRole(context.Guild); + var rolesRemoved = config.RolesRemovedOnMute!; + var toMute = await Utils.ParseMember(context.Guild, args[0]); + + TimeSpan duration; try { duration = Utils.GetTimeSpan(args[1]); reason = Utils.JoinString(args, 2); - } - catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { + } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { duration = TimeSpan.FromMilliseconds(-1); } - var author = context.Guild.GetUser(context.User.Id); - var toMute = await Utils.ParseMember(context.Guild, args[0]); if (toMute == null) throw new ApplicationException(Messages.UserNotInGuild); - var role = Utils.GetMuteRole(context.Guild); + if (role != null && toMute.RoleIds.Any(x => x == role.Id) || toMute.TimedOutUntil != null && toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > DateTimeOffset.Now.ToUnixTimeMilliseconds()) throw new ApplicationException(Messages.MemberAlreadyMuted); - var config = Boyfriend.GetGuildConfig(context.Guild); - var rolesRemoved = config.RolesRemovedOnMute!; + if (rolesRemoved.ContainsKey(toMute.Id)) { foreach (var roleId in rolesRemoved[toMute.Id]) await toMute.AddRoleAsync(roleId); rolesRemoved.Remove(toMute.Id); @@ -40,44 +43,53 @@ public class MuteCommand : Command { await CommandHandler.CheckPermissions(author, GuildPermission.ModerateMembers, GuildPermission.ManageRoles); await CommandHandler.CheckInteractions(author, toMute); - MuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toMute, + + await MuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toMute, duration, reason); } - private static async void MuteMember(IGuild guild, ITextChannel? channel, IGuildUser author, IGuildUser toMute, + private static async Task MuteMember(IGuild guild, ITextChannel? channel, IGuildUser author, IGuildUser toMute, TimeSpan duration, string reason) { await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); var authorMention = author.Mention; - var role = Utils.GetMuteRole(guild); var config = Boyfriend.GetGuildConfig(guild); - if (config.RemoveRolesOnMute.GetValueOrDefault(false) && role != null) { - var rolesRemoved = new List(); - try { + var requestOptions = Utils.GetRequestOptions($"({Utils.GetNameAndDiscrim(author)}) {reason}"); + var role = Utils.GetMuteRole(guild); + + if (role != null) { + if (config.RemoveRolesOnMute.GetValueOrDefault(false)) { + var rolesRemoved = new List(); foreach (var roleId in toMute.RoleIds) { - if (roleId == guild.Id) continue; - await toMute.RemoveRoleAsync(roleId); - rolesRemoved.Add(roleId); + try { + if (roleId == guild.Id) continue; + if (roleId == role.Id) continue; + await toMute.RemoveRoleAsync(roleId); + rolesRemoved.Add(roleId); + } catch (HttpException) {} } + + config.RolesRemovedOnMute!.Add(toMute.Id, rolesRemoved); + await config.Save(); } - catch (NullReferenceException) { } - config.RolesRemovedOnMute!.Add(toMute.Id, rolesRemoved); - await config.Save(); - } - - if (role != null) - await toMute.AddRoleAsync(role); - else - await toMute.SetTimeOutAsync(duration); + await toMute.AddRoleAsync(role, requestOptions); + } else + await toMute.SetTimeOutAsync(duration, requestOptions); var notification = string.Format(Messages.MemberMuted, authorMention, toMute.Mention, Utils.WrapInline(reason)); await Utils.SilentSendAsync(channel, string.Format(Messages.MuteResponse, toMute.Mention, Utils.WrapInline(reason))); await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - var task = new Task(() => UnmuteCommand.UnmuteMember(guild, null, guild.GetCurrentUserAsync().Result, toMute, - Messages.PunishmentExpired)); + + async void UnmuteWhenExpires() { + try { + await UnmuteCommand.UnmuteMember(guild, null, await guild.GetCurrentUserAsync(), toMute, + Messages.PunishmentExpired); + } catch (ApplicationException) {} + } + if (role != null) - await Utils.StartDelayed(task, duration, () => toMute.RoleIds.Any(x => x == role.Id)); + await Utils.StartDelayed(new Task(UnmuteWhenExpires), duration); } public override List GetAliases() { diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 9cdfe6e..fd5da44 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -9,9 +9,11 @@ namespace Boyfriend.Commands; public class SettingsCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { - await CommandHandler.CheckPermissions(context.Guild.GetUser(context.User.Id), GuildPermission.ManageGuild); var config = Boyfriend.GetGuildConfig(context.Guild); var guild = context.Guild; + + await CommandHandler.CheckPermissions(context.Guild.GetUser(context.User.Id), GuildPermission.ManageGuild); + if (args.Length == 0) { var nl = Environment.NewLine; var adminLogChannel = guild.GetTextChannel(config.AdminLogChannel.GetValueOrDefault(0)); @@ -43,8 +45,9 @@ public class SettingsCommand : Command { } var setting = args[0].ToLower(); - var value = ""; var shouldDefault = false; + var value = ""; + if (args.Length >= 2) value = args[1].ToLower(); else diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index d2d223f..69874de 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -9,20 +9,25 @@ namespace Boyfriend.Commands; public class UnbanCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { - var toUnban = await Utils.ParseUser(args[0]); - if (context.Guild.GetBanAsync(toUnban.Id) == null) - throw new ApplicationException(Messages.UserNotBanned); - UnbanUser(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toUnban, - Utils.JoinString(args, 1)); + await UnbanUser(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), + await Utils.ParseUser(args[0]), Utils.JoinString(args, 1)); } - public static async void UnbanUser(IGuild guild, ITextChannel? channel, IGuildUser author, IUser toUnban, + public static async Task UnbanUser(IGuild guild, ITextChannel? channel, IGuildUser author, IUser toUnban, string reason) { - await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); + var authorMention = author.Mention; var notification = string.Format(Messages.UserUnbanned, authorMention, toUnban.Mention, Utils.WrapInline(reason)); - await guild.RemoveBanAsync(toUnban); + var requestOptions = Utils.GetRequestOptions($"({Utils.GetNameAndDiscrim(author)}) {reason}"); + + await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); + + if (guild.GetBanAsync(toUnban.Id) == null) + throw new ApplicationException(Messages.UserNotBanned); + + await guild.RemoveBanAsync(toUnban, requestOptions); + await Utils.SilentSendAsync(channel, string.Format(Messages.UnbanResponse, toUnban.Mention, Utils.WrapInline(reason))); await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index 75eec78..9710d5a 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -9,49 +9,45 @@ namespace Boyfriend.Commands; public class UnmuteCommand : Command { public override async Task Run(SocketCommandContext context, string[] args) { - var toUnmute = await Utils.ParseMember(context.Guild, args[0]); - var author = context.Guild.GetUser(context.User.Id); - await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); + await UnmuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), + await Utils.ParseMember(context.Guild, args[0]), Utils.JoinString(args, 1)); + } + + public static async Task UnmuteMember(IGuild guild, ITextChannel? channel, IGuildUser author, IGuildUser toUnmute, + string reason) { + await CommandHandler.CheckPermissions(author, GuildPermission.ModerateMembers, GuildPermission.ManageRoles); await CommandHandler.CheckInteractions(author, toUnmute); - var role = Utils.GetMuteRole(context.Guild); + var authorMention = author.Mention; + var config = Boyfriend.GetGuildConfig(guild); + var notification = string.Format(Messages.MemberUnmuted, authorMention, toUnmute.Mention, + Utils.WrapInline(reason)); + var requestOptions = Utils.GetRequestOptions($"({Utils.GetNameAndDiscrim(author)}) {reason}"); + var role = Utils.GetMuteRole(guild); + if (role != null) { if (toUnmute.RoleIds.All(x => x != role.Id)) { - var config = Boyfriend.GetGuildConfig(context.Guild); var rolesRemoved = config.RolesRemovedOnMute; - foreach (var roleId in rolesRemoved![toUnmute.Id]) await toUnmute.AddRoleAsync(roleId); + await toUnmute.AddRolesAsync(rolesRemoved![toUnmute.Id]); rolesRemoved.Remove(toUnmute.Id); await config.Save(); throw new ApplicationException(Messages.RolesReturned); } - } - if (role != null && toUnmute.RoleIds.All(x => x != role.Id) || - toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() - < DateTimeOffset.Now.ToUnixTimeMilliseconds()) - throw new ApplicationException(Messages.MemberNotMuted); - UnmuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), - toUnmute, Utils.JoinString(args, 1)); - } - - public static async void UnmuteMember(IGuild guild, ITextChannel? channel, IGuildUser author, IGuildUser toUnmute, - string reason) { - await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); - var authorMention = author.Mention; - var notification = string.Format(Messages.MemberUnmuted, authorMention, toUnmute.Mention, - Utils.WrapInline(reason)); - var role = Utils.GetMuteRole(guild); - - if (role != null) { - await toUnmute.RemoveRoleAsync(role); - var config = Boyfriend.GetGuildConfig(guild); + if (toUnmute.RoleIds.All(x => x != role.Id)) + throw new ApplicationException(Messages.MemberNotMuted); + await toUnmute.RemoveRoleAsync(role, requestOptions); if (config.RolesRemovedOnMute!.ContainsKey(toUnmute.Id)) { - foreach (var roleId in config.RolesRemovedOnMute[toUnmute.Id]) await toUnmute.AddRoleAsync(roleId); + await toUnmute.AddRolesAsync(config.RolesRemovedOnMute[toUnmute.Id]); config.RolesRemovedOnMute.Remove(toUnmute.Id); await config.Save(); } } else { + if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() + < DateTimeOffset.Now.ToUnixTimeMilliseconds()) + throw new ApplicationException(Messages.MemberNotMuted); + await toUnmute.RemoveTimeOutAsync(); } diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 3ca9897..0435da4 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -21,10 +21,12 @@ public class EventHandler { await Boyfriend.SetupGuildConfigs(); var i = new Random().Next(3); + foreach (var guild in Boyfriend.Client.Guilds) { var config = Boyfriend.GetGuildConfig(guild); - Messages.Culture = new CultureInfo(config.Lang!); var channel = guild.GetTextChannel(config.BotLogChannel.GetValueOrDefault(0)); + Messages.Culture = new CultureInfo(config.Lang!); + if (!config.ReceiveStartupMessages.GetValueOrDefault(true) || channel == null) continue; await channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(config.Lang!, i))); } @@ -33,6 +35,7 @@ public class EventHandler { private static async Task MessageDeletedEvent(Cacheable message, Cacheable channel) { var msg = message.Value; + var toSend = msg == null ? string.Format(Messages.UncachedMessageDeleted, Utils.MentionChannel(channel.Id)) : string.Format(Messages.CachedMessageDeleted, msg.Author.Mention) + @@ -43,21 +46,23 @@ public class EventHandler { private static async Task MessageReceivedEvent(SocketMessage messageParam) { if (messageParam is not SocketUserMessage message) return; + + var argPos = 0; var user = (IGuildUser) message.Author; var guild = user.Guild; - var argPos = 0; - var guildConfig = Boyfriend.GetGuildConfig(guild); + var prev = ""; + var prevFailsafe = ""; + var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); + var prevsArray = prevs as IMessage[] ?? prevs.ToArray(); + Messages.Culture = new CultureInfo(guildConfig.Lang!); + if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && !user.GuildPermissions.MentionEveryone) await BanCommand.BanUser(guild, null, await guild.GetCurrentUserAsync(), user, TimeSpan.FromMilliseconds(-1), Messages.AutobanReason); - var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); - var prevsArray = prevs as IMessage[] ?? prevs.ToArray(); - var prev = ""; - var prevFailsafe = ""; try { prev = prevsArray[1].Content; prevFailsafe = prevsArray[2].Content; @@ -77,7 +82,9 @@ public class EventHandler { ISocketMessageChannel channel) { var msg = messageCached.Value; var nl = Environment.NewLine; + if (msg != null && msg.Content == messageSocket.Content) return; + var toSend = msg == null ? string.Format(Messages.UncachedMessageEdited, messageSocket.Author.Mention, Utils.MentionChannel(channel.Id)) + @@ -91,9 +98,11 @@ public class EventHandler { private static async Task UserJoinedEvent(SocketGuildUser user) { var guild = user.Guild; var config = Boyfriend.GetGuildConfig(guild); + if (config.SendWelcomeMessages.GetValueOrDefault(true)) await Utils.SilentSendAsync(guild.SystemChannel, string.Format(config.WelcomeMessage!, user.Mention, guild.Name)); + if (config.DefaultRole != 0) await user.AddRoleAsync(Utils.ParseRole(guild, config.DefaultRole.ToString()!)); } diff --git a/Boyfriend/GuildConfig.cs b/Boyfriend/GuildConfig.cs index 6b45aa7..57654c4 100644 --- a/Boyfriend/GuildConfig.cs +++ b/Boyfriend/GuildConfig.cs @@ -7,15 +7,19 @@ public class GuildConfig { public ulong? Id { get; } public string? Lang { get; set; } public string? Prefix { get; set; } + public bool? RemoveRolesOnMute { get; set; } public bool? UseSystemChannel { get; set; } public bool? SendWelcomeMessages { get; set; } public bool? ReceiveStartupMessages { get; set; } + public string? WelcomeMessage { get; set; } + public ulong? DefaultRole { get; set; } public ulong? MuteRole { get; set; } public ulong? AdminLogChannel { get; set; } public ulong? BotLogChannel { get; set; } + public Dictionary>? RolesRemovedOnMute { get; private set; } public GuildConfig(ulong id) { @@ -25,6 +29,7 @@ public class GuildConfig { public void Validate() { if (Id == null) throw new Exception("Something went horribly, horribly wrong"); + Lang ??= "ru"; Messages.Culture = new CultureInfo(Lang); Prefix ??= "!"; @@ -43,6 +48,7 @@ public class GuildConfig { public async Task Save() { Validate(); RolesRemovedOnMute!.TrimExcess(); + await File.WriteAllTextAsync("config_" + Id + ".json", JsonConvert.SerializeObject(this)); } } diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index eedeaf7..e653b86 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -16,6 +16,7 @@ public static class Utils { public static string GetBeep(string cultureInfo, int i = -1) { Messages.Culture = new CultureInfo(cultureInfo); + var beeps = new[] {Messages.Beep1, Messages.Beep2, Messages.Beep3}; return beeps[i < 0 ? new Random().Next(3) : i]; } @@ -38,11 +39,9 @@ public static class Utils { return $"<#{id}>"; } - public static async Task StartDelayed(Task toRun, TimeSpan delay, Func? condition = null) { + public static async Task StartDelayed(Task toRun, TimeSpan delay) { await Task.Delay(delay); - var conditionResult = condition?.Invoke() ?? true; - if (conditionResult) - toRun.Start(); + toRun.Start(); } private static ulong ParseMention(string mention) { @@ -52,8 +51,7 @@ public static class Utils { private static ulong? ParseMentionNullable(string mention) { try { return ParseMention(mention) == 0 ? throw new FormatException() : ParseMention(mention); - } - catch (FormatException) { + } catch (FormatException) { return null; } } @@ -99,6 +97,7 @@ public static class Utils { public static async Task SilentSendAsync(ITextChannel? channel, string text) { if (channel == null) return; + try { await channel.SendMessageAsync(text, false, null, null, AllowedMentions.None); } catch (ArgumentException) {} @@ -111,4 +110,14 @@ public static class Utils { public static string JoinString(string[] args, int startIndex) { return string.Join(" ", args, startIndex, args.Length - startIndex); } + + public static string GetNameAndDiscrim(IUser user) { + return $"{user.Username}#{user.Discriminator}"; + } + + public static RequestOptions GetRequestOptions(string reason) { + var options = RequestOptions.Default; + options.AuditLogReason = reason; + return options; + } } From e41a459f6f551d790dc9879aa09a446ae3250223 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Sat, 12 Feb 2022 23:54:45 +0500 Subject: [PATCH 009/329] Fix durations blocking command handler thread Add "This punishment will expire in" text to ban/mute notifications --- Boyfriend/Commands/BanCommand.cs | 23 +++++++++++++++-------- Boyfriend/Commands/MuteCommand.cs | 24 +++++++++++++++--------- Boyfriend/Messages.Designer.cs | 13 +++++++++++-- Boyfriend/Messages.resx | 7 +++++-- Boyfriend/Messages.ru.resx | 7 +++++-- Boyfriend/Utils.cs | 5 ----- 6 files changed, 51 insertions(+), 28 deletions(-) diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 145316a..05a18f0 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -28,7 +28,12 @@ public class BanCommand : Command { var authorMention = author.Mention; var guildBanMessage = $"({Utils.GetNameAndDiscrim(author)}) {reason}"; var memberToBan = await guild.GetUserAsync(toBan.Id); - var notification = string.Format(Messages.UserBanned, authorMention, toBan.Mention, Utils.WrapInline(reason)); + var expiresIn = duration.TotalSeconds > 0 + ? string.Format(Messages.PunishmentExpiresIn, Environment.NewLine, + DateTimeOffset.Now.ToUnixTimeSeconds() + duration.TotalSeconds) + : ""; + var notification = string.Format(Messages.UserBanned, authorMention, toBan.Mention, Utils.WrapInline(reason), + expiresIn); await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); if (memberToBan != null) @@ -44,14 +49,16 @@ public class BanCommand : Command { await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - async void UnbanWhenExpires() { - try { - await UnbanCommand.UnbanUser(guild, null, await guild.GetCurrentUserAsync(), toBan, - Messages.PunishmentExpired); - } catch (ApplicationException) {} + if (duration.TotalSeconds > 0) { + var task = new Task(async () => { + await Task.Delay(duration); + try { + await UnbanCommand.UnbanUser(guild, null, await guild.GetCurrentUserAsync(), toBan, + Messages.PunishmentExpired); + } catch (ApplicationException) {} + }); + task.Start(); } - - await Utils.StartDelayed(new Task(UnbanWhenExpires), duration); } public override List GetAliases() { diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index aece45d..931384e 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -55,6 +55,12 @@ public class MuteCommand : Command { var config = Boyfriend.GetGuildConfig(guild); var requestOptions = Utils.GetRequestOptions($"({Utils.GetNameAndDiscrim(author)}) {reason}"); var role = Utils.GetMuteRole(guild); + var expiresIn = duration.TotalSeconds > 0 + ? string.Format(Messages.PunishmentExpiresIn, Environment.NewLine, + DateTimeOffset.Now.ToUnixTimeSeconds() + duration.TotalSeconds) + : ""; + var notification = string.Format(Messages.MemberMuted, authorMention, toMute.Mention, Utils.WrapInline(reason), + expiresIn); if (role != null) { if (config.RemoveRolesOnMute.GetValueOrDefault(false)) { @@ -75,21 +81,21 @@ public class MuteCommand : Command { await toMute.AddRoleAsync(role, requestOptions); } else await toMute.SetTimeOutAsync(duration, requestOptions); - var notification = string.Format(Messages.MemberMuted, authorMention, toMute.Mention, Utils.WrapInline(reason)); await Utils.SilentSendAsync(channel, string.Format(Messages.MuteResponse, toMute.Mention, Utils.WrapInline(reason))); await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - async void UnmuteWhenExpires() { - try { - await UnmuteCommand.UnmuteMember(guild, null, await guild.GetCurrentUserAsync(), toMute, - Messages.PunishmentExpired); - } catch (ApplicationException) {} + if (role != null && duration.TotalSeconds > 0) { + var task = new Task(async () => { + await Task.Delay(duration); + try { + await UnmuteCommand.UnmuteMember(guild, null, await guild.GetCurrentUserAsync(), toMute, + Messages.PunishmentExpired); + } catch (ApplicationException) {} + }); + task.Start(); } - - if (role != null) - await Utils.StartDelayed(new Task(UnmuteWhenExpires), duration); } public override List GetAliases() { diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 5e7fd44..1214e00 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -475,7 +475,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to {0} muted {1} for {2}. + /// Looks up a localized string similar to {0} muted {1} for {2}{3}. /// internal static string MemberMuted { get { @@ -573,6 +573,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to {0}This punishment will expire <t:{1}:R>. + /// + internal static string PunishmentExpiresIn { + get { + return ResourceManager.GetString("PunishmentExpiresIn", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0}I'm ready! (C#). /// @@ -673,7 +682,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to {0} banned {1} for {2}. + /// Looks up a localized string similar to {0} banned {1} for {2}{3}. /// internal static string UserBanned { get { diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 75ee336..1714f55 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -109,7 +109,7 @@ You were banned by {0} in guild {1} for {2} - {0} banned {1} for {2} + {0} banned {1} for {2}{3} Punishment expired @@ -133,7 +133,7 @@ {0} kicked {1} for {2} - {0} muted {1} for {2} + {0} muted {1} for {2}{3} ms @@ -249,4 +249,7 @@ Receive startup messages (`receiveStartupMessages`): {0}{1} + + {0}This punishment will expire <t:{1}:R> + \ No newline at end of file diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 2790d0a..7ff5cd2 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -100,7 +100,7 @@ Тебя забанил {0} на сервере {1} за {2} - {0} банит {1} за {2} + {0} банит {1} за {2}{3} Время наказания истекло @@ -124,7 +124,7 @@ {0} выгоняет {1} за {2} - {0} глушит {1} за {2} + {0} глушит {1} за {2}{3} мс @@ -240,4 +240,7 @@ Получать сообщения о запуске (`receiveStartupMessages`): {0}{1} + + {0}Это наказание истечёт <t:{1}:R> + \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index e653b86..bfc3756 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -39,11 +39,6 @@ public static class Utils { return $"<#{id}>"; } - public static async Task StartDelayed(Task toRun, TimeSpan delay) { - await Task.Delay(delay); - toRun.Start(); - } - private static ulong ParseMention(string mention) { return Convert.ToUInt64(Regex.Replace(mention, "[^0-9]", "")); } From 868b6bcaa7ae15017f427020b112285e69345028 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Mon, 21 Feb 2022 22:08:55 +0500 Subject: [PATCH 010/329] time-out failsafes and new warnings rewrote setting values in SettingsCommand.cs fixed a bug with message edited notification on mobile fixed an exploit with WrapInline where you could escape the code block by simply using ` moved a few things in MuteCommand.cs cleaned up code updated library to 3.3.2 --- Boyfriend/Boyfriend.cs | 17 +-- Boyfriend/Boyfriend.csproj | 2 +- Boyfriend/Commands/BanCommand.cs | 20 ++-- Boyfriend/Commands/Command.cs | 5 +- Boyfriend/Commands/MuteCommand.cs | 55 +++++----- Boyfriend/Commands/SettingsCommand.cs | 150 ++++++++++---------------- Boyfriend/EventHandler.cs | 35 +++--- Boyfriend/GuildConfig.cs | 11 +- Boyfriend/Messages.Designer.cs | 137 ++++++++--------------- Boyfriend/Messages.resx | 59 ++++------ Boyfriend/Messages.ru.resx | 59 ++++------ Boyfriend/Utils.cs | 15 +-- 12 files changed, 220 insertions(+), 345 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 1793c3e..96d4df8 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -1,7 +1,7 @@ using System.Globalization; -using Newtonsoft.Json; using Discord; using Discord.WebSocket; +using Newtonsoft.Json; namespace Boyfriend; @@ -56,20 +56,11 @@ public static class Boyfriend { } } - public static void ResetGuildConfig(IGuild guild) { - GuildConfigDictionary.Remove(guild.Id); - - var config = new GuildConfig(guild.Id); - config.Validate(); - - GuildConfigDictionary.Add(guild.Id, config); - } - public static GuildConfig GetGuildConfig(IGuild guild) { Messages.Culture = new CultureInfo("ru"); - var config = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] : - new GuildConfig(guild.Id); + var config = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] + : new GuildConfig(guild.Id); config.Validate(); return config; @@ -82,4 +73,4 @@ public static class Boyfriend { throw new Exception(Messages.CouldntFindGuildByChannel); } -} +} \ No newline at end of file diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index eaa313b..cd969e0 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -15,7 +15,7 @@ - + diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 05a18f0..60e945f 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -16,6 +16,7 @@ public class BanCommand : Command { duration = Utils.GetTimeSpan(args[1]); reason = Utils.JoinString(args, 2); } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { + await Warn(context.Channel as ITextChannel, Messages.DurationParseFailed); duration = TimeSpan.FromMilliseconds(-1); } @@ -28,10 +29,8 @@ public class BanCommand : Command { var authorMention = author.Mention; var guildBanMessage = $"({Utils.GetNameAndDiscrim(author)}) {reason}"; var memberToBan = await guild.GetUserAsync(toBan.Id); - var expiresIn = duration.TotalSeconds > 0 - ? string.Format(Messages.PunishmentExpiresIn, Environment.NewLine, - DateTimeOffset.Now.ToUnixTimeSeconds() + duration.TotalSeconds) - : ""; + var expiresIn = duration.TotalSeconds > 0 ? string.Format(Messages.PunishmentExpiresIn, Environment.NewLine, + DateTimeOffset.Now.ToUnixTimeSeconds() + duration.TotalSeconds) : ""; var notification = string.Format(Messages.UserBanned, authorMention, toBan.Mention, Utils.WrapInline(reason), expiresIn); @@ -39,25 +38,24 @@ public class BanCommand : Command { if (memberToBan != null) await CommandHandler.CheckInteractions(author, memberToBan); - await Utils.SendDirectMessage(toBan, string.Format(Messages.YouWereBanned, author.Mention, guild.Name, - Utils.WrapInline(reason))); + await Utils.SendDirectMessage(toBan, + string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.WrapInline(reason))); await guild.AddBanAsync(toBan, 0, guildBanMessage); - await Utils.SilentSendAsync(channel, string.Format(Messages.BanResponse, toBan.Mention, - Utils.WrapInline(reason))); + await Utils.SilentSendAsync(channel, + string.Format(Messages.BanResponse, toBan.Mention, Utils.WrapInline(reason))); await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); if (duration.TotalSeconds > 0) { - var task = new Task(async () => { + await Task.Run(async () => { await Task.Delay(duration); try { await UnbanCommand.UnbanUser(guild, null, await guild.GetCurrentUserAsync(), toBan, Messages.PunishmentExpired); } catch (ApplicationException) {} }); - task.Start(); } } @@ -72,4 +70,4 @@ public class BanCommand : Command { public override string GetSummary() { return "Банит пользователя"; } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/Command.cs b/Boyfriend/Commands/Command.cs index b90d6b0..af2c845 100644 --- a/Boyfriend/Commands/Command.cs +++ b/Boyfriend/Commands/Command.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using Discord.WebSocket; namespace Boyfriend.Commands; @@ -13,7 +12,7 @@ public abstract class Command { public abstract string GetSummary(); - protected static async Task Warn(ISocketMessageChannel channel, string warning) { - await Utils.SilentSendAsync(channel as ITextChannel, ":warning: " + warning); + protected static async Task Warn(ITextChannel? channel, string warning) { + await Utils.SilentSendAsync(channel, ":warning: " + warning); } } \ No newline at end of file diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index 931384e..4ad55e8 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -22,23 +22,22 @@ public class MuteCommand : Command { duration = Utils.GetTimeSpan(args[1]); reason = Utils.JoinString(args, 2); } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { + await Warn(context.Channel as ITextChannel, Messages.DurationParseFailed); duration = TimeSpan.FromMilliseconds(-1); } if (toMute == null) throw new ApplicationException(Messages.UserNotInGuild); - if (role != null && toMute.RoleIds.Any(x => x == role.Id) || - toMute.TimedOutUntil != null && toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() - > DateTimeOffset.Now.ToUnixTimeMilliseconds()) + if (role != null && toMute.RoleIds.Any(x => x == role.Id) || toMute.TimedOutUntil != null && + toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > DateTimeOffset.Now.ToUnixTimeMilliseconds()) throw new ApplicationException(Messages.MemberAlreadyMuted); if (rolesRemoved.ContainsKey(toMute.Id)) { foreach (var roleId in rolesRemoved[toMute.Id]) await toMute.AddRoleAsync(roleId); rolesRemoved.Remove(toMute.Id); await config.Save(); - await Warn(context.Channel, Messages.RolesReturned); - return; + throw new ApplicationException(Messages.RolesReturned); } await CommandHandler.CheckPermissions(author, GuildPermission.ModerateMembers, GuildPermission.ManageRoles); @@ -55,10 +54,9 @@ public class MuteCommand : Command { var config = Boyfriend.GetGuildConfig(guild); var requestOptions = Utils.GetRequestOptions($"({Utils.GetNameAndDiscrim(author)}) {reason}"); var role = Utils.GetMuteRole(guild); - var expiresIn = duration.TotalSeconds > 0 - ? string.Format(Messages.PunishmentExpiresIn, Environment.NewLine, - DateTimeOffset.Now.ToUnixTimeSeconds() + duration.TotalSeconds) - : ""; + var hasDuration = duration.TotalSeconds > 0; + var expiresIn = hasDuration ? string.Format(Messages.PunishmentExpiresIn, Environment.NewLine, + DateTimeOffset.Now.ToUnixTimeSeconds() + duration.TotalSeconds) : ""; var notification = string.Format(Messages.MemberMuted, authorMention, toMute.Mention, Utils.WrapInline(reason), expiresIn); @@ -71,31 +69,38 @@ public class MuteCommand : Command { if (roleId == role.Id) continue; await toMute.RemoveRoleAsync(roleId); rolesRemoved.Add(roleId); - } catch (HttpException) {} + } catch (HttpException e) { + await Warn(channel, + string.Format(Messages.RoleRemovalFailed, $"<@&{roleId}>", Utils.WrapInline(e.Reason))); + } } config.RolesRemovedOnMute!.Add(toMute.Id, rolesRemoved); await config.Save(); + + if (hasDuration) + await Task.Run(async () => { + await Task.Delay(duration); + try { + await UnmuteCommand.UnmuteMember(guild, null, await guild.GetCurrentUserAsync(), toMute, + Messages.PunishmentExpired); + } catch (ApplicationException) {} + }); } await toMute.AddRoleAsync(role, requestOptions); - } else + } else { + if (!hasDuration) + throw new ApplicationException(Messages.DurationRequiredForTimeOuts); + if (toMute.IsBot) + throw new ApplicationException(Messages.CannotTimeOutBot); + await toMute.SetTimeOutAsync(duration, requestOptions); - await Utils.SilentSendAsync(channel, string.Format(Messages.MuteResponse, toMute.Mention, - Utils.WrapInline(reason))); + } + await Utils.SilentSendAsync(channel, + string.Format(Messages.MuteResponse, toMute.Mention, Utils.WrapInline(reason))); await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - - if (role != null && duration.TotalSeconds > 0) { - var task = new Task(async () => { - await Task.Delay(duration); - try { - await UnmuteCommand.UnmuteMember(guild, null, await guild.GetCurrentUserAsync(), toMute, - Messages.PunishmentExpired); - } catch (ApplicationException) {} - }); - task.Start(); - } } public override List GetAliases() { @@ -109,4 +114,4 @@ public class MuteCommand : Command { public override string GetSummary() { return "Глушит участника"; } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index fd5da44..c0e189a 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Reflection; using Discord; using Discord.Commands; @@ -16,117 +16,83 @@ public class SettingsCommand : Command { if (args.Length == 0) { var nl = Environment.NewLine; - var adminLogChannel = guild.GetTextChannel(config.AdminLogChannel.GetValueOrDefault(0)); - var admin = adminLogChannel == null ? Messages.ChannelNotSpecified : adminLogChannel.Mention; - var botLogChannel = guild.GetTextChannel(config.BotLogChannel.GetValueOrDefault(0)); - var bot = botLogChannel == null ? Messages.ChannelNotSpecified : botLogChannel.Mention; - var muteRole = guild.GetRole(config.MuteRole.GetValueOrDefault(0)); - var mute = muteRole == null ? Messages.RoleNotSpecified : muteRole.Mention; - var defaultRole = guild.GetRole(config.DefaultRole.GetValueOrDefault(0)); - var defaultr = defaultRole == null ? Messages.RoleNotSpecified : defaultRole.Mention; + dynamic forCheck; + var adminLogChannel = (forCheck = guild.GetTextChannel(config.AdminLogChannel.GetValueOrDefault(0))) == null + ? Messages.ChannelNotSpecified : forCheck.Mention; + var botLogChannel = (forCheck = guild.GetTextChannel(config.BotLogChannel.GetValueOrDefault(0))) == null + ? Messages.ChannelNotSpecified : forCheck.Mention; + var muteRole = (forCheck = guild.GetRole(config.MuteRole.GetValueOrDefault(0))) == null + ? Messages.RoleNotSpecified : forCheck.Mention; + var defaultRole = (forCheck = guild.GetRole(config.DefaultRole.GetValueOrDefault(0))) == null + ? Messages.RoleNotSpecified : forCheck.Mention; var toSend = string.Format(Messages.CurrentSettings, nl) + string.Format(Messages.CurrentSettingsLang, config.Lang, nl) + string.Format(Messages.CurrentSettingsPrefix, config.Prefix, nl) + - string.Format(Messages.CurrentSettingsRemoveRoles, YesOrNo( - config.RemoveRolesOnMute.GetValueOrDefault(false)), nl) + - string.Format(Messages.CurrentSettingsUseSystemChannel, YesOrNo( - config.UseSystemChannel.GetValueOrDefault(true)), nl) + - string.Format(Messages.CurrentSettingsSendWelcomeMessages, YesOrNo( - config.SendWelcomeMessages.GetValueOrDefault(true)), nl) + - string.Format(Messages.CurrentSettingsReceiveStartupMessages, YesOrNo( - config.ReceiveStartupMessages.GetValueOrDefault(true)), nl) + + string.Format(Messages.CurrentSettingsRemoveRoles, + YesOrNo(config.RemoveRolesOnMute.GetValueOrDefault(false)), nl) + + string.Format(Messages.CurrentSettingsUseSystemChannel, + YesOrNo(config.UseSystemChannel.GetValueOrDefault(true)), nl) + + string.Format(Messages.CurrentSettingsSendWelcomeMessages, + YesOrNo(config.SendWelcomeMessages.GetValueOrDefault(true)), nl) + + string.Format(Messages.CurrentSettingsReceiveStartupMessages, + YesOrNo(config.ReceiveStartupMessages.GetValueOrDefault(true)), nl) + string.Format(Messages.CurrentSettingsWelcomeMessage, config.WelcomeMessage, nl) + - string.Format(Messages.CurrentSettingsDefaultRole, defaultr, nl) + - string.Format(Messages.CurrentSettingsMuteRole, mute, nl) + - string.Format(Messages.CurrentSettingsAdminLogChannel, admin, nl) + - string.Format(Messages.CurrentSettingsBotLogChannel, bot); + string.Format(Messages.CurrentSettingsDefaultRole, defaultRole, nl) + + string.Format(Messages.CurrentSettingsMuteRole, muteRole, nl) + + string.Format(Messages.CurrentSettingsAdminLogChannel, adminLogChannel, nl) + + string.Format(Messages.CurrentSettingsBotLogChannel, botLogChannel); await Utils.SilentSendAsync(context.Channel as ITextChannel ?? throw new ApplicationException(), toSend); return; } var setting = args[0].ToLower(); - var shouldDefault = false; var value = ""; if (args.Length >= 2) - value = args[1].ToLower(); - else - shouldDefault = true; + try { + value = args[1].ToLower(); + } catch (IndexOutOfRangeException) { + throw new ApplicationException(Messages.InvalidSettingValue); + } - var boolValue = ParseBool(value); - var channel = await Utils.ParseChannelNullable(value) as IGuildChannel; - var role = Utils.ParseRoleNullable(guild, value); + PropertyInfo? property = null; + foreach (var prop in typeof(GuildConfig).GetProperties()) + if (setting == prop.Name.ToLower()) + property = prop; + if (property == null || !property.CanWrite) + throw new ApplicationException(Messages.SettingDoesntExist); + var type = property.PropertyType; - switch (setting) { - case "reset": - Boyfriend.ResetGuildConfig(guild); - break; - case "lang" when value is not ("ru" or "en"): + if (value is "reset" or "default") { + property.SetValue(config, null); + } else if (type == typeof(string)) { + if (setting == "lang" && value is not ("ru" or "en")) throw new ApplicationException(Messages.LanguageNotSupported); - case "lang": - config.Lang = shouldDefault ? "ru" : value; - Messages.Culture = new CultureInfo(shouldDefault ? "ru" : value); - break; - case "prefix": - config.Prefix = shouldDefault ? "!" : value; - break; - case "removerolesonmute": - config.RemoveRolesOnMute = !shouldDefault && GetBoolValue(boolValue); - break; - case "usesystemchannel": - config.UseSystemChannel = shouldDefault || GetBoolValue(boolValue); - break; - case "sendwelcomemessages": - config.SendWelcomeMessages = shouldDefault || GetBoolValue(boolValue); - break; - case "receivestartupmessages": - config.ReceiveStartupMessages = shouldDefault || GetBoolValue(boolValue); - break; - case "welcomemessage": - config.WelcomeMessage = shouldDefault ? Messages.DefaultWelcomeMessage : value; - break; - case "defaultrole": - config.DefaultRole = shouldDefault ? 0 : GetRoleId(role); - break; - case "muterole": - config.MuteRole = shouldDefault ? 0 : GetRoleId(role); - break; - case "adminlogchannel": - config.AdminLogChannel = shouldDefault ? 0 : GetChannelId(channel); - break; - case "botlogchannel": - config.BotLogChannel = shouldDefault ? 0 : GetChannelId(channel); - break; - default: - await context.Channel.SendMessageAsync(Messages.SettingDoesntExist); - return; + property.SetValue(config, value); + } else { + try { + if (type == typeof(bool?)) + property.SetValue(config, Convert.ToBoolean(value)); + + if (type == typeof(ulong?)) { + var id = Convert.ToUInt64(value); + if (property.Name.EndsWith("Channel") && guild.GetTextChannel(id) == null) + throw new ApplicationException(Messages.InvalidChannel); + if (property.Name.EndsWith("Role") && guild.GetRole(id) == null) + throw new ApplicationException(Messages.InvalidRole); + + property.SetValue(config, id); + } + } catch (Exception e) when (e is FormatException or OverflowException) { + throw new ApplicationException(Messages.InvalidSettingValue); + } } + config.Validate(); await config.Save(); - await context.Channel.SendMessageAsync(Messages.SettingsUpdated); } - private static bool? ParseBool(string toParse) { - try { - return bool.Parse(toParse.ToLower()); - } catch (FormatException) { - return null; - } - } - - private static bool GetBoolValue(bool? from) { - return from ?? throw new ApplicationException(Messages.InvalidBoolean); - } - - private static ulong GetRoleId(IRole? role) { - return (role ?? throw new ApplicationException(Messages.InvalidRoleSpecified)).Id; - } - - private static ulong GetChannelId(IGuildChannel? channel) { - return (channel ?? throw new ApplicationException(Messages.InvalidChannelSpecified)).Id; - } - private static string YesOrNo(bool isYes) { return isYes ? Messages.Yes : Messages.No; } @@ -142,4 +108,4 @@ public class SettingsCommand : Command { public override string GetSummary() { return "Настраивает бота отдельно для этого сервера"; } -} +} \ No newline at end of file diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 0435da4..bb484c8 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -36,12 +36,10 @@ public class EventHandler { Cacheable channel) { var msg = message.Value; - var toSend = msg == null - ? string.Format(Messages.UncachedMessageDeleted, Utils.MentionChannel(channel.Id)) + var toSend = msg == null ? string.Format(Messages.UncachedMessageDeleted, Utils.MentionChannel(channel.Id)) : string.Format(Messages.CachedMessageDeleted, msg.Author.Mention) + $"{Utils.MentionChannel(channel.Id)}: {Environment.NewLine}{Utils.Wrap(msg.Content)}"; - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel( - Boyfriend.FindGuild(channel.Value)), toSend); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Boyfriend.FindGuild(channel.Value)), toSend); } private static async Task MessageReceivedEvent(SocketMessage messageParam) { @@ -58,21 +56,20 @@ public class EventHandler { Messages.Culture = new CultureInfo(guildConfig.Lang!); - if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) - && !user.GuildPermissions.MentionEveryone) + if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && + !user.GuildPermissions.MentionEveryone) await BanCommand.BanUser(guild, null, await guild.GetCurrentUserAsync(), user, TimeSpan.FromMilliseconds(-1), Messages.AutobanReason); try { prev = prevsArray[1].Content; prevFailsafe = prevsArray[2].Content; - } - catch (IndexOutOfRangeException) { } + } catch (IndexOutOfRangeException) {} - if (!(message.HasStringPrefix(guildConfig.Prefix, ref argPos) - || message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) - || user == await guild.GetCurrentUserAsync() - || user.IsBot && (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe))) + if (!(message.HasStringPrefix(guildConfig.Prefix, ref argPos) || + message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) || + user == await guild.GetCurrentUserAsync() || + user.IsBot && (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe))) return; await CommandHandler.HandleCommand(message); @@ -87,12 +84,10 @@ public class EventHandler { var toSend = msg == null ? string.Format(Messages.UncachedMessageEdited, messageSocket.Author.Mention, - Utils.MentionChannel(channel.Id)) + - Utils.Wrap(messageSocket.Content) - : string.Format(Messages.CachedMessageEdited, msg.Author.Mention, Utils.MentionChannel(channel.Id), nl, nl, + Utils.MentionChannel(channel.Id)) + Utils.Wrap(messageSocket.Content) : string.Format( + Messages.CachedMessageEdited, msg.Author.Mention, Utils.MentionChannel(channel.Id), nl, nl, Utils.Wrap(msg.Content), nl, nl, Utils.Wrap(messageSocket.Content)); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Boyfriend.FindGuild(channel)), - toSend); + await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Boyfriend.FindGuild(channel)), toSend); } private static async Task UserJoinedEvent(SocketGuildUser user) { @@ -100,10 +95,10 @@ public class EventHandler { var config = Boyfriend.GetGuildConfig(guild); if (config.SendWelcomeMessages.GetValueOrDefault(true)) - await Utils.SilentSendAsync(guild.SystemChannel, string.Format(config.WelcomeMessage!, user.Mention, - guild.Name)); + await Utils.SilentSendAsync(guild.SystemChannel, + string.Format(config.WelcomeMessage!, user.Mention, guild.Name)); if (config.DefaultRole != 0) await user.AddRoleAsync(Utils.ParseRole(guild, config.DefaultRole.ToString()!)); } -} +} \ No newline at end of file diff --git a/Boyfriend/GuildConfig.cs b/Boyfriend/GuildConfig.cs index 57654c4..59d25a3 100644 --- a/Boyfriend/GuildConfig.cs +++ b/Boyfriend/GuildConfig.cs @@ -1,9 +1,15 @@ using System.Globalization; using Newtonsoft.Json; +// ReSharper disable MemberCanBePrivate.Global namespace Boyfriend; public class GuildConfig { + + public GuildConfig(ulong id) { + Id = id; + Validate(); + } public ulong? Id { get; } public string? Lang { get; set; } public string? Prefix { get; set; } @@ -22,11 +28,6 @@ public class GuildConfig { public Dictionary>? RolesRemovedOnMute { get; private set; } - public GuildConfig(ulong id) { - Id = id; - Validate(); - } - public void Validate() { if (Id == null) throw new Exception("Something went horribly, horribly wrong"); diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 1214e00..5f6d7f5 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -60,15 +60,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Arguments not present! {0}. - /// - internal static string ArgumentNotPresent { - get { - return ResourceManager.GetString("ArgumentNotPresent", resourceCulture); - } - } - /// /// Looks up a localized string similar to Too many mentions in 1 message. /// @@ -78,15 +69,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Invalid argument count! {0}. - /// - internal static string BadArgumentCount { - get { - return ResourceManager.GetString("BadArgumentCount", resourceCulture); - } - } - /// /// Looks up a localized string similar to :white_check_mark: Successfully banned {0} for {1}. /// @@ -141,6 +123,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. + /// + internal static string CannotTimeOutBot { + get { + return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); + } + } + /// /// Looks up a localized string similar to Not specified. /// @@ -177,15 +168,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Command execution was unsuccessful: {0}. - /// - internal static string CommandExecutionUnsuccessful { - get { - return ResourceManager.GetString("CommandExecutionUnsuccessful", resourceCulture); - } - } - /// /// Looks up a localized string similar to Command help:{0}. /// @@ -213,15 +195,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Command parsing failed: {0}. - /// - internal static string CommandParseFailed { - get { - return ResourceManager.GetString("CommandParseFailed", resourceCulture); - } - } - /// /// Looks up a localized string similar to Couldn't find guild by message!. /// @@ -348,6 +321,24 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to I couldn't parse the specified duration! One of the components could be outside it's valid range (e.g. `24h` or `60m`). + /// + internal static string DurationParseFailed { + get { + return ResourceManager.GetString("DurationParseFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot mute someone forever using timeouts! Either specify a proper duration, or set a mute role in settings. + /// + internal static string DurationRequiredForTimeOuts { + get { + return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); + } + } + /// /// Looks up a localized string similar to Members are in different guilds!. /// @@ -403,38 +394,29 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Invalid admin log channel for guild. + /// Looks up a localized string similar to This channel does not exist!. /// - internal static string InvalidAdminLogChannel { + internal static string InvalidChannel { get { - return ResourceManager.GetString("InvalidAdminLogChannel", resourceCulture); + return ResourceManager.GetString("InvalidChannel", resourceCulture); } } /// - /// Looks up a localized string similar to Invalid argument! 'true' or 'false' required!. + /// Looks up a localized string similar to This role does not exist!. /// - internal static string InvalidBoolean { + internal static string InvalidRole { get { - return ResourceManager.GetString("InvalidBoolean", resourceCulture); + return ResourceManager.GetString("InvalidRole", resourceCulture); } } /// - /// Looks up a localized string similar to Invalid channel specified!. + /// Looks up a localized string similar to Invalid setting value specified!. /// - internal static string InvalidChannelSpecified { + internal static string InvalidSettingValue { get { - return ResourceManager.GetString("InvalidChannelSpecified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid role specified!. - /// - internal static string InvalidRoleSpecified { - get { - return ResourceManager.GetString("InvalidRoleSpecified", resourceCulture); + return ResourceManager.GetString("InvalidSettingValue", resourceCulture); } } @@ -528,24 +510,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Someone removed the mute role manually!. - /// - internal static string MuteRoleManuallyRemoved { - get { - return ResourceManager.GetString("MuteRoleManuallyRemoved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You must set up a mute role in settings!. - /// - internal static string MuteRoleRequired { - get { - return ResourceManager.GetString("MuteRoleRequired", resourceCulture); - } - } - /// /// Looks up a localized string similar to No. /// @@ -591,15 +555,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Repeated arguments detected! {0}. - /// - internal static string RepeatedArgumentsDetected { - get { - return ResourceManager.GetString("RepeatedArgumentsDetected", resourceCulture); - } - } - /// /// Looks up a localized string similar to Not specified. /// @@ -609,6 +564,15 @@ 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. /// @@ -655,7 +619,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Message edited from {0} in channel {1}, but I forgot what was there before the edit. + /// Looks up a localized string similar to Message edited from {0} in channel {1}, but I forgot what was there before the edit: . /// internal static string UncachedMessageEdited { get { @@ -663,15 +627,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Unknown command! {0}. - /// - internal static string UnknownCommand { - get { - return ResourceManager.GetString("UnknownCommand", resourceCulture); - } - } - /// /// Looks up a localized string similar to :white_check_mark: Successfully unmuted {0} for {1}. /// diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 1714f55..6f8ac2e 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -40,7 +40,7 @@ Too many mentions in 1 message - Message edited from {0} in channel {1}, but I forgot what was there before the edit + Message edited from {0} in channel {1}, but I forgot what was there before the edit: Message edited from {0} in channel {1}.{2}Before:{3}{4}{5}After:{6}{7} @@ -57,30 +57,6 @@ Beep! - - Invalid admin log channel for guild - - - You must set up a mute role in settings! - - - Command execution was unsuccessful: {0} - - - Repeated arguments detected! {0} - - - Command parsing failed: {0} - - - Unknown command! {0} - - - Invalid argument count! {0} - - - Arguments not present! {0} - I do not have permission to execute this command! @@ -141,9 +117,6 @@ Member is already muted! - - Someone removed the mute role manually! - Not specified @@ -186,15 +159,6 @@ Settings successfully updated - - Invalid argument! 'true' or 'false' required! - - - Invalid role specified! - - - Invalid channel specified! - Yes @@ -252,4 +216,25 @@ {0}This punishment will expire <t:{1}:R> + + Invalid setting value specified! + + + This role does not exist! + + + This channel does not exist! + + + I couldn't parse the specified duration! One of the components could be outside it's valid range (e.g. `24h` or `60m`) + + + I couldn't remove role {0} because of an error! {1} + + + I cannot mute someone forever using timeouts! Either specify a proper duration, or set a mute role in settings + + + I cannot use time-outs on other bots! Try to set a mute role in settings + \ No newline at end of file diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 7ff5cd2..0aad39e 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -31,7 +31,7 @@ Слишком много упоминаний в одном сообщении - Отредактировано сообщение от {0} в канале {1}, но я забыл что там было до редактирования + Отредактировано сообщение от {0} в канале {1}, но я забыл что там было до редактирования: Отредактировано сообщение от {0} в канале {1}.{2}До:{3}{4}{5}После:{6}{7} @@ -48,30 +48,6 @@ Бип! - - Требуется указать роль мута в настройках! - - - Неверный канал админ-логов для гильдии - - - Выполнение команды завершилось неудачей: {0} - - - Обнаружены повторяющиеся типы аргументов! {0} - - - Не удалось обработать команду: {0} - - - Неизвестная команда! {0} - - - Неверное количество аргументов! {0} - - - Нету нужных аргументов! {0} - У меня недостаточно прав для выполнения этой команды! @@ -132,9 +108,6 @@ Участник уже заглушен! - - Кто-то убрал роль мута самостоятельно! - Не указан @@ -177,15 +150,6 @@ Настройки успешно обновлены! - - Неверный параметр! Требуется 'true' или 'false' - - - Указана недействительная роль! - - - Указан недействильный канал! - Да @@ -243,4 +207,25 @@ {0}Это наказание истечёт <t:{1}:R> + + Указано недействительное значение для настройки! + + + Эта роль не существует! + + + Этот канал не существует! + + + Мне не удалось обработать продолжительность! Один из компонентов может быть за пределами допустимого диапазона (например, `24ч` или `60м`) + + + Я не смог забрать роль {0} в связи с ошибкой! {1} + + + Я не могу заглушить кого-то навсегда, используя тайм-ауты! Или укажи правильную продолжительность, или установи роль мута в настройках + + + Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках + \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index bfc3756..8795c82 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -27,12 +27,12 @@ public static class Utils { } public static string Wrap(string original) { - var toReturn = original.Replace("```", "​`​`​`​"); + var toReturn = original.Replace("```", "ˋˋˋ"); return $"```{toReturn}{(toReturn.EndsWith("`") || toReturn.Trim().Equals("") ? " " : "")}```"; } public static string WrapInline(string original) { - return $"`{original}`"; + return $"`{original.Replace("`", "ˋ")}`"; } public static string MentionChannel(ulong id) { @@ -64,7 +64,7 @@ public static class Utils { return await Boyfriend.Client.GetChannelAsync(ParseMention(mention)); } - public static async Task ParseChannelNullable(string mention) { + private static async Task ParseChannelNullable(string mention) { return ParseMentionNullable(mention) == null ? null : await ParseChannel(mention); } @@ -72,10 +72,6 @@ public static class Utils { return guild.GetRole(ParseMention(mention)); } - public static IRole? ParseRoleNullable(IGuild guild, string mention) { - return ParseMentionNullable(mention) == null ? null : ParseRole(guild, mention); - } - public static async Task SendDirectMessage(IUser user, string toSend) { try { await user.SendMessageAsync(toSend); @@ -98,8 +94,7 @@ public static class Utils { } catch (ArgumentException) {} } public static TimeSpan GetTimeSpan(string from) { - return TimeSpan.ParseExact(from.ToLowerInvariant(), Formats, - CultureInfo.InvariantCulture); + return TimeSpan.ParseExact(from.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture); } public static string JoinString(string[] args, int startIndex) { @@ -115,4 +110,4 @@ public static class Utils { options.AuditLogReason = reason; return options; } -} +} \ No newline at end of file From 790f77aa496527ea087cb809464feb2e8da34a88 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Sat, 14 May 2022 18:12:24 +0500 Subject: [PATCH 011/329] another two or three refactors --- Boyfriend-CSharp.sln | 9 + Boyfriend/Boyfriend.cs | 107 +++-- Boyfriend/Boyfriend.csproj | 6 +- Boyfriend/CommandHandler.cs | 122 +++--- Boyfriend/Commands/BanCommand.cs | 101 +++-- Boyfriend/Commands/ClearCommand.cs | 62 ++- Boyfriend/Commands/Command.cs | 26 +- Boyfriend/Commands/HelpCommand.cs | 34 +- Boyfriend/Commands/KickCommand.cs | 67 ++-- Boyfriend/Commands/MuteCommand.cs | 159 ++++---- Boyfriend/Commands/PingCommand.cs | 26 +- Boyfriend/Commands/SettingsCommand.cs | 219 ++++++---- Boyfriend/Commands/UnbanCommand.cs | 65 ++- Boyfriend/Commands/UnmuteCommand.cs | 103 ++--- Boyfriend/EventHandler.cs | 163 ++++++-- Boyfriend/GuildConfig.cs | 55 --- Boyfriend/Messages.Designer.cs | 552 +++++++++++++++++--------- Boyfriend/Messages.resx | 184 ++++++--- Boyfriend/Messages.ru.resx | 184 ++++++--- Boyfriend/Utils.cs | 147 ++++--- UpgradeLog.htm | Bin 0 -> 32818 bytes 21 files changed, 1447 insertions(+), 944 deletions(-) delete mode 100644 Boyfriend/GuildConfig.cs create mode 100644 UpgradeLog.htm diff --git a/Boyfriend-CSharp.sln b/Boyfriend-CSharp.sln index 003c58b..6178bea 100644 --- a/Boyfriend-CSharp.sln +++ b/Boyfriend-CSharp.sln @@ -1,5 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32407.343 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Boyfriend", "Boyfriend\Boyfriend.csproj", "{21640A7A-75C2-4515-A1DF-CE8B6EEBD260}" EndProject Global @@ -13,4 +16,10 @@ Global {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Release|Any CPU.ActiveCfg = Release|Any CPU {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2EDBF9CE-35F0-4810-93F7-FE0159EFD865} + EndGlobalSection EndGlobal diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 96d4df8..8ff5ca8 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Collections.ObjectModel; +using System.Text; using Discord; using Discord.WebSocket; using Newtonsoft.Json; @@ -6,6 +7,8 @@ using Newtonsoft.Json; namespace Boyfriend; public static class Boyfriend { + public static readonly StringBuilder StringBuilder = new(); + private static readonly Dictionary GuildCache = new(); private static readonly DiscordSocketConfig Config = new() { MessageCacheSize = 250, @@ -13,8 +16,34 @@ public static class Boyfriend { }; public static readonly DiscordSocketClient Client = new(Config); + private static readonly Game Activity = new("Retrospecter - Genocide", ActivityType.Listening); - private static readonly Dictionary GuildConfigDictionary = new(); + private static readonly Dictionary> GuildConfigDictionary = new(); + private static readonly Dictionary>> RemovedRolesDictionary = + new(); + + private static readonly Dictionary EmptyGuildConfig = new(); + private static readonly Dictionary> EmptyRemovedRoles = new(); + + public static readonly Dictionary DefaultConfig = new() { + {"Lang", "en"}, + {"Prefix", "!"}, + {"RemoveRolesOnMute", "false"}, + {"SendWelcomeMessages", "true"}, + {"ReceiveStartupMessages", "false"}, + {"FrowningFace", "true"}, + {"WelcomeMessage", Messages.DefaultWelcomeMessage}, + {"EventStartedReceivers", "interested,role"}, + {"StarterRole", "0"}, + {"MuteRole", "0"}, + {"EventNotifyReceiverRole", "0"}, + {"AdminLogChannel", "0"}, + {"BotLogChannel", "0"}, + {"EventCreatedChannel", "0"}, + {"EventStartedChannel", "0"}, + {"EventCancelledChannel", "0"}, + {"EventCompletedChannel", "0"} + }; public static void Main() { Init().GetAwaiter().GetResult(); @@ -27,7 +56,7 @@ public static class Boyfriend { await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); - await Client.SetActivityAsync(new Game("Retrospecter - Expurgation", ActivityType.Listening)); + await Client.SetActivityAsync(Activity); new EventHandler().InitEvents(); @@ -40,37 +69,65 @@ public static class Boyfriend { return Task.CompletedTask; } - public static async Task SetupGuildConfigs() { - foreach (var guild in Client.Guilds) { - var path = "config_" + guild.Id + ".json"; - if (!File.Exists(path)) File.Create(path); + public static async Task WriteGuildConfig(ulong id) { + var json = JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented); + var removedRoles = JsonConvert.SerializeObject(RemovedRolesDictionary[id], Formatting.Indented); - var config = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(path)); - if (config == null) { - Messages.Culture = new CultureInfo("ru"); - config = new GuildConfig(guild.Id); - } - config.Validate(); - - GuildConfigDictionary.Add(config.Id.GetValueOrDefault(0), config); - } + await File.WriteAllTextAsync($"config_{id}.json", json); + await File.WriteAllTextAsync($"removedroles_{id}.json", removedRoles); } - public static GuildConfig GetGuildConfig(IGuild guild) { - Messages.Culture = new CultureInfo("ru"); + public static Dictionary GetGuildConfig(ulong id) { + if (!RemovedRolesDictionary.ContainsKey(id)) + RemovedRolesDictionary.Add(id, EmptyRemovedRoles); - var config = GuildConfigDictionary.ContainsKey(guild.Id) ? GuildConfigDictionary[guild.Id] - : new GuildConfig(guild.Id); - config.Validate(); + if (GuildConfigDictionary.ContainsKey(id)) return GuildConfigDictionary[id]; + + var path = $"config_{id}.json"; + + if (!File.Exists(path)) File.Create(path).Dispose(); + + var json = File.ReadAllText(path); + var config = JsonConvert.DeserializeObject>(json) ?? EmptyGuildConfig; + + foreach (var key in DefaultConfig.Keys) + if (!config.ContainsKey(key)) + config.Add(key, DefaultConfig[key]); + + foreach (var key in config.Keys) + if (!DefaultConfig.ContainsKey(key)) + config.Remove(key); + + GuildConfigDictionary.Add(id, config); return config; } - public static IGuild FindGuild(IMessageChannel channel) { + public static Dictionary> GetRemovedRoles(ulong id) { + if (RemovedRolesDictionary.ContainsKey(id)) return RemovedRolesDictionary[id]; + + var path = $"removedroles_{id}.json"; + + if (!File.Exists(path)) File.Create(path); + + var json = File.ReadAllText(path); + var removedRoles = JsonConvert.DeserializeObject>>(json) ?? + EmptyRemovedRoles; + + RemovedRolesDictionary.Add(id, removedRoles); + + return removedRoles; + } + + public static SocketGuild FindGuild(ulong channel) { + if (GuildCache.ContainsKey(channel)) return GuildCache[channel]; foreach (var guild in Client.Guilds) - if (guild.Channels.Any(x => x == channel)) - return guild; + foreach (var x in guild.Channels) { + if (x.Id != channel) continue; + GuildCache.Add(channel, guild); + return guild; + } throw new Exception(Messages.CouldntFindGuildByChannel); } -} \ No newline at end of file +} diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index cd969e0..a05d38d 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -15,8 +15,10 @@ - - + + + + diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs index 610b69a..7754d53 100644 --- a/Boyfriend/CommandHandler.cs +++ b/Boyfriend/CommandHandler.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Text; +using System.Text.RegularExpressions; using Boyfriend.Commands; using Discord; using Discord.Commands; @@ -7,75 +8,104 @@ using Discord.WebSocket; namespace Boyfriend; public static class CommandHandler { + public static readonly Command[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), new KickCommand(), new MuteCommand(), new PingCommand(), new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() }; + private static readonly Dictionary RegexCache = new(); + + public static readonly StringBuilder StackedReplyMessage = new(); + public static readonly StringBuilder StackedPublicFeedback = new(); + public static readonly StringBuilder StackedPrivateFeedback = new(); + +#pragma warning disable CA2211 + public static bool ConfigWriteScheduled = false; // HOW IT CAN BE PRIVATE???? +#pragma warning restore CA2211 + public static async Task HandleCommand(SocketUserMessage message) { + StackedReplyMessage.Clear(); + StackedPrivateFeedback.Clear(); + StackedPublicFeedback.Clear(); var context = new SocketCommandContext(Boyfriend.Client, message); + var guild = context.Guild; + var config = Boyfriend.GetGuildConfig(guild.Id); - foreach (var command in Commands) { - var regex = new Regex(Regex.Escape(Boyfriend.GetGuildConfig(context.Guild).Prefix!)); - if (!command.GetAliases().Contains(regex.Replace(message.Content, "", 1).Split()[0])) - continue; + Regex regex; + if (RegexCache.ContainsKey(config["Prefix"])) { + regex = RegexCache[config["Prefix"]]; + } else { + regex = new Regex(Regex.Escape(config["Prefix"])); + RegexCache.Add(config["Prefix"], regex); + } - var args = message.Content.Split().Skip(1).ToArray(); - try { - if (command.GetArgumentsAmountRequired() > args.Length) - throw new ApplicationException(string.Format(Messages.NotEnoughArguments, - command.GetArgumentsAmountRequired(), args.Length)); - await command.Run(context, args); - } catch (Exception e) { - var signature = e switch { - ApplicationException => ":x:", - UnauthorizedAccessException => ":no_entry_sign:", - _ => ":stop_sign:" - }; - var stacktrace = e.StackTrace; - var toSend = $"{signature} `{e.Message}`"; - if (stacktrace != null && e is not ApplicationException && e is not UnauthorizedAccessException) - toSend += $"{Environment.NewLine}{Utils.Wrap(stacktrace)}"; - await context.Channel.SendMessageAsync(toSend); - throw; + var list = message.Content.Split("\n"); + var currentLine = 0; + foreach (var line in list) { + currentLine++; + foreach (var command in Commands) { + if (!command.Aliases.Contains(regex.Replace(line, "", 1).ToLower().Split()[0])) + continue; + + await context.Channel.TriggerTypingAsync(); + + var args = line.Split().Skip(1).ToArray(); + + if (command.ArgsLengthRequired <= args.Length) + await command.Run(context, args); + else + StackedReplyMessage.AppendFormat(Messages.NotEnoughArguments, command.ArgsLengthRequired.ToString(), + args.Length.ToString()); + + if (currentLine != list.Length) continue; + if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfig(guild.Id); + await context.Message.ReplyAsync(StackedReplyMessage.ToString(), false, null, AllowedMentions.None); + + var adminChannel = Utils.GetAdminLogChannel(guild.Id); + var systemChannel = guild.SystemChannel; + if (adminChannel != null) + await Utils.SilentSendAsync(adminChannel, StackedPrivateFeedback.ToString()); + if (systemChannel != null) + await Utils.SilentSendAsync(systemChannel, StackedPublicFeedback.ToString()); } - - break; } } - public static async Task CheckPermissions(IGuildUser user, GuildPermission toCheck, + public static string HasPermission(ref SocketGuildUser user, GuildPermission toCheck, GuildPermission forBot = GuildPermission.StartEmbeddedActivities) { - var me = await user.Guild.GetCurrentUserAsync(); + var me = user.Guild.CurrentUser; + + if (user.Id == user.Guild.OwnerId || (me.GuildPermissions.Has(GuildPermission.Administrator) && + user.GuildPermissions.Has(GuildPermission.Administrator))) return ""; + if (forBot == GuildPermission.StartEmbeddedActivities) forBot = toCheck; - if (user.Id != user.Guild.OwnerId - && (!me.GuildPermissions.Has(GuildPermission.Administrator) - || !user.GuildPermissions.Has(GuildPermission.Administrator))) { - if (!me.GuildPermissions.Has(forBot)) - throw new UnauthorizedAccessException(Messages.CommandNoPermissionBot); - if (!user.GuildPermissions.Has(toCheck)) - throw new UnauthorizedAccessException(Messages.CommandNoPermissionUser); - } + if (!me.GuildPermissions.Has(forBot)) + return Messages.CommandNoPermissionBot; + + return !user.GuildPermissions.Has(toCheck) ? Messages.CommandNoPermissionUser : ""; } - public static async Task CheckInteractions(IGuildUser actor, IGuildUser target) { - var me = await target.Guild.GetCurrentUserAsync(); - + public static string CanInteract(ref SocketGuildUser actor, ref SocketGuildUser target) { if (actor.Guild != target.Guild) - throw new Exception(Messages.InteractionsDifferentGuilds); - if (actor.Id == actor.Guild.OwnerId) return; + return Messages.InteractionsDifferentGuilds; + if (actor.Id == actor.Guild.OwnerId) + return ""; if (target.Id == target.Guild.OwnerId) - throw new UnauthorizedAccessException(Messages.InteractionsOwner); + return Messages.InteractionsOwner; if (actor == target) - throw new UnauthorizedAccessException(Messages.InteractionsYourself); + return Messages.InteractionsYourself; + + var me = target.Guild.CurrentUser; + if (target == me) - throw new UnauthorizedAccessException(Messages.InteractionsMe); + return Messages.InteractionsMe; if (me.Hierarchy <= target.Hierarchy) - throw new UnauthorizedAccessException(Messages.InteractionsFailedBot); - if (actor.Hierarchy <= target.Hierarchy) - throw new UnauthorizedAccessException(Messages.InteractionsFailedUser); + return Messages.InteractionsFailedBot; + + return actor.Hierarchy <= target.Hierarchy ? Messages.InteractionsFailedUser : ""; } } diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 60e945f..80e8b0c 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -1,73 +1,72 @@ using Discord; using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global +using Discord.WebSocket; namespace Boyfriend.Commands; public class BanCommand : Command { - public override async Task Run(SocketCommandContext context, string[] args) { - var reason = Utils.JoinString(args, 1); + public override string[] Aliases { get; } = {"ban", "бан"}; + public override int ArgsLengthRequired => 2; - TimeSpan duration; - try { - duration = Utils.GetTimeSpan(args[1]); - reason = Utils.JoinString(args, 2); - } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { - await Warn(context.Channel as ITextChannel, Messages.DurationParseFailed); - duration = TimeSpan.FromMilliseconds(-1); + public override async Task Run(SocketCommandContext context, string[] args) { + var toBan = Utils.ParseUser(args[0]); + + if (toBan == null) { + Error(Messages.UserDoesntExist, false); + return; } - await BanUser(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), - await Utils.ParseUser(args[0]), duration, reason); + var guild = context.Guild; + var author = (SocketGuildUser) context.User; + + var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.BanMembers); + if (permissionCheckResponse != "") { + Error(permissionCheckResponse, true); + return; + } + + var reason = Utils.JoinString(ref args, 2); + var memberToBan = Utils.ParseMember(guild, args[0]); + + if (memberToBan != null) { + var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref memberToBan); + if (interactionCheckResponse != "") { + Error(interactionCheckResponse, true); + return; + } + } + + var duration = Utils.GetTimeSpan(ref args[1]) ?? TimeSpan.FromMilliseconds(-1); + if (duration.TotalSeconds < 0) { + Warn(Messages.DurationParseFailed); + reason = Utils.JoinString(ref args, 1); + } + + await BanUser(guild, author, toBan, duration, reason); } - public static async Task BanUser(IGuild guild, ITextChannel? channel, IGuildUser author, IUser toBan, - TimeSpan duration, string reason) { - var authorMention = author.Mention; - var guildBanMessage = $"({Utils.GetNameAndDiscrim(author)}) {reason}"; - var memberToBan = await guild.GetUserAsync(toBan.Id); - var expiresIn = duration.TotalSeconds > 0 ? string.Format(Messages.PunishmentExpiresIn, Environment.NewLine, - DateTimeOffset.Now.ToUnixTimeSeconds() + duration.TotalSeconds) : ""; - var notification = string.Format(Messages.UserBanned, authorMention, toBan.Mention, Utils.WrapInline(reason), - expiresIn); - - await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); - if (memberToBan != null) - await CommandHandler.CheckInteractions(author, memberToBan); + public static async Task BanUser(SocketGuild guild, SocketGuildUser author, SocketUser toBan, TimeSpan duration, + string reason) { + var guildBanMessage = $"({author}) {reason}"; await Utils.SendDirectMessage(toBan, string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.WrapInline(reason))); await guild.AddBanAsync(toBan, 0, guildBanMessage); - await Utils.SilentSendAsync(channel, - string.Format(Messages.BanResponse, toBan.Mention, Utils.WrapInline(reason))); - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); + var feedback = string.Format(Messages.FeedbackUserBanned, toBan.Mention, + Utils.GetHumanizedTimeOffset(ref duration), Utils.WrapInline(reason)); + Success(feedback, author.Mention, false, false); + await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); if (duration.TotalSeconds > 0) { - await Task.Run(async () => { + async void DelayUnban() { await Task.Delay(duration); - try { - await UnbanCommand.UnbanUser(guild, null, await guild.GetCurrentUserAsync(), toBan, - Messages.PunishmentExpired); - } catch (ApplicationException) {} - }); + await UnbanCommand.UnbanUser(guild, guild.CurrentUser, toBan, Messages.PunishmentExpired); + } + + var task = new Task(DelayUnban); + task.Start(); } } - - public override List GetAliases() { - return new List {"ban", "бан"}; - } - - public override int GetArgumentsAmountRequired() { - return 2; - } - - public override string GetSummary() { - return "Банит пользователя"; - } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs index 473d8d6..504c396 100644 --- a/Boyfriend/Commands/ClearCommand.cs +++ b/Boyfriend/Commands/ClearCommand.cs @@ -1,54 +1,46 @@ using Discord; using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global +using Discord.WebSocket; namespace Boyfriend.Commands; public class ClearCommand : Command { - public override async Task Run(SocketCommandContext context, string[] args) { - var user = context.User; + public override string[] Aliases { get; } = {"clear", "purge", "очистить", "стереть"}; + public override int ArgsLengthRequired => 1; - int toDelete; - try { - toDelete = Convert.ToInt32(args[0]); - } catch (Exception e) when (e is FormatException or OverflowException) { - throw new ApplicationException(Messages.ClearInvalidAmountSpecified); + public override async Task Run(SocketCommandContext context, string[] args) { + var user = (SocketGuildUser) context.User; + + if (context.Channel is not SocketTextChannel channel) throw new Exception(); + + var permissionCheckResponse = CommandHandler.HasPermission(ref user, GuildPermission.ManageMessages); + if (permissionCheckResponse != "") { + Error(permissionCheckResponse, true); + return; } - if (context.Channel is not ITextChannel channel) return; - - await CommandHandler.CheckPermissions(context.Guild.GetUser(user.Id), GuildPermission.ManageMessages); + if (!int.TryParse(args[0], out var toDelete)) { + Error(Messages.ClearInvalidAmountSpecified, false); + return; + } switch (toDelete) { case < 1: - throw new ApplicationException(Messages.ClearNegativeAmount); + Error(Messages.ClearNegativeAmount, false); + break; case > 200: - throw new ApplicationException(Messages.ClearAmountTooLarge); - default: { + Error(Messages.ClearAmountTooLarge, false); + break; + default: var messages = await channel.GetMessagesAsync(toDelete + 1).FlattenAsync(); - await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(Utils.GetNameAndDiscrim(user))); + await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(user.ToString()!)); + + await Utils.SendFeedback( + string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString(), channel.Mention), + context.Guild.Id, user.Mention); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(context.Guild), - string.Format(Messages.MessagesDeleted, user.Mention, toDelete + 1, - Utils.MentionChannel(context.Channel.Id))); break; - } } } - - public override List GetAliases() { - return new List {"clear", "purge", "очистить", "стереть"}; - } - - public override int GetArgumentsAmountRequired() { - return 1; - } - - public override string GetSummary() { - return "Очищает сообщения"; - } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/Command.cs b/Boyfriend/Commands/Command.cs index af2c845..4903177 100644 --- a/Boyfriend/Commands/Command.cs +++ b/Boyfriend/Commands/Command.cs @@ -1,18 +1,32 @@ -using Discord; +using System.Text; using Discord.Commands; namespace Boyfriend.Commands; public abstract class Command { + + public abstract string[] Aliases { get; } + + public abstract int ArgsLengthRequired { get; } public abstract Task Run(SocketCommandContext context, string[] args); - public abstract List GetAliases(); + protected static void Output(ref StringBuilder message) { + CommandHandler.StackedReplyMessage.Append(message).AppendLine(); + } - public abstract int GetArgumentsAmountRequired(); + protected static void Success(string message, string userMention, bool sendPublicFeedback = false, + bool sendPrivateFeedback = true) { + CommandHandler.StackedReplyMessage.Append(":white_check_mark: ").AppendLine(message); + if (sendPrivateFeedback) + Utils.StackFeedback(ref message, ref userMention, sendPublicFeedback); + } - public abstract string GetSummary(); + protected static void Warn(string message) { + CommandHandler.StackedReplyMessage.Append(":warning: ").AppendLine(message); + } - protected static async Task Warn(ITextChannel? channel, string warning) { - await Utils.SilentSendAsync(channel, ":warning: " + warning); + protected static void Error(string message, bool accessDenied) { + var symbol = accessDenied ? ":no_entry_sign: " : ":x: "; + CommandHandler.StackedReplyMessage.Append(symbol).AppendLine(message); } } \ No newline at end of file diff --git a/Boyfriend/Commands/HelpCommand.cs b/Boyfriend/Commands/HelpCommand.cs index 50b9818..656fa48 100644 --- a/Boyfriend/Commands/HelpCommand.cs +++ b/Boyfriend/Commands/HelpCommand.cs @@ -1,30 +1,22 @@ using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global +using Humanizer; namespace Boyfriend.Commands; public class HelpCommand : Command { - public override async Task Run(SocketCommandContext context, string[] args) { - var nl = Environment.NewLine; - var prefix = Boyfriend.GetGuildConfig(context.Guild).Prefix; - var toSend = string.Format(Messages.CommandHelp, nl); + public override string[] Aliases { get; } = {"help", "помощь", "справка"}; + public override int ArgsLengthRequired => 0; - toSend = CommandHandler.Commands.Aggregate(toSend, - (current, command) => current + $"`{prefix}{command.GetAliases()[0]}`: {command.GetSummary()}{nl}"); - await context.Channel.SendMessageAsync(toSend); - } + public override Task Run(SocketCommandContext context, string[] args) { + var prefix = Boyfriend.GetGuildConfig(context.Guild.Id)["Prefix"]; + var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp); - public override List GetAliases() { - return new List {"help", "помощь", "справка"}; - } + foreach (var command in CommandHandler.Commands) + toSend.Append( + $"\n`{prefix}{command.Aliases[0]}`: {Utils.GetMessage($"CommandDescription{command.Aliases[0].Titleize()}")}"); + Output(ref toSend); + toSend.Clear(); - public override int GetArgumentsAmountRequired() { - return 0; + return Task.CompletedTask; } - - public override string GetSummary() { - return "Показывает эту справку"; - } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index 0145ebc..ffa61eb 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -1,50 +1,49 @@ using Discord; using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global +using Discord.WebSocket; namespace Boyfriend.Commands; public class KickCommand : Command { + public override string[] Aliases { get; } = {"kick", "кик", "выгнать"}; + public override int ArgsLengthRequired => 2; + public override async Task Run(SocketCommandContext context, string[] args) { - var author = context.Guild.GetUser(context.User.Id); - var toKick = await Utils.ParseMember(context.Guild, args[0]); + var author = (SocketGuildUser) context.User; - await CommandHandler.CheckPermissions(author, GuildPermission.KickMembers); - await CommandHandler.CheckInteractions(author, toKick); + var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.KickMembers); + if (permissionCheckResponse != "") { + Error(permissionCheckResponse, true); + return; + } - await KickMember(context.Guild, context.Channel as ITextChannel, author, toKick, Utils.JoinString(args, 1)); + var toKick = Utils.ParseMember(context.Guild, args[0]); + + if (toKick == null) { + Error(Messages.UserNotInGuild, false); + return; + } + + var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toKick); + if (interactionCheckResponse != "") { + Error(interactionCheckResponse, true); + return; + } + + await KickMember(context.Guild, author, toKick, Utils.JoinString(ref args, 1)); + + Success( + string.Format(Messages.FeedbackMemberKicked, toKick.Mention, + Utils.WrapInline(Utils.JoinString(ref args, 1))), author.Mention); } - private static async Task KickMember(IGuild guild, ITextChannel? channel, IUser author, IGuildUser toKick, - string reason) { + private static async Task KickMember(IGuild guild, SocketUser author, SocketGuildUser toKick, string reason) { var authorMention = author.Mention; - var guildKickMessage = $"({Utils.GetNameAndDiscrim(author)}) {reason}"; - var notification = string.Format(Messages.MemberKicked, authorMention, toKick.Mention, - Utils.WrapInline(reason)); + var guildKickMessage = $"({author}) {reason}"; - await Utils.SendDirectMessage(toKick, string.Format(Messages.YouWereKicked, authorMention, guild.Name, - Utils.WrapInline(reason))); + await Utils.SendDirectMessage(toKick, + string.Format(Messages.YouWereKicked, authorMention, guild.Name, Utils.WrapInline(reason))); await toKick.KickAsync(guildKickMessage); - - await Utils.SilentSendAsync(channel, string.Format(Messages.KickResponse, toKick.Mention, - Utils.WrapInline(reason))); - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); } - - public override List GetAliases() { - return new List {"kick", "кик"}; - } - - public override int GetArgumentsAmountRequired() { - return 2; - } - - public override string GetSummary() { - return "Выгоняет участника"; - } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index 4ad55e8..144d76f 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -1,117 +1,122 @@ using Discord; using Discord.Commands; using Discord.Net; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global +using Discord.WebSocket; namespace Boyfriend.Commands; public class MuteCommand : Command { - public override async Task Run(SocketCommandContext context, string[] args) { - var author = context.Guild.GetUser(context.User.Id); - var config = Boyfriend.GetGuildConfig(context.Guild); - var reason = Utils.JoinString(args, 1); - var role = Utils.GetMuteRole(context.Guild); - var rolesRemoved = config.RolesRemovedOnMute!; - var toMute = await Utils.ParseMember(context.Guild, args[0]); + public override string[] Aliases { get; } = {"mute", "timeout", "заглушить", "мут"}; + public override int ArgsLengthRequired => 2; - TimeSpan duration; - try { - duration = Utils.GetTimeSpan(args[1]); - reason = Utils.JoinString(args, 2); - } catch (Exception e) when (e is ArgumentNullException or FormatException or OverflowException) { - await Warn(context.Channel as ITextChannel, Messages.DurationParseFailed); - duration = TimeSpan.FromMilliseconds(-1); + public override async Task Run(SocketCommandContext context, string[] args) { + var toMute = Utils.ParseMember(context.Guild, args[0]); + var reason = Utils.JoinString(ref args, 2); + + var duration = Utils.GetTimeSpan(ref args[1]) ?? TimeSpan.FromMilliseconds(-1); + if (duration.TotalSeconds < 0) { + Warn(Messages.DurationParseFailed); + reason = Utils.JoinString(ref args, 1); } - if (toMute == null) - throw new ApplicationException(Messages.UserNotInGuild); + if (toMute == null) { + Error(Messages.UserNotInGuild, false); + return; + } - if (role != null && toMute.RoleIds.Any(x => x == role.Id) || toMute.TimedOutUntil != null && - toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > DateTimeOffset.Now.ToUnixTimeMilliseconds()) - throw new ApplicationException(Messages.MemberAlreadyMuted); + var guild = context.Guild; + var role = Utils.GetMuteRole(ref guild); + + if (role != null) { + var hasMuteRole = false; + foreach (var x in toMute.Roles) { + if (x != role) continue; + hasMuteRole = true; + break; + } + + if (hasMuteRole || (toMute.TimedOutUntil != null && toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > + DateTimeOffset.Now.ToUnixTimeMilliseconds())) { + Error(Messages.MemberAlreadyMuted, false); + return; + } + } + + var rolesRemoved = Boyfriend.GetRemovedRoles(context.Guild.Id); if (rolesRemoved.ContainsKey(toMute.Id)) { foreach (var roleId in rolesRemoved[toMute.Id]) await toMute.AddRoleAsync(roleId); rolesRemoved.Remove(toMute.Id); - await config.Save(); - throw new ApplicationException(Messages.RolesReturned); + CommandHandler.ConfigWriteScheduled = true; + Warn(Messages.RolesReturned); } - await CommandHandler.CheckPermissions(author, GuildPermission.ModerateMembers, GuildPermission.ManageRoles); - await CommandHandler.CheckInteractions(author, toMute); + var author = (SocketGuildUser) context.User; - await MuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), toMute, - duration, reason); + var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ModerateMembers); + if (permissionCheckResponse != "") { + Error(permissionCheckResponse, true); + return; + } + + var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toMute); + if (interactionCheckResponse != "") { + Error(interactionCheckResponse, true); + return; + } + + await MuteMember(guild, author, toMute, duration, reason); + + Success( + string.Format(Messages.FeedbackMemberMuted, toMute.Mention, Utils.GetHumanizedTimeOffset(ref duration), + Utils.WrapInline(reason)), author.Mention, true); } - private static async Task MuteMember(IGuild guild, ITextChannel? channel, IGuildUser author, IGuildUser toMute, + private static async Task MuteMember(SocketGuild guild, SocketUser author, SocketGuildUser toMute, TimeSpan duration, string reason) { - await CommandHandler.CheckPermissions(author, GuildPermission.ManageMessages, GuildPermission.ManageRoles); - var authorMention = author.Mention; - var config = Boyfriend.GetGuildConfig(guild); - var requestOptions = Utils.GetRequestOptions($"({Utils.GetNameAndDiscrim(author)}) {reason}"); - var role = Utils.GetMuteRole(guild); + var config = Boyfriend.GetGuildConfig(guild.Id); + var requestOptions = Utils.GetRequestOptions($"({author}) {reason}"); + var role = Utils.GetMuteRole(ref guild); var hasDuration = duration.TotalSeconds > 0; - var expiresIn = hasDuration ? string.Format(Messages.PunishmentExpiresIn, Environment.NewLine, - DateTimeOffset.Now.ToUnixTimeSeconds() + duration.TotalSeconds) : ""; - var notification = string.Format(Messages.MemberMuted, authorMention, toMute.Mention, Utils.WrapInline(reason), - expiresIn); if (role != null) { - if (config.RemoveRolesOnMute.GetValueOrDefault(false)) { + if (config["RemoveRolesOnMute"] == "true") { var rolesRemoved = new List(); - foreach (var roleId in toMute.RoleIds) { + foreach (var userRole in toMute.Roles) try { - if (roleId == guild.Id) continue; - if (roleId == role.Id) continue; - await toMute.RemoveRoleAsync(roleId); - rolesRemoved.Add(roleId); + if (userRole == guild.EveryoneRole || userRole == role) continue; + await toMute.RemoveRoleAsync(role); + rolesRemoved.Add(userRole.Id); } catch (HttpException e) { - await Warn(channel, - string.Format(Messages.RoleRemovalFailed, $"<@&{roleId}>", Utils.WrapInline(e.Reason))); + Warn(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.WrapInline(e.Reason))); } - } - config.RolesRemovedOnMute!.Add(toMute.Id, rolesRemoved); - await config.Save(); + Boyfriend.GetRemovedRoles(guild.Id).Add(toMute.Id, rolesRemoved.AsReadOnly()); + CommandHandler.ConfigWriteScheduled = true; - if (hasDuration) - await Task.Run(async () => { + if (hasDuration) { + async void DelayUnmute() { await Task.Delay(duration); - try { - await UnmuteCommand.UnmuteMember(guild, null, await guild.GetCurrentUserAsync(), toMute, - Messages.PunishmentExpired); - } catch (ApplicationException) {} - }); + await UnmuteCommand.UnmuteMember(guild, guild.CurrentUser, toMute, Messages.PunishmentExpired); + } + + var task = new Task(DelayUnmute); + task.Start(); + } } await toMute.AddRoleAsync(role, requestOptions); } else { - if (!hasDuration) - throw new ApplicationException(Messages.DurationRequiredForTimeOuts); - if (toMute.IsBot) - throw new ApplicationException(Messages.CannotTimeOutBot); + if (!hasDuration) { + Error(Messages.DurationRequiredForTimeOuts, false); + return; + } + if (toMute.IsBot) { + Error(Messages.CannotTimeOutBot, false); + return; + } await toMute.SetTimeOutAsync(duration, requestOptions); } - await Utils.SilentSendAsync(channel, - string.Format(Messages.MuteResponse, toMute.Mention, Utils.WrapInline(reason))); - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - } - - public override List GetAliases() { - return new List {"mute", "мут", "мьют"}; - } - - public override int GetArgumentsAmountRequired() { - return 2; - } - - public override string GetSummary() { - return "Глушит участника"; } } \ No newline at end of file diff --git a/Boyfriend/Commands/PingCommand.cs b/Boyfriend/Commands/PingCommand.cs index ae0c4af..c5189e4 100644 --- a/Boyfriend/Commands/PingCommand.cs +++ b/Boyfriend/Commands/PingCommand.cs @@ -1,25 +1,19 @@ using Discord.Commands; -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; public class PingCommand : Command { - public override async Task Run(SocketCommandContext context, string[] args) { - await context.Channel.SendMessageAsync($"{Utils.GetBeep(Boyfriend.GetGuildConfig(context.Guild).Lang!)}" + - $"{Boyfriend.Client.Latency}{Messages.Milliseconds}"); - } + public override string[] Aliases { get; } = {"ping", "latency", "pong", "пинг", "задержка", "понг"}; + public override int ArgsLengthRequired => 0; - public override List GetAliases() { - return new List {"ping", "пинг", "задержка"}; - } + public override Task Run(SocketCommandContext context, string[] args) { + var builder = Boyfriend.StringBuilder; - public override int GetArgumentsAmountRequired() { - return 0; - } + builder.Append(Utils.GetBeep()).Append(Boyfriend.Client.Latency).Append(Messages.Milliseconds); - public override string GetSummary() { - return "Измеряет время обработки REST-запроса"; + Output(ref builder); + builder.Clear(); + + return Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index c0e189a..368c5d8 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -1,111 +1,156 @@ -using System.Reflection; -using Discord; +using Discord; using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global +using Discord.WebSocket; namespace Boyfriend.Commands; public class SettingsCommand : Command { - public override async Task Run(SocketCommandContext context, string[] args) { - var config = Boyfriend.GetGuildConfig(context.Guild); - var guild = context.Guild; + public override string[] Aliases { get; } = {"settings", "config", "настройки", "конфиг"}; + public override int ArgsLengthRequired => 0; - await CommandHandler.CheckPermissions(context.Guild.GetUser(context.User.Id), GuildPermission.ManageGuild); + public override Task Run(SocketCommandContext context, string[] args) { + var author = (SocketGuildUser) context.User; + + var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ManageGuild); + if (permissionCheckResponse != "") { + Error(permissionCheckResponse, true); + return Task.CompletedTask; + } + + var guild = context.Guild; + var config = Boyfriend.GetGuildConfig(guild.Id); if (args.Length == 0) { - var nl = Environment.NewLine; - dynamic forCheck; - var adminLogChannel = (forCheck = guild.GetTextChannel(config.AdminLogChannel.GetValueOrDefault(0))) == null - ? Messages.ChannelNotSpecified : forCheck.Mention; - var botLogChannel = (forCheck = guild.GetTextChannel(config.BotLogChannel.GetValueOrDefault(0))) == null - ? Messages.ChannelNotSpecified : forCheck.Mention; - var muteRole = (forCheck = guild.GetRole(config.MuteRole.GetValueOrDefault(0))) == null - ? Messages.RoleNotSpecified : forCheck.Mention; - var defaultRole = (forCheck = guild.GetRole(config.DefaultRole.GetValueOrDefault(0))) == null - ? Messages.RoleNotSpecified : forCheck.Mention; - var toSend = string.Format(Messages.CurrentSettings, nl) + - string.Format(Messages.CurrentSettingsLang, config.Lang, nl) + - string.Format(Messages.CurrentSettingsPrefix, config.Prefix, nl) + - string.Format(Messages.CurrentSettingsRemoveRoles, - YesOrNo(config.RemoveRolesOnMute.GetValueOrDefault(false)), nl) + - string.Format(Messages.CurrentSettingsUseSystemChannel, - YesOrNo(config.UseSystemChannel.GetValueOrDefault(true)), nl) + - string.Format(Messages.CurrentSettingsSendWelcomeMessages, - YesOrNo(config.SendWelcomeMessages.GetValueOrDefault(true)), nl) + - string.Format(Messages.CurrentSettingsReceiveStartupMessages, - YesOrNo(config.ReceiveStartupMessages.GetValueOrDefault(true)), nl) + - string.Format(Messages.CurrentSettingsWelcomeMessage, config.WelcomeMessage, nl) + - string.Format(Messages.CurrentSettingsDefaultRole, defaultRole, nl) + - string.Format(Messages.CurrentSettingsMuteRole, muteRole, nl) + - string.Format(Messages.CurrentSettingsAdminLogChannel, adminLogChannel, nl) + - string.Format(Messages.CurrentSettingsBotLogChannel, botLogChannel); - await Utils.SilentSendAsync(context.Channel as ITextChannel ?? throw new ApplicationException(), toSend); - return; - } + var currentSettings = Boyfriend.StringBuilder.AppendLine(Messages.CurrentSettings); - var setting = args[0].ToLower(); - var value = ""; + foreach (var setting in Boyfriend.DefaultConfig) { + var format = "{0}"; + var currentValue = config[setting.Key]; - if (args.Length >= 2) - try { - value = args[1].ToLower(); - } catch (IndexOutOfRangeException) { - throw new ApplicationException(Messages.InvalidSettingValue); + if (setting.Key.EndsWith("Channel")) { + if (guild.GetTextChannel(Convert.ToUInt64(currentValue)) != null) + format = "<#{0}>"; + else + currentValue = Messages.ChannelNotSpecified; + } else if (setting.Key.EndsWith("Role")) { + if (guild.GetRole(Convert.ToUInt64(currentValue)) != null) + format = "<@&{0}>"; + else + currentValue = Messages.RoleNotSpecified; + } else { + if (IsBool(currentValue)) + currentValue = YesOrNo(currentValue == "true"); + else + format = Utils.WrapInline("{0}")!; + } + + currentSettings.Append($"{Utils.GetMessage($"Settings{setting.Key}")} (`{setting.Key}`): ") + .AppendFormat(format, currentValue).AppendLine(); } - PropertyInfo? property = null; - foreach (var prop in typeof(GuildConfig).GetProperties()) - if (setting == prop.Name.ToLower()) - property = prop; - if (property == null || !property.CanWrite) - throw new ApplicationException(Messages.SettingDoesntExist); - var type = property.PropertyType; + Output(ref currentSettings); + currentSettings.Clear(); + return Task.CompletedTask; + } + + var selectedSetting = args[0].ToLower(); + + var exists = false; + foreach (var setting in Boyfriend.DefaultConfig) { + if (selectedSetting != setting.Key.ToLower()) continue; + selectedSetting = setting.Key; + exists = true; + break; + } + + if (!exists) { + Error(Messages.SettingDoesntExist, false); + return Task.CompletedTask; + } + + string value; + + if (args.Length >= 2) { + value = Utils.JoinString(ref args, 1); + if (selectedSetting != "WelcomeMessage") + value = value.Replace(" ", "").ToLower(); + if (value.StartsWith(",") || value.Count(x => x == ',') > 1) { + Error(Messages.InvalidSettingValue, false); + return Task.CompletedTask; + } + } else { + value = "reset"; + } + + if (IsBool(Boyfriend.DefaultConfig[selectedSetting]) && !IsBool(value)) { + value = value switch { + "y" or "yes" => "true", + "n" or "no" => "false", + _ => value + }; + if (!IsBool(value)) { + Error(Messages.InvalidSettingValue, false); + return Task.CompletedTask; + } + } + + var localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); + + var mention = Utils.ParseMention(value); + if (mention != 0) value = mention.ToString(); + + var formatting = Utils.WrapInline("{0}")!; + if (selectedSetting.EndsWith("Channel")) + formatting = "<#{0}>"; + if (selectedSetting.EndsWith("Role")) + formatting = "<@&{0}>"; + if (value is "0" or "reset" or "default") + formatting = Messages.SettingNotDefined; + var formattedValue = IsBool(value) ? YesOrNo(value == "true") : string.Format(formatting, value); if (value is "reset" or "default") { - property.SetValue(config, null); - } else if (type == typeof(string)) { - if (setting == "lang" && value is not ("ru" or "en")) - throw new ApplicationException(Messages.LanguageNotSupported); - property.SetValue(config, value); + config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; } else { - try { - if (type == typeof(bool?)) - property.SetValue(config, Convert.ToBoolean(value)); - - if (type == typeof(ulong?)) { - var id = Convert.ToUInt64(value); - if (property.Name.EndsWith("Channel") && guild.GetTextChannel(id) == null) - throw new ApplicationException(Messages.InvalidChannel); - if (property.Name.EndsWith("Role") && guild.GetRole(id) == null) - throw new ApplicationException(Messages.InvalidRole); - - property.SetValue(config, id); - } - } catch (Exception e) when (e is FormatException or OverflowException) { - throw new ApplicationException(Messages.InvalidSettingValue); + if (value == config[selectedSetting]) { + Error(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), false); + return Task.CompletedTask; } - } - config.Validate(); - await config.Save(); - await context.Channel.SendMessageAsync(Messages.SettingsUpdated); + if (selectedSetting == "Lang" && value is not "ru" and not "en") { + Error(Messages.LanguageNotSupported, false); + return Task.CompletedTask; + } + + if (selectedSetting.EndsWith("Channel") && guild.GetTextChannel(mention) == null) { + Error(Messages.InvalidChannel, false); + return Task.CompletedTask; + } + + if (selectedSetting.EndsWith("Role") && guild.GetRole(mention) == null) { + Error(Messages.InvalidRole, false); + return Task.CompletedTask; + } + + config[selectedSetting] = value; + } + + if (selectedSetting == "Lang") { + Utils.SetCurrentLanguage(guild.Id); + localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); + } + + CommandHandler.ConfigWriteScheduled = true; + + Success(string.Format(Messages.FeedbackSettingsUpdated, localizedSelectedSetting, formattedValue), + author.Mention); + return Task.CompletedTask; } private static string YesOrNo(bool isYes) { return isYes ? Messages.Yes : Messages.No; } - public override List GetAliases() { - return new List {"settings", "настройки", "config", "конфиг "}; + private static bool IsBool(string value) { + return value is "true" or "false"; } - - public override int GetArgumentsAmountRequired() { - return 0; - } - - public override string GetSummary() { - return "Настраивает бота отдельно для этого сервера"; - } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index 69874de..8741a3e 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -1,48 +1,45 @@ using Discord; using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global +using Discord.WebSocket; namespace Boyfriend.Commands; public class UnbanCommand : Command { + public override string[] Aliases { get; } = {"unban", "разбан"}; + public override int ArgsLengthRequired => 2; + public override async Task Run(SocketCommandContext context, string[] args) { - await UnbanUser(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), - await Utils.ParseUser(args[0]), Utils.JoinString(args, 1)); + var author = (SocketGuildUser) context.User; + + var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.BanMembers); + if (permissionCheckResponse != "") { + Error(permissionCheckResponse, true); + return; + } + + var toUnban = Utils.ParseUser(args[0]); + + if (toUnban == null) { + Error(Messages.UserDoesntExist, false); + return; + } + + var reason = Utils.JoinString(ref args, 1); + + await UnbanUser(context.Guild, author, toUnban, reason); } - public static async Task UnbanUser(IGuild guild, ITextChannel? channel, IGuildUser author, IUser toUnban, - string reason) { - - var authorMention = author.Mention; - var notification = string.Format(Messages.UserUnbanned, authorMention, toUnban.Mention, - Utils.WrapInline(reason)); - var requestOptions = Utils.GetRequestOptions($"({Utils.GetNameAndDiscrim(author)}) {reason}"); - - await CommandHandler.CheckPermissions(author, GuildPermission.BanMembers); - - if (guild.GetBanAsync(toUnban.Id) == null) - throw new ApplicationException(Messages.UserNotBanned); + public static async Task UnbanUser(SocketGuild guild, SocketGuildUser author, SocketUser toUnban, string reason) { + if (guild.GetBanAsync(toUnban.Id) == null) { + Error(Messages.UserNotBanned, false); + return; + } + var requestOptions = Utils.GetRequestOptions($"({author}) {reason}"); await guild.RemoveBanAsync(toUnban, requestOptions); - await Utils.SilentSendAsync(channel, string.Format(Messages.UnbanResponse, toUnban.Mention, - Utils.WrapInline(reason))); - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - } - - public override List GetAliases() { - return new List {"unban", "разбан"}; - } - - public override int GetArgumentsAmountRequired() { - return 2; - } - - public override string GetSummary() { - return "Возвращает пользователя из бана"; + var feedback = string.Format(Messages.FeedbackUserUnbanned, toUnban.Mention, Utils.WrapInline(reason)); + Success(feedback, author.Mention, false, false); + await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); } } diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index 9710d5a..e4af83b 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -1,71 +1,78 @@ using Discord; using Discord.Commands; - -// ReSharper disable UnusedType.Global -// ReSharper disable UnusedMember.Global -// ReSharper disable ClassNeverInstantiated.Global +using Discord.WebSocket; namespace Boyfriend.Commands; public class UnmuteCommand : Command { + public override string[] Aliases { get; } = {"unmute", "размут"}; + public override int ArgsLengthRequired => 2; + public override async Task Run(SocketCommandContext context, string[] args) { - await UnmuteMember(context.Guild, context.Channel as ITextChannel, context.Guild.GetUser(context.User.Id), - await Utils.ParseMember(context.Guild, args[0]), Utils.JoinString(args, 1)); + var author = (SocketGuildUser) context.User; + + var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ModerateMembers); + if (permissionCheckResponse != "") { + Error(permissionCheckResponse, true); + return; + } + + var toUnmute = Utils.ParseMember(context.Guild, args[0]); + + if (toUnmute == null) { + Error(Messages.UserDoesntExist, false); + return; + } + + var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toUnmute); + if (interactionCheckResponse != "") { + Error(interactionCheckResponse, true); + return; + } + + var reason = Utils.JoinString(ref args, 1); + await UnmuteMember(context.Guild, author, toUnmute, reason); } - public static async Task UnmuteMember(IGuild guild, ITextChannel? channel, IGuildUser author, IGuildUser toUnmute, + public static async Task UnmuteMember(SocketGuild guild, SocketGuildUser author, SocketGuildUser toUnmute, string reason) { - await CommandHandler.CheckPermissions(author, GuildPermission.ModerateMembers, GuildPermission.ManageRoles); - await CommandHandler.CheckInteractions(author, toUnmute); - var authorMention = author.Mention; - var config = Boyfriend.GetGuildConfig(guild); - var notification = string.Format(Messages.MemberUnmuted, authorMention, toUnmute.Mention, - Utils.WrapInline(reason)); - var requestOptions = Utils.GetRequestOptions($"({Utils.GetNameAndDiscrim(author)}) {reason}"); - var role = Utils.GetMuteRole(guild); + var requestOptions = Utils.GetRequestOptions($"({author}) {reason}"); + var role = Utils.GetMuteRole(ref guild); if (role != null) { - if (toUnmute.RoleIds.All(x => x != role.Id)) { - var rolesRemoved = config.RolesRemovedOnMute; - - await toUnmute.AddRolesAsync(rolesRemoved![toUnmute.Id]); - rolesRemoved.Remove(toUnmute.Id); - await config.Save(); - throw new ApplicationException(Messages.RolesReturned); + var muted = false; + foreach (var x in toUnmute.Roles) { + if (x != role) continue; + muted = true; + break; } - if (toUnmute.RoleIds.All(x => x != role.Id)) - throw new ApplicationException(Messages.MemberNotMuted); + var rolesRemoved = Boyfriend.GetRemovedRoles(guild.Id); - await toUnmute.RemoveRoleAsync(role, requestOptions); - if (config.RolesRemovedOnMute!.ContainsKey(toUnmute.Id)) { - await toUnmute.AddRolesAsync(config.RolesRemovedOnMute[toUnmute.Id]); - config.RolesRemovedOnMute.Remove(toUnmute.Id); - await config.Save(); + if (rolesRemoved.ContainsKey(toUnmute.Id)) { + await toUnmute.AddRolesAsync(rolesRemoved[toUnmute.Id]); + rolesRemoved.Remove(toUnmute.Id); + CommandHandler.ConfigWriteScheduled = true; + } + + if (muted) { + await toUnmute.RemoveRoleAsync(role, requestOptions); + } else { + Error(Messages.MemberNotMuted, false); + return; } } else { - if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() - < DateTimeOffset.Now.ToUnixTimeMilliseconds()) - throw new ApplicationException(Messages.MemberNotMuted); + if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() < + DateTimeOffset.Now.ToUnixTimeMilliseconds()) { + Error(Messages.MemberNotMuted, false); + return; + } await toUnmute.RemoveTimeOutAsync(); } - await Utils.SilentSendAsync(channel, string.Format(Messages.UnmuteResponse, toUnmute.Mention, - Utils.WrapInline(reason))); - await Utils.SilentSendAsync(await guild.GetSystemChannelAsync(), notification); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(guild), notification); - } - - public override List GetAliases() { - return new List {"unmute", "размут"}; - } - - public override int GetArgumentsAmountRequired() { - return 2; - } - - public override string GetSummary() { - return "Снимает мут с участника"; + var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.WrapInline(reason)); + Success(feedback, author.Mention, false, false); + await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); } } diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index bb484c8..8109395 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using Boyfriend.Commands; +using Boyfriend.Commands; using Discord; using Discord.Commands; using Discord.WebSocket; @@ -15,61 +14,75 @@ public class EventHandler { _client.MessageReceived += MessageReceivedEvent; _client.MessageUpdated += MessageUpdatedEvent; _client.UserJoined += UserJoinedEvent; + _client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; + _client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; + _client.GuildScheduledEventStarted += ScheduledEventStartedEvent; + _client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; } private static async Task ReadyEvent() { - await Boyfriend.SetupGuildConfigs(); - - var i = new Random().Next(3); + var i = Utils.Random.Next(3); foreach (var guild in Boyfriend.Client.Guilds) { - var config = Boyfriend.GetGuildConfig(guild); - var channel = guild.GetTextChannel(config.BotLogChannel.GetValueOrDefault(0)); - Messages.Culture = new CultureInfo(config.Lang!); + var config = Boyfriend.GetGuildConfig(guild.Id); + var channel = guild.GetTextChannel(Convert.ToUInt64(config["BotLogChannel"])); + Utils.SetCurrentLanguage(guild.Id); - if (!config.ReceiveStartupMessages.GetValueOrDefault(true) || channel == null) continue; - await channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(config.Lang!, i))); + if (config["ReceiveStartupMessages"] != "true" || channel == null) continue; + await channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); } } private static async Task MessageDeletedEvent(Cacheable message, Cacheable channel) { var msg = message.Value; + if (msg is null or ISystemMessage || msg.Author.IsBot) return; - var toSend = msg == null ? string.Format(Messages.UncachedMessageDeleted, Utils.MentionChannel(channel.Id)) - : string.Format(Messages.CachedMessageDeleted, msg.Author.Mention) + - $"{Utils.MentionChannel(channel.Id)}: {Environment.NewLine}{Utils.Wrap(msg.Content)}"; - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Boyfriend.FindGuild(channel.Value)), toSend); + var guild = Boyfriend.FindGuild(channel.Value.Id); + + Utils.SetCurrentLanguage(guild.Id); + + var auditLogEntry = (await guild.GetAuditLogsAsync(1).FlattenAsync()).First(); + var mention = auditLogEntry.User.Mention; + if (auditLogEntry.Action != ActionType.MessageDeleted || + DateTimeOffset.Now.Subtract(auditLogEntry.CreatedAt).TotalMilliseconds > 500 || + auditLogEntry.User.IsBot) mention = msg.Author.Mention; + + await Utils.SendFeedback( + string.Format(Messages.CachedMessageDeleted, msg.Author.Mention, Utils.MentionChannel(channel.Id), + Utils.WrapAsNeeded(msg.CleanContent)), guild.Id, mention); } private static async Task MessageReceivedEvent(SocketMessage messageParam) { if (messageParam is not SocketUserMessage message) return; - var argPos = 0; - var user = (IGuildUser) message.Author; + var user = (SocketGuildUser) message.Author; var guild = user.Guild; - var guildConfig = Boyfriend.GetGuildConfig(guild); + var guildConfig = Boyfriend.GetGuildConfig(guild.Id); + + Utils.SetCurrentLanguage(guild.Id); + + if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && + !user.GuildPermissions.MentionEveryone) { + await BanCommand.BanUser(guild, guild.CurrentUser, user, TimeSpan.FromMilliseconds(-1), + Messages.AutobanReason); + return; + } + + var argPos = 0; var prev = ""; var prevFailsafe = ""; var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); var prevsArray = prevs as IMessage[] ?? prevs.ToArray(); - Messages.Culture = new CultureInfo(guildConfig.Lang!); - - if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && - !user.GuildPermissions.MentionEveryone) - await BanCommand.BanUser(guild, null, await guild.GetCurrentUserAsync(), user, - TimeSpan.FromMilliseconds(-1), Messages.AutobanReason); - - try { + if (prevsArray.Length >= 3) { prev = prevsArray[1].Content; prevFailsafe = prevsArray[2].Content; - } catch (IndexOutOfRangeException) {} + } - if (!(message.HasStringPrefix(guildConfig.Prefix, ref argPos) || - message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) || - user == await guild.GetCurrentUserAsync() || - user.IsBot && (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe))) + if (!(message.HasStringPrefix(guildConfig["Prefix"], ref argPos) || + message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) || user == guild.CurrentUser || + (user.IsBot && (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)))) return; await CommandHandler.HandleCommand(message); @@ -78,27 +91,89 @@ public class EventHandler { private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, ISocketMessageChannel channel) { var msg = messageCached.Value; - var nl = Environment.NewLine; - if (msg != null && msg.Content == messageSocket.Content) return; + if (msg is null or ISystemMessage || msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return; - var toSend = msg == null - ? string.Format(Messages.UncachedMessageEdited, messageSocket.Author.Mention, - Utils.MentionChannel(channel.Id)) + Utils.Wrap(messageSocket.Content) : string.Format( - Messages.CachedMessageEdited, msg.Author.Mention, Utils.MentionChannel(channel.Id), nl, nl, - Utils.Wrap(msg.Content), nl, nl, Utils.Wrap(messageSocket.Content)); - await Utils.SilentSendAsync(await Utils.GetAdminLogChannel(Boyfriend.FindGuild(channel)), toSend); + var guildId = Boyfriend.FindGuild(channel.Id).Id; + + Utils.SetCurrentLanguage(guildId); + + await Utils.SendFeedback( + string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id), + Utils.WrapAsNeeded(msg.CleanContent), Utils.WrapAsNeeded(messageSocket.Content)), guildId, + msg.Author.Mention); } private static async Task UserJoinedEvent(SocketGuildUser user) { var guild = user.Guild; - var config = Boyfriend.GetGuildConfig(guild); + var config = Boyfriend.GetGuildConfig(guild.Id); - if (config.SendWelcomeMessages.GetValueOrDefault(true)) + if (config["SendWelcomeMessages"] == "true") await Utils.SilentSendAsync(guild.SystemChannel, - string.Format(config.WelcomeMessage!, user.Mention, guild.Name)); + string.Format(config["WelcomeMessage"], user.Mention, guild.Name)); - if (config.DefaultRole != 0) - await user.AddRoleAsync(Utils.ParseRole(guild, config.DefaultRole.ToString()!)); + if (config["StarterRole"] != "0") + await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); } -} \ No newline at end of file + + private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) { + var guild = scheduledEvent.Guild; + var eventConfig = Boyfriend.GetGuildConfig(guild.Id); + var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCreatedChannel"])); + + if (channel != null) { + var roleMention = ""; + var role = guild.GetRole(Convert.ToUInt64(eventConfig["EventNotifyReceiverRole"])); + if (role != null) + roleMention = $"{role.Mention} "; + + var location = Utils.WrapInline(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); + + await Utils.SilentSendAsync(channel, + string.Format(Messages.EventCreated, "\n", roleMention, scheduledEvent.Creator.Mention, + Utils.WrapInline(scheduledEvent.Name), location, + scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description)), + true); + } + } + + private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { + var guild = scheduledEvent.Guild; + var eventConfig = Boyfriend.GetGuildConfig(guild.Id); + var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCancelledChannel"])); + if (channel != null) + await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.WrapInline(scheduledEvent.Name), + eventConfig["FrowningFace"] == "true" ? $" {Messages.SettingsFrowningFace}" : "")); + } + + private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) { + var guild = scheduledEvent.Guild; + var eventConfig = Boyfriend.GetGuildConfig(guild.Id); + var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventStartedChannel"])); + + if (channel != null) { + var receivers = eventConfig["EventStartedReceivers"]; + var role = guild.GetRole(Convert.ToUInt64(eventConfig["EventNotifyReceiverRole"])); + var mentions = Boyfriend.StringBuilder; + + if (receivers.Contains("role") && role != null) mentions.Append($"{role.Mention} "); + if (receivers.Contains("users") || receivers.Contains("interested")) + foreach (var user in await scheduledEvent.GetUsersAsync(15)) + mentions = mentions.Append($"{user.Mention} "); + + await channel.SendMessageAsync(string.Format(Messages.EventStarted, mentions, + Utils.WrapInline(scheduledEvent.Name), + Utils.WrapInline(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id))); + mentions.Clear(); + } + } + + private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { + var guild = scheduledEvent.Guild; + var eventConfig = Boyfriend.GetGuildConfig(guild.Id); + var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCompletedChannel"])); + if (channel != null) + await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.WrapInline(scheduledEvent.Name), + Utils.WrapInline(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); + } +} diff --git a/Boyfriend/GuildConfig.cs b/Boyfriend/GuildConfig.cs deleted file mode 100644 index 59d25a3..0000000 --- a/Boyfriend/GuildConfig.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Globalization; -using Newtonsoft.Json; -// ReSharper disable MemberCanBePrivate.Global - -namespace Boyfriend; - -public class GuildConfig { - - public GuildConfig(ulong id) { - Id = id; - Validate(); - } - public ulong? Id { get; } - public string? Lang { get; set; } - public string? Prefix { get; set; } - - public bool? RemoveRolesOnMute { get; set; } - public bool? UseSystemChannel { get; set; } - public bool? SendWelcomeMessages { get; set; } - public bool? ReceiveStartupMessages { get; set; } - - public string? WelcomeMessage { get; set; } - - public ulong? DefaultRole { get; set; } - public ulong? MuteRole { get; set; } - public ulong? AdminLogChannel { get; set; } - public ulong? BotLogChannel { get; set; } - - public Dictionary>? RolesRemovedOnMute { get; private set; } - - public void Validate() { - if (Id == null) throw new Exception("Something went horribly, horribly wrong"); - - Lang ??= "ru"; - Messages.Culture = new CultureInfo(Lang); - Prefix ??= "!"; - RemoveRolesOnMute ??= false; - UseSystemChannel ??= true; - SendWelcomeMessages ??= true; - ReceiveStartupMessages ??= true; - WelcomeMessage ??= Messages.DefaultWelcomeMessage; - DefaultRole ??= 0; - MuteRole ??= 0; - AdminLogChannel ??= 0; - BotLogChannel ??= 0; - RolesRemovedOnMute ??= new Dictionary>(); - } - - public async Task Save() { - Validate(); - RolesRemovedOnMute!.TrimExcess(); - - await File.WriteAllTextAsync("config_" + Id + ".json", JsonConvert.SerializeObject(this)); - } -} diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 5f6d7f5..31866d9 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -69,15 +69,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to :white_check_mark: Successfully banned {0} for {1}. - /// - internal static string BanResponse { - get { - return ResourceManager.GetString("BanResponse", resourceCulture); - } - } - /// /// Looks up a localized string similar to Bah! . /// @@ -106,7 +97,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Deleted message from {0} in channel . + /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. /// internal static string CachedMessageDeleted { get { @@ -115,7 +106,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Message edited from {0} in channel {1}.{2}Before:{3}{4}{5}After:{6}{7}. + /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. /// internal static string CachedMessageEdited { get { @@ -169,7 +160,88 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Command help:{0}. + /// Looks up a localized string similar to Bans a user. + /// + internal static string CommandDescriptionBan { + get { + return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. + /// + internal static string CommandDescriptionClear { + get { + return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows this message. + /// + internal static string CommandDescriptionHelp { + get { + return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kicks a member. + /// + internal static string CommandDescriptionKick { + get { + return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mutes a member. + /// + internal static string CommandDescriptionMute { + get { + return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows latency to Discord servers (not counting local processing time). + /// + internal static string CommandDescriptionPing { + get { + return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allows you to change certain preferences for this guild. + /// + internal static string CommandDescriptionSettings { + get { + return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unbans a user. + /// + internal static string CommandDescriptionUnban { + get { + return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unmutes a member. + /// + internal static string CommandDescriptionUnmute { + get { + return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command help:. /// internal static string CommandHelp { get { @@ -205,7 +277,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Current settings:{0}. + /// Looks up a localized string similar to Current settings:. /// internal static string CurrentSettings { get { @@ -213,105 +285,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Admin log channel (`adminLogChannel`): {0}{1}. - /// - internal static string CurrentSettingsAdminLogChannel { - get { - return ResourceManager.GetString("CurrentSettingsAdminLogChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bot log channel (`botLogChannel`): {0}. - /// - internal static string CurrentSettingsBotLogChannel { - get { - return ResourceManager.GetString("CurrentSettingsBotLogChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Default role (`defaultRole`): {0}{1}. - /// - internal static string CurrentSettingsDefaultRole { - get { - return ResourceManager.GetString("CurrentSettingsDefaultRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language (`lang`): `{0}`{1}. - /// - internal static string CurrentSettingsLang { - get { - return ResourceManager.GetString("CurrentSettingsLang", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mute role (`muteRole`): {0}{1}. - /// - internal static string CurrentSettingsMuteRole { - get { - return ResourceManager.GetString("CurrentSettingsMuteRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prefix (`prefix`): `{0}`{1}. - /// - internal static string CurrentSettingsPrefix { - get { - return ResourceManager.GetString("CurrentSettingsPrefix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Receive startup messages (`receiveStartupMessages`): {0}{1}. - /// - internal static string CurrentSettingsReceiveStartupMessages { - get { - return ResourceManager.GetString("CurrentSettingsReceiveStartupMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remove roles on mute (`removeRolesOnMute`): {0}{1}. - /// - internal static string CurrentSettingsRemoveRoles { - get { - return ResourceManager.GetString("CurrentSettingsRemoveRoles", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send welcome messages (`sendWelcomeMessages`): {0}{1}. - /// - internal static string CurrentSettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("CurrentSettingsSendWelcomeMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use system channel for notifications (`useSystemChannel`): {0}{1}. - /// - internal static string CurrentSettingsUseSystemChannel { - get { - return ResourceManager.GetString("CurrentSettingsUseSystemChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Welcome message: `{0}`{1}. - /// - internal static string CurrentSettingsWelcomeMessage { - get { - return ResourceManager.GetString("CurrentSettingsWelcomeMessage", resourceCulture); - } - } - /// /// Looks up a localized string similar to {0}, welcome to {1}. /// @@ -339,6 +312,123 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to Event {0} is cancelled!{1}. + /// + internal static string EventCancelled { + get { + return ResourceManager.GetString("EventCancelled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event {0} has completed! Duration: {1}. + /// + internal static string EventCompleted { + get { + return ResourceManager.GetString("EventCompleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}. + /// + internal static string EventCreated { + get { + return ResourceManager.GetString("EventCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. + /// + internal static string EventStarted { + get { + return ResourceManager.GetString("EventStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ever. + /// + internal static string Ever { + get { + return ResourceManager.GetString("Ever", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to *[{0}: {1}]*. + /// + internal static string FeedbackFormat { + get { + return ResourceManager.GetString("FeedbackFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kicked {0}: {1}. + /// + internal static string FeedbackMemberKicked { + get { + return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Muted {0} for{1}: {2}. + /// + internal static string FeedbackMemberMuted { + get { + return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unmuted {0}: {1}. + /// + internal static string FeedbackMemberUnmuted { + get { + return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleted {0} messages in {1}. + /// + internal static string FeedbackMessagesCleared { + get { + return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. + /// + internal static string FeedbackSettingsUpdated { + get { + return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Banned {0} for{1}: {2}. + /// + internal static string FeedbackUserBanned { + get { + return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unbanned {0}: {1}. + /// + internal static string FeedbackUserUnbanned { + get { + return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); + } + } + /// /// Looks up a localized string similar to Members are in different guilds!. /// @@ -420,15 +510,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to :white_check_mark: Successfully kicked {0} for {1}. - /// - internal static string KickResponse { - get { - return ResourceManager.GetString("KickResponse", resourceCulture); - } - } - /// /// Looks up a localized string similar to Language not supported!. /// @@ -447,24 +528,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to {0} kicked {1} for {2}. - /// - internal static string MemberKicked { - get { - return ResourceManager.GetString("MemberKicked", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} muted {1} for {2}{3}. - /// - internal static string MemberMuted { - get { - return ResourceManager.GetString("MemberMuted", resourceCulture); - } - } - /// /// Looks up a localized string similar to Member not muted!. /// @@ -483,15 +546,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to {0} deleted {1} messages in channel {2}. - /// - internal static string MessagesDeleted { - get { - return ResourceManager.GetString("MessagesDeleted", resourceCulture); - } - } - /// /// Looks up a localized string similar to ms. /// @@ -501,15 +555,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to :white_check_mark: Successfully muted {0} for {1}. - /// - internal static string MuteResponse { - get { - return ResourceManager.GetString("MuteResponse", resourceCulture); - } - } - /// /// Looks up a localized string similar to No. /// @@ -537,15 +582,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to {0}This punishment will expire <t:{1}:R>. - /// - internal static string PunishmentExpiresIn { - get { - return ResourceManager.GetString("PunishmentExpiresIn", resourceCulture); - } - } - /// /// Looks up a localized string similar to {0}I'm ready! (C#). /// @@ -592,25 +628,178 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Settings successfully updated. + /// Looks up a localized string similar to Not specified. /// - internal static string SettingsUpdated { + internal static string SettingNotDefined { get { - return ResourceManager.GetString("SettingsUpdated", resourceCulture); + return ResourceManager.GetString("SettingNotDefined", resourceCulture); } } /// - /// Looks up a localized string similar to :white_check_mark: Successfully unbanned {0} for {1}. + /// Looks up a localized string similar to Admin log channel. /// - internal static string UnbanResponse { + internal static string SettingsAdminLogChannel { get { - return ResourceManager.GetString("UnbanResponse", resourceCulture); + return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); } } /// - /// Looks up a localized string similar to Deleted message in {0}, but I forgot what was there. + /// Looks up a localized string similar to Bot log channel. + /// + internal static string SettingsBotLogChannel { + get { + return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event cancellation notifications. + /// + internal static string SettingsEventCancelledChannel { + get { + return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event completion notifications. + /// + internal static string SettingsEventCompletedChannel { + get { + return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event creation notifications. + /// + internal static string SettingsEventCreatedChannel { + get { + return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Role for event creation notifications. + /// + internal static string SettingsEventNotifyReceiverRole { + get { + return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event start notifications. + /// + internal static string SettingsEventStartedChannel { + get { + return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event start notifications receivers. + /// + internal static string SettingsEventStartedReceivers { + get { + return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to :(. + /// + internal static string SettingsFrowningFace { + get { + return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language. + /// + internal static string SettingsLang { + get { + return ResourceManager.GetString("SettingsLang", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mute role. + /// + internal static string SettingsMuteRole { + get { + return ResourceManager.GetString("SettingsMuteRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. + /// + internal static string SettingsNothingChanged { + get { + return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix. + /// + internal static string SettingsPrefix { + get { + return ResourceManager.GetString("SettingsPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Receive startup messages. + /// + internal static string SettingsReceiveStartupMessages { + get { + return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove roles on mute. + /// + internal static string SettingsRemoveRolesOnMute { + get { + return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send welcome messages. + /// + internal static string SettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Starter role. + /// + internal static string SettingsStarterRole { + get { + return ResourceManager.GetString("SettingsStarterRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome message. + /// + internal static string SettingsWelcomeMessage { + get { + return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message deleted in {0}, but I forgot what was there. /// internal static string UncachedMessageDeleted { get { @@ -619,7 +808,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Message edited from {0} in channel {1}, but I forgot what was there before the edit: . + /// Looks up a localized string similar to Message edited from {0} in channel {1}, but I forgot what was there before the edit: {2}. /// internal static string UncachedMessageEdited { get { @@ -628,20 +817,11 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to :white_check_mark: Successfully unmuted {0} for {1}. + /// Looks up a localized string similar to That user doesn't exist!. /// - internal static string UnmuteResponse { + internal static string UserDoesntExist { get { - return ResourceManager.GetString("UnmuteResponse", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} banned {1} for {2}{3}. - /// - internal static string UserBanned { - get { - return ResourceManager.GetString("UserBanned", resourceCulture); + return ResourceManager.GetString("UserDoesntExist", resourceCulture); } } diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 6f8ac2e..1f8a6e8 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -31,19 +31,19 @@ {0}I'm ready! (C#) - Deleted message in {0}, but I forgot what was there + Message deleted in {0}, but I forgot what was there - Deleted message from {0} in channel + Deleted message from {0} in channel {1}: {2} Too many mentions in 1 message - Message edited from {0} in channel {1}, but I forgot what was there before the edit: + Message edited from {0} in channel {1}, but I forgot what was there before the edit: {2} - Message edited from {0} in channel {1}.{2}Before:{3}{4}{5}After:{6}{7} + Edited message in channel {0}: {1} -> {2} {0}, welcome to {1} @@ -84,9 +84,6 @@ You were banned by {0} in guild {1} for {2} - - {0} banned {1} for {2}{3} - Punishment expired @@ -96,21 +93,12 @@ Too many messages specified! - - {0} deleted {1} messages in channel {2} - - Command help:{0} + Command help: You were kicked by {0} in guild {1} for {2} - - {0} kicked {1} for {2} - - - {0} muted {1} for {2}{3} - ms @@ -124,41 +112,35 @@ Not specified - Current settings:{0} + Current settings: - - Language (`lang`): `{0}`{1} + + Language - - Prefix (`prefix`): `{0}`{1} + + Prefix - - Remove roles on mute (`removeRolesOnMute`): {0}{1} + + Remove roles on mute - - Use system channel for notifications (`useSystemChannel`): {0}{1} + + Send welcome messages - - Send welcome messages (`sendWelcomeMessages`): {0}{1} + + Starter role - - Default role (`defaultRole`): {0}{1} + + Mute role - - Mute role (`muteRole`): {0}{1} + + Admin log channel - - Admin log channel (`adminLogChannel`): {0}{1} - - - Bot log channel (`botLogChannel`): {0} + + Bot log channel Language not supported! - - Settings successfully updated - Yes @@ -180,8 +162,8 @@ {0} unbanned {1} for {2} - - Welcome message: `{0}`{1} + + Welcome message Not enough arguments! Needed: {0}, provided: {1} @@ -189,32 +171,17 @@ Invalid message amount specified! - - :white_check_mark: Successfully banned {0} for {1} - - - :white_check_mark: Successfully kicked {0} for {1} + + Banned {0} for{1}: {2} The specified user is not a member of this server! - - :white_check_mark: Successfully muted {0} for {1} - - - :white_check_mark: Successfully unbanned {0} for {1} - - - :white_check_mark: Successfully unmuted {0} for {1} - That setting doesn't exist! - - Receive startup messages (`receiveStartupMessages`): {0}{1} - - - {0}This punishment will expire <t:{1}:R> + + Receive startup messages Invalid setting value specified! @@ -237,4 +204,97 @@ I cannot use time-outs on other bots! Try to set a mute role in settings - \ No newline at end of file + + {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6} + + + Role for event creation notifications + + + Channel for event creation notifications + + + Channel for event start notifications + + + Event start notifications receivers + + + {0}Event {1} is starting at {2}! + + + :( + + + Event {0} is cancelled!{1} + + + Channel for event cancellation notifications + + + Channel for event completion notifications + + + Event {0} has completed! Duration: {1} + + + That user doesn't exist! + + + *[{0}: {1}]* + + + ever + + + Deleted {0} messages in {1} + + + Kicked {0}: {1} + + + Muted {0} for{1}: {2} + + + Unbanned {0}: {1} + + + Unmuted {0}: {1} + + + Nothing changed! `{0}` is already set to {1} + + + Not specified + + + Value of setting `{0}` is now set to {1} + + + Bans a user + + + Deletes a specified amount of messages in this channel + + + Shows this message + + + Kicks a member + + + Mutes a member + + + Shows latency to Discord servers (not counting local processing time) + + + Allows you to change certain preferences for this guild + + + Unbans a user + + + Unmutes a member + + diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 0aad39e..e5eb732 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -25,16 +25,16 @@ Удалено сообщение в канале {0}, но я забыл что там было - Удалено сообщение от {0} в канале + Удалено сообщение от {0} в канале {1}: {2} Слишком много упоминаний в одном сообщении - Отредактировано сообщение от {0} в канале {1}, но я забыл что там было до редактирования: + Отредактировано сообщение от {0} в канале {1}, но я забыл что там было до редактирования: {2} - Отредактировано сообщение от {0} в канале {1}.{2}До:{3}{4}{5}После:{6}{7} + Отредактировано сообщение в канале {0}: {1} -> {2} {0}, добро пожаловать на сервер {1} @@ -75,9 +75,6 @@ Тебя забанил {0} на сервере {1} за {2} - - {0} банит {1} за {2}{3} - Время наказания истекло @@ -87,21 +84,12 @@ Указано слишком много сообщений! - - {0} удаляет {1} сообщений в канале {2} - - Справка по командам:{0} + Справка по командам: Тебя кикнул {0} на сервере {1} за {2} - - {0} выгоняет {1} за {2} - - - {0} глушит {1} за {2}{3} - мс @@ -115,41 +103,32 @@ Не указана - Текущие настройки:{0} + Текущие настройки: - - Язык (`lang`): `{0}`{1} + + Язык - - Префикс (`prefix`): `{0}`{1} + + Префикс - - Удалять роли при муте (`removeRolesOnMute`): {0}{1} + + Удалять роли при муте - - Использовать канал системных сообщений для уведомлений (`useSystemChannel`): {0}{1} + + Отправлять приветствия - - Отправлять приветствия (`sendWelcomeMessages`): {0}{1} + + Роль мута - - Стандартная роль (`defaultRole`): {0}{1} + + Канал админ-уведомлений - - Роль мута (`muteRole`): {0}{1} - - - Канал админ-уведомлений (`adminLogChannel`): {0}{1} - - - Канал бот-уведомлений (`botLogChannel`): {0} + + Канал бот-уведомлений Язык не поддерживается! - - Настройки успешно обновлены! - Да @@ -171,8 +150,8 @@ {0} возвращает из бана {1} за {2} - - Приветствие: `{0}`{1} + + Приветствие Недостаточно аргументов! Требуется: {0}, указано: {1} @@ -180,32 +159,17 @@ Указано неверное количество сообщений! - - :white_check_mark: Успешно забанен {0} за {1} - - - :white_check_mark: Успешно выгнан {0} за {1} + + Забанен {0} на{1}: {2} Указанный пользователь не является участником этого сервера! - - :white_check_mark: Успешно заглушен {0} за {1} - - - :white_check_mark: Успешно возвращён из бана {0} за {1} - - - :white_check_mark: Успешно возвращён из мута {0} за {1} - Такая настройка не существует! - - Получать сообщения о запуске (`receiveStartupMessages`): {0}{1} - - - {0}Это наказание истечёт <t:{1}:R> + + Получать сообщения о запуске Указано недействительное значение для настройки! @@ -228,4 +192,100 @@ Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках - \ No newline at end of file + + Начальная роль + + + {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} + + + Роль для уведомлений о создании событий + + + Канал для уведомлений о создании событий + + + Канал для уведомлений о начале событий + + + Получатели уведомлений о начале событий + + + {0}Событие {1} начинается в {2}! + + + :( + + + Событие {0} отменено!{1} + + + Канал для уведомлений о отмене событий + + + Канал для уведомлений о завершении событий + + + Событие {0} завершено! Продолжительность: {1} + + + Такого пользователя не существует! + + + *[{0}: {1}]* + + + всегда + + + Удалено {0} сообщений в {1} + + + Выгнан {0}: {1} + + + Заглушен {0} на{1}: {2} + + + Возвращён из бана {0}: {1} + + + Разглушен {0}: {1} + + + Ничего не изменилось! Значение настройки `{0}` уже {1} + + + Не указано + + + Значение настройки `{0}` теперь установлено на {1} + + + Банит пользователя + + + Удаляет указанное количество сообщений в этом канале + + + Показывает эту справку + + + Выгоняет участника + + + Глушит участника + + + Показывает задержку до серверов Discord (не считая времени на локальные вычисления) + + + Позволяет менять некоторые настройки под этот сервер + + + Возвращает пользователя из бана + + + Разглушает участника + + diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 8795c82..32e3bfa 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -1,11 +1,16 @@ using System.Globalization; +using System.Reflection; using System.Text.RegularExpressions; using Discord; using Discord.Net; +using Discord.WebSocket; +using Humanizer; +using Humanizer.Localisation; namespace Boyfriend; public static class Utils { + private static readonly string[] Formats = { "%d'd'%h'h'%m'm'%s's'", "%d'd'%h'h'%m'm'", "%d'd'%h'h'%s's'", "%d'd'%h'h'", "%d'd'%m'm'%s's'", "%d'd'%m'm'", "%d'd'%s's'", "%d'd'", "%h'h'%m'm'%s's'", "%h'h'%m'm'", "%h'h'%s's'", "%h'h'", "%m'm'%s's'", "%m'm'", "%s's'", @@ -14,65 +19,60 @@ public static class Utils { "%d'д'%s'с'", "%d'д'", "%h'ч'%m'м'%s'с'", "%h'ч'%m'м'", "%h'ч'%s'с'", "%h'ч'", "%m'м'%s'с'", "%m'м'", "%s'с'" }; - public static string GetBeep(string cultureInfo, int i = -1) { - Messages.Culture = new CultureInfo(cultureInfo); + public static readonly Random Random = new(); + private static readonly Dictionary ReflectionMessageCache = new(); + private static readonly Dictionary CultureInfoCache = new() { + {"ru", new CultureInfo("ru-RU")}, + {"en", new CultureInfo("en-US")} + }; + private static readonly Dictionary MuteRoleCache = new(); - var beeps = new[] {Messages.Beep1, Messages.Beep2, Messages.Beep3}; - return beeps[i < 0 ? new Random().Next(3) : i]; + private static readonly AllowedMentions AllowRoles = new() { + AllowedTypes = AllowedMentionTypes.Roles + }; + + public static string GetBeep(int i = -1) { + return GetMessage($"Beep{(i < 0 ? Random.Next(3) + 1 : ++i)}"); } - public static async Task GetAdminLogChannel(IGuild guild) { - var adminLogChannel = await ParseChannelNullable(Boyfriend.GetGuildConfig(guild).AdminLogChannel.ToString()!); - return adminLogChannel as ITextChannel; + public static SocketTextChannel? GetAdminLogChannel(ulong id) { + return Boyfriend.Client.GetGuild(id) + .GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(id)["AdminLogChannel"])); } - public static string Wrap(string original) { + public static string Wrap(string? original) { + if (original == null) return ""; var toReturn = original.Replace("```", "ˋˋˋ"); return $"```{toReturn}{(toReturn.EndsWith("`") || toReturn.Trim().Equals("") ? " " : "")}```"; } - public static string WrapInline(string original) { - return $"`{original.Replace("`", "ˋ")}`"; + public static string? WrapInline(string? original) { + return original == null ? null : $"`{original.Replace("`", "ˋ")}`"; + } + + public static string? WrapAsNeeded(string? original) { + if (original == null) return null; + return original.Contains('\n') ? Wrap(original) : WrapInline(original); } public static string MentionChannel(ulong id) { return $"<#{id}>"; } - private static ulong ParseMention(string mention) { - return Convert.ToUInt64(Regex.Replace(mention, "[^0-9]", "")); + public static ulong ParseMention(string mention) { + return ulong.TryParse(Regex.Replace(mention, "[^0-9]", ""), out var id) ? id : 0; } - private static ulong? ParseMentionNullable(string mention) { - try { - return ParseMention(mention) == 0 ? throw new FormatException() : ParseMention(mention); - } catch (FormatException) { - return null; - } + public static SocketUser? ParseUser(string mention) { + var user = Boyfriend.Client.GetUser(ParseMention(mention)); + return user; } - public static async Task ParseUser(string mention) { - var user = Boyfriend.Client.GetUserAsync(ParseMention(mention)); - return await user; + public static SocketGuildUser? ParseMember(SocketGuild guild, string mention) { + return guild.GetUser(ParseMention(mention)); } - public static async Task ParseMember(IGuild guild, string mention) { - return await guild.GetUserAsync(ParseMention(mention)); - } - - private static async Task ParseChannel(string mention) { - return await Boyfriend.Client.GetChannelAsync(ParseMention(mention)); - } - - private static async Task ParseChannelNullable(string mention) { - return ParseMentionNullable(mention) == null ? null : await ParseChannel(mention); - } - - public static IRole? ParseRole(IGuild guild, string mention) { - return guild.GetRole(ParseMention(mention)); - } - - public static async Task SendDirectMessage(IUser user, string toSend) { + public static async Task SendDirectMessage(SocketUser user, string toSend) { try { await user.SendMessageAsync(toSend); } catch (HttpException e) { @@ -81,33 +81,74 @@ public static class Utils { } } - public static IRole? GetMuteRole(IGuild guild) { - var role = guild.Roles.FirstOrDefault(x => x.Id == Boyfriend.GetGuildConfig(guild).MuteRole); + public static SocketRole? GetMuteRole(ref SocketGuild guild) { + var id = ulong.Parse(Boyfriend.GetGuildConfig(guild.Id)["MuteRole"]); + if (MuteRoleCache.ContainsKey(id)) return MuteRoleCache[id]; + SocketRole? role = null; + foreach (var x in guild.Roles) { + if (x.Id != id) continue; + role = x; + MuteRoleCache.Add(id, role); + break; + } return role; } - public static async Task SilentSendAsync(ITextChannel? channel, string text) { - if (channel == null) return; + public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) { + if (channel == null || text.Length is 0 or > 2000) return; - try { - await channel.SendMessageAsync(text, false, null, null, AllowedMentions.None); - } catch (ArgumentException) {} + await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); } - public static TimeSpan GetTimeSpan(string from) { - return TimeSpan.ParseExact(from.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture); + public static TimeSpan? GetTimeSpan(ref string from) { + if (TimeSpan.TryParseExact(from.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) + return timeSpan; + return null; } - public static string JoinString(string[] args, int startIndex) { + public static string JoinString(ref string[] args, int startIndex) { return string.Join(" ", args, startIndex, args.Length - startIndex); } - public static string GetNameAndDiscrim(IUser user) { - return $"{user.Username}#{user.Discriminator}"; - } - public static RequestOptions GetRequestOptions(string reason) { var options = RequestOptions.Default; options.AuditLogReason = reason; return options; } -} \ No newline at end of file + + public static string GetMessage(string name) { + var propertyName = name; + name = $"{Messages.Culture}/{name}"; + if (ReflectionMessageCache.ContainsKey(name)) return ReflectionMessageCache[name]; + + var toReturn = + typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) + ?.ToString()! ?? throw new Exception($"Could not find localized property: {name}"); + ReflectionMessageCache.Add(name, toReturn); + return toReturn; + } + + public static async Task SendFeedback(string feedback, ulong guildId, string mention, bool sendPublic = false) { + var adminChannel = GetAdminLogChannel(guildId); + var systemChannel = Boyfriend.Client.GetGuild(guildId).SystemChannel; + var toSend = string.Format(Messages.FeedbackFormat, mention, feedback); + if (adminChannel != null) + await SilentSendAsync(adminChannel, toSend); + if (sendPublic && systemChannel != null) + await SilentSendAsync(systemChannel, toSend); + } + + public static void StackFeedback(ref string feedback, ref string mention, bool isPublic) { + var toAppend = string.Format(Messages.FeedbackFormat, mention, feedback); + CommandHandler.StackedPrivateFeedback.AppendLine(toAppend); + if (isPublic) CommandHandler.StackedPublicFeedback.AppendLine(toAppend); + } + + public static string GetHumanizedTimeOffset(ref TimeSpan span) { + return span.TotalSeconds > 0 ? $" {span.Humanize(minUnit: TimeUnit.Second, culture: Messages.Culture)}" + : Messages.Ever; + } + + public static void SetCurrentLanguage(ulong guildId) { + Messages.Culture = CultureInfoCache[Boyfriend.GetGuildConfig(guildId)["Lang"]]; + } +} diff --git a/UpgradeLog.htm b/UpgradeLog.htm new file mode 100644 index 0000000000000000000000000000000000000000..5c4398b1aabe67a5c275fb88dc1491c3c7bd7fd3 GIT binary patch literal 32818 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 Date: Sat, 14 May 2022 18:17:26 +0500 Subject: [PATCH 012/329] get the FUCK out of here who asked you to be here?? --- UpgradeLog.htm | Bin 32818 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 UpgradeLog.htm diff --git a/UpgradeLog.htm b/UpgradeLog.htm deleted file mode 100644 index 5c4398b1aabe67a5c275fb88dc1491c3c7bd7fd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32818 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 Date: Mon, 6 Jun 2022 20:39:47 +0500 Subject: [PATCH 013/329] i can't be bothered to keep track of these changes --- Boyfriend/Boyfriend.cs | 67 +- Boyfriend/Boyfriend.csproj | 4 +- Boyfriend/CommandHandler.cs | 12 +- Boyfriend/Commands/BanCommand.cs | 16 +- Boyfriend/Commands/Command.cs | 1 - Boyfriend/Commands/KickCommand.cs | 8 +- Boyfriend/Commands/MuteCommand.cs | 31 +- Boyfriend/Commands/SettingsCommand.cs | 24 +- Boyfriend/Commands/UnbanCommand.cs | 8 +- Boyfriend/Commands/UnmuteCommand.cs | 17 +- Boyfriend/EventHandler.cs | 39 +- Boyfriend/Messages.Designer.cs | 1298 ++++++++++--------------- Boyfriend/Messages.resx | 3 + Boyfriend/Messages.ru.resx | 3 + Boyfriend/Utils.cs | 44 +- 15 files changed, 649 insertions(+), 926 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 8ff5ca8..b4aa241 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -19,30 +19,28 @@ public static class Boyfriend { private static readonly Game Activity = new("Retrospecter - Genocide", ActivityType.Listening); private static readonly Dictionary> GuildConfigDictionary = new(); + private static readonly Dictionary>> RemovedRolesDictionary = new(); - private static readonly Dictionary EmptyGuildConfig = new(); - private static readonly Dictionary> EmptyRemovedRoles = new(); - public static readonly Dictionary DefaultConfig = new() { - {"Lang", "en"}, - {"Prefix", "!"}, - {"RemoveRolesOnMute", "false"}, - {"SendWelcomeMessages", "true"}, - {"ReceiveStartupMessages", "false"}, - {"FrowningFace", "true"}, - {"WelcomeMessage", Messages.DefaultWelcomeMessage}, - {"EventStartedReceivers", "interested,role"}, - {"StarterRole", "0"}, - {"MuteRole", "0"}, - {"EventNotifyReceiverRole", "0"}, - {"AdminLogChannel", "0"}, - {"BotLogChannel", "0"}, - {"EventCreatedChannel", "0"}, - {"EventStartedChannel", "0"}, - {"EventCancelledChannel", "0"}, - {"EventCompletedChannel", "0"} + { "Lang", "en" }, + { "Prefix", "!" }, + { "RemoveRolesOnMute", "false" }, + { "SendWelcomeMessages", "true" }, + { "ReceiveStartupMessages", "false" }, + { "FrowningFace", "true" }, + { "WelcomeMessage", Messages.DefaultWelcomeMessage }, + { "EventStartedReceivers", "interested,role" }, + { "StarterRole", "0" }, + { "MuteRole", "0" }, + { "EventNotifyReceiverRole", "0" }, + { "AdminLogChannel", "0" }, + { "BotLogChannel", "0" }, + { "EventCreatedChannel", "0" }, + { "EventStartedChannel", "0" }, + { "EventCancelledChannel", "0" }, + { "EventCompletedChannel", "0" } }; public static void Main() { @@ -79,7 +77,7 @@ public static class Boyfriend { public static Dictionary GetGuildConfig(ulong id) { if (!RemovedRolesDictionary.ContainsKey(id)) - RemovedRolesDictionary.Add(id, EmptyRemovedRoles); + RemovedRolesDictionary.Add(id, new Dictionary>()); if (GuildConfigDictionary.ContainsKey(id)) return GuildConfigDictionary[id]; @@ -88,15 +86,19 @@ public static class Boyfriend { if (!File.Exists(path)) File.Create(path).Dispose(); var json = File.ReadAllText(path); - var config = JsonConvert.DeserializeObject>(json) ?? EmptyGuildConfig; + var config = JsonConvert.DeserializeObject>(json) + ?? new Dictionary(); - foreach (var key in DefaultConfig.Keys) - if (!config.ContainsKey(key)) - config.Add(key, DefaultConfig[key]); - - foreach (var key in config.Keys) - if (!DefaultConfig.ContainsKey(key)) + if (config.Keys.Count < DefaultConfig.Keys.Count) { + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + // Avoids a closure allocation with the config variable + 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); @@ -111,8 +113,8 @@ public static class Boyfriend { if (!File.Exists(path)) File.Create(path); var json = File.ReadAllText(path); - var removedRoles = JsonConvert.DeserializeObject>>(json) ?? - EmptyRemovedRoles; + var removedRoles = JsonConvert.DeserializeObject>>(json) + ?? new Dictionary>(); RemovedRolesDictionary.Add(id, removedRoles); @@ -121,9 +123,8 @@ public static class Boyfriend { public static SocketGuild FindGuild(ulong channel) { if (GuildCache.ContainsKey(channel)) return GuildCache[channel]; - foreach (var guild in Client.Guilds) - foreach (var x in guild.Channels) { - if (x.Id != channel) continue; + foreach (var guild in Client.Guilds) { + if (guild.Channels.All(x => x.Id != channel)) continue; GuildCache.Add(channel, guild); return guild; } diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index a05d38d..80329ec 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -15,10 +15,10 @@ - + - + diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs index 7754d53..0cd494b 100644 --- a/Boyfriend/CommandHandler.cs +++ b/Boyfriend/CommandHandler.cs @@ -8,7 +8,6 @@ using Discord.WebSocket; namespace Boyfriend; public static class CommandHandler { - public static readonly Command[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), new KickCommand(), new MuteCommand(), new PingCommand(), @@ -34,9 +33,7 @@ public static class CommandHandler { var config = Boyfriend.GetGuildConfig(guild.Id); Regex regex; - if (RegexCache.ContainsKey(config["Prefix"])) { - regex = RegexCache[config["Prefix"]]; - } else { + if (RegexCache.ContainsKey(config["Prefix"])) { regex = RegexCache[config["Prefix"]]; } else { regex = new Regex(Regex.Escape(config["Prefix"])); RegexCache.Add(config["Prefix"], regex); } @@ -61,13 +58,14 @@ public static class CommandHandler { if (currentLine != list.Length) continue; if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfig(guild.Id); - await context.Message.ReplyAsync(StackedReplyMessage.ToString(), false, null, AllowedMentions.None); + await message.ReplyAsync(StackedReplyMessage.ToString(), false, null, AllowedMentions.None); var adminChannel = Utils.GetAdminLogChannel(guild.Id); var systemChannel = guild.SystemChannel; - if (adminChannel != null) + if (StackedPrivateFeedback.Length > 0 && adminChannel != null && adminChannel.Id != message.Channel.Id) await Utils.SilentSendAsync(adminChannel, StackedPrivateFeedback.ToString()); - if (systemChannel != null) + if (StackedPublicFeedback.Length > 0 && systemChannel != null && systemChannel.Id != adminChannel?.Id + && systemChannel.Id != message.Channel.Id) await Utils.SilentSendAsync(systemChannel, StackedPublicFeedback.ToString()); } } diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 80e8b0c..b2cad58 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -5,7 +5,7 @@ using Discord.WebSocket; namespace Boyfriend.Commands; public class BanCommand : Command { - public override string[] Aliases { get; } = {"ban", "бан"}; + public override string[] Aliases { get; } = { "ban", "бан" }; public override int ArgsLengthRequired => 2; public override async Task Run(SocketCommandContext context, string[] args) { @@ -17,7 +17,7 @@ public class BanCommand : Command { } var guild = context.Guild; - var author = (SocketGuildUser) context.User; + var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.BanMembers); if (permissionCheckResponse != "") { @@ -40,6 +40,11 @@ public class BanCommand : Command { if (duration.TotalSeconds < 0) { Warn(Messages.DurationParseFailed); reason = Utils.JoinString(ref args, 1); + + if (reason == "") { + Error(Messages.ReasonRequired, false); + return; + } } await BanUser(guild, author, toBan, duration, reason); @@ -50,12 +55,12 @@ public class BanCommand : Command { var guildBanMessage = $"({author}) {reason}"; await Utils.SendDirectMessage(toBan, - string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.WrapInline(reason))); + string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason))); await guild.AddBanAsync(toBan, 0, guildBanMessage); var feedback = string.Format(Messages.FeedbackUserBanned, toBan.Mention, - Utils.GetHumanizedTimeOffset(ref duration), Utils.WrapInline(reason)); + Utils.GetHumanizedTimeOffset(ref duration), Utils.Wrap(reason)); Success(feedback, author.Mention, false, false); await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); @@ -65,8 +70,7 @@ public class BanCommand : Command { await UnbanCommand.UnbanUser(guild, guild.CurrentUser, toBan, Messages.PunishmentExpired); } - var task = new Task(DelayUnban); - task.Start(); + new Task(DelayUnban).Start(); } } } diff --git a/Boyfriend/Commands/Command.cs b/Boyfriend/Commands/Command.cs index 4903177..ec8590b 100644 --- a/Boyfriend/Commands/Command.cs +++ b/Boyfriend/Commands/Command.cs @@ -4,7 +4,6 @@ using Discord.Commands; namespace Boyfriend.Commands; public abstract class Command { - public abstract string[] Aliases { get; } public abstract int ArgsLengthRequired { get; } diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index ffa61eb..505178e 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -5,11 +5,11 @@ using Discord.WebSocket; namespace Boyfriend.Commands; public class KickCommand : Command { - public override string[] Aliases { get; } = {"kick", "кик", "выгнать"}; + public override string[] Aliases { get; } = { "kick", "кик", "выгнать" }; public override int ArgsLengthRequired => 2; public override async Task Run(SocketCommandContext context, string[] args) { - var author = (SocketGuildUser) context.User; + var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.KickMembers); if (permissionCheckResponse != "") { @@ -34,7 +34,7 @@ public class KickCommand : Command { Success( string.Format(Messages.FeedbackMemberKicked, toKick.Mention, - Utils.WrapInline(Utils.JoinString(ref args, 1))), author.Mention); + Utils.Wrap(Utils.JoinString(ref args, 1))), author.Mention); } private static async Task KickMember(IGuild guild, SocketUser author, SocketGuildUser toKick, string reason) { @@ -42,7 +42,7 @@ public class KickCommand : Command { var guildKickMessage = $"({author}) {reason}"; await Utils.SendDirectMessage(toKick, - string.Format(Messages.YouWereKicked, authorMention, guild.Name, Utils.WrapInline(reason))); + string.Format(Messages.YouWereKicked, authorMention, guild.Name, Utils.Wrap(reason))); await toKick.KickAsync(guildKickMessage); } diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index 144d76f..47c3ded 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -6,7 +6,7 @@ using Discord.WebSocket; namespace Boyfriend.Commands; public class MuteCommand : Command { - public override string[] Aliases { get; } = {"mute", "timeout", "заглушить", "мут"}; + public override string[] Aliases { get; } = { "mute", "timeout", "заглушить", "мут" }; public override int ArgsLengthRequired => 2; public override async Task Run(SocketCommandContext context, string[] args) { @@ -17,6 +17,11 @@ public class MuteCommand : Command { if (duration.TotalSeconds < 0) { Warn(Messages.DurationParseFailed); reason = Utils.JoinString(ref args, 1); + + if (reason == "") { + Error(Messages.ReasonRequired, false); + return; + } } if (toMute == null) { @@ -28,15 +33,9 @@ public class MuteCommand : Command { var role = Utils.GetMuteRole(ref guild); if (role != null) { - var hasMuteRole = false; - foreach (var x in toMute.Roles) { - if (x != role) continue; - hasMuteRole = true; - break; - } - - if (hasMuteRole || (toMute.TimedOutUntil != null && toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > - DateTimeOffset.Now.ToUnixTimeMilliseconds())) { + if (toMute.Roles.Contains(role) || (toMute.TimedOutUntil != null && + toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > + DateTimeOffset.Now.ToUnixTimeMilliseconds())) { Error(Messages.MemberAlreadyMuted, false); return; } @@ -51,7 +50,7 @@ public class MuteCommand : Command { Warn(Messages.RolesReturned); } - var author = (SocketGuildUser) context.User; + var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ModerateMembers); if (permissionCheckResponse != "") { @@ -69,7 +68,7 @@ public class MuteCommand : Command { Success( string.Format(Messages.FeedbackMemberMuted, toMute.Mention, Utils.GetHumanizedTimeOffset(ref duration), - Utils.WrapInline(reason)), author.Mention, true); + Utils.Wrap(reason)), author.Mention, true); } private static async Task MuteMember(SocketGuild guild, SocketUser author, SocketGuildUser toMute, @@ -88,7 +87,7 @@ public class MuteCommand : Command { await toMute.RemoveRoleAsync(role); rolesRemoved.Add(userRole.Id); } catch (HttpException e) { - Warn(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.WrapInline(e.Reason))); + Warn(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.Wrap(e.Reason))); } Boyfriend.GetRemovedRoles(guild.Id).Add(toMute.Id, rolesRemoved.AsReadOnly()); @@ -100,8 +99,7 @@ public class MuteCommand : Command { await UnmuteCommand.UnmuteMember(guild, guild.CurrentUser, toMute, Messages.PunishmentExpired); } - var task = new Task(DelayUnmute); - task.Start(); + new Task(DelayUnmute).Start(); } } @@ -111,6 +109,7 @@ public class MuteCommand : Command { Error(Messages.DurationRequiredForTimeOuts, false); return; } + if (toMute.IsBot) { Error(Messages.CannotTimeOutBot, false); return; @@ -119,4 +118,4 @@ public class MuteCommand : Command { await toMute.SetTimeOutAsync(duration, requestOptions); } } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 368c5d8..cfd1c23 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -5,11 +5,11 @@ using Discord.WebSocket; namespace Boyfriend.Commands; public class SettingsCommand : Command { - public override string[] Aliases { get; } = {"settings", "config", "настройки", "конфиг"}; + public override string[] Aliases { get; } = { "settings", "config", "настройки", "конфиг" }; public override int ArgsLengthRequired => 0; public override Task Run(SocketCommandContext context, string[] args) { - var author = (SocketGuildUser) context.User; + var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ManageGuild); if (permissionCheckResponse != "") { @@ -41,7 +41,7 @@ public class SettingsCommand : Command { if (IsBool(currentValue)) currentValue = YesOrNo(currentValue == "true"); else - format = Utils.WrapInline("{0}")!; + format = Utils.Wrap("{0}")!; } currentSettings.Append($"{Utils.GetMessage($"Settings{setting.Key}")} (`{setting.Key}`): ") @@ -56,9 +56,11 @@ public class SettingsCommand : Command { var selectedSetting = args[0].ToLower(); var exists = false; - foreach (var setting in Boyfriend.DefaultConfig) { - if (selectedSetting != setting.Key.ToLower()) continue; - selectedSetting = setting.Key; + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + // The performance impact is not worth it + foreach (var setting in Boyfriend.DefaultConfig.Keys) { + if (selectedSetting != setting.ToLower()) continue; + selectedSetting = setting; exists = true; break; } @@ -78,9 +80,7 @@ public class SettingsCommand : Command { Error(Messages.InvalidSettingValue, false); return Task.CompletedTask; } - } else { - value = "reset"; - } + } else { value = "reset"; } if (IsBool(Boyfriend.DefaultConfig[selectedSetting]) && !IsBool(value)) { value = value switch { @@ -99,7 +99,7 @@ public class SettingsCommand : Command { var mention = Utils.ParseMention(value); if (mention != 0) value = mention.ToString(); - var formatting = Utils.WrapInline("{0}")!; + var formatting = Utils.Wrap("{0}")!; if (selectedSetting.EndsWith("Channel")) formatting = "<#{0}>"; if (selectedSetting.EndsWith("Role")) @@ -131,6 +131,8 @@ public class SettingsCommand : Command { return Task.CompletedTask; } + if (selectedSetting == "MuteRole") Utils.RemoveMuteRoleFromCache(ulong.Parse(config[selectedSetting])); + config[selectedSetting] = value; } @@ -153,4 +155,4 @@ public class SettingsCommand : Command { private static bool IsBool(string value) { return value is "true" or "false"; } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index 8741a3e..a6576af 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -5,11 +5,11 @@ using Discord.WebSocket; namespace Boyfriend.Commands; public class UnbanCommand : Command { - public override string[] Aliases { get; } = {"unban", "разбан"}; + public override string[] Aliases { get; } = { "unban", "разбан" }; public override int ArgsLengthRequired => 2; public override async Task Run(SocketCommandContext context, string[] args) { - var author = (SocketGuildUser) context.User; + var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.BanMembers); if (permissionCheckResponse != "") { @@ -38,8 +38,8 @@ public class UnbanCommand : Command { var requestOptions = Utils.GetRequestOptions($"({author}) {reason}"); await guild.RemoveBanAsync(toUnban, requestOptions); - var feedback = string.Format(Messages.FeedbackUserUnbanned, toUnban.Mention, Utils.WrapInline(reason)); + var feedback = string.Format(Messages.FeedbackUserUnbanned, toUnban.Mention, Utils.Wrap(reason)); Success(feedback, author.Mention, false, false); await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index e4af83b..f0e7951 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -5,11 +5,11 @@ using Discord.WebSocket; namespace Boyfriend.Commands; public class UnmuteCommand : Command { - public override string[] Aliases { get; } = {"unmute", "размут"}; + public override string[] Aliases { get; } = { "unmute", "размут" }; public override int ArgsLengthRequired => 2; public override async Task Run(SocketCommandContext context, string[] args) { - var author = (SocketGuildUser) context.User; + var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ModerateMembers); if (permissionCheckResponse != "") { @@ -40,13 +40,6 @@ public class UnmuteCommand : Command { var role = Utils.GetMuteRole(ref guild); if (role != null) { - var muted = false; - foreach (var x in toUnmute.Roles) { - if (x != role) continue; - muted = true; - break; - } - var rolesRemoved = Boyfriend.GetRemovedRoles(guild.Id); if (rolesRemoved.ContainsKey(toUnmute.Id)) { @@ -55,9 +48,7 @@ public class UnmuteCommand : Command { CommandHandler.ConfigWriteScheduled = true; } - if (muted) { - await toUnmute.RemoveRoleAsync(role, requestOptions); - } else { + if (toUnmute.Roles.Contains(role)) { await toUnmute.RemoveRoleAsync(role, requestOptions); } else { Error(Messages.MemberNotMuted, false); return; } @@ -71,7 +62,7 @@ public class UnmuteCommand : Command { await toUnmute.RemoveTimeOutAsync(); } - var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.WrapInline(reason)); + var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason)); Success(feedback, author.Mention, false, false); await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); } diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 8109395..fd31995 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,6 +1,7 @@ using Boyfriend.Commands; using Discord; using Discord.Commands; +using Discord.Rest; using Discord.WebSocket; namespace Boyfriend; @@ -42,21 +43,23 @@ public class EventHandler { Utils.SetCurrentLanguage(guild.Id); + var mention = msg.Author.Mention; + + await Task.Delay(500); + var auditLogEntry = (await guild.GetAuditLogsAsync(1).FlattenAsync()).First(); - var mention = auditLogEntry.User.Mention; - if (auditLogEntry.Action != ActionType.MessageDeleted || - DateTimeOffset.Now.Subtract(auditLogEntry.CreatedAt).TotalMilliseconds > 500 || - auditLogEntry.User.IsBot) mention = msg.Author.Mention; + if (auditLogEntry.Data is MessageDeleteAuditLogData data && msg.Author.Id == data.Target.Id) + mention = auditLogEntry.User.Mention; await Utils.SendFeedback( string.Format(Messages.CachedMessageDeleted, msg.Author.Mention, Utils.MentionChannel(channel.Id), - Utils.WrapAsNeeded(msg.CleanContent)), guild.Id, mention); + Utils.Wrap(msg.CleanContent)), guild.Id, mention); } private static async Task MessageReceivedEvent(SocketMessage messageParam) { if (messageParam is not SocketUserMessage message) return; - var user = (SocketGuildUser) message.Author; + var user = (SocketGuildUser)message.Author; var guild = user.Guild; var guildConfig = Boyfriend.GetGuildConfig(guild.Id); @@ -98,10 +101,12 @@ public class EventHandler { Utils.SetCurrentLanguage(guildId); + var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940; + await Utils.SendFeedback( string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id), - Utils.WrapAsNeeded(msg.CleanContent), Utils.WrapAsNeeded(messageSocket.Content)), guildId, - msg.Author.Mention); + Utils.Wrap(msg.CleanContent, isLimitedSpace), Utils.Wrap(messageSocket.CleanContent, isLimitedSpace)), + guildId, msg.Author.Mention); } private static async Task UserJoinedEvent(SocketGuildUser user) { @@ -127,11 +132,11 @@ public class EventHandler { if (role != null) roleMention = $"{role.Mention} "; - var location = Utils.WrapInline(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); + var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); await Utils.SilentSendAsync(channel, string.Format(Messages.EventCreated, "\n", roleMention, scheduledEvent.Creator.Mention, - Utils.WrapInline(scheduledEvent.Name), location, + Utils.Wrap(scheduledEvent.Name), location, scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description)), true); } @@ -142,7 +147,7 @@ public class EventHandler { var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCancelledChannel"])); if (channel != null) - await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.WrapInline(scheduledEvent.Name), + await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name), eventConfig["FrowningFace"] == "true" ? $" {Messages.SettingsFrowningFace}" : "")); } @@ -158,12 +163,12 @@ public class EventHandler { if (receivers.Contains("role") && role != null) mentions.Append($"{role.Mention} "); if (receivers.Contains("users") || receivers.Contains("interested")) - foreach (var user in await scheduledEvent.GetUsersAsync(15)) - mentions = mentions.Append($"{user.Mention} "); + mentions = (await scheduledEvent.GetUsersAsync(15)).Aggregate(mentions, + (current, user) => current.Append($"{user.Mention} ")); await channel.SendMessageAsync(string.Format(Messages.EventStarted, mentions, - Utils.WrapInline(scheduledEvent.Name), - Utils.WrapInline(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id))); + Utils.Wrap(scheduledEvent.Name), + Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id))); mentions.Clear(); } } @@ -173,7 +178,7 @@ public class EventHandler { var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCompletedChannel"])); if (channel != null) - await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.WrapInline(scheduledEvent.Name), - Utils.WrapInline(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); + await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), + Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); } } diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 31866d9..08276fc 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -12,46 +11,32 @@ namespace Boyfriend { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -60,823 +45,556 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Too many mentions in 1 message. - /// - internal static string AutobanReason { - get { - return ResourceManager.GetString("AutobanReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bah! . - /// - internal static string Beep1 { - get { - return ResourceManager.GetString("Beep1", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bop! . - /// - internal static string Beep2 { - get { - return ResourceManager.GetString("Beep2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Beep! . - /// - internal static string Beep3 { - get { - return ResourceManager.GetString("Beep3", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. - /// - internal static string CachedMessageDeleted { - get { - return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. - /// - internal static string CachedMessageEdited { - get { - return ResourceManager.GetString("CachedMessageEdited", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. - /// - internal static string CannotTimeOutBot { - get { - return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string ChannelNotSpecified { - get { - return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Too many messages specified!. - /// - internal static string ClearAmountTooLarge { - get { - return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid message amount specified!. - /// - internal static string ClearInvalidAmountSpecified { - get { - return ResourceManager.GetString("ClearInvalidAmountSpecified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Negative message amount specified!. - /// - internal static string ClearNegativeAmount { - get { - return ResourceManager.GetString("ClearNegativeAmount", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bans a user. - /// - internal static string CommandDescriptionBan { - get { - return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. - /// - internal static string CommandDescriptionClear { - get { - return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Shows this message. - /// - internal static string CommandDescriptionHelp { - get { - return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Kicks a member. - /// - internal static string CommandDescriptionKick { - get { - return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mutes a member. - /// - internal static string CommandDescriptionMute { - get { - return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Shows latency to Discord servers (not counting local processing time). - /// - internal static string CommandDescriptionPing { - get { - return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allows you to change certain preferences for this guild. - /// - internal static string CommandDescriptionSettings { - get { - return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unbans a user. - /// - internal static string CommandDescriptionUnban { - get { - return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unmutes a member. - /// - internal static string CommandDescriptionUnmute { - get { - return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Command help:. - /// - internal static string CommandHelp { - get { - return ResourceManager.GetString("CommandHelp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I do not have permission to execute this command!. - /// - internal static string CommandNoPermissionBot { - get { - return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You do not have permission to execute this command!. - /// - internal static string CommandNoPermissionUser { - get { - return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Couldn't find guild by message!. - /// internal static string CouldntFindGuildByChannel { get { return ResourceManager.GetString("CouldntFindGuildByChannel", resourceCulture); } } - /// - /// Looks up a localized string similar to Current settings:. - /// - internal static string CurrentSettings { - get { - return ResourceManager.GetString("CurrentSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}, welcome to {1}. - /// - internal static string DefaultWelcomeMessage { - get { - return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I couldn't parse the specified duration! One of the components could be outside it's valid range (e.g. `24h` or `60m`). - /// - internal static string DurationParseFailed { - get { - return ResourceManager.GetString("DurationParseFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot mute someone forever using timeouts! Either specify a proper duration, or set a mute role in settings. - /// - internal static string DurationRequiredForTimeOuts { - get { - return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event {0} is cancelled!{1}. - /// - internal static string EventCancelled { - get { - return ResourceManager.GetString("EventCancelled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event {0} has completed! Duration: {1}. - /// - internal static string EventCompleted { - get { - return ResourceManager.GetString("EventCompleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}. - /// - internal static string EventCreated { - get { - return ResourceManager.GetString("EventCreated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. - /// - internal static string EventStarted { - get { - return ResourceManager.GetString("EventStarted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ever. - /// - internal static string Ever { - get { - return ResourceManager.GetString("Ever", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to *[{0}: {1}]*. - /// - internal static string FeedbackFormat { - get { - return ResourceManager.GetString("FeedbackFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Kicked {0}: {1}. - /// - internal static string FeedbackMemberKicked { - get { - return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Muted {0} for{1}: {2}. - /// - internal static string FeedbackMemberMuted { - get { - return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unmuted {0}: {1}. - /// - internal static string FeedbackMemberUnmuted { - get { - return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deleted {0} messages in {1}. - /// - internal static string FeedbackMessagesCleared { - get { - return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. - /// - internal static string FeedbackSettingsUpdated { - get { - return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Banned {0} for{1}: {2}. - /// - internal static string FeedbackUserBanned { - get { - return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unbanned {0}: {1}. - /// - internal static string FeedbackUserUnbanned { - get { - return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Members are in different guilds!. - /// - internal static string InteractionsDifferentGuilds { - get { - return ResourceManager.GetString("InteractionsDifferentGuilds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot interact with this member!. - /// - internal static string InteractionsFailedBot { - get { - return ResourceManager.GetString("InteractionsFailedBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot interact with this member!. - /// - internal static string InteractionsFailedUser { - get { - return ResourceManager.GetString("InteractionsFailedUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot interact with me!. - /// - internal static string InteractionsMe { - get { - return ResourceManager.GetString("InteractionsMe", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot interact with guild owner!. - /// - internal static string InteractionsOwner { - get { - return ResourceManager.GetString("InteractionsOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot interact with yourself!. - /// - internal static string InteractionsYourself { - get { - return ResourceManager.GetString("InteractionsYourself", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This channel does not exist!. - /// - internal static string InvalidChannel { - get { - return ResourceManager.GetString("InvalidChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This role does not exist!. - /// - internal static string InvalidRole { - get { - return ResourceManager.GetString("InvalidRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid setting value specified!. - /// - internal static string InvalidSettingValue { - get { - return ResourceManager.GetString("InvalidSettingValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language not supported!. - /// - internal static string LanguageNotSupported { - get { - return ResourceManager.GetString("LanguageNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Member is already muted!. - /// - internal static string MemberAlreadyMuted { - get { - return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Member not muted!. - /// - internal static string MemberNotMuted { - get { - return ResourceManager.GetString("MemberNotMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} unmuted {1} for {2}. - /// - internal static string MemberUnmuted { - get { - return ResourceManager.GetString("MemberUnmuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ms. - /// - internal static string Milliseconds { - get { - return ResourceManager.GetString("Milliseconds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No. - /// - internal static string No { - get { - return ResourceManager.GetString("No", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not enough arguments! Needed: {0}, provided: {1}. - /// - internal static string NotEnoughArguments { - get { - return ResourceManager.GetString("NotEnoughArguments", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Punishment expired. - /// - internal static string PunishmentExpired { - get { - return ResourceManager.GetString("PunishmentExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}I'm ready! (C#). - /// internal static string Ready { get { return ResourceManager.GetString("Ready", resourceCulture); } } - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string RoleNotSpecified { - get { - return ResourceManager.GetString("RoleNotSpecified", resourceCulture); - } - } - - /// - /// 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!. - /// - internal static string SettingDoesntExist { - get { - return ResourceManager.GetString("SettingDoesntExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string SettingNotDefined { - get { - return ResourceManager.GetString("SettingNotDefined", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Admin log channel. - /// - internal static string SettingsAdminLogChannel { - get { - return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bot log channel. - /// - internal static string SettingsBotLogChannel { - get { - return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event cancellation notifications. - /// - internal static string SettingsEventCancelledChannel { - get { - return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event completion notifications. - /// - internal static string SettingsEventCompletedChannel { - get { - return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event creation notifications. - /// - internal static string SettingsEventCreatedChannel { - get { - return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Role for event creation notifications. - /// - internal static string SettingsEventNotifyReceiverRole { - get { - return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event start notifications. - /// - internal static string SettingsEventStartedChannel { - get { - return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event start notifications receivers. - /// - internal static string SettingsEventStartedReceivers { - get { - return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to :(. - /// - internal static string SettingsFrowningFace { - get { - return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language. - /// - internal static string SettingsLang { - get { - return ResourceManager.GetString("SettingsLang", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mute role. - /// - internal static string SettingsMuteRole { - get { - return ResourceManager.GetString("SettingsMuteRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. - /// - internal static string SettingsNothingChanged { - get { - return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prefix. - /// - internal static string SettingsPrefix { - get { - return ResourceManager.GetString("SettingsPrefix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Receive startup messages. - /// - internal static string SettingsReceiveStartupMessages { - get { - return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remove roles on mute. - /// - internal static string SettingsRemoveRolesOnMute { - get { - return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send welcome messages. - /// - internal static string SettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Starter role. - /// - internal static string SettingsStarterRole { - get { - return ResourceManager.GetString("SettingsStarterRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Welcome message. - /// - internal static string SettingsWelcomeMessage { - get { - return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Message deleted in {0}, but I forgot what was there. - /// internal static string UncachedMessageDeleted { get { return ResourceManager.GetString("UncachedMessageDeleted", resourceCulture); } } - /// - /// Looks up a localized string similar to Message edited from {0} in channel {1}, but I forgot what was there before the edit: {2}. - /// + internal static string CachedMessageDeleted { + get { + return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); + } + } + + internal static string AutobanReason { + get { + return ResourceManager.GetString("AutobanReason", resourceCulture); + } + } + internal static string UncachedMessageEdited { get { return ResourceManager.GetString("UncachedMessageEdited", resourceCulture); } } - /// - /// Looks up a localized string similar to That user doesn't exist!. - /// - internal static string UserDoesntExist { + internal static string CachedMessageEdited { get { - return ResourceManager.GetString("UserDoesntExist", resourceCulture); + return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - /// - /// Looks up a localized string similar to User not banned!. - /// - internal static string UserNotBanned { + internal static string DefaultWelcomeMessage { get { - return ResourceManager.GetString("UserNotBanned", resourceCulture); + return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified user is not a member of this server!. - /// - internal static string UserNotInGuild { + internal static string Beep1 { get { - return ResourceManager.GetString("UserNotInGuild", resourceCulture); + return ResourceManager.GetString("Beep1", resourceCulture); } } - /// - /// Looks up a localized string similar to {0} unbanned {1} for {2}. - /// - internal static string UserUnbanned { + internal static string Beep2 { get { - return ResourceManager.GetString("UserUnbanned", resourceCulture); + return ResourceManager.GetString("Beep2", resourceCulture); } } - /// - /// Looks up a localized string similar to Yes. - /// - internal static string Yes { + internal static string Beep3 { get { - return ResourceManager.GetString("Yes", resourceCulture); + return ResourceManager.GetString("Beep3", resourceCulture); + } + } + + internal static string CommandNoPermissionBot { + get { + return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); + } + } + + internal static string CommandNoPermissionUser { + get { + return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); + } + } + + internal static string InteractionsDifferentGuilds { + get { + return ResourceManager.GetString("InteractionsDifferentGuilds", resourceCulture); + } + } + + internal static string InteractionsOwner { + get { + return ResourceManager.GetString("InteractionsOwner", resourceCulture); + } + } + + internal static string InteractionsYourself { + get { + return ResourceManager.GetString("InteractionsYourself", resourceCulture); + } + } + + internal static string InteractionsMe { + get { + return ResourceManager.GetString("InteractionsMe", resourceCulture); + } + } + + internal static string InteractionsFailedUser { + get { + return ResourceManager.GetString("InteractionsFailedUser", resourceCulture); + } + } + + internal static string InteractionsFailedBot { + get { + return ResourceManager.GetString("InteractionsFailedBot", resourceCulture); } } - /// - /// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. - /// internal static string YouWereBanned { get { return ResourceManager.GetString("YouWereBanned", resourceCulture); } } - /// - /// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. - /// + internal static string PunishmentExpired { + get { + return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + + internal static string ClearNegativeAmount { + get { + return ResourceManager.GetString("ClearNegativeAmount", resourceCulture); + } + } + + internal static string ClearAmountTooLarge { + get { + return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); + } + } + + internal static string CommandHelp { + get { + return ResourceManager.GetString("CommandHelp", resourceCulture); + } + } + internal static string YouWereKicked { get { return ResourceManager.GetString("YouWereKicked", resourceCulture); } } + + internal static string Milliseconds { + get { + return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + + internal static string MemberAlreadyMuted { + get { + return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + + internal static string ChannelNotSpecified { + get { + return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); + } + } + + internal static string RoleNotSpecified { + get { + return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + } + } + + internal static string CurrentSettings { + get { + return ResourceManager.GetString("CurrentSettings", resourceCulture); + } + } + + internal static string SettingsLang { + get { + return ResourceManager.GetString("SettingsLang", resourceCulture); + } + } + + internal static string SettingsPrefix { + get { + return ResourceManager.GetString("SettingsPrefix", resourceCulture); + } + } + + internal static string SettingsRemoveRolesOnMute { + get { + return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); + } + } + + internal static string SettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); + } + } + + internal static string SettingsStarterRole { + get { + return ResourceManager.GetString("SettingsStarterRole", resourceCulture); + } + } + + internal static string SettingsMuteRole { + get { + return ResourceManager.GetString("SettingsMuteRole", resourceCulture); + } + } + + internal static string SettingsAdminLogChannel { + get { + return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); + } + } + + internal static string SettingsBotLogChannel { + get { + return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); + } + } + + internal static string LanguageNotSupported { + get { + return ResourceManager.GetString("LanguageNotSupported", resourceCulture); + } + } + + internal static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } + + internal static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + + internal static string UserNotBanned { + get { + return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + + internal static string MemberNotMuted { + get { + return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + + internal static string RolesReturned { + get { + return ResourceManager.GetString("RolesReturned", resourceCulture); + } + } + + internal static string MemberUnmuted { + get { + return ResourceManager.GetString("MemberUnmuted", resourceCulture); + } + } + + internal static string UserUnbanned { + get { + return ResourceManager.GetString("UserUnbanned", resourceCulture); + } + } + + internal static string SettingsWelcomeMessage { + get { + return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); + } + } + + internal static string NotEnoughArguments { + get { + return ResourceManager.GetString("NotEnoughArguments", resourceCulture); + } + } + + internal static string ClearInvalidAmountSpecified { + get { + return ResourceManager.GetString("ClearInvalidAmountSpecified", resourceCulture); + } + } + + internal static string FeedbackUserBanned { + get { + return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); + } + } + + internal static string UserNotInGuild { + get { + return ResourceManager.GetString("UserNotInGuild", resourceCulture); + } + } + + internal static string SettingDoesntExist { + get { + return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + } + } + + internal static string SettingsReceiveStartupMessages { + get { + return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); + } + } + + internal static string InvalidSettingValue { + get { + return ResourceManager.GetString("InvalidSettingValue", resourceCulture); + } + } + + internal static string InvalidRole { + get { + return ResourceManager.GetString("InvalidRole", resourceCulture); + } + } + + internal static string InvalidChannel { + get { + return ResourceManager.GetString("InvalidChannel", resourceCulture); + } + } + + internal static string DurationParseFailed { + get { + return ResourceManager.GetString("DurationParseFailed", resourceCulture); + } + } + + internal static string RoleRemovalFailed { + get { + return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); + } + } + + internal static string DurationRequiredForTimeOuts { + get { + return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); + } + } + + internal static string CannotTimeOutBot { + get { + return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); + } + } + + internal static string EventCreated { + get { + return ResourceManager.GetString("EventCreated", resourceCulture); + } + } + + internal static string SettingsEventNotifyReceiverRole { + get { + return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); + } + } + + internal static string SettingsEventCreatedChannel { + get { + return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); + } + } + + internal static string SettingsEventStartedChannel { + get { + return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); + } + } + + internal static string SettingsEventStartedReceivers { + get { + return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); + } + } + + internal static string EventStarted { + get { + return ResourceManager.GetString("EventStarted", resourceCulture); + } + } + + internal static string SettingsFrowningFace { + get { + return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); + } + } + + internal static string EventCancelled { + get { + return ResourceManager.GetString("EventCancelled", resourceCulture); + } + } + + internal static string SettingsEventCancelledChannel { + get { + return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); + } + } + + internal static string SettingsEventCompletedChannel { + get { + return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); + } + } + + internal static string EventCompleted { + get { + return ResourceManager.GetString("EventCompleted", resourceCulture); + } + } + + internal static string UserDoesntExist { + get { + return ResourceManager.GetString("UserDoesntExist", resourceCulture); + } + } + + internal static string FeedbackFormat { + get { + return ResourceManager.GetString("FeedbackFormat", resourceCulture); + } + } + + internal static string Ever { + get { + return ResourceManager.GetString("Ever", resourceCulture); + } + } + + internal static string FeedbackMessagesCleared { + get { + return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); + } + } + + internal static string FeedbackMemberKicked { + get { + return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); + } + } + + internal static string FeedbackMemberMuted { + get { + return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); + } + } + + internal static string FeedbackUserUnbanned { + get { + return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); + } + } + + internal static string FeedbackMemberUnmuted { + get { + return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); + } + } + + internal static string SettingsNothingChanged { + get { + return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); + } + } + + internal static string SettingNotDefined { + get { + return ResourceManager.GetString("SettingNotDefined", resourceCulture); + } + } + + internal static string FeedbackSettingsUpdated { + get { + return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); + } + } + + internal static string CommandDescriptionBan { + get { + return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); + } + } + + internal static string CommandDescriptionClear { + get { + return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); + } + } + + internal static string CommandDescriptionHelp { + get { + return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); + } + } + + internal static string CommandDescriptionKick { + get { + return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); + } + } + + internal static string CommandDescriptionMute { + get { + return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); + } + } + + internal static string CommandDescriptionPing { + get { + return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); + } + } + + internal static string CommandDescriptionSettings { + get { + return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); + } + } + + internal static string CommandDescriptionUnban { + get { + return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); + } + } + + internal static string CommandDescriptionUnmute { + get { + return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); + } + } + + internal static string ReasonRequired { + get { + return ResourceManager.GetString("ReasonRequired", resourceCulture); + } + } } } diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 1f8a6e8..ed21ef5 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -297,4 +297,7 @@ Unmutes a member + + You must specify a reason! + diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index e5eb732..f252015 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -288,4 +288,7 @@ Разглушает участника + + Требуется указать причину! + diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 32e3bfa..3238cd6 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -10,7 +10,6 @@ using Humanizer.Localisation; namespace Boyfriend; public static class Utils { - private static readonly string[] Formats = { "%d'd'%h'h'%m'm'%s's'", "%d'd'%h'h'%m'm'", "%d'd'%h'h'%s's'", "%d'd'%h'h'", "%d'd'%m'm'%s's'", "%d'd'%m'm'", "%d'd'%s's'", "%d'd'", "%h'h'%m'm'%s's'", "%h'h'%m'm'", "%h'h'%s's'", "%h'h'", "%m'm'%s's'", "%m'm'", "%s's'", @@ -21,10 +20,12 @@ public static class Utils { public static readonly Random Random = new(); private static readonly Dictionary ReflectionMessageCache = new(); + private static readonly Dictionary CultureInfoCache = new() { - {"ru", new CultureInfo("ru-RU")}, - {"en", new CultureInfo("en-US")} + { "ru", new CultureInfo("ru-RU") }, + { "en", new CultureInfo("en-US") } }; + private static readonly Dictionary MuteRoleCache = new(); private static readonly AllowedMentions AllowRoles = new() { @@ -40,19 +41,12 @@ public static class Utils { .GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(id)["AdminLogChannel"])); } - public static string Wrap(string? original) { - if (original == null) return ""; - var toReturn = original.Replace("```", "ˋˋˋ"); - return $"```{toReturn}{(toReturn.EndsWith("`") || toReturn.Trim().Equals("") ? " " : "")}```"; - } - - public static string? WrapInline(string? original) { - return original == null ? null : $"`{original.Replace("`", "ˋ")}`"; - } - - public static string? WrapAsNeeded(string? original) { + public static string? Wrap(string? original, bool limitedSpace = false) { if (original == null) return null; - return original.Contains('\n') ? Wrap(original) : WrapInline(original); + var maxChars = limitedSpace ? 970 : 1940; + if (original.Length > maxChars) original = original[..maxChars]; + var style = original.Contains('\n') ? "```" : "`"; + return $"{style}{original}{(original.Equals("") ? " " : "")}{style}"; } public static string MentionChannel(ulong id) { @@ -73,9 +67,7 @@ public static class Utils { } public static async Task SendDirectMessage(SocketUser user, string toSend) { - try { - await user.SendMessageAsync(toSend); - } catch (HttpException e) { + try { await user.SendMessageAsync(toSend); } catch (HttpException e) { if (e.DiscordCode != DiscordErrorCode.CannotSendMessageToUser) throw; } @@ -91,14 +83,21 @@ public static class Utils { MuteRoleCache.Add(id, role); break; } + return role; } + public static void RemoveMuteRoleFromCache(ulong id) { + if (MuteRoleCache.ContainsKey(id)) MuteRoleCache.Remove(id); + } + public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) { - if (channel == null || text.Length is 0 or > 2000) return; + if (channel == null || text.Length is 0 or > 2000) + throw new Exception($"Message length is out of range: {text.Length}"); await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); } + public static TimeSpan? GetTimeSpan(ref string from) { if (TimeSpan.TryParseExact(from.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) return timeSpan; @@ -122,7 +121,7 @@ public static class Utils { var toReturn = typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) - ?.ToString()! ?? throw new Exception($"Could not find localized property: {name}"); + ?.ToString()! ?? throw new Exception($"Could not find localized property: {propertyName}"); ReflectionMessageCache.Add(name, toReturn); return toReturn; } @@ -144,11 +143,12 @@ public static class Utils { } public static string GetHumanizedTimeOffset(ref TimeSpan span) { - return span.TotalSeconds > 0 ? $" {span.Humanize(minUnit: TimeUnit.Second, culture: Messages.Culture)}" + return span.TotalSeconds > 0 + ? $" {span.Humanize(minUnit: TimeUnit.Second, culture: Messages.Culture)}" : Messages.Ever; } public static void SetCurrentLanguage(ulong guildId) { Messages.Culture = CultureInfoCache[Boyfriend.GetGuildConfig(guildId)["Lang"]]; } -} +} \ No newline at end of file From a06376443fa45c3c4ec0ae4206994b8837c0bc0e Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Fri, 15 Jul 2022 18:32:54 +0500 Subject: [PATCH 014/329] Remove timeout from user if it exists, but there is a mute role configured --- Boyfriend/Commands/UnmuteCommand.cs | 7 ++----- Boyfriend/EventHandler.cs | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index f0e7951..5659e17 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -39,7 +39,7 @@ public class UnmuteCommand : Command { var requestOptions = Utils.GetRequestOptions($"({author}) {reason}"); var role = Utils.GetMuteRole(ref guild); - if (role != null) { + if (role != null && toUnmute.Roles.Contains(role)) { var rolesRemoved = Boyfriend.GetRemovedRoles(guild.Id); if (rolesRemoved.ContainsKey(toUnmute.Id)) { @@ -48,10 +48,7 @@ public class UnmuteCommand : Command { CommandHandler.ConfigWriteScheduled = true; } - if (toUnmute.Roles.Contains(role)) { await toUnmute.RemoveRoleAsync(role, requestOptions); } else { - Error(Messages.MemberNotMuted, false); - return; - } + await toUnmute.RemoveRoleAsync(role, requestOptions); } else { if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() < DateTimeOffset.Now.ToUnixTimeMilliseconds()) { diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index fd31995..f562d38 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -57,9 +57,8 @@ public class EventHandler { } private static async Task MessageReceivedEvent(SocketMessage messageParam) { - if (messageParam is not SocketUserMessage message) return; + if (messageParam is not SocketUserMessage { Author: SocketGuildUser user } message) return; - var user = (SocketGuildUser)message.Author; var guild = user.Guild; var guildConfig = Boyfriend.GetGuildConfig(guild.Id); From 2493e317d02ec44dc1bcb56d17d6b93d7cfa5080 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Fri, 15 Jul 2022 21:23:45 +0500 Subject: [PATCH 015/329] fixed a few bugs in settings + code style consistency with string comparison --- Boyfriend/Commands/BanCommand.cs | 8 ++-- Boyfriend/Commands/ClearCommand.cs | 6 +-- Boyfriend/Commands/KickCommand.cs | 4 +- Boyfriend/Commands/MuteCommand.cs | 16 ++++--- Boyfriend/Commands/SettingsCommand.cs | 68 +++++++++++++++++---------- Boyfriend/Commands/UnbanCommand.cs | 2 +- Boyfriend/Commands/UnmuteCommand.cs | 9 ++-- Boyfriend/EventHandler.cs | 10 ++-- 8 files changed, 72 insertions(+), 51 deletions(-) diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index b2cad58..ce83d35 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -20,7 +20,7 @@ public class BanCommand : Command { var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.BanMembers); - if (permissionCheckResponse != "") { + if (permissionCheckResponse is not "") { Error(permissionCheckResponse, true); return; } @@ -30,7 +30,7 @@ public class BanCommand : Command { if (memberToBan != null) { var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref memberToBan); - if (interactionCheckResponse != "") { + if (interactionCheckResponse is not "") { Error(interactionCheckResponse, true); return; } @@ -41,7 +41,7 @@ public class BanCommand : Command { Warn(Messages.DurationParseFailed); reason = Utils.JoinString(ref args, 1); - if (reason == "") { + if (reason is "") { Error(Messages.ReasonRequired, false); return; } @@ -73,4 +73,4 @@ public class BanCommand : Command { new Task(DelayUnban).Start(); } } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs index 504c396..6b51075 100644 --- a/Boyfriend/Commands/ClearCommand.cs +++ b/Boyfriend/Commands/ClearCommand.cs @@ -5,16 +5,16 @@ using Discord.WebSocket; namespace Boyfriend.Commands; public class ClearCommand : Command { - public override string[] Aliases { get; } = {"clear", "purge", "очистить", "стереть"}; + public override string[] Aliases { get; } = { "clear", "purge", "очистить", "стереть" }; public override int ArgsLengthRequired => 1; public override async Task Run(SocketCommandContext context, string[] args) { - var user = (SocketGuildUser) context.User; + var user = (SocketGuildUser)context.User; if (context.Channel is not SocketTextChannel channel) throw new Exception(); var permissionCheckResponse = CommandHandler.HasPermission(ref user, GuildPermission.ManageMessages); - if (permissionCheckResponse != "") { + if (permissionCheckResponse is not "") { Error(permissionCheckResponse, true); return; } diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index 505178e..fa1ab32 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -12,7 +12,7 @@ public class KickCommand : Command { var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.KickMembers); - if (permissionCheckResponse != "") { + if (permissionCheckResponse is not "") { Error(permissionCheckResponse, true); return; } @@ -25,7 +25,7 @@ public class KickCommand : Command { } var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toKick); - if (interactionCheckResponse != "") { + if (interactionCheckResponse is not "") { Error(interactionCheckResponse, true); return; } diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index 47c3ded..7e9fb13 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -18,7 +18,7 @@ public class MuteCommand : Command { Warn(Messages.DurationParseFailed); reason = Utils.JoinString(ref args, 1); - if (reason == "") { + if (reason is "") { Error(Messages.ReasonRequired, false); return; } @@ -53,13 +53,13 @@ public class MuteCommand : Command { var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ModerateMembers); - if (permissionCheckResponse != "") { + if (permissionCheckResponse is not "") { Error(permissionCheckResponse, true); return; } var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toMute); - if (interactionCheckResponse != "") { + if (interactionCheckResponse is not "") { Error(interactionCheckResponse, true); return; } @@ -79,14 +79,15 @@ public class MuteCommand : Command { var hasDuration = duration.TotalSeconds > 0; if (role != null) { - if (config["RemoveRolesOnMute"] == "true") { + 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) { + } + catch (HttpException e) { Warn(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.Wrap(e.Reason))); } @@ -104,7 +105,8 @@ public class MuteCommand : Command { } await toMute.AddRoleAsync(role, requestOptions); - } else { + } + else { if (!hasDuration) { Error(Messages.DurationRequiredForTimeOuts, false); return; @@ -118,4 +120,4 @@ public class MuteCommand : Command { await toMute.SetTimeOutAsync(duration, requestOptions); } } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index cfd1c23..42bfb1c 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -12,7 +12,7 @@ public class SettingsCommand : Command { var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ManageGuild); - if (permissionCheckResponse != "") { + if (permissionCheckResponse is not "") { Error(permissionCheckResponse, true); return Task.CompletedTask; } @@ -32,14 +32,16 @@ public class SettingsCommand : Command { format = "<#{0}>"; else currentValue = Messages.ChannelNotSpecified; - } else if (setting.Key.EndsWith("Role")) { + } + else if (setting.Key.EndsWith("Role")) { if (guild.GetRole(Convert.ToUInt64(currentValue)) != null) format = "<@&{0}>"; else currentValue = Messages.RoleNotSpecified; - } else { + } + else { if (IsBool(currentValue)) - currentValue = YesOrNo(currentValue == "true"); + currentValue = YesOrNo(currentValue is "true"); else format = Utils.Wrap("{0}")!; } @@ -74,18 +76,23 @@ public class SettingsCommand : Command { if (args.Length >= 2) { value = Utils.JoinString(ref args, 1); - if (selectedSetting != "WelcomeMessage") + if (selectedSetting is "EventStartedReceivers") { value = value.Replace(" ", "").ToLower(); - if (value.StartsWith(",") || value.Count(x => x == ',') > 1) { - Error(Messages.InvalidSettingValue, false); - return Task.CompletedTask; + if (value.StartsWith(",") || value.Count(x => x == ',') > 1 || + (!value.Contains("interested") && !value.Contains("role"))) { + Error(Messages.InvalidSettingValue, false); + return Task.CompletedTask; + } } - } else { value = "reset"; } + } + else { + value = "reset"; + } if (IsBool(Boyfriend.DefaultConfig[selectedSetting]) && !IsBool(value)) { value = value switch { - "y" or "yes" => "true", - "n" or "no" => "false", + "y" or "yes" or "д" or "да" => "true", + "n" or "no" or "н" or "нет" => "false", _ => value }; if (!IsBool(value)) { @@ -97,26 +104,37 @@ public class SettingsCommand : Command { var localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); var mention = Utils.ParseMention(value); - if (mention != 0) value = mention.ToString(); + if (mention != 0 && selectedSetting is not "WelcomeMessage") value = mention.ToString(); var formatting = Utils.Wrap("{0}")!; - if (selectedSetting.EndsWith("Channel")) - formatting = "<#{0}>"; - if (selectedSetting.EndsWith("Role")) - formatting = "<@&{0}>"; - if (value is "0" or "reset" or "default") - formatting = Messages.SettingNotDefined; - var formattedValue = IsBool(value) ? YesOrNo(value == "true") : string.Format(formatting, value); + if (selectedSetting is not "WelcomeMessage") { + if (selectedSetting.EndsWith("Channel")) + formatting = "<#{0}>"; + if (selectedSetting.EndsWith("Role")) + formatting = "<@&{0}>"; + } + + var formattedValue = selectedSetting switch { + "WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage), + "EventStartedReceivers" => Utils.Wrap(Boyfriend.DefaultConfig[selectedSetting])!, + _ => value is "reset" or "default" + ? IsBool(value) ? YesOrNo(value is "true") : string.Format(formatting, value) + : Messages.SettingNotDefined + }; if (value is "reset" or "default") { - config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; - } else { + if (selectedSetting is "WelcomeMessage") + config[selectedSetting] = Messages.DefaultWelcomeMessage; + else + config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; + } + else { if (value == config[selectedSetting]) { Error(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), false); return Task.CompletedTask; } - if (selectedSetting == "Lang" && value is not "ru" and not "en") { + if (selectedSetting is "Lang" && value is not "ru" and not "en") { Error(Messages.LanguageNotSupported, false); return Task.CompletedTask; } @@ -131,12 +149,12 @@ public class SettingsCommand : Command { return Task.CompletedTask; } - if (selectedSetting == "MuteRole") Utils.RemoveMuteRoleFromCache(ulong.Parse(config[selectedSetting])); + if (selectedSetting is "MuteRole") Utils.RemoveMuteRoleFromCache(ulong.Parse(config[selectedSetting])); config[selectedSetting] = value; } - if (selectedSetting == "Lang") { + if (selectedSetting is "Lang") { Utils.SetCurrentLanguage(guild.Id); localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); } @@ -155,4 +173,4 @@ public class SettingsCommand : Command { private static bool IsBool(string value) { return value is "true" or "false"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index a6576af..ac2edef 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -12,7 +12,7 @@ public class UnbanCommand : Command { var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.BanMembers); - if (permissionCheckResponse != "") { + if (permissionCheckResponse is not "") { Error(permissionCheckResponse, true); return; } diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index 5659e17..a5e8d76 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -12,7 +12,7 @@ public class UnmuteCommand : Command { var author = (SocketGuildUser)context.User; var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ModerateMembers); - if (permissionCheckResponse != "") { + if (permissionCheckResponse is not "") { Error(permissionCheckResponse, true); return; } @@ -25,7 +25,7 @@ public class UnmuteCommand : Command { } var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toUnmute); - if (interactionCheckResponse != "") { + if (interactionCheckResponse is not "") { Error(interactionCheckResponse, true); return; } @@ -49,7 +49,8 @@ public class UnmuteCommand : Command { } await toUnmute.RemoveRoleAsync(role, requestOptions); - } else { + } + else { if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() < DateTimeOffset.Now.ToUnixTimeMilliseconds()) { Error(Messages.MemberNotMuted, false); @@ -63,4 +64,4 @@ public class UnmuteCommand : Command { Success(feedback, author.Mention, false, false); await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); } -} +} \ No newline at end of file diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index f562d38..4b09fd7 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -29,7 +29,7 @@ public class EventHandler { var channel = guild.GetTextChannel(Convert.ToUInt64(config["BotLogChannel"])); Utils.SetCurrentLanguage(guild.Id); - if (config["ReceiveStartupMessages"] != "true" || channel == null) continue; + if (config["ReceiveStartupMessages"] is not "true" || channel == null) continue; await channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); } } @@ -112,11 +112,11 @@ public class EventHandler { var guild = user.Guild; var config = Boyfriend.GetGuildConfig(guild.Id); - if (config["SendWelcomeMessages"] == "true") + if (config["SendWelcomeMessages"] is "true") await Utils.SilentSendAsync(guild.SystemChannel, string.Format(config["WelcomeMessage"], user.Mention, guild.Name)); - if (config["StarterRole"] != "0") + if (config["StarterRole"] is not "0") await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); } @@ -147,7 +147,7 @@ public class EventHandler { var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCancelledChannel"])); if (channel != null) await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name), - eventConfig["FrowningFace"] == "true" ? $" {Messages.SettingsFrowningFace}" : "")); + eventConfig["FrowningFace"] is "true" ? $" {Messages.SettingsFrowningFace}" : "")); } private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) { @@ -180,4 +180,4 @@ public class EventHandler { await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); } -} +} \ No newline at end of file From b2b54b7fd4a4200e80c903e6b16349c1ae2a8469 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Fri, 5 Aug 2022 21:01:06 +0500 Subject: [PATCH 016/329] Allow mentions to act as command prefixes + bug fixes --- Boyfriend/CommandHandler.cs | 7 +++++-- Boyfriend/Commands/SettingsCommand.cs | 20 +++++++------------- Boyfriend/EventHandler.cs | 8 +++----- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs index 0cd494b..097993e 100644 --- a/Boyfriend/CommandHandler.cs +++ b/Boyfriend/CommandHandler.cs @@ -15,6 +15,7 @@ public static class CommandHandler { }; private static readonly Dictionary RegexCache = new(); + private static readonly Regex MentionRegex = new(Regex.Escape("<@855023234407333888>")); public static readonly StringBuilder StackedReplyMessage = new(); public static readonly StringBuilder StackedPublicFeedback = new(); @@ -43,7 +44,8 @@ public static class CommandHandler { foreach (var line in list) { currentLine++; foreach (var command in Commands) { - if (!command.Aliases.Contains(regex.Replace(line, "", 1).ToLower().Split()[0])) + var lineNoMention = MentionRegex.Replace(line, "", 1); + if (!command.Aliases.Contains(regex.Replace(lineNoMention, "", 1).Trim().ToLower().Split()[0])) continue; await context.Channel.TriggerTypingAsync(); @@ -58,7 +60,8 @@ public static class CommandHandler { if (currentLine != list.Length) continue; if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfig(guild.Id); - await message.ReplyAsync(StackedReplyMessage.ToString(), false, null, AllowedMentions.None); + if (StackedReplyMessage.Length > 0) + await message.ReplyAsync(StackedReplyMessage.ToString(), false, null, AllowedMentions.None); var adminChannel = Utils.GetAdminLogChannel(guild.Id); var systemChannel = guild.SystemChannel; diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 42bfb1c..0a7fabc 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -32,14 +32,12 @@ public class SettingsCommand : Command { format = "<#{0}>"; else currentValue = Messages.ChannelNotSpecified; - } - else if (setting.Key.EndsWith("Role")) { + } else if (setting.Key.EndsWith("Role")) { if (guild.GetRole(Convert.ToUInt64(currentValue)) != null) format = "<@&{0}>"; else currentValue = Messages.RoleNotSpecified; - } - else { + } else { if (IsBool(currentValue)) currentValue = YesOrNo(currentValue is "true"); else @@ -84,10 +82,7 @@ public class SettingsCommand : Command { return Task.CompletedTask; } } - } - else { - value = "reset"; - } + } else { value = "reset"; } if (IsBool(Boyfriend.DefaultConfig[selectedSetting]) && !IsBool(value)) { value = value switch { @@ -117,9 +112,9 @@ public class SettingsCommand : Command { var formattedValue = selectedSetting switch { "WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage), "EventStartedReceivers" => Utils.Wrap(Boyfriend.DefaultConfig[selectedSetting])!, - _ => value is "reset" or "default" - ? IsBool(value) ? YesOrNo(value is "true") : string.Format(formatting, value) - : Messages.SettingNotDefined + _ => value is "reset" or "default" ? Messages.SettingNotDefined + : IsBool(value) ? YesOrNo(value is "true") + : string.Format(formatting, value) }; if (value is "reset" or "default") { @@ -127,8 +122,7 @@ public class SettingsCommand : Command { config[selectedSetting] = Messages.DefaultWelcomeMessage; else config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; - } - else { + } else { if (value == config[selectedSetting]) { Error(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), false); return Task.CompletedTask; diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 4b09fd7..d47ff07 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,6 +1,5 @@ using Boyfriend.Commands; using Discord; -using Discord.Commands; using Discord.Rest; using Discord.WebSocket; @@ -82,9 +81,8 @@ public class EventHandler { prevFailsafe = prevsArray[2].Content; } - if (!(message.HasStringPrefix(guildConfig["Prefix"], ref argPos) || - message.HasMentionPrefix(Boyfriend.Client.CurrentUser, ref argPos)) || user == guild.CurrentUser || - (user.IsBot && (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)))) + if (user == guild.CurrentUser || (user.IsBot && + (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)))) return; await CommandHandler.HandleCommand(message); @@ -180,4 +178,4 @@ public class EventHandler { await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); } -} \ No newline at end of file +} From c57b8452176ab47cddc91612e622141defe92684 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 5 Aug 2022 21:28:50 +0500 Subject: [PATCH 017/329] Remove unused variables --- Boyfriend/EventHandler.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index d47ff07..91b8a25 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -59,7 +59,6 @@ public class EventHandler { if (messageParam is not SocketUserMessage { Author: SocketGuildUser user } message) return; var guild = user.Guild; - var guildConfig = Boyfriend.GetGuildConfig(guild.Id); Utils.SetCurrentLanguage(guild.Id); @@ -70,7 +69,6 @@ public class EventHandler { return; } - var argPos = 0; var prev = ""; var prevFailsafe = ""; var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); @@ -178,4 +176,4 @@ public class EventHandler { await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); } -} +} \ No newline at end of file From 51c24c1e2317f2d7c86d63b2ae287c8b45a03650 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 22 Aug 2022 19:48:51 +0500 Subject: [PATCH 018/329] Custom duration parser + bugfixes --- Boyfriend/Commands/MuteCommand.cs | 29 +- Boyfriend/Messages.Designer.cs | 1190 ++++++++++++++++++----------- Boyfriend/Messages.resx | 2 +- Boyfriend/Messages.ru.resx | 2 +- Boyfriend/Utils.cs | 41 +- 5 files changed, 786 insertions(+), 478 deletions(-) diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index 7e9fb13..7dd8aef 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -17,11 +17,12 @@ public class MuteCommand : Command { if (duration.TotalSeconds < 0) { Warn(Messages.DurationParseFailed); reason = Utils.JoinString(ref args, 1); + } - if (reason is "") { - Error(Messages.ReasonRequired, false); - return; - } + + if (reason is "") { + Error(Messages.ReasonRequired, false); + return; } if (toMute == null) { @@ -65,10 +66,6 @@ public class MuteCommand : Command { } await MuteMember(guild, author, toMute, duration, reason); - - Success( - string.Format(Messages.FeedbackMemberMuted, toMute.Mention, Utils.GetHumanizedTimeOffset(ref duration), - Utils.Wrap(reason)), author.Mention, true); } private static async Task MuteMember(SocketGuild guild, SocketUser author, SocketGuildUser toMute, @@ -86,8 +83,7 @@ public class MuteCommand : Command { if (userRole == guild.EveryoneRole || userRole == role) continue; await toMute.RemoveRoleAsync(role); rolesRemoved.Add(userRole.Id); - } - catch (HttpException e) { + } catch (HttpException e) { Warn(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.Wrap(e.Reason))); } @@ -105,9 +101,8 @@ public class MuteCommand : Command { } await toMute.AddRoleAsync(role, requestOptions); - } - else { - if (!hasDuration) { + } else { + if (!hasDuration || duration.TotalDays > 28) { Error(Messages.DurationRequiredForTimeOuts, false); return; } @@ -119,5 +114,11 @@ public class MuteCommand : Command { await toMute.SetTimeOutAsync(duration, requestOptions); } + + var feedback = string.Format(Messages.FeedbackMemberMuted, toMute.Mention, + Utils.GetHumanizedTimeOffset(ref duration), + Utils.Wrap(reason)); + Success(feedback, author.Mention, false, false); + await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); } -} \ No newline at end of file +} diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 08276fc..3b588bf 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -11,32 +11,46 @@ namespace Boyfriend { using System; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -45,556 +59,832 @@ namespace Boyfriend { } } - internal static string CouldntFindGuildByChannel { - get { - return ResourceManager.GetString("CouldntFindGuildByChannel", resourceCulture); - } - } - - internal static string Ready { - get { - return ResourceManager.GetString("Ready", resourceCulture); - } - } - - internal static string UncachedMessageDeleted { - get { - return ResourceManager.GetString("UncachedMessageDeleted", resourceCulture); - } - } - - internal static string CachedMessageDeleted { - get { - return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); - } - } - + /// + /// Looks up a localized string similar to Too many mentions in 1 message. + /// internal static string AutobanReason { get { return ResourceManager.GetString("AutobanReason", resourceCulture); } } - internal static string UncachedMessageEdited { - get { - return ResourceManager.GetString("UncachedMessageEdited", resourceCulture); - } - } - - internal static string CachedMessageEdited { - get { - return ResourceManager.GetString("CachedMessageEdited", resourceCulture); - } - } - - internal static string DefaultWelcomeMessage { - get { - return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); - } - } - + /// + /// Looks up a localized string similar to Bah! . + /// internal static string Beep1 { get { return ResourceManager.GetString("Beep1", resourceCulture); } } + /// + /// Looks up a localized string similar to Bop! . + /// internal static string Beep2 { get { return ResourceManager.GetString("Beep2", resourceCulture); } } + /// + /// Looks up a localized string similar to Beep! . + /// internal static string Beep3 { get { return ResourceManager.GetString("Beep3", resourceCulture); } } - internal static string CommandNoPermissionBot { + /// + /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. + /// + internal static string CachedMessageDeleted { get { - return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); + return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - internal static string CommandNoPermissionUser { + /// + /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. + /// + internal static string CachedMessageEdited { get { - return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); - } - } - - internal static string InteractionsDifferentGuilds { - get { - return ResourceManager.GetString("InteractionsDifferentGuilds", resourceCulture); - } - } - - internal static string InteractionsOwner { - get { - return ResourceManager.GetString("InteractionsOwner", resourceCulture); - } - } - - internal static string InteractionsYourself { - get { - return ResourceManager.GetString("InteractionsYourself", resourceCulture); - } - } - - internal static string InteractionsMe { - get { - return ResourceManager.GetString("InteractionsMe", resourceCulture); - } - } - - internal static string InteractionsFailedUser { - get { - return ResourceManager.GetString("InteractionsFailedUser", resourceCulture); - } - } - - internal static string InteractionsFailedBot { - get { - return ResourceManager.GetString("InteractionsFailedBot", resourceCulture); - } - } - - internal static string YouWereBanned { - get { - return ResourceManager.GetString("YouWereBanned", resourceCulture); - } - } - - internal static string PunishmentExpired { - get { - return ResourceManager.GetString("PunishmentExpired", resourceCulture); - } - } - - internal static string ClearNegativeAmount { - get { - return ResourceManager.GetString("ClearNegativeAmount", resourceCulture); - } - } - - internal static string ClearAmountTooLarge { - get { - return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); - } - } - - internal static string CommandHelp { - get { - return ResourceManager.GetString("CommandHelp", resourceCulture); - } - } - - internal static string YouWereKicked { - get { - return ResourceManager.GetString("YouWereKicked", resourceCulture); - } - } - - internal static string Milliseconds { - get { - return ResourceManager.GetString("Milliseconds", resourceCulture); - } - } - - internal static string MemberAlreadyMuted { - get { - return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - - internal static string ChannelNotSpecified { - get { - return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); - } - } - - internal static string RoleNotSpecified { - get { - return ResourceManager.GetString("RoleNotSpecified", resourceCulture); - } - } - - internal static string CurrentSettings { - get { - return ResourceManager.GetString("CurrentSettings", resourceCulture); - } - } - - internal static string SettingsLang { - get { - return ResourceManager.GetString("SettingsLang", resourceCulture); - } - } - - internal static string SettingsPrefix { - get { - return ResourceManager.GetString("SettingsPrefix", resourceCulture); - } - } - - internal static string SettingsRemoveRolesOnMute { - get { - return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); - } - } - - internal static string SettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); - } - } - - internal static string SettingsStarterRole { - get { - return ResourceManager.GetString("SettingsStarterRole", resourceCulture); - } - } - - internal static string SettingsMuteRole { - get { - return ResourceManager.GetString("SettingsMuteRole", resourceCulture); - } - } - - internal static string SettingsAdminLogChannel { - get { - return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); - } - } - - internal static string SettingsBotLogChannel { - get { - return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); - } - } - - internal static string LanguageNotSupported { - get { - return ResourceManager.GetString("LanguageNotSupported", resourceCulture); - } - } - - internal static string Yes { - get { - return ResourceManager.GetString("Yes", resourceCulture); - } - } - - internal static string No { - get { - return ResourceManager.GetString("No", resourceCulture); - } - } - - internal static string UserNotBanned { - get { - return ResourceManager.GetString("UserNotBanned", resourceCulture); - } - } - - internal static string MemberNotMuted { - get { - return ResourceManager.GetString("MemberNotMuted", resourceCulture); - } - } - - internal static string RolesReturned { - get { - return ResourceManager.GetString("RolesReturned", resourceCulture); - } - } - - internal static string MemberUnmuted { - get { - return ResourceManager.GetString("MemberUnmuted", resourceCulture); - } - } - - internal static string UserUnbanned { - get { - return ResourceManager.GetString("UserUnbanned", resourceCulture); - } - } - - internal static string SettingsWelcomeMessage { - get { - return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); - } - } - - internal static string NotEnoughArguments { - get { - return ResourceManager.GetString("NotEnoughArguments", resourceCulture); - } - } - - internal static string ClearInvalidAmountSpecified { - get { - return ResourceManager.GetString("ClearInvalidAmountSpecified", resourceCulture); - } - } - - internal static string FeedbackUserBanned { - get { - return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); - } - } - - internal static string UserNotInGuild { - get { - return ResourceManager.GetString("UserNotInGuild", resourceCulture); - } - } - - internal static string SettingDoesntExist { - get { - return ResourceManager.GetString("SettingDoesntExist", resourceCulture); - } - } - - internal static string SettingsReceiveStartupMessages { - get { - return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); - } - } - - internal static string InvalidSettingValue { - get { - return ResourceManager.GetString("InvalidSettingValue", resourceCulture); - } - } - - internal static string InvalidRole { - get { - return ResourceManager.GetString("InvalidRole", resourceCulture); - } - } - - internal static string InvalidChannel { - get { - return ResourceManager.GetString("InvalidChannel", resourceCulture); - } - } - - internal static string DurationParseFailed { - get { - return ResourceManager.GetString("DurationParseFailed", resourceCulture); - } - } - - internal static string RoleRemovalFailed { - get { - return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); - } - } - - internal static string DurationRequiredForTimeOuts { - get { - return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); + return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } + /// + /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. + /// internal static string CannotTimeOutBot { get { return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); } } - internal static string EventCreated { + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string ChannelNotSpecified { get { - return ResourceManager.GetString("EventCreated", resourceCulture); + return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); } } - internal static string SettingsEventNotifyReceiverRole { + /// + /// Looks up a localized string similar to Too many messages specified!. + /// + internal static string ClearAmountTooLarge { get { - return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); + return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); } } - internal static string SettingsEventCreatedChannel { + /// + /// Looks up a localized string similar to Invalid message amount specified!. + /// + internal static string ClearInvalidAmountSpecified { get { - return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); + return ResourceManager.GetString("ClearInvalidAmountSpecified", resourceCulture); } } - internal static string SettingsEventStartedChannel { + /// + /// Looks up a localized string similar to Negative message amount specified!. + /// + internal static string ClearNegativeAmount { get { - return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); - } - } - - internal static string SettingsEventStartedReceivers { - get { - return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); - } - } - - internal static string EventStarted { - get { - return ResourceManager.GetString("EventStarted", resourceCulture); - } - } - - internal static string SettingsFrowningFace { - get { - return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); - } - } - - internal static string EventCancelled { - get { - return ResourceManager.GetString("EventCancelled", resourceCulture); - } - } - - internal static string SettingsEventCancelledChannel { - get { - return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); - } - } - - internal static string SettingsEventCompletedChannel { - get { - return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); - } - } - - internal static string EventCompleted { - get { - return ResourceManager.GetString("EventCompleted", resourceCulture); - } - } - - internal static string UserDoesntExist { - get { - return ResourceManager.GetString("UserDoesntExist", resourceCulture); - } - } - - internal static string FeedbackFormat { - get { - return ResourceManager.GetString("FeedbackFormat", resourceCulture); - } - } - - internal static string Ever { - get { - return ResourceManager.GetString("Ever", resourceCulture); - } - } - - internal static string FeedbackMessagesCleared { - get { - return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); - } - } - - internal static string FeedbackMemberKicked { - get { - return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); - } - } - - internal static string FeedbackMemberMuted { - get { - return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); - } - } - - internal static string FeedbackUserUnbanned { - get { - return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); - } - } - - internal static string FeedbackMemberUnmuted { - get { - return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); - } - } - - internal static string SettingsNothingChanged { - get { - return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); - } - } - - internal static string SettingNotDefined { - get { - return ResourceManager.GetString("SettingNotDefined", resourceCulture); - } - } - - internal static string FeedbackSettingsUpdated { - get { - return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); + return ResourceManager.GetString("ClearNegativeAmount", resourceCulture); } } + /// + /// Looks up a localized string similar to Bans a user. + /// internal static string CommandDescriptionBan { get { return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); } } + /// + /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. + /// internal static string CommandDescriptionClear { get { return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); } } + /// + /// Looks up a localized string similar to Shows this message. + /// internal static string CommandDescriptionHelp { get { return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); } } + /// + /// Looks up a localized string similar to Kicks a member. + /// internal static string CommandDescriptionKick { get { return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); } } + /// + /// Looks up a localized string similar to Mutes a member. + /// internal static string CommandDescriptionMute { get { return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); } } + /// + /// Looks up a localized string similar to Shows latency to Discord servers (not counting local processing time). + /// internal static string CommandDescriptionPing { get { return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); } } + /// + /// Looks up a localized string similar to Allows you to change certain preferences for this guild. + /// internal static string CommandDescriptionSettings { get { return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); } } + /// + /// Looks up a localized string similar to Unbans a user. + /// internal static string CommandDescriptionUnban { get { return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); } } + /// + /// Looks up a localized string similar to Unmutes a member. + /// internal static string CommandDescriptionUnmute { get { return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); } } + /// + /// Looks up a localized string similar to Command help:. + /// + internal static string CommandHelp { + get { + return ResourceManager.GetString("CommandHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I do not have permission to execute this command!. + /// + internal static string CommandNoPermissionBot { + get { + return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You do not have permission to execute this command!. + /// + internal static string CommandNoPermissionUser { + get { + return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find guild by message!. + /// + internal static string CouldntFindGuildByChannel { + get { + return ResourceManager.GetString("CouldntFindGuildByChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current settings:. + /// + internal static string CurrentSettings { + get { + return ResourceManager.GetString("CurrentSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}, welcome to {1}. + /// + internal static string DefaultWelcomeMessage { + get { + return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I couldn't parse the specified duration! One of the components could be outside it's valid range (e.g. `24h` or `60m`). + /// + internal static string DurationParseFailed { + get { + return ResourceManager.GetString("DurationParseFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 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. + /// + internal static string DurationRequiredForTimeOuts { + get { + return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event {0} is cancelled!{1}. + /// + internal static string EventCancelled { + get { + return ResourceManager.GetString("EventCancelled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event {0} has completed! Duration: {1}. + /// + internal static string EventCompleted { + get { + return ResourceManager.GetString("EventCompleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}. + /// + internal static string EventCreated { + get { + return ResourceManager.GetString("EventCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. + /// + internal static string EventStarted { + get { + return ResourceManager.GetString("EventStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ever. + /// + internal static string Ever { + get { + return ResourceManager.GetString("Ever", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to *[{0}: {1}]*. + /// + internal static string FeedbackFormat { + get { + return ResourceManager.GetString("FeedbackFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kicked {0}: {1}. + /// + internal static string FeedbackMemberKicked { + get { + return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Muted {0} for{1}: {2}. + /// + internal static string FeedbackMemberMuted { + get { + return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unmuted {0}: {1}. + /// + internal static string FeedbackMemberUnmuted { + get { + return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleted {0} messages in {1}. + /// + internal static string FeedbackMessagesCleared { + get { + return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. + /// + internal static string FeedbackSettingsUpdated { + get { + return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Banned {0} for{1}: {2}. + /// + internal static string FeedbackUserBanned { + get { + return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unbanned {0}: {1}. + /// + internal static string FeedbackUserUnbanned { + get { + return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Members are in different guilds!. + /// + internal static string InteractionsDifferentGuilds { + get { + return ResourceManager.GetString("InteractionsDifferentGuilds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot interact with this member!. + /// + internal static string InteractionsFailedBot { + get { + return ResourceManager.GetString("InteractionsFailedBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot interact with this member!. + /// + internal static string InteractionsFailedUser { + get { + return ResourceManager.GetString("InteractionsFailedUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot interact with me!. + /// + internal static string InteractionsMe { + get { + return ResourceManager.GetString("InteractionsMe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot interact with guild owner!. + /// + internal static string InteractionsOwner { + get { + return ResourceManager.GetString("InteractionsOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot interact with yourself!. + /// + internal static string InteractionsYourself { + get { + return ResourceManager.GetString("InteractionsYourself", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This channel does not exist!. + /// + internal static string InvalidChannel { + get { + return ResourceManager.GetString("InvalidChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This role does not exist!. + /// + internal static string InvalidRole { + get { + return ResourceManager.GetString("InvalidRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid setting value specified!. + /// + internal static string InvalidSettingValue { + get { + return ResourceManager.GetString("InvalidSettingValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language not supported!. + /// + internal static string LanguageNotSupported { + get { + return ResourceManager.GetString("LanguageNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member is already muted!. + /// + internal static string MemberAlreadyMuted { + get { + return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member not muted!. + /// + internal static string MemberNotMuted { + get { + return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} unmuted {1} for {2}. + /// + internal static string MemberUnmuted { + get { + return ResourceManager.GetString("MemberUnmuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ms. + /// + internal static string Milliseconds { + get { + return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No. + /// + internal static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not enough arguments! Needed: {0}, provided: {1}. + /// + internal static string NotEnoughArguments { + get { + return ResourceManager.GetString("NotEnoughArguments", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Punishment expired. + /// + internal static string PunishmentExpired { + get { + return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}I'm ready! (C#). + /// + internal static string Ready { + get { + return ResourceManager.GetString("Ready", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You must specify a reason!. + /// internal static string ReasonRequired { get { return ResourceManager.GetString("ReasonRequired", resourceCulture); } } + + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string RoleNotSpecified { + get { + return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + } + } + + /// + /// 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!. + /// + internal static string SettingDoesntExist { + get { + return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string SettingNotDefined { + get { + return ResourceManager.GetString("SettingNotDefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Admin log channel. + /// + internal static string SettingsAdminLogChannel { + get { + return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bot log channel. + /// + internal static string SettingsBotLogChannel { + get { + return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event cancellation notifications. + /// + internal static string SettingsEventCancelledChannel { + get { + return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event completion notifications. + /// + internal static string SettingsEventCompletedChannel { + get { + return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event creation notifications. + /// + internal static string SettingsEventCreatedChannel { + get { + return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Role for event creation notifications. + /// + internal static string SettingsEventNotifyReceiverRole { + get { + return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event start notifications. + /// + internal static string SettingsEventStartedChannel { + get { + return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event start notifications receivers. + /// + internal static string SettingsEventStartedReceivers { + get { + return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to :(. + /// + internal static string SettingsFrowningFace { + get { + return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language. + /// + internal static string SettingsLang { + get { + return ResourceManager.GetString("SettingsLang", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mute role. + /// + internal static string SettingsMuteRole { + get { + return ResourceManager.GetString("SettingsMuteRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. + /// + internal static string SettingsNothingChanged { + get { + return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix. + /// + internal static string SettingsPrefix { + get { + return ResourceManager.GetString("SettingsPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Receive startup messages. + /// + internal static string SettingsReceiveStartupMessages { + get { + return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove roles on mute. + /// + internal static string SettingsRemoveRolesOnMute { + get { + return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send welcome messages. + /// + internal static string SettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Starter role. + /// + internal static string SettingsStarterRole { + get { + return ResourceManager.GetString("SettingsStarterRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome message. + /// + internal static string SettingsWelcomeMessage { + get { + return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message deleted in {0}, but I forgot what was there. + /// + internal static string UncachedMessageDeleted { + get { + return ResourceManager.GetString("UncachedMessageDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message edited from {0} in channel {1}, but I forgot what was there before the edit: {2}. + /// + internal static string UncachedMessageEdited { + get { + return ResourceManager.GetString("UncachedMessageEdited", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to That user doesn't exist!. + /// + internal static string UserDoesntExist { + get { + return ResourceManager.GetString("UserDoesntExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User not banned!. + /// + internal static string UserNotBanned { + get { + return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified user is not a member of this server!. + /// + internal static string UserNotInGuild { + get { + return ResourceManager.GetString("UserNotInGuild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} unbanned {1} for {2}. + /// + internal static string UserUnbanned { + get { + return ResourceManager.GetString("UserUnbanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yes. + /// + internal static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. + /// + internal static string YouWereBanned { + get { + return ResourceManager.GetString("YouWereBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. + /// + internal static string YouWereKicked { + get { + return ResourceManager.GetString("YouWereKicked", resourceCulture); + } + } } } diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index ed21ef5..4d1685d 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -199,7 +199,7 @@ I couldn't remove role {0} because of an error! {1} - I cannot mute someone forever using timeouts! Either specify a proper duration, or set a mute role in settings + 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 I cannot use time-outs on other bots! Try to set a mute role in settings diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index f252015..0c10ab5 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -187,7 +187,7 @@ Я не смог забрать роль {0} в связи с ошибкой! {1} - Я не могу заглушить кого-то навсегда, используя тайм-ауты! Или укажи правильную продолжительность, или установи роль мута в настройках + Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 3238cd6..292b611 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -10,14 +10,6 @@ using Humanizer.Localisation; namespace Boyfriend; public static class Utils { - private static readonly string[] Formats = { - "%d'd'%h'h'%m'm'%s's'", "%d'd'%h'h'%m'm'", "%d'd'%h'h'%s's'", "%d'd'%h'h'", "%d'd'%m'm'%s's'", "%d'd'%m'm'", - "%d'd'%s's'", "%d'd'", "%h'h'%m'm'%s's'", "%h'h'%m'm'", "%h'h'%s's'", "%h'h'", "%m'm'%s's'", "%m'm'", "%s's'", - - "%d'д'%h'ч'%m'м'%s'с'", "%d'д'%h'ч'%m'м'", "%d'д'%h'ч'%s'с'", "%d'д'%h'ч'", "%d'д'%m'м'%s'с'", "%d'д'%m'м'", - "%d'д'%s'с'", "%d'д'", "%h'ч'%m'м'%s'с'", "%h'ч'%m'м'", "%h'ч'%s'с'", "%h'ч'", "%m'м'%s'с'", "%m'м'", "%s'с'" - }; - public static readonly Random Random = new(); private static readonly Dictionary ReflectionMessageCache = new(); @@ -99,9 +91,34 @@ public static class Utils { } public static TimeSpan? GetTimeSpan(ref string from) { - if (TimeSpan.TryParseExact(from.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) - return timeSpan; - return null; + var chars = from.AsSpan(); + var numberBuilder = Boyfriend.StringBuilder; + int days = 0, hours = 0, minutes = 0, seconds = 0; + foreach (var c in chars) + if (char.IsDigit(c)) { numberBuilder.Append(c); } else { + if (numberBuilder.Length == 0) return null; + switch (c) { + case 'd' or 'D' or 'д' or 'Д': + days += int.Parse(numberBuilder.ToString()); + numberBuilder.Clear(); + break; + case 'h' or 'H' or 'ч' or 'Ч': + hours += int.Parse(numberBuilder.ToString()); + numberBuilder.Clear(); + break; + case 'm' or 'M' or 'м' or 'М': + minutes += int.Parse(numberBuilder.ToString()); + numberBuilder.Clear(); + break; + case 's' or 'S' or 'с' or 'С': + seconds += int.Parse(numberBuilder.ToString()); + numberBuilder.Clear(); + break; + default: return null; + } + } + + return new TimeSpan(days, hours, minutes, seconds); } public static string JoinString(ref string[] args, int startIndex) { @@ -144,7 +161,7 @@ public static class Utils { public static string GetHumanizedTimeOffset(ref TimeSpan span) { return span.TotalSeconds > 0 - ? $" {span.Humanize(minUnit: TimeUnit.Second, culture: Messages.Culture)}" + ? $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture)}" : Messages.Ever; } From 53f13d88a57dfc60162aeaeb9a4372d693f140c4 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 29 Aug 2022 21:24:38 +0500 Subject: [PATCH 019/329] Async command handling --- Boyfriend/Boyfriend.cs | 8 +++-- Boyfriend/Boyfriend.csproj | 2 +- Boyfriend/CommandHandler.cs | 60 ++++++++++++++++++------------- Boyfriend/Commands/BanCommand.cs | 8 ++--- Boyfriend/Commands/MuteCommand.cs | 9 +++-- Boyfriend/EventHandler.cs | 4 +-- Boyfriend/Utils.cs | 3 +- 7 files changed, 54 insertions(+), 40 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index b4aa241..876bd20 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -12,11 +12,15 @@ public static class Boyfriend { private static readonly DiscordSocketConfig Config = new() { MessageCacheSize = 250, - GatewayIntents = GatewayIntents.All + GatewayIntents = GatewayIntents.All, + AlwaysDownloadUsers = true, + AlwaysResolveStickers = false, + AlwaysDownloadDefaultStickers = false, + LargeThreshold = 500 }; public static readonly DiscordSocketClient Client = new(Config); - private static readonly Game Activity = new("Retrospecter - Genocide", ActivityType.Listening); + private static readonly Game Activity = new("Toby Fox - The World Revolving", ActivityType.Listening); private static readonly Dictionary> GuildConfigDictionary = new(); diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index 80329ec..8839dbb 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -15,7 +15,7 @@ - + diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs index 097993e..a8eed4d 100644 --- a/Boyfriend/CommandHandler.cs +++ b/Boyfriend/CommandHandler.cs @@ -22,10 +22,14 @@ public static class CommandHandler { public static readonly StringBuilder StackedPrivateFeedback = new(); #pragma warning disable CA2211 - public static bool ConfigWriteScheduled = false; // HOW IT CAN BE PRIVATE???? + public static bool ConfigWriteScheduled = false; // Can't be private #pragma warning restore CA2211 + private static bool _handlerBusy; + public static async Task HandleCommand(SocketUserMessage message) { + while (_handlerBusy) await Task.Delay(200); + _handlerBusy = true; StackedReplyMessage.Clear(); StackedPrivateFeedback.Clear(); StackedPublicFeedback.Clear(); @@ -43,34 +47,42 @@ public static class CommandHandler { var currentLine = 0; foreach (var line in list) { currentLine++; - foreach (var command in Commands) { - var lineNoMention = MentionRegex.Replace(line, "", 1); - if (!command.Aliases.Contains(regex.Replace(lineNoMention, "", 1).Trim().ToLower().Split()[0])) - continue; + await RunCommands(line, regex, context, currentLine == list.Length); + } - await context.Channel.TriggerTypingAsync(); + if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfig(guild.Id); - var args = line.Split().Skip(1).ToArray(); + var adminChannel = Utils.GetAdminLogChannel(guild.Id); + var systemChannel = guild.SystemChannel; + if (StackedPrivateFeedback.Length > 0 && adminChannel != null && adminChannel.Id != message.Channel.Id) + await Utils.SilentSendAsync(adminChannel, StackedPrivateFeedback.ToString()); + if (StackedPublicFeedback.Length > 0 && systemChannel != null && systemChannel.Id != adminChannel?.Id + && systemChannel.Id != message.Channel.Id) + await Utils.SilentSendAsync(systemChannel, StackedPublicFeedback.ToString()); + _handlerBusy = false; + } - if (command.ArgsLengthRequired <= args.Length) + private static async Task RunCommands(string line, Regex regex, SocketCommandContext context, bool shouldAwait) { + foreach (var command in Commands) { + var lineNoMention = MentionRegex.Replace(line, "", 1); + if (!command.Aliases.Contains(regex.Replace(lineNoMention, "", 1).Trim().ToLower().Split()[0])) + continue; + + await context.Channel.TriggerTypingAsync(); + + var args = line.Split().Skip(1).ToArray(); + + if (command.ArgsLengthRequired <= args.Length) + if (shouldAwait) await command.Run(context, args); else - StackedReplyMessage.AppendFormat(Messages.NotEnoughArguments, command.ArgsLengthRequired.ToString(), - args.Length.ToString()); - - if (currentLine != list.Length) continue; - if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfig(guild.Id); - if (StackedReplyMessage.Length > 0) - await message.ReplyAsync(StackedReplyMessage.ToString(), false, null, AllowedMentions.None); - - var adminChannel = Utils.GetAdminLogChannel(guild.Id); - var systemChannel = guild.SystemChannel; - if (StackedPrivateFeedback.Length > 0 && adminChannel != null && adminChannel.Id != message.Channel.Id) - await Utils.SilentSendAsync(adminChannel, StackedPrivateFeedback.ToString()); - if (StackedPublicFeedback.Length > 0 && systemChannel != null && systemChannel.Id != adminChannel?.Id - && systemChannel.Id != message.Channel.Id) - await Utils.SilentSendAsync(systemChannel, StackedPublicFeedback.ToString()); - } + _ = command.Run(context, args); + else + StackedReplyMessage.AppendFormat(Messages.NotEnoughArguments, command.ArgsLengthRequired.ToString(), + args.Length.ToString()).AppendLine(); + if (StackedReplyMessage.Length <= 1675 && !shouldAwait) continue; + await context.Message.ReplyAsync(StackedReplyMessage.ToString(), false, null, AllowedMentions.None); + StackedReplyMessage.Clear(); } } diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index ce83d35..0f67c81 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -65,12 +65,10 @@ public class BanCommand : Command { await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); if (duration.TotalSeconds > 0) { - async void DelayUnban() { + var _ = async () => { await Task.Delay(duration); await UnbanCommand.UnbanUser(guild, guild.CurrentUser, toBan, Messages.PunishmentExpired); - } - - new Task(DelayUnban).Start(); + }; } } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index 7dd8aef..d9b61d7 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -91,12 +91,11 @@ public class MuteCommand : Command { CommandHandler.ConfigWriteScheduled = true; if (hasDuration) { - async void DelayUnmute() { - await Task.Delay(duration); + var copy = duration; + var _ = async () => { + await Task.Delay(copy); await UnmuteCommand.UnmuteMember(guild, guild.CurrentUser, toMute, Messages.PunishmentExpired); - } - - new Task(DelayUnmute).Start(); + }; } } diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 91b8a25..c4f6a99 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -83,7 +83,7 @@ public class EventHandler { (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)))) return; - await CommandHandler.HandleCommand(message); + _ = CommandHandler.HandleCommand(message); } private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, @@ -176,4 +176,4 @@ public class EventHandler { await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); } -} \ No newline at end of file +} diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 292b611..213196e 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -118,6 +118,7 @@ public static class Utils { } } + numberBuilder.Clear(); return new TimeSpan(days, hours, minutes, seconds); } @@ -168,4 +169,4 @@ public static class Utils { public static void SetCurrentLanguage(ulong guildId) { Messages.Culture = CultureInfoCache[Boyfriend.GetGuildConfig(guildId)["Lang"]]; } -} \ No newline at end of file +} From ac63719a0bddce0342e5cc8662318aa50ecc0a57 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 30 Aug 2022 20:15:01 +0500 Subject: [PATCH 020/329] Async message handling, CommandHandler rewrite and rename --- Boyfriend/Boyfriend.cs | 2 +- Boyfriend/Boyfriend.csproj | 1 - Boyfriend/CommandHandler.cs | 124 ------- Boyfriend/CommandProcessor.cs | 271 ++++++++++++++ Boyfriend/Commands/BanCommand.cs | 66 +--- Boyfriend/Commands/ClearCommand.cs | 41 +-- Boyfriend/Commands/Command.cs | 28 +- Boyfriend/Commands/HelpCommand.cs | 14 +- Boyfriend/Commands/KickCommand.cs | 44 +-- Boyfriend/Commands/MuteCommand.cs | 94 ++--- Boyfriend/Commands/PingCommand.cs | 15 +- Boyfriend/Commands/SettingsCommand.cs | 46 ++- Boyfriend/Commands/UnbanCommand.cs | 44 +-- Boyfriend/Commands/UnmuteCommand.cs | 50 +-- Boyfriend/EventHandler.cs | 6 +- Boyfriend/Messages.Designer.cs | 496 +++++++++++++++++++------- Boyfriend/Messages.resx | 178 ++++++--- Boyfriend/Messages.ru.resx | 178 ++++++--- Boyfriend/Utils.cs | 74 ++-- 19 files changed, 1061 insertions(+), 711 deletions(-) delete mode 100644 Boyfriend/CommandHandler.cs create mode 100644 Boyfriend/CommandProcessor.cs diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 876bd20..77607fb 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -133,6 +133,6 @@ public static class Boyfriend { return guild; } - throw new Exception(Messages.CouldntFindGuildByChannel); + throw new Exception("Could not find guild by channel!"); } } diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index 8839dbb..a37a533 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -18,7 +18,6 @@ - diff --git a/Boyfriend/CommandHandler.cs b/Boyfriend/CommandHandler.cs deleted file mode 100644 index a8eed4d..0000000 --- a/Boyfriend/CommandHandler.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Text; -using System.Text.RegularExpressions; -using Boyfriend.Commands; -using Discord; -using Discord.Commands; -using Discord.WebSocket; - -namespace Boyfriend; - -public static class CommandHandler { - public static readonly Command[] Commands = { - new BanCommand(), new ClearCommand(), new HelpCommand(), - new KickCommand(), new MuteCommand(), new PingCommand(), - new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() - }; - - private static readonly Dictionary RegexCache = new(); - private static readonly Regex MentionRegex = new(Regex.Escape("<@855023234407333888>")); - - public static readonly StringBuilder StackedReplyMessage = new(); - public static readonly StringBuilder StackedPublicFeedback = new(); - public static readonly StringBuilder StackedPrivateFeedback = new(); - -#pragma warning disable CA2211 - public static bool ConfigWriteScheduled = false; // Can't be private -#pragma warning restore CA2211 - - private static bool _handlerBusy; - - public static async Task HandleCommand(SocketUserMessage message) { - while (_handlerBusy) await Task.Delay(200); - _handlerBusy = true; - StackedReplyMessage.Clear(); - StackedPrivateFeedback.Clear(); - StackedPublicFeedback.Clear(); - var context = new SocketCommandContext(Boyfriend.Client, message); - var guild = context.Guild; - var config = Boyfriend.GetGuildConfig(guild.Id); - - Regex regex; - if (RegexCache.ContainsKey(config["Prefix"])) { regex = RegexCache[config["Prefix"]]; } else { - regex = new Regex(Regex.Escape(config["Prefix"])); - RegexCache.Add(config["Prefix"], regex); - } - - var list = message.Content.Split("\n"); - var currentLine = 0; - foreach (var line in list) { - currentLine++; - await RunCommands(line, regex, context, currentLine == list.Length); - } - - if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfig(guild.Id); - - var adminChannel = Utils.GetAdminLogChannel(guild.Id); - var systemChannel = guild.SystemChannel; - if (StackedPrivateFeedback.Length > 0 && adminChannel != null && adminChannel.Id != message.Channel.Id) - await Utils.SilentSendAsync(adminChannel, StackedPrivateFeedback.ToString()); - if (StackedPublicFeedback.Length > 0 && systemChannel != null && systemChannel.Id != adminChannel?.Id - && systemChannel.Id != message.Channel.Id) - await Utils.SilentSendAsync(systemChannel, StackedPublicFeedback.ToString()); - _handlerBusy = false; - } - - private static async Task RunCommands(string line, Regex regex, SocketCommandContext context, bool shouldAwait) { - foreach (var command in Commands) { - var lineNoMention = MentionRegex.Replace(line, "", 1); - if (!command.Aliases.Contains(regex.Replace(lineNoMention, "", 1).Trim().ToLower().Split()[0])) - continue; - - await context.Channel.TriggerTypingAsync(); - - var args = line.Split().Skip(1).ToArray(); - - if (command.ArgsLengthRequired <= args.Length) - if (shouldAwait) - await command.Run(context, args); - else - _ = command.Run(context, args); - else - StackedReplyMessage.AppendFormat(Messages.NotEnoughArguments, command.ArgsLengthRequired.ToString(), - args.Length.ToString()).AppendLine(); - if (StackedReplyMessage.Length <= 1675 && !shouldAwait) continue; - await context.Message.ReplyAsync(StackedReplyMessage.ToString(), false, null, AllowedMentions.None); - StackedReplyMessage.Clear(); - } - } - - public static string HasPermission(ref SocketGuildUser user, GuildPermission toCheck, - GuildPermission forBot = GuildPermission.StartEmbeddedActivities) { - var me = user.Guild.CurrentUser; - - if (user.Id == user.Guild.OwnerId || (me.GuildPermissions.Has(GuildPermission.Administrator) && - user.GuildPermissions.Has(GuildPermission.Administrator))) return ""; - - if (forBot == GuildPermission.StartEmbeddedActivities) forBot = toCheck; - - if (!me.GuildPermissions.Has(forBot)) - return Messages.CommandNoPermissionBot; - - return !user.GuildPermissions.Has(toCheck) ? Messages.CommandNoPermissionUser : ""; - } - - public static string CanInteract(ref SocketGuildUser actor, ref SocketGuildUser target) { - if (actor.Guild != target.Guild) - return Messages.InteractionsDifferentGuilds; - if (actor.Id == actor.Guild.OwnerId) - return ""; - - if (target.Id == target.Guild.OwnerId) - return Messages.InteractionsOwner; - if (actor == target) - return Messages.InteractionsYourself; - - var me = target.Guild.CurrentUser; - - if (target == me) - return Messages.InteractionsMe; - if (me.Hierarchy <= target.Hierarchy) - return Messages.InteractionsFailedBot; - - return actor.Hierarchy <= target.Hierarchy ? Messages.InteractionsFailedUser : ""; - } -} diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs new file mode 100644 index 0000000..2d83b54 --- /dev/null +++ b/Boyfriend/CommandProcessor.cs @@ -0,0 +1,271 @@ +using System.Text; +using System.Text.RegularExpressions; +using Boyfriend.Commands; +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +namespace Boyfriend; + +public class CommandProcessor { + private const string Success = ":white_check_mark: "; + private const string MissingArgument = ":keyboard: "; + private const string InvalidArgument = ":construction: "; + private const string NoAccess = ":no_entry_sign: "; + private const string CantInteract = ":vertical_traffic_light: "; + + public static readonly Command[] Commands = { + new BanCommand(), new ClearCommand(), new HelpCommand(), + new KickCommand(), new MuteCommand(), new PingCommand(), + new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() + }; + + private static readonly Dictionary RegexCache = new(); + private static readonly Regex MentionRegex = new(Regex.Escape("<@855023234407333888>")); + private readonly StringBuilder _stackedPrivateFeedback = new(); + private readonly StringBuilder _stackedPublicFeedback = new(); + private readonly StringBuilder _stackedReplyMessage = new(); + private readonly List _tasks = new(); + + public readonly SocketCommandContext Context; + + public bool ConfigWriteScheduled = false; + + public CommandProcessor(SocketUserMessage message) { + Context = new SocketCommandContext(Boyfriend.Client, message); + } + + public async Task HandleCommand() { + _stackedReplyMessage.Clear(); + _stackedPrivateFeedback.Clear(); + _stackedPublicFeedback.Clear(); + var guild = Context.Guild; + var config = Boyfriend.GetGuildConfig(guild.Id); + + if (GetMember().Roles.Contains(Utils.GetMuteRole(guild))) { + await Context.Message.ReplyAsync(Messages.UserCannotUnmuteThemselves); + return; + } + + Regex regex; + if (RegexCache.ContainsKey(config["Prefix"])) { regex = RegexCache[config["Prefix"]]; } else { + regex = new Regex(Regex.Escape(config["Prefix"])); + RegexCache.Add(config["Prefix"], regex); + } + + var list = Context.Message.Content.Split("\n"); + foreach (var line in list) { + RunCommandOnLine(line, regex); + if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync(); + } + + await Task.WhenAll(_tasks); + + if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfig(guild.Id); + + if (_stackedReplyMessage.Length > 0) _ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString()); + + var adminChannel = Utils.GetAdminLogChannel(guild.Id); + var systemChannel = guild.SystemChannel; + if (_stackedPrivateFeedback.Length > 0 && adminChannel != null && adminChannel.Id != Context.Message.Channel.Id) + _ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString()); + if (_stackedPublicFeedback.Length > 0 && systemChannel != null && systemChannel.Id != adminChannel?.Id + && systemChannel.Id != Context.Message.Channel.Id) + _ = Utils.SilentSendAsync(systemChannel, _stackedPublicFeedback.ToString()); + } + + private void RunCommandOnLine(string line, Regex regex) { + foreach (var command in Commands) { + var lineNoMention = regex.Replace(MentionRegex.Replace(line, "", 1), "", 1); + if (lineNoMention == line + || !command.Aliases.Contains(regex.Replace(lineNoMention, "", 1).Trim().ToLower().Split()[0])) + continue; + + var args = line.Split().Skip(1).ToArray(); + _tasks.Add(command.Run(this, args)); + } + } + + public void Reply(string response, string? customEmoji = null) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{customEmoji ?? Success}{response}", Context.Message); + } + + public void Audit(string action, bool isPublic = true) { + var format = string.Format(Messages.FeedbackFormat, Context.User.Mention, action); + if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, Context.Guild.SystemChannel); + Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, Utils.GetAdminLogChannel(Context.Guild.Id)); + } + + public string? GetRemaining(string[] from, int startIndex, string? argument) { + if (startIndex >= from.Length && argument != null) + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{MissingArgument}{Utils.GetMessage($"Missing{argument}")}", Context.Message); + else + return string.Join(" ", from, startIndex, from.Length - startIndex); + return null; + } + + public SocketUser? GetUser(string[] args, int index, string? argument) { + if (index >= args.Length) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Messages.MissingUser}", + Context.Message); + return null; + } + + var user = Utils.ParseUser(args[index]); + if (user == null && argument != null) + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{InvalidArgument}{string.Format(Messages.InvalidUser, args[index])}", Context.Message); + return user; + } + + public bool HasPermission(GuildPermission permission) { + if (!Context.Guild.CurrentUser.GuildPermissions.Has(permission)) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{NoAccess}{Utils.GetMessage($"BotCannot{permission}")}", + Context.Message); + return false; + } + + if (Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission) + || Context.Guild.Owner.Id == Context.User.Id) return true; + + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{NoAccess}{Utils.GetMessage($"UserCannot{permission}")}", + Context.Message); + return false; + } + + public SocketGuildUser? GetMember(SocketUser user, string? argument) { + var member = Context.Guild.GetUser(user.Id); + if (member == null && argument != null) + Utils.SafeAppendToBuilder(_stackedReplyMessage, $":x: {Messages.UserNotInGuild}", Context.Message); + return member; + } + + public SocketGuildUser? GetMember(string[] args, int index, string? argument) { + if (index >= args.Length) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Messages.MissingMember}", + Context.Message); + return null; + } + + var member = Context.Guild.GetUser(Utils.ParseMention(args[index])); + if (member == null && argument != null) + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{InvalidArgument}{string.Format(Messages.InvalidMember, Utils.Wrap(args[index]))}", 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, $"{MissingArgument}{Messages.MissingUser}", + Context.Message); + return null; + } + + var id = Utils.ParseMention(args[index]); + if (Context.Guild.GetBanAsync(id) != null) return id; + Utils.SafeAppendToBuilder(_stackedReplyMessage, Messages.UserNotBanned, Context.Message); + return null; + } + + public int? GetNumberRange(string[] args, int index, int min, int max, string? argument) { + if (index >= args.Length) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{MissingArgument}{string.Format(Messages.MissingNumber, min.ToString(), max.ToString())}", + Context.Message); + return null; + } + + if (!int.TryParse(args[index], out var i)) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{InvalidArgument}{string.Format(Utils.GetMessage($"{argument}Invalid"), min.ToString(), max.ToString(), Utils.Wrap(args[index]))}", + Context.Message); + return null; + } + + if (argument == null) return i; + if (i < min) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{InvalidArgument}{string.Format(Utils.GetMessage($"{argument}TooSmall"), min.ToString())}", + Context.Message); + return null; + } + + if (i <= max) return i; + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{InvalidArgument}{string.Format(Utils.GetMessage($"{argument}TooLarge"), max.ToString())}", + Context.Message); + return null; + } + + public static TimeSpan GetTimeSpan(string[] args, int index) { + var infinity = TimeSpan.FromMilliseconds(-1); + if (index >= args.Length) + return infinity; + var chars = args[index].AsSpan(); + var numberBuilder = Boyfriend.StringBuilder; + int days = 0, hours = 0, minutes = 0, seconds = 0; + foreach (var c in chars) + if (char.IsDigit(c)) { numberBuilder.Append(c); } else { + if (numberBuilder.Length == 0) return infinity; + switch (c) { + case 'd' or 'D' or 'д' or 'Д': + days += int.Parse(numberBuilder.ToString()); + numberBuilder.Clear(); + break; + case 'h' or 'H' or 'ч' or 'Ч': + hours += int.Parse(numberBuilder.ToString()); + numberBuilder.Clear(); + break; + case 'm' or 'M' or 'м' or 'М': + minutes += int.Parse(numberBuilder.ToString()); + numberBuilder.Clear(); + break; + case 's' or 'S' or 'с' or 'С': + seconds += int.Parse(numberBuilder.ToString()); + numberBuilder.Clear(); + break; + default: return infinity; + } + } + + numberBuilder.Clear(); + return new TimeSpan(days, hours, minutes, seconds); + } + + public bool CanInteractWith(SocketGuildUser user, string action) { + if (Context.Guild.Owner.Id == Context.User.Id) return true; + if (Context.Guild.Owner.Id == user.Id) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Owner")}", Context.Message); + return false; + } + + if (Context.User.Id == user.Id) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Themselves")}", Context.Message); + return false; + } + + if (Context.Guild.CurrentUser.Id == user.Id) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Bot")}", Context.Message); + return false; + } + + if (Context.Guild.CurrentUser.Hierarchy <= user.Hierarchy) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{CantInteract}{Utils.GetMessage($"BotCannot{action}Target")}", Context.Message); + return false; + } + + if (GetMember().Hierarchy > user.Hierarchy) return true; + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Target")}", Context.Message); + return false; + } +} diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 0f67c81..7e42b6a 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -1,74 +1,44 @@ using Discord; -using Discord.Commands; using Discord.WebSocket; namespace Boyfriend.Commands; public class BanCommand : Command { public override string[] Aliases { get; } = { "ban", "бан" }; - public override int ArgsLengthRequired => 2; - public override async Task Run(SocketCommandContext context, string[] args) { - var toBan = Utils.ParseUser(args[0]); + public override async Task Run(CommandProcessor cmd, string[] args) { + var toBan = cmd.GetUser(args, 0, "ToBan"); + if (toBan == null || !cmd.HasPermission(GuildPermission.BanMembers)) return; - if (toBan == null) { - Error(Messages.UserDoesntExist, false); - return; - } + var memberToBan = cmd.GetMember(toBan, null); + if (memberToBan != null && !cmd.CanInteractWith(memberToBan, "Ban")) return; - var guild = context.Guild; - var author = (SocketGuildUser)context.User; + var duration = CommandProcessor.GetTimeSpan(args, 1); + var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "BanReason"); + if (reason == null) return; - var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.BanMembers); - if (permissionCheckResponse is not "") { - Error(permissionCheckResponse, true); - return; - } - - var reason = Utils.JoinString(ref args, 2); - var memberToBan = Utils.ParseMember(guild, args[0]); - - if (memberToBan != null) { - var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref memberToBan); - if (interactionCheckResponse is not "") { - Error(interactionCheckResponse, true); - return; - } - } - - var duration = Utils.GetTimeSpan(ref args[1]) ?? TimeSpan.FromMilliseconds(-1); - if (duration.TotalSeconds < 0) { - Warn(Messages.DurationParseFailed); - reason = Utils.JoinString(ref args, 1); - - if (reason is "") { - Error(Messages.ReasonRequired, false); - return; - } - } - - await BanUser(guild, author, toBan, duration, reason); + await BanUser(cmd, toBan, duration, reason); } - public static async Task BanUser(SocketGuild guild, SocketGuildUser author, SocketUser toBan, TimeSpan duration, - string reason) { - var guildBanMessage = $"({author}) {reason}"; - + public static async Task BanUser(CommandProcessor cmd, SocketUser 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))); + var guildBanMessage = $"({author}) {reason}"; await guild.AddBanAsync(toBan, 0, guildBanMessage); var feedback = string.Format(Messages.FeedbackUserBanned, toBan.Mention, - Utils.GetHumanizedTimeOffset(ref duration), Utils.Wrap(reason)); - Success(feedback, author.Mention, false, false); - await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); + Utils.GetHumanizedTimeOffset(duration), Utils.Wrap(reason)); + cmd.Reply(feedback, ":hammer: "); + cmd.Audit(feedback); if (duration.TotalSeconds > 0) { var _ = async () => { await Task.Delay(duration); - await UnbanCommand.UnbanUser(guild, guild.CurrentUser, toBan, Messages.PunishmentExpired); + await UnbanCommand.UnbanUser(cmd, toBan.Id, Messages.PunishmentExpired); }; } } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs index 6b51075..164e0c6 100644 --- a/Boyfriend/Commands/ClearCommand.cs +++ b/Boyfriend/Commands/ClearCommand.cs @@ -1,46 +1,23 @@ using Discord; -using Discord.Commands; using Discord.WebSocket; namespace Boyfriend.Commands; public class ClearCommand : Command { public override string[] Aliases { get; } = { "clear", "purge", "очистить", "стереть" }; - public override int ArgsLengthRequired => 1; - public override async Task Run(SocketCommandContext context, string[] args) { - var user = (SocketGuildUser)context.User; + public override async Task Run(CommandProcessor cmd, string[] args) { + if (cmd.Context.Channel is not SocketTextChannel channel) throw new Exception(); - if (context.Channel is not SocketTextChannel channel) throw new Exception(); + if (!cmd.HasPermission(GuildPermission.ManageMessages)) return; - var permissionCheckResponse = CommandHandler.HasPermission(ref user, GuildPermission.ManageMessages); - if (permissionCheckResponse is not "") { - Error(permissionCheckResponse, true); - return; - } + var toDelete = cmd.GetNumberRange(args, 0, 1, 200, "ClearAmount"); + if (toDelete == null) return; + var messages = await channel.GetMessagesAsync((int)(toDelete + 1)).FlattenAsync(); - if (!int.TryParse(args[0], out var toDelete)) { - Error(Messages.ClearInvalidAmountSpecified, false); - return; - } + var user = (SocketGuildUser)cmd.Context.User; + await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(user.ToString()!)); - switch (toDelete) { - case < 1: - Error(Messages.ClearNegativeAmount, false); - break; - case > 200: - Error(Messages.ClearAmountTooLarge, false); - break; - default: - var messages = await channel.GetMessagesAsync(toDelete + 1).FlattenAsync(); - - await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(user.ToString()!)); - - await Utils.SendFeedback( - string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString(), channel.Mention), - context.Guild.Id, user.Mention); - - break; - } + cmd.Audit(string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString())); } } \ No newline at end of file diff --git a/Boyfriend/Commands/Command.cs b/Boyfriend/Commands/Command.cs index ec8590b..cf8563b 100644 --- a/Boyfriend/Commands/Command.cs +++ b/Boyfriend/Commands/Command.cs @@ -1,31 +1,7 @@ -using System.Text; -using Discord.Commands; - -namespace Boyfriend.Commands; +namespace Boyfriend.Commands; public abstract class Command { public abstract string[] Aliases { get; } - public abstract int ArgsLengthRequired { get; } - public abstract Task Run(SocketCommandContext context, string[] args); - - protected static void Output(ref StringBuilder message) { - CommandHandler.StackedReplyMessage.Append(message).AppendLine(); - } - - protected static void Success(string message, string userMention, bool sendPublicFeedback = false, - bool sendPrivateFeedback = true) { - CommandHandler.StackedReplyMessage.Append(":white_check_mark: ").AppendLine(message); - if (sendPrivateFeedback) - Utils.StackFeedback(ref message, ref userMention, sendPublicFeedback); - } - - protected static void Warn(string message) { - CommandHandler.StackedReplyMessage.Append(":warning: ").AppendLine(message); - } - - protected static void Error(string message, bool accessDenied) { - var symbol = accessDenied ? ":no_entry_sign: " : ":x: "; - CommandHandler.StackedReplyMessage.Append(symbol).AppendLine(message); - } + public abstract Task Run(CommandProcessor cmd, string[] args); } \ No newline at end of file diff --git a/Boyfriend/Commands/HelpCommand.cs b/Boyfriend/Commands/HelpCommand.cs index 656fa48..1a27bcd 100644 --- a/Boyfriend/Commands/HelpCommand.cs +++ b/Boyfriend/Commands/HelpCommand.cs @@ -1,20 +1,18 @@ -using Discord.Commands; -using Humanizer; +using Humanizer; namespace Boyfriend.Commands; public class HelpCommand : Command { - public override string[] Aliases { get; } = {"help", "помощь", "справка"}; - public override int ArgsLengthRequired => 0; + public override string[] Aliases { get; } = { "help", "помощь", "справка" }; - public override Task Run(SocketCommandContext context, string[] args) { - var prefix = Boyfriend.GetGuildConfig(context.Guild.Id)["Prefix"]; + public override Task Run(CommandProcessor cmd, string[] args) { + var prefix = Boyfriend.GetGuildConfig(cmd.Context.Guild.Id)["Prefix"]; var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp); - foreach (var command in CommandHandler.Commands) + foreach (var command in CommandProcessor.Commands) toSend.Append( $"\n`{prefix}{command.Aliases[0]}`: {Utils.GetMessage($"CommandDescription{command.Aliases[0].Titleize()}")}"); - Output(ref toSend); + cmd.Reply(toSend.ToString(), ":page_facing_up: "); toSend.Clear(); return Task.CompletedTask; diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index fa1ab32..ba1a139 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -1,49 +1,31 @@ using Discord; -using Discord.Commands; using Discord.WebSocket; namespace Boyfriend.Commands; public class KickCommand : Command { public override string[] Aliases { get; } = { "kick", "кик", "выгнать" }; - public override int ArgsLengthRequired => 2; - public override async Task Run(SocketCommandContext context, string[] args) { - var author = (SocketGuildUser)context.User; + public override async Task Run(CommandProcessor cmd, string[] args) { + var toKick = cmd.GetMember(args, 0, "ToKick"); + if (toKick == null || !cmd.HasPermission(GuildPermission.KickMembers)) return; - var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.KickMembers); - if (permissionCheckResponse is not "") { - Error(permissionCheckResponse, true); - return; - } + if (!cmd.CanInteractWith(toKick, "Kick")) return; - var toKick = Utils.ParseMember(context.Guild, args[0]); - - if (toKick == null) { - Error(Messages.UserNotInGuild, false); - return; - } - - var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toKick); - if (interactionCheckResponse is not "") { - Error(interactionCheckResponse, true); - return; - } - - await KickMember(context.Guild, author, toKick, Utils.JoinString(ref args, 1)); - - Success( - string.Format(Messages.FeedbackMemberKicked, toKick.Mention, - Utils.Wrap(Utils.JoinString(ref args, 1))), author.Mention); + await KickMember(cmd, toKick, cmd.GetRemaining(args, 1, "KickReason")); } - private static async Task KickMember(IGuild guild, SocketUser author, SocketGuildUser toKick, string reason) { - var authorMention = author.Mention; - var guildKickMessage = $"({author}) {reason}"; + private static async Task KickMember(CommandProcessor cmd, SocketGuildUser toKick, string? reason) { + if (reason == null) return; + var guildKickMessage = $"({cmd.Context.User}) {reason}"; await Utils.SendDirectMessage(toKick, - string.Format(Messages.YouWereKicked, authorMention, guild.Name, Utils.Wrap(reason))); + string.Format(Messages.YouWereKicked, cmd.Context.User.Mention, cmd.Context.Guild.Name, + Utils.Wrap(reason))); await toKick.KickAsync(guildKickMessage); + var format = string.Format(Messages.FeedbackMemberKicked, toKick.Mention, Utils.Wrap(reason)); + cmd.Reply(format, ":police_car: "); + cmd.Audit(format); } } \ No newline at end of file diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index d9b61d7..c972614 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -1,5 +1,4 @@ using Discord; -using Discord.Commands; using Discord.Net; using Discord.WebSocket; @@ -7,72 +6,45 @@ namespace Boyfriend.Commands; public class MuteCommand : Command { public override string[] Aliases { get; } = { "mute", "timeout", "заглушить", "мут" }; - public override int ArgsLengthRequired => 2; - public override async Task Run(SocketCommandContext context, string[] args) { - var toMute = Utils.ParseMember(context.Guild, args[0]); - var reason = Utils.JoinString(ref args, 2); + public override async Task Run(CommandProcessor cmd, string[] args) { + var toMute = cmd.GetMember(args, 0, "ToMute"); + if (toMute == null) return; - var duration = Utils.GetTimeSpan(ref args[1]) ?? TimeSpan.FromMilliseconds(-1); - if (duration.TotalSeconds < 0) { - Warn(Messages.DurationParseFailed); - reason = Utils.JoinString(ref args, 1); - } - - - if (reason is "") { - Error(Messages.ReasonRequired, false); - return; - } - - if (toMute == null) { - Error(Messages.UserNotInGuild, false); - return; - } - - var guild = context.Guild; - var role = Utils.GetMuteRole(ref guild); + var duration = CommandProcessor.GetTimeSpan(args, 1); + var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "MuteReason"); + if (reason == null) return; + var role = Utils.GetMuteRole(cmd.Context.Guild); if (role != null) { if (toMute.Roles.Contains(role) || (toMute.TimedOutUntil != null && toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > DateTimeOffset.Now.ToUnixTimeMilliseconds())) { - Error(Messages.MemberAlreadyMuted, false); + cmd.Reply(Messages.MemberAlreadyMuted, ":x: "); return; } } - var rolesRemoved = Boyfriend.GetRemovedRoles(context.Guild.Id); + var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id); if (rolesRemoved.ContainsKey(toMute.Id)) { foreach (var roleId in rolesRemoved[toMute.Id]) await toMute.AddRoleAsync(roleId); rolesRemoved.Remove(toMute.Id); - CommandHandler.ConfigWriteScheduled = true; - Warn(Messages.RolesReturned); + cmd.ConfigWriteScheduled = true; + cmd.Reply(Messages.RolesReturned, ":warning: "); } - var author = (SocketGuildUser)context.User; + if (!cmd.HasPermission(GuildPermission.ModerateMembers) || !cmd.CanInteractWith(toMute, "Mute")) return; - var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ModerateMembers); - if (permissionCheckResponse is not "") { - Error(permissionCheckResponse, true); - return; - } - - var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toMute); - if (interactionCheckResponse is not "") { - Error(interactionCheckResponse, true); - return; - } - - await MuteMember(guild, author, toMute, duration, reason); + await MuteMember(cmd, toMute, duration, reason); } - private static async Task MuteMember(SocketGuild guild, SocketUser author, SocketGuildUser toMute, + private static async Task MuteMember(CommandProcessor cmd, SocketGuildUser toMute, TimeSpan duration, string reason) { + var guild = cmd.Context.Guild; var config = Boyfriend.GetGuildConfig(guild.Id); - var requestOptions = Utils.GetRequestOptions($"({author}) {reason}"); - var role = Utils.GetMuteRole(ref guild); + var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); + var role = Utils.GetMuteRole(guild); var hasDuration = duration.TotalSeconds > 0; if (role != null) { @@ -84,30 +56,30 @@ public class MuteCommand : Command { await toMute.RemoveRoleAsync(role); rolesRemoved.Add(userRole.Id); } catch (HttpException e) { - Warn(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.Wrap(e.Reason))); + cmd.Reply(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.Wrap(e.Reason)), + ":warning: "); } Boyfriend.GetRemovedRoles(guild.Id).Add(toMute.Id, rolesRemoved.AsReadOnly()); - CommandHandler.ConfigWriteScheduled = true; - - if (hasDuration) { - var copy = duration; - var _ = async () => { - await Task.Delay(copy); - await UnmuteCommand.UnmuteMember(guild, guild.CurrentUser, toMute, Messages.PunishmentExpired); - }; - } + cmd.ConfigWriteScheduled = true; } await toMute.AddRoleAsync(role, requestOptions); + + if (hasDuration) { + var _ = async () => { + await Task.Delay(duration); + await UnmuteCommand.UnmuteMember(cmd, toMute, Messages.PunishmentExpired); + }; + } } else { if (!hasDuration || duration.TotalDays > 28) { - Error(Messages.DurationRequiredForTimeOuts, false); + cmd.Reply(Messages.DurationRequiredForTimeOuts, ":x: "); return; } if (toMute.IsBot) { - Error(Messages.CannotTimeOutBot, false); + cmd.Reply(Messages.CannotTimeOutBot, ":x: "); return; } @@ -115,9 +87,9 @@ public class MuteCommand : Command { } var feedback = string.Format(Messages.FeedbackMemberMuted, toMute.Mention, - Utils.GetHumanizedTimeOffset(ref duration), + Utils.GetHumanizedTimeOffset(duration), Utils.Wrap(reason)); - Success(feedback, author.Mention, false, false); - await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); + cmd.Reply(feedback, ":mute: "); + cmd.Audit(feedback); } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/PingCommand.cs b/Boyfriend/Commands/PingCommand.cs index c5189e4..4034f96 100644 --- a/Boyfriend/Commands/PingCommand.cs +++ b/Boyfriend/Commands/PingCommand.cs @@ -1,17 +1,16 @@ -using Discord.Commands; - -namespace Boyfriend.Commands; +namespace Boyfriend.Commands; public class PingCommand : Command { - public override string[] Aliases { get; } = {"ping", "latency", "pong", "пинг", "задержка", "понг"}; - public override int ArgsLengthRequired => 0; + public override string[] Aliases { get; } = { "ping", "latency", "pong", "пинг", "задержка", "понг" }; - public override Task Run(SocketCommandContext context, string[] args) { + public override Task Run(CommandProcessor cmd, string[] args) { var builder = Boyfriend.StringBuilder; - builder.Append(Utils.GetBeep()).Append(Boyfriend.Client.Latency).Append(Messages.Milliseconds); + builder.Append(Utils.GetBeep()) + .Append(Math.Abs(DateTimeOffset.Now.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds)) + .Append(Messages.Milliseconds); - Output(ref builder); + cmd.Reply(builder.ToString(), ":signal_strength: "); builder.Clear(); return Task.CompletedTask; diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 0a7fabc..0a5f3cf 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -1,23 +1,14 @@ using Discord; -using Discord.Commands; -using Discord.WebSocket; namespace Boyfriend.Commands; public class SettingsCommand : Command { public override string[] Aliases { get; } = { "settings", "config", "настройки", "конфиг" }; - public override int ArgsLengthRequired => 0; - public override Task Run(SocketCommandContext context, string[] args) { - var author = (SocketGuildUser)context.User; + public override Task Run(CommandProcessor cmd, string[] args) { + if (!cmd.HasPermission(GuildPermission.ManageGuild)) return Task.CompletedTask; - var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ManageGuild); - if (permissionCheckResponse is not "") { - Error(permissionCheckResponse, true); - return Task.CompletedTask; - } - - var guild = context.Guild; + var guild = cmd.Context.Guild; var config = Boyfriend.GetGuildConfig(guild.Id); if (args.Length == 0) { @@ -48,7 +39,7 @@ public class SettingsCommand : Command { .AppendFormat(format, currentValue).AppendLine(); } - Output(ref currentSettings); + cmd.Reply(currentSettings.ToString(), ":gear: "); currentSettings.Clear(); return Task.CompletedTask; } @@ -66,19 +57,20 @@ public class SettingsCommand : Command { } if (!exists) { - Error(Messages.SettingDoesntExist, false); + cmd.Reply(Messages.SettingDoesntExist, ":x: "); return Task.CompletedTask; } - string value; + string? value; if (args.Length >= 2) { - value = Utils.JoinString(ref args, 1); + value = cmd.GetRemaining(args, 1, "Setting"); + if (value == null) return Task.CompletedTask; if (selectedSetting is "EventStartedReceivers") { value = value.Replace(" ", "").ToLower(); if (value.StartsWith(",") || value.Count(x => x == ',') > 1 || (!value.Contains("interested") && !value.Contains("role"))) { - Error(Messages.InvalidSettingValue, false); + cmd.Reply(Messages.InvalidSettingValue, ":x: "); return Task.CompletedTask; } } @@ -91,7 +83,7 @@ public class SettingsCommand : Command { _ => value }; if (!IsBool(value)) { - Error(Messages.InvalidSettingValue, false); + cmd.Reply(Messages.InvalidSettingValue, ":x: "); return Task.CompletedTask; } } @@ -124,22 +116,23 @@ public class SettingsCommand : Command { config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; } else { if (value == config[selectedSetting]) { - Error(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), false); + cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), + ":x: "); return Task.CompletedTask; } if (selectedSetting is "Lang" && value is not "ru" and not "en") { - Error(Messages.LanguageNotSupported, false); + cmd.Reply(Messages.LanguageNotSupported, ":x: "); return Task.CompletedTask; } if (selectedSetting.EndsWith("Channel") && guild.GetTextChannel(mention) == null) { - Error(Messages.InvalidChannel, false); + cmd.Reply(Messages.InvalidChannel, ":x: "); return Task.CompletedTask; } if (selectedSetting.EndsWith("Role") && guild.GetRole(mention) == null) { - Error(Messages.InvalidRole, false); + cmd.Reply(Messages.InvalidRole, ":x: "); return Task.CompletedTask; } @@ -153,10 +146,11 @@ public class SettingsCommand : Command { localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); } - CommandHandler.ConfigWriteScheduled = true; + cmd.ConfigWriteScheduled = true; - Success(string.Format(Messages.FeedbackSettingsUpdated, localizedSelectedSetting, formattedValue), - author.Mention); + var replyFormat = string.Format(Messages.FeedbackSettingsUpdated, localizedSelectedSetting, formattedValue); + cmd.Reply(replyFormat, ":control_knobs: "); + cmd.Audit(replyFormat, false); return Task.CompletedTask; } @@ -167,4 +161,4 @@ public class SettingsCommand : Command { private static bool IsBool(string value) { return value is "true" or "false"; } -} +} \ No newline at end of file diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index ac2edef..602f497 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -1,45 +1,27 @@ using Discord; -using Discord.Commands; -using Discord.WebSocket; namespace Boyfriend.Commands; public class UnbanCommand : Command { public override string[] Aliases { get; } = { "unban", "разбан" }; - public override int ArgsLengthRequired => 2; - public override async Task Run(SocketCommandContext context, string[] args) { - var author = (SocketGuildUser)context.User; + public override async Task Run(CommandProcessor cmd, string[] args) { + if (!cmd.HasPermission(GuildPermission.BanMembers)) return; - var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.BanMembers); - if (permissionCheckResponse is not "") { - Error(permissionCheckResponse, true); - return; - } + var id = cmd.GetBan(args, 0); + if (id == null) return; + var reason = cmd.GetRemaining(args, 1, "UnbanReason"); + if (reason == null) return; - var toUnban = Utils.ParseUser(args[0]); - - if (toUnban == null) { - Error(Messages.UserDoesntExist, false); - return; - } - - var reason = Utils.JoinString(ref args, 1); - - await UnbanUser(context.Guild, author, toUnban, reason); + await UnbanUser(cmd, id.Value, reason); } - public static async Task UnbanUser(SocketGuild guild, SocketGuildUser author, SocketUser toUnban, string reason) { - if (guild.GetBanAsync(toUnban.Id) == null) { - Error(Messages.UserNotBanned, false); - return; - } + public static async Task UnbanUser(CommandProcessor cmd, ulong id, string reason) { + var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); + await cmd.Context.Guild.RemoveBanAsync(id, requestOptions); - var requestOptions = Utils.GetRequestOptions($"({author}) {reason}"); - await guild.RemoveBanAsync(toUnban, requestOptions); - - var feedback = string.Format(Messages.FeedbackUserUnbanned, toUnban.Mention, Utils.Wrap(reason)); - Success(feedback, author.Mention, false, false); - await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); + var feedback = string.Format(Messages.FeedbackUserUnbanned, $"<@{id.ToString()}>", Utils.Wrap(reason)); + cmd.Reply(feedback); + cmd.Audit(feedback); } } \ No newline at end of file diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index a5e8d76..f5de6ed 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -1,59 +1,39 @@ using Discord; -using Discord.Commands; using Discord.WebSocket; namespace Boyfriend.Commands; public class UnmuteCommand : Command { public override string[] Aliases { get; } = { "unmute", "размут" }; - public override int ArgsLengthRequired => 2; - public override async Task Run(SocketCommandContext context, string[] args) { - var author = (SocketGuildUser)context.User; + public override async Task Run(CommandProcessor cmd, string[] args) { + if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return; - var permissionCheckResponse = CommandHandler.HasPermission(ref author, GuildPermission.ModerateMembers); - if (permissionCheckResponse is not "") { - Error(permissionCheckResponse, true); - return; - } - - var toUnmute = Utils.ParseMember(context.Guild, args[0]); - - if (toUnmute == null) { - Error(Messages.UserDoesntExist, false); - return; - } - - var interactionCheckResponse = CommandHandler.CanInteract(ref author, ref toUnmute); - if (interactionCheckResponse is not "") { - Error(interactionCheckResponse, true); - return; - } - - var reason = Utils.JoinString(ref args, 1); - await UnmuteMember(context.Guild, author, toUnmute, reason); + var toUnmute = cmd.GetMember(args, 0, "ToUnmute"); + var reason = cmd.GetRemaining(args, 1, "UnmuteReason"); + if (toUnmute == null || reason == null || !cmd.CanInteractWith(toUnmute, "Unmute")) return; + await UnmuteMember(cmd, toUnmute, reason); } - public static async Task UnmuteMember(SocketGuild guild, SocketGuildUser author, SocketGuildUser toUnmute, + public static async Task UnmuteMember(CommandProcessor cmd, SocketGuildUser toUnmute, string reason) { - var requestOptions = Utils.GetRequestOptions($"({author}) {reason}"); - var role = Utils.GetMuteRole(ref guild); + var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); + var role = Utils.GetMuteRole(cmd.Context.Guild); if (role != null && toUnmute.Roles.Contains(role)) { - var rolesRemoved = Boyfriend.GetRemovedRoles(guild.Id); + var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id); if (rolesRemoved.ContainsKey(toUnmute.Id)) { await toUnmute.AddRolesAsync(rolesRemoved[toUnmute.Id]); rolesRemoved.Remove(toUnmute.Id); - CommandHandler.ConfigWriteScheduled = true; + cmd.ConfigWriteScheduled = true; } await toUnmute.RemoveRoleAsync(role, requestOptions); - } - else { + } else { if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() < DateTimeOffset.Now.ToUnixTimeMilliseconds()) { - Error(Messages.MemberNotMuted, false); + cmd.Reply(Messages.MemberNotMuted, ":x: "); return; } @@ -61,7 +41,7 @@ public class UnmuteCommand : Command { } var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason)); - Success(feedback, author.Mention, false, false); - await Utils.SendFeedback(feedback, guild.Id, author.Mention, true); + cmd.Reply(feedback, ":loud_sound: "); + cmd.Audit(feedback); } } \ No newline at end of file diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index c4f6a99..94e75ac 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -64,7 +64,7 @@ public class EventHandler { if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && !user.GuildPermissions.MentionEveryone) { - await BanCommand.BanUser(guild, guild.CurrentUser, user, TimeSpan.FromMilliseconds(-1), + await BanCommand.BanUser(new CommandProcessor(message), user, TimeSpan.FromMilliseconds(-1), Messages.AutobanReason); return; } @@ -83,7 +83,7 @@ public class EventHandler { (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)))) return; - _ = CommandHandler.HandleCommand(message); + _ = new CommandProcessor(message).HandleCommand(); } private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, @@ -176,4 +176,4 @@ public class EventHandler { await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); } -} +} \ No newline at end of file diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 3b588bf..ad39dc1 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -95,6 +95,87 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to I cannot ban users from this guild!. + /// + internal static string BotCannotBanMembers { + get { + return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot ban this user!. + /// + internal static string BotCannotBanTarget { + get { + return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot kick members from this guild!. + /// + internal static string BotCannotKickMembers { + get { + return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot kick this member!. + /// + internal static string BotCannotKickTarget { + get { + return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot manage this guild!. + /// + internal static string BotCannotManageGuild { + get { + return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot manage messages in this guild!. + /// + internal static string BotCannotManageMessages { + get { + return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot moderate members in this guild!. + /// + internal static string BotCannotModerateMembers { + get { + return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot mute this member!. + /// + internal static string BotCannotMuteTarget { + get { + return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I cannot unmute this member!. + /// + internal static string BotCannotUnmuteTarget { + get { + return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); + } + } + /// /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. /// @@ -132,7 +213,16 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Too many messages specified!. + /// Looks up a localized string similar to You need to specify an integer from {0} to {1} instead of {2}!. + /// + internal static string ClearAmountInvalid { + get { + return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You specified more than {0} messages!. /// internal static string ClearAmountTooLarge { get { @@ -141,20 +231,11 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Invalid message amount specified!. + /// Looks up a localized string similar to You specified less than {0} messages!. /// - internal static string ClearInvalidAmountSpecified { + internal static string ClearAmountTooSmall { get { - return ResourceManager.GetString("ClearInvalidAmountSpecified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Negative message amount specified!. - /// - internal static string ClearNegativeAmount { - get { - return ResourceManager.GetString("ClearNegativeAmount", resourceCulture); + return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); } } @@ -266,15 +347,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Couldn't find guild by message!. - /// - internal static string CouldntFindGuildByChannel { - get { - return ResourceManager.GetString("CouldntFindGuildByChannel", resourceCulture); - } - } - /// /// Looks up a localized string similar to Current settings:. /// @@ -293,15 +365,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to I couldn't parse the specified duration! One of the components could be outside it's valid range (e.g. `24h` or `60m`). - /// - internal static string DurationParseFailed { - get { - return ResourceManager.GetString("DurationParseFailed", resourceCulture); - } - } - /// /// Looks up a localized string similar to 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. /// @@ -428,60 +491,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Members are in different guilds!. - /// - internal static string InteractionsDifferentGuilds { - get { - return ResourceManager.GetString("InteractionsDifferentGuilds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot interact with this member!. - /// - internal static string InteractionsFailedBot { - get { - return ResourceManager.GetString("InteractionsFailedBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot interact with this member!. - /// - internal static string InteractionsFailedUser { - get { - return ResourceManager.GetString("InteractionsFailedUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot interact with me!. - /// - internal static string InteractionsMe { - get { - return ResourceManager.GetString("InteractionsMe", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot interact with guild owner!. - /// - internal static string InteractionsOwner { - get { - return ResourceManager.GetString("InteractionsOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot interact with yourself!. - /// - internal static string InteractionsYourself { - get { - return ResourceManager.GetString("InteractionsYourself", resourceCulture); - } - } - /// /// Looks up a localized string similar to This channel does not exist!. /// @@ -491,6 +500,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to You need to specify a guild member instead of {0}!. + /// + internal static string InvalidMember { + get { + return ResourceManager.GetString("InvalidMember", resourceCulture); + } + } + /// /// Looks up a localized string similar to This role does not exist!. /// @@ -509,6 +527,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to You need to specify a user instead of {0}!. + /// + internal static string InvalidUser { + get { + return ResourceManager.GetString("InvalidUser", resourceCulture); + } + } + /// /// Looks up a localized string similar to Language not supported!. /// @@ -536,15 +563,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to {0} unmuted {1} for {2}. - /// - internal static string MemberUnmuted { - get { - return ResourceManager.GetString("MemberUnmuted", resourceCulture); - } - } - /// /// Looks up a localized string similar to ms. /// @@ -554,6 +572,87 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to You need to specify a reason to ban this user!. + /// + internal static string MissingBanReason { + get { + return ResourceManager.GetString("MissingBanReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to kick this member!. + /// + internal static string MissingKickReason { + get { + return ResourceManager.GetString("MissingKickReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a guild member!. + /// + internal static string MissingMember { + get { + return ResourceManager.GetString("MissingMember", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to mute this member!. + /// + internal static string MissingMuteReason { + get { + return ResourceManager.GetString("MissingMuteReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. + /// + internal static string MissingNumber { + get { + return ResourceManager.GetString("MissingNumber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a setting to change!. + /// + internal static string MissingSetting { + get { + return ResourceManager.GetString("MissingSetting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to unban this user!. + /// + internal static string MissingUnbanReason { + get { + return ResourceManager.GetString("MissingUnbanReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason for unmute this member!. + /// + internal static string MissingUnmuteReason { + get { + return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a user!. + /// + internal static string MissingUser { + get { + return ResourceManager.GetString("MissingUser", resourceCulture); + } + } + /// /// Looks up a localized string similar to No. /// @@ -563,15 +662,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Not enough arguments! Needed: {0}, provided: {1}. - /// - internal static string NotEnoughArguments { - get { - return ResourceManager.GetString("NotEnoughArguments", resourceCulture); - } - } - /// /// Looks up a localized string similar to Punishment expired. /// @@ -590,15 +680,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to You must specify a reason!. - /// - internal static string ReasonRequired { - get { - return ResourceManager.GetString("ReasonRequired", resourceCulture); - } - } - /// /// Looks up a localized string similar to Not specified. /// @@ -807,34 +888,196 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Message deleted in {0}, but I forgot what was there. + /// Looks up a localized string similar to You cannot ban me!. /// - internal static string UncachedMessageDeleted { + internal static string UserCannotBanBot { get { - return ResourceManager.GetString("UncachedMessageDeleted", resourceCulture); + return ResourceManager.GetString("UserCannotBanBot", resourceCulture); } } /// - /// Looks up a localized string similar to Message edited from {0} in channel {1}, but I forgot what was there before the edit: {2}. + /// Looks up a localized string similar to You cannot ban users from this guild!. /// - internal static string UncachedMessageEdited { + internal static string UserCannotBanMembers { get { - return ResourceManager.GetString("UncachedMessageEdited", resourceCulture); + return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); } } /// - /// Looks up a localized string similar to That user doesn't exist!. + /// Looks up a localized string similar to You cannot ban the owner of this guild!. /// - internal static string UserDoesntExist { + internal static string UserCannotBanOwner { get { - return ResourceManager.GetString("UserDoesntExist", resourceCulture); + return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); } } /// - /// Looks up a localized string similar to User not banned!. + /// Looks up a localized string similar to You cannot ban this user!. + /// + internal static string UserCannotBanTarget { + get { + return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban yourself!. + /// + internal static string UserCannotBanThemselves { + get { + return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick me!. + /// + internal static string UserCannotKickBot { + get { + return ResourceManager.GetString("UserCannotKickBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick members from this guild!. + /// + internal static string UserCannotKickMembers { + get { + return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick the owner of this guild!. + /// + internal static string UserCannotKickOwner { + get { + return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick this member!. + /// + internal static string UserCannotKickTarget { + get { + return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick yourself!. + /// + internal static string UserCannotKickThemselves { + get { + return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot manage this guild!. + /// + internal static string UserCannotManageGuild { + get { + return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot manage messages in this guild!. + /// + internal static string UserCannotManageMessages { + get { + return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot moderate members in this guild!. + /// + internal static string UserCannotModerateMembers { + get { + return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute me!. + /// + internal static string UserCannotMuteBot { + get { + return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute the owner of this guild!. + /// + internal static string UserCannotMuteOwner { + get { + return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute this member!. + /// + internal static string UserCannotMuteTarget { + get { + return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute yourself!. + /// + internal static string UserCannotMuteThemselves { + get { + return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to .... + /// + internal static string UserCannotUnmuteBot { + get { + return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. + /// + internal static string UserCannotUnmuteOwner { + get { + return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot unmute this user!. + /// + internal static string UserCannotUnmuteTarget { + get { + return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are muted!. + /// + internal static string UserCannotUnmuteThemselves { + get { + return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This user is not banned!. /// internal static string UserNotBanned { get { @@ -851,15 +1094,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to {0} unbanned {1} for {2}. - /// - internal static string UserUnbanned { - get { - return ResourceManager.GetString("UserUnbanned", resourceCulture); - } - } - /// /// Looks up a localized string similar to Yes. /// diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 4d1685d..88ab26c 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -24,24 +24,15 @@ PublicKeyToken=b77a5c561934e089 - - Couldn't find guild by message! - {0}I'm ready! (C#) - - Message deleted in {0}, but I forgot what was there - Deleted message from {0} in channel {1}: {2} Too many mentions in 1 message - - Message edited from {0} in channel {1}, but I forgot what was there before the edit: {2} - Edited message in channel {0}: {1} -> {2} @@ -63,35 +54,17 @@ You do not have permission to execute this command! - - Members are in different guilds! - - - You cannot interact with guild owner! - - - You cannot interact with yourself! - - - You cannot interact with me! - - - You cannot interact with this member! - - - I cannot interact with this member! - You were banned by {0} in guild {1} for {2} Punishment expired - - Negative message amount specified! + + You specified less than {0} messages! - Too many messages specified! + You specified more than {0} messages! Command help: @@ -148,7 +121,7 @@ No - User not banned! + This user is not banned! Member not muted! @@ -156,20 +129,11 @@ Someone removed the mute role manually! I added back all roles that I removed during the mute - - {0} unmuted {1} for {2} - - - {0} unbanned {1} for {2} - Welcome message - - Not enough arguments! Needed: {0}, provided: {1} - - - Invalid message amount specified! + + You need to specify an integer from {0} to {1} instead of {2}! Banned {0} for{1}: {2} @@ -192,9 +156,6 @@ This channel does not exist! - - I couldn't parse the specified duration! One of the components could be outside it's valid range (e.g. `24h` or `60m`) - I couldn't remove role {0} because of an error! {1} @@ -237,9 +198,6 @@ Event {0} has completed! Duration: {1} - - That user doesn't exist! - *[{0}: {1}]* @@ -297,7 +255,127 @@ Unmutes a member - - You must specify a reason! + + 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 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! + + + You cannot manage this guild! + + + I cannot ban users from this guild! + + + I cannot manage messages in this guild! + + + I cannot kick members from this guild! + + + I cannot moderate members in this guild! + + + I cannot manage this guild! + + + You need to specify a reason to ban this user! + + + You need to specify a reason to kick this member! + + + You need to specify a reason to mute this member! + + + You need to specify a reason to unban this user! + + + 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! + + + You cannot ban yourself! + + + You cannot ban me! + + + I cannot ban this user! + + + You cannot ban this user! + + + You cannot kick the owner of this guild! + + + You cannot kick yourself! + + + You cannot kick me! + + + I cannot kick this member! + + + You cannot kick this member! + + + You cannot mute the owner of this guild! + + + You cannot mute yourself! + + + You cannot mute me! + + + I cannot mute this member! + + + You cannot mute this member! + + + You don't need to unmute the owner of this guild! + + + You are muted! + + + ... + + + I cannot unmute this member! + + + You cannot unmute this user! diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 0c10ab5..78359d6 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -15,24 +15,15 @@ PublicKeyToken=b77a5c561934e089 - - Не удалось найти сервер по каналу! - {0}Я запустился! (C#) - - Удалено сообщение в канале {0}, но я забыл что там было - Удалено сообщение от {0} в канале {1}: {2} Слишком много упоминаний в одном сообщении - - Отредактировано сообщение от {0} в канале {1}, но я забыл что там было до редактирования: {2} - Отредактировано сообщение в канале {0}: {1} -> {2} @@ -54,35 +45,17 @@ У тебя недостаточно прав для выполнения этой команды! - - Участники находятся в разных гильдиях! - - - Ты не можешь взаимодействовать с владельцем сервера! - - - Ты не можешь взаимодействовать с самим собой! - - - Ты не можешь со мной взаимодействовать! - - - Ты не можешь взаимодействовать с этим участником! - - - Я не могу взаимодействовать с этим участником! - Тебя забанил {0} на сервере {1} за {2} Время наказания истекло - - Указано отрицательное количество сообщений! + + Указано менее {0} сообщений! - Указано слишком много сообщений! + Указано более {0} сообщений! Справка по командам: @@ -136,7 +109,7 @@ Нет - Пользователь не забанен! + Этот пользователь не забанен! Участник не заглушен! @@ -144,20 +117,11 @@ Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте - - {0} возвращает из мута {1} за {2} - - - {0} возвращает из бана {1} за {2} - Приветствие - - Недостаточно аргументов! Требуется: {0}, указано: {1} - - - Указано неверное количество сообщений! + + Надо указать целое число от {0} до {1} вместо {2}! Забанен {0} на{1}: {2} @@ -180,9 +144,6 @@ Этот канал не существует! - - Мне не удалось обработать продолжительность! Один из компонентов может быть за пределами допустимого диапазона (например, `24ч` или `60м`) - Я не смог забрать роль {0} в связи с ошибкой! {1} @@ -228,9 +189,6 @@ Событие {0} завершено! Продолжительность: {1} - - Такого пользователя не существует! - *[{0}: {1}]* @@ -288,7 +246,127 @@ Разглушает участника - - Требуется указать причину! + + Надо указать целое число от {0} до {1}! + + + Надо указать пользователя! + + + Надо указать пользователя вместо {0}! + + + Надо указать участника сервера! + + + Надо указать участника сервера вместо {0}! + + + Ты не можешь банить пользователей на этом сервере! + + + Ты не можешь управлять сообщениями этого сервера! + + + Ты не можешь выгонять участников с этого сервера! + + + Ты не можешь модерировать участников этого сервера! + + + Ты не можешь настраивать этот сервер! + + + Я не могу банить пользователей на этом сервере! + + + Я не могу управлять сообщениями этого сервера! + + + Я не могу выгонять участников с этого сервера! + + + Я не могу модерировать участников этого сервера! + + + Я не могу настраивать этот сервер! + + + Надо указать причину для бана этого участника! + + + Надо указать причину для кика этого участника! + + + Надо указать причину для мута этого участника! + + + Надо указать настройку, которую нужно изменить! + + + Надо указать причину для разбана этого пользователя! + + + Надо указать причину для размута этого участника! + + + Ты не можешь меня забанить! + + + Ты не можешь забанить владельца этого сервера! + + + Ты не можешь забанить этого участника! + + + Ты не можешь себя забанить! + + + Я не могу забанить этого пользователя! + + + Ты не можешь выгнать владельца этого сервера! + + + Ты не можешь себя выгнать! + + + Ты не можешь меня выгнать! + + + Я не могу выгнать этого участника + + + Ты не можешь выгнать этого участника! + + + Ты не можешь заглушить владельца этого сервера! + + + Ты не можешь себя заглушить! + + + Ты не можешь заглушить меня! + + + Я не могу заглушить этого пользователя! + + + Ты не можешь заглушить этого участника! + + + Тебе не надо возвращать из мута владельца этого сервера! + + + Ты заглушен! + + + ... + + + Ты не можешь вернуть из мута этого пользователя! + + + Я не могу вернуть из мута этого пользователя! diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 213196e..3802ceb 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Reflection; +using System.Text; using System.Text.RegularExpressions; using Discord; using Discord.Net; @@ -65,7 +66,7 @@ public static class Utils { } } - public static SocketRole? GetMuteRole(ref SocketGuild guild) { + public static SocketRole? GetMuteRole(SocketGuild guild) { var id = ulong.Parse(Boyfriend.GetGuildConfig(guild.Id)["MuteRole"]); if (MuteRoleCache.ContainsKey(id)) return MuteRoleCache[id]; SocketRole? role = null; @@ -90,41 +91,6 @@ public static class Utils { await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); } - public static TimeSpan? GetTimeSpan(ref string from) { - var chars = from.AsSpan(); - var numberBuilder = Boyfriend.StringBuilder; - int days = 0, hours = 0, minutes = 0, seconds = 0; - foreach (var c in chars) - if (char.IsDigit(c)) { numberBuilder.Append(c); } else { - if (numberBuilder.Length == 0) return null; - switch (c) { - case 'd' or 'D' or 'д' or 'Д': - days += int.Parse(numberBuilder.ToString()); - numberBuilder.Clear(); - break; - case 'h' or 'H' or 'ч' or 'Ч': - hours += int.Parse(numberBuilder.ToString()); - numberBuilder.Clear(); - break; - case 'm' or 'M' or 'м' or 'М': - minutes += int.Parse(numberBuilder.ToString()); - numberBuilder.Clear(); - break; - case 's' or 'S' or 'с' or 'С': - seconds += int.Parse(numberBuilder.ToString()); - numberBuilder.Clear(); - break; - default: return null; - } - } - - numberBuilder.Clear(); - return new TimeSpan(days, hours, minutes, seconds); - } - - public static string JoinString(ref string[] args, int startIndex) { - return string.Join(" ", args, startIndex, args.Length - startIndex); - } public static RequestOptions GetRequestOptions(string reason) { var options = RequestOptions.Default; @@ -139,7 +105,12 @@ public static class Utils { var toReturn = typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) - ?.ToString()! ?? throw new Exception($"Could not find localized property: {propertyName}"); + ?.ToString(); + if (toReturn == null) { + Console.WriteLine($@"Could not find localized property: {propertyName}"); + return name; + } + ReflectionMessageCache.Add(name, toReturn); return toReturn; } @@ -154,13 +125,7 @@ public static class Utils { await SilentSendAsync(systemChannel, toSend); } - public static void StackFeedback(ref string feedback, ref string mention, bool isPublic) { - var toAppend = string.Format(Messages.FeedbackFormat, mention, feedback); - CommandHandler.StackedPrivateFeedback.AppendLine(toAppend); - if (isPublic) CommandHandler.StackedPublicFeedback.AppendLine(toAppend); - } - - public static string GetHumanizedTimeOffset(ref TimeSpan span) { + public static string GetHumanizedTimeOffset(TimeSpan span) { return span.TotalSeconds > 0 ? $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture)}" : Messages.Ever; @@ -169,4 +134,23 @@ public static class Utils { public static void SetCurrentLanguage(ulong guildId) { Messages.Culture = CultureInfoCache[Boyfriend.GetGuildConfig(guildId)["Lang"]]; } -} + + public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketTextChannel? channel) { + if (channel == null) return; + if (appendTo.Length + appendWhat.Length > 2000) { + _ = SilentSendAsync(channel, appendTo.ToString()); + appendTo.Clear(); + } + + appendTo.AppendLine(appendWhat); + } + + public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketUserMessage message) { + if (appendTo.Length + appendWhat.Length > 2000) { + _ = message.ReplyAsync(appendTo.ToString(), false, null, AllowedMentions.None); + appendTo.Clear(); + } + + appendTo.AppendLine(appendWhat); + } +} \ No newline at end of file From e767205c1a2168f37ec8138d4467feeecac2ba83 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 18 Sep 2022 19:41:29 +0500 Subject: [PATCH 021/329] Seal all possible classes, add LICENSE, follow async naming conventions --- .editorconfig | 39 ++ Boyfriend/Boyfriend.cs | 4 +- Boyfriend/Boyfriend.csproj | 13 +- Boyfriend/CommandProcessor.cs | 85 ++-- Boyfriend/Commands/BanCommand.cs | 18 +- Boyfriend/Commands/ClearCommand.cs | 10 +- Boyfriend/Commands/Command.cs | 7 - Boyfriend/Commands/HelpCommand.cs | 8 +- Boyfriend/Commands/ICommand.cs | 7 + Boyfriend/Commands/KickCommand.cs | 14 +- Boyfriend/Commands/MuteCommand.cs | 35 +- Boyfriend/Commands/PingCommand.cs | 8 +- Boyfriend/Commands/SelfBanCommand.cs | 9 + Boyfriend/Commands/SettingsCommand.cs | 10 +- Boyfriend/Commands/UnbanCommand.cs | 12 +- Boyfriend/Commands/UnmuteCommand.cs | 18 +- Boyfriend/EventHandler.cs | 10 +- Boyfriend/Utils.cs | 21 +- LICENSE | 661 ++++++++++++++++++++++++++ global.json | 7 + 20 files changed, 873 insertions(+), 123 deletions(-) create mode 100644 .editorconfig delete mode 100644 Boyfriend/Commands/Command.cs create mode 100644 Boyfriend/Commands/ICommand.cs create mode 100644 Boyfriend/Commands/SelfBanCommand.cs create mode 100644 LICENSE create mode 100644 global.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a647b0a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,39 @@ +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 +tab_width = 4 + +# Microsoft .NET properties +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = none +csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_csharp_wrap_before_first_type_parameter_constraint = true +resharper_place_simple_case_statement_on_same_line = true +resharper_place_simple_embedded_block_on_same_line = true +resharper_place_simple_switch_expression_on_single_line = true +resharper_wrap_before_arrow_with_expressions = true +resharper_wrap_before_eq = true +resharper_wrap_before_extends_colon = true +resharper_wrap_before_linq_expression = true diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 77607fb..ec670e9 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -20,7 +20,7 @@ public static class Boyfriend { }; public static readonly DiscordSocketClient Client = new(Config); - private static readonly Game Activity = new("Toby Fox - The World Revolving", ActivityType.Listening); + private static readonly Game Activity = new("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening); private static readonly Dictionary> GuildConfigDictionary = new(); @@ -71,7 +71,7 @@ public static class Boyfriend { return Task.CompletedTask; } - public static async Task WriteGuildConfig(ulong id) { + public static async Task WriteGuildConfigAsync(ulong id) { var json = JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented); var removedRoles = JsonConvert.SerializeObject(RemovedRolesDictionary[id], Formatting.Indented); diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index a37a533..bb143c7 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -8,10 +8,19 @@ default Boyfriend l1ttle - https://github.com/l1ttleO/Boyfriend-CSharp - https://github.com/l1ttleO/Boyfriend-CSharp + https://git.cavej376.xyz/Octol1ttle/Boyfriend-CSharp + https://git.cavej376.xyz/Octol1ttle/Boyfriend-CSharp git 1.0.1 + https://git.cavej376.xyz/Octol1ttle/Boyfriend-CSharp/src/branch/master/LICENSE + en + + + + + true + x64 + none diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index 2d83b54..bac4013 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -7,16 +7,17 @@ using Discord.WebSocket; namespace Boyfriend; -public class CommandProcessor { +public sealed class CommandProcessor { private const string Success = ":white_check_mark: "; private const string MissingArgument = ":keyboard: "; private const string InvalidArgument = ":construction: "; private const string NoAccess = ":no_entry_sign: "; private const string CantInteract = ":vertical_traffic_light: "; - public static readonly Command[] Commands = { + public static readonly ICommand[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), new KickCommand(), new MuteCommand(), new PingCommand(), + new SelfBanCommand(), new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() }; @@ -35,14 +36,12 @@ public class CommandProcessor { Context = new SocketCommandContext(Boyfriend.Client, message); } - public async Task HandleCommand() { - _stackedReplyMessage.Clear(); - _stackedPrivateFeedback.Clear(); - _stackedPublicFeedback.Clear(); + public async Task HandleCommandAsync() { var guild = Context.Guild; var config = Boyfriend.GetGuildConfig(guild.Id); + var muteRole = Utils.GetMuteRole(guild); - if (GetMember().Roles.Contains(Utils.GetMuteRole(guild))) { + if (GetMember().Roles.Contains(muteRole)) { await Context.Message.ReplyAsync(Messages.UserCannotUnmuteThemselves); return; } @@ -54,35 +53,36 @@ public class CommandProcessor { } var list = Context.Message.Content.Split("\n"); - foreach (var line in list) { - RunCommandOnLine(line, regex); + var cleanList = Context.Message.CleanContent.Split("\n"); + for (var i = 0; i < list.Length; i++) { + RunCommandOnLine(list[i], cleanList[i], regex); if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync(); + var member = Boyfriend.Client.GetGuild(Context.Guild.Id) + .GetUser(Context.User.Id); // Getting an up-to-date copy + if (member == null || member.Roles.Contains(muteRole) + || member.TimedOutUntil.GetValueOrDefault(DateTimeOffset.UnixEpoch).ToUnixTimeSeconds() > + DateTimeOffset.Now.ToUnixTimeSeconds()) + break; } await Task.WhenAll(_tasks); + _tasks.Clear(); - if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfig(guild.Id); + if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfigAsync(guild.Id); - if (_stackedReplyMessage.Length > 0) _ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString()); - - var adminChannel = Utils.GetAdminLogChannel(guild.Id); - var systemChannel = guild.SystemChannel; - if (_stackedPrivateFeedback.Length > 0 && adminChannel != null && adminChannel.Id != Context.Message.Channel.Id) - _ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString()); - if (_stackedPublicFeedback.Length > 0 && systemChannel != null && systemChannel.Id != adminChannel?.Id - && systemChannel.Id != Context.Message.Channel.Id) - _ = Utils.SilentSendAsync(systemChannel, _stackedPublicFeedback.ToString()); + SendFeedbacks(); } - private void RunCommandOnLine(string line, Regex regex) { + private void RunCommandOnLine(string line, string cleanLine, Regex regex) { foreach (var command in Commands) { var lineNoMention = regex.Replace(MentionRegex.Replace(line, "", 1), "", 1); if (lineNoMention == line - || !command.Aliases.Contains(regex.Replace(lineNoMention, "", 1).Trim().ToLower().Split()[0])) + || !command.Aliases.Contains(lineNoMention.Trim().ToLower().Split()[0])) continue; - var args = line.Split().Skip(1).ToArray(); - _tasks.Add(command.Run(this, args)); + var args = line.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); + var cleanArgs = cleanLine.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); + _tasks.Add(command.RunAsync(this, args, cleanArgs)); } } @@ -94,6 +94,26 @@ public class CommandProcessor { var format = string.Format(Messages.FeedbackFormat, Context.User.Mention, action); if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, Context.Guild.SystemChannel); Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, Utils.GetAdminLogChannel(Context.Guild.Id)); + if (_tasks.Count == 0) SendFeedbacks(false); + } + + private void SendFeedbacks(bool reply = true) { + if (reply && _stackedReplyMessage.Length > 0) + _ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None); + + var adminChannel = Utils.GetAdminLogChannel(Context.Guild.Id); + var systemChannel = Context.Guild.SystemChannel; + if (_stackedPrivateFeedback.Length > 0 && adminChannel != null && + adminChannel.Id != Context.Message.Channel.Id) { + _ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString()); + _stackedPrivateFeedback.Clear(); + } + + if (_stackedPublicFeedback.Length > 0 && systemChannel != null && systemChannel.Id != adminChannel?.Id + && systemChannel.Id != Context.Message.Channel.Id) { + _ = Utils.SilentSendAsync(systemChannel, _stackedPublicFeedback.ToString()); + _stackedPublicFeedback.Clear(); + } } public string? GetRemaining(string[] from, int startIndex, string? argument) { @@ -105,7 +125,7 @@ public class CommandProcessor { return null; } - public SocketUser? GetUser(string[] args, int index, string? argument) { + public SocketUser? GetUser(string[] args, string[] cleanArgs, int index, string? argument) { if (index >= args.Length) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Messages.MissingUser}", Context.Message); @@ -115,7 +135,8 @@ public class CommandProcessor { var user = Utils.ParseUser(args[index]); if (user == null && argument != null) Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{InvalidArgument}{string.Format(Messages.InvalidUser, args[index])}", Context.Message); + $"{InvalidArgument}{string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}", + Context.Message); return user; } @@ -141,7 +162,7 @@ public class CommandProcessor { return member; } - public SocketGuildUser? GetMember(string[] args, int index, string? argument) { + public SocketGuildUser? GetMember(string[] args, string[] cleanArgs, int index, string? argument) { if (index >= args.Length) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Messages.MissingMember}", Context.Message); @@ -151,7 +172,8 @@ public class CommandProcessor { var member = Context.Guild.GetUser(Utils.ParseMention(args[index])); if (member == null && argument != null) Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{InvalidArgument}{string.Format(Messages.InvalidMember, Utils.Wrap(args[index]))}", Context.Message); + $"{InvalidArgument}{string.Format(Messages.InvalidMember, Utils.Wrap(cleanArgs[index]))}", + Context.Message); return member; } @@ -167,9 +189,12 @@ public class CommandProcessor { } var id = Utils.ParseMention(args[index]); - if (Context.Guild.GetBanAsync(id) != null) return id; - Utils.SafeAppendToBuilder(_stackedReplyMessage, Messages.UserNotBanned, Context.Message); - return null; + if (Context.Guild.GetBanAsync(id) == null) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, Messages.UserNotBanned, Context.Message); + return null; + } + + return id; } public int? GetNumberRange(string[] args, int index, int min, int max, string? argument) { diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 7e42b6a..99676eb 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -3,11 +3,11 @@ using Discord.WebSocket; namespace Boyfriend.Commands; -public class BanCommand : Command { - public override string[] Aliases { get; } = { "ban", "бан" }; +public sealed class BanCommand : ICommand { + public string[] Aliases { get; } = { "ban", "бан" }; - public override async Task Run(CommandProcessor cmd, string[] args) { - var toBan = cmd.GetUser(args, 0, "ToBan"); + public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { + var toBan = cmd.GetUser(args, cleanArgs, 0, "ToBan"); if (toBan == null || !cmd.HasPermission(GuildPermission.BanMembers)) return; var memberToBan = cmd.GetMember(toBan, null); @@ -34,11 +34,7 @@ public class BanCommand : Command { cmd.Reply(feedback, ":hammer: "); cmd.Audit(feedback); - if (duration.TotalSeconds > 0) { - var _ = async () => { - await Task.Delay(duration); - await UnbanCommand.UnbanUser(cmd, toBan.Id, Messages.PunishmentExpired); - }; - } + if (duration.TotalSeconds > 0) + await Task.FromResult(Utils.DelayedUnbanAsync(cmd, toBan.Id, Messages.PunishmentExpired, duration)); } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs index 164e0c6..9c46ec8 100644 --- a/Boyfriend/Commands/ClearCommand.cs +++ b/Boyfriend/Commands/ClearCommand.cs @@ -3,15 +3,15 @@ using Discord.WebSocket; namespace Boyfriend.Commands; -public class ClearCommand : Command { - public override string[] Aliases { get; } = { "clear", "purge", "очистить", "стереть" }; +public sealed class ClearCommand : ICommand { + public string[] Aliases { get; } = { "clear", "purge", "очистить", "стереть" }; - public override async Task Run(CommandProcessor cmd, string[] args) { + public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { if (cmd.Context.Channel is not SocketTextChannel channel) throw new Exception(); if (!cmd.HasPermission(GuildPermission.ManageMessages)) return; - var toDelete = cmd.GetNumberRange(args, 0, 1, 200, "ClearAmount"); + var toDelete = cmd.GetNumberRange(cleanArgs, 0, 1, 200, "ClearAmount"); if (toDelete == null) return; var messages = await channel.GetMessagesAsync((int)(toDelete + 1)).FlattenAsync(); @@ -20,4 +20,4 @@ public class ClearCommand : Command { cmd.Audit(string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString())); } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/Command.cs b/Boyfriend/Commands/Command.cs deleted file mode 100644 index cf8563b..0000000 --- a/Boyfriend/Commands/Command.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Boyfriend.Commands; - -public abstract class Command { - public abstract string[] Aliases { get; } - - public abstract Task Run(CommandProcessor cmd, string[] args); -} \ No newline at end of file diff --git a/Boyfriend/Commands/HelpCommand.cs b/Boyfriend/Commands/HelpCommand.cs index 1a27bcd..948b716 100644 --- a/Boyfriend/Commands/HelpCommand.cs +++ b/Boyfriend/Commands/HelpCommand.cs @@ -2,10 +2,10 @@ namespace Boyfriend.Commands; -public class HelpCommand : Command { - public override string[] Aliases { get; } = { "help", "помощь", "справка" }; +public sealed class HelpCommand : ICommand { + public string[] Aliases { get; } = { "help", "помощь", "справка" }; - public override Task Run(CommandProcessor cmd, string[] args) { + public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { var prefix = Boyfriend.GetGuildConfig(cmd.Context.Guild.Id)["Prefix"]; var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp); @@ -17,4 +17,4 @@ public class HelpCommand : Command { return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/ICommand.cs b/Boyfriend/Commands/ICommand.cs new file mode 100644 index 0000000..5dccb98 --- /dev/null +++ b/Boyfriend/Commands/ICommand.cs @@ -0,0 +1,7 @@ +namespace Boyfriend.Commands; + +public interface ICommand { + public string[] Aliases { get; } + + public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs); +} diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index ba1a139..b3eda26 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -3,19 +3,19 @@ using Discord.WebSocket; namespace Boyfriend.Commands; -public class KickCommand : Command { - public override string[] Aliases { get; } = { "kick", "кик", "выгнать" }; +public sealed class KickCommand : ICommand { + public string[] Aliases { get; } = { "kick", "кик", "выгнать" }; - public override async Task Run(CommandProcessor cmd, string[] args) { - var toKick = cmd.GetMember(args, 0, "ToKick"); + public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { + var toKick = cmd.GetMember(args, cleanArgs, 0, "ToKick"); if (toKick == null || !cmd.HasPermission(GuildPermission.KickMembers)) return; if (!cmd.CanInteractWith(toKick, "Kick")) return; - await KickMember(cmd, toKick, cmd.GetRemaining(args, 1, "KickReason")); + await KickMemberAsync(cmd, toKick, cmd.GetRemaining(args, 1, "KickReason")); } - private static async Task KickMember(CommandProcessor cmd, SocketGuildUser toKick, string? reason) { + private static async Task KickMemberAsync(CommandProcessor cmd, SocketGuildUser toKick, string? reason) { if (reason == null) return; var guildKickMessage = $"({cmd.Context.User}) {reason}"; @@ -28,4 +28,4 @@ public class KickCommand : Command { cmd.Reply(format, ":police_car: "); cmd.Audit(format); } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index c972614..998c65e 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -4,11 +4,11 @@ using Discord.WebSocket; namespace Boyfriend.Commands; -public class MuteCommand : Command { - public override string[] Aliases { get; } = { "mute", "timeout", "заглушить", "мут" }; +public sealed class MuteCommand : ICommand { + public string[] Aliases { get; } = { "mute", "timeout", "заглушить", "мут" }; - public override async Task Run(CommandProcessor cmd, string[] args) { - var toMute = cmd.GetMember(args, 0, "ToMute"); + public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { + var toMute = cmd.GetMember(args, cleanArgs, 0, "ToMute"); if (toMute == null) return; var duration = CommandProcessor.GetTimeSpan(args, 1); @@ -16,13 +16,12 @@ public class MuteCommand : Command { if (reason == null) return; var role = Utils.GetMuteRole(cmd.Context.Guild); - if (role != null) { - if (toMute.Roles.Contains(role) || (toMute.TimedOutUntil != null && - toMute.TimedOutUntil.Value.ToUnixTimeMilliseconds() > - DateTimeOffset.Now.ToUnixTimeMilliseconds())) { - cmd.Reply(Messages.MemberAlreadyMuted, ":x: "); - return; - } + if ((role != null && toMute.Roles.Contains(role)) + || (toMute.TimedOutUntil != null + && toMute.TimedOutUntil.Value.ToUnixTimeSeconds() + > DateTimeOffset.Now.ToUnixTimeSeconds())) { + cmd.Reply(Messages.MemberAlreadyMuted, ":x: "); + return; } var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id); @@ -36,10 +35,10 @@ public class MuteCommand : Command { if (!cmd.HasPermission(GuildPermission.ModerateMembers) || !cmd.CanInteractWith(toMute, "Mute")) return; - await MuteMember(cmd, toMute, duration, reason); + await MuteMemberAsync(cmd, toMute, duration, reason); } - private static async Task MuteMember(CommandProcessor cmd, SocketGuildUser toMute, + private static async Task MuteMemberAsync(CommandProcessor cmd, SocketGuildUser toMute, TimeSpan duration, string reason) { var guild = cmd.Context.Guild; var config = Boyfriend.GetGuildConfig(guild.Id); @@ -66,12 +65,8 @@ public class MuteCommand : Command { await toMute.AddRoleAsync(role, requestOptions); - if (hasDuration) { - var _ = async () => { - await Task.Delay(duration); - await UnmuteCommand.UnmuteMember(cmd, toMute, Messages.PunishmentExpired); - }; - } + if (hasDuration) + await Task.FromResult(Utils.DelayedUnmuteAsync(cmd, toMute, Messages.PunishmentExpired, duration)); } else { if (!hasDuration || duration.TotalDays > 28) { cmd.Reply(Messages.DurationRequiredForTimeOuts, ":x: "); @@ -92,4 +87,4 @@ public class MuteCommand : Command { cmd.Reply(feedback, ":mute: "); cmd.Audit(feedback); } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/PingCommand.cs b/Boyfriend/Commands/PingCommand.cs index 4034f96..c9c9b8e 100644 --- a/Boyfriend/Commands/PingCommand.cs +++ b/Boyfriend/Commands/PingCommand.cs @@ -1,9 +1,9 @@ namespace Boyfriend.Commands; -public class PingCommand : Command { - public override string[] Aliases { get; } = { "ping", "latency", "pong", "пинг", "задержка", "понг" }; +public sealed class PingCommand : ICommand { + public string[] Aliases { get; } = { "ping", "latency", "pong", "пинг", "задержка", "понг" }; - public override Task Run(CommandProcessor cmd, string[] args) { + public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { var builder = Boyfriend.StringBuilder; builder.Append(Utils.GetBeep()) @@ -15,4 +15,4 @@ public class PingCommand : Command { return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/SelfBanCommand.cs b/Boyfriend/Commands/SelfBanCommand.cs new file mode 100644 index 0000000..e5b1cbe --- /dev/null +++ b/Boyfriend/Commands/SelfBanCommand.cs @@ -0,0 +1,9 @@ +namespace Boyfriend.Commands; + +public sealed class SelfBanCommand : ICommand { + public string[] Aliases { get; } = { "grantoverseer", "grant", "overseer", "voooo", "overseergrant", "special" }; + + public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { + await BanCommand.BanUser(cmd, cmd.Context.User, TimeSpan.FromMilliseconds(-1), ""); + } +} diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 0a5f3cf..8738972 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -2,10 +2,10 @@ namespace Boyfriend.Commands; -public class SettingsCommand : Command { - public override string[] Aliases { get; } = { "settings", "config", "настройки", "конфиг" }; +public sealed class SettingsCommand : ICommand { + public string[] Aliases { get; } = { "settings", "config", "настройки", "конфиг" }; - public override Task Run(CommandProcessor cmd, string[] args) { + public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { if (!cmd.HasPermission(GuildPermission.ManageGuild)) return Task.CompletedTask; var guild = cmd.Context.Guild; @@ -48,7 +48,7 @@ public class SettingsCommand : Command { var exists = false; // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - // The performance impact is not worth it + // Too many allocations foreach (var setting in Boyfriend.DefaultConfig.Keys) { if (selectedSetting != setting.ToLower()) continue; selectedSetting = setting; @@ -161,4 +161,4 @@ public class SettingsCommand : Command { private static bool IsBool(string value) { return value is "true" or "false"; } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index 602f497..3ef5c15 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -2,10 +2,10 @@ namespace Boyfriend.Commands; -public class UnbanCommand : Command { - public override string[] Aliases { get; } = { "unban", "разбан" }; +public sealed class UnbanCommand : ICommand { + public string[] Aliases { get; } = { "unban", "разбан" }; - public override async Task Run(CommandProcessor cmd, string[] args) { + public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { if (!cmd.HasPermission(GuildPermission.BanMembers)) return; var id = cmd.GetBan(args, 0); @@ -13,10 +13,10 @@ public class UnbanCommand : Command { var reason = cmd.GetRemaining(args, 1, "UnbanReason"); if (reason == null) return; - await UnbanUser(cmd, id.Value, reason); + await UnbanUserAsync(cmd, id.Value, reason); } - public static async Task UnbanUser(CommandProcessor cmd, ulong id, string reason) { + public 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); @@ -24,4 +24,4 @@ public class UnbanCommand : Command { cmd.Reply(feedback); cmd.Audit(feedback); } -} \ No newline at end of file +} diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index f5de6ed..8297830 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -3,19 +3,19 @@ using Discord.WebSocket; namespace Boyfriend.Commands; -public class UnmuteCommand : Command { - public override string[] Aliases { get; } = { "unmute", "размут" }; +public sealed class UnmuteCommand : ICommand { + public string[] Aliases { get; } = { "unmute", "размут" }; - public override async Task Run(CommandProcessor cmd, string[] args) { + public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return; - var toUnmute = cmd.GetMember(args, 0, "ToUnmute"); + var toUnmute = cmd.GetMember(args, cleanArgs, 0, "ToUnmute"); var reason = cmd.GetRemaining(args, 1, "UnmuteReason"); if (toUnmute == null || reason == null || !cmd.CanInteractWith(toUnmute, "Unmute")) return; - await UnmuteMember(cmd, toUnmute, reason); + await UnmuteMemberAsync(cmd, toUnmute, reason); } - public static async Task UnmuteMember(CommandProcessor cmd, SocketGuildUser toUnmute, + 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); @@ -31,8 +31,8 @@ public class UnmuteCommand : Command { await toUnmute.RemoveRoleAsync(role, requestOptions); } else { - if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeMilliseconds() < - DateTimeOffset.Now.ToUnixTimeMilliseconds()) { + if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeSeconds() < + DateTimeOffset.Now.ToUnixTimeSeconds()) { cmd.Reply(Messages.MemberNotMuted, ":x: "); return; } @@ -44,4 +44,4 @@ public class UnmuteCommand : Command { cmd.Reply(feedback, ":loud_sound: "); cmd.Audit(feedback); } -} \ No newline at end of file +} diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 94e75ac..d4eb73e 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -5,7 +5,7 @@ using Discord.WebSocket; namespace Boyfriend; -public class EventHandler { +public sealed class EventHandler { private readonly DiscordSocketClient _client = Boyfriend.Client; public void InitEvents() { @@ -50,7 +50,7 @@ public class EventHandler { if (auditLogEntry.Data is MessageDeleteAuditLogData data && msg.Author.Id == data.Target.Id) mention = auditLogEntry.User.Mention; - await Utils.SendFeedback( + await Utils.SendFeedbackAsync( string.Format(Messages.CachedMessageDeleted, msg.Author.Mention, Utils.MentionChannel(channel.Id), Utils.Wrap(msg.CleanContent)), guild.Id, mention); } @@ -83,7 +83,7 @@ public class EventHandler { (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)))) return; - _ = new CommandProcessor(message).HandleCommand(); + _ = new CommandProcessor(message).HandleCommandAsync(); } private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, @@ -98,7 +98,7 @@ public class EventHandler { var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940; - await Utils.SendFeedback( + await Utils.SendFeedbackAsync( string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id), Utils.Wrap(msg.CleanContent, isLimitedSpace), Utils.Wrap(messageSocket.CleanContent, isLimitedSpace)), guildId, msg.Author.Mention); @@ -176,4 +176,4 @@ public class EventHandler { await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); } -} \ No newline at end of file +} diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 3802ceb..43f2658 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using Boyfriend.Commands; using Discord; using Discord.Net; using Discord.WebSocket; @@ -55,10 +56,6 @@ public static class Utils { return user; } - public static SocketGuildUser? ParseMember(SocketGuild guild, string mention) { - return guild.GetUser(ParseMention(mention)); - } - public static async Task SendDirectMessage(SocketUser user, string toSend) { try { await user.SendMessageAsync(toSend); } catch (HttpException e) { if (e.DiscordCode != DiscordErrorCode.CannotSendMessageToUser) @@ -115,7 +112,8 @@ public static class Utils { return toReturn; } - public static async Task SendFeedback(string feedback, ulong guildId, string mention, bool sendPublic = false) { + public static async Task + SendFeedbackAsync(string feedback, ulong guildId, string mention, bool sendPublic = false) { var adminChannel = GetAdminLogChannel(guildId); var systemChannel = Boyfriend.Client.GetGuild(guildId).SystemChannel; var toSend = string.Format(Messages.FeedbackFormat, mention, feedback); @@ -153,4 +151,15 @@ public static class Utils { appendTo.AppendLine(appendWhat); } -} \ No newline at end of file + + public static async Task DelayedUnbanAsync(CommandProcessor cmd, ulong banned, string reason, TimeSpan duration) { + await Task.Delay(duration); + await UnbanCommand.UnbanUserAsync(cmd, banned, reason); + } + + public static async Task DelayedUnmuteAsync(CommandProcessor cmd, SocketGuildUser muted, string reason, + TimeSpan duration) { + await Task.Delay(duration); + await UnmuteCommand.UnmuteMemberAsync(cmd, muted, reason); + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..593265c --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/global.json b/global.json new file mode 100644 index 0000000..4c1d7e5 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.0", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} From c0ae850fb8ca53f3fbf5286f77c7c301756119a5 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 18 Oct 2022 22:55:16 +0500 Subject: [PATCH 022/329] Guild blacklist implementation --- Boyfriend/Boyfriend.cs | 11 +- Boyfriend/Boyfriend.csproj | 2 +- Boyfriend/CommandProcessor.cs | 10 + Boyfriend/Commands/SelfBanCommand.cs | 2 +- Boyfriend/EventHandler.cs | 64 ++- Boyfriend/Messages.Designer.cs | 20 +- Boyfriend/Messages.resx | 768 ++++++++++++++------------- Boyfriend/Messages.ru.resx | 750 +++++++++++++------------- Boyfriend/Utils.cs | 5 + 9 files changed, 843 insertions(+), 789 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index ec670e9..df38768 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -60,7 +60,7 @@ public static class Boyfriend { await Client.StartAsync(); await Client.SetActivityAsync(Activity); - new EventHandler().InitEvents(); + EventHandler.InitEvents(); await Task.Delay(-1); } @@ -128,9 +128,12 @@ public static class Boyfriend { public static SocketGuild FindGuild(ulong channel) { if (GuildCache.ContainsKey(channel)) return GuildCache[channel]; foreach (var guild in Client.Guilds) { - if (guild.Channels.All(x => x.Id != channel)) continue; - GuildCache.Add(channel, guild); - return guild; + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var x in guild.Channels) + if (x.Id == channel) { + GuildCache.Add(channel, guild); + return guild; + } } throw new Exception("Could not find guild by channel!"); diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index bb143c7..a578853 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -24,7 +24,7 @@ - + diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index bac4013..b9e4562 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -29,6 +29,7 @@ public sealed class CommandProcessor { private readonly List _tasks = new(); public readonly SocketCommandContext Context; + private bool _serverBlacklisted; public bool ConfigWriteScheduled = false; @@ -56,6 +57,11 @@ public sealed class CommandProcessor { var cleanList = Context.Message.CleanContent.Split("\n"); for (var i = 0; i < list.Length; i++) { RunCommandOnLine(list[i], cleanList[i], regex); + if (_serverBlacklisted) { + await Context.Message.ReplyAsync(Messages.ServerBlacklisted); + return; + } + if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync(); var member = Boyfriend.Client.GetGuild(Context.Guild.Id) .GetUser(Context.User.Id); // Getting an up-to-date copy @@ -79,6 +85,10 @@ public sealed class CommandProcessor { if (lineNoMention == line || !command.Aliases.Contains(lineNoMention.Trim().ToLower().Split()[0])) continue; + if (Utils.IsServerBlacklisted(Context.Guild)) { + _serverBlacklisted = true; + return; + } var args = line.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); var cleanArgs = cleanLine.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); diff --git a/Boyfriend/Commands/SelfBanCommand.cs b/Boyfriend/Commands/SelfBanCommand.cs index e5b1cbe..aca49a4 100644 --- a/Boyfriend/Commands/SelfBanCommand.cs +++ b/Boyfriend/Commands/SelfBanCommand.cs @@ -1,7 +1,7 @@ namespace Boyfriend.Commands; public sealed class SelfBanCommand : ICommand { - public string[] Aliases { get; } = { "grantoverseer", "grant", "overseer", "voooo", "overseergrant", "special" }; + public string[] Aliases { get; } = { "cavepleaselisten" }; public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { await BanCommand.BanUser(cmd, cmd.Context.User, TimeSpan.FromMilliseconds(-1), ""); diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index d4eb73e..2818ba1 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,36 +1,42 @@ -using Boyfriend.Commands; -using Discord; +using Discord; using Discord.Rest; using Discord.WebSocket; +using Humanizer; namespace Boyfriend; -public sealed class EventHandler { - private readonly DiscordSocketClient _client = Boyfriend.Client; +public static class EventHandler { + private static readonly DiscordSocketClient Client = Boyfriend.Client; + private static bool _sendReadyMessages = true; - public void InitEvents() { - _client.Ready += ReadyEvent; - _client.MessageDeleted += MessageDeletedEvent; - _client.MessageReceived += MessageReceivedEvent; - _client.MessageUpdated += MessageUpdatedEvent; - _client.UserJoined += UserJoinedEvent; - _client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; - _client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; - _client.GuildScheduledEventStarted += ScheduledEventStartedEvent; - _client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; + public static void InitEvents() { + Client.Ready += ReadyEvent; + Client.MessageDeleted += MessageDeletedEvent; + Client.MessageReceived += MessageReceivedEvent; + Client.MessageUpdated += MessageUpdatedEvent; + Client.UserJoined += UserJoinedEvent; + Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; + Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; + Client.GuildScheduledEventStarted += ScheduledEventStartedEvent; + Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; } - private static async Task ReadyEvent() { + private static Task ReadyEvent() { + if (!_sendReadyMessages) return Task.CompletedTask; var i = Utils.Random.Next(3); - foreach (var guild in Boyfriend.Client.Guilds) { + foreach (var guild in Client.Guilds) { var config = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(config["BotLogChannel"])); Utils.SetCurrentLanguage(guild.Id); - if (config["ReceiveStartupMessages"] is not "true" || channel == null) continue; - await channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); + if (config["ReceiveStartupMessages"] is not "true" || channel == null || + Utils.IsServerBlacklisted(guild)) continue; + _ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); } + + _sendReadyMessages = false; + return Task.CompletedTask; } private static async Task MessageDeletedEvent(Cacheable message, @@ -39,6 +45,7 @@ public sealed class EventHandler { if (msg is null or ISystemMessage || msg.Author.IsBot) return; var guild = Boyfriend.FindGuild(channel.Value.Id); + if (Utils.IsServerBlacklisted(guild)) return; Utils.SetCurrentLanguage(guild.Id); @@ -62,13 +69,6 @@ public sealed class EventHandler { Utils.SetCurrentLanguage(guild.Id); - if ((message.MentionedUsers.Count > 3 || message.MentionedRoles.Count > 2) && - !user.GuildPermissions.MentionEveryone) { - await BanCommand.BanUser(new CommandProcessor(message), user, TimeSpan.FromMilliseconds(-1), - Messages.AutobanReason); - return; - } - var prev = ""; var prevFailsafe = ""; var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); @@ -92,20 +92,22 @@ public sealed class EventHandler { if (msg is null or ISystemMessage || msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return; - var guildId = Boyfriend.FindGuild(channel.Id).Id; + var guild = Boyfriend.FindGuild(channel.Id); + if (Utils.IsServerBlacklisted(guild)) return; - Utils.SetCurrentLanguage(guildId); + Utils.SetCurrentLanguage(guild.Id); 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)), - guildId, msg.Author.Mention); + guild.Id, msg.Author.Mention); } private static async Task UserJoinedEvent(SocketGuildUser user) { var guild = user.Guild; + if (Utils.IsServerBlacklisted(guild)) return; var config = Boyfriend.GetGuildConfig(guild.Id); if (config["SendWelcomeMessages"] is "true") @@ -118,6 +120,7 @@ public sealed class EventHandler { private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; + if (Utils.IsServerBlacklisted(guild)) return; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCreatedChannel"])); @@ -139,6 +142,7 @@ public sealed class EventHandler { private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; + if (Utils.IsServerBlacklisted(guild)) return; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCancelledChannel"])); if (channel != null) @@ -148,6 +152,7 @@ public sealed class EventHandler { private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; + if (Utils.IsServerBlacklisted(guild)) return; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventStartedChannel"])); @@ -170,10 +175,11 @@ public sealed class EventHandler { private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; + if (Utils.IsServerBlacklisted(guild)) return; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCompletedChannel"])); if (channel != null) await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), - Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().ToString()))); + Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().Humanize()))); } } diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index ad39dc1..c4ab511 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -248,6 +248,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to We do not support hate towards our fellow members. And sometimes, we are not able to ban the offender.. + /// + internal static string CommandDescriptionCavepleaselisten { + get { + return ResourceManager.GetString("CommandDescriptionCavepleaselisten", resourceCulture); + } + } + /// /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. /// @@ -285,7 +294,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Shows latency to Discord servers (not counting local processing time). + /// Looks up a localized string similar to Shows (inaccurate) latency. /// internal static string CommandDescriptionPing { get { @@ -707,6 +716,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to This feature is unavailable because this guild is currently blacklisted.. + /// + internal static string ServerBlacklisted { + get { + return ResourceManager.GetString("ServerBlacklisted", resourceCulture); + } + } + /// /// Looks up a localized string similar to That setting doesn't exist!. /// diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 88ab26c..cbb20ea 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -1,381 +1,387 @@ - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - {0}I'm ready! (C#) - - - Deleted message from {0} in channel {1}: {2} - - - Too many mentions in 1 message - - - Edited message in channel {0}: {1} -> {2} - - - {0}, welcome to {1} - - - Bah! - - - 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} - - - 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} - - - ms - - - Member is already muted! - - - Not specified - - - Not specified - - - Current settings: - - - Language - - - Prefix - - - Remove roles on mute - - - Send welcome messages - - - Starter role - - - Mute role - - - Admin log channel - - - Bot log channel - - - Language not supported! - - - Yes - - - No - - - This user is not banned! - - - Member not muted! - - - Someone removed the mute role manually! I added back all roles that I removed during the mute - - - Welcome message - - - You need to specify an integer from {0} to {1} instead of {2}! - - - Banned {0} for{1}: {2} - - - The specified user is not a member of this server! - - - That setting doesn't exist! - - - Receive startup messages - - - Invalid setting value specified! - - - This role does not exist! - - - 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 - - - I cannot use time-outs on other bots! Try to set a mute role in settings - - - {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6} - - - Role for event creation notifications - - - Channel for event creation notifications - - - Channel for event start notifications - - - Event start notifications receivers - - - {0}Event {1} is starting at {2}! - - - :( - - - Event {0} is cancelled!{1} - - - Channel for event cancellation notifications - - - Channel for event completion notifications - - - Event {0} has completed! Duration: {1} - - - *[{0}: {1}]* - - - ever - - - Deleted {0} messages in {1} - - - Kicked {0}: {1} - - - Muted {0} for{1}: {2} - - - Unbanned {0}: {1} - - - Unmuted {0}: {1} - - - Nothing changed! `{0}` is already set to {1} - - - Not specified - - - Value of setting `{0}` is now set to {1} - - - Bans a user - - - Deletes a specified amount of messages in this channel - - - Shows this message - - - Kicks a member - - - Mutes a member - - - Shows latency to Discord servers (not counting local processing time) - - - Allows you to change certain preferences for this guild - - - Unbans a user - - - Unmutes a member - - - 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 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! - - - You cannot manage this guild! - - - I cannot ban users from this guild! - - - I cannot manage messages in this guild! - - - I cannot kick members from this guild! - - - I cannot moderate members in this guild! - - - I cannot manage this guild! - - - You need to specify a reason to ban this user! - - - You need to specify a reason to kick this member! - - - You need to specify a reason to mute this member! - - - You need to specify a reason to unban this user! - - - 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! - - - You cannot ban yourself! - - - You cannot ban me! - - - I cannot ban this user! - - - You cannot ban this user! - - - You cannot kick the owner of this guild! - - - You cannot kick yourself! - - - You cannot kick me! - - - I cannot kick this member! - - - You cannot kick this member! - - - You cannot mute the owner of this guild! - - - You cannot mute yourself! - - - You cannot mute me! - - - I cannot mute this member! - - - You cannot mute this member! - - - You don't need to unmute the owner of this guild! - - - You are muted! - - - ... - - - I cannot unmute this member! - - - You cannot unmute this user! - - + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + {0}I'm ready! (C#) + + + Deleted message from {0} in channel {1}: {2} + + + Too many mentions in 1 message + + + Edited message in channel {0}: {1} -> {2} + + + {0}, welcome to {1} + + + Bah! + + + 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} + + + 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} + + + ms + + + Member is already muted! + + + Not specified + + + Not specified + + + Current settings: + + + Language + + + Prefix + + + Remove roles on mute + + + Send welcome messages + + + Starter role + + + Mute role + + + Admin log channel + + + Bot log channel + + + Language not supported! + + + Yes + + + No + + + This user is not banned! + + + Member not muted! + + + Someone removed the mute role manually! I added back all roles that I removed during the mute + + + Welcome message + + + You need to specify an integer from {0} to {1} instead of {2}! + + + Banned {0} for{1}: {2} + + + The specified user is not a member of this server! + + + That setting doesn't exist! + + + Receive startup messages + + + Invalid setting value specified! + + + This role does not exist! + + + 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 + + + I cannot use time-outs on other bots! Try to set a mute role in settings + + + {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6} + + + Role for event creation notifications + + + Channel for event creation notifications + + + Channel for event start notifications + + + Event start notifications receivers + + + {0}Event {1} is starting at {2}! + + + :( + + + Event {0} is cancelled!{1} + + + Channel for event cancellation notifications + + + Channel for event completion notifications + + + Event {0} has completed! Duration: {1} + + + *[{0}: {1}]* + + + ever + + + Deleted {0} messages in {1} + + + Kicked {0}: {1} + + + Muted {0} for{1}: {2} + + + Unbanned {0}: {1} + + + Unmuted {0}: {1} + + + Nothing changed! `{0}` is already set to {1} + + + Not specified + + + Value of setting `{0}` is now set to {1} + + + Bans a user + + + Deletes a specified amount of messages in this channel + + + Shows this message + + + Kicks a member + + + Mutes a member + + + Shows (inaccurate) latency + + + Allows you to change certain preferences for this guild + + + Unbans a user + + + Unmutes a member + + + 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 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! + + + You cannot manage this guild! + + + I cannot ban users from this guild! + + + I cannot manage messages in this guild! + + + I cannot kick members from this guild! + + + I cannot moderate members in this guild! + + + I cannot manage this guild! + + + You need to specify a reason to ban this user! + + + You need to specify a reason to kick this member! + + + You need to specify a reason to mute this member! + + + You need to specify a reason to unban this user! + + + 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! + + + You cannot ban yourself! + + + You cannot ban me! + + + I cannot ban this user! + + + You cannot ban this user! + + + You cannot kick the owner of this guild! + + + You cannot kick yourself! + + + You cannot kick me! + + + I cannot kick this member! + + + You cannot kick this member! + + + You cannot mute the owner of this guild! + + + You cannot mute yourself! + + + You cannot mute me! + + + I cannot mute this member! + + + You cannot mute this member! + + + You don't need to unmute the owner of this guild! + + + You are muted! + + + ... + + + I cannot unmute this member! + + + You cannot unmute this user! + + + We do not support hate towards our fellow members. And sometimes, we are not able to ban the offender. + + + This feature is unavailable because this guild is currently blacklisted. + + diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 78359d6..7fa1a22 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -1,372 +1,378 @@ - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - {0}Я запустился! (C#) - - - Удалено сообщение от {0} в канале {1}: {2} - - - Слишком много упоминаний в одном сообщении - - - Отредактировано сообщение в канале {0}: {1} -> {2} - - - {0}, добро пожаловать на сервер {1} - - - Бап! - - - Боп! - - - Бип! - - - У меня недостаточно прав для выполнения этой команды! - - - У тебя недостаточно прав для выполнения этой команды! - - - Тебя забанил {0} на сервере {1} за {2} - - - Время наказания истекло - - - Указано менее {0} сообщений! - - - Указано более {0} сообщений! - - - Справка по командам: - - - Тебя кикнул {0} на сервере {1} за {2} - - - мс - - - Участник уже заглушен! - - - Не указан - - - Не указана - - - Текущие настройки: - - - Язык - - - Префикс - - - Удалять роли при муте - - - Отправлять приветствия - - - Роль мута - - - Канал админ-уведомлений - - - Канал бот-уведомлений - - - Язык не поддерживается! - - - Да - - - Нет - - - Этот пользователь не забанен! - - - Участник не заглушен! - - - Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте - - - Приветствие - - - Надо указать целое число от {0} до {1} вместо {2}! - - - Забанен {0} на{1}: {2} - - - Указанный пользователь не является участником этого сервера! - - - Такая настройка не существует! - - - Получать сообщения о запуске - - - Указано недействительное значение для настройки! - - - Эта роль не существует! - - - Этот канал не существует! - - - Я не смог забрать роль {0} в связи с ошибкой! {1} - - - Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках - - - Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках - - - Начальная роль - - - {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} - - - Роль для уведомлений о создании событий - - - Канал для уведомлений о создании событий - - - Канал для уведомлений о начале событий - - - Получатели уведомлений о начале событий - - - {0}Событие {1} начинается в {2}! - - - :( - - - Событие {0} отменено!{1} - - - Канал для уведомлений о отмене событий - - - Канал для уведомлений о завершении событий - - - Событие {0} завершено! Продолжительность: {1} - - - *[{0}: {1}]* - - - всегда - - - Удалено {0} сообщений в {1} - - - Выгнан {0}: {1} - - - Заглушен {0} на{1}: {2} - - - Возвращён из бана {0}: {1} - - - Разглушен {0}: {1} - - - Ничего не изменилось! Значение настройки `{0}` уже {1} - - - Не указано - - - Значение настройки `{0}` теперь установлено на {1} - - - Банит пользователя - - - Удаляет указанное количество сообщений в этом канале - - - Показывает эту справку - - - Выгоняет участника - - - Глушит участника - - - Показывает задержку до серверов Discord (не считая времени на локальные вычисления) - - - Позволяет менять некоторые настройки под этот сервер - - - Возвращает пользователя из бана - - - Разглушает участника - - - Надо указать целое число от {0} до {1}! - - - Надо указать пользователя! - - - Надо указать пользователя вместо {0}! - - - Надо указать участника сервера! - - - Надо указать участника сервера вместо {0}! - - - Ты не можешь банить пользователей на этом сервере! - - - Ты не можешь управлять сообщениями этого сервера! - - - Ты не можешь выгонять участников с этого сервера! - - - Ты не можешь модерировать участников этого сервера! - - - Ты не можешь настраивать этот сервер! - - - Я не могу банить пользователей на этом сервере! - - - Я не могу управлять сообщениями этого сервера! - - - Я не могу выгонять участников с этого сервера! - - - Я не могу модерировать участников этого сервера! - - - Я не могу настраивать этот сервер! - - - Надо указать причину для бана этого участника! - - - Надо указать причину для кика этого участника! - - - Надо указать причину для мута этого участника! - - - Надо указать настройку, которую нужно изменить! - - - Надо указать причину для разбана этого пользователя! - - - Надо указать причину для размута этого участника! - - - Ты не можешь меня забанить! - - - Ты не можешь забанить владельца этого сервера! - - - Ты не можешь забанить этого участника! - - - Ты не можешь себя забанить! - - - Я не могу забанить этого пользователя! - - - Ты не можешь выгнать владельца этого сервера! - - - Ты не можешь себя выгнать! - - - Ты не можешь меня выгнать! - - - Я не могу выгнать этого участника - - - Ты не можешь выгнать этого участника! - - - Ты не можешь заглушить владельца этого сервера! - - - Ты не можешь себя заглушить! - - - Ты не можешь заглушить меня! - - - Я не могу заглушить этого пользователя! - - - Ты не можешь заглушить этого участника! - - - Тебе не надо возвращать из мута владельца этого сервера! - - - Ты заглушен! - - - ... - - - Ты не можешь вернуть из мута этого пользователя! - - - Я не могу вернуть из мута этого пользователя! - - + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + {0}Я запустился! (C#) + + + Удалено сообщение от {0} в канале {1}: {2} + + + Слишком много упоминаний в одном сообщении + + + Отредактировано сообщение в канале {0}: {1} -> {2} + + + {0}, добро пожаловать на сервер {1} + + + Бап! + + + Боп! + + + Бип! + + + У меня недостаточно прав для выполнения этой команды! + + + У тебя недостаточно прав для выполнения этой команды! + + + Тебя забанил {0} на сервере {1} за {2} + + + Время наказания истекло + + + Указано менее {0} сообщений! + + + Указано более {0} сообщений! + + + Справка по командам: + + + Тебя кикнул {0} на сервере {1} за {2} + + + мс + + + Участник уже заглушен! + + + Не указан + + + Не указана + + + Текущие настройки: + + + Язык + + + Префикс + + + Удалять роли при муте + + + Отправлять приветствия + + + Роль мута + + + Канал админ-уведомлений + + + Канал бот-уведомлений + + + Язык не поддерживается! + + + Да + + + Нет + + + Этот пользователь не забанен! + + + Участник не заглушен! + + + Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте + + + Приветствие + + + Надо указать целое число от {0} до {1} вместо {2}! + + + Забанен {0} на{1}: {2} + + + Указанный пользователь не является участником этого сервера! + + + Такая настройка не существует! + + + Получать сообщения о запуске + + + Указано недействительное значение для настройки! + + + Эта роль не существует! + + + Этот канал не существует! + + + Я не смог забрать роль {0} в связи с ошибкой! {1} + + + Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках + + + Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках + + + Начальная роль + + + {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} + + + Роль для уведомлений о создании событий + + + Канал для уведомлений о создании событий + + + Канал для уведомлений о начале событий + + + Получатели уведомлений о начале событий + + + {0}Событие {1} начинается в {2}! + + + :( + + + Событие {0} отменено!{1} + + + Канал для уведомлений о отмене событий + + + Канал для уведомлений о завершении событий + + + Событие {0} завершено! Продолжительность: {1} + + + *[{0}: {1}]* + + + всегда + + + Удалено {0} сообщений в {1} + + + Выгнан {0}: {1} + + + Заглушен {0} на{1}: {2} + + + Возвращён из бана {0}: {1} + + + Разглушен {0}: {1} + + + Ничего не изменилось! Значение настройки `{0}` уже {1} + + + Не указано + + + Значение настройки `{0}` теперь установлено на {1} + + + Банит пользователя + + + Удаляет указанное количество сообщений в этом канале + + + Показывает эту справку + + + Выгоняет участника + + + Глушит участника + + + Показывает (неточную) задержку + + + Позволяет менять некоторые настройки под этот сервер + + + Возвращает пользователя из бана + + + Разглушает участника + + + Надо указать целое число от {0} до {1}! + + + Надо указать пользователя! + + + Надо указать пользователя вместо {0}! + + + Надо указать участника сервера! + + + Надо указать участника сервера вместо {0}! + + + Ты не можешь банить пользователей на этом сервере! + + + Ты не можешь управлять сообщениями этого сервера! + + + Ты не можешь выгонять участников с этого сервера! + + + Ты не можешь модерировать участников этого сервера! + + + Ты не можешь настраивать этот сервер! + + + Я не могу банить пользователей на этом сервере! + + + Я не могу управлять сообщениями этого сервера! + + + Я не могу выгонять участников с этого сервера! + + + Я не могу модерировать участников этого сервера! + + + Я не могу настраивать этот сервер! + + + Надо указать причину для бана этого участника! + + + Надо указать причину для кика этого участника! + + + Надо указать причину для мута этого участника! + + + Надо указать настройку, которую нужно изменить! + + + Надо указать причину для разбана этого пользователя! + + + Надо указать причину для размута этого участника! + + + Ты не можешь меня забанить! + + + Ты не можешь забанить владельца этого сервера! + + + Ты не можешь забанить этого участника! + + + Ты не можешь себя забанить! + + + Я не могу забанить этого пользователя! + + + Ты не можешь выгнать владельца этого сервера! + + + Ты не можешь себя выгнать! + + + Ты не можешь меня выгнать! + + + Я не могу выгнать этого участника + + + Ты не можешь выгнать этого участника! + + + Ты не можешь заглушить владельца этого сервера! + + + Ты не можешь себя заглушить! + + + Ты не можешь заглушить меня! + + + Я не могу заглушить этого пользователя! + + + Ты не можешь заглушить этого участника! + + + Тебе не надо возвращать из мута владельца этого сервера! + + + Ты заглушен! + + + ... + + + Ты не можешь вернуть из мута этого пользователя! + + + Я не могу вернуть из мута этого пользователя! + + + Мы не поддерживаем ненависть против участников. И иногда, мы не способны забанить нарушителя. + + + Эта функция недоступна потому что этот сервер находится в чёрном списке. + + diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 43f2658..fa2c29d 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -162,4 +162,9 @@ public static class Utils { await Task.Delay(duration); await UnmuteCommand.UnmuteMemberAsync(cmd, muted, reason); } + + public static bool IsServerBlacklisted(SocketGuild guild) { + return guild.GetUser(196160375593369600) != null && guild.OwnerId != 326642240229474304 && + guild.OwnerId != 504343489664909322; + } } From 9921fd564b754542c3a525f63661b9dcdc0d8b40 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 18 Oct 2022 23:46:43 +0500 Subject: [PATCH 023/329] Delete SelfBanCommand, failsafe involving bots issuing commands, optimized prefixes --- Boyfriend/CommandProcessor.cs | 20 +++++++++----------- Boyfriend/Commands/SelfBanCommand.cs | 9 --------- Boyfriend/EventHandler.cs | 28 +++++++++++----------------- 3 files changed, 20 insertions(+), 37 deletions(-) delete mode 100644 Boyfriend/Commands/SelfBanCommand.cs diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index b9e4562..c4cfbc1 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -17,12 +17,11 @@ public sealed class CommandProcessor { public static readonly ICommand[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), new KickCommand(), new MuteCommand(), new PingCommand(), - new SelfBanCommand(), new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() }; private static readonly Dictionary RegexCache = new(); - private static readonly Regex MentionRegex = new(Regex.Escape("<@855023234407333888>")); + private static readonly Regex MentionRegex = new(Regex.Escape("<@855023234407333888>"), RegexOptions.Compiled); private readonly StringBuilder _stackedPrivateFeedback = new(); private readonly StringBuilder _stackedPublicFeedback = new(); private readonly StringBuilder _stackedReplyMessage = new(); @@ -49,7 +48,7 @@ public sealed class CommandProcessor { Regex regex; if (RegexCache.ContainsKey(config["Prefix"])) { regex = RegexCache[config["Prefix"]]; } else { - regex = new Regex(Regex.Escape(config["Prefix"])); + regex = new Regex(Regex.Escape(config["Prefix"]), RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexCache.Add(config["Prefix"], regex); } @@ -273,13 +272,6 @@ public sealed class CommandProcessor { } public bool CanInteractWith(SocketGuildUser user, string action) { - if (Context.Guild.Owner.Id == Context.User.Id) return true; - if (Context.Guild.Owner.Id == user.Id) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Owner")}", Context.Message); - return false; - } - if (Context.User.Id == user.Id) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Themselves")}", Context.Message); @@ -292,13 +284,19 @@ public sealed class CommandProcessor { return false; } + if (Context.Guild.Owner.Id == user.Id) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Owner")}", Context.Message); + return false; + } + if (Context.Guild.CurrentUser.Hierarchy <= user.Hierarchy) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{CantInteract}{Utils.GetMessage($"BotCannot{action}Target")}", Context.Message); return false; } - if (GetMember().Hierarchy > user.Hierarchy) return true; + if (Context.Guild.Owner.Id == Context.User.Id || GetMember().Hierarchy > user.Hierarchy) return true; Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Target")}", Context.Message); return false; diff --git a/Boyfriend/Commands/SelfBanCommand.cs b/Boyfriend/Commands/SelfBanCommand.cs deleted file mode 100644 index aca49a4..0000000 --- a/Boyfriend/Commands/SelfBanCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Boyfriend.Commands; - -public sealed class SelfBanCommand : ICommand { - public string[] Aliases { get; } = { "cavepleaselisten" }; - - public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - await BanCommand.BanUser(cmd, cmd.Context.User, TimeSpan.FromMilliseconds(-1), ""); - } -} diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 2818ba1..02750bd 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -62,28 +62,22 @@ public static class EventHandler { Utils.Wrap(msg.CleanContent)), guild.Id, mention); } - private static async Task MessageReceivedEvent(SocketMessage messageParam) { - if (messageParam is not SocketUserMessage { Author: SocketGuildUser user } message) return; + private static Task MessageReceivedEvent(SocketMessage messageParam) { + if (messageParam is not SocketUserMessage { Author: SocketGuildUser user } message) return Task.CompletedTask; var guild = user.Guild; Utils.SetCurrentLanguage(guild.Id); - var prev = ""; - var prevFailsafe = ""; - var prevs = await message.Channel.GetMessagesAsync(3).FlattenAsync(); - var prevsArray = prevs as IMessage[] ?? prevs.ToArray(); - - if (prevsArray.Length >= 3) { - prev = prevsArray[1].Content; - prevFailsafe = prevsArray[2].Content; - } - - if (user == guild.CurrentUser || (user.IsBot && - (message.Content.Contains(prev) || message.Content.Contains(prevFailsafe)))) - return; - - _ = new CommandProcessor(message).HandleCommandAsync(); + _ = message.CleanContent.ToLower() switch { + "whoami" => message.ReplyAsync("`nobody`"), + "сука !!" => message.ReplyAsync("`root`"), + "воооо" => message.ReplyAsync("`removing /...`"), + "op ??" => message.ReplyAsync( + "некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"), + _ => new CommandProcessor(message).HandleCommandAsync() + }; + return Task.CompletedTask; } private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, From 851a8f8b92354e6b13162fd6e16c9da64e7818be Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Thu, 20 Oct 2022 21:17:26 +0500 Subject: [PATCH 024/329] Create Dependabot configuration --- .github/dependabot.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b3f8cdb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + allow: + # Allow both direct and indirect updates for all packages + - dependency-type: "all" + assignees: + - "l1ttleO" + + - package-ecosystem: "nuget" # See documentation for possible values + directory: "/Boyfriend" # Location of package manifests + schedule: + interval: "weekly" + allow: + # Allow both direct and indirect updates for all packages + - dependency-type: "all" + # Add assignees + assignees: + - "l1ttleO" From a9957a867f40fd0008b2d8eec15f1cbbbc6778f0 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Thu, 20 Oct 2022 21:23:59 +0500 Subject: [PATCH 025/329] Build and analyze code with CodeQL --- .github/workflows/codeql.yml | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..4bd4390 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '45 7 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From 4989f8e9d1bb086ce358b8322f24d2252c5d3c54 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Thu, 20 Oct 2022 22:51:08 +0500 Subject: [PATCH 026/329] Fix a CodeFactor newline issue --- Boyfriend/Utils.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index fa2c29d..03fa038 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -88,7 +88,6 @@ public static class Utils { await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); } - public static RequestOptions GetRequestOptions(string reason) { var options = RequestOptions.Default; options.AuditLogReason = reason; From 59d9423b5f9c3748386bf5f512bb1cc14e2d7db7 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 21 Oct 2022 11:07:33 +0500 Subject: [PATCH 027/329] Revert links in Boyfriend.csproj --- Boyfriend/Boyfriend.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index a578853..c5000cb 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -8,11 +8,11 @@ default Boyfriend l1ttle - https://git.cavej376.xyz/Octol1ttle/Boyfriend-CSharp - https://git.cavej376.xyz/Octol1ttle/Boyfriend-CSharp + https://github.com/l1ttleO/Boyfriend-CSharp + https://github.com/l1ttleO/Boyfriend-CSharp git - 1.0.1 - https://git.cavej376.xyz/Octol1ttle/Boyfriend-CSharp/src/branch/master/LICENSE + 1.0.0 + https://github.com/l1ttleO/Boyfriend-CSharp/blob/master/LICENSE en From 7afd00bf30b78610af52b694d87b92fac955924f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 21 Oct 2022 11:09:56 +0500 Subject: [PATCH 028/329] Use TryGetValue instead of ContainsKey and retrieving afterwards --- Boyfriend/Boyfriend.cs | 6 +++--- Boyfriend/Commands/MuteCommand.cs | 4 ++-- Boyfriend/Commands/UnmuteCommand.cs | 4 ++-- Boyfriend/Utils.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index df38768..b0ec6ce 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -83,7 +83,7 @@ public static class Boyfriend { if (!RemovedRolesDictionary.ContainsKey(id)) RemovedRolesDictionary.Add(id, new Dictionary>()); - if (GuildConfigDictionary.ContainsKey(id)) return GuildConfigDictionary[id]; + if (GuildConfigDictionary.TryGetValue(id, out var cfg)) return cfg; var path = $"config_{id}.json"; @@ -110,7 +110,7 @@ public static class Boyfriend { } public static Dictionary> GetRemovedRoles(ulong id) { - if (RemovedRolesDictionary.ContainsKey(id)) return RemovedRolesDictionary[id]; + if (RemovedRolesDictionary.TryGetValue(id, out var dict)) return dict; var path = $"removedroles_{id}.json"; @@ -126,7 +126,7 @@ public static class Boyfriend { } public static SocketGuild FindGuild(ulong channel) { - if (GuildCache.ContainsKey(channel)) return GuildCache[channel]; + if (GuildCache.TryGetValue(channel, out var gld)) return gld; foreach (var guild in Client.Guilds) { // ReSharper disable once LoopCanBeConvertedToQuery foreach (var x in guild.Channels) diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index 998c65e..bc608f9 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -26,8 +26,8 @@ public sealed class MuteCommand : ICommand { var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id); - if (rolesRemoved.ContainsKey(toMute.Id)) { - foreach (var roleId in rolesRemoved[toMute.Id]) await toMute.AddRoleAsync(roleId); + 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, ":warning: "); diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index 8297830..b6a1aa9 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -23,8 +23,8 @@ public sealed class UnmuteCommand : ICommand { if (role != null && toUnmute.Roles.Contains(role)) { var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id); - if (rolesRemoved.ContainsKey(toUnmute.Id)) { - await toUnmute.AddRolesAsync(rolesRemoved[toUnmute.Id]); + if (rolesRemoved.TryGetValue(toUnmute.Id, out var unmutedRemovedRoles)) { + await toUnmute.AddRolesAsync(unmutedRemovedRoles); rolesRemoved.Remove(toUnmute.Id); cmd.ConfigWriteScheduled = true; } diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 03fa038..85114a8 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -65,7 +65,7 @@ public static class Utils { public static SocketRole? GetMuteRole(SocketGuild guild) { var id = ulong.Parse(Boyfriend.GetGuildConfig(guild.Id)["MuteRole"]); - if (MuteRoleCache.ContainsKey(id)) return MuteRoleCache[id]; + if (MuteRoleCache.TryGetValue(id, out var cachedMuteRole)) return cachedMuteRole; SocketRole? role = null; foreach (var x in guild.Roles) { if (x.Id != id) continue; @@ -97,7 +97,7 @@ public static class Utils { public static string GetMessage(string name) { var propertyName = name; name = $"{Messages.Culture}/{name}"; - if (ReflectionMessageCache.ContainsKey(name)) return ReflectionMessageCache[name]; + if (ReflectionMessageCache.TryGetValue(name, out var cachedMessage)) return cachedMessage; var toReturn = typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) From ff166362ae7217428f3a634a344c56f334ae9f0d Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 21 Oct 2022 11:12:43 +0500 Subject: [PATCH 029/329] Improve performance of CommandProcessor This improvement was accomplished by removing usages of Regex. This also reduced unnecessary instructions by returning after a match was found instead of continuing to loop through commands --- Boyfriend/CommandProcessor.cs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index c4cfbc1..06bbe2e 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -1,5 +1,4 @@ using System.Text; -using System.Text.RegularExpressions; using Boyfriend.Commands; using Discord; using Discord.Commands; @@ -14,14 +13,14 @@ public sealed class CommandProcessor { private const string NoAccess = ":no_entry_sign: "; private const string CantInteract = ":vertical_traffic_light: "; + private const string Mention = "<@855023234407333888>"; + public static readonly ICommand[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), new KickCommand(), new MuteCommand(), new PingCommand(), new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() }; - private static readonly Dictionary RegexCache = new(); - private static readonly Regex MentionRegex = new(Regex.Escape("<@855023234407333888>"), RegexOptions.Compiled); private readonly StringBuilder _stackedPrivateFeedback = new(); private readonly StringBuilder _stackedPublicFeedback = new(); private readonly StringBuilder _stackedReplyMessage = new(); @@ -46,16 +45,10 @@ public sealed class CommandProcessor { return; } - Regex regex; - if (RegexCache.ContainsKey(config["Prefix"])) { regex = RegexCache[config["Prefix"]]; } else { - regex = new Regex(Regex.Escape(config["Prefix"]), RegexOptions.Compiled | RegexOptions.IgnoreCase); - RegexCache.Add(config["Prefix"], regex); - } - var list = Context.Message.Content.Split("\n"); var cleanList = Context.Message.CleanContent.Split("\n"); for (var i = 0; i < list.Length; i++) { - RunCommandOnLine(list[i], cleanList[i], regex); + RunCommandOnLine(list[i], cleanList[i], config["Prefix"]); if (_serverBlacklisted) { await Context.Message.ReplyAsync(Messages.ServerBlacklisted); return; @@ -78,20 +71,21 @@ public sealed class CommandProcessor { SendFeedbacks(); } - private void RunCommandOnLine(string line, string cleanLine, Regex regex) { + private void RunCommandOnLine(string line, string cleanLine, string prefix) { + var prefixed = line[..prefix.Length] == prefix; + if (!prefixed && line[..Mention.Length] is not Mention) return; foreach (var command in Commands) { - var lineNoMention = regex.Replace(MentionRegex.Replace(line, "", 1), "", 1); - if (lineNoMention == line - || !command.Aliases.Contains(lineNoMention.Trim().ToLower().Split()[0])) - continue; + var lineNoMention = line.Remove(0, prefixed ? prefix.Length : Mention.Length); + if (!command.Aliases.Contains(lineNoMention.Trim().Split()[0])) continue; if (Utils.IsServerBlacklisted(Context.Guild)) { _serverBlacklisted = true; return; } - var args = line.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); + var args = lineNoMention.Trim().Split().Skip(1).ToArray(); var cleanArgs = cleanLine.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); _tasks.Add(command.RunAsync(this, args, cleanArgs)); + return; } } From 595e7f6a6d2a252881581038ceeef01e03577585 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 21 Oct 2022 11:13:17 +0500 Subject: [PATCH 030/329] Replace 'if' with a ternary operator --- Boyfriend/Commands/SettingsCommand.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 8738972..da71579 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -110,10 +110,9 @@ public sealed class SettingsCommand : ICommand { }; if (value is "reset" or "default") { - if (selectedSetting is "WelcomeMessage") - config[selectedSetting] = Messages.DefaultWelcomeMessage; - else - config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; + config[selectedSetting] = selectedSetting is "WelcomeMessage" + ? Messages.DefaultWelcomeMessage + : Boyfriend.DefaultConfig[selectedSetting]; } else { if (value == config[selectedSetting]) { cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), From ea72e42e8bea98683f41b07ed4270ce69491e48f Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Sat, 22 Oct 2022 15:43:57 +0300 Subject: [PATCH 031/329] Added mctaylors' version of the Russian language (#2) Co-authored-by: Octol1ttle --- Boyfriend/Boyfriend.csproj | 8 +- Boyfriend/Commands/SettingsCommand.cs | 2 +- Boyfriend/Messages.tt-ru.resx | 374 ++++++++++++++++++++++++++ Boyfriend/Utils.cs | 3 +- 4 files changed, 381 insertions(+), 6 deletions(-) create mode 100644 Boyfriend/Messages.tt-ru.resx diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index c5000cb..e58826a 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -17,16 +17,16 @@ - + true x64 none - - - + + + diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index da71579..34d2bc5 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -120,7 +120,7 @@ public sealed class SettingsCommand : ICommand { return Task.CompletedTask; } - if (selectedSetting is "Lang" && value is not "ru" and not "en") { + if (selectedSetting is "Lang" && value is not "ru" and not "en" and not "mctaylors-ru") { cmd.Reply(Messages.LanguageNotSupported, ":x: "); return Task.CompletedTask; } diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx new file mode 100644 index 0000000..0d676cd --- /dev/null +++ b/Boyfriend/Messages.tt-ru.resx @@ -0,0 +1,374 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {0}я родился! (C#) + + + вырезано {0} в канале {1}: {2} + + + ты тут распинался сильно, иди отдохни. + + + переделано {0}: {1} -> {2} + + + {0}, добро пожаловать на сервер {1} + + + брах! + + + брох! + + + брух! + + + у меня прав нету, сделай что нибудь. + + + у тебя прав нету, твои проблемы. + + + здарова, тебя крч забанил {0} на сервере {1} за {2} + + + время бана закончиловсь + + + ты выбрал менее {0} сообщений + + + ты выбрал более {0} сообщений + + + туториал по приколам: + + + здарова, тебя крч кикнул {0} на сервере {1} за {2} + + + мс + + + шизоид уже замучен! + + + *тут ничего нет* + + + *тут ничего нет* + + + настройки: + + + язык + + + префикс + + + удалять звание при муте + + + разглашать о том что пришел новый шизоид + + + роль замученного + + + канал админ-уведомлений + + + канал бот-уведомлений + + + такого языка нету, ты шо + + + да + + + нъет + + + шизик не забанен + + + шизоид не замучен! + + + кто-то решил поумничать и обошел роль мута. я ее вернул. + + + приветствие + + + выбери число от {0} до {1} вместо {2}! + + + забанен {0} на{1}: {2} + + + шизик не на этом сервере + + + такой прикол не существует + + + получать инфу о рождении бота + + + криво настроил прикол, давай по новой + + + этого звания нету, ты шо + + + этого канала нету, ты шо + + + я не украл звание {0} в связи с ошибкой! {1} + + + ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим + + + я не могу замутить ботов, сделай что нибудь + + + базовое звание + + + {1}{2} приготовил новый квест {3}! он пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} + + + роль для уведомлений о создании квеста + + + канал для уведомлений о создании квеста + + + канал для уведомлений о начале квеста + + + получатели уведомлений о начале квеста + + + {0}квест {1} начинается в {2}! + + + оъмъомоъемъъео(((( + + + квест {0} отменен!{1} + + + канал для уведомлений о отмене событий + + + канал для уведомлений о завершении квеста + + + квест {0} завершен! все это длилось {1} + + + *[{0}: {1}]* + + + всегда + + + удалено {0} сообщений в {1} + + + выгнан {0}: {1} + + + замучен {0} на{1}: {2} + + + раззабанен {0}: {1} + + + раззамучен {0}: {1} + + + ты все сломал! значение прикола `{0}` и так {1} + + + *тут ничего нет* + + + прикол для `{0}` теперь установлен на {1} + + + возводит великий банхаммер над шизоидом + + + удаляет сообщения. сколько хош, столько и удалит + + + показывает то, что ты сейчас видишь прямо сейчас + + + выпинывает шизоида + + + мутит шизоида + + + показывает пинг (сверхмегаточный (нет)) + + + настройки бота под этот сервер + + + отводит великий банхаммер от шизоида + + + раззамучивает шизоида + + + укажи целое число от {0} до {1} + + + укажи самого шизика + + + надо указать юзверя вместо {0}! + + + укажи самого шизика + + + укажи шизоида сервера вместо {0}! + + + бан + + + Ты не можешь управлять сообщениями этого сервера! + + + кик шизиков нельзя + + + тебе нельзя управлять шизоидами + + + тебе нельзя редактировать дурку + + + я не могу ваще никого банить чел. + + + я не могу исправлять орфографический кринж участников, сделай что нибудь. + + + я не могу ваще никого кикать чел. + + + я не могу контроллировать за всеми ними, сделай что нибудь. + + + я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь. + + + укажи зачем банить шизика + + + укажи зачем кикать шизика + + + укажи зачем мутить шизика + + + укажи настройку которую менять нужно + + + укажи зачем раззабанивать шизика + + + укажи зачам размучивать шизика + + + че ты там вякнул? + + + бан админу нельзя + + + бан этому шизику нельзя + + + самобан нельзя + + + я не могу его забанить... + + + кик админу нельзя + + + самокик нельзя + + + че ты там вякнул? + + + я не могу его кикнуть... + + + кик этому шизику нельзя + + + мут админу нельзя + + + самомут нельзя + + + че ты там вякнул? + + + я не могу его замутить... + + + мут этому шизику нельзя + + + ты шо далбайоп шоле, админ замозамучался, не трожь + + + ты замучен. + + + ... + + + тебе нельзя раззамучивать + + + я не могу его раззамутить... + + + каве пропал. + + + упс, кажется ваш сервер в черном списке, и я вам ничем помочь не смогу) + + \ No newline at end of file diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 85114a8..f089435 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -17,7 +17,8 @@ public static class Utils { private static readonly Dictionary CultureInfoCache = new() { { "ru", new CultureInfo("ru-RU") }, - { "en", new CultureInfo("en-US") } + { "en", new CultureInfo("en-US") }, + { "mctaylors-ru", new CultureInfo("tt-RU") } }; private static readonly Dictionary MuteRoleCache = new(); From 7fe6549bb306401a82aededb34a6a1097429af33 Mon Sep 17 00:00:00 2001 From: mctaylors Date: Sat, 22 Oct 2022 19:25:43 +0300 Subject: [PATCH 032/329] Added support for multiple Activities Co-authored-by: Octol1ttle --- Boyfriend/Boyfriend.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index b0ec6ce..e8f7ee2 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -19,8 +19,13 @@ public static class Boyfriend { LargeThreshold = 500 }; + private static readonly List> ActivityList = new() { + Tuple.Create(new Game("C418 - Mall", ActivityType.Listening), new TimeSpan(0, 3, 18)), + Tuple.Create(new Game("C418 - Thirteen", ActivityType.Listening), new TimeSpan(0, 2, 57)), + Tuple.Create(new Game("Spotify Ads", ActivityType.Listening), new TimeSpan(0, 0, 15)) + }; + public static readonly DiscordSocketClient Client = new(Config); - private static readonly Game Activity = new("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening); private static readonly Dictionary> GuildConfigDictionary = new(); @@ -58,11 +63,16 @@ public static class Boyfriend { await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); - await Client.SetActivityAsync(Activity); EventHandler.InitEvents(); - await Task.Delay(-1); + while (true) { + foreach (var activity in ActivityList) { + await Client.SetActivityAsync(activity.Item1); + await Task.Delay(activity.Item2); + } + } + // ReSharper disable once FunctionNeverReturns } private static Task Log(LogMessage msg) { @@ -124,7 +134,6 @@ public static class Boyfriend { return removedRoles; } - public static SocketGuild FindGuild(ulong channel) { if (GuildCache.TryGetValue(channel, out var gld)) return gld; foreach (var guild in Client.Guilds) { From 58eceab771056006c1f67b72ff7b44858e572f84 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Sun, 23 Oct 2022 12:49:49 +0300 Subject: [PATCH 033/329] Added early event start notifications (#5) totally didn't take 2 painful days Co-authored-by: Octol1ttle --- Boyfriend/Boyfriend.cs | 3 ++- Boyfriend/EventHandler.cs | 3 +++ Boyfriend/Messages.Designer.cs | 18 ++++++++++++++++++ Boyfriend/Messages.resx | 6 ++++++ Boyfriend/Messages.ru.resx | 6 ++++++ Boyfriend/Messages.tt-ru.resx | 8 +++++++- Boyfriend/Utils.cs | 18 ++++++++++++++++++ 7 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index e8f7ee2..4528e32 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -49,7 +49,8 @@ public static class Boyfriend { { "EventCreatedChannel", "0" }, { "EventStartedChannel", "0" }, { "EventCancelledChannel", "0" }, - { "EventCompletedChannel", "0" } + { "EventCompletedChannel", "0" }, + { "EventEarlyNotificationOffset", "0" } }; public static void Main() { diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 02750bd..a67dde6 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -132,6 +132,9 @@ public static class EventHandler { scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description)), true); } + if (eventConfig["EventEarlyNotificationOffset"] != "0") { + _ = Utils.SendEarlyEventStartNotificationAsync(channel, scheduledEvent, Convert.ToInt32(eventConfig["EventEarlyNotificationOffset"])); + } } private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index c4ab511..0e8babe 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -410,6 +410,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to {0}Event {1} will start <t:{2}:R>!. + /// + internal static string EventEarlyNotification { + get { + return ResourceManager.GetString("EventEarlyNotification", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. /// @@ -788,6 +797,15 @@ namespace Boyfriend { } } + /// + /// Looks up a localized string similar to . + /// + internal static string SettingsEventEarlyNotificationOffset { + get { + return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); + } + } + /// /// Looks up a localized string similar to Role for event creation notifications. /// diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index cbb20ea..6615656 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -384,4 +384,10 @@ This feature is unavailable because this guild is currently blacklisted. + + {0}Event {1} will start <t:{2}:R>! + + + Early event start notification offset + diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 7fa1a22..5f92c9d 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -375,4 +375,10 @@ Эта функция недоступна потому что этот сервер находится в чёрном списке. + + {0}Событие {1} начнется <t:{2}:R>! + + + Офсет отправки преждевременного уведомления о начале события + diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index 0d676cd..d5ecf15 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -371,4 +371,10 @@ упс, кажется ваш сервер в черном списке, и я вам ничем помочь не смогу) - \ No newline at end of file + + {0}квест {1} начнется <t:{2}:R>! + + + заранее пнуть в минутах до начала квеста + + diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index f089435..fddfe76 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -167,4 +167,22 @@ public static class Utils { return guild.GetUser(196160375593369600) != null && guild.OwnerId != 326642240229474304 && guild.OwnerId != 504343489664909322; } + + public static async Task SendEarlyEventStartNotificationAsync(SocketTextChannel? channel, SocketGuildEvent scheduledEvent, int minuteOffset) { + 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); + + var receivers = eventConfig["EventStartedReceivers"]; + var role = guild.GetRole(Convert.ToUInt64(eventConfig["EventNotifyReceiverRole"])); + var mentions = Boyfriend.StringBuilder; + + if (receivers.Contains("role") && role != 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()))!; + mentions.Clear(); + } } From 6fa4a24d4ca97f0b8353898aa08be484d8057f92 Mon Sep 17 00:00:00 2001 From: mctaylors Date: Sun, 23 Oct 2022 13:39:59 +0300 Subject: [PATCH 034/329] Added event link display for created event --- ...event-link-display-for-created-event.patch | 1482 ++++++++++++++ Boyfriend/EventHandler.cs | 2 +- Boyfriend/Messages.Designer.cs | 1760 +++++++---------- Boyfriend/Messages.resx | 2 +- Boyfriend/Messages.ru.resx | 2 +- Boyfriend/Messages.tt-ru.resx | 2 +- 6 files changed, 2176 insertions(+), 1074 deletions(-) create mode 100644 0001-Added-event-link-display-for-created-event.patch diff --git a/0001-Added-event-link-display-for-created-event.patch b/0001-Added-event-link-display-for-created-event.patch new file mode 100644 index 0000000..b94b298 --- /dev/null +++ b/0001-Added-event-link-display-for-created-event.patch @@ -0,0 +1,1482 @@ +From 14795337605f14acecb1617e6477c307c4802528 Mon Sep 17 00:00:00 2001 +From: mctaylors +Date: Sun, 23 Oct 2022 13:39:59 +0300 +Subject: [PATCH] Added event link display for created event + +--- + Boyfriend/EventHandler.cs | 2 +- + Boyfriend/Messages.Designer.cs | 884 ++++++++++----------------------- + Boyfriend/Messages.resx | 2 +- + Boyfriend/Messages.ru.resx | 2 +- + Boyfriend/Messages.tt-ru.resx | 5 +- + 5 files changed, 259 insertions(+), 636 deletions(-) + +diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs +index 6ba2bdf..25a4410 100644 +--- a/Boyfriend/EventHandler.cs ++++ b/Boyfriend/EventHandler.cs +@@ -129,7 +129,7 @@ public static class EventHandler { + await Utils.SilentSendAsync(channel, + string.Format(Messages.EventCreated, "\n", roleMention, scheduledEvent.Creator.Mention, + Utils.Wrap(scheduledEvent.Name), location, +- scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description)), ++ scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description), guild.Id, scheduledEvent.Id), + true); + } + if (eventConfig["EventEarlyNotificationOffset"] != "0") +diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs +index 0e8babe..093cc8b 100644 +--- a/Boyfriend/Messages.Designer.cs ++++ b/Boyfriend/Messages.Designer.cs +@@ -11,46 +11,32 @@ namespace Boyfriend { + using System; + + +- /// +- /// A strongly-typed resource class, for looking up localized strings, etc. +- /// +- // This class was auto-generated by the StronglyTypedResourceBuilder +- // class via a tool like ResGen or Visual Studio. +- // To add or remove a member, edit your .ResX file then rerun ResGen +- // with the /str option, or rebuild your VS project. +- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] +- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] ++ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] ++ [System.Diagnostics.DebuggerNonUserCodeAttribute()] ++ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Messages { + +- private static global::System.Resources.ResourceManager resourceMan; ++ private static System.Resources.ResourceManager resourceMan; + +- private static global::System.Globalization.CultureInfo resourceCulture; ++ private static System.Globalization.CultureInfo resourceCulture; + +- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] ++ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Messages() { + } + +- /// +- /// Returns the cached ResourceManager instance used by this class. +- /// +- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] +- internal static global::System.Resources.ResourceManager ResourceManager { ++ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] ++ internal static System.Resources.ResourceManager ResourceManager { + get { +- if (object.ReferenceEquals(resourceMan, null)) { +- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); ++ if (object.Equals(null, resourceMan)) { ++ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + +- /// +- /// Overrides the current thread's CurrentUICulture property for all +- /// resource lookups using this strongly typed resource class. +- /// +- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] +- internal static global::System.Globalization.CultureInfo Culture { ++ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] ++ internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } +@@ -59,1101 +45,735 @@ namespace Boyfriend { + } + } + +- /// +- /// Looks up a localized string similar to Too many mentions in 1 message. +- /// +- internal static string AutobanReason { ++ internal static string Ready { + get { +- return ResourceManager.GetString("AutobanReason", resourceCulture); ++ return ResourceManager.GetString("Ready", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Bah! . +- /// +- internal static string Beep1 { ++ internal static string CachedMessageDeleted { + get { +- return ResourceManager.GetString("Beep1", resourceCulture); ++ return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Bop! . +- /// +- internal static string Beep2 { ++ internal static string AutobanReason { + get { +- return ResourceManager.GetString("Beep2", resourceCulture); ++ return ResourceManager.GetString("AutobanReason", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Beep! . +- /// +- internal static string Beep3 { ++ internal static string CachedMessageEdited { + get { +- return ResourceManager.GetString("Beep3", resourceCulture); ++ return ResourceManager.GetString("CachedMessageEdited", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot ban users from this guild!. +- /// +- internal static string BotCannotBanMembers { ++ internal static string DefaultWelcomeMessage { + get { +- return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); ++ return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot ban this user!. +- /// +- internal static string BotCannotBanTarget { ++ internal static string Beep1 { + get { +- return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); ++ return ResourceManager.GetString("Beep1", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot kick members from this guild!. +- /// +- internal static string BotCannotKickMembers { ++ internal static string Beep2 { + get { +- return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); ++ return ResourceManager.GetString("Beep2", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot kick this member!. +- /// +- internal static string BotCannotKickTarget { ++ internal static string Beep3 { + get { +- return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); ++ return ResourceManager.GetString("Beep3", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot manage this guild!. +- /// +- internal static string BotCannotManageGuild { ++ internal static string CommandNoPermissionBot { + get { +- return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); ++ return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot manage messages in this guild!. +- /// +- internal static string BotCannotManageMessages { ++ internal static string CommandNoPermissionUser { + get { +- return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); ++ return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot moderate members in this guild!. +- /// +- internal static string BotCannotModerateMembers { ++ internal static string YouWereBanned { + get { +- return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); ++ return ResourceManager.GetString("YouWereBanned", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot mute this member!. +- /// +- internal static string BotCannotMuteTarget { ++ internal static string PunishmentExpired { + get { +- return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); ++ return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot unmute this member!. +- /// +- internal static string BotCannotUnmuteTarget { ++ internal static string ClearAmountTooSmall { + get { +- return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); ++ return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. +- /// +- internal static string CachedMessageDeleted { ++ internal static string ClearAmountTooLarge { + get { +- return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); ++ return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. +- /// +- internal static string CachedMessageEdited { ++ internal static string CommandHelp { + get { +- return ResourceManager.GetString("CachedMessageEdited", resourceCulture); ++ return ResourceManager.GetString("CommandHelp", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. +- /// +- internal static string CannotTimeOutBot { ++ internal static string YouWereKicked { + get { +- return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); ++ return ResourceManager.GetString("YouWereKicked", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Not specified. +- /// +- internal static string ChannelNotSpecified { ++ internal static string Milliseconds { + get { +- return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); ++ return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify an integer from {0} to {1} instead of {2}!. +- /// +- internal static string ClearAmountInvalid { ++ internal static string MemberAlreadyMuted { + get { +- return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); ++ return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You specified more than {0} messages!. +- /// +- internal static string ClearAmountTooLarge { ++ internal static string ChannelNotSpecified { + get { +- return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); ++ return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You specified less than {0} messages!. +- /// +- internal static string ClearAmountTooSmall { ++ internal static string RoleNotSpecified { + get { +- return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); ++ return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Bans a user. +- /// +- internal static string CommandDescriptionBan { ++ internal static string CurrentSettings { + get { +- return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); ++ return ResourceManager.GetString("CurrentSettings", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to We do not support hate towards our fellow members. And sometimes, we are not able to ban the offender.. +- /// +- internal static string CommandDescriptionCavepleaselisten { ++ internal static string SettingsLang { + get { +- return ResourceManager.GetString("CommandDescriptionCavepleaselisten", resourceCulture); ++ return ResourceManager.GetString("SettingsLang", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. +- /// +- internal static string CommandDescriptionClear { ++ internal static string SettingsPrefix { + get { +- return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); ++ return ResourceManager.GetString("SettingsPrefix", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Shows this message. +- /// +- internal static string CommandDescriptionHelp { ++ internal static string SettingsRemoveRolesOnMute { + get { +- return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); ++ return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Kicks a member. +- /// +- internal static string CommandDescriptionKick { ++ internal static string SettingsSendWelcomeMessages { + get { +- return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); ++ return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Mutes a member. +- /// +- internal static string CommandDescriptionMute { ++ internal static string SettingsStarterRole { + get { +- return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); ++ return ResourceManager.GetString("SettingsStarterRole", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Shows (inaccurate) latency. +- /// +- internal static string CommandDescriptionPing { ++ internal static string SettingsMuteRole { + get { +- return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); ++ return ResourceManager.GetString("SettingsMuteRole", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Allows you to change certain preferences for this guild. +- /// +- internal static string CommandDescriptionSettings { ++ internal static string SettingsAdminLogChannel { + get { +- return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); ++ return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Unbans a user. +- /// +- internal static string CommandDescriptionUnban { ++ internal static string SettingsBotLogChannel { + get { +- return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); ++ return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Unmutes a member. +- /// +- internal static string CommandDescriptionUnmute { ++ internal static string LanguageNotSupported { + get { +- return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); ++ return ResourceManager.GetString("LanguageNotSupported", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Command help:. +- /// +- internal static string CommandHelp { ++ internal static string Yes { + get { +- return ResourceManager.GetString("CommandHelp", resourceCulture); ++ return ResourceManager.GetString("Yes", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I do not have permission to execute this command!. +- /// +- internal static string CommandNoPermissionBot { ++ internal static string No { + get { +- return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); ++ return ResourceManager.GetString("No", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You do not have permission to execute this command!. +- /// +- internal static string CommandNoPermissionUser { ++ internal static string UserNotBanned { + get { +- return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); ++ return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Current settings:. +- /// +- internal static string CurrentSettings { ++ internal static string MemberNotMuted { + get { +- return ResourceManager.GetString("CurrentSettings", resourceCulture); ++ return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to {0}, welcome to {1}. +- /// +- internal static string DefaultWelcomeMessage { ++ internal static string RolesReturned { + get { +- return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); ++ return ResourceManager.GetString("RolesReturned", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to 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. +- /// +- internal static string DurationRequiredForTimeOuts { ++ internal static string SettingsWelcomeMessage { + get { +- return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); ++ return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Event {0} is cancelled!{1}. +- /// +- internal static string EventCancelled { ++ internal static string ClearAmountInvalid { + get { +- return ResourceManager.GetString("EventCancelled", resourceCulture); ++ return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Event {0} has completed! Duration: {1}. +- /// +- internal static string EventCompleted { ++ internal static string FeedbackUserBanned { + get { +- return ResourceManager.GetString("EventCompleted", resourceCulture); ++ return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}. +- /// +- internal static string EventCreated { ++ internal static string UserNotInGuild { + get { +- return ResourceManager.GetString("EventCreated", resourceCulture); ++ return ResourceManager.GetString("UserNotInGuild", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to {0}Event {1} will start <t:{2}:R>!. +- /// +- internal static string EventEarlyNotification { ++ internal static string SettingDoesntExist { + get { +- return ResourceManager.GetString("EventEarlyNotification", resourceCulture); ++ return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. +- /// +- internal static string EventStarted { ++ internal static string SettingsReceiveStartupMessages { + get { +- return ResourceManager.GetString("EventStarted", resourceCulture); ++ return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to ever. +- /// +- internal static string Ever { ++ internal static string InvalidSettingValue { + get { +- return ResourceManager.GetString("Ever", resourceCulture); ++ return ResourceManager.GetString("InvalidSettingValue", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to *[{0}: {1}]*. +- /// +- internal static string FeedbackFormat { ++ internal static string InvalidRole { + get { +- return ResourceManager.GetString("FeedbackFormat", resourceCulture); ++ return ResourceManager.GetString("InvalidRole", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Kicked {0}: {1}. +- /// +- internal static string FeedbackMemberKicked { ++ internal static string InvalidChannel { + get { +- return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); ++ return ResourceManager.GetString("InvalidChannel", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Muted {0} for{1}: {2}. +- /// +- internal static string FeedbackMemberMuted { ++ internal static string RoleRemovalFailed { + get { +- return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); ++ return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Unmuted {0}: {1}. +- /// +- internal static string FeedbackMemberUnmuted { ++ internal static string DurationRequiredForTimeOuts { + get { +- return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); ++ return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Deleted {0} messages in {1}. +- /// +- internal static string FeedbackMessagesCleared { ++ internal static string CannotTimeOutBot { + get { +- return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); ++ return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. +- /// +- internal static string FeedbackSettingsUpdated { ++ internal static string EventCreated { + get { +- return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); ++ return ResourceManager.GetString("EventCreated", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Banned {0} for{1}: {2}. +- /// +- internal static string FeedbackUserBanned { ++ internal static string SettingsEventNotifyReceiverRole { + get { +- return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); ++ return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Unbanned {0}: {1}. +- /// +- internal static string FeedbackUserUnbanned { ++ internal static string SettingsEventCreatedChannel { + get { +- return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); ++ return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to This channel does not exist!. +- /// +- internal static string InvalidChannel { ++ internal static string SettingsEventStartedChannel { + get { +- return ResourceManager.GetString("InvalidChannel", resourceCulture); ++ return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a guild member instead of {0}!. +- /// +- internal static string InvalidMember { ++ internal static string SettingsEventStartedReceivers { + get { +- return ResourceManager.GetString("InvalidMember", resourceCulture); ++ return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to This role does not exist!. +- /// +- internal static string InvalidRole { ++ internal static string EventStarted { + get { +- return ResourceManager.GetString("InvalidRole", resourceCulture); ++ return ResourceManager.GetString("EventStarted", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Invalid setting value specified!. +- /// +- internal static string InvalidSettingValue { ++ internal static string SettingsFrowningFace { + get { +- return ResourceManager.GetString("InvalidSettingValue", resourceCulture); ++ return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a user instead of {0}!. +- /// +- internal static string InvalidUser { ++ internal static string EventCancelled { + get { +- return ResourceManager.GetString("InvalidUser", resourceCulture); ++ return ResourceManager.GetString("EventCancelled", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Language not supported!. +- /// +- internal static string LanguageNotSupported { ++ internal static string SettingsEventCancelledChannel { + get { +- return ResourceManager.GetString("LanguageNotSupported", resourceCulture); ++ return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Member is already muted!. +- /// +- internal static string MemberAlreadyMuted { ++ internal static string SettingsEventCompletedChannel { + get { +- return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); ++ return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Member not muted!. +- /// +- internal static string MemberNotMuted { ++ internal static string EventCompleted { + get { +- return ResourceManager.GetString("MemberNotMuted", resourceCulture); ++ return ResourceManager.GetString("EventCompleted", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to ms. +- /// +- internal static string Milliseconds { ++ internal static string FeedbackFormat { + get { +- return ResourceManager.GetString("Milliseconds", resourceCulture); ++ return ResourceManager.GetString("FeedbackFormat", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a reason to ban this user!. +- /// +- internal static string MissingBanReason { ++ internal static string Ever { + get { +- return ResourceManager.GetString("MissingBanReason", resourceCulture); ++ return ResourceManager.GetString("Ever", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a reason to kick this member!. +- /// +- internal static string MissingKickReason { ++ internal static string FeedbackMessagesCleared { + get { +- return ResourceManager.GetString("MissingKickReason", resourceCulture); ++ return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a guild member!. +- /// +- internal static string MissingMember { ++ internal static string FeedbackMemberKicked { + get { +- return ResourceManager.GetString("MissingMember", resourceCulture); ++ return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a reason to mute this member!. +- /// +- internal static string MissingMuteReason { ++ internal static string FeedbackMemberMuted { + get { +- return ResourceManager.GetString("MissingMuteReason", resourceCulture); ++ return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. +- /// +- internal static string MissingNumber { ++ internal static string FeedbackUserUnbanned { + get { +- return ResourceManager.GetString("MissingNumber", resourceCulture); ++ return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a setting to change!. +- /// +- internal static string MissingSetting { ++ internal static string FeedbackMemberUnmuted { + get { +- return ResourceManager.GetString("MissingSetting", resourceCulture); ++ return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a reason to unban this user!. +- /// +- internal static string MissingUnbanReason { ++ internal static string SettingsNothingChanged { + get { +- return ResourceManager.GetString("MissingUnbanReason", resourceCulture); ++ return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a reason for unmute this member!. +- /// +- internal static string MissingUnmuteReason { ++ internal static string SettingNotDefined { + get { +- return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); ++ return ResourceManager.GetString("SettingNotDefined", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You need to specify a user!. +- /// +- internal static string MissingUser { ++ internal static string FeedbackSettingsUpdated { + get { +- return ResourceManager.GetString("MissingUser", resourceCulture); ++ return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to No. +- /// +- internal static string No { ++ internal static string CommandDescriptionBan { + get { +- return ResourceManager.GetString("No", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Punishment expired. +- /// +- internal static string PunishmentExpired { ++ internal static string CommandDescriptionClear { + get { +- return ResourceManager.GetString("PunishmentExpired", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to {0}I'm ready! (C#). +- /// +- internal static string Ready { ++ internal static string CommandDescriptionHelp { + get { +- return ResourceManager.GetString("Ready", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Not specified. +- /// +- internal static string RoleNotSpecified { ++ internal static string CommandDescriptionKick { + get { +- return ResourceManager.GetString("RoleNotSpecified", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to I couldn't remove role {0} because of an error! {1}. +- /// +- internal static string RoleRemovalFailed { ++ internal static string CommandDescriptionMute { + get { +- return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionMute", 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 { ++ internal static string CommandDescriptionPing { + get { +- return ResourceManager.GetString("RolesReturned", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to This feature is unavailable because this guild is currently blacklisted.. +- /// +- internal static string ServerBlacklisted { ++ internal static string CommandDescriptionSettings { + get { +- return ResourceManager.GetString("ServerBlacklisted", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to That setting doesn't exist!. +- /// +- internal static string SettingDoesntExist { ++ internal static string CommandDescriptionUnban { + get { +- return ResourceManager.GetString("SettingDoesntExist", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Not specified. +- /// +- internal static string SettingNotDefined { ++ internal static string CommandDescriptionUnmute { + get { +- return ResourceManager.GetString("SettingNotDefined", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Admin log channel. +- /// +- internal static string SettingsAdminLogChannel { ++ internal static string MissingNumber { + get { +- return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); ++ return ResourceManager.GetString("MissingNumber", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Bot log channel. +- /// +- internal static string SettingsBotLogChannel { ++ internal static string MissingUser { + get { +- return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); ++ return ResourceManager.GetString("MissingUser", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Channel for event cancellation notifications. +- /// +- internal static string SettingsEventCancelledChannel { ++ internal static string InvalidUser { + get { +- return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); ++ return ResourceManager.GetString("InvalidUser", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Channel for event completion notifications. +- /// +- internal static string SettingsEventCompletedChannel { ++ internal static string MissingMember { + get { +- return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); ++ return ResourceManager.GetString("MissingMember", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Channel for event creation notifications. +- /// +- internal static string SettingsEventCreatedChannel { ++ internal static string InvalidMember { + get { +- return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); ++ return ResourceManager.GetString("InvalidMember", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to . +- /// +- internal static string SettingsEventEarlyNotificationOffset { ++ internal static string UserCannotBanMembers { + get { +- return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); ++ return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Role for event creation notifications. +- /// +- internal static string SettingsEventNotifyReceiverRole { ++ internal static string UserCannotManageMessages { + get { +- return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); ++ return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Channel for event start notifications. +- /// +- internal static string SettingsEventStartedChannel { ++ internal static string UserCannotKickMembers { + get { +- return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); ++ return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Event start notifications receivers. +- /// +- internal static string SettingsEventStartedReceivers { ++ internal static string UserCannotModerateMembers { + get { +- return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); ++ return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to :(. +- /// +- internal static string SettingsFrowningFace { ++ internal static string UserCannotManageGuild { + get { +- return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); ++ return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Language. +- /// +- internal static string SettingsLang { ++ internal static string BotCannotBanMembers { + get { +- return ResourceManager.GetString("SettingsLang", resourceCulture); ++ return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Mute role. +- /// +- internal static string SettingsMuteRole { ++ internal static string BotCannotManageMessages { + get { +- return ResourceManager.GetString("SettingsMuteRole", resourceCulture); ++ return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. +- /// +- internal static string SettingsNothingChanged { ++ internal static string BotCannotKickMembers { + get { +- return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); ++ return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Prefix. +- /// +- internal static string SettingsPrefix { ++ internal static string BotCannotModerateMembers { + get { +- return ResourceManager.GetString("SettingsPrefix", resourceCulture); ++ return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Receive startup messages. +- /// +- internal static string SettingsReceiveStartupMessages { ++ internal static string BotCannotManageGuild { + get { +- return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); ++ return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Remove roles on mute. +- /// +- internal static string SettingsRemoveRolesOnMute { ++ internal static string MissingBanReason { + get { +- return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); ++ return ResourceManager.GetString("MissingBanReason", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Send welcome messages. +- /// +- internal static string SettingsSendWelcomeMessages { ++ internal static string MissingKickReason { + get { +- return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); ++ return ResourceManager.GetString("MissingKickReason", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Starter role. +- /// +- internal static string SettingsStarterRole { ++ internal static string MissingMuteReason { + get { +- return ResourceManager.GetString("SettingsStarterRole", resourceCulture); ++ return ResourceManager.GetString("MissingMuteReason", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Welcome message. +- /// +- internal static string SettingsWelcomeMessage { ++ internal static string MissingUnbanReason { + get { +- return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); ++ return ResourceManager.GetString("MissingUnbanReason", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot ban me!. +- /// +- internal static string UserCannotBanBot { ++ internal static string MissingUnmuteReason { + get { +- return ResourceManager.GetString("UserCannotBanBot", resourceCulture); ++ return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot ban users from this guild!. +- /// +- internal static string UserCannotBanMembers { ++ internal static string MissingSetting { + get { +- return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); ++ return ResourceManager.GetString("MissingSetting", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot ban the owner of this guild!. +- /// + internal static string UserCannotBanOwner { + get { + return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot ban this user!. +- /// +- internal static string UserCannotBanTarget { +- get { +- return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); +- } +- } +- +- /// +- /// Looks up a localized string similar to You cannot ban yourself!. +- /// + internal static string UserCannotBanThemselves { + get { + return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot kick me!. +- /// +- internal static string UserCannotKickBot { ++ internal static string UserCannotBanBot { + get { +- return ResourceManager.GetString("UserCannotKickBot", resourceCulture); ++ return ResourceManager.GetString("UserCannotBanBot", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot kick members from this guild!. +- /// +- internal static string UserCannotKickMembers { ++ internal static string BotCannotBanTarget { + get { +- return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); ++ return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot kick the owner of this guild!. +- /// +- internal static string UserCannotKickOwner { ++ internal static string UserCannotBanTarget { + get { +- return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); ++ return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot kick this member!. +- /// +- internal static string UserCannotKickTarget { ++ internal static string UserCannotKickOwner { + get { +- return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); ++ return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot kick yourself!. +- /// + internal static string UserCannotKickThemselves { + get { + return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot manage this guild!. +- /// +- internal static string UserCannotManageGuild { ++ internal static string UserCannotKickBot { + get { +- return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); ++ return ResourceManager.GetString("UserCannotKickBot", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot manage messages in this guild!. +- /// +- internal static string UserCannotManageMessages { ++ internal static string BotCannotKickTarget { + get { +- return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); ++ return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot moderate members in this guild!. +- /// +- internal static string UserCannotModerateMembers { ++ internal static string UserCannotKickTarget { + get { +- return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); ++ return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot mute me!. +- /// +- internal static string UserCannotMuteBot { ++ internal static string UserCannotMuteOwner { + get { +- return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); ++ return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot mute the owner of this guild!. +- /// +- internal static string UserCannotMuteOwner { ++ internal static string UserCannotMuteThemselves { + get { +- return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); ++ return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot mute this member!. +- /// +- internal static string UserCannotMuteTarget { ++ internal static string UserCannotMuteBot { + get { +- return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); ++ return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot mute yourself!. +- /// +- internal static string UserCannotMuteThemselves { ++ internal static string BotCannotMuteTarget { + get { +- return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); ++ return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to .... +- /// +- internal static string UserCannotUnmuteBot { ++ internal static string UserCannotMuteTarget { + get { +- return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); ++ return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. +- /// + internal static string UserCannotUnmuteOwner { + get { + return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You cannot unmute this user!. +- /// +- internal static string UserCannotUnmuteTarget { ++ internal static string UserCannotUnmuteThemselves { + get { +- return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); ++ return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You are muted!. +- /// +- internal static string UserCannotUnmuteThemselves { ++ internal static string UserCannotUnmuteBot { + get { +- return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); ++ return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to This user is not banned!. +- /// +- internal static string UserNotBanned { ++ internal static string BotCannotUnmuteTarget { + get { +- return ResourceManager.GetString("UserNotBanned", resourceCulture); ++ return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to The specified user is not a member of this server!. +- /// +- internal static string UserNotInGuild { ++ internal static string UserCannotUnmuteTarget { + get { +- return ResourceManager.GetString("UserNotInGuild", resourceCulture); ++ return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to Yes. +- /// +- internal static string Yes { ++ internal static string CommandDescriptionCavepleaselisten { + get { +- return ResourceManager.GetString("Yes", resourceCulture); ++ return ResourceManager.GetString("CommandDescriptionCavepleaselisten", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. +- /// +- internal static string YouWereBanned { ++ internal static string ServerBlacklisted { + get { +- return ResourceManager.GetString("YouWereBanned", resourceCulture); ++ return ResourceManager.GetString("ServerBlacklisted", resourceCulture); + } + } + +- /// +- /// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. +- /// +- internal static string YouWereKicked { ++ internal static string EventEarlyNotification { + get { +- return ResourceManager.GetString("YouWereKicked", resourceCulture); ++ return ResourceManager.GetString("EventEarlyNotification", resourceCulture); ++ } ++ } ++ ++ internal static string SettingsEventEarlyNotificationOffset { ++ get { ++ return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); + } + } + } +diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx +index 6615656..a06e8cc 100644 +--- a/Boyfriend/Messages.resx ++++ b/Boyfriend/Messages.resx +@@ -166,7 +166,7 @@ + I cannot use time-outs on other bots! Try to set a mute role in settings + + +- {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6} ++ {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} + + + Role for event creation notifications +diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx +index 5f92c9d..b593ef5 100644 +--- a/Boyfriend/Messages.ru.resx ++++ b/Boyfriend/Messages.ru.resx +@@ -157,7 +157,7 @@ + Начальная роль + + +- {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} ++ {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} + + + Роль для уведомлений о создании событий +diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx +index 0d676cd..b49d3ef 100644 +--- a/Boyfriend/Messages.tt-ru.resx ++++ b/Boyfriend/Messages.tt-ru.resx +@@ -153,7 +153,7 @@ + базовое звание + + +- {1}{2} приготовил новый квест {3}! он пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} ++ {1}{2} приготовил новый квест {3}! он пройдёт в {4} и начнётся <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} + + + роль для уведомлений о создании квеста +@@ -371,4 +371,7 @@ + + упс, кажется ваш сервер в черном списке, и я вам ничем помочь не смогу) + ++ ++ {0}квест {1} начнется<t:{2}:R>! ++ + +\ No newline at end of file +-- +2.34.1.windows.1 + diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index a67dde6..1c0a3a0 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -129,7 +129,7 @@ public static class EventHandler { await Utils.SilentSendAsync(channel, string.Format(Messages.EventCreated, "\n", roleMention, scheduledEvent.Creator.Mention, Utils.Wrap(scheduledEvent.Name), location, - scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description)), + scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description), guild.Id, scheduledEvent.Id), true); } if (eventConfig["EventEarlyNotificationOffset"] != "0") { diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 0e8babe..093cc8b 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -11,46 +11,32 @@ namespace Boyfriend { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -59,1102 +45,736 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Too many mentions in 1 message. - /// - internal static string AutobanReason { - get { - return ResourceManager.GetString("AutobanReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bah! . - /// - internal static string Beep1 { - get { - return ResourceManager.GetString("Beep1", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bop! . - /// - internal static string Beep2 { - get { - return ResourceManager.GetString("Beep2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Beep! . - /// - internal static string Beep3 { - get { - return ResourceManager.GetString("Beep3", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot ban users from this guild!. - /// - internal static string BotCannotBanMembers { - get { - return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot ban this user!. - /// - internal static string BotCannotBanTarget { - get { - return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot kick members from this guild!. - /// - internal static string BotCannotKickMembers { - get { - return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot kick this member!. - /// - internal static string BotCannotKickTarget { - get { - return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot manage this guild!. - /// - internal static string BotCannotManageGuild { - get { - return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot manage messages in this guild!. - /// - internal static string BotCannotManageMessages { - get { - return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot moderate members in this guild!. - /// - internal static string BotCannotModerateMembers { - get { - return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot mute this member!. - /// - internal static string BotCannotMuteTarget { - get { - return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot unmute this member!. - /// - internal static string BotCannotUnmuteTarget { - get { - return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. - /// - internal static string CachedMessageDeleted { - get { - return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. - /// - internal static string CachedMessageEdited { - get { - return ResourceManager.GetString("CachedMessageEdited", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. - /// - internal static string CannotTimeOutBot { - get { - return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string ChannelNotSpecified { - get { - return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify an integer from {0} to {1} instead of {2}!. - /// - internal static string ClearAmountInvalid { - get { - return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You specified more than {0} messages!. - /// - internal static string ClearAmountTooLarge { - get { - return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You specified less than {0} messages!. - /// - internal static string ClearAmountTooSmall { - get { - return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bans a user. - /// - internal static string CommandDescriptionBan { - get { - return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to We do not support hate towards our fellow members. And sometimes, we are not able to ban the offender.. - /// - internal static string CommandDescriptionCavepleaselisten { - get { - return ResourceManager.GetString("CommandDescriptionCavepleaselisten", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. - /// - internal static string CommandDescriptionClear { - get { - return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Shows this message. - /// - internal static string CommandDescriptionHelp { - get { - return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Kicks a member. - /// - internal static string CommandDescriptionKick { - get { - return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mutes a member. - /// - internal static string CommandDescriptionMute { - get { - return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Shows (inaccurate) latency. - /// - internal static string CommandDescriptionPing { - get { - return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allows you to change certain preferences for this guild. - /// - internal static string CommandDescriptionSettings { - get { - return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unbans a user. - /// - internal static string CommandDescriptionUnban { - get { - return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unmutes a member. - /// - internal static string CommandDescriptionUnmute { - get { - return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Command help:. - /// - internal static string CommandHelp { - get { - return ResourceManager.GetString("CommandHelp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I do not have permission to execute this command!. - /// - internal static string CommandNoPermissionBot { - get { - return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You do not have permission to execute this command!. - /// - internal static string CommandNoPermissionUser { - get { - return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Current settings:. - /// - internal static string CurrentSettings { - get { - return ResourceManager.GetString("CurrentSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}, welcome to {1}. - /// - internal static string DefaultWelcomeMessage { - get { - return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to 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. - /// - internal static string DurationRequiredForTimeOuts { - get { - return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event {0} is cancelled!{1}. - /// - internal static string EventCancelled { - get { - return ResourceManager.GetString("EventCancelled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event {0} has completed! Duration: {1}. - /// - internal static string EventCompleted { - get { - return ResourceManager.GetString("EventCompleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}. - /// - internal static string EventCreated { - get { - return ResourceManager.GetString("EventCreated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}Event {1} will start <t:{2}:R>!. - /// - internal static string EventEarlyNotification { - get { - return ResourceManager.GetString("EventEarlyNotification", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. - /// - internal static string EventStarted { - get { - return ResourceManager.GetString("EventStarted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ever. - /// - internal static string Ever { - get { - return ResourceManager.GetString("Ever", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to *[{0}: {1}]*. - /// - internal static string FeedbackFormat { - get { - return ResourceManager.GetString("FeedbackFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Kicked {0}: {1}. - /// - internal static string FeedbackMemberKicked { - get { - return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Muted {0} for{1}: {2}. - /// - internal static string FeedbackMemberMuted { - get { - return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unmuted {0}: {1}. - /// - internal static string FeedbackMemberUnmuted { - get { - return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deleted {0} messages in {1}. - /// - internal static string FeedbackMessagesCleared { - get { - return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. - /// - internal static string FeedbackSettingsUpdated { - get { - return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Banned {0} for{1}: {2}. - /// - internal static string FeedbackUserBanned { - get { - return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unbanned {0}: {1}. - /// - internal static string FeedbackUserUnbanned { - get { - return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This channel does not exist!. - /// - internal static string InvalidChannel { - get { - return ResourceManager.GetString("InvalidChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a guild member instead of {0}!. - /// - internal static string InvalidMember { - get { - return ResourceManager.GetString("InvalidMember", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This role does not exist!. - /// - internal static string InvalidRole { - get { - return ResourceManager.GetString("InvalidRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid setting value specified!. - /// - internal static string InvalidSettingValue { - get { - return ResourceManager.GetString("InvalidSettingValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a user instead of {0}!. - /// - internal static string InvalidUser { - get { - return ResourceManager.GetString("InvalidUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language not supported!. - /// - internal static string LanguageNotSupported { - get { - return ResourceManager.GetString("LanguageNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Member is already muted!. - /// - internal static string MemberAlreadyMuted { - get { - return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Member not muted!. - /// - internal static string MemberNotMuted { - get { - return ResourceManager.GetString("MemberNotMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ms. - /// - internal static string Milliseconds { - get { - return ResourceManager.GetString("Milliseconds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to ban this user!. - /// - internal static string MissingBanReason { - get { - return ResourceManager.GetString("MissingBanReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to kick this member!. - /// - internal static string MissingKickReason { - get { - return ResourceManager.GetString("MissingKickReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a guild member!. - /// - internal static string MissingMember { - get { - return ResourceManager.GetString("MissingMember", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to mute this member!. - /// - internal static string MissingMuteReason { - get { - return ResourceManager.GetString("MissingMuteReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. - /// - internal static string MissingNumber { - get { - return ResourceManager.GetString("MissingNumber", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a setting to change!. - /// - internal static string MissingSetting { - get { - return ResourceManager.GetString("MissingSetting", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to unban this user!. - /// - internal static string MissingUnbanReason { - get { - return ResourceManager.GetString("MissingUnbanReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason for unmute this member!. - /// - internal static string MissingUnmuteReason { - get { - return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a user!. - /// - internal static string MissingUser { - get { - return ResourceManager.GetString("MissingUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No. - /// - internal static string No { - get { - return ResourceManager.GetString("No", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Punishment expired. - /// - internal static string PunishmentExpired { - get { - return ResourceManager.GetString("PunishmentExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}I'm ready! (C#). - /// internal static string Ready { get { return ResourceManager.GetString("Ready", resourceCulture); } } - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string RoleNotSpecified { + internal static string CachedMessageDeleted { get { - return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - /// - /// Looks up a localized string similar to I couldn't remove role {0} because of an error! {1}. - /// - internal static string RoleRemovalFailed { + internal static string AutobanReason { get { - return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); + return ResourceManager.GetString("AutobanReason", 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 { + internal static string CachedMessageEdited { get { - return ResourceManager.GetString("RolesReturned", resourceCulture); + return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - /// - /// Looks up a localized string similar to This feature is unavailable because this guild is currently blacklisted.. - /// - internal static string ServerBlacklisted { + internal static string DefaultWelcomeMessage { get { - return ResourceManager.GetString("ServerBlacklisted", resourceCulture); + return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); } } - /// - /// Looks up a localized string similar to That setting doesn't exist!. - /// - internal static string SettingDoesntExist { + internal static string Beep1 { get { - return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + return ResourceManager.GetString("Beep1", resourceCulture); } } - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string SettingNotDefined { + internal static string Beep2 { get { - return ResourceManager.GetString("SettingNotDefined", resourceCulture); + return ResourceManager.GetString("Beep2", resourceCulture); } } - /// - /// Looks up a localized string similar to Admin log channel. - /// - internal static string SettingsAdminLogChannel { + internal static string Beep3 { get { - return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); + return ResourceManager.GetString("Beep3", resourceCulture); } } - /// - /// Looks up a localized string similar to Bot log channel. - /// - internal static string SettingsBotLogChannel { + internal static string CommandNoPermissionBot { get { - return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); + return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); } } - /// - /// Looks up a localized string similar to Channel for event cancellation notifications. - /// - internal static string SettingsEventCancelledChannel { + internal static string CommandNoPermissionUser { get { - return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); + return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); } } - /// - /// Looks up a localized string similar to Channel for event completion notifications. - /// - internal static string SettingsEventCompletedChannel { - get { - return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event creation notifications. - /// - internal static string SettingsEventCreatedChannel { - get { - return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to . - /// - internal static string SettingsEventEarlyNotificationOffset { - get { - return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Role for event creation notifications. - /// - internal static string SettingsEventNotifyReceiverRole { - get { - return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event start notifications. - /// - internal static string SettingsEventStartedChannel { - get { - return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event start notifications receivers. - /// - internal static string SettingsEventStartedReceivers { - get { - return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to :(. - /// - internal static string SettingsFrowningFace { - get { - return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language. - /// - internal static string SettingsLang { - get { - return ResourceManager.GetString("SettingsLang", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mute role. - /// - internal static string SettingsMuteRole { - get { - return ResourceManager.GetString("SettingsMuteRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. - /// - internal static string SettingsNothingChanged { - get { - return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prefix. - /// - internal static string SettingsPrefix { - get { - return ResourceManager.GetString("SettingsPrefix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Receive startup messages. - /// - internal static string SettingsReceiveStartupMessages { - get { - return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remove roles on mute. - /// - internal static string SettingsRemoveRolesOnMute { - get { - return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send welcome messages. - /// - internal static string SettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Starter role. - /// - internal static string SettingsStarterRole { - get { - return ResourceManager.GetString("SettingsStarterRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Welcome message. - /// - internal static string SettingsWelcomeMessage { - get { - return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban me!. - /// - internal static string UserCannotBanBot { - get { - return ResourceManager.GetString("UserCannotBanBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban users from this guild!. - /// - internal static string UserCannotBanMembers { - get { - return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban the owner of this guild!. - /// - internal static string UserCannotBanOwner { - get { - return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban this user!. - /// - internal static string UserCannotBanTarget { - get { - return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban yourself!. - /// - internal static string UserCannotBanThemselves { - get { - return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick me!. - /// - internal static string UserCannotKickBot { - get { - return ResourceManager.GetString("UserCannotKickBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick members from this guild!. - /// - internal static string UserCannotKickMembers { - get { - return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick the owner of this guild!. - /// - internal static string UserCannotKickOwner { - get { - return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick this member!. - /// - internal static string UserCannotKickTarget { - get { - return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick yourself!. - /// - internal static string UserCannotKickThemselves { - get { - return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot manage this guild!. - /// - internal static string UserCannotManageGuild { - get { - return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot manage messages in this guild!. - /// - internal static string UserCannotManageMessages { - get { - return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot moderate members in this guild!. - /// - internal static string UserCannotModerateMembers { - get { - return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute me!. - /// - internal static string UserCannotMuteBot { - get { - return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute the owner of this guild!. - /// - internal static string UserCannotMuteOwner { - get { - return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute this member!. - /// - internal static string UserCannotMuteTarget { - get { - return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute yourself!. - /// - internal static string UserCannotMuteThemselves { - get { - return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to .... - /// - internal static string UserCannotUnmuteBot { - get { - return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. - /// - internal static string UserCannotUnmuteOwner { - get { - return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot unmute this user!. - /// - internal static string UserCannotUnmuteTarget { - get { - return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You are muted!. - /// - internal static string UserCannotUnmuteThemselves { - get { - return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This user is not banned!. - /// - internal static string UserNotBanned { - get { - return ResourceManager.GetString("UserNotBanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified user is not a member of this server!. - /// - internal static string UserNotInGuild { - get { - return ResourceManager.GetString("UserNotInGuild", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yes. - /// - internal static string Yes { - get { - return ResourceManager.GetString("Yes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. - /// internal static string YouWereBanned { get { return ResourceManager.GetString("YouWereBanned", resourceCulture); } } - /// - /// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. - /// + internal static string PunishmentExpired { + get { + return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + + internal static string ClearAmountTooSmall { + get { + return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); + } + } + + internal static string ClearAmountTooLarge { + get { + return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); + } + } + + internal static string CommandHelp { + get { + return ResourceManager.GetString("CommandHelp", resourceCulture); + } + } + internal static string YouWereKicked { get { return ResourceManager.GetString("YouWereKicked", resourceCulture); } } + + internal static string Milliseconds { + get { + return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + + internal static string MemberAlreadyMuted { + get { + return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + + internal static string ChannelNotSpecified { + get { + return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); + } + } + + internal static string RoleNotSpecified { + get { + return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + } + } + + internal static string CurrentSettings { + get { + return ResourceManager.GetString("CurrentSettings", resourceCulture); + } + } + + internal static string SettingsLang { + get { + return ResourceManager.GetString("SettingsLang", resourceCulture); + } + } + + internal static string SettingsPrefix { + get { + return ResourceManager.GetString("SettingsPrefix", resourceCulture); + } + } + + internal static string SettingsRemoveRolesOnMute { + get { + return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); + } + } + + internal static string SettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); + } + } + + internal static string SettingsStarterRole { + get { + return ResourceManager.GetString("SettingsStarterRole", resourceCulture); + } + } + + internal static string SettingsMuteRole { + get { + return ResourceManager.GetString("SettingsMuteRole", resourceCulture); + } + } + + internal static string SettingsAdminLogChannel { + get { + return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); + } + } + + internal static string SettingsBotLogChannel { + get { + return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); + } + } + + internal static string LanguageNotSupported { + get { + return ResourceManager.GetString("LanguageNotSupported", resourceCulture); + } + } + + internal static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } + + internal static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + + internal static string UserNotBanned { + get { + return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + + internal static string MemberNotMuted { + get { + return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + + internal static string RolesReturned { + get { + return ResourceManager.GetString("RolesReturned", resourceCulture); + } + } + + internal static string SettingsWelcomeMessage { + get { + return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); + } + } + + internal static string ClearAmountInvalid { + get { + return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); + } + } + + internal static string FeedbackUserBanned { + get { + return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); + } + } + + internal static string UserNotInGuild { + get { + return ResourceManager.GetString("UserNotInGuild", resourceCulture); + } + } + + internal static string SettingDoesntExist { + get { + return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + } + } + + internal static string SettingsReceiveStartupMessages { + get { + return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); + } + } + + internal static string InvalidSettingValue { + get { + return ResourceManager.GetString("InvalidSettingValue", resourceCulture); + } + } + + internal static string InvalidRole { + get { + return ResourceManager.GetString("InvalidRole", resourceCulture); + } + } + + internal static string InvalidChannel { + get { + return ResourceManager.GetString("InvalidChannel", resourceCulture); + } + } + + internal static string RoleRemovalFailed { + get { + return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); + } + } + + internal static string DurationRequiredForTimeOuts { + get { + return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); + } + } + + internal static string CannotTimeOutBot { + get { + return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); + } + } + + internal static string EventCreated { + get { + return ResourceManager.GetString("EventCreated", resourceCulture); + } + } + + internal static string SettingsEventNotifyReceiverRole { + get { + return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); + } + } + + internal static string SettingsEventCreatedChannel { + get { + return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); + } + } + + internal static string SettingsEventStartedChannel { + get { + return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); + } + } + + internal static string SettingsEventStartedReceivers { + get { + return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); + } + } + + internal static string EventStarted { + get { + return ResourceManager.GetString("EventStarted", resourceCulture); + } + } + + internal static string SettingsFrowningFace { + get { + return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); + } + } + + internal static string EventCancelled { + get { + return ResourceManager.GetString("EventCancelled", resourceCulture); + } + } + + internal static string SettingsEventCancelledChannel { + get { + return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); + } + } + + internal static string SettingsEventCompletedChannel { + get { + return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); + } + } + + internal static string EventCompleted { + get { + return ResourceManager.GetString("EventCompleted", resourceCulture); + } + } + + internal static string FeedbackFormat { + get { + return ResourceManager.GetString("FeedbackFormat", resourceCulture); + } + } + + internal static string Ever { + get { + return ResourceManager.GetString("Ever", resourceCulture); + } + } + + internal static string FeedbackMessagesCleared { + get { + return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); + } + } + + internal static string FeedbackMemberKicked { + get { + return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); + } + } + + internal static string FeedbackMemberMuted { + get { + return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); + } + } + + internal static string FeedbackUserUnbanned { + get { + return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); + } + } + + internal static string FeedbackMemberUnmuted { + get { + return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); + } + } + + internal static string SettingsNothingChanged { + get { + return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); + } + } + + internal static string SettingNotDefined { + get { + return ResourceManager.GetString("SettingNotDefined", resourceCulture); + } + } + + internal static string FeedbackSettingsUpdated { + get { + return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); + } + } + + internal static string CommandDescriptionBan { + get { + return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); + } + } + + internal static string CommandDescriptionClear { + get { + return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); + } + } + + internal static string CommandDescriptionHelp { + get { + return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); + } + } + + internal static string CommandDescriptionKick { + get { + return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); + } + } + + internal static string CommandDescriptionMute { + get { + return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); + } + } + + internal static string CommandDescriptionPing { + get { + return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); + } + } + + internal static string CommandDescriptionSettings { + get { + return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); + } + } + + internal static string CommandDescriptionUnban { + get { + return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); + } + } + + internal static string CommandDescriptionUnmute { + get { + return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); + } + } + + internal static string MissingNumber { + get { + return ResourceManager.GetString("MissingNumber", resourceCulture); + } + } + + internal static string MissingUser { + get { + return ResourceManager.GetString("MissingUser", resourceCulture); + } + } + + internal static string InvalidUser { + get { + return ResourceManager.GetString("InvalidUser", resourceCulture); + } + } + + internal static string MissingMember { + get { + return ResourceManager.GetString("MissingMember", resourceCulture); + } + } + + internal static string InvalidMember { + get { + return ResourceManager.GetString("InvalidMember", resourceCulture); + } + } + + internal static string UserCannotBanMembers { + get { + return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); + } + } + + internal static string UserCannotManageMessages { + get { + return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); + } + } + + internal static string UserCannotKickMembers { + get { + return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); + } + } + + internal static string UserCannotModerateMembers { + get { + return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); + } + } + + internal static string UserCannotManageGuild { + get { + return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); + } + } + + internal static string BotCannotBanMembers { + get { + return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); + } + } + + internal static string BotCannotManageMessages { + get { + return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); + } + } + + internal static string BotCannotKickMembers { + get { + return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); + } + } + + internal static string BotCannotModerateMembers { + get { + return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); + } + } + + internal static string BotCannotManageGuild { + get { + return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); + } + } + + internal static string MissingBanReason { + get { + return ResourceManager.GetString("MissingBanReason", resourceCulture); + } + } + + internal static string MissingKickReason { + get { + return ResourceManager.GetString("MissingKickReason", resourceCulture); + } + } + + internal static string MissingMuteReason { + get { + return ResourceManager.GetString("MissingMuteReason", resourceCulture); + } + } + + internal static string MissingUnbanReason { + get { + return ResourceManager.GetString("MissingUnbanReason", resourceCulture); + } + } + + internal static string MissingUnmuteReason { + get { + return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); + } + } + + internal static string MissingSetting { + get { + return ResourceManager.GetString("MissingSetting", resourceCulture); + } + } + + internal static string UserCannotBanOwner { + get { + return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); + } + } + + internal static string UserCannotBanThemselves { + get { + return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); + } + } + + internal static string UserCannotBanBot { + get { + return ResourceManager.GetString("UserCannotBanBot", resourceCulture); + } + } + + internal static string BotCannotBanTarget { + get { + return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); + } + } + + internal static string UserCannotBanTarget { + get { + return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); + } + } + + internal static string UserCannotKickOwner { + get { + return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); + } + } + + internal static string UserCannotKickThemselves { + get { + return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); + } + } + + internal static string UserCannotKickBot { + get { + return ResourceManager.GetString("UserCannotKickBot", resourceCulture); + } + } + + internal static string BotCannotKickTarget { + get { + return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); + } + } + + internal static string UserCannotKickTarget { + get { + return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); + } + } + + internal static string UserCannotMuteOwner { + get { + return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); + } + } + + internal static string UserCannotMuteThemselves { + get { + return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); + } + } + + internal static string UserCannotMuteBot { + get { + return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); + } + } + + internal static string BotCannotMuteTarget { + get { + return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); + } + } + + internal static string UserCannotMuteTarget { + get { + return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); + } + } + + internal static string UserCannotUnmuteOwner { + get { + return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); + } + } + + internal static string UserCannotUnmuteThemselves { + get { + return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); + } + } + + internal static string UserCannotUnmuteBot { + get { + return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); + } + } + + internal static string BotCannotUnmuteTarget { + get { + return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); + } + } + + internal static string UserCannotUnmuteTarget { + get { + return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); + } + } + + internal static string CommandDescriptionCavepleaselisten { + get { + return ResourceManager.GetString("CommandDescriptionCavepleaselisten", resourceCulture); + } + } + + internal static string ServerBlacklisted { + get { + return ResourceManager.GetString("ServerBlacklisted", resourceCulture); + } + } + + internal static string EventEarlyNotification { + get { + return ResourceManager.GetString("EventEarlyNotification", resourceCulture); + } + } + + internal static string SettingsEventEarlyNotificationOffset { + get { + return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); + } + } } } diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 6615656..a06e8cc 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -166,7 +166,7 @@ I cannot use time-outs on other bots! Try to set a mute role in settings - {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6} + {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} Role for event creation notifications diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 5f92c9d..b593ef5 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -157,7 +157,7 @@ Начальная роль - {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} + {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} Роль для уведомлений о создании событий diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index d5ecf15..4917af6 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -153,7 +153,7 @@ базовое звание - {1}{2} приготовил новый квест {3}! он пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} + {1}{2} приготовил новый квест {3}! он пройдёт в {4} и начнётся <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} роль для уведомлений о создании квеста From 7ca540dabef4ca0ad7239f3c3fa02624d865b700 Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Sun, 23 Oct 2022 16:08:32 +0500 Subject: [PATCH 035/329] Delete a leftover file This was created while resolving merge conflicts via applying commits from #6 on top of current `master` using patches --- ...event-link-display-for-created-event.patch | 1482 ----------------- 1 file changed, 1482 deletions(-) delete mode 100644 0001-Added-event-link-display-for-created-event.patch diff --git a/0001-Added-event-link-display-for-created-event.patch b/0001-Added-event-link-display-for-created-event.patch deleted file mode 100644 index b94b298..0000000 --- a/0001-Added-event-link-display-for-created-event.patch +++ /dev/null @@ -1,1482 +0,0 @@ -From 14795337605f14acecb1617e6477c307c4802528 Mon Sep 17 00:00:00 2001 -From: mctaylors -Date: Sun, 23 Oct 2022 13:39:59 +0300 -Subject: [PATCH] Added event link display for created event - ---- - Boyfriend/EventHandler.cs | 2 +- - Boyfriend/Messages.Designer.cs | 884 ++++++++++----------------------- - Boyfriend/Messages.resx | 2 +- - Boyfriend/Messages.ru.resx | 2 +- - Boyfriend/Messages.tt-ru.resx | 5 +- - 5 files changed, 259 insertions(+), 636 deletions(-) - -diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs -index 6ba2bdf..25a4410 100644 ---- a/Boyfriend/EventHandler.cs -+++ b/Boyfriend/EventHandler.cs -@@ -129,7 +129,7 @@ public static class EventHandler { - await Utils.SilentSendAsync(channel, - string.Format(Messages.EventCreated, "\n", roleMention, scheduledEvent.Creator.Mention, - Utils.Wrap(scheduledEvent.Name), location, -- scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description)), -+ scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description), guild.Id, scheduledEvent.Id), - true); - } - if (eventConfig["EventEarlyNotificationOffset"] != "0") -diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs -index 0e8babe..093cc8b 100644 ---- a/Boyfriend/Messages.Designer.cs -+++ b/Boyfriend/Messages.Designer.cs -@@ -11,46 +11,32 @@ namespace Boyfriend { - using System; - - -- /// -- /// A strongly-typed resource class, for looking up localized strings, etc. -- /// -- // This class was auto-generated by the StronglyTypedResourceBuilder -- // class via a tool like ResGen or Visual Studio. -- // To add or remove a member, edit your .ResX file then rerun ResGen -- // with the /str option, or rebuild your VS project. -- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] -- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] -- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] -+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] -+ [System.Diagnostics.DebuggerNonUserCodeAttribute()] -+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Messages { - -- private static global::System.Resources.ResourceManager resourceMan; -+ private static System.Resources.ResourceManager resourceMan; - -- private static global::System.Globalization.CultureInfo resourceCulture; -+ private static System.Globalization.CultureInfo resourceCulture; - -- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] -+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Messages() { - } - -- /// -- /// Returns the cached ResourceManager instance used by this class. -- /// -- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] -- internal static global::System.Resources.ResourceManager ResourceManager { -+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] -+ internal static System.Resources.ResourceManager ResourceManager { - get { -- if (object.ReferenceEquals(resourceMan, null)) { -- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); -+ if (object.Equals(null, resourceMan)) { -+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - -- /// -- /// Overrides the current thread's CurrentUICulture property for all -- /// resource lookups using this strongly typed resource class. -- /// -- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] -- internal static global::System.Globalization.CultureInfo Culture { -+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] -+ internal static System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } -@@ -59,1101 +45,735 @@ namespace Boyfriend { - } - } - -- /// -- /// Looks up a localized string similar to Too many mentions in 1 message. -- /// -- internal static string AutobanReason { -+ internal static string Ready { - get { -- return ResourceManager.GetString("AutobanReason", resourceCulture); -+ return ResourceManager.GetString("Ready", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Bah! . -- /// -- internal static string Beep1 { -+ internal static string CachedMessageDeleted { - get { -- return ResourceManager.GetString("Beep1", resourceCulture); -+ return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Bop! . -- /// -- internal static string Beep2 { -+ internal static string AutobanReason { - get { -- return ResourceManager.GetString("Beep2", resourceCulture); -+ return ResourceManager.GetString("AutobanReason", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Beep! . -- /// -- internal static string Beep3 { -+ internal static string CachedMessageEdited { - get { -- return ResourceManager.GetString("Beep3", resourceCulture); -+ return ResourceManager.GetString("CachedMessageEdited", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot ban users from this guild!. -- /// -- internal static string BotCannotBanMembers { -+ internal static string DefaultWelcomeMessage { - get { -- return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); -+ return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot ban this user!. -- /// -- internal static string BotCannotBanTarget { -+ internal static string Beep1 { - get { -- return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); -+ return ResourceManager.GetString("Beep1", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot kick members from this guild!. -- /// -- internal static string BotCannotKickMembers { -+ internal static string Beep2 { - get { -- return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); -+ return ResourceManager.GetString("Beep2", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot kick this member!. -- /// -- internal static string BotCannotKickTarget { -+ internal static string Beep3 { - get { -- return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); -+ return ResourceManager.GetString("Beep3", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot manage this guild!. -- /// -- internal static string BotCannotManageGuild { -+ internal static string CommandNoPermissionBot { - get { -- return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); -+ return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot manage messages in this guild!. -- /// -- internal static string BotCannotManageMessages { -+ internal static string CommandNoPermissionUser { - get { -- return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); -+ return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot moderate members in this guild!. -- /// -- internal static string BotCannotModerateMembers { -+ internal static string YouWereBanned { - get { -- return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); -+ return ResourceManager.GetString("YouWereBanned", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot mute this member!. -- /// -- internal static string BotCannotMuteTarget { -+ internal static string PunishmentExpired { - get { -- return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); -+ return ResourceManager.GetString("PunishmentExpired", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot unmute this member!. -- /// -- internal static string BotCannotUnmuteTarget { -+ internal static string ClearAmountTooSmall { - get { -- return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); -+ return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. -- /// -- internal static string CachedMessageDeleted { -+ internal static string ClearAmountTooLarge { - get { -- return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); -+ return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. -- /// -- internal static string CachedMessageEdited { -+ internal static string CommandHelp { - get { -- return ResourceManager.GetString("CachedMessageEdited", resourceCulture); -+ return ResourceManager.GetString("CommandHelp", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. -- /// -- internal static string CannotTimeOutBot { -+ internal static string YouWereKicked { - get { -- return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); -+ return ResourceManager.GetString("YouWereKicked", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Not specified. -- /// -- internal static string ChannelNotSpecified { -+ internal static string Milliseconds { - get { -- return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); -+ return ResourceManager.GetString("Milliseconds", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify an integer from {0} to {1} instead of {2}!. -- /// -- internal static string ClearAmountInvalid { -+ internal static string MemberAlreadyMuted { - get { -- return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); -+ return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You specified more than {0} messages!. -- /// -- internal static string ClearAmountTooLarge { -+ internal static string ChannelNotSpecified { - get { -- return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); -+ return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You specified less than {0} messages!. -- /// -- internal static string ClearAmountTooSmall { -+ internal static string RoleNotSpecified { - get { -- return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); -+ return ResourceManager.GetString("RoleNotSpecified", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Bans a user. -- /// -- internal static string CommandDescriptionBan { -+ internal static string CurrentSettings { - get { -- return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); -+ return ResourceManager.GetString("CurrentSettings", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to We do not support hate towards our fellow members. And sometimes, we are not able to ban the offender.. -- /// -- internal static string CommandDescriptionCavepleaselisten { -+ internal static string SettingsLang { - get { -- return ResourceManager.GetString("CommandDescriptionCavepleaselisten", resourceCulture); -+ return ResourceManager.GetString("SettingsLang", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. -- /// -- internal static string CommandDescriptionClear { -+ internal static string SettingsPrefix { - get { -- return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); -+ return ResourceManager.GetString("SettingsPrefix", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Shows this message. -- /// -- internal static string CommandDescriptionHelp { -+ internal static string SettingsRemoveRolesOnMute { - get { -- return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); -+ return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Kicks a member. -- /// -- internal static string CommandDescriptionKick { -+ internal static string SettingsSendWelcomeMessages { - get { -- return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); -+ return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Mutes a member. -- /// -- internal static string CommandDescriptionMute { -+ internal static string SettingsStarterRole { - get { -- return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); -+ return ResourceManager.GetString("SettingsStarterRole", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Shows (inaccurate) latency. -- /// -- internal static string CommandDescriptionPing { -+ internal static string SettingsMuteRole { - get { -- return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); -+ return ResourceManager.GetString("SettingsMuteRole", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Allows you to change certain preferences for this guild. -- /// -- internal static string CommandDescriptionSettings { -+ internal static string SettingsAdminLogChannel { - get { -- return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); -+ return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Unbans a user. -- /// -- internal static string CommandDescriptionUnban { -+ internal static string SettingsBotLogChannel { - get { -- return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); -+ return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Unmutes a member. -- /// -- internal static string CommandDescriptionUnmute { -+ internal static string LanguageNotSupported { - get { -- return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); -+ return ResourceManager.GetString("LanguageNotSupported", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Command help:. -- /// -- internal static string CommandHelp { -+ internal static string Yes { - get { -- return ResourceManager.GetString("CommandHelp", resourceCulture); -+ return ResourceManager.GetString("Yes", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I do not have permission to execute this command!. -- /// -- internal static string CommandNoPermissionBot { -+ internal static string No { - get { -- return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); -+ return ResourceManager.GetString("No", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You do not have permission to execute this command!. -- /// -- internal static string CommandNoPermissionUser { -+ internal static string UserNotBanned { - get { -- return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); -+ return ResourceManager.GetString("UserNotBanned", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Current settings:. -- /// -- internal static string CurrentSettings { -+ internal static string MemberNotMuted { - get { -- return ResourceManager.GetString("CurrentSettings", resourceCulture); -+ return ResourceManager.GetString("MemberNotMuted", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to {0}, welcome to {1}. -- /// -- internal static string DefaultWelcomeMessage { -+ internal static string RolesReturned { - get { -- return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); -+ return ResourceManager.GetString("RolesReturned", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to 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. -- /// -- internal static string DurationRequiredForTimeOuts { -+ internal static string SettingsWelcomeMessage { - get { -- return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); -+ return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Event {0} is cancelled!{1}. -- /// -- internal static string EventCancelled { -+ internal static string ClearAmountInvalid { - get { -- return ResourceManager.GetString("EventCancelled", resourceCulture); -+ return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Event {0} has completed! Duration: {1}. -- /// -- internal static string EventCompleted { -+ internal static string FeedbackUserBanned { - get { -- return ResourceManager.GetString("EventCompleted", resourceCulture); -+ return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}. -- /// -- internal static string EventCreated { -+ internal static string UserNotInGuild { - get { -- return ResourceManager.GetString("EventCreated", resourceCulture); -+ return ResourceManager.GetString("UserNotInGuild", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to {0}Event {1} will start <t:{2}:R>!. -- /// -- internal static string EventEarlyNotification { -+ internal static string SettingDoesntExist { - get { -- return ResourceManager.GetString("EventEarlyNotification", resourceCulture); -+ return ResourceManager.GetString("SettingDoesntExist", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. -- /// -- internal static string EventStarted { -+ internal static string SettingsReceiveStartupMessages { - get { -- return ResourceManager.GetString("EventStarted", resourceCulture); -+ return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to ever. -- /// -- internal static string Ever { -+ internal static string InvalidSettingValue { - get { -- return ResourceManager.GetString("Ever", resourceCulture); -+ return ResourceManager.GetString("InvalidSettingValue", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to *[{0}: {1}]*. -- /// -- internal static string FeedbackFormat { -+ internal static string InvalidRole { - get { -- return ResourceManager.GetString("FeedbackFormat", resourceCulture); -+ return ResourceManager.GetString("InvalidRole", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Kicked {0}: {1}. -- /// -- internal static string FeedbackMemberKicked { -+ internal static string InvalidChannel { - get { -- return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); -+ return ResourceManager.GetString("InvalidChannel", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Muted {0} for{1}: {2}. -- /// -- internal static string FeedbackMemberMuted { -+ internal static string RoleRemovalFailed { - get { -- return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); -+ return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Unmuted {0}: {1}. -- /// -- internal static string FeedbackMemberUnmuted { -+ internal static string DurationRequiredForTimeOuts { - get { -- return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); -+ return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Deleted {0} messages in {1}. -- /// -- internal static string FeedbackMessagesCleared { -+ internal static string CannotTimeOutBot { - get { -- return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); -+ return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. -- /// -- internal static string FeedbackSettingsUpdated { -+ internal static string EventCreated { - get { -- return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); -+ return ResourceManager.GetString("EventCreated", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Banned {0} for{1}: {2}. -- /// -- internal static string FeedbackUserBanned { -+ internal static string SettingsEventNotifyReceiverRole { - get { -- return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); -+ return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Unbanned {0}: {1}. -- /// -- internal static string FeedbackUserUnbanned { -+ internal static string SettingsEventCreatedChannel { - get { -- return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); -+ return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to This channel does not exist!. -- /// -- internal static string InvalidChannel { -+ internal static string SettingsEventStartedChannel { - get { -- return ResourceManager.GetString("InvalidChannel", resourceCulture); -+ return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a guild member instead of {0}!. -- /// -- internal static string InvalidMember { -+ internal static string SettingsEventStartedReceivers { - get { -- return ResourceManager.GetString("InvalidMember", resourceCulture); -+ return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to This role does not exist!. -- /// -- internal static string InvalidRole { -+ internal static string EventStarted { - get { -- return ResourceManager.GetString("InvalidRole", resourceCulture); -+ return ResourceManager.GetString("EventStarted", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Invalid setting value specified!. -- /// -- internal static string InvalidSettingValue { -+ internal static string SettingsFrowningFace { - get { -- return ResourceManager.GetString("InvalidSettingValue", resourceCulture); -+ return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a user instead of {0}!. -- /// -- internal static string InvalidUser { -+ internal static string EventCancelled { - get { -- return ResourceManager.GetString("InvalidUser", resourceCulture); -+ return ResourceManager.GetString("EventCancelled", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Language not supported!. -- /// -- internal static string LanguageNotSupported { -+ internal static string SettingsEventCancelledChannel { - get { -- return ResourceManager.GetString("LanguageNotSupported", resourceCulture); -+ return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Member is already muted!. -- /// -- internal static string MemberAlreadyMuted { -+ internal static string SettingsEventCompletedChannel { - get { -- return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); -+ return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Member not muted!. -- /// -- internal static string MemberNotMuted { -+ internal static string EventCompleted { - get { -- return ResourceManager.GetString("MemberNotMuted", resourceCulture); -+ return ResourceManager.GetString("EventCompleted", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to ms. -- /// -- internal static string Milliseconds { -+ internal static string FeedbackFormat { - get { -- return ResourceManager.GetString("Milliseconds", resourceCulture); -+ return ResourceManager.GetString("FeedbackFormat", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a reason to ban this user!. -- /// -- internal static string MissingBanReason { -+ internal static string Ever { - get { -- return ResourceManager.GetString("MissingBanReason", resourceCulture); -+ return ResourceManager.GetString("Ever", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a reason to kick this member!. -- /// -- internal static string MissingKickReason { -+ internal static string FeedbackMessagesCleared { - get { -- return ResourceManager.GetString("MissingKickReason", resourceCulture); -+ return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a guild member!. -- /// -- internal static string MissingMember { -+ internal static string FeedbackMemberKicked { - get { -- return ResourceManager.GetString("MissingMember", resourceCulture); -+ return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a reason to mute this member!. -- /// -- internal static string MissingMuteReason { -+ internal static string FeedbackMemberMuted { - get { -- return ResourceManager.GetString("MissingMuteReason", resourceCulture); -+ return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. -- /// -- internal static string MissingNumber { -+ internal static string FeedbackUserUnbanned { - get { -- return ResourceManager.GetString("MissingNumber", resourceCulture); -+ return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a setting to change!. -- /// -- internal static string MissingSetting { -+ internal static string FeedbackMemberUnmuted { - get { -- return ResourceManager.GetString("MissingSetting", resourceCulture); -+ return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a reason to unban this user!. -- /// -- internal static string MissingUnbanReason { -+ internal static string SettingsNothingChanged { - get { -- return ResourceManager.GetString("MissingUnbanReason", resourceCulture); -+ return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a reason for unmute this member!. -- /// -- internal static string MissingUnmuteReason { -+ internal static string SettingNotDefined { - get { -- return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); -+ return ResourceManager.GetString("SettingNotDefined", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You need to specify a user!. -- /// -- internal static string MissingUser { -+ internal static string FeedbackSettingsUpdated { - get { -- return ResourceManager.GetString("MissingUser", resourceCulture); -+ return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to No. -- /// -- internal static string No { -+ internal static string CommandDescriptionBan { - get { -- return ResourceManager.GetString("No", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Punishment expired. -- /// -- internal static string PunishmentExpired { -+ internal static string CommandDescriptionClear { - get { -- return ResourceManager.GetString("PunishmentExpired", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to {0}I'm ready! (C#). -- /// -- internal static string Ready { -+ internal static string CommandDescriptionHelp { - get { -- return ResourceManager.GetString("Ready", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Not specified. -- /// -- internal static string RoleNotSpecified { -+ internal static string CommandDescriptionKick { - get { -- return ResourceManager.GetString("RoleNotSpecified", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to I couldn't remove role {0} because of an error! {1}. -- /// -- internal static string RoleRemovalFailed { -+ internal static string CommandDescriptionMute { - get { -- return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionMute", 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 { -+ internal static string CommandDescriptionPing { - get { -- return ResourceManager.GetString("RolesReturned", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to This feature is unavailable because this guild is currently blacklisted.. -- /// -- internal static string ServerBlacklisted { -+ internal static string CommandDescriptionSettings { - get { -- return ResourceManager.GetString("ServerBlacklisted", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to That setting doesn't exist!. -- /// -- internal static string SettingDoesntExist { -+ internal static string CommandDescriptionUnban { - get { -- return ResourceManager.GetString("SettingDoesntExist", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Not specified. -- /// -- internal static string SettingNotDefined { -+ internal static string CommandDescriptionUnmute { - get { -- return ResourceManager.GetString("SettingNotDefined", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Admin log channel. -- /// -- internal static string SettingsAdminLogChannel { -+ internal static string MissingNumber { - get { -- return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); -+ return ResourceManager.GetString("MissingNumber", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Bot log channel. -- /// -- internal static string SettingsBotLogChannel { -+ internal static string MissingUser { - get { -- return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); -+ return ResourceManager.GetString("MissingUser", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Channel for event cancellation notifications. -- /// -- internal static string SettingsEventCancelledChannel { -+ internal static string InvalidUser { - get { -- return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); -+ return ResourceManager.GetString("InvalidUser", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Channel for event completion notifications. -- /// -- internal static string SettingsEventCompletedChannel { -+ internal static string MissingMember { - get { -- return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); -+ return ResourceManager.GetString("MissingMember", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Channel for event creation notifications. -- /// -- internal static string SettingsEventCreatedChannel { -+ internal static string InvalidMember { - get { -- return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); -+ return ResourceManager.GetString("InvalidMember", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to . -- /// -- internal static string SettingsEventEarlyNotificationOffset { -+ internal static string UserCannotBanMembers { - get { -- return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); -+ return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Role for event creation notifications. -- /// -- internal static string SettingsEventNotifyReceiverRole { -+ internal static string UserCannotManageMessages { - get { -- return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); -+ return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Channel for event start notifications. -- /// -- internal static string SettingsEventStartedChannel { -+ internal static string UserCannotKickMembers { - get { -- return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); -+ return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Event start notifications receivers. -- /// -- internal static string SettingsEventStartedReceivers { -+ internal static string UserCannotModerateMembers { - get { -- return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); -+ return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to :(. -- /// -- internal static string SettingsFrowningFace { -+ internal static string UserCannotManageGuild { - get { -- return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); -+ return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Language. -- /// -- internal static string SettingsLang { -+ internal static string BotCannotBanMembers { - get { -- return ResourceManager.GetString("SettingsLang", resourceCulture); -+ return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Mute role. -- /// -- internal static string SettingsMuteRole { -+ internal static string BotCannotManageMessages { - get { -- return ResourceManager.GetString("SettingsMuteRole", resourceCulture); -+ return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. -- /// -- internal static string SettingsNothingChanged { -+ internal static string BotCannotKickMembers { - get { -- return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); -+ return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Prefix. -- /// -- internal static string SettingsPrefix { -+ internal static string BotCannotModerateMembers { - get { -- return ResourceManager.GetString("SettingsPrefix", resourceCulture); -+ return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Receive startup messages. -- /// -- internal static string SettingsReceiveStartupMessages { -+ internal static string BotCannotManageGuild { - get { -- return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); -+ return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Remove roles on mute. -- /// -- internal static string SettingsRemoveRolesOnMute { -+ internal static string MissingBanReason { - get { -- return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); -+ return ResourceManager.GetString("MissingBanReason", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Send welcome messages. -- /// -- internal static string SettingsSendWelcomeMessages { -+ internal static string MissingKickReason { - get { -- return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); -+ return ResourceManager.GetString("MissingKickReason", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Starter role. -- /// -- internal static string SettingsStarterRole { -+ internal static string MissingMuteReason { - get { -- return ResourceManager.GetString("SettingsStarterRole", resourceCulture); -+ return ResourceManager.GetString("MissingMuteReason", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Welcome message. -- /// -- internal static string SettingsWelcomeMessage { -+ internal static string MissingUnbanReason { - get { -- return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); -+ return ResourceManager.GetString("MissingUnbanReason", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot ban me!. -- /// -- internal static string UserCannotBanBot { -+ internal static string MissingUnmuteReason { - get { -- return ResourceManager.GetString("UserCannotBanBot", resourceCulture); -+ return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot ban users from this guild!. -- /// -- internal static string UserCannotBanMembers { -+ internal static string MissingSetting { - get { -- return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); -+ return ResourceManager.GetString("MissingSetting", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot ban the owner of this guild!. -- /// - internal static string UserCannotBanOwner { - get { - return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot ban this user!. -- /// -- internal static string UserCannotBanTarget { -- get { -- return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); -- } -- } -- -- /// -- /// Looks up a localized string similar to You cannot ban yourself!. -- /// - internal static string UserCannotBanThemselves { - get { - return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot kick me!. -- /// -- internal static string UserCannotKickBot { -+ internal static string UserCannotBanBot { - get { -- return ResourceManager.GetString("UserCannotKickBot", resourceCulture); -+ return ResourceManager.GetString("UserCannotBanBot", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot kick members from this guild!. -- /// -- internal static string UserCannotKickMembers { -+ internal static string BotCannotBanTarget { - get { -- return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); -+ return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot kick the owner of this guild!. -- /// -- internal static string UserCannotKickOwner { -+ internal static string UserCannotBanTarget { - get { -- return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); -+ return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot kick this member!. -- /// -- internal static string UserCannotKickTarget { -+ internal static string UserCannotKickOwner { - get { -- return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); -+ return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot kick yourself!. -- /// - internal static string UserCannotKickThemselves { - get { - return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot manage this guild!. -- /// -- internal static string UserCannotManageGuild { -+ internal static string UserCannotKickBot { - get { -- return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); -+ return ResourceManager.GetString("UserCannotKickBot", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot manage messages in this guild!. -- /// -- internal static string UserCannotManageMessages { -+ internal static string BotCannotKickTarget { - get { -- return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); -+ return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot moderate members in this guild!. -- /// -- internal static string UserCannotModerateMembers { -+ internal static string UserCannotKickTarget { - get { -- return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); -+ return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot mute me!. -- /// -- internal static string UserCannotMuteBot { -+ internal static string UserCannotMuteOwner { - get { -- return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); -+ return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot mute the owner of this guild!. -- /// -- internal static string UserCannotMuteOwner { -+ internal static string UserCannotMuteThemselves { - get { -- return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); -+ return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot mute this member!. -- /// -- internal static string UserCannotMuteTarget { -+ internal static string UserCannotMuteBot { - get { -- return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); -+ return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot mute yourself!. -- /// -- internal static string UserCannotMuteThemselves { -+ internal static string BotCannotMuteTarget { - get { -- return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); -+ return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to .... -- /// -- internal static string UserCannotUnmuteBot { -+ internal static string UserCannotMuteTarget { - get { -- return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); -+ return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. -- /// - internal static string UserCannotUnmuteOwner { - get { - return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You cannot unmute this user!. -- /// -- internal static string UserCannotUnmuteTarget { -+ internal static string UserCannotUnmuteThemselves { - get { -- return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); -+ return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You are muted!. -- /// -- internal static string UserCannotUnmuteThemselves { -+ internal static string UserCannotUnmuteBot { - get { -- return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); -+ return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to This user is not banned!. -- /// -- internal static string UserNotBanned { -+ internal static string BotCannotUnmuteTarget { - get { -- return ResourceManager.GetString("UserNotBanned", resourceCulture); -+ return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to The specified user is not a member of this server!. -- /// -- internal static string UserNotInGuild { -+ internal static string UserCannotUnmuteTarget { - get { -- return ResourceManager.GetString("UserNotInGuild", resourceCulture); -+ return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to Yes. -- /// -- internal static string Yes { -+ internal static string CommandDescriptionCavepleaselisten { - get { -- return ResourceManager.GetString("Yes", resourceCulture); -+ return ResourceManager.GetString("CommandDescriptionCavepleaselisten", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. -- /// -- internal static string YouWereBanned { -+ internal static string ServerBlacklisted { - get { -- return ResourceManager.GetString("YouWereBanned", resourceCulture); -+ return ResourceManager.GetString("ServerBlacklisted", resourceCulture); - } - } - -- /// -- /// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. -- /// -- internal static string YouWereKicked { -+ internal static string EventEarlyNotification { - get { -- return ResourceManager.GetString("YouWereKicked", resourceCulture); -+ return ResourceManager.GetString("EventEarlyNotification", resourceCulture); -+ } -+ } -+ -+ internal static string SettingsEventEarlyNotificationOffset { -+ get { -+ return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); - } - } - } -diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx -index 6615656..a06e8cc 100644 ---- a/Boyfriend/Messages.resx -+++ b/Boyfriend/Messages.resx -@@ -166,7 +166,7 @@ - I cannot use time-outs on other bots! Try to set a mute role in settings - - -- {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6} -+ {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} - - - Role for event creation notifications -diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx -index 5f92c9d..b593ef5 100644 ---- a/Boyfriend/Messages.ru.resx -+++ b/Boyfriend/Messages.ru.resx -@@ -157,7 +157,7 @@ - Начальная роль - - -- {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} -+ {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} - - - Роль для уведомлений о создании событий -diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx -index 0d676cd..b49d3ef 100644 ---- a/Boyfriend/Messages.tt-ru.resx -+++ b/Boyfriend/Messages.tt-ru.resx -@@ -153,7 +153,7 @@ - базовое звание - - -- {1}{2} приготовил новый квест {3}! он пройдёт в {4} и начнётся <t:{5}:R>!{0}{6} -+ {1}{2} приготовил новый квест {3}! он пройдёт в {4} и начнётся <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} - - - роль для уведомлений о создании квеста -@@ -371,4 +371,7 @@ - - упс, кажется ваш сервер в черном списке, и я вам ничем помочь не смогу) - -+ -+ {0}квест {1} начнется<t:{2}:R>! -+ - -\ No newline at end of file --- -2.34.1.windows.1 - From 857047c77cf7a25ce0b0a6584940d6ff8bc53c6a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 25 Oct 2022 22:57:25 +0500 Subject: [PATCH 036/329] Update the song list Candy-Coated Rocks is the best tho --- Boyfriend/Boyfriend.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 4528e32..d757ccc 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -20,9 +20,13 @@ public static class Boyfriend { }; private static readonly List> ActivityList = new() { - Tuple.Create(new Game("C418 - Mall", ActivityType.Listening), new TimeSpan(0, 3, 18)), - Tuple.Create(new Game("C418 - Thirteen", ActivityType.Listening), new TimeSpan(0, 2, 57)), - Tuple.Create(new Game("Spotify Ads", ActivityType.Listening), new TimeSpan(0, 0, 15)) + Tuple.Create(new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), + new TimeSpan(0, 3, 18)), + Tuple.Create(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)), + Tuple.Create(new Game("Kurokotei - Scattered Faith", ActivityType.Listening), new TimeSpan(0, 8, 21)), + Tuple.Create(new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), + Tuple.Create(new Game("RetroSpecter - Genocide", ActivityType.Listening), new TimeSpan(0, 5, 52)), + Tuple.Create(new Game("Dimrain47 - At the Speed of Light", ActivityType.Listening), new TimeSpan(0, 4, 10)) }; public static readonly DiscordSocketClient Client = new(Config); @@ -135,6 +139,7 @@ public static class Boyfriend { return removedRoles; } + public static SocketGuild FindGuild(ulong channel) { if (GuildCache.TryGetValue(channel, out var gld)) return gld; foreach (var guild in Client.Guilds) { From 0e144db2e2da6023e1ebfaa0e791a465c5bf0ea0 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 11 Nov 2022 23:17:44 +0500 Subject: [PATCH 037/329] Delete the guild blacklist --- Boyfriend/CommandProcessor.cs | 13 +++---------- Boyfriend/EventHandler.cs | 32 ++++++++++++-------------------- Boyfriend/Utils.cs | 23 +++++++++-------------- 3 files changed, 24 insertions(+), 44 deletions(-) diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index 06bbe2e..9d3b7ea 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -59,8 +59,7 @@ public sealed class CommandProcessor { .GetUser(Context.User.Id); // Getting an up-to-date copy if (member == null || member.Roles.Contains(muteRole) || member.TimedOutUntil.GetValueOrDefault(DateTimeOffset.UnixEpoch).ToUnixTimeSeconds() > - DateTimeOffset.Now.ToUnixTimeSeconds()) - break; + DateTimeOffset.Now.ToUnixTimeSeconds()) break; } await Task.WhenAll(_tasks); @@ -77,10 +76,6 @@ public sealed class CommandProcessor { foreach (var command in Commands) { var lineNoMention = line.Remove(0, prefixed ? prefix.Length : Mention.Length); if (!command.Aliases.Contains(lineNoMention.Trim().Split()[0])) continue; - if (Utils.IsServerBlacklisted(Context.Guild)) { - _serverBlacklisted = true; - return; - } var args = lineNoMention.Trim().Split().Skip(1).ToArray(); var cleanArgs = cleanLine.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); @@ -123,8 +118,7 @@ public sealed class CommandProcessor { if (startIndex >= from.Length && argument != null) Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Utils.GetMessage($"Missing{argument}")}", Context.Message); - else - return string.Join(" ", from, startIndex, from.Length - startIndex); + else return string.Join(" ", from, startIndex, from.Length - startIndex); return null; } @@ -232,8 +226,7 @@ public sealed class CommandProcessor { public static TimeSpan GetTimeSpan(string[] args, int index) { var infinity = TimeSpan.FromMilliseconds(-1); - if (index >= args.Length) - return infinity; + if (index >= args.Length) return infinity; var chars = args[index].AsSpan(); var numberBuilder = Boyfriend.StringBuilder; int days = 0, hours = 0, minutes = 0, seconds = 0; diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 1c0a3a0..cd34119 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -30,8 +30,7 @@ public static class EventHandler { var channel = guild.GetTextChannel(Convert.ToUInt64(config["BotLogChannel"])); Utils.SetCurrentLanguage(guild.Id); - if (config["ReceiveStartupMessages"] is not "true" || channel == null || - Utils.IsServerBlacklisted(guild)) continue; + if (config["ReceiveStartupMessages"] is not "true" || channel == null) continue; _ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); } @@ -45,7 +44,6 @@ public static class EventHandler { if (msg is null or ISystemMessage || msg.Author.IsBot) return; var guild = Boyfriend.FindGuild(channel.Value.Id); - if (Utils.IsServerBlacklisted(guild)) return; Utils.SetCurrentLanguage(guild.Id); @@ -57,9 +55,9 @@ public static class EventHandler { if (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); + await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageDeleted, msg.Author.Mention, + Utils.MentionChannel(channel.Id), + Utils.Wrap(msg.CleanContent)), guild.Id, mention); } private static Task MessageReceivedEvent(SocketMessage messageParam) { @@ -87,59 +85,55 @@ public static class EventHandler { if (msg is null or ISystemMessage || msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return; var guild = Boyfriend.FindGuild(channel.Id); - if (Utils.IsServerBlacklisted(guild)) return; Utils.SetCurrentLanguage(guild.Id); var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940; - await Utils.SendFeedbackAsync( - string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id), + 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); } private static async Task UserJoinedEvent(SocketGuildUser user) { var guild = user.Guild; - if (Utils.IsServerBlacklisted(guild)) return; var config = Boyfriend.GetGuildConfig(guild.Id); if (config["SendWelcomeMessages"] is "true") await Utils.SilentSendAsync(guild.SystemChannel, string.Format(config["WelcomeMessage"], user.Mention, guild.Name)); - if (config["StarterRole"] is not "0") - await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); + if (config["StarterRole"] is not "0") await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); } private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; - if (Utils.IsServerBlacklisted(guild)) return; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCreatedChannel"])); if (channel != null) { var roleMention = ""; var role = guild.GetRole(Convert.ToUInt64(eventConfig["EventNotifyReceiverRole"])); - if (role != null) - roleMention = $"{role.Mention} "; + if (role != null) roleMention = $"{role.Mention} "; var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); await Utils.SilentSendAsync(channel, string.Format(Messages.EventCreated, "\n", roleMention, scheduledEvent.Creator.Mention, Utils.Wrap(scheduledEvent.Name), location, - scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description), guild.Id, scheduledEvent.Id), + scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description), + guild.Id, scheduledEvent.Id), true); } + if (eventConfig["EventEarlyNotificationOffset"] != "0") { - _ = Utils.SendEarlyEventStartNotificationAsync(channel, scheduledEvent, Convert.ToInt32(eventConfig["EventEarlyNotificationOffset"])); + _ = Utils.SendEarlyEventStartNotificationAsync(channel, scheduledEvent, + Convert.ToInt32(eventConfig["EventEarlyNotificationOffset"])); } } private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; - if (Utils.IsServerBlacklisted(guild)) return; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCancelledChannel"])); if (channel != null) @@ -149,7 +143,6 @@ public static class EventHandler { private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; - if (Utils.IsServerBlacklisted(guild)) return; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventStartedChannel"])); @@ -172,7 +165,6 @@ public static class EventHandler { private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; - if (Utils.IsServerBlacklisted(guild)) return; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCompletedChannel"])); if (channel != null) diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index fddfe76..b2c798f 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -59,8 +59,7 @@ public static class Utils { public static async Task SendDirectMessage(SocketUser user, string toSend) { try { await user.SendMessageAsync(toSend); } catch (HttpException e) { - if (e.DiscordCode != DiscordErrorCode.CannotSendMessageToUser) - throw; + if (e.DiscordCode != DiscordErrorCode.CannotSendMessageToUser) throw; } } @@ -117,10 +116,8 @@ public static class Utils { var adminChannel = GetAdminLogChannel(guildId); var systemChannel = Boyfriend.Client.GetGuild(guildId).SystemChannel; var toSend = string.Format(Messages.FeedbackFormat, mention, feedback); - if (adminChannel != null) - await SilentSendAsync(adminChannel, toSend); - if (sendPublic && systemChannel != null) - await SilentSendAsync(systemChannel, toSend); + if (adminChannel != null) await SilentSendAsync(adminChannel, toSend); + if (sendPublic && systemChannel != null) await SilentSendAsync(systemChannel, toSend); } public static string GetHumanizedTimeOffset(TimeSpan span) { @@ -163,13 +160,10 @@ public static class Utils { await UnmuteCommand.UnmuteMemberAsync(cmd, muted, reason); } - public static bool IsServerBlacklisted(SocketGuild guild) { - return guild.GetUser(196160375593369600) != null && guild.OwnerId != 326642240229474304 && - guild.OwnerId != 504343489664909322; - } - - public static async Task SendEarlyEventStartNotificationAsync(SocketTextChannel? channel, SocketGuildEvent scheduledEvent, int minuteOffset) { - await Task.Delay(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Subtract(TimeSpan.FromMinutes(minuteOffset))); + public static async Task SendEarlyEventStartNotificationAsync(SocketTextChannel? channel, + SocketGuildEvent scheduledEvent, int minuteOffset) { + 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); @@ -182,7 +176,8 @@ public static class Utils { 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()))!; + await channel?.SendMessageAsync(string.Format(Messages.EventEarlyNotification, mentions, + Wrap(scheduledEvent.Name), scheduledEvent.StartTime.ToUnixTimeSeconds()))!; mentions.Clear(); } } From 552c575dd211315641d8b524d829bdfdef801cde Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 12 Nov 2022 00:59:11 +0500 Subject: [PATCH 038/329] General code refactor (and a few breaking config changes) --- Boyfriend/Boyfriend.cs | 74 +- Boyfriend/CommandProcessor.cs | 36 +- Boyfriend/Commands/BanCommand.cs | 10 +- Boyfriend/Commands/ClearCommand.cs | 2 +- Boyfriend/Commands/KickCommand.cs | 9 +- Boyfriend/Commands/MuteCommand.cs | 15 +- Boyfriend/Commands/SettingsCommand.cs | 44 +- Boyfriend/Commands/UnbanCommand.cs | 6 +- Boyfriend/Commands/UnmuteCommand.cs | 9 +- Boyfriend/EventHandler.cs | 72 +- Boyfriend/Messages.Designer.cs | 1656 +++++++++++++++---------- Boyfriend/Messages.resx | 16 +- Boyfriend/Messages.ru.resx | 16 +- Boyfriend/Messages.tt-ru.resx | 26 +- Boyfriend/Utils.cs | 52 +- 15 files changed, 1164 insertions(+), 879 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index d757ccc..9bc80a9 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -8,11 +8,10 @@ namespace Boyfriend; public static class Boyfriend { public static readonly StringBuilder StringBuilder = new(); - private static readonly Dictionary GuildCache = new(); private static readonly DiscordSocketConfig Config = new() { MessageCacheSize = 250, - GatewayIntents = GatewayIntents.All, + GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers, AlwaysDownloadUsers = true, AlwaysResolveStickers = false, AlwaysDownloadDefaultStickers = false, @@ -37,23 +36,20 @@ public static class Boyfriend { new(); public static readonly Dictionary DefaultConfig = new() { - { "Lang", "en" }, { "Prefix", "!" }, - { "RemoveRolesOnMute", "false" }, - { "SendWelcomeMessages", "true" }, + { "Lang", "en" }, { "ReceiveStartupMessages", "false" }, - { "FrowningFace", "true" }, - { "WelcomeMessage", Messages.DefaultWelcomeMessage }, - { "EventStartedReceivers", "interested,role" }, + { "WelcomeMessage", "default" }, + { "SendWelcomeMessages", "true" }, + { "BotLogChannel", "0" }, { "StarterRole", "0" }, { "MuteRole", "0" }, - { "EventNotifyReceiverRole", "0" }, - { "AdminLogChannel", "0" }, - { "BotLogChannel", "0" }, - { "EventCreatedChannel", "0" }, - { "EventStartedChannel", "0" }, - { "EventCancelledChannel", "0" }, - { "EventCompletedChannel", "0" }, + { "RemoveRolesOnMute", "false" }, + { "FrowningFace", "true" }, + + { "EventStartedReceivers", "interested,role" }, + { "EventNotificationRole", "0" }, + { "EventNotificationChannel", "0" }, { "EventEarlyNotificationOffset", "0" } }; @@ -81,8 +77,29 @@ public static class Boyfriend { } private static Task Log(LogMessage msg) { - Console.WriteLine(msg.ToString()); + switch (msg.Severity) { + case LogSeverity.Critical: + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.Error.WriteLine(msg.ToString()); + break; + case LogSeverity.Error: + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine(msg.ToString()); + break; + case LogSeverity.Warning: + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(msg.ToString()); + break; + case LogSeverity.Info: + Console.WriteLine(msg.ToString()); + break; + case LogSeverity.Verbose: + case LogSeverity.Debug: + default: return Task.CompletedTask; + } + + Console.ResetColor(); return Task.CompletedTask; } @@ -95,9 +112,6 @@ public static class Boyfriend { } public static Dictionary GetGuildConfig(ulong id) { - if (!RemovedRolesDictionary.ContainsKey(id)) - RemovedRolesDictionary.Add(id, new Dictionary>()); - if (GuildConfigDictionary.TryGetValue(id, out var cfg)) return cfg; var path = $"config_{id}.json"; @@ -110,13 +124,12 @@ public static class Boyfriend { if (config.Keys.Count < DefaultConfig.Keys.Count) { // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - // Avoids a closure allocation with the config variable + // 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); + foreach (var key in config.Keys.Where(key => !DefaultConfig.ContainsKey(key))) config.Remove(key); } GuildConfigDictionary.Add(id, config); @@ -126,10 +139,9 @@ public static class Boyfriend { 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); + if (!File.Exists(path)) File.Create(path).Dispose(); var json = File.ReadAllText(path); var removedRoles = JsonConvert.DeserializeObject>>(json) @@ -139,18 +151,4 @@ public static class Boyfriend { return removedRoles; } - - public static SocketGuild FindGuild(ulong channel) { - if (GuildCache.TryGetValue(channel, out var gld)) return gld; - foreach (var guild in Client.Guilds) { - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var x in guild.Channels) - if (x.Id == channel) { - GuildCache.Add(channel, guild); - return guild; - } - } - - throw new Exception("Could not find guild by channel!"); - } } diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index 9d3b7ea..da21d00 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -27,7 +27,6 @@ public sealed class CommandProcessor { private readonly List _tasks = new(); public readonly SocketCommandContext Context; - private bool _serverBlacklisted; public bool ConfigWriteScheduled = false; @@ -39,6 +38,7 @@ public sealed class CommandProcessor { var guild = Context.Guild; var config = Boyfriend.GetGuildConfig(guild.Id); var muteRole = Utils.GetMuteRole(guild); + Utils.SetCurrentLanguage(guild.Id); if (GetMember().Roles.Contains(muteRole)) { await Context.Message.ReplyAsync(Messages.UserCannotUnmuteThemselves); @@ -49,15 +49,11 @@ public sealed class CommandProcessor { var cleanList = Context.Message.CleanContent.Split("\n"); for (var i = 0; i < list.Length; i++) { RunCommandOnLine(list[i], cleanList[i], config["Prefix"]); - if (_serverBlacklisted) { - await Context.Message.ReplyAsync(Messages.ServerBlacklisted); - return; - } if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync(); var member = Boyfriend.Client.GetGuild(Context.Guild.Id) .GetUser(Context.User.Id); // Getting an up-to-date copy - if (member == null || member.Roles.Contains(muteRole) + if (member is null || member.Roles.Contains(muteRole) || member.TimedOutUntil.GetValueOrDefault(DateTimeOffset.UnixEpoch).ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds()) break; } @@ -89,25 +85,25 @@ public sealed class CommandProcessor { } public void Audit(string action, bool isPublic = true) { - var format = string.Format(Messages.FeedbackFormat, Context.User.Mention, action); + var format = $"*[{Context.User.Mention}: {action}]*"; if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, Context.Guild.SystemChannel); - Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, Utils.GetAdminLogChannel(Context.Guild.Id)); - if (_tasks.Count == 0) SendFeedbacks(false); + Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, Utils.GetBotLogChannel(Context.Guild.Id)); + if (_tasks.Count is 0) SendFeedbacks(false); } private void SendFeedbacks(bool reply = true) { if (reply && _stackedReplyMessage.Length > 0) _ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None); - var adminChannel = Utils.GetAdminLogChannel(Context.Guild.Id); + var adminChannel = Utils.GetBotLogChannel(Context.Guild.Id); var systemChannel = Context.Guild.SystemChannel; - if (_stackedPrivateFeedback.Length > 0 && adminChannel != null && + if (_stackedPrivateFeedback.Length > 0 && adminChannel is not null && adminChannel.Id != Context.Message.Channel.Id) { _ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString()); _stackedPrivateFeedback.Clear(); } - if (_stackedPublicFeedback.Length > 0 && systemChannel != null && systemChannel.Id != adminChannel?.Id + if (_stackedPublicFeedback.Length > 0 && systemChannel is not null && systemChannel.Id != adminChannel?.Id && systemChannel.Id != Context.Message.Channel.Id) { _ = Utils.SilentSendAsync(systemChannel, _stackedPublicFeedback.ToString()); _stackedPublicFeedback.Clear(); @@ -115,7 +111,7 @@ public sealed class CommandProcessor { } public string? GetRemaining(string[] from, int startIndex, string? argument) { - if (startIndex >= from.Length && argument != null) + if (startIndex >= from.Length && argument is not null) Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Utils.GetMessage($"Missing{argument}")}", Context.Message); else return string.Join(" ", from, startIndex, from.Length - startIndex); @@ -129,8 +125,8 @@ public sealed class CommandProcessor { return null; } - var user = Utils.ParseUser(args[index]); - if (user == null && argument != null) + var user = Boyfriend.Client.GetUser(Utils.ParseMention(args[index])); + if (user is null && argument is not null) Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{InvalidArgument}{string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}", Context.Message); @@ -154,7 +150,7 @@ public sealed class CommandProcessor { public SocketGuildUser? GetMember(SocketUser user, string? argument) { var member = Context.Guild.GetUser(user.Id); - if (member == null && argument != null) + if (member is null && argument is not null) Utils.SafeAppendToBuilder(_stackedReplyMessage, $":x: {Messages.UserNotInGuild}", Context.Message); return member; } @@ -167,7 +163,7 @@ public sealed class CommandProcessor { } var member = Context.Guild.GetUser(Utils.ParseMention(args[index])); - if (member == null && argument != null) + if (member is null && argument is not null) Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{InvalidArgument}{string.Format(Messages.InvalidMember, Utils.Wrap(cleanArgs[index]))}", Context.Message); @@ -186,7 +182,7 @@ public sealed class CommandProcessor { } var id = Utils.ParseMention(args[index]); - if (Context.Guild.GetBanAsync(id) == null) { + if (Context.Guild.GetBanAsync(id) is null) { Utils.SafeAppendToBuilder(_stackedReplyMessage, Messages.UserNotBanned, Context.Message); return null; } @@ -209,7 +205,7 @@ public sealed class CommandProcessor { return null; } - if (argument == null) return i; + if (argument is null) return i; if (i < min) { Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{InvalidArgument}{string.Format(Utils.GetMessage($"{argument}TooSmall"), min.ToString())}", @@ -232,7 +228,7 @@ public sealed class CommandProcessor { int days = 0, hours = 0, minutes = 0, seconds = 0; foreach (var c in chars) if (char.IsDigit(c)) { numberBuilder.Append(c); } else { - if (numberBuilder.Length == 0) return infinity; + if (numberBuilder.Length is 0) return infinity; switch (c) { case 'd' or 'D' or 'д' or 'Д': days += int.Parse(numberBuilder.ToString()); diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 99676eb..b4d8e1b 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -8,19 +8,17 @@ public sealed class BanCommand : ICommand { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { var toBan = cmd.GetUser(args, cleanArgs, 0, "ToBan"); - if (toBan == null || !cmd.HasPermission(GuildPermission.BanMembers)) return; + if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return; var memberToBan = cmd.GetMember(toBan, null); - if (memberToBan != null && !cmd.CanInteractWith(memberToBan, "Ban")) return; + if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return; var duration = CommandProcessor.GetTimeSpan(args, 1); var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "BanReason"); - if (reason == null) return; - - await BanUser(cmd, toBan, duration, reason); + if (reason is not null) await BanUserAsync(cmd, toBan, duration, reason); } - public static async Task BanUser(CommandProcessor cmd, SocketUser toBan, TimeSpan duration, string reason) { + private static async Task BanUserAsync(CommandProcessor cmd, SocketUser toBan, TimeSpan duration, string reason) { var author = cmd.Context.User; var guild = cmd.Context.Guild; await Utils.SendDirectMessage(toBan, diff --git a/Boyfriend/Commands/ClearCommand.cs b/Boyfriend/Commands/ClearCommand.cs index 9c46ec8..ad5408e 100644 --- a/Boyfriend/Commands/ClearCommand.cs +++ b/Boyfriend/Commands/ClearCommand.cs @@ -12,7 +12,7 @@ public sealed class ClearCommand : ICommand { if (!cmd.HasPermission(GuildPermission.ManageMessages)) return; var toDelete = cmd.GetNumberRange(cleanArgs, 0, 1, 200, "ClearAmount"); - if (toDelete == null) return; + if (toDelete is null) return; var messages = await channel.GetMessagesAsync((int)(toDelete + 1)).FlattenAsync(); var user = (SocketGuildUser)cmd.Context.User; diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index b3eda26..5a32f93 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -8,15 +8,14 @@ public sealed class KickCommand : ICommand { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { var toKick = cmd.GetMember(args, cleanArgs, 0, "ToKick"); - if (toKick == null || !cmd.HasPermission(GuildPermission.KickMembers)) return; + if (toKick is null || !cmd.HasPermission(GuildPermission.KickMembers)) return; - if (!cmd.CanInteractWith(toKick, "Kick")) return; - - await KickMemberAsync(cmd, toKick, cmd.GetRemaining(args, 1, "KickReason")); + if (cmd.CanInteractWith(toKick, "Kick")) + await KickMemberAsync(cmd, toKick, cmd.GetRemaining(args, 1, "KickReason")); } private static async Task KickMemberAsync(CommandProcessor cmd, SocketGuildUser toKick, string? reason) { - if (reason == null) return; + if (reason is null) return; var guildKickMessage = $"({cmd.Context.User}) {reason}"; await Utils.SendDirectMessage(toKick, diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index bc608f9..fb258ca 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -9,15 +9,15 @@ public sealed class MuteCommand : ICommand { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { var toMute = cmd.GetMember(args, cleanArgs, 0, "ToMute"); - if (toMute == null) return; + if (toMute is null) return; var duration = CommandProcessor.GetTimeSpan(args, 1); var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "MuteReason"); - if (reason == null) return; + if (reason is null) return; var role = Utils.GetMuteRole(cmd.Context.Guild); - if ((role != null && toMute.Roles.Contains(role)) - || (toMute.TimedOutUntil != null + if ((role is not null && toMute.Roles.Contains(role)) + || (toMute.TimedOutUntil is not null && toMute.TimedOutUntil.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds())) { cmd.Reply(Messages.MemberAlreadyMuted, ":x: "); @@ -33,9 +33,8 @@ public sealed class MuteCommand : ICommand { cmd.Reply(Messages.RolesReturned, ":warning: "); } - if (!cmd.HasPermission(GuildPermission.ModerateMembers) || !cmd.CanInteractWith(toMute, "Mute")) return; - - await MuteMemberAsync(cmd, toMute, duration, reason); + if (cmd.HasPermission(GuildPermission.ModerateMembers) && cmd.CanInteractWith(toMute, "Mute")) + await MuteMemberAsync(cmd, toMute, duration, reason); } private static async Task MuteMemberAsync(CommandProcessor cmd, SocketGuildUser toMute, @@ -46,7 +45,7 @@ public sealed class MuteCommand : ICommand { var role = Utils.GetMuteRole(guild); var hasDuration = duration.TotalSeconds > 0; - if (role != null) { + if (role is not null) { if (config["RemoveRolesOnMute"] is "true") { var rolesRemoved = new List(); foreach (var userRole in toMute.Roles) diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 34d2bc5..baccdc2 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -11,7 +11,7 @@ public sealed class SettingsCommand : ICommand { var guild = cmd.Context.Guild; var config = Boyfriend.GetGuildConfig(guild.Id); - if (args.Length == 0) { + if (args.Length is 0) { var currentSettings = Boyfriend.StringBuilder.AppendLine(Messages.CurrentSettings); foreach (var setting in Boyfriend.DefaultConfig) { @@ -19,20 +19,14 @@ public sealed class SettingsCommand : ICommand { var currentValue = config[setting.Key]; if (setting.Key.EndsWith("Channel")) { - if (guild.GetTextChannel(Convert.ToUInt64(currentValue)) != null) - format = "<#{0}>"; - else - currentValue = Messages.ChannelNotSpecified; + if (guild.GetTextChannel(ulong.Parse(currentValue)) is not null) format = "<#{0}>"; + else currentValue = Messages.ChannelNotSpecified; } else if (setting.Key.EndsWith("Role")) { - if (guild.GetRole(Convert.ToUInt64(currentValue)) != null) - format = "<@&{0}>"; - else - currentValue = Messages.RoleNotSpecified; + if (guild.GetRole(ulong.Parse(currentValue)) is not null) format = "<@&{0}>"; + else currentValue = Messages.RoleNotSpecified; } else { - if (IsBool(currentValue)) - currentValue = YesOrNo(currentValue is "true"); - else - format = Utils.Wrap("{0}")!; + if (!IsBool(currentValue)) format = Utils.Wrap("{0}")!; + else currentValue = YesOrNo(currentValue is "true"); } currentSettings.Append($"{Utils.GetMessage($"Settings{setting.Key}")} (`{setting.Key}`): ") @@ -65,11 +59,11 @@ public sealed class SettingsCommand : ICommand { if (args.Length >= 2) { value = cmd.GetRemaining(args, 1, "Setting"); - if (value == null) return Task.CompletedTask; + if (value is null) return Task.CompletedTask; if (selectedSetting is "EventStartedReceivers") { value = value.Replace(" ", "").ToLower(); - if (value.StartsWith(",") || value.Count(x => x == ',') > 1 || - (!value.Contains("interested") && !value.Contains("role"))) { + if (value.StartsWith(",") || value.Count(x => x is ',') > 1 || + (!value.Contains("interested") && !value.Contains("users") && !value.Contains("role"))) { cmd.Reply(Messages.InvalidSettingValue, ":x: "); return Task.CompletedTask; } @@ -91,14 +85,12 @@ public sealed class SettingsCommand : ICommand { var localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); var mention = Utils.ParseMention(value); - if (mention != 0 && selectedSetting is not "WelcomeMessage") value = mention.ToString(); + if (mention is not 0 && selectedSetting is not "WelcomeMessage") value = mention.ToString(); var formatting = Utils.Wrap("{0}")!; if (selectedSetting is not "WelcomeMessage") { - if (selectedSetting.EndsWith("Channel")) - formatting = "<#{0}>"; - if (selectedSetting.EndsWith("Role")) - formatting = "<@&{0}>"; + if (selectedSetting.EndsWith("Channel")) formatting = "<#{0}>"; + if (selectedSetting.EndsWith("Role")) formatting = "<@&{0}>"; } var formattedValue = selectedSetting switch { @@ -110,9 +102,7 @@ public sealed class SettingsCommand : ICommand { }; if (value is "reset" or "default") { - config[selectedSetting] = selectedSetting is "WelcomeMessage" - ? Messages.DefaultWelcomeMessage - : Boyfriend.DefaultConfig[selectedSetting]; + config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; } else { if (value == config[selectedSetting]) { cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), @@ -120,17 +110,17 @@ public sealed class SettingsCommand : ICommand { return Task.CompletedTask; } - if (selectedSetting is "Lang" && value is not "ru" and not "en" and not "mctaylors-ru") { + if (selectedSetting is "Lang" && !Utils.CultureInfoCache.ContainsKey(value)) { cmd.Reply(Messages.LanguageNotSupported, ":x: "); return Task.CompletedTask; } - if (selectedSetting.EndsWith("Channel") && guild.GetTextChannel(mention) == null) { + if (selectedSetting.EndsWith("Channel") && guild.GetTextChannel(mention) is null) { cmd.Reply(Messages.InvalidChannel, ":x: "); return Task.CompletedTask; } - if (selectedSetting.EndsWith("Role") && guild.GetRole(mention) == null) { + if (selectedSetting.EndsWith("Role") && guild.GetRole(mention) is null) { cmd.Reply(Messages.InvalidRole, ":x: "); return Task.CompletedTask; } diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Boyfriend/Commands/UnbanCommand.cs index 3ef5c15..f1eb9e6 100644 --- a/Boyfriend/Commands/UnbanCommand.cs +++ b/Boyfriend/Commands/UnbanCommand.cs @@ -9,11 +9,9 @@ public sealed class UnbanCommand : ICommand { if (!cmd.HasPermission(GuildPermission.BanMembers)) return; var id = cmd.GetBan(args, 0); - if (id == null) return; + if (id is null) return; var reason = cmd.GetRemaining(args, 1, "UnbanReason"); - if (reason == null) return; - - await UnbanUserAsync(cmd, id.Value, reason); + if (reason is not null) await UnbanUserAsync(cmd, id.Value, reason); } public static async Task UnbanUserAsync(CommandProcessor cmd, ulong id, string reason) { diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index b6a1aa9..976ce61 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -10,9 +10,10 @@ public sealed class UnmuteCommand : ICommand { if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return; var toUnmute = cmd.GetMember(args, cleanArgs, 0, "ToUnmute"); + if (toUnmute is null) return; var reason = cmd.GetRemaining(args, 1, "UnmuteReason"); - if (toUnmute == null || reason == null || !cmd.CanInteractWith(toUnmute, "Unmute")) return; - await UnmuteMemberAsync(cmd, toUnmute, reason); + if (reason is not null && cmd.CanInteractWith(toUnmute, "Unmute")) + await UnmuteMemberAsync(cmd, toUnmute, reason); } public static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute, @@ -20,7 +21,7 @@ public sealed class UnmuteCommand : ICommand { var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); var role = Utils.GetMuteRole(cmd.Context.Guild); - if (role != null && toUnmute.Roles.Contains(role)) { + 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)) { @@ -31,7 +32,7 @@ public sealed class UnmuteCommand : ICommand { await toUnmute.RemoveRoleAsync(role, requestOptions); } else { - if (toUnmute.TimedOutUntil == null || toUnmute.TimedOutUntil.Value.ToUnixTimeSeconds() < + if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) { cmd.Reply(Messages.MemberNotMuted, ":x: "); return; diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index cd34119..be3ff2a 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Rest; using Discord.WebSocket; -using Humanizer; namespace Boyfriend; @@ -23,14 +22,14 @@ public static class EventHandler { private static Task ReadyEvent() { if (!_sendReadyMessages) return Task.CompletedTask; - var i = Utils.Random.Next(3); + var i = Random.Shared.Next(3); foreach (var guild in Client.Guilds) { var config = Boyfriend.GetGuildConfig(guild.Id); - var channel = guild.GetTextChannel(Convert.ToUInt64(config["BotLogChannel"])); + var channel = guild.GetTextChannel(Utils.ParseMention(config["BotLogChannel"])); Utils.SetCurrentLanguage(guild.Id); - if (config["ReceiveStartupMessages"] is not "true" || channel == null) continue; + if (config["ReceiveStartupMessages"] is not "true" || channel is null) continue; _ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); } @@ -41,9 +40,10 @@ public static class EventHandler { private static async Task MessageDeletedEvent(Cacheable message, Cacheable channel) { var msg = message.Value; - if (msg is null or ISystemMessage || msg.Author.IsBot) return; + if (channel.Value is not SocketGuildChannel gChannel || msg is null or ISystemMessage || + msg.Author.IsBot) return; - var guild = Boyfriend.FindGuild(channel.Value.Id); + var guild = gChannel.Guild; Utils.SetCurrentLanguage(guild.Id); @@ -61,11 +61,7 @@ public static class EventHandler { } private static Task MessageReceivedEvent(SocketMessage messageParam) { - if (messageParam is not SocketUserMessage { Author: SocketGuildUser user } message) return Task.CompletedTask; - - var guild = user.Guild; - - Utils.SetCurrentLanguage(guild.Id); + if (messageParam is not SocketUserMessage message) return Task.CompletedTask; _ = message.CleanContent.ToLower() switch { "whoami" => message.ReplyAsync("`nobody`"), @@ -81,11 +77,10 @@ public static class EventHandler { private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, ISocketMessageChannel channel) { var msg = messageCached.Value; + if (channel is not SocketGuildChannel gChannel || msg is null or ISystemMessage || + msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return; - if (msg is null or ISystemMessage || msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return; - - var guild = Boyfriend.FindGuild(channel.Id); - + var guild = gChannel.Guild; Utils.SetCurrentLanguage(guild.Id); var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940; @@ -98,10 +93,13 @@ public static class EventHandler { private static async Task UserJoinedEvent(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"], user.Mention, guild.Name)); + config["WelcomeMessage"] is "default" + ? Messages.DefaultWelcomeMessage + : string.Format(config["WelcomeMessage"], user.Mention, guild.Name)); if (config["StarterRole"] is not "0") await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); } @@ -109,34 +107,35 @@ public static class EventHandler { private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); - var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCreatedChannel"])); + var channel = Utils.GetEventNotificationChannel(guild); - if (channel != null) { - var roleMention = ""; - var role = guild.GetRole(Convert.ToUInt64(eventConfig["EventNotifyReceiverRole"])); - if (role != null) roleMention = $"{role.Mention} "; + if (channel is not null) { + var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); + var mentions = role is not null + ? $"{role.Mention} {scheduledEvent.Creator.Mention}" + : "{scheduledEvent.Creator.Mention}"; var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); + var descAndLink + = $"{Utils.Wrap(scheduledEvent.Description)}\nhttps://discord.com/events/{guild.Id}/{scheduledEvent.Id}"; await Utils.SilentSendAsync(channel, - string.Format(Messages.EventCreated, "\n", roleMention, scheduledEvent.Creator.Mention, + string.Format(Messages.EventCreated, mentions, Utils.Wrap(scheduledEvent.Name), location, - scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), Utils.Wrap(scheduledEvent.Description), - guild.Id, scheduledEvent.Id), + scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink), true); } - if (eventConfig["EventEarlyNotificationOffset"] != "0") { + if (eventConfig["EventEarlyNotificationOffset"] is not "0") _ = Utils.SendEarlyEventStartNotificationAsync(channel, scheduledEvent, - Convert.ToInt32(eventConfig["EventEarlyNotificationOffset"])); - } + int.Parse(eventConfig["EventEarlyNotificationOffset"])); } private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); - var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCancelledChannel"])); - if (channel != null) + var channel = Utils.GetEventNotificationChannel(guild); + if (channel is not null) await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name), eventConfig["FrowningFace"] is "true" ? $" {Messages.SettingsFrowningFace}" : "")); } @@ -144,14 +143,14 @@ public static class EventHandler { private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); - var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventStartedChannel"])); + var channel = Utils.GetEventNotificationChannel(guild); - if (channel != null) { + if (channel is not null) { var receivers = eventConfig["EventStartedReceivers"]; - var role = guild.GetRole(Convert.ToUInt64(eventConfig["EventNotifyReceiverRole"])); + var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); var mentions = Boyfriend.StringBuilder; - if (receivers.Contains("role") && role != null) mentions.Append($"{role.Mention} "); + 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} ")); @@ -165,10 +164,9 @@ public static class EventHandler { private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; - var eventConfig = Boyfriend.GetGuildConfig(guild.Id); - var channel = guild.GetTextChannel(Convert.ToUInt64(eventConfig["EventCompletedChannel"])); - if (channel != null) + var channel = Utils.GetEventNotificationChannel(guild); + if (channel is not null) await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), - Utils.Wrap(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now).Negate().Humanize()))); + Utils.GetHumanizedTimeOffset(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); } } diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 093cc8b..1264e3f 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -11,32 +11,46 @@ namespace Boyfriend { using System; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -45,736 +59,1066 @@ namespace Boyfriend { } } - internal static string Ready { - get { - return ResourceManager.GetString("Ready", resourceCulture); - } - } - - internal static string CachedMessageDeleted { - get { - return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); - } - } - - internal static string AutobanReason { - get { - return ResourceManager.GetString("AutobanReason", resourceCulture); - } - } - - internal static string CachedMessageEdited { - get { - return ResourceManager.GetString("CachedMessageEdited", resourceCulture); - } - } - - internal static string DefaultWelcomeMessage { - get { - return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); - } - } - + /// + /// Looks up a localized string similar to Bah! . + /// internal static string Beep1 { get { return ResourceManager.GetString("Beep1", resourceCulture); } } + /// + /// Looks up a localized string similar to Bop! . + /// internal static string Beep2 { get { return ResourceManager.GetString("Beep2", resourceCulture); } } + /// + /// Looks up a localized string similar to Beep! . + /// internal static string Beep3 { get { return ResourceManager.GetString("Beep3", resourceCulture); } } - internal static string CommandNoPermissionBot { - get { - return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); - } - } - - internal static string CommandNoPermissionUser { - get { - return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); - } - } - - internal static string YouWereBanned { - get { - return ResourceManager.GetString("YouWereBanned", resourceCulture); - } - } - - internal static string PunishmentExpired { - get { - return ResourceManager.GetString("PunishmentExpired", resourceCulture); - } - } - - internal static string ClearAmountTooSmall { - get { - return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); - } - } - - internal static string ClearAmountTooLarge { - get { - return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); - } - } - - internal static string CommandHelp { - get { - return ResourceManager.GetString("CommandHelp", resourceCulture); - } - } - - internal static string YouWereKicked { - get { - return ResourceManager.GetString("YouWereKicked", resourceCulture); - } - } - - internal static string Milliseconds { - get { - return ResourceManager.GetString("Milliseconds", resourceCulture); - } - } - - internal static string MemberAlreadyMuted { - get { - return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - - internal static string ChannelNotSpecified { - get { - return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); - } - } - - internal static string RoleNotSpecified { - get { - return ResourceManager.GetString("RoleNotSpecified", resourceCulture); - } - } - - internal static string CurrentSettings { - get { - return ResourceManager.GetString("CurrentSettings", resourceCulture); - } - } - - internal static string SettingsLang { - get { - return ResourceManager.GetString("SettingsLang", resourceCulture); - } - } - - internal static string SettingsPrefix { - get { - return ResourceManager.GetString("SettingsPrefix", resourceCulture); - } - } - - internal static string SettingsRemoveRolesOnMute { - get { - return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); - } - } - - internal static string SettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); - } - } - - internal static string SettingsStarterRole { - get { - return ResourceManager.GetString("SettingsStarterRole", resourceCulture); - } - } - - internal static string SettingsMuteRole { - get { - return ResourceManager.GetString("SettingsMuteRole", resourceCulture); - } - } - - internal static string SettingsAdminLogChannel { - get { - return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); - } - } - - internal static string SettingsBotLogChannel { - get { - return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); - } - } - - internal static string LanguageNotSupported { - get { - return ResourceManager.GetString("LanguageNotSupported", resourceCulture); - } - } - - internal static string Yes { - get { - return ResourceManager.GetString("Yes", resourceCulture); - } - } - - internal static string No { - get { - return ResourceManager.GetString("No", resourceCulture); - } - } - - internal static string UserNotBanned { - get { - return ResourceManager.GetString("UserNotBanned", resourceCulture); - } - } - - internal static string MemberNotMuted { - get { - return ResourceManager.GetString("MemberNotMuted", resourceCulture); - } - } - - internal static string RolesReturned { - get { - return ResourceManager.GetString("RolesReturned", resourceCulture); - } - } - - internal static string SettingsWelcomeMessage { - get { - return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); - } - } - - internal static string ClearAmountInvalid { - get { - return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); - } - } - - internal static string FeedbackUserBanned { - get { - return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); - } - } - - internal static string UserNotInGuild { - get { - return ResourceManager.GetString("UserNotInGuild", resourceCulture); - } - } - - internal static string SettingDoesntExist { - get { - return ResourceManager.GetString("SettingDoesntExist", resourceCulture); - } - } - - internal static string SettingsReceiveStartupMessages { - get { - return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); - } - } - - internal static string InvalidSettingValue { - get { - return ResourceManager.GetString("InvalidSettingValue", resourceCulture); - } - } - - internal static string InvalidRole { - get { - return ResourceManager.GetString("InvalidRole", resourceCulture); - } - } - - internal static string InvalidChannel { - get { - return ResourceManager.GetString("InvalidChannel", resourceCulture); - } - } - - internal static string RoleRemovalFailed { - get { - return ResourceManager.GetString("RoleRemovalFailed", resourceCulture); - } - } - - internal static string DurationRequiredForTimeOuts { - get { - return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); - } - } - - internal static string CannotTimeOutBot { - get { - return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); - } - } - - internal static string EventCreated { - get { - return ResourceManager.GetString("EventCreated", resourceCulture); - } - } - - internal static string SettingsEventNotifyReceiverRole { - get { - return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); - } - } - - internal static string SettingsEventCreatedChannel { - get { - return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); - } - } - - internal static string SettingsEventStartedChannel { - get { - return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); - } - } - - internal static string SettingsEventStartedReceivers { - get { - return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); - } - } - - internal static string EventStarted { - get { - return ResourceManager.GetString("EventStarted", resourceCulture); - } - } - - internal static string SettingsFrowningFace { - get { - return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); - } - } - - internal static string EventCancelled { - get { - return ResourceManager.GetString("EventCancelled", resourceCulture); - } - } - - internal static string SettingsEventCancelledChannel { - get { - return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); - } - } - - internal static string SettingsEventCompletedChannel { - get { - return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); - } - } - - internal static string EventCompleted { - get { - return ResourceManager.GetString("EventCompleted", resourceCulture); - } - } - - internal static string FeedbackFormat { - get { - return ResourceManager.GetString("FeedbackFormat", resourceCulture); - } - } - - internal static string Ever { - get { - return ResourceManager.GetString("Ever", resourceCulture); - } - } - - internal static string FeedbackMessagesCleared { - get { - return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); - } - } - - internal static string FeedbackMemberKicked { - get { - return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); - } - } - - internal static string FeedbackMemberMuted { - get { - return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); - } - } - - internal static string FeedbackUserUnbanned { - get { - return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); - } - } - - internal static string FeedbackMemberUnmuted { - get { - return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); - } - } - - internal static string SettingsNothingChanged { - get { - return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); - } - } - - internal static string SettingNotDefined { - get { - return ResourceManager.GetString("SettingNotDefined", resourceCulture); - } - } - - internal static string FeedbackSettingsUpdated { - get { - return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); - } - } - - internal static string CommandDescriptionBan { - get { - return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); - } - } - - internal static string CommandDescriptionClear { - get { - return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); - } - } - - internal static string CommandDescriptionHelp { - get { - return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); - } - } - - internal static string CommandDescriptionKick { - get { - return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); - } - } - - internal static string CommandDescriptionMute { - get { - return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); - } - } - - internal static string CommandDescriptionPing { - get { - return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); - } - } - - internal static string CommandDescriptionSettings { - get { - return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); - } - } - - internal static string CommandDescriptionUnban { - get { - return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); - } - } - - internal static string CommandDescriptionUnmute { - get { - return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); - } - } - - internal static string MissingNumber { - get { - return ResourceManager.GetString("MissingNumber", resourceCulture); - } - } - - internal static string MissingUser { - get { - return ResourceManager.GetString("MissingUser", resourceCulture); - } - } - - internal static string InvalidUser { - get { - return ResourceManager.GetString("InvalidUser", resourceCulture); - } - } - - internal static string MissingMember { - get { - return ResourceManager.GetString("MissingMember", resourceCulture); - } - } - - internal static string InvalidMember { - get { - return ResourceManager.GetString("InvalidMember", resourceCulture); - } - } - - internal static string UserCannotBanMembers { - get { - return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); - } - } - - internal static string UserCannotManageMessages { - get { - return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); - } - } - - internal static string UserCannotKickMembers { - get { - return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); - } - } - - internal static string UserCannotModerateMembers { - get { - return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); - } - } - - internal static string UserCannotManageGuild { - get { - return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); - } - } - + /// + /// Looks up a localized string similar to I cannot ban users from this guild!. + /// internal static string BotCannotBanMembers { get { return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); } } - internal static string BotCannotManageMessages { - get { - return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); - } - } - - internal static string BotCannotKickMembers { - get { - return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); - } - } - - internal static string BotCannotModerateMembers { - get { - return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); - } - } - - internal static string BotCannotManageGuild { - get { - return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); - } - } - - internal static string MissingBanReason { - get { - return ResourceManager.GetString("MissingBanReason", resourceCulture); - } - } - - internal static string MissingKickReason { - get { - return ResourceManager.GetString("MissingKickReason", resourceCulture); - } - } - - internal static string MissingMuteReason { - get { - return ResourceManager.GetString("MissingMuteReason", resourceCulture); - } - } - - internal static string MissingUnbanReason { - get { - return ResourceManager.GetString("MissingUnbanReason", resourceCulture); - } - } - - internal static string MissingUnmuteReason { - get { - return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); - } - } - - internal static string MissingSetting { - get { - return ResourceManager.GetString("MissingSetting", resourceCulture); - } - } - - internal static string UserCannotBanOwner { - get { - return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); - } - } - - internal static string UserCannotBanThemselves { - get { - return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); - } - } - - internal static string UserCannotBanBot { - get { - return ResourceManager.GetString("UserCannotBanBot", resourceCulture); - } - } - + /// + /// Looks up a localized string similar to I cannot ban this user!. + /// internal static string BotCannotBanTarget { get { return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); } } - internal static string UserCannotBanTarget { + /// + /// Looks up a localized string similar to I cannot kick members from this guild!. + /// + internal static string BotCannotKickMembers { get { - return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); - } - } - - internal static string UserCannotKickOwner { - get { - return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); - } - } - - internal static string UserCannotKickThemselves { - get { - return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); - } - } - - internal static string UserCannotKickBot { - get { - return ResourceManager.GetString("UserCannotKickBot", resourceCulture); + return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); } } + /// + /// Looks up a localized string similar to I cannot kick this member!. + /// internal static string BotCannotKickTarget { get { return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); } } - internal static string UserCannotKickTarget { + /// + /// Looks up a localized string similar to I cannot manage this guild!. + /// + internal static string BotCannotManageGuild { get { - return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); + return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); } } - internal static string UserCannotMuteOwner { + /// + /// Looks up a localized string similar to I cannot manage messages in this guild!. + /// + internal static string BotCannotManageMessages { get { - return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); + return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); } } - internal static string UserCannotMuteThemselves { + /// + /// Looks up a localized string similar to I cannot moderate members in this guild!. + /// + internal static string BotCannotModerateMembers { get { - return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); - } - } - - internal static string UserCannotMuteBot { - get { - return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); + return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); } } + /// + /// Looks up a localized string similar to I cannot mute this member!. + /// internal static string BotCannotMuteTarget { get { return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); } } - internal static string UserCannotMuteTarget { - get { - return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); - } - } - - internal static string UserCannotUnmuteOwner { - get { - return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); - } - } - - internal static string UserCannotUnmuteThemselves { - get { - return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); - } - } - - internal static string UserCannotUnmuteBot { - get { - return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); - } - } - + /// + /// Looks up a localized string similar to I cannot unmute this member!. + /// internal static string BotCannotUnmuteTarget { get { return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); } } - internal static string UserCannotUnmuteTarget { + /// + /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. + /// + internal static string CachedMessageDeleted { get { - return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); + return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - internal static string CommandDescriptionCavepleaselisten { + /// + /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. + /// + internal static string CachedMessageEdited { get { - return ResourceManager.GetString("CommandDescriptionCavepleaselisten", resourceCulture); + return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - internal static string ServerBlacklisted { + /// + /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. + /// + internal static string CannotTimeOutBot { get { - return ResourceManager.GetString("ServerBlacklisted", resourceCulture); + return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); } } + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string ChannelNotSpecified { + get { + return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify an integer from {0} to {1} instead of {2}!. + /// + internal static string ClearAmountInvalid { + get { + return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You specified more than {0} messages!. + /// + internal static string ClearAmountTooLarge { + get { + return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You specified less than {0} messages!. + /// + internal static string ClearAmountTooSmall { + get { + return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bans a user. + /// + internal static string CommandDescriptionBan { + get { + return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. + /// + internal static string CommandDescriptionClear { + get { + return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows this message. + /// + internal static string CommandDescriptionHelp { + get { + return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kicks a member. + /// + internal static string CommandDescriptionKick { + get { + return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mutes a member. + /// + internal static string CommandDescriptionMute { + get { + return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows (inaccurate) latency. + /// + internal static string CommandDescriptionPing { + get { + return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allows you to change certain preferences for this guild. + /// + internal static string CommandDescriptionSettings { + get { + return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unbans a user. + /// + internal static string CommandDescriptionUnban { + get { + return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unmutes a member. + /// + internal static string CommandDescriptionUnmute { + get { + return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command help:. + /// + internal static string CommandHelp { + get { + return ResourceManager.GetString("CommandHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I do not have permission to execute this command!. + /// + internal static string CommandNoPermissionBot { + get { + return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You do not have permission to execute this command!. + /// + internal static string CommandNoPermissionUser { + get { + return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current settings:. + /// + internal static string CurrentSettings { + get { + return ResourceManager.GetString("CurrentSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}, welcome to {1}. + /// + internal static string DefaultWelcomeMessage { + get { + return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 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. + /// + internal static string DurationRequiredForTimeOuts { + get { + return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event {0} is cancelled!{1}. + /// + internal static string EventCancelled { + get { + return ResourceManager.GetString("EventCancelled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event {0} has completed! Duration: {1}. + /// + internal static string EventCompleted { + get { + return ResourceManager.GetString("EventCompleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>!\n{4}. + /// + internal static string EventCreated { + get { + return ResourceManager.GetString("EventCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}Event {1} will start <t:{2}:R>!. + /// internal static string EventEarlyNotification { get { return ResourceManager.GetString("EventEarlyNotification", resourceCulture); } } + /// + /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. + /// + internal static string EventStarted { + get { + return ResourceManager.GetString("EventStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ever. + /// + internal static string Ever { + get { + return ResourceManager.GetString("Ever", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kicked {0}: {1}. + /// + internal static string FeedbackMemberKicked { + get { + return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Muted {0} for{1}: {2}. + /// + internal static string FeedbackMemberMuted { + get { + return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unmuted {0}: {1}. + /// + internal static string FeedbackMemberUnmuted { + get { + return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleted {0} messages in {1}. + /// + internal static string FeedbackMessagesCleared { + get { + return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. + /// + internal static string FeedbackSettingsUpdated { + get { + return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Banned {0} for{1}: {2}. + /// + internal static string FeedbackUserBanned { + get { + return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unbanned {0}: {1}. + /// + internal static string FeedbackUserUnbanned { + get { + return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This channel does not exist!. + /// + internal static string InvalidChannel { + get { + return ResourceManager.GetString("InvalidChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a guild member instead of {0}!. + /// + internal static string InvalidMember { + get { + return ResourceManager.GetString("InvalidMember", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This role does not exist!. + /// + internal static string InvalidRole { + get { + return ResourceManager.GetString("InvalidRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid setting value specified!. + /// + internal static string InvalidSettingValue { + get { + return ResourceManager.GetString("InvalidSettingValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a user instead of {0}!. + /// + internal static string InvalidUser { + get { + return ResourceManager.GetString("InvalidUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language not supported!. + /// + internal static string LanguageNotSupported { + get { + return ResourceManager.GetString("LanguageNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member is already muted!. + /// + internal static string MemberAlreadyMuted { + get { + return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member not muted!. + /// + internal static string MemberNotMuted { + get { + return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ms. + /// + internal static string Milliseconds { + get { + return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to ban this user!. + /// + internal static string MissingBanReason { + get { + return ResourceManager.GetString("MissingBanReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to kick this member!. + /// + internal static string MissingKickReason { + get { + return ResourceManager.GetString("MissingKickReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a guild member!. + /// + internal static string MissingMember { + get { + return ResourceManager.GetString("MissingMember", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to mute this member!. + /// + internal static string MissingMuteReason { + get { + return ResourceManager.GetString("MissingMuteReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. + /// + internal static string MissingNumber { + get { + return ResourceManager.GetString("MissingNumber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a setting to change!. + /// + internal static string MissingSetting { + get { + return ResourceManager.GetString("MissingSetting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to unban this user!. + /// + internal static string MissingUnbanReason { + get { + return ResourceManager.GetString("MissingUnbanReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason for unmute this member!. + /// + internal static string MissingUnmuteReason { + get { + return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a user!. + /// + internal static string MissingUser { + get { + return ResourceManager.GetString("MissingUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No. + /// + internal static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Punishment expired. + /// + internal static string PunishmentExpired { + get { + return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}I'm ready!. + /// + internal static string Ready { + get { + return ResourceManager.GetString("Ready", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string RoleNotSpecified { + get { + return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + } + } + + /// + /// 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!. + /// + internal static string SettingDoesntExist { + get { + return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string SettingNotDefined { + get { + return ResourceManager.GetString("SettingNotDefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Admin log channel. + /// + internal static string SettingsAdminLogChannel { + get { + return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bot log channel. + /// + internal static string SettingsBotLogChannel { + get { + return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event cancellation notifications. + /// + internal static string SettingsEventCancelledChannel { + get { + return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event completion notifications. + /// + internal static string SettingsEventCompletedChannel { + get { + return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event creation notifications. + /// + internal static string SettingsEventCreatedChannel { + get { + return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Early event start notification offset. + /// internal static string SettingsEventEarlyNotificationOffset { get { return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); } } + + /// + /// Looks up a localized string similar to Role for event creation notifications. + /// + internal static string SettingsEventNotifyReceiverRole { + get { + return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event start notifications. + /// + internal static string SettingsEventStartedChannel { + get { + return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event start notifications receivers. + /// + internal static string SettingsEventStartedReceivers { + get { + return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to :(. + /// + internal static string SettingsFrowningFace { + get { + return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language. + /// + internal static string SettingsLang { + get { + return ResourceManager.GetString("SettingsLang", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mute role. + /// + internal static string SettingsMuteRole { + get { + return ResourceManager.GetString("SettingsMuteRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. + /// + internal static string SettingsNothingChanged { + get { + return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix. + /// + internal static string SettingsPrefix { + get { + return ResourceManager.GetString("SettingsPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Receive startup messages. + /// + internal static string SettingsReceiveStartupMessages { + get { + return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove roles on mute. + /// + internal static string SettingsRemoveRolesOnMute { + get { + return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send welcome messages. + /// + internal static string SettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Starter role. + /// + internal static string SettingsStarterRole { + get { + return ResourceManager.GetString("SettingsStarterRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome message. + /// + internal static string SettingsWelcomeMessage { + get { + return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban me!. + /// + internal static string UserCannotBanBot { + get { + return ResourceManager.GetString("UserCannotBanBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban users from this guild!. + /// + internal static string UserCannotBanMembers { + get { + return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban the owner of this guild!. + /// + internal static string UserCannotBanOwner { + get { + return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban this user!. + /// + internal static string UserCannotBanTarget { + get { + return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban yourself!. + /// + internal static string UserCannotBanThemselves { + get { + return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick me!. + /// + internal static string UserCannotKickBot { + get { + return ResourceManager.GetString("UserCannotKickBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick members from this guild!. + /// + internal static string UserCannotKickMembers { + get { + return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick the owner of this guild!. + /// + internal static string UserCannotKickOwner { + get { + return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick this member!. + /// + internal static string UserCannotKickTarget { + get { + return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick yourself!. + /// + internal static string UserCannotKickThemselves { + get { + return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot manage this guild!. + /// + internal static string UserCannotManageGuild { + get { + return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot manage messages in this guild!. + /// + internal static string UserCannotManageMessages { + get { + return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot moderate members in this guild!. + /// + internal static string UserCannotModerateMembers { + get { + return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute me!. + /// + internal static string UserCannotMuteBot { + get { + return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute the owner of this guild!. + /// + internal static string UserCannotMuteOwner { + get { + return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute this member!. + /// + internal static string UserCannotMuteTarget { + get { + return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute yourself!. + /// + internal static string UserCannotMuteThemselves { + get { + return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to .... + /// + internal static string UserCannotUnmuteBot { + get { + return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. + /// + internal static string UserCannotUnmuteOwner { + get { + return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot unmute this user!. + /// + internal static string UserCannotUnmuteTarget { + get { + return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are muted!. + /// + internal static string UserCannotUnmuteThemselves { + get { + return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This user is not banned!. + /// + internal static string UserNotBanned { + get { + return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified user is not a member of this server!. + /// + internal static string UserNotInGuild { + get { + return ResourceManager.GetString("UserNotInGuild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yes. + /// + internal static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. + /// + internal static string YouWereBanned { + get { + return ResourceManager.GetString("YouWereBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. + /// + internal static string YouWereKicked { + get { + return ResourceManager.GetString("YouWereKicked", resourceCulture); + } + } } } diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index a06e8cc..5b41851 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -25,14 +25,11 @@ - {0}I'm ready! (C#) + {0}I'm ready! Deleted message from {0} in channel {1}: {2} - - Too many mentions in 1 message - Edited message in channel {0}: {1} -> {2} @@ -166,7 +163,7 @@ I cannot use time-outs on other bots! Try to set a mute role in settings - {1}{2} created event {3}! It will take place in {4} and will start <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} + {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>!\n{4} Role for event creation notifications @@ -198,9 +195,6 @@ Event {0} has completed! Duration: {1} - - *[{0}: {1}]* - ever @@ -378,12 +372,6 @@ You cannot unmute this user! - - We do not support hate towards our fellow members. And sometimes, we are not able to ban the offender. - - - This feature is unavailable because this guild is currently blacklisted. - {0}Event {1} will start <t:{2}:R>! diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index b593ef5..f009e22 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -16,14 +16,11 @@ - {0}Я запустился! (C#) + {0}Я запустился! Удалено сообщение от {0} в канале {1}: {2} - - Слишком много упоминаний в одном сообщении - Отредактировано сообщение в канале {0}: {1} -> {2} @@ -157,7 +154,7 @@ Начальная роль - {1}{2} создал событие {3}! Оно пройдёт в {4} и начнётся <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} + {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!\n{4} Роль для уведомлений о создании событий @@ -189,9 +186,6 @@ Событие {0} завершено! Продолжительность: {1} - - *[{0}: {1}]* - всегда @@ -369,12 +363,6 @@ Я не могу вернуть из мута этого пользователя! - - Мы не поддерживаем ненависть против участников. И иногда, мы не способны забанить нарушителя. - - - Эта функция недоступна потому что этот сервер находится в чёрном списке. - {0}Событие {1} начнется <t:{2}:R>! diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index 4917af6..2b8c8ae 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -6,20 +6,21 @@ 1.3 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - - {0}я родился! (C#) + + {0}я родился! вырезано {0} в канале {1}: {2} - - ты тут распинался сильно, иди отдохни. - переделано {0}: {1} -> {2} @@ -153,7 +154,7 @@ базовое звание - {1}{2} приготовил новый квест {3}! он пройдёт в {4} и начнётся <t:{5}:R>!{0}{6}{0}https://discord.com/events/{7}/{8} + {0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!\n{4} роль для уведомлений о создании квеста @@ -185,9 +186,6 @@ квест {0} завершен! все это длилось {1} - - *[{0}: {1}]* - всегда @@ -365,12 +363,6 @@ я не могу его раззамутить... - - каве пропал. - - - упс, кажется ваш сервер в черном списке, и я вам ничем помочь не смогу) - {0}квест {1} начнется <t:{2}:R>! diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index b2c798f..d9c2bd6 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -12,10 +12,9 @@ using Humanizer.Localisation; namespace Boyfriend; public static class Utils { - public static readonly Random Random = new(); private static readonly Dictionary ReflectionMessageCache = new(); - private static readonly Dictionary CultureInfoCache = new() { + public static readonly Dictionary CultureInfoCache = new() { { "ru", new CultureInfo("ru-RU") }, { "en", new CultureInfo("en-US") }, { "mctaylors-ru", new CultureInfo("tt-RU") } @@ -28,16 +27,16 @@ public static class Utils { }; public static string GetBeep(int i = -1) { - return GetMessage($"Beep{(i < 0 ? Random.Next(3) + 1 : ++i)}"); + return GetMessage($"Beep{(i < 0 ? Random.Shared.Next(3) + 1 : ++i)}"); } - public static SocketTextChannel? GetAdminLogChannel(ulong id) { + public static SocketTextChannel? GetBotLogChannel(ulong id) { return Boyfriend.Client.GetGuild(id) - .GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(id)["AdminLogChannel"])); + .GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(id)["BotLogChannel"])); } public static string? Wrap(string? original, bool limitedSpace = false) { - if (original == null) return null; + if (original is null) return null; var maxChars = limitedSpace ? 970 : 1940; if (original.Length > maxChars) original = original[..maxChars]; var style = original.Contains('\n') ? "```" : "`"; @@ -52,29 +51,22 @@ public static class Utils { return ulong.TryParse(Regex.Replace(mention, "[^0-9]", ""), out var id) ? id : 0; } - public static SocketUser? ParseUser(string mention) { - var user = Boyfriend.Client.GetUser(ParseMention(mention)); - return user; - } - public static async Task SendDirectMessage(SocketUser user, string toSend) { try { await user.SendMessageAsync(toSend); } catch (HttpException e) { - if (e.DiscordCode != DiscordErrorCode.CannotSendMessageToUser) throw; + if (e.DiscordCode is not DiscordErrorCode.CannotSendMessageToUser) throw; } } public static SocketRole? GetMuteRole(SocketGuild guild) { var id = ulong.Parse(Boyfriend.GetGuildConfig(guild.Id)["MuteRole"]); if (MuteRoleCache.TryGetValue(id, out var cachedMuteRole)) return cachedMuteRole; - SocketRole? role = null; foreach (var x in guild.Roles) { if (x.Id != id) continue; - role = x; - MuteRoleCache.Add(id, role); - break; + MuteRoleCache.Add(id, x); + return x; } - return role; + return null; } public static void RemoveMuteRoleFromCache(ulong id) { @@ -82,7 +74,7 @@ public static class Utils { } public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) { - if (channel == null || text.Length is 0 or > 2000) + if (channel is null || text.Length is 0 or > 2000) throw new Exception($"Message length is out of range: {text.Length}"); await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); @@ -102,8 +94,8 @@ public static class Utils { var toReturn = typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) ?.ToString(); - if (toReturn == null) { - Console.WriteLine($@"Could not find localized property: {propertyName}"); + if (toReturn is null) { + Console.Error.WriteLine($@"Could not find localized property: {propertyName}"); return name; } @@ -113,11 +105,11 @@ public static class Utils { public static async Task SendFeedbackAsync(string feedback, ulong guildId, string mention, bool sendPublic = false) { - var adminChannel = GetAdminLogChannel(guildId); + var adminChannel = GetBotLogChannel(guildId); var systemChannel = Boyfriend.Client.GetGuild(guildId).SystemChannel; - var toSend = string.Format(Messages.FeedbackFormat, mention, feedback); - if (adminChannel != null) await SilentSendAsync(adminChannel, toSend); - if (sendPublic && systemChannel != null) await SilentSendAsync(systemChannel, toSend); + 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) { @@ -131,7 +123,7 @@ public static class Utils { } public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketTextChannel? channel) { - if (channel == null) return; + if (channel is null) return; if (appendTo.Length + appendWhat.Length > 2000) { _ = SilentSendAsync(channel, appendTo.ToString()); appendTo.Clear(); @@ -169,15 +161,19 @@ public static class Utils { var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var receivers = eventConfig["EventStartedReceivers"]; - var role = guild.GetRole(Convert.ToUInt64(eventConfig["EventNotifyReceiverRole"])); + var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); var mentions = Boyfriend.StringBuilder; - if (receivers.Contains("role") && role != null) mentions.Append($"{role.Mention} "); + 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()))!; + Wrap(scheduledEvent.Name), scheduledEvent.StartTime.ToUnixTimeSeconds().ToString()))!; mentions.Clear(); } + + public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) { + return guild.GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(guild.Id)["EventCreatedChannel"])); + } } From 3b12fb7e41185996aedce531314229600c7fd0b6 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 12 Nov 2022 09:57:23 +0500 Subject: [PATCH 039/329] Fix a CodeFactor issue --- Boyfriend/Boyfriend.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 9bc80a9..a1130b4 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -46,7 +46,6 @@ public static class Boyfriend { { "MuteRole", "0" }, { "RemoveRolesOnMute", "false" }, { "FrowningFace", "true" }, - { "EventStartedReceivers", "interested,role" }, { "EventNotificationRole", "0" }, { "EventNotificationChannel", "0" }, From 7dbc4472f745a4c1e21d0cf18c91870dcc33e5eb Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 12 Nov 2022 11:02:44 +0500 Subject: [PATCH 040/329] Fix issues caused by refactor --- Boyfriend/Boyfriend.cs | 10 +++--- Boyfriend/CommandProcessor.cs | 20 ++++------- Boyfriend/Commands/BanCommand.cs | 2 +- Boyfriend/Commands/SettingsCommand.cs | 2 +- Boyfriend/Messages.Designer.cs | 48 ++++----------------------- Boyfriend/Messages.resx | 18 ++-------- Boyfriend/Messages.ru.resx | 18 ++-------- Boyfriend/Messages.tt-ru.resx | 18 ++-------- 8 files changed, 28 insertions(+), 108 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index a1130b4..2e36a76 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -11,7 +11,7 @@ public static class Boyfriend { private static readonly DiscordSocketConfig Config = new() { MessageCacheSize = 250, - GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers, + GatewayIntents = (GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers) & ~GatewayIntents.GuildInvites, AlwaysDownloadUsers = true, AlwaysResolveStickers = false, AlwaysDownloadDefaultStickers = false, @@ -103,11 +103,11 @@ public static class Boyfriend { } public static async Task WriteGuildConfigAsync(ulong id) { - var json = JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented); - var removedRoles = JsonConvert.SerializeObject(RemovedRolesDictionary[id], Formatting.Indented); + await File.WriteAllTextAsync($"config_{id}.json", JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented)); - await File.WriteAllTextAsync($"config_{id}.json", json); - await File.WriteAllTextAsync($"removedroles_{id}.json", removedRoles); + if (RemovedRolesDictionary.TryGetValue(id, out var removedRoles)) + await File.WriteAllTextAsync($"removedroles_{id}.json", + JsonConvert.SerializeObject(removedRoles, Formatting.Indented)); } public static Dictionary GetGuildConfig(ulong id) { diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index da21d00..f34242d 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -41,7 +41,7 @@ public sealed class CommandProcessor { Utils.SetCurrentLanguage(guild.Id); if (GetMember().Roles.Contains(muteRole)) { - await Context.Message.ReplyAsync(Messages.UserCannotUnmuteThemselves); + _ = Context.Message.ReplyAsync(Messages.UserCannotUnmuteThemselves); return; } @@ -51,11 +51,6 @@ public sealed class CommandProcessor { RunCommandOnLine(list[i], cleanList[i], config["Prefix"]); if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync(); - var member = Boyfriend.Client.GetGuild(Context.Guild.Id) - .GetUser(Context.User.Id); // Getting an up-to-date copy - if (member is null || member.Roles.Contains(muteRole) - || member.TimedOutUntil.GetValueOrDefault(DateTimeOffset.UnixEpoch).ToUnixTimeSeconds() > - DateTimeOffset.Now.ToUnixTimeSeconds()) break; } await Task.WhenAll(_tasks); @@ -67,8 +62,8 @@ public sealed class CommandProcessor { } private void RunCommandOnLine(string line, string cleanLine, string prefix) { - var prefixed = line[..prefix.Length] == prefix; - if (!prefixed && line[..Mention.Length] is not Mention) return; + var prefixed = line.StartsWith(prefix); + if (!prefixed && !line.StartsWith(Mention)) return; foreach (var command in Commands) { var lineNoMention = line.Remove(0, prefixed ? prefix.Length : Mention.Length); if (!command.Aliases.Contains(lineNoMention.Trim().Split()[0])) continue; @@ -141,18 +136,15 @@ public sealed class CommandProcessor { } if (Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission) - || Context.Guild.Owner.Id == Context.User.Id) return true; + || Context.Guild.OwnerId == Context.User.Id) return true; Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{NoAccess}{Utils.GetMessage($"UserCannot{permission}")}", Context.Message); return false; } - public SocketGuildUser? GetMember(SocketUser user, string? argument) { - var member = Context.Guild.GetUser(user.Id); - if (member is null && argument is not null) - Utils.SafeAppendToBuilder(_stackedReplyMessage, $":x: {Messages.UserNotInGuild}", Context.Message); - return member; + public SocketGuildUser? GetMember(SocketUser user) { + return Context.Guild.GetUser(user.Id); } public SocketGuildUser? GetMember(string[] args, string[] cleanArgs, int index, string? argument) { diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index b4d8e1b..78cae2c 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -10,7 +10,7 @@ public sealed class BanCommand : ICommand { var toBan = cmd.GetUser(args, cleanArgs, 0, "ToBan"); if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return; - var memberToBan = cmd.GetMember(toBan, null); + var memberToBan = cmd.GetMember(toBan); if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return; var duration = CommandProcessor.GetTimeSpan(args, 1); diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index baccdc2..46cec36 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -16,7 +16,7 @@ public sealed class SettingsCommand : ICommand { foreach (var setting in Boyfriend.DefaultConfig) { var format = "{0}"; - var currentValue = 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}>"; diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 1264e3f..f11dd7b 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -716,15 +716,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Admin log channel. - /// - internal static string SettingsAdminLogChannel { - get { - return ResourceManager.GetString("SettingsAdminLogChannel", resourceCulture); - } - } - /// /// Looks up a localized string similar to Bot log channel. /// @@ -734,33 +725,6 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to Channel for event cancellation notifications. - /// - internal static string SettingsEventCancelledChannel { - get { - return ResourceManager.GetString("SettingsEventCancelledChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event completion notifications. - /// - internal static string SettingsEventCompletedChannel { - get { - return ResourceManager.GetString("SettingsEventCompletedChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event creation notifications. - /// - internal static string SettingsEventCreatedChannel { - get { - return ResourceManager.GetString("SettingsEventCreatedChannel", resourceCulture); - } - } - /// /// Looks up a localized string similar to Early event start notification offset. /// @@ -771,20 +735,20 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Role for event creation notifications. + /// Looks up a localized string similar to Channel for event notifications. /// - internal static string SettingsEventNotifyReceiverRole { + internal static string SettingsEventNotificationChannel { get { - return ResourceManager.GetString("SettingsEventNotifyReceiverRole", resourceCulture); + return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); } } /// - /// Looks up a localized string similar to Channel for event start notifications. + /// Looks up a localized string similar to Role for event creation notifications. /// - internal static string SettingsEventStartedChannel { + internal static string SettingsEventNotificationRole { get { - return ResourceManager.GetString("SettingsEventStartedChannel", resourceCulture); + return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); } } diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index 5b41851..e98e804 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -102,9 +102,6 @@ Mute role - - Admin log channel - Bot log channel @@ -165,14 +162,11 @@ {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>!\n{4} - + Role for event creation notifications - - Channel for event creation notifications - - - Channel for event start notifications + + Channel for event notifications Event start notifications receivers @@ -186,12 +180,6 @@ Event {0} is cancelled!{1} - - Channel for event cancellation notifications - - - Channel for event completion notifications - Event {0} has completed! Duration: {1} diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index f009e22..e55cabe 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -90,9 +90,6 @@ Роль мута - - Канал админ-уведомлений - Канал бот-уведомлений @@ -156,14 +153,11 @@ {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!\n{4} - + Роль для уведомлений о создании событий - - Канал для уведомлений о создании событий - - - Канал для уведомлений о начале событий + + Канал для уведомлений о событиях Получатели уведомлений о начале событий @@ -177,12 +171,6 @@ Событие {0} отменено!{1} - - Канал для уведомлений о отмене событий - - - Канал для уведомлений о завершении событий - Событие {0} завершено! Продолжительность: {1} diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index 2b8c8ae..ba3f2a2 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -90,9 +90,6 @@ роль замученного - - канал админ-уведомлений - канал бот-уведомлений @@ -156,14 +153,11 @@ {0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!\n{4} - + роль для уведомлений о создании квеста - - канал для уведомлений о создании квеста - - - канал для уведомлений о начале квеста + + канал для уведомлений о квестах получатели уведомлений о начале квеста @@ -177,12 +171,6 @@ квест {0} отменен!{1} - - канал для уведомлений о отмене событий - - - канал для уведомлений о завершении квеста - квест {0} завершен! все это длилось {1} From 14ebcb1f1c563bf9424677e7da709189586c4a6a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 13 Nov 2022 17:40:46 +0300 Subject: [PATCH 041/329] Fix some messages not being formatted --- Boyfriend/EventHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index be3ff2a..3f89d7c 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -97,9 +97,9 @@ public static class EventHandler { if (config["SendWelcomeMessages"] is "true") await Utils.SilentSendAsync(guild.SystemChannel, - config["WelcomeMessage"] is "default" + string.Format(config["WelcomeMessage"] is "default" ? Messages.DefaultWelcomeMessage - : string.Format(config["WelcomeMessage"], user.Mention, guild.Name)); + : config["WelcomeMessage"], user.Mention, guild.Name)); if (config["StarterRole"] is not "0") await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); } @@ -113,7 +113,7 @@ public static class EventHandler { var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); var mentions = role is not null ? $"{role.Mention} {scheduledEvent.Creator.Mention}" - : "{scheduledEvent.Creator.Mention}"; + : $"{scheduledEvent.Creator.Mention}"; var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); var descAndLink From 29c2332ad93a56bbdcfab21fa73f142ce2a1a9db Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 16 Nov 2022 23:27:10 +0500 Subject: [PATCH 042/329] Log exceptions thrown during "fire-and-forget" async method calls --- Boyfriend/Boyfriend.cs | 11 ++++++--- Boyfriend/CommandProcessor.cs | 15 +++++++----- Boyfriend/Utils.cs | 46 +++++++++++++++++++++-------------- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 2e36a76..f33caec 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -11,7 +11,9 @@ public static class Boyfriend { private static readonly DiscordSocketConfig Config = new() { MessageCacheSize = 250, - GatewayIntents = (GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers) & ~GatewayIntents.GuildInvites, + GatewayIntents + = (GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers) & + ~GatewayIntents.GuildInvites, AlwaysDownloadUsers = true, AlwaysResolveStickers = false, AlwaysDownloadDefaultStickers = false, @@ -59,7 +61,7 @@ public static class Boyfriend { private static async Task Init() { var token = (await File.ReadAllTextAsync("token.txt")).Trim(); - Client.Log += Log; + Client.Log += x => Log(x); await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); @@ -75,7 +77,7 @@ public static class Boyfriend { // ReSharper disable once FunctionNeverReturns } - private static Task Log(LogMessage msg) { + public static Task Log(LogMessage msg) { switch (msg.Severity) { case LogSeverity.Critical: Console.ForegroundColor = ConsoleColor.DarkRed; @@ -103,7 +105,8 @@ public static class Boyfriend { } public static async Task WriteGuildConfigAsync(ulong id) { - await File.WriteAllTextAsync($"config_{id}.json", JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented)); + 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", diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index f34242d..ccba1e8 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -47,13 +47,15 @@ public sealed class CommandProcessor { var list = Context.Message.Content.Split("\n"); var cleanList = Context.Message.CleanContent.Split("\n"); - for (var i = 0; i < list.Length; i++) { - RunCommandOnLine(list[i], cleanList[i], config["Prefix"]); + for (var i = 0; i < list.Length; i++) + _tasks.Add(RunCommandOnLine(list[i], cleanList[i], config["Prefix"])); - if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync(); + try { Task.WaitAll(_tasks.ToArray()); } catch (AggregateException e) { + foreach (var ex in e.InnerExceptions) + await Boyfriend.Log(new LogMessage(LogSeverity.Error, nameof(CommandProcessor), + "Exception while executing commands", ex)); } - await Task.WhenAll(_tasks); _tasks.Clear(); if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfigAsync(guild.Id); @@ -61,7 +63,7 @@ public sealed class CommandProcessor { SendFeedbacks(); } - private void RunCommandOnLine(string line, string cleanLine, string prefix) { + private async Task RunCommandOnLine(string line, string cleanLine, string prefix) { var prefixed = line.StartsWith(prefix); if (!prefixed && !line.StartsWith(Mention)) return; foreach (var command in Commands) { @@ -70,7 +72,8 @@ public sealed class CommandProcessor { var args = lineNoMention.Trim().Split().Skip(1).ToArray(); var cleanArgs = cleanLine.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); - _tasks.Add(command.RunAsync(this, args, cleanArgs)); + await command.RunAsync(this, args, cleanArgs); + if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync(); return; } } diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index d9c2bd6..1c2316d 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -74,10 +74,15 @@ public static class Utils { } public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) { - if (channel is null || text.Length is 0 or > 2000) - throw new Exception($"Message length is out of range: {text.Length}"); + try { + if (channel is null || text.Length is 0 or > 2000) + throw new Exception($"Message length is out of range: {text.Length}"); - await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); + await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); + } catch (Exception e) { + await Boyfriend.Log(new LogMessage(LogSeverity.Error, nameof(Utils), + "Exception while silently sending message", e)); + } } public static RequestOptions GetRequestOptions(string reason) { @@ -154,23 +159,28 @@ public static class Utils { public static async Task SendEarlyEventStartNotificationAsync(SocketTextChannel? channel, SocketGuildEvent scheduledEvent, int minuteOffset) { - 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); + 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); - var receivers = eventConfig["EventStartedReceivers"]; - var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); - var mentions = Boyfriend.StringBuilder; + 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(); + 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) { From 917e7e5775e95193e9eddbb06cc59c0ca3e573f1 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 16 Nov 2022 23:28:05 +0500 Subject: [PATCH 043/329] Optimize event handlers until we update to .NET 7 --- Boyfriend/EventHandler.cs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 3f89d7c..a7a4e9f 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,4 +1,5 @@ -using Discord; +using System.Diagnostics.CodeAnalysis; +using Discord; using Discord.Rest; using Discord.WebSocket; @@ -8,16 +9,17 @@ public static class EventHandler { private static readonly DiscordSocketClient Client = Boyfriend.Client; private static bool _sendReadyMessages = true; + [SuppressMessage("ReSharper", "ConvertClosureToMethodGroup")] public static void InitEvents() { - Client.Ready += ReadyEvent; - Client.MessageDeleted += MessageDeletedEvent; - Client.MessageReceived += MessageReceivedEvent; - Client.MessageUpdated += MessageUpdatedEvent; - Client.UserJoined += UserJoinedEvent; - Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; - Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; - Client.GuildScheduledEventStarted += ScheduledEventStartedEvent; - Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; + Client.Ready += () => ReadyEvent(); + Client.MessageDeleted += (x, y) => MessageDeletedEvent(x, y); + Client.MessageReceived += x => MessageReceivedEvent(x); + Client.MessageUpdated += (x, y, z) => MessageUpdatedEvent(x, y, z); + Client.UserJoined += x => UserJoinedEvent(x); + Client.GuildScheduledEventCreated += x => ScheduledEventCreatedEvent(x); + Client.GuildScheduledEventCancelled += x => ScheduledEventCancelledEvent(x); + Client.GuildScheduledEventStarted += x => ScheduledEventStartedEvent(x); + Client.GuildScheduledEventCompleted += x => ScheduledEventCompletedEvent(x); } private static Task ReadyEvent() { From 8f10f37d2e7e2604b5702d2b7e385761bf1fbfe5 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 20 Nov 2022 15:26:40 +0500 Subject: [PATCH 044/329] Fix event notifications not being delivered due to invalid config reference --- Boyfriend/Utils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 1c2316d..95538b4 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -184,6 +184,6 @@ public static class Utils { } public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) { - return guild.GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(guild.Id)["EventCreatedChannel"])); + return guild.GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(guild.Id)["EventNotificationChannel"])); } } From e3cdcbcf0cf806c015884c098c27de41be99de92 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 20 Nov 2022 15:46:12 +0500 Subject: [PATCH 045/329] Fix newlines in event start notifications --- Boyfriend/EventHandler.cs | 2 +- Boyfriend/Messages.Designer.cs | 2 +- Boyfriend/Messages.resx | 2 +- Boyfriend/Messages.ru.resx | 2 +- Boyfriend/Messages.tt-ru.resx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index a7a4e9f..99318a7 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -119,7 +119,7 @@ public static class EventHandler { var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); var descAndLink - = $"{Utils.Wrap(scheduledEvent.Description)}\nhttps://discord.com/events/{guild.Id}/{scheduledEvent.Id}"; + = $"\n{Utils.Wrap(scheduledEvent.Description)}\nhttps://discord.com/events/{guild.Id}/{scheduledEvent.Id}"; await Utils.SilentSendAsync(channel, string.Format(Messages.EventCreated, mentions, diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index f11dd7b..948681b 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -384,7 +384,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>!\n{4}. + /// Looks up a localized string similar to {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4}. /// internal static string EventCreated { get { diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index e98e804..b4054cb 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -160,7 +160,7 @@ I cannot use time-outs on other bots! Try to set a mute role in settings - {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>!\n{4} + {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4} Role for event creation notifications diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index e55cabe..0994efa 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -151,7 +151,7 @@ Начальная роль - {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!\n{4} + {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4} Роль для уведомлений о создании событий diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index ba3f2a2..4205065 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -151,7 +151,7 @@ базовое звание - {0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!\n{4} + {0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!{4} роль для уведомлений о создании квеста From c02f50355759ec03310f7b6b46d77533d8c18a56 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 20 Nov 2022 20:45:07 +0500 Subject: [PATCH 046/329] Fix wrong languages being used in some places --- Boyfriend/EventHandler.cs | 4 ++++ Boyfriend/Utils.cs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 99318a7..a7805b6 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -110,6 +110,7 @@ public static class EventHandler { var guild = scheduledEvent.Guild; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = Utils.GetEventNotificationChannel(guild); + Utils.SetCurrentLanguage(guild.Id); if (channel is not null) { var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); @@ -137,6 +138,7 @@ public static class EventHandler { var guild = scheduledEvent.Guild; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = Utils.GetEventNotificationChannel(guild); + Utils.SetCurrentLanguage(guild.Id); if (channel is not null) await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name), eventConfig["FrowningFace"] is "true" ? $" {Messages.SettingsFrowningFace}" : "")); @@ -146,6 +148,7 @@ public static class EventHandler { var guild = scheduledEvent.Guild; var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var channel = Utils.GetEventNotificationChannel(guild); + Utils.SetCurrentLanguage(guild.Id); if (channel is not null) { var receivers = eventConfig["EventStartedReceivers"]; @@ -167,6 +170,7 @@ public static class EventHandler { private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { var guild = scheduledEvent.Guild; var channel = Utils.GetEventNotificationChannel(guild); + Utils.SetCurrentLanguage(guild.Id); if (channel is not null) await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), Utils.GetHumanizedTimeOffset(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 95538b4..6fe8452 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -148,12 +148,14 @@ public static class Utils { 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); } @@ -165,6 +167,7 @@ public static class Utils { 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"])); From 1258496697d1c7b2e40ed641dc751186730bb22f Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Mon, 5 Dec 2022 17:04:27 +0300 Subject: [PATCH 047/329] Unhardcoded bot mention + some small fixes (#7) Co-authored-by: Octol1ttle --- .gitignore | 3 +- Boyfriend/Boyfriend.cs | 5 +- Boyfriend/CommandProcessor.cs | 4 +- Boyfriend/Messages.tt-ru.resx | 820 +++++++++++++++++++--------------- 4 files changed, 467 insertions(+), 365 deletions(-) diff --git a/.gitignore b/.gitignore index 2976bab..3816d9a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +/.vs/ diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index f33caec..f7d00d4 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Text; using Discord; using Discord.WebSocket; @@ -21,8 +21,7 @@ public static class Boyfriend { }; private static readonly List> ActivityList = new() { - Tuple.Create(new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), - new TimeSpan(0, 3, 18)), + Tuple.Create(new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)), Tuple.Create(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)), Tuple.Create(new Game("Kurokotei - Scattered Faith", ActivityType.Listening), new TimeSpan(0, 8, 21)), Tuple.Create(new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index ccba1e8..00bc77e 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using Boyfriend.Commands; using Discord; using Discord.Commands; @@ -13,7 +13,7 @@ public sealed class CommandProcessor { private const string NoAccess = ":no_entry_sign: "; private const string CantInteract = ":vertical_traffic_light: "; - private const string Mention = "<@855023234407333888>"; + private static readonly string Mention = $"<@{Boyfriend.Client.CurrentUser.Id}>"; public static readonly ICommand[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index 4205065..77bb6f3 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -1,360 +1,462 @@ + - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - {0}я родился! - - - вырезано {0} в канале {1}: {2} - - - переделано {0}: {1} -> {2} - - - {0}, добро пожаловать на сервер {1} - - - брах! - - - брох! - - - брух! - - - у меня прав нету, сделай что нибудь. - - - у тебя прав нету, твои проблемы. - - - здарова, тебя крч забанил {0} на сервере {1} за {2} - - - время бана закончиловсь - - - ты выбрал менее {0} сообщений - - - ты выбрал более {0} сообщений - - - туториал по приколам: - - - здарова, тебя крч кикнул {0} на сервере {1} за {2} - - - мс - - - шизоид уже замучен! - - - *тут ничего нет* - - - *тут ничего нет* - - - настройки: - - - язык - - - префикс - - - удалять звание при муте - - - разглашать о том что пришел новый шизоид - - - роль замученного - - - канал бот-уведомлений - - - такого языка нету, ты шо - - - да - - - нъет - - - шизик не забанен - - - шизоид не замучен! - - - кто-то решил поумничать и обошел роль мута. я ее вернул. - - - приветствие - - - выбери число от {0} до {1} вместо {2}! - - - забанен {0} на{1}: {2} - - - шизик не на этом сервере - - - такой прикол не существует - - - получать инфу о рождении бота - - - криво настроил прикол, давай по новой - - - этого звания нету, ты шо - - - этого канала нету, ты шо - - - я не украл звание {0} в связи с ошибкой! {1} - - - ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим - - - я не могу замутить ботов, сделай что нибудь - - - базовое звание - - - {0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!{4} - - - роль для уведомлений о создании квеста - - - канал для уведомлений о квестах - - - получатели уведомлений о начале квеста - - - {0}квест {1} начинается в {2}! - - - оъмъомоъемъъео(((( - - - квест {0} отменен!{1} - - - квест {0} завершен! все это длилось {1} - - - всегда - - - удалено {0} сообщений в {1} - - - выгнан {0}: {1} - - - замучен {0} на{1}: {2} - - - раззабанен {0}: {1} - - - раззамучен {0}: {1} - - - ты все сломал! значение прикола `{0}` и так {1} - - - *тут ничего нет* - - - прикол для `{0}` теперь установлен на {1} - - - возводит великий банхаммер над шизоидом - - - удаляет сообщения. сколько хош, столько и удалит - - - показывает то, что ты сейчас видишь прямо сейчас - - - выпинывает шизоида - - - мутит шизоида - - - показывает пинг (сверхмегаточный (нет)) - - - настройки бота под этот сервер - - - отводит великий банхаммер от шизоида - - - раззамучивает шизоида - - - укажи целое число от {0} до {1} - - - укажи самого шизика - - - надо указать юзверя вместо {0}! - - - укажи самого шизика - - - укажи шизоида сервера вместо {0}! - - - бан - - - Ты не можешь управлять сообщениями этого сервера! - - - кик шизиков нельзя - - - тебе нельзя управлять шизоидами - - - тебе нельзя редактировать дурку - - - я не могу ваще никого банить чел. - - - я не могу исправлять орфографический кринж участников, сделай что нибудь. - - - я не могу ваще никого кикать чел. - - - я не могу контроллировать за всеми ними, сделай что нибудь. - - - я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь. - - - укажи зачем банить шизика - - - укажи зачем кикать шизика - - - укажи зачем мутить шизика - - - укажи настройку которую менять нужно - - - укажи зачем раззабанивать шизика - - - укажи зачам размучивать шизика - - - че ты там вякнул? - - - бан админу нельзя - - - бан этому шизику нельзя - - - самобан нельзя - - - я не могу его забанить... - - - кик админу нельзя - - - самокик нельзя - - - че ты там вякнул? - - - я не могу его кикнуть... - - - кик этому шизику нельзя - - - мут админу нельзя - - - самомут нельзя - - - че ты там вякнул? - - - я не могу его замутить... - - - мут этому шизику нельзя - - - ты шо далбайоп шоле, админ замозамучался, не трожь - - - ты замучен. - - - ... - - - тебе нельзя раззамучивать - - - я не могу его раззамутить... - - - {0}квест {1} начнется <t:{2}:R>! - - - заранее пнуть в минутах до начала квеста - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {0}я родился! + + + вырезано {0} в канале {1}: {2} + + + переделано {0}: {1} -> {2} + + + {0}, добро пожаловать на сервер {1} + + + брах! + + + брох! + + + брух! + + + у меня прав нету, сделай что нибудь. + + + у тебя прав нету, твои проблемы. + + + здарова, тебя крч забанил {0} на сервере {1} за {2} + + + время бана закончиловсь + + + ты выбрал менее {0} сообщений + + + ты выбрал более {0} сообщений + + + туториал по приколам: + + + здарова, тебя крч кикнул {0} на сервере {1} за {2} + + + мс + + + шизоид уже замучен! + + + *тут ничего нет* + + + *тут ничего нет* + + + настройки: + + + язык + + + префикс + + + удалять звание при муте + + + разглашать о том что пришел новый шизоид + + + роль замученного + + + канал бот-уведомлений + + + такого языка нету, ты шо + + + да + + + нъет + + + шизик не забанен + + + шизоид не замучен! + + + кто-то решил поумничать и обошел роль мута. я ее вернул. + + + приветствие + + + выбери число от {0} до {1} вместо {2}! + + + забанен {0} на{1}: {2} + + + шизик не на этом сервере + + + такой прикол не существует + + + получать инфу о рождении бота + + + криво настроил прикол, давай по новой + + + этого звания нету, ты шо + + + этого канала нету, ты шо + + + я не украл звание {0} в связи с ошибкой! {1} + + + ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим + + + я не могу замутить ботов, сделай что нибудь + + + базовое звание + + + {0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!{4} + + + роль для уведомлений о создании квеста + + + канал для уведомлений о квестах + + + получатели уведомлений о начале квеста + + + {0}квест {1} начинается в {2}! + + + оъмъомоъемъъео(((( + + + квест {0} отменен!{1} + + + квест {0} завершен! все это длилось {1} + + + всегда + + + удалено {0} сообщений в {1} + + + выгнан {0}: {1} + + + замучен {0} на{1}: {2} + + + раззабанен {0}: {1} + + + раззамучен {0}: {1} + + + ты все сломал! значение прикола `{0}` и так {1} + + + *тут ничего нет* + + + прикол для `{0}` теперь установлен на {1} + + + возводит великий банхаммер над шизоидом + + + удаляет сообщения. сколько хош, столько и удалит + + + показывает то, что ты сейчас видишь прямо сейчас + + + выпинывает шизоида + + + мутит шизоида + + + показывает пинг (сверхмегаточный (нет)) + + + настройки бота под этот сервер + + + отводит великий банхаммер от шизоида + + + раззамучивает шизоида + + + укажи целое число от {0} до {1} + + + укажи самого шизика + + + надо указать юзверя вместо {0}! + + + укажи самого шизика + + + укажи шизоида сервера вместо {0}! + + + бан + + + тебе нельзя иметь власть над сообщениями шизоидов + + + кик шизиков нельзя + + + тебе нельзя управлять шизоидами + + + тебе нельзя редактировать дурку + + + я не могу ваще никого банить чел. + + + я не могу исправлять орфографический кринж участников, сделай что нибудь. + + + я не могу ваще никого кикать чел. + + + я не могу контроллировать за всеми ними, сделай что нибудь. + + + я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь. + + + укажи зачем банить шизика + + + укажи зачем кикать шизика + + + укажи зачем мутить шизика + + + укажи настройку которую менять нужно + + + укажи зачем раззабанивать шизика + + + укажи зачам размучивать шизика + + + ээбля френдли фаер огонь по своим + + + бан админу нельзя + + + бан этому шизику нельзя + + + самобан нельзя + + + я не могу его забанить... + + + кик админу нельзя + + + самокик нельзя + + + ээбля френдли фаер огонь по своим + + + я не могу его кикнуть... + + + кик этому шизику нельзя + + + мут админу нельзя + + + самомут нельзя + + + ээбля френдли фаер огонь по своим + + + я не могу его замутить... + + + мут этому шизику нельзя + + + ты шо далбайоп шоле, админ замозамучался, не трожь + + + ты замучен. + + + ... + + + тебе нельзя раззамучивать + + + я не могу его раззамутить... + + + {0}квест {1} начнется <t:{2}:R>! + + + заранее пнуть в минутах до начала квеста + + \ No newline at end of file From 2596b48bde1552bcf41abb2b8443d0115ac190a9 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Tue, 6 Dec 2022 18:33:46 +0300 Subject: [PATCH 048/329] Keep reply emojis as consts in a separate class (#8) --- Boyfriend/CommandProcessor.cs | 42 ++++++++++++--------------- Boyfriend/Commands/BanCommand.cs | 4 +-- Boyfriend/Commands/HelpCommand.cs | 4 +-- Boyfriend/Commands/KickCommand.cs | 4 +-- Boyfriend/Commands/MuteCommand.cs | 14 ++++----- Boyfriend/Commands/PingCommand.cs | 4 +-- Boyfriend/Commands/SettingsCommand.cs | 20 ++++++------- Boyfriend/Commands/UnmuteCommand.cs | 6 ++-- Boyfriend/ReplyEmojis.cs | 19 ++++++++++++ 9 files changed, 65 insertions(+), 52 deletions(-) create mode 100644 Boyfriend/ReplyEmojis.cs diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index 00bc77e..574ad2a 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -7,12 +7,6 @@ using Discord.WebSocket; namespace Boyfriend; public sealed class CommandProcessor { - private const string Success = ":white_check_mark: "; - private const string MissingArgument = ":keyboard: "; - private const string InvalidArgument = ":construction: "; - private const string NoAccess = ":no_entry_sign: "; - private const string CantInteract = ":vertical_traffic_light: "; - private static readonly string Mention = $"<@{Boyfriend.Client.CurrentUser.Id}>"; public static readonly ICommand[] Commands = { @@ -79,7 +73,7 @@ public sealed class CommandProcessor { } public void Reply(string response, string? customEmoji = null) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{customEmoji ?? Success}{response}", Context.Message); + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{customEmoji ?? ReplyEmojis.Success} {response}", Context.Message); } public void Audit(string action, bool isPublic = true) { @@ -111,14 +105,14 @@ public sealed class CommandProcessor { public string? GetRemaining(string[] from, int startIndex, string? argument) { if (startIndex >= from.Length && argument is not null) Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{MissingArgument}{Utils.GetMessage($"Missing{argument}")}", Context.Message); + $"{ReplyEmojis.MissingArgument} {Utils.GetMessage($"Missing{argument}")}", Context.Message); else return string.Join(" ", from, startIndex, from.Length - startIndex); return null; } public SocketUser? GetUser(string[] args, string[] cleanArgs, int index, string? argument) { if (index >= args.Length) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Messages.MissingUser}", + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", Context.Message); return null; } @@ -126,14 +120,14 @@ public sealed class CommandProcessor { var user = Boyfriend.Client.GetUser(Utils.ParseMention(args[index])); if (user is null && argument is not null) Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{InvalidArgument}{string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}", + $"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}", Context.Message); return user; } public bool HasPermission(GuildPermission permission) { if (!Context.Guild.CurrentUser.GuildPermissions.Has(permission)) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{NoAccess}{Utils.GetMessage($"BotCannot{permission}")}", + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"BotCannot{permission}")}", Context.Message); return false; } @@ -141,7 +135,7 @@ public sealed class CommandProcessor { if (Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission) || Context.Guild.OwnerId == Context.User.Id) return true; - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{NoAccess}{Utils.GetMessage($"UserCannot{permission}")}", + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}", Context.Message); return false; } @@ -152,7 +146,7 @@ public sealed class CommandProcessor { public SocketGuildUser? GetMember(string[] args, string[] cleanArgs, int index, string? argument) { if (index >= args.Length) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Messages.MissingMember}", + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingMember}", Context.Message); return null; } @@ -160,7 +154,7 @@ public sealed class CommandProcessor { var member = Context.Guild.GetUser(Utils.ParseMention(args[index])); if (member is null && argument is not null) Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{InvalidArgument}{string.Format(Messages.InvalidMember, Utils.Wrap(cleanArgs[index]))}", + $"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidMember, Utils.Wrap(cleanArgs[index]))}", Context.Message); return member; } @@ -171,7 +165,7 @@ public sealed class CommandProcessor { public ulong? GetBan(string[] args, int index) { if (index >= args.Length) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{MissingArgument}{Messages.MissingUser}", + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", Context.Message); return null; } @@ -188,14 +182,14 @@ public sealed class CommandProcessor { public int? GetNumberRange(string[] args, int index, int min, int max, string? argument) { if (index >= args.Length) { Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{MissingArgument}{string.Format(Messages.MissingNumber, min.ToString(), max.ToString())}", + $"{ReplyEmojis.MissingArgument} {string.Format(Messages.MissingNumber, min.ToString(), max.ToString())}", Context.Message); return null; } if (!int.TryParse(args[index], out var i)) { Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{InvalidArgument}{string.Format(Utils.GetMessage($"{argument}Invalid"), min.ToString(), max.ToString(), Utils.Wrap(args[index]))}", + $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}Invalid"), min.ToString(), max.ToString(), Utils.Wrap(args[index]))}", Context.Message); return null; } @@ -203,14 +197,14 @@ public sealed class CommandProcessor { if (argument is null) return i; if (i < min) { Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{InvalidArgument}{string.Format(Utils.GetMessage($"{argument}TooSmall"), min.ToString())}", + $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooSmall"), min.ToString())}", Context.Message); return null; } if (i <= max) return i; Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{InvalidArgument}{string.Format(Utils.GetMessage($"{argument}TooLarge"), max.ToString())}", + $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooLarge"), max.ToString())}", Context.Message); return null; } @@ -252,31 +246,31 @@ public sealed class CommandProcessor { public bool CanInteractWith(SocketGuildUser user, string action) { if (Context.User.Id == user.Id) { Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Themselves")}", Context.Message); + $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Themselves")}", Context.Message); return false; } if (Context.Guild.CurrentUser.Id == user.Id) { Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Bot")}", Context.Message); + $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Bot")}", Context.Message); return false; } if (Context.Guild.Owner.Id == user.Id) { Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Owner")}", Context.Message); + $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Owner")}", Context.Message); return false; } if (Context.Guild.CurrentUser.Hierarchy <= user.Hierarchy) { Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{CantInteract}{Utils.GetMessage($"BotCannot{action}Target")}", Context.Message); + $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"BotCannot{action}Target")}", Context.Message); return false; } if (Context.Guild.Owner.Id == Context.User.Id || GetMember().Hierarchy > user.Hierarchy) return true; Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{CantInteract}{Utils.GetMessage($"UserCannot{action}Target")}", Context.Message); + $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Target")}", Context.Message); return false; } } diff --git a/Boyfriend/Commands/BanCommand.cs b/Boyfriend/Commands/BanCommand.cs index 78cae2c..561edb1 100644 --- a/Boyfriend/Commands/BanCommand.cs +++ b/Boyfriend/Commands/BanCommand.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; using Discord.WebSocket; namespace Boyfriend.Commands; @@ -29,7 +29,7 @@ public sealed class BanCommand : ICommand { var feedback = string.Format(Messages.FeedbackUserBanned, toBan.Mention, Utils.GetHumanizedTimeOffset(duration), Utils.Wrap(reason)); - cmd.Reply(feedback, ":hammer: "); + cmd.Reply(feedback, ReplyEmojis.Banned); cmd.Audit(feedback); if (duration.TotalSeconds > 0) diff --git a/Boyfriend/Commands/HelpCommand.cs b/Boyfriend/Commands/HelpCommand.cs index 948b716..0fc91d8 100644 --- a/Boyfriend/Commands/HelpCommand.cs +++ b/Boyfriend/Commands/HelpCommand.cs @@ -1,4 +1,4 @@ -using Humanizer; +using Humanizer; namespace Boyfriend.Commands; @@ -12,7 +12,7 @@ public sealed class HelpCommand : ICommand { foreach (var command in CommandProcessor.Commands) toSend.Append( $"\n`{prefix}{command.Aliases[0]}`: {Utils.GetMessage($"CommandDescription{command.Aliases[0].Titleize()}")}"); - cmd.Reply(toSend.ToString(), ":page_facing_up: "); + cmd.Reply(toSend.ToString(), ReplyEmojis.Help); toSend.Clear(); return Task.CompletedTask; diff --git a/Boyfriend/Commands/KickCommand.cs b/Boyfriend/Commands/KickCommand.cs index 5a32f93..9e4cfe4 100644 --- a/Boyfriend/Commands/KickCommand.cs +++ b/Boyfriend/Commands/KickCommand.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; using Discord.WebSocket; namespace Boyfriend.Commands; @@ -24,7 +24,7 @@ public sealed class KickCommand : ICommand { await toKick.KickAsync(guildKickMessage); var format = string.Format(Messages.FeedbackMemberKicked, toKick.Mention, Utils.Wrap(reason)); - cmd.Reply(format, ":police_car: "); + cmd.Reply(format, ReplyEmojis.Kicked); cmd.Audit(format); } } diff --git a/Boyfriend/Commands/MuteCommand.cs b/Boyfriend/Commands/MuteCommand.cs index fb258ca..b01f1f1 100644 --- a/Boyfriend/Commands/MuteCommand.cs +++ b/Boyfriend/Commands/MuteCommand.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; using Discord.Net; using Discord.WebSocket; @@ -20,7 +20,7 @@ public sealed class MuteCommand : ICommand { || (toMute.TimedOutUntil is not null && toMute.TimedOutUntil.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds())) { - cmd.Reply(Messages.MemberAlreadyMuted, ":x: "); + cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error); return; } @@ -30,7 +30,7 @@ public sealed class MuteCommand : ICommand { foreach (var roleId in mutedRemovedRoles) await toMute.AddRoleAsync(roleId); rolesRemoved.Remove(toMute.Id); cmd.ConfigWriteScheduled = true; - cmd.Reply(Messages.RolesReturned, ":warning: "); + cmd.Reply(Messages.RolesReturned, ReplyEmojis.Warning); } if (cmd.HasPermission(GuildPermission.ModerateMembers) && cmd.CanInteractWith(toMute, "Mute")) @@ -55,7 +55,7 @@ public sealed class MuteCommand : ICommand { rolesRemoved.Add(userRole.Id); } catch (HttpException e) { cmd.Reply(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.Wrap(e.Reason)), - ":warning: "); + ReplyEmojis.Warning); } Boyfriend.GetRemovedRoles(guild.Id).Add(toMute.Id, rolesRemoved.AsReadOnly()); @@ -68,12 +68,12 @@ public sealed class MuteCommand : ICommand { await Task.FromResult(Utils.DelayedUnmuteAsync(cmd, toMute, Messages.PunishmentExpired, duration)); } else { if (!hasDuration || duration.TotalDays > 28) { - cmd.Reply(Messages.DurationRequiredForTimeOuts, ":x: "); + cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error); return; } if (toMute.IsBot) { - cmd.Reply(Messages.CannotTimeOutBot, ":x: "); + cmd.Reply(Messages.CannotTimeOutBot, ReplyEmojis.Error); return; } @@ -83,7 +83,7 @@ public sealed class MuteCommand : ICommand { var feedback = string.Format(Messages.FeedbackMemberMuted, toMute.Mention, Utils.GetHumanizedTimeOffset(duration), Utils.Wrap(reason)); - cmd.Reply(feedback, ":mute: "); + cmd.Reply(feedback, ReplyEmojis.Muted); cmd.Audit(feedback); } } diff --git a/Boyfriend/Commands/PingCommand.cs b/Boyfriend/Commands/PingCommand.cs index c9c9b8e..88034a5 100644 --- a/Boyfriend/Commands/PingCommand.cs +++ b/Boyfriend/Commands/PingCommand.cs @@ -1,4 +1,4 @@ -namespace Boyfriend.Commands; +namespace Boyfriend.Commands; public sealed class PingCommand : ICommand { public string[] Aliases { get; } = { "ping", "latency", "pong", "пинг", "задержка", "понг" }; @@ -10,7 +10,7 @@ public sealed class PingCommand : ICommand { .Append(Math.Abs(DateTimeOffset.Now.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds)) .Append(Messages.Milliseconds); - cmd.Reply(builder.ToString(), ":signal_strength: "); + cmd.Reply(builder.ToString(), ReplyEmojis.Ping); builder.Clear(); return Task.CompletedTask; diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 46cec36..1a522ac 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; namespace Boyfriend.Commands; @@ -33,7 +33,7 @@ public sealed class SettingsCommand : ICommand { .AppendFormat(format, currentValue).AppendLine(); } - cmd.Reply(currentSettings.ToString(), ":gear: "); + cmd.Reply(currentSettings.ToString(), ReplyEmojis.SettingsList); currentSettings.Clear(); return Task.CompletedTask; } @@ -51,7 +51,7 @@ public sealed class SettingsCommand : ICommand { } if (!exists) { - cmd.Reply(Messages.SettingDoesntExist, ":x: "); + cmd.Reply(Messages.SettingDoesntExist, ReplyEmojis.Error); return Task.CompletedTask; } @@ -64,7 +64,7 @@ public sealed class SettingsCommand : ICommand { value = value.Replace(" ", "").ToLower(); if (value.StartsWith(",") || value.Count(x => x is ',') > 1 || (!value.Contains("interested") && !value.Contains("users") && !value.Contains("role"))) { - cmd.Reply(Messages.InvalidSettingValue, ":x: "); + cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error); return Task.CompletedTask; } } @@ -77,7 +77,7 @@ public sealed class SettingsCommand : ICommand { _ => value }; if (!IsBool(value)) { - cmd.Reply(Messages.InvalidSettingValue, ":x: "); + cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error); return Task.CompletedTask; } } @@ -106,22 +106,22 @@ public sealed class SettingsCommand : ICommand { } else { if (value == config[selectedSetting]) { cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), - ":x: "); + ReplyEmojis.Error); return Task.CompletedTask; } if (selectedSetting is "Lang" && !Utils.CultureInfoCache.ContainsKey(value)) { - cmd.Reply(Messages.LanguageNotSupported, ":x: "); + cmd.Reply(Messages.LanguageNotSupported, ReplyEmojis.Error); return Task.CompletedTask; } if (selectedSetting.EndsWith("Channel") && guild.GetTextChannel(mention) is null) { - cmd.Reply(Messages.InvalidChannel, ":x: "); + cmd.Reply(Messages.InvalidChannel, ReplyEmojis.Error); return Task.CompletedTask; } if (selectedSetting.EndsWith("Role") && guild.GetRole(mention) is null) { - cmd.Reply(Messages.InvalidRole, ":x: "); + cmd.Reply(Messages.InvalidRole, ReplyEmojis.Error); return Task.CompletedTask; } @@ -138,7 +138,7 @@ public sealed class SettingsCommand : ICommand { cmd.ConfigWriteScheduled = true; var replyFormat = string.Format(Messages.FeedbackSettingsUpdated, localizedSelectedSetting, formattedValue); - cmd.Reply(replyFormat, ":control_knobs: "); + cmd.Reply(replyFormat, ReplyEmojis.SettingsSet); cmd.Audit(replyFormat, false); return Task.CompletedTask; } diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Boyfriend/Commands/UnmuteCommand.cs index 976ce61..f312231 100644 --- a/Boyfriend/Commands/UnmuteCommand.cs +++ b/Boyfriend/Commands/UnmuteCommand.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; using Discord.WebSocket; namespace Boyfriend.Commands; @@ -34,7 +34,7 @@ public sealed class UnmuteCommand : ICommand { } else { if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) { - cmd.Reply(Messages.MemberNotMuted, ":x: "); + cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error); return; } @@ -42,7 +42,7 @@ public sealed class UnmuteCommand : ICommand { } var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason)); - cmd.Reply(feedback, ":loud_sound: "); + cmd.Reply(feedback, ReplyEmojis.Unmuted); cmd.Audit(feedback); } } diff --git a/Boyfriend/ReplyEmojis.cs b/Boyfriend/ReplyEmojis.cs new file mode 100644 index 0000000..9460621 --- /dev/null +++ b/Boyfriend/ReplyEmojis.cs @@ -0,0 +1,19 @@ +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:"; + public const string NoPermission = ":no_entry_sign:"; + public const string CantInteract = ":vertical_traffic_light:"; + public const string Help = ":page_facing_up:"; + public const string SettingsList = ":gear:"; + public const string SettingsSet = ":control_knobs:"; + public const string Ping = ":signal_strength:"; + public const string Banned = ":hammer:"; + public const string Kicked = ":police_car:"; + public const string Muted = ":mute:"; + public const string Unmuted = ":loud_sound:"; +} From fc00558dce58250433bda92bea02d49bfcce392a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 8 Dec 2022 13:51:49 +0500 Subject: [PATCH 049/329] Update to .NET 7 --- Boyfriend/Boyfriend.cs | 8 ++++---- Boyfriend/Boyfriend.csproj | 16 ++++++++++------ Boyfriend/EventHandler.cs | 5 +++-- Boyfriend/Utils.cs | 9 ++++++--- global.json | 6 +++--- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index f7d00d4..aee4b8f 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -21,7 +21,8 @@ public static class Boyfriend { }; private static readonly List> ActivityList = new() { - Tuple.Create(new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)), + Tuple.Create(new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), + new TimeSpan(0, 3, 18)), Tuple.Create(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)), Tuple.Create(new Game("Kurokotei - Scattered Faith", ActivityType.Listening), new TimeSpan(0, 8, 21)), Tuple.Create(new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), @@ -67,13 +68,11 @@ public static class Boyfriend { EventHandler.InitEvents(); - while (true) { + while (ActivityList.Count > 0) foreach (var activity in ActivityList) { await Client.SetActivityAsync(activity.Item1); await Task.Delay(activity.Item2); } - } - // ReSharper disable once FunctionNeverReturns } public static Task Log(LogMessage msg) { @@ -153,3 +152,4 @@ public static class Boyfriend { return removedRoles; } } + diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj index e58826a..97db937 100644 --- a/Boyfriend/Boyfriend.csproj +++ b/Boyfriend/Boyfriend.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net7.0 enable enable default Boyfriend - l1ttle + Octol1ttle, mctaylors https://github.com/l1ttleO/Boyfriend-CSharp https://github.com/l1ttleO/Boyfriend-CSharp git @@ -17,16 +17,20 @@ - + true x64 none + + x64 + + - - - + + + diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index a7805b6..488daa3 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -62,7 +62,7 @@ public static class EventHandler { Utils.Wrap(msg.CleanContent)), guild.Id, mention); } - private static Task MessageReceivedEvent(SocketMessage messageParam) { + private static Task MessageReceivedEvent(IDeletable messageParam) { if (messageParam is not SocketUserMessage message) return Task.CompletedTask; _ = message.CleanContent.ToLower() switch { @@ -76,7 +76,7 @@ public static class EventHandler { return Task.CompletedTask; } - private static async Task MessageUpdatedEvent(Cacheable messageCached, SocketMessage messageSocket, + private static async Task MessageUpdatedEvent(Cacheable messageCached, IMessage messageSocket, ISocketMessageChannel channel) { var msg = messageCached.Value; if (channel is not SocketGuildChannel gChannel || msg is null or ISystemMessage || @@ -176,3 +176,4 @@ public static class EventHandler { Utils.GetHumanizedTimeOffset(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); } } + diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index 6fe8452..e9e34f0 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -11,7 +11,7 @@ using Humanizer.Localisation; namespace Boyfriend; -public static class Utils { +public static partial class Utils { private static readonly Dictionary ReflectionMessageCache = new(); public static readonly Dictionary CultureInfoCache = new() { @@ -48,7 +48,7 @@ public static class Utils { } public static ulong ParseMention(string mention) { - return ulong.TryParse(Regex.Replace(mention, "[^0-9]", ""), out var id) ? id : 0; + return ulong.TryParse(NumbersOnlyRegex().Replace(mention, ""), out var id) ? id : 0; } public static async Task SendDirectMessage(SocketUser user, string toSend) { @@ -70,7 +70,7 @@ public static class Utils { } public static void RemoveMuteRoleFromCache(ulong id) { - if (MuteRoleCache.ContainsKey(id)) MuteRoleCache.Remove(id); + MuteRoleCache.Remove(id); } public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) { @@ -189,4 +189,7 @@ public static class Utils { public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) { return guild.GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(guild.Id)["EventNotificationChannel"])); } + + [GeneratedRegex("[^0-9]")] + private static partial Regex NumbersOnlyRegex(); } diff --git a/global.json b/global.json index 4c1d7e5..36e1a9e 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "6.0.0", - "rollForward": "latestFeature", + "version": "7.0.0", + "rollForward": "latestMajor", "allowPrerelease": false } -} +} \ No newline at end of file From 938f918445c72b024499faeaf7d70f8dc850c688 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Fri, 9 Dec 2022 14:39:21 +0300 Subject: [PATCH 050/329] List supported languages if an invalid one is provided (#9) Co-authored-by: l1ttleO --- Boyfriend/Boyfriend.cs | 3 +- Boyfriend/Commands/SettingsCommand.cs | 6 +- Boyfriend/EventHandler.cs | 5 +- Boyfriend/Messages.Designer.cs | 5 +- Boyfriend/Messages.resx | 825 ++++++++++++++------------ Boyfriend/Messages.ru.resx | 822 ++++++++++++++----------- Boyfriend/Messages.tt-ru.resx | 2 +- Boyfriend/ReplyEmojis.cs | 2 +- 8 files changed, 934 insertions(+), 736 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index aee4b8f..839b1e8 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -21,8 +21,7 @@ public static class Boyfriend { }; private static readonly List> ActivityList = new() { - Tuple.Create(new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), - new TimeSpan(0, 3, 18)), + 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)), Tuple.Create(new Game("Kurokotei - Scattered Faith", ActivityType.Listening), new TimeSpan(0, 8, 21)), Tuple.Create(new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Boyfriend/Commands/SettingsCommand.cs index 1a522ac..c996ba5 100644 --- a/Boyfriend/Commands/SettingsCommand.cs +++ b/Boyfriend/Commands/SettingsCommand.cs @@ -111,7 +111,11 @@ public sealed class SettingsCommand : ICommand { } if (selectedSetting is "Lang" && !Utils.CultureInfoCache.ContainsKey(value)) { - cmd.Reply(Messages.LanguageNotSupported, ReplyEmojis.Error); + var langNotSupported = Boyfriend.StringBuilder.Append($"{Messages.LanguageNotSupported} "); + foreach (var lang in Utils.CultureInfoCache) langNotSupported.Append($"`{lang.Key}`, "); + langNotSupported.Remove(langNotSupported.Length - 2, 2); + cmd.Reply(langNotSupported.ToString(), ReplyEmojis.Error); + langNotSupported.Clear(); return Task.CompletedTask; } diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index 488daa3..f23b34a 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Discord; using Discord.Rest; using Discord.WebSocket; @@ -69,8 +69,7 @@ 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; diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 948681b..3c9b01c 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -18,7 +19,7 @@ namespace Boyfriend { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { @@ -528,7 +529,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Language not supported!. + /// Looks up a localized string similar to Language not supported! Supported languages:. /// internal static string LanguageNotSupported { get { diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index b4054cb..d6bcd39 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -1,369 +1,462 @@  - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {0}I'm ready! + + + Deleted message from {0} in channel {1}: {2} + + + Edited message in channel {0}: {1} -> {2} + + + {0}, welcome to {1} + + + Bah! + + + 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} + + + 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} + + + ms + + + Member is already muted! + + + Not specified + + + Not specified + + + Current settings: + + + Language + + + Prefix + + + Remove roles on mute + + + Send welcome messages + + + Starter role + + + Mute role + + + Bot log channel + + + Language not supported! Supported languages: + + + Yes + + + No + + + This user is not banned! + + + Member not muted! + + + Someone removed the mute role manually! I added back all roles that I removed during the mute + + + Welcome message + + + You need to specify an integer from {0} to {1} instead of {2}! + + + Banned {0} for{1}: {2} + + + The specified user is not a member of this server! + + + That setting doesn't exist! + + + Receive startup messages + + + Invalid setting value specified! + + + This role does not exist! + + + 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 + + + I cannot use time-outs on other bots! Try to set a mute role in settings + + + {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4} + + + Role for event creation notifications + + + Channel for event notifications + + + Event start notifications receivers + + + {0}Event {1} is starting at {2}! + + + :( + + + Event {0} is cancelled!{1} + + + Event {0} has completed! Duration: {1} + + + ever + + + Deleted {0} messages in {1} + + + Kicked {0}: {1} + + + Muted {0} for{1}: {2} + + + Unbanned {0}: {1} + + + Unmuted {0}: {1} + + + Nothing changed! `{0}` is already set to {1} + + + Not specified + + + Value of setting `{0}` is now set to {1} + + + Bans a user + + + Deletes a specified amount of messages in this channel + + + Shows this message + + + Kicks a member + + + Mutes a member + + + Shows (inaccurate) latency + + + Allows you to change certain preferences for this guild + + + Unbans a user + + + Unmutes a member + + + 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 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! + + + You cannot manage this guild! + + + I cannot ban users from this guild! + + + I cannot manage messages in this guild! + + + I cannot kick members from this guild! + + + I cannot moderate members in this guild! + + + I cannot manage this guild! + + + You need to specify a reason to ban this user! + + + You need to specify a reason to kick this member! + + + You need to specify a reason to mute this member! + + + You need to specify a reason to unban this user! + + + 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! + + + You cannot ban yourself! + + + You cannot ban me! + + + I cannot ban this user! + + + You cannot ban this user! + + + You cannot kick the owner of this guild! + + + You cannot kick yourself! + + + You cannot kick me! + + + I cannot kick this member! + + + You cannot kick this member! + + + You cannot mute the owner of this guild! + + + You cannot mute yourself! + + + You cannot mute me! + + + I cannot mute this member! + + + You cannot mute this member! + + + You don't need to unmute the owner of this guild! + + + You are muted! + + + ... + + + 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 diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 0994efa..25197b6 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -1,360 +1,462 @@ - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - {0}Я запустился! - - - Удалено сообщение от {0} в канале {1}: {2} - - - Отредактировано сообщение в канале {0}: {1} -> {2} - - - {0}, добро пожаловать на сервер {1} - - - Бап! - - - Боп! - - - Бип! - - - У меня недостаточно прав для выполнения этой команды! - - - У тебя недостаточно прав для выполнения этой команды! - - - Тебя забанил {0} на сервере {1} за {2} - - - Время наказания истекло - - - Указано менее {0} сообщений! - - - Указано более {0} сообщений! - - - Справка по командам: - - - Тебя кикнул {0} на сервере {1} за {2} - - - мс - - - Участник уже заглушен! - - - Не указан - - - Не указана - - - Текущие настройки: - - - Язык - - - Префикс - - - Удалять роли при муте - - - Отправлять приветствия - - - Роль мута - - - Канал бот-уведомлений - - - Язык не поддерживается! - - - Да - - - Нет - - - Этот пользователь не забанен! - - - Участник не заглушен! - - - Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте - - - Приветствие - - - Надо указать целое число от {0} до {1} вместо {2}! - - - Забанен {0} на{1}: {2} - - - Указанный пользователь не является участником этого сервера! - - - Такая настройка не существует! - - - Получать сообщения о запуске - - - Указано недействительное значение для настройки! - - - Эта роль не существует! - - - Этот канал не существует! - - - Я не смог забрать роль {0} в связи с ошибкой! {1} - - - Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках - - - Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках - - - Начальная роль - - - {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4} - - - Роль для уведомлений о создании событий - - - Канал для уведомлений о событиях - - - Получатели уведомлений о начале событий - - - {0}Событие {1} начинается в {2}! - - - :( - - - Событие {0} отменено!{1} - - - Событие {0} завершено! Продолжительность: {1} - - - всегда - - - Удалено {0} сообщений в {1} - - - Выгнан {0}: {1} - - - Заглушен {0} на{1}: {2} - - - Возвращён из бана {0}: {1} - - - Разглушен {0}: {1} - - - Ничего не изменилось! Значение настройки `{0}` уже {1} - - - Не указано - - - Значение настройки `{0}` теперь установлено на {1} - - - Банит пользователя - - - Удаляет указанное количество сообщений в этом канале - - - Показывает эту справку - - - Выгоняет участника - - - Глушит участника - - - Показывает (неточную) задержку - - - Позволяет менять некоторые настройки под этот сервер - - - Возвращает пользователя из бана - - - Разглушает участника - - - Надо указать целое число от {0} до {1}! - - - Надо указать пользователя! - - - Надо указать пользователя вместо {0}! - - - Надо указать участника сервера! - - - Надо указать участника сервера вместо {0}! - - - Ты не можешь банить пользователей на этом сервере! - - - Ты не можешь управлять сообщениями этого сервера! - - - Ты не можешь выгонять участников с этого сервера! - - - Ты не можешь модерировать участников этого сервера! - - - Ты не можешь настраивать этот сервер! - - - Я не могу банить пользователей на этом сервере! - - - Я не могу управлять сообщениями этого сервера! - - - Я не могу выгонять участников с этого сервера! - - - Я не могу модерировать участников этого сервера! - - - Я не могу настраивать этот сервер! - - - Надо указать причину для бана этого участника! - - - Надо указать причину для кика этого участника! - - - Надо указать причину для мута этого участника! - - - Надо указать настройку, которую нужно изменить! - - - Надо указать причину для разбана этого пользователя! - - - Надо указать причину для размута этого участника! - - - Ты не можешь меня забанить! - - - Ты не можешь забанить владельца этого сервера! - - - Ты не можешь забанить этого участника! - - - Ты не можешь себя забанить! - - - Я не могу забанить этого пользователя! - - - Ты не можешь выгнать владельца этого сервера! - - - Ты не можешь себя выгнать! - - - Ты не можешь меня выгнать! - - - Я не могу выгнать этого участника - - - Ты не можешь выгнать этого участника! - - - Ты не можешь заглушить владельца этого сервера! - - - Ты не можешь себя заглушить! - - - Ты не можешь заглушить меня! - - - Я не могу заглушить этого пользователя! - - - Ты не можешь заглушить этого участника! - - - Тебе не надо возвращать из мута владельца этого сервера! - - - Ты заглушен! - - - ... - - - Ты не можешь вернуть из мута этого пользователя! - - - Я не могу вернуть из мута этого пользователя! - - - {0}Событие {1} начнется <t:{2}:R>! - - - Офсет отправки преждевременного уведомления о начале события - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {0}Я запустился! + + + Удалено сообщение от {0} в канале {1}: {2} + + + Отредактировано сообщение в канале {0}: {1} -> {2} + + + {0}, добро пожаловать на сервер {1} + + + Бап! + + + Боп! + + + Бип! + + + У меня недостаточно прав для выполнения этой команды! + + + У тебя недостаточно прав для выполнения этой команды! + + + Тебя забанил {0} на сервере {1} за {2} + + + Время наказания истекло + + + Указано менее {0} сообщений! + + + Указано более {0} сообщений! + + + Справка по командам: + + + Тебя кикнул {0} на сервере {1} за {2} + + + мс + + + Участник уже заглушен! + + + Не указан + + + Не указана + + + Текущие настройки: + + + Язык + + + Префикс + + + Удалять роли при муте + + + Отправлять приветствия + + + Роль мута + + + Канал бот-уведомлений + + + Язык не поддерживается! Поддерживаемые языки: + + + Да + + + Нет + + + Этот пользователь не забанен! + + + Участник не заглушен! + + + Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте + + + Приветствие + + + Надо указать целое число от {0} до {1} вместо {2}! + + + Забанен {0} на{1}: {2} + + + Указанный пользователь не является участником этого сервера! + + + Такая настройка не существует! + + + Получать сообщения о запуске + + + Указано недействительное значение для настройки! + + + Эта роль не существует! + + + Этот канал не существует! + + + Я не смог забрать роль {0} в связи с ошибкой! {1} + + + Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках + + + Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках + + + Начальная роль + + + {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4} + + + Роль для уведомлений о создании событий + + + Канал для уведомлений о событиях + + + Получатели уведомлений о начале событий + + + {0}Событие {1} начинается в {2}! + + + :( + + + Событие {0} отменено!{1} + + + Событие {0} завершено! Продолжительность: {1} + + + всегда + + + Удалено {0} сообщений в {1} + + + Выгнан {0}: {1} + + + Заглушен {0} на{1}: {2} + + + Возвращён из бана {0}: {1} + + + Разглушен {0}: {1} + + + Ничего не изменилось! Значение настройки `{0}` уже {1} + + + Не указано + + + Значение настройки `{0}` теперь установлено на {1} + + + Банит пользователя + + + Удаляет указанное количество сообщений в этом канале + + + Показывает эту справку + + + Выгоняет участника + + + Глушит участника + + + Показывает (неточную) задержку + + + Позволяет менять некоторые настройки под этот сервер + + + Возвращает пользователя из бана + + + Разглушает участника + + + Надо указать целое число от {0} до {1}! + + + Надо указать пользователя! + + + Надо указать пользователя вместо {0}! + + + Надо указать участника сервера! + + + Надо указать участника сервера вместо {0}! + + + Ты не можешь банить пользователей на этом сервере! + + + Ты не можешь управлять сообщениями этого сервера! + + + Ты не можешь выгонять участников с этого сервера! + + + Ты не можешь модерировать участников этого сервера! + + + Ты не можешь настраивать этот сервер! + + + Я не могу банить пользователей на этом сервере! + + + Я не могу управлять сообщениями этого сервера! + + + Я не могу выгонять участников с этого сервера! + + + Я не могу модерировать участников этого сервера! + + + Я не могу настраивать этот сервер! + + + Надо указать причину для бана этого участника! + + + Надо указать причину для кика этого участника! + + + Надо указать причину для мута этого участника! + + + Надо указать настройку, которую нужно изменить! + + + Надо указать причину для разбана этого пользователя! + + + Надо указать причину для размута этого участника! + + + Ты не можешь меня забанить! + + + Ты не можешь забанить владельца этого сервера! + + + Ты не можешь забанить этого участника! + + + Ты не можешь себя забанить! + + + Я не могу забанить этого пользователя! + + + Ты не можешь выгнать владельца этого сервера! + + + Ты не можешь себя выгнать! + + + Ты не можешь меня выгнать! + + + Я не могу выгнать этого участника + + + Ты не можешь выгнать этого участника! + + + Ты не можешь заглушить владельца этого сервера! + + + Ты не можешь себя заглушить! + + + Ты не можешь заглушить меня! + + + Я не могу заглушить этого пользователя! + + + Ты не можешь заглушить этого участника! + + + Тебе не надо возвращать из мута владельца этого сервера! + + + Ты заглушен! + + + ... + + + Ты не можешь вернуть из мута этого пользователя! + + + Я не могу вернуть из мута этого пользователя! + + + {0}Событие {1} начнется <t:{2}:R>! + + + Офсет отправки преждевременного уведомления о начале события + + \ No newline at end of file diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index 77bb6f3..3439ed2 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -196,7 +196,7 @@ канал бот-уведомлений - такого языка нету, ты шо + такого языка нету, ты шо, есть только такие: да diff --git a/Boyfriend/ReplyEmojis.cs b/Boyfriend/ReplyEmojis.cs index 9460621..c26d420 100644 --- a/Boyfriend/ReplyEmojis.cs +++ b/Boyfriend/ReplyEmojis.cs @@ -2,7 +2,7 @@ namespace Boyfriend; public static class ReplyEmojis { public const string Success = ":white_check_mark:"; - public const string Warning = ":warning: "; + public const string Warning = ":warning:"; public const string Error = ":x:"; public const string MissingArgument = ":keyboard:"; public const string InvalidArgument = ":construction:"; From 28b06686281b1966775533e4b3ef24876f54a7df Mon Sep 17 00:00:00 2001 From: l1ttleO Date: Fri, 9 Dec 2022 16:47:11 +0500 Subject: [PATCH 051/329] Add ReSharper code inspection (#10) And cancel workflows in progress to avoid having multiple of the same workflow running --- .github/workflows/codeql.yml | 12 ++++----- .github/workflows/resharper.yml | 36 +++++++++++++++++++++++++++ Boyfriend/Boyfriend.cs | 3 +-- Boyfriend/CommandProcessor.cs | 43 +++++++++++++++++++++------------ Boyfriend/EventHandler.cs | 23 ++++++++---------- 5 files changed, 80 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/resharper.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4bd4390..36ae7a7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,4 +1,7 @@ name: "CodeQL" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true on: push: @@ -10,7 +13,7 @@ on: jobs: analyze: - name: Analyze + name: Analyze code runs-on: ubuntu-latest permissions: actions: read @@ -26,19 +29,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} queries: +security-extended,security-and-quality - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually - - name: Autobuild + - name: Build solution uses: github/codeql-action/autobuild@v2 - - name: Perform CodeQL Analysis + - name: Perform CodeQL analysis uses: github/codeql-action/analyze@v2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml new file mode 100644 index 0000000..8c55f0f --- /dev/null +++ b/.github/workflows/resharper.yml @@ -0,0 +1,36 @@ +name: "ReSharper" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + inspect-code: + name: Inspect code + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Restore dependencies and tools + run: dotnet restore + + - name: ReSharper CLI InspectCode + uses: muno92/resharper_inspectcode@1.6.0 + with: + solutionPath: ./Boyfriend-CSharp.sln + ignoreIssueType: InvertIf + solutionWideAnalysis: true diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 839b1e8..837b962 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -60,7 +60,7 @@ public static class Boyfriend { private static async Task Init() { var token = (await File.ReadAllTextAsync("token.txt")).Trim(); - Client.Log += x => Log(x); + Client.Log += Log; await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); @@ -151,4 +151,3 @@ public static class Boyfriend { return removedRoles; } } - diff --git a/Boyfriend/CommandProcessor.cs b/Boyfriend/CommandProcessor.cs index 574ad2a..ab077ef 100644 --- a/Boyfriend/CommandProcessor.cs +++ b/Boyfriend/CommandProcessor.cs @@ -73,7 +73,8 @@ public sealed class CommandProcessor { } public void Reply(string response, string? customEmoji = null) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{customEmoji ?? ReplyEmojis.Success} {response}", Context.Message); + Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{customEmoji ?? ReplyEmojis.Success} {response}", + Context.Message); } public void Audit(string action, bool isPublic = true) { @@ -127,17 +128,21 @@ public sealed class CommandProcessor { public bool HasPermission(GuildPermission permission) { if (!Context.Guild.CurrentUser.GuildPermissions.Has(permission)) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"BotCannot{permission}")}", + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"BotCannot{permission}")}", Context.Message); return false; } - if (Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission) - || Context.Guild.OwnerId == Context.User.Id) return true; + if (!Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission) + && Context.Guild.OwnerId != Context.User.Id) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}", + Context.Message); + return false; + } - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}", - Context.Message); - return false; + return true; } public SocketGuildUser? GetMember(SocketUser user) { @@ -202,11 +207,14 @@ public sealed class CommandProcessor { return null; } - if (i <= max) return i; - Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooLarge"), max.ToString())}", - Context.Message); - return null; + if (i > max) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooLarge"), max.ToString())}", + Context.Message); + return null; + } + + return i; } public static TimeSpan GetTimeSpan(string[] args, int index) { @@ -268,9 +276,12 @@ public sealed class CommandProcessor { return false; } - if (Context.Guild.Owner.Id == Context.User.Id || GetMember().Hierarchy > user.Hierarchy) return true; - Utils.SafeAppendToBuilder(_stackedReplyMessage, - $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Target")}", Context.Message); - return false; + if (Context.Guild.Owner.Id != Context.User.Id && GetMember().Hierarchy <= user.Hierarchy) { + Utils.SafeAppendToBuilder(_stackedReplyMessage, + $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Target")}", Context.Message); + return false; + } + + return true; } } diff --git a/Boyfriend/EventHandler.cs b/Boyfriend/EventHandler.cs index f23b34a..9f5af5f 100644 --- a/Boyfriend/EventHandler.cs +++ b/Boyfriend/EventHandler.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Discord; using Discord.Rest; using Discord.WebSocket; @@ -9,17 +8,16 @@ public static class EventHandler { private static readonly DiscordSocketClient Client = Boyfriend.Client; private static bool _sendReadyMessages = true; - [SuppressMessage("ReSharper", "ConvertClosureToMethodGroup")] public static void InitEvents() { - Client.Ready += () => ReadyEvent(); - Client.MessageDeleted += (x, y) => MessageDeletedEvent(x, y); - Client.MessageReceived += x => MessageReceivedEvent(x); - Client.MessageUpdated += (x, y, z) => MessageUpdatedEvent(x, y, z); - Client.UserJoined += x => UserJoinedEvent(x); - Client.GuildScheduledEventCreated += x => ScheduledEventCreatedEvent(x); - Client.GuildScheduledEventCancelled += x => ScheduledEventCancelledEvent(x); - Client.GuildScheduledEventStarted += x => ScheduledEventStartedEvent(x); - Client.GuildScheduledEventCompleted += x => ScheduledEventCompletedEvent(x); + Client.Ready += ReadyEvent; + Client.MessageDeleted += MessageDeletedEvent; + Client.MessageReceived += MessageReceivedEvent; + Client.MessageUpdated += MessageUpdatedEvent; + Client.UserJoined += UserJoinedEvent; + Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; + Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; + Client.GuildScheduledEventStarted += ScheduledEventStartedEvent; + Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; } private static Task ReadyEvent() { @@ -174,5 +172,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 From 93ae380c9f2cd709cbcadf1a34dfa00367c96515 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Sat, 10 Dec 2022 08:10:48 +0300 Subject: [PATCH 052/329] Create README.md (#11) --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3adb02 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +![Boyfriend](https://user-images.githubusercontent.com/95250141/206785237-e3074c8b-07a1-45cd-9ca9-71e48e80b267.png) + +![GitHub License](https://img.shields.io/github/license/l1ttleO/Boyfriend-CSharp) +![GitHub Workflow Status](https://img.shields.io/github/workflow/status/l1ttleO/Boyfriend-CSharp/ReSharper) +![GitHub last commit](https://img.shields.io/github/last-commit/l1ttleO/Boyfriend-CSharp) + +## Building +To build Boyfriend, you need to clone this repo and compile it with [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0). +``` +git clone https://github.com/l1ttleO/Boyfriend-CSharp +cd Boyfriend-CSharp +dotnet build +``` + +## Initial setup +Create `token.txt` in `Boyfriend/bin/Debug/net7.0/` and enter your bot token. Then, run `Boyfriend.exe`. From f175544211b2fe93433e7f0e97b71aa1f6be1824 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Sun, 11 Dec 2022 12:18:46 +0300 Subject: [PATCH 053/329] Change the logo in README depending on the website theme (#12) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c3adb02..bd292f0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -![Boyfriend](https://user-images.githubusercontent.com/95250141/206785237-e3074c8b-07a1-45cd-9ca9-71e48e80b267.png) +![Boyfriend-CSharp-Dark](https://user-images.githubusercontent.com/95250141/206895339-ef5510c8-8b30-4887-b89c-5dc14a24b18a.png#gh-dark-mode-only) +![Boyfriend-CSharp-Light](https://user-images.githubusercontent.com/95250141/206895340-3415d97d-91fd-4fb6-8c17-4e1bf340e1df.png#gh-light-mode-only) ![GitHub License](https://img.shields.io/github/license/l1ttleO/Boyfriend-CSharp) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/l1ttleO/Boyfriend-CSharp/ReSharper) From f0a6c8faff0d4563150dd1d6ae9181e327f6852d Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 21 Dec 2022 11:52:09 +0500 Subject: [PATCH 054/329] Minor issue fixes --- Boyfriend/Boyfriend.cs | 5 +++-- Boyfriend/Messages.Designer.cs | 3 +-- Boyfriend/Messages.resx | 2 +- Boyfriend/Messages.ru.resx | 2 +- Boyfriend/Messages.tt-ru.resx | 2 +- Boyfriend/Utils.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend/Boyfriend.cs index 837b962..cdbbe7d 100644 --- a/Boyfriend/Boyfriend.cs +++ b/Boyfriend/Boyfriend.cs @@ -21,12 +21,13 @@ public static class Boyfriend { }; private static readonly List> ActivityList = new() { - Tuple.Create(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening), new TimeSpan(0, 3, 40)), + 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)), Tuple.Create(new Game("Kurokotei - Scattered Faith", ActivityType.Listening), new TimeSpan(0, 8, 21)), Tuple.Create(new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), Tuple.Create(new Game("RetroSpecter - Genocide", ActivityType.Listening), new TimeSpan(0, 5, 52)), - Tuple.Create(new Game("Dimrain47 - At the Speed of Light", ActivityType.Listening), new TimeSpan(0, 4, 10)) + Tuple.Create(new Game("beatMARIO - Night of Knights", ActivityType.Listening), new TimeSpan(0, 4, 10)) }; public static readonly DiscordSocketClient Client = new(Config); diff --git a/Boyfriend/Messages.Designer.cs b/Boyfriend/Messages.Designer.cs index 3c9b01c..9aea677 100644 --- a/Boyfriend/Messages.Designer.cs +++ b/Boyfriend/Messages.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -376,7 +375,7 @@ namespace Boyfriend { } /// - /// Looks up a localized string similar to Event {0} has completed! Duration: {1}. + /// Looks up a localized string similar to Event {0} has completed! Duration:{1}. /// internal static string EventCompleted { get { diff --git a/Boyfriend/Messages.resx b/Boyfriend/Messages.resx index d6bcd39..56fea2d 100644 --- a/Boyfriend/Messages.resx +++ b/Boyfriend/Messages.resx @@ -274,7 +274,7 @@ Event {0} is cancelled!{1} - Event {0} has completed! Duration: {1} + Event {0} has completed! Duration:{1} ever diff --git a/Boyfriend/Messages.ru.resx b/Boyfriend/Messages.ru.resx index 25197b6..e6a8836 100644 --- a/Boyfriend/Messages.ru.resx +++ b/Boyfriend/Messages.ru.resx @@ -274,7 +274,7 @@ Событие {0} отменено!{1} - Событие {0} завершено! Продолжительность: {1} + Событие {0} завершено! Продолжительность:{1} всегда diff --git a/Boyfriend/Messages.tt-ru.resx b/Boyfriend/Messages.tt-ru.resx index 3439ed2..8d57c67 100644 --- a/Boyfriend/Messages.tt-ru.resx +++ b/Boyfriend/Messages.tt-ru.resx @@ -274,7 +274,7 @@ квест {0} отменен!{1} - квест {0} завершен! все это длилось {1} + квест {0} завершен! все это длилось{1} всегда diff --git a/Boyfriend/Utils.cs b/Boyfriend/Utils.cs index e9e34f0..b08eca3 100644 --- a/Boyfriend/Utils.cs +++ b/Boyfriend/Utils.cs @@ -119,7 +119,7 @@ public static partial class Utils { public static string GetHumanizedTimeOffset(TimeSpan span) { return span.TotalSeconds > 0 - ? $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture)}" + ? $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture.Name.Contains("RU") ? CultureInfoCache["ru"] : Messages.Culture)}" : Messages.Ever; } From 7b8888dae3db6a0b2720ba6698cf8ebcede01c6b Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 18 Jan 2023 19:39:24 +0500 Subject: [PATCH 055/329] 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]")] From b486d2d3d9c7778eb77c8025737794f095c21ba2 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Wed, 18 Jan 2023 17:46:03 +0300 Subject: [PATCH 056/329] Fix GitHub workflow status badge (#16) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd292f0..41ea23c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Boyfriend-CSharp-Light](https://user-images.githubusercontent.com/95250141/206895340-3415d97d-91fd-4fb6-8c17-4e1bf340e1df.png#gh-light-mode-only) ![GitHub License](https://img.shields.io/github/license/l1ttleO/Boyfriend-CSharp) -![GitHub Workflow Status](https://img.shields.io/github/workflow/status/l1ttleO/Boyfriend-CSharp/ReSharper) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/l1ttleO/Boyfriend-CSharp/.github/workflows/resharper.yml?branch=master) ![GitHub last commit](https://img.shields.io/github/last-commit/l1ttleO/Boyfriend-CSharp) ## Building From c220a0f37916c5b480c9e15634bdc66139954a34 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 18 Jan 2023 20:17:33 +0500 Subject: [PATCH 057/329] Rename solution & project, move the project files one layer up --- Boyfriend-CSharp.sln | 25 --------- Boyfriend/Boyfriend.cs => Boyfriend.cs | 0 Boyfriend.csproj | 36 +++++++++++++ Boyfriend.sln | 16 ++++++ Boyfriend/Boyfriend.csproj | 51 ------------------- ...CommandProcessor.cs => CommandProcessor.cs | 0 .../Commands => Commands}/BanCommand.cs | 0 .../Commands => Commands}/ClearCommand.cs | 0 .../Commands => Commands}/HelpCommand.cs | 0 {Boyfriend/Commands => Commands}/ICommand.cs | 0 .../Commands => Commands}/KickCommand.cs | 0 .../Commands => Commands}/MuteCommand.cs | 0 .../Commands => Commands}/PingCommand.cs | 0 .../Commands => Commands}/RemindCommand.cs | 0 .../Commands => Commands}/SettingsCommand.cs | 0 .../Commands => Commands}/UnbanCommand.cs | 0 .../Commands => Commands}/UnmuteCommand.cs | 0 {Boyfriend/Data => Data}/GuildData.cs | 0 {Boyfriend/Data => Data}/MemberData.cs | 0 {Boyfriend/Data => Data}/Reminder.cs | 0 Boyfriend/EventHandler.cs => EventHandler.cs | 0 ...ssages.Designer.cs => Messages.Designer.cs | 0 Boyfriend/Messages.resx => Messages.resx | 0 .../Messages.ru.resx => Messages.ru.resx | 0 ...Messages.tt-ru.resx => Messages.tt-ru.resx | 0 Boyfriend/ReplyEmojis.cs => ReplyEmojis.cs | 0 Boyfriend/Utils.cs => Utils.cs | 0 27 files changed, 52 insertions(+), 76 deletions(-) delete mode 100644 Boyfriend-CSharp.sln rename Boyfriend/Boyfriend.cs => Boyfriend.cs (100%) create mode 100644 Boyfriend.csproj create mode 100644 Boyfriend.sln delete mode 100644 Boyfriend/Boyfriend.csproj rename Boyfriend/CommandProcessor.cs => CommandProcessor.cs (100%) rename {Boyfriend/Commands => Commands}/BanCommand.cs (100%) rename {Boyfriend/Commands => Commands}/ClearCommand.cs (100%) rename {Boyfriend/Commands => Commands}/HelpCommand.cs (100%) rename {Boyfriend/Commands => Commands}/ICommand.cs (100%) rename {Boyfriend/Commands => Commands}/KickCommand.cs (100%) rename {Boyfriend/Commands => Commands}/MuteCommand.cs (100%) rename {Boyfriend/Commands => Commands}/PingCommand.cs (100%) rename {Boyfriend/Commands => Commands}/RemindCommand.cs (100%) rename {Boyfriend/Commands => Commands}/SettingsCommand.cs (100%) rename {Boyfriend/Commands => Commands}/UnbanCommand.cs (100%) rename {Boyfriend/Commands => Commands}/UnmuteCommand.cs (100%) rename {Boyfriend/Data => Data}/GuildData.cs (100%) rename {Boyfriend/Data => Data}/MemberData.cs (100%) rename {Boyfriend/Data => Data}/Reminder.cs (100%) rename Boyfriend/EventHandler.cs => EventHandler.cs (100%) rename Boyfriend/Messages.Designer.cs => Messages.Designer.cs (100%) rename Boyfriend/Messages.resx => Messages.resx (100%) rename Boyfriend/Messages.ru.resx => Messages.ru.resx (100%) rename Boyfriend/Messages.tt-ru.resx => Messages.tt-ru.resx (100%) rename Boyfriend/ReplyEmojis.cs => ReplyEmojis.cs (100%) rename Boyfriend/Utils.cs => Utils.cs (100%) diff --git a/Boyfriend-CSharp.sln b/Boyfriend-CSharp.sln deleted file mode 100644 index 6178bea..0000000 --- a/Boyfriend-CSharp.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32407.343 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Boyfriend", "Boyfriend\Boyfriend.csproj", "{21640A7A-75C2-4515-A1DF-CE8B6EEBD260}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Debug|Any CPU.Build.0 = Debug|Any CPU - {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Release|Any CPU.ActiveCfg = Release|Any CPU - {21640A7A-75C2-4515-A1DF-CE8B6EEBD260}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {2EDBF9CE-35F0-4810-93F7-FE0159EFD865} - EndGlobalSection -EndGlobal diff --git a/Boyfriend/Boyfriend.cs b/Boyfriend.cs similarity index 100% rename from Boyfriend/Boyfriend.cs rename to Boyfriend.cs diff --git a/Boyfriend.csproj b/Boyfriend.csproj new file mode 100644 index 0000000..25e7b7d --- /dev/null +++ b/Boyfriend.csproj @@ -0,0 +1,36 @@ + + + + Exe + net7.0 + enable + enable + 1.0.0 + Boyfriend + Octol1ttle, mctaylors + AGPLv3 + https://github.com/TeamOctolings/Boyfriend + https://github.com/TeamOctolings/Boyfriend/blob/master/LICENSE + https://github.com/Octol1ttle/Boyfriend-CSharp + github + TeamOctolings + en + A legacy-driven Discord bot written in C# + + + + x64 + + + + x64 + none + + + + + + + + + diff --git a/Boyfriend.sln b/Boyfriend.sln new file mode 100644 index 0000000..b85c5f6 --- /dev/null +++ b/Boyfriend.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Boyfriend", "Boyfriend.csproj", "{9CA7A44F-167C-46D4-923D-88CE71044144}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9CA7A44F-167C-46D4-923D-88CE71044144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CA7A44F-167C-46D4-923D-88CE71044144}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CA7A44F-167C-46D4-923D-88CE71044144}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CA7A44F-167C-46D4-923D-88CE71044144}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Boyfriend/Boyfriend.csproj b/Boyfriend/Boyfriend.csproj deleted file mode 100644 index 97db937..0000000 --- a/Boyfriend/Boyfriend.csproj +++ /dev/null @@ -1,51 +0,0 @@ - - - - Exe - net7.0 - enable - enable - default - Boyfriend - Octol1ttle, mctaylors - https://github.com/l1ttleO/Boyfriend-CSharp - https://github.com/l1ttleO/Boyfriend-CSharp - git - 1.0.0 - https://github.com/l1ttleO/Boyfriend-CSharp/blob/master/LICENSE - en - - - - - true - x64 - none - - - - x64 - - - - - - - - - - - ResXFileCodeGenerator - Messages.Designer.cs - - - - - - True - True - Messages.resx - - - - diff --git a/Boyfriend/CommandProcessor.cs b/CommandProcessor.cs similarity index 100% rename from Boyfriend/CommandProcessor.cs rename to CommandProcessor.cs diff --git a/Boyfriend/Commands/BanCommand.cs b/Commands/BanCommand.cs similarity index 100% rename from Boyfriend/Commands/BanCommand.cs rename to Commands/BanCommand.cs diff --git a/Boyfriend/Commands/ClearCommand.cs b/Commands/ClearCommand.cs similarity index 100% rename from Boyfriend/Commands/ClearCommand.cs rename to Commands/ClearCommand.cs diff --git a/Boyfriend/Commands/HelpCommand.cs b/Commands/HelpCommand.cs similarity index 100% rename from Boyfriend/Commands/HelpCommand.cs rename to Commands/HelpCommand.cs diff --git a/Boyfriend/Commands/ICommand.cs b/Commands/ICommand.cs similarity index 100% rename from Boyfriend/Commands/ICommand.cs rename to Commands/ICommand.cs diff --git a/Boyfriend/Commands/KickCommand.cs b/Commands/KickCommand.cs similarity index 100% rename from Boyfriend/Commands/KickCommand.cs rename to Commands/KickCommand.cs diff --git a/Boyfriend/Commands/MuteCommand.cs b/Commands/MuteCommand.cs similarity index 100% rename from Boyfriend/Commands/MuteCommand.cs rename to Commands/MuteCommand.cs diff --git a/Boyfriend/Commands/PingCommand.cs b/Commands/PingCommand.cs similarity index 100% rename from Boyfriend/Commands/PingCommand.cs rename to Commands/PingCommand.cs diff --git a/Boyfriend/Commands/RemindCommand.cs b/Commands/RemindCommand.cs similarity index 100% rename from Boyfriend/Commands/RemindCommand.cs rename to Commands/RemindCommand.cs diff --git a/Boyfriend/Commands/SettingsCommand.cs b/Commands/SettingsCommand.cs similarity index 100% rename from Boyfriend/Commands/SettingsCommand.cs rename to Commands/SettingsCommand.cs diff --git a/Boyfriend/Commands/UnbanCommand.cs b/Commands/UnbanCommand.cs similarity index 100% rename from Boyfriend/Commands/UnbanCommand.cs rename to Commands/UnbanCommand.cs diff --git a/Boyfriend/Commands/UnmuteCommand.cs b/Commands/UnmuteCommand.cs similarity index 100% rename from Boyfriend/Commands/UnmuteCommand.cs rename to Commands/UnmuteCommand.cs diff --git a/Boyfriend/Data/GuildData.cs b/Data/GuildData.cs similarity index 100% rename from Boyfriend/Data/GuildData.cs rename to Data/GuildData.cs diff --git a/Boyfriend/Data/MemberData.cs b/Data/MemberData.cs similarity index 100% rename from Boyfriend/Data/MemberData.cs rename to Data/MemberData.cs diff --git a/Boyfriend/Data/Reminder.cs b/Data/Reminder.cs similarity index 100% rename from Boyfriend/Data/Reminder.cs rename to Data/Reminder.cs diff --git a/Boyfriend/EventHandler.cs b/EventHandler.cs similarity index 100% rename from Boyfriend/EventHandler.cs rename to EventHandler.cs diff --git a/Boyfriend/Messages.Designer.cs b/Messages.Designer.cs similarity index 100% rename from Boyfriend/Messages.Designer.cs rename to Messages.Designer.cs diff --git a/Boyfriend/Messages.resx b/Messages.resx similarity index 100% rename from Boyfriend/Messages.resx rename to Messages.resx diff --git a/Boyfriend/Messages.ru.resx b/Messages.ru.resx similarity index 100% rename from Boyfriend/Messages.ru.resx rename to Messages.ru.resx diff --git a/Boyfriend/Messages.tt-ru.resx b/Messages.tt-ru.resx similarity index 100% rename from Boyfriend/Messages.tt-ru.resx rename to Messages.tt-ru.resx diff --git a/Boyfriend/ReplyEmojis.cs b/ReplyEmojis.cs similarity index 100% rename from Boyfriend/ReplyEmojis.cs rename to ReplyEmojis.cs diff --git a/Boyfriend/Utils.cs b/Utils.cs similarity index 100% rename from Boyfriend/Utils.cs rename to Utils.cs From 0c89df47910bda6ff482c2cfa11a4a88856c4208 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 18 Jan 2023 20:35:22 +0500 Subject: [PATCH 058/329] Fix CI failures Signed-off-by: Octol1ttle --- .github/dependabot.yml | 2 +- .github/workflows/resharper.yml | 2 +- CODEOWNERS | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 CODEOWNERS diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 18ba8f9..f573c5e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,7 +18,7 @@ updates: - "type: dependencies" - package-ecosystem: "nuget" # See documentation for possible values - directory: "/Boyfriend" # Location of package manifests + directory: "/" # Location of package manifests schedule: interval: "weekly" allow: diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index 9a91364..7de5d34 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -31,6 +31,6 @@ jobs: - name: ReSharper CLI InspectCode uses: muno92/resharper_inspectcode@1.6.6 with: - solutionPath: ./Boyfriend-CSharp.sln + solutionPath: ./Boyfriend.sln ignoreIssueType: InvertIf solutionWideAnalysis: true diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..470a6fb --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +* @boyfriend-admins +Messages.tt-ru.resx @mctaylors From 22fdb5302906431be7900dd3a0ea8f72901cc69a Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Wed, 18 Jan 2023 18:44:53 +0300 Subject: [PATCH 059/329] Update repository link in README (#18) Co-authored-by: Octol1ttle --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41ea23c..171d37f 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ ## Building To build Boyfriend, you need to clone this repo and compile it with [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0). ``` -git clone https://github.com/l1ttleO/Boyfriend-CSharp -cd Boyfriend-CSharp +git clone https://github.com/TeamOctolings/Boyfriend +cd Boyfriend dotnet build ``` From 6f5a96970449440c1be6084784374546475e4c94 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 18 Jan 2023 21:01:22 +0500 Subject: [PATCH 060/329] One last touch... Signed-off-by: Octol1ttle --- .github/CODEOWNERS | 2 ++ .github/README.md | 17 +++++++++++++++++ Boyfriend.csproj | 2 +- CODEOWNERS | 2 -- README.md | 17 ----------------- global.json | 7 ------- 6 files changed, 20 insertions(+), 27 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/README.md delete mode 100644 CODEOWNERS delete mode 100644 README.md delete mode 100644 global.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1915f8c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +* @TeamOctolings/boyfriend-admins +Messages.tt-ru.resx @mctaylors diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..15bc0ff --- /dev/null +++ b/.github/README.md @@ -0,0 +1,17 @@ +![Boyfriend-Dark](https://user-images.githubusercontent.com/95250141/206895339-ef5510c8-8b30-4887-b89c-5dc14a24b18a.png#gh-dark-mode-only) +![Boyfriend-Light](https://user-images.githubusercontent.com/95250141/206895340-3415d97d-91fd-4fb6-8c17-4e1bf340e1df.png#gh-light-mode-only) + +![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master) +![GitHub last commit](https://img.shields.io/github/last-commit/TeamOctolings/Boyfriend) + +## Building +To build Boyfriend, you need to clone this repo and compile it with [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0). +``` +git clone https://github.com/TeamOctolings/Boyfriend +cd Boyfriend +dotnet build +``` + +## Initial setup +Create `token.txt` in `Boyfriend/bin/Debug/net7.0/` and enter your bot token. Then, run `Boyfriend.exe`. diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 25e7b7d..69b6840 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -11,7 +11,7 @@ AGPLv3 https://github.com/TeamOctolings/Boyfriend https://github.com/TeamOctolings/Boyfriend/blob/master/LICENSE - https://github.com/Octol1ttle/Boyfriend-CSharp + https://github.com/TeamOctolings/Boyfriend github TeamOctolings en diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 470a6fb..0000000 --- a/CODEOWNERS +++ /dev/null @@ -1,2 +0,0 @@ -* @boyfriend-admins -Messages.tt-ru.resx @mctaylors diff --git a/README.md b/README.md deleted file mode 100644 index 171d37f..0000000 --- a/README.md +++ /dev/null @@ -1,17 +0,0 @@ -![Boyfriend-CSharp-Dark](https://user-images.githubusercontent.com/95250141/206895339-ef5510c8-8b30-4887-b89c-5dc14a24b18a.png#gh-dark-mode-only) -![Boyfriend-CSharp-Light](https://user-images.githubusercontent.com/95250141/206895340-3415d97d-91fd-4fb6-8c17-4e1bf340e1df.png#gh-light-mode-only) - -![GitHub License](https://img.shields.io/github/license/l1ttleO/Boyfriend-CSharp) -![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/l1ttleO/Boyfriend-CSharp/.github/workflows/resharper.yml?branch=master) -![GitHub last commit](https://img.shields.io/github/last-commit/l1ttleO/Boyfriend-CSharp) - -## Building -To build Boyfriend, you need to clone this repo and compile it with [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0). -``` -git clone https://github.com/TeamOctolings/Boyfriend -cd Boyfriend -dotnet build -``` - -## Initial setup -Create `token.txt` in `Boyfriend/bin/Debug/net7.0/` and enter your bot token. Then, run `Boyfriend.exe`. diff --git a/global.json b/global.json deleted file mode 100644 index 36e1a9e..0000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "7.0.0", - "rollForward": "latestMajor", - "allowPrerelease": false - } -} \ No newline at end of file From 0bdf2cd33e76e89a5009ab4e6f27f102cfc3e51a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 18 Jan 2023 22:10:31 +0500 Subject: [PATCH 061/329] Reduce indenting, fix critical bug with private feedback being public and vice versa Signed-off-by: Octol1ttle --- Boyfriend.cs | 79 ++++++++++++++++------------- CommandProcessor.cs | 82 +++++++++++++++++++----------- EventHandler.cs | 121 +++++++++++++++++++++++++------------------- 3 files changed, 166 insertions(+), 116 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index f22da61..c5c8a73 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -14,8 +14,8 @@ public static class Boyfriend { private static readonly DiscordSocketConfig Config = new() { MessageCacheSize = 250, GatewayIntents - = (GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers) & - ~GatewayIntents.GuildInvites, + = (GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers) + & ~GatewayIntents.GuildInvites, AlwaysDownloadUsers = true, AlwaysResolveStickers = false, AlwaysDownloadDefaultStickers = false, @@ -23,10 +23,11 @@ public static class Boyfriend { }; private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; - private static uint _nextSongIndex; + private static uint _nextSongIndex; private static readonly Tuple[] ActivityList = { - Tuple.Create(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening), + 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)), Tuple.Create(new Game("Kurokotei - Scattered Faith", ActivityType.Listening), new TimeSpan(0, 8, 21)), @@ -75,8 +76,10 @@ public static class Boyfriend { 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)); + await Log( + new LogMessage( + LogSeverity.Error, nameof(Boyfriend), + "Exception while ticking guilds", exc)); } GuildTickTasks.Clear(); @@ -115,10 +118,13 @@ public static class Boyfriend { 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))) { + 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"])); @@ -127,42 +133,43 @@ public static class Boyfriend { 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} ")); + .Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id)) + .Aggregate(mentions, (current, user) => current.Append($"{user.Mention} ")); - await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync(string.Format( - Messages.EventEarlyNotification, - mentions, - Utils.Wrap(schEvent.Name), - schEvent.StartTime.ToUnixTimeSeconds().ToString()))!; + await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync( + string.Format( + Messages.EventEarlyNotification, + mentions, + Utils.Wrap(schEvent.Name), + schEvent.StartTime.ToUnixTimeSeconds().ToString()))!; mentions.Clear(); } foreach (var mData in data.MemberData.Values) { if (DateTimeOffset.Now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id); + if (!mData.IsInGuild) continue; - if (mData.IsInGuild) { - if (DateTimeOffset.Now >= mData.MutedUntil) { - await Utils.UnmuteMemberAsync(data, Client.CurrentUser.ToString(), guild.GetUser(mData.Id), - Messages.PunishmentExpired); - saveData = true; + if (DateTimeOffset.Now >= mData.MutedUntil) { + await Utils.UnmuteMemberAsync( + data, Client.CurrentUser.ToString(), guild.GetUser(mData.Id), + Messages.PunishmentExpired); + saveData = true; + } + + for (var i = mData.Reminders.Count - 1; i >= 0; i--) { + var reminder = mData.Reminders[i]; + if (DateTimeOffset.Now < reminder.RemindAt) continue; + + var channel = guild.GetTextChannel(reminder.ReminderChannel); + if (channel is null) { + await Utils.SendDirectMessage(Client.GetUser(mData.Id), reminder.ReminderText); + continue; } - 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; - } + await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}"); + mData.Reminders.RemoveAt(i); - await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}"); - mData.Reminders.RemoveAt(i); - - saveData = true; - } - } + saveData = true; } } diff --git a/CommandProcessor.cs b/CommandProcessor.cs index c20603c..739ab4d 100644 --- a/CommandProcessor.cs +++ b/CommandProcessor.cs @@ -18,9 +18,9 @@ public sealed class CommandProcessor { }; private readonly StringBuilder _stackedPrivateFeedback = new(); - private readonly StringBuilder _stackedPublicFeedback = new(); - private readonly StringBuilder _stackedReplyMessage = new(); - private readonly List _tasks = new(); + private readonly StringBuilder _stackedPublicFeedback = new(); + private readonly StringBuilder _stackedReplyMessage = new(); + private readonly List _tasks = new(); public readonly SocketCommandContext Context; @@ -47,8 +47,10 @@ public sealed class CommandProcessor { try { Task.WaitAll(_tasks.ToArray()); } catch (AggregateException e) { foreach (var ex in e.InnerExceptions) - await Boyfriend.Log(new LogMessage(LogSeverity.Error, nameof(CommandProcessor), - "Exception while executing commands", ex)); + await Boyfriend.Log( + new LogMessage( + LogSeverity.Error, nameof(CommandProcessor), + "Exception while executing commands", ex)); } _tasks.Clear(); @@ -74,7 +76,8 @@ public sealed class CommandProcessor { } public void Reply(string response, string? customEmoji = null) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{customEmoji ?? ReplyEmojis.Success} {response}", + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{customEmoji ?? ReplyEmojis.Success} {response}", Context.Message); } @@ -91,15 +94,18 @@ public sealed class CommandProcessor { _ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None); 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) { + var adminChannel = data.PrivateFeedbackChannel; + var systemChannel = data.PublicFeedbackChannel; + if (_stackedPrivateFeedback.Length > 0 + && adminChannel is not null + && adminChannel.Id != Context.Message.Channel.Id) { _ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString()); _stackedPrivateFeedback.Clear(); } - if (_stackedPublicFeedback.Length > 0 && systemChannel is not null && systemChannel.Id != adminChannel?.Id + if (_stackedPublicFeedback.Length > 0 + && systemChannel is not null + && systemChannel.Id != adminChannel?.Id && systemChannel.Id != Context.Message.Channel.Id) { _ = Utils.SilentSendAsync(systemChannel, _stackedPublicFeedback.ToString()); _stackedPublicFeedback.Clear(); @@ -108,7 +114,8 @@ public sealed class CommandProcessor { public string? GetRemaining(string[] from, int startIndex, string? argument) { if (startIndex >= from.Length && argument is not null) - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Utils.GetMessage($"Missing{argument}")}", Context.Message); else return string.Join(" ", from, startIndex, from.Length - startIndex); return null; @@ -116,14 +123,16 @@ public sealed class CommandProcessor { public Tuple? GetUser(string[] args, string[] cleanArgs, int index) { if (index >= args.Length) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", Context.Message); return null; } var mention = Utils.ParseMention(args[index]); if (mention is 0) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}", Context.Message); return null; @@ -131,7 +140,8 @@ public sealed class CommandProcessor { var exists = Utils.UserExists(mention); if (!exists) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.Error} {string.Format(Messages.UserNotFound, Utils.Wrap(cleanArgs[index]))}", Context.Message); return null; @@ -142,7 +152,8 @@ public sealed class CommandProcessor { public bool HasPermission(GuildPermission permission) { if (!Context.Guild.CurrentUser.GuildPermissions.Has(permission)) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"BotCannot{permission}")}", Context.Message); return false; @@ -150,7 +161,8 @@ public sealed class CommandProcessor { if (!GetMember().GuildPermissions.Has(permission) && Context.Guild.OwnerId != Context.User.Id) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}", Context.Message); return false; @@ -169,14 +181,16 @@ public sealed class CommandProcessor { public SocketGuildUser? GetMember(string[] args, int index) { if (index >= args.Length) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingMember}", + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingMember}", Context.Message); return null; } var member = Context.Guild.GetUser(Utils.ParseMention(args[index])); if (member is null) - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.InvalidArgument} {Messages.InvalidMember}", Context.Message); return member; @@ -184,7 +198,8 @@ public sealed class CommandProcessor { public ulong? GetBan(string[] args, int index) { if (index >= args.Length) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", Context.Message); return null; } @@ -200,14 +215,16 @@ public sealed class CommandProcessor { public int? GetNumberRange(string[] args, int index, int min, int max, string? argument) { if (index >= args.Length) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {string.Format(Messages.MissingNumber, min.ToString(), max.ToString())}", Context.Message); return null; } if (!int.TryParse(args[index], out var i)) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}Invalid"), min.ToString(), max.ToString(), Utils.Wrap(args[index]))}", Context.Message); return null; @@ -215,14 +232,16 @@ public sealed class CommandProcessor { if (argument is null) return i; if (i < min) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooSmall"), min.ToString())}", Context.Message); return null; } if (i > max) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooLarge"), max.ToString())}", Context.Message); return null; @@ -267,31 +286,36 @@ public sealed class CommandProcessor { public bool CanInteractWith(SocketGuildUser user, string action) { if (Context.User.Id == user.Id) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Themselves")}", Context.Message); return false; } if (Context.Guild.CurrentUser.Id == user.Id) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Bot")}", Context.Message); return false; } if (Context.Guild.Owner.Id == user.Id) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Owner")}", Context.Message); return false; } if (Context.Guild.CurrentUser.Hierarchy <= user.Hierarchy) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"BotCannot{action}Target")}", Context.Message); return false; } if (Context.Guild.Owner.Id != Context.User.Id && GetMember().Hierarchy <= user.Hierarchy) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, + Utils.SafeAppendToBuilder( + _stackedReplyMessage, $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Target")}", Context.Message); return false; } diff --git a/EventHandler.cs b/EventHandler.cs index 3e3bcfd..0fb41d8 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -7,8 +7,8 @@ using Discord.WebSocket; namespace Boyfriend; public static class EventHandler { - private static readonly DiscordSocketClient Client = Boyfriend.Client; - private static bool _sendReadyMessages = true; + private static readonly DiscordSocketClient Client = Boyfriend.Client; + private static bool _sendReadyMessages = true; public static void InitEvents() { Client.Ready += ReadyEvent; @@ -49,11 +49,13 @@ public static class EventHandler { return Task.CompletedTask; } - private static async Task MessageDeletedEvent(Cacheable message, + private static async Task MessageDeletedEvent( + Cacheable message, Cacheable channel) { var msg = message.Value; - if (channel.Value is not SocketGuildChannel gChannel || msg is null or ISystemMessage || - msg.Author.IsBot) return; + if (channel.Value is not SocketGuildChannel gChannel + || msg is null or ISystemMessage + || msg.Author.IsBot) return; var guild = gChannel.Guild; @@ -64,22 +66,25 @@ public static class EventHandler { await Task.Delay(500); var auditLogEntry = (await guild.GetAuditLogsAsync(1).FlattenAsync()).First(); - if (auditLogEntry.CreatedAt >= DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(1)) && - 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, mention); + await Utils.SendFeedbackAsync( + string.Format( + Messages.CachedMessageDeleted, msg.Author.Mention, + Utils.MentionChannel(channel.Id), + Utils.Wrap(msg.CleanContent)), guild, mention); } private static Task MessageReceivedEvent(IDeletable messageParam) { if (messageParam is not SocketUserMessage message) return Task.CompletedTask; _ = message.CleanContent.ToLower() switch { - "whoami" => message.ReplyAsync("`nobody`"), + "whoami" => message.ReplyAsync("`nobody`"), "сука !!" => message.ReplyAsync("`root`"), - "воооо" => message.ReplyAsync("`removing /...`"), + "воооо" => message.ReplyAsync("`removing /...`"), "op ??" => message.ReplyAsync( "некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"), _ => new CommandProcessor(message).HandleCommandAsync() @@ -87,18 +92,23 @@ public static class EventHandler { return Task.CompletedTask; } - private static async Task MessageUpdatedEvent(Cacheable messageCached, IMessage messageSocket, - ISocketMessageChannel channel) { + private static async Task MessageUpdatedEvent( + Cacheable messageCached, IMessage messageSocket, + ISocketMessageChannel channel) { var msg = messageCached.Value; - if (channel is not SocketGuildChannel gChannel || msg is null or ISystemMessage || - msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return; + if (channel is not SocketGuildChannel gChannel + || msg is null or ISystemMessage + || msg.CleanContent == messageSocket.CleanContent + || msg.Author.IsBot) return; var guild = gChannel.Guild; Utils.SetCurrentLanguage(guild); var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940; - await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id), + await Utils.SendFeedbackAsync( + string.Format( + Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id), Utils.Wrap(msg.CleanContent, isLimitedSpace), Utils.Wrap(messageSocket.CleanContent, isLimitedSpace)), guild, msg.Author.Mention); } @@ -111,10 +121,12 @@ public static class EventHandler { Utils.SetCurrentLanguage(guild); 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)); + 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"])); @@ -148,23 +160,24 @@ public static class EventHandler { var eventConfig = GuildData.Get(guild).Preferences; var channel = Utils.GetEventNotificationChannel(guild); Utils.SetCurrentLanguage(guild); + if (channel is null) return; - if (channel is not null) { - var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); - var mentions = role is not null - ? $"{role.Mention} {scheduledEvent.Creator.Mention}" - : $"{scheduledEvent.Creator.Mention}"; + var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); + var mentions = role is not null + ? $"{role.Mention} {scheduledEvent.Creator.Mention}" + : $"{scheduledEvent.Creator.Mention}"; - var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); - var descAndLink - = $"\n{Utils.Wrap(scheduledEvent.Description)}\nhttps://discord.com/events/{guild.Id}/{scheduledEvent.Id}"; + var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); + var descAndLink + = $"\n{Utils.Wrap(scheduledEvent.Description)}\nhttps://discord.com/events/{guild.Id}/{scheduledEvent.Id}"; - await Utils.SilentSendAsync(channel, - string.Format(Messages.EventCreated, mentions, - Utils.Wrap(scheduledEvent.Name), location, - scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink), - true); - } + await Utils.SilentSendAsync( + channel, + string.Format( + Messages.EventCreated, mentions, + Utils.Wrap(scheduledEvent.Name), location, + scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink), + true); } private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { @@ -173,8 +186,10 @@ public static class EventHandler { var channel = Utils.GetEventNotificationChannel(guild); 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}" : "")); + await channel.SendMessageAsync( + string.Format( + Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name), + eventConfig["FrowningFace"] is "true" ? $" {Messages.SettingsFrowningFace}" : "")); } private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) { @@ -183,22 +198,24 @@ public static class EventHandler { var channel = Utils.GetEventNotificationChannel(guild); Utils.SetCurrentLanguage(guild); - if (channel is not null) { - var receivers = eventConfig["EventStartedReceivers"]; - var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); - var mentions = Boyfriend.StringBuilder; + if (channel is null) return; - if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} "); - if (receivers.Contains("users") || receivers.Contains("interested")) - mentions = (await scheduledEvent.GetUsersAsync(15)) - .Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id)) - .Aggregate(mentions, (current, user) => current.Append($"{user.Mention} ")); + var receivers = eventConfig["EventStartedReceivers"]; + var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); + var mentions = Boyfriend.StringBuilder; - await channel.SendMessageAsync(string.Format(Messages.EventStarted, mentions, + if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} "); + if (receivers.Contains("users") || receivers.Contains("interested")) + 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), Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id))); - mentions.Clear(); - } + mentions.Clear(); } private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { @@ -206,7 +223,9 @@ public static class EventHandler { var channel = Utils.GetEventNotificationChannel(guild); Utils.SetCurrentLanguage(guild); if (channel is not null) - await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), - Utils.GetHumanizedTimeSpan(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); + await channel.SendMessageAsync( + string.Format( + Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), + Utils.GetHumanizedTimeSpan(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); } } From c1e3abce57280ca0c5ad406ceea1f9443a436e53 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 23 Jan 2023 20:29:44 +0500 Subject: [PATCH 062/329] Do not cache public & private feedback channels for no reason Signed-off-by: Octol1ttle --- Commands/SettingsCommand.cs | 29 ++++++++-------------- Data/GuildData.cs | 48 ++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 47 deletions(-) diff --git a/Commands/SettingsCommand.cs b/Commands/SettingsCommand.cs index 7ed012c..e056cdc 100644 --- a/Commands/SettingsCommand.cs +++ b/Commands/SettingsCommand.cs @@ -34,7 +34,7 @@ public sealed class SettingsCommand : ICommand { } currentSettings.Append($"{Utils.GetMessage($"Settings{setting.Key}")} (`{setting.Key}`): ") - .AppendFormat(format, currentValue).AppendLine(); + .AppendFormat(format, currentValue).AppendLine(); } cmd.Reply(currentSettings.ToString(), ReplyEmojis.SettingsList); @@ -63,8 +63,9 @@ public sealed class SettingsCommand : ICommand { if (value is null) return Task.CompletedTask; if (selectedSetting is "EventStartedReceivers") { value = value.Replace(" ", "").ToLower(); - if (value.StartsWith(",") || value.Count(x => x is ',') > 1 || - (!value.Contains("interested") && !value.Contains("users") && !value.Contains("role"))) { + if (value.StartsWith(",") + || value.Count(x => x is ',') > 1 + || (!value.Contains("interested") && !value.Contains("users") && !value.Contains("role"))) { cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error); return Task.CompletedTask; } @@ -73,9 +74,7 @@ public sealed class SettingsCommand : ICommand { if (IsBool(GuildData.DefaultPreferences[selectedSetting]) && !IsBool(value)) { value = value switch { - "y" or "yes" or "д" or "да" => "true", - "n" or "no" or "н" or "нет" => "false", - _ => value + "y" or "yes" or "д" or "да" => "true", "n" or "no" or "н" or "нет" => "false", _ => value }; if (!IsBool(value)) { cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error); @@ -95,7 +94,7 @@ public sealed class SettingsCommand : ICommand { } var formattedValue = selectedSetting switch { - "WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage), + "WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage), "EventStartedReceivers" => Utils.Wrap(GuildData.DefaultPreferences[selectedSetting])!, _ => value is "reset" or "default" ? Messages.SettingNotDefined : IsBool(value) ? YesOrNo(value is "true") @@ -106,7 +105,8 @@ public sealed class SettingsCommand : ICommand { config[selectedSetting] = GuildData.DefaultPreferences[selectedSetting]; } else { if (value == config[selectedSetting]) { - cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), + cmd.Reply( + string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), ReplyEmojis.Error); return Task.CompletedTask; } @@ -135,17 +135,8 @@ public sealed class SettingsCommand : ICommand { 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; - } + if (selectedSetting is "MuteRole") + data.MuteRole = guild.GetRole(mention); config[selectedSetting] = value; } diff --git a/Data/GuildData.cs b/Data/GuildData.cs index 9d00525..53d7114 100644 --- a/Data/GuildData.cs +++ b/Data/GuildData.cs @@ -43,8 +43,6 @@ public record GuildData { 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 @@ -57,8 +55,8 @@ public record GuildData { if (!Directory.Exists(memberDataDir)) Directory.CreateDirectory(memberDataDir); if (!File.Exists(_configurationFile)) File.WriteAllText(_configurationFile, "{}"); Preferences - = JsonSerializer.Deserialize>(File.ReadAllText(_configurationFile)) ?? - new Dictionary(); + = 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))) @@ -78,11 +76,12 @@ public record GuildData { 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) { + 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); } @@ -100,28 +99,19 @@ public record GuildData { get { if (Preferences["MuteRole"] is "0") return null; return _cachedMuteRole ??= Boyfriend.Client.GetGuild(_id).Roles - .Single(x => x.Id == ulong.Parse(Preferences["MuteRole"])); + .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? PublicFeedbackChannel => Boyfriend.Client.GetGuild(_id) + .GetTextChannel( + ulong.Parse(Preferences["PublicFeedbackChannel"])); - 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 SocketTextChannel? PrivateFeedbackChannel => Boyfriend.Client.GetGuild(_id) + .GetTextChannel( + ulong.Parse( + Preferences["PrivateFeedbackChannel"])); public static GuildData Get(SocketGuild guild) { if (GuildDataDictionary.TryGetValue(guild.Id, out var stored)) return stored; @@ -132,11 +122,13 @@ public record GuildData { public async Task Save(bool saveMemberData) { Preferences.TrimExcess(); - await File.WriteAllTextAsync(_configurationFile, + await File.WriteAllTextAsync( + _configurationFile, JsonSerializer.Serialize(Preferences)); if (saveMemberData) foreach (var data in MemberData.Values) - await File.WriteAllTextAsync($"{_id}/MemberData/{data.Id}.json", + await File.WriteAllTextAsync( + $"{_id}/MemberData/{data.Id}.json", JsonSerializer.Serialize(data, Options)); } } From 9d5bafbbf54f6690694daa3164cb199975697a7b Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 23 Jan 2023 21:06:04 +0500 Subject: [PATCH 063/329] Make sure every member has the starter role when ticking guilds Closes #20 Signed-off-by: Octol1ttle --- .editorconfig | 111 +++++++++++++++++++++++++----------- Boyfriend.cs | 23 ++++---- Commands/SettingsCommand.cs | 2 +- Data/GuildData.cs | 15 ++--- EventHandler.cs | 8 +-- 5 files changed, 102 insertions(+), 57 deletions(-) diff --git a/.editorconfig b/.editorconfig index a647b0a..bb647a7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,39 +1,82 @@ [*] -charset = utf-8 -end_of_line = lf -trim_trailing_whitespace = true -insert_final_newline = true -indent_style = space -indent_size = 4 -tab_width = 4 +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 +tab_width = 4 # Microsoft .NET properties -csharp_new_line_before_catch = false -csharp_new_line_before_else = false -csharp_new_line_before_finally = false -csharp_new_line_before_members_in_object_initializers = false -csharp_new_line_before_open_brace = none -csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async -csharp_style_var_elsewhere = true:suggestion -csharp_style_var_for_built_in_types = true:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none -dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion -dotnet_style_qualification_for_event = false:suggestion -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = none +csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async +csharp_style_var_elsewhere = true : suggestion +csharp_style_var_for_built_in_types = true : suggestion +csharp_style_var_when_type_is_apparent = true : suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary : none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity : none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary : none +dotnet_style_predefined_type_for_locals_parameters_members = true : suggestion +dotnet_style_predefined_type_for_member_access = true : suggestion +dotnet_style_qualification_for_event = false : suggestion +dotnet_style_qualification_for_field = false : suggestion +dotnet_style_qualification_for_method = false : suggestion +dotnet_style_qualification_for_property = false : suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members : suggestion # ReSharper properties -resharper_csharp_wrap_before_first_type_parameter_constraint = true -resharper_place_simple_case_statement_on_same_line = true -resharper_place_simple_embedded_block_on_same_line = true -resharper_place_simple_switch_expression_on_single_line = true -resharper_wrap_before_arrow_with_expressions = true -resharper_wrap_before_eq = true -resharper_wrap_before_extends_colon = true -resharper_wrap_before_linq_expression = true +resharper_align_linq_query = true +resharper_align_multiline_argument = true +resharper_align_multiline_binary_patterns = true +resharper_align_multiline_extends_list = true +resharper_align_multiline_parameter = true +resharper_align_multiple_declaration = true +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = true +resharper_align_tuple_components = true +resharper_allow_comment_after_lbrace = true +resharper_csharp_empty_block_style = together_same_line +resharper_csharp_indent_type_constraints = false +resharper_csharp_int_align_comments = true +resharper_csharp_outdent_commas = true +resharper_csharp_stick_comment = false +resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_after_invocation_lpar = true +resharper_csharp_wrap_before_binary_opsign = true +resharper_csharp_wrap_before_first_type_parameter_constraint = true +resharper_csharp_wrap_multiple_declaration_style = wrap_if_long +resharper_csharp_wrap_multiple_type_parameter_constraints_style = chop_always +resharper_indent_nested_fixed_stmt = true +resharper_indent_nested_foreach_stmt = true +resharper_indent_nested_for_stmt = true +resharper_indent_nested_lock_stmt = true +resharper_indent_nested_usings_stmt = true +resharper_indent_nested_while_stmt = true +resharper_indent_preprocessor_if = usual_indent +resharper_indent_preprocessor_other = usual_indent +resharper_int_align_fields = true +resharper_int_align_methods = true +resharper_int_align_parameters = true +resharper_int_align_properties = true +resharper_int_align_switch_expressions = true +resharper_int_align_switch_sections = true +resharper_keep_existing_switch_expression_arrangement = false +resharper_outdent_statement_labels = true +resharper_place_field_attribute_on_same_line = if_owner_is_single_line +resharper_place_simple_accessorholder_on_single_line = true +resharper_place_simple_accessor_on_single_line = false +resharper_place_simple_case_statement_on_same_line = true +resharper_place_simple_embedded_block_on_same_line = true +resharper_place_simple_switch_expression_on_single_line = true +resharper_space_around_arrow_op = true +resharper_wrap_before_arrow_with_expressions = true +resharper_wrap_before_eq = true +resharper_wrap_before_extends_colon = true +resharper_wrap_before_linq_expression = true +resharper_wrap_chained_binary_expressions = chop_if_long +resharper_wrap_for_stmt_header_style = wrap_if_long +resharper_wrap_switch_expression = chop_if_long diff --git a/Boyfriend.cs b/Boyfriend.cs index c5c8a73..ffb3fbc 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -121,7 +121,7 @@ public static class Boyfriend { if (schEvent.Status is GuildScheduledEventStatus.Scheduled && config["AutoStartEvents"] is "true" && DateTimeOffset - .Now + .Now >= schEvent.StartTime) await schEvent.StartAsync(); else if (!data.EarlyNotifications.Contains(schEvent.Id) && DateTimeOffset.Now >= schEvent.StartTime.Subtract(new TimeSpan(0, offset, 0))) { @@ -133,8 +133,8 @@ public static class Boyfriend { 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} ")); + .Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id)) + .Aggregate(mentions, (current, user) => current.Append($"{user.Mention} ")); await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync( string.Format( @@ -145,13 +145,16 @@ public static class Boyfriend { mentions.Clear(); } + _ = ulong.TryParse(config["StarterRole"], out var starterRoleId); foreach (var mData in data.MemberData.Values) { + var user = guild.GetUser(mData.Id); if (DateTimeOffset.Now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id); if (!mData.IsInGuild) continue; + if (!mData.Roles.Contains(starterRoleId)) _ = user.AddRoleAsync(starterRoleId); if (DateTimeOffset.Now >= mData.MutedUntil) { await Utils.UnmuteMemberAsync( - data, Client.CurrentUser.ToString(), guild.GetUser(mData.Id), + data, Client.CurrentUser.ToString(), user, Messages.PunishmentExpired); saveData = true; } @@ -161,18 +164,16 @@ public static class Boyfriend { if (DateTimeOffset.Now < reminder.RemindAt) continue; var channel = guild.GetTextChannel(reminder.ReminderChannel); - if (channel is null) { - await Utils.SendDirectMessage(Client.GetUser(mData.Id), reminder.ReminderText); - continue; - } + if (channel is not null) + await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}"); + else + await Utils.SendDirectMessage(user, reminder.ReminderText); - await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}"); mData.Reminders.RemoveAt(i); - saveData = true; } } - if (saveData) data.Save(true).Wait(); + if (saveData) await data.Save(true); } } diff --git a/Commands/SettingsCommand.cs b/Commands/SettingsCommand.cs index e056cdc..b987394 100644 --- a/Commands/SettingsCommand.cs +++ b/Commands/SettingsCommand.cs @@ -34,7 +34,7 @@ public sealed class SettingsCommand : ICommand { } currentSettings.Append($"{Utils.GetMessage($"Settings{setting.Key}")} (`{setting.Key}`): ") - .AppendFormat(format, currentValue).AppendLine(); + .AppendFormat(format, currentValue).AppendLine(); } cmd.Reply(currentSettings.ToString(), ReplyEmojis.SettingsList); diff --git a/Data/GuildData.cs b/Data/GuildData.cs index 53d7114..41947d6 100644 --- a/Data/GuildData.cs +++ b/Data/GuildData.cs @@ -99,19 +99,20 @@ public record GuildData { get { if (Preferences["MuteRole"] is "0") return null; return _cachedMuteRole ??= Boyfriend.Client.GetGuild(_id).Roles - .Single(x => x.Id == ulong.Parse(Preferences["MuteRole"])); + .Single(x => x.Id == ulong.Parse(Preferences["MuteRole"])); } set => _cachedMuteRole = value; } - public SocketTextChannel? PublicFeedbackChannel => Boyfriend.Client.GetGuild(_id) - .GetTextChannel( - ulong.Parse(Preferences["PublicFeedbackChannel"])); + public SocketTextChannel? PublicFeedbackChannel + => Boyfriend.Client.GetGuild(_id) + .GetTextChannel( + ulong.Parse(Preferences["PublicFeedbackChannel"])); public SocketTextChannel? PrivateFeedbackChannel => Boyfriend.Client.GetGuild(_id) - .GetTextChannel( - ulong.Parse( - Preferences["PrivateFeedbackChannel"])); + .GetTextChannel( + ulong.Parse( + Preferences["PrivateFeedbackChannel"])); public static GuildData Get(SocketGuild guild) { if (GuildDataDictionary.TryGetValue(guild.Id, out var stored)) return stored; diff --git a/EventHandler.cs b/EventHandler.cs index 0fb41d8..e91f228 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -17,14 +17,14 @@ public static class EventHandler { Client.MessageUpdated += MessageUpdatedEvent; Client.UserJoined += UserJoinedEvent; Client.UserLeft += UserLeftEvent; - Client.GuildMemberUpdated += RolesUpdatedEvent; + Client.GuildMemberUpdated += MemberRolesUpdatedEvent; Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; Client.GuildScheduledEventStarted += ScheduledEventStartedEvent; Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; } - private static Task RolesUpdatedEvent(Cacheable oldUser, SocketGuildUser newUser) { + private static Task MemberRolesUpdatedEvent(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); @@ -207,8 +207,8 @@ 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)) - .Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id)) - .Aggregate(mentions, (current, user) => current.Append($"{user.Mention} ")); + .Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id)) + .Aggregate(mentions, (current, user) => current.Append($"{user.Mention} ")); await channel.SendMessageAsync( string.Format( From b1e43611de6948aa6a68b78dcaf72f5608ddab75 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 24 Jan 2023 12:36:53 +0500 Subject: [PATCH 064/329] Resync roles when restarting bot Signed-off-by: Octol1ttle --- Data/GuildData.cs | 6 ++++++ EventHandler.cs | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Data/GuildData.cs b/Data/GuildData.cs index 41947d6..42959b0 100644 --- a/Data/GuildData.cs +++ b/Data/GuildData.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using Discord; using Discord.WebSocket; namespace Boyfriend.Data; @@ -86,6 +87,11 @@ public record GuildData { MemberData.Remove(memberData.Id); } + if (memberData.MutedUntil is null) { + memberData.Roles = ((IGuildUser)member).RoleIds.ToList(); + memberData.Roles.Remove(guild.Id); + } + continue; } diff --git a/EventHandler.cs b/EventHandler.cs index e91f228..88a4e9c 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -141,7 +141,7 @@ public static class EventHandler { } if (memberData.MutedUntil < DateTimeOffset.Now) { - if (data.MuteRole is not null) + if (data.MuteRole is not null && !user.TimedOutUntil.HasValue) await user.AddRoleAsync(data.MuteRole); if (config["RemoveRolesOnMute"] is "false" && config["ReturnRolesOnRejoin"] is "true") await user.AddRolesAsync(memberData.Roles); @@ -149,6 +149,7 @@ public static class EventHandler { } private static Task UserLeftEvent(SocketGuild guild, SocketUser user) { + if (user.IsBot) return Task.CompletedTask; var data = GuildData.Get(guild).MemberData[user.Id]; data.IsInGuild = false; data.LeftAt.Add(DateTimeOffset.Now); From 482d7ea91f52fa52e2ece19442e4ef7b09099eaa Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Thu, 26 Jan 2023 18:03:28 +0300 Subject: [PATCH 065/329] Reply to "++++" with "#" (#22) --- EventHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/EventHandler.cs b/EventHandler.cs index 88a4e9c..23c6510 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -87,6 +87,7 @@ public static class EventHandler { "воооо" => message.ReplyAsync("`removing /...`"), "op ??" => message.ReplyAsync( "некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"), + "++++" => message.ReplyAsync("#"), _ => new CommandProcessor(message).HandleCommandAsync() }; return Task.CompletedTask; From 00f5fa0d8d0884c3129f6b9461ae5fe808c77ee2 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Fri, 27 Jan 2023 07:46:03 +0300 Subject: [PATCH 066/329] Fix removing everyone role while muting (#23) Co-authored-by: Octol1ttle --- Commands/MuteCommand.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Commands/MuteCommand.cs b/Commands/MuteCommand.cs index 1d2ebeb..cdd7fe5 100644 --- a/Commands/MuteCommand.cs +++ b/Commands/MuteCommand.cs @@ -1,6 +1,5 @@ using Boyfriend.Data; using Discord; -using Discord.WebSocket; namespace Boyfriend.Commands; @@ -29,15 +28,16 @@ public sealed class MuteCommand : ICommand { await MuteMemberAsync(cmd, toMute, duration, guildData, reason); } - private static async Task MuteMemberAsync(CommandProcessor cmd, SocketGuildUser toMute, + private static async Task MuteMemberAsync(CommandProcessor cmd, IGuildUser toMute, TimeSpan duration, GuildData data, string reason) { var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); var role = data.MuteRole; var hasDuration = duration.TotalSeconds > 0; + var memberData = data.MemberData[toMute.Id]; if (role is not null) { if (data.Preferences["RemoveRolesOnMute"] is "true") - await toMute.RemoveRolesAsync(toMute.Roles, requestOptions); + await toMute.RemoveRolesAsync(memberData.Roles, requestOptions); await toMute.AddRoleAsync(role, requestOptions); } else { @@ -54,7 +54,7 @@ public sealed class MuteCommand : ICommand { await toMute.SetTimeOutAsync(duration, requestOptions); } - data.MemberData[toMute.Id].MutedUntil = DateTimeOffset.Now.Add(duration); + memberData.MutedUntil = DateTimeOffset.Now.Add(duration); cmd.ConfigWriteScheduled = true; var feedback = string.Format(Messages.FeedbackMemberMuted, toMute.Mention, From 8ca4b83b9cabdb9b46b4b3bdcd2e270596904625 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 27 Jan 2023 09:52:13 +0500 Subject: [PATCH 067/329] Do not grant starter role if member is muted Signed-off-by: Octol1ttle --- Boyfriend.cs | 2 +- EventHandler.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index ffb3fbc..4301e20 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -150,7 +150,7 @@ public static class Boyfriend { var user = guild.GetUser(mData.Id); if (DateTimeOffset.Now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id); if (!mData.IsInGuild) continue; - if (!mData.Roles.Contains(starterRoleId)) _ = user.AddRoleAsync(starterRoleId); + if (!mData.Roles.Contains(starterRoleId) && mData.MutedUntil is null) _ = user.AddRoleAsync(starterRoleId); if (DateTimeOffset.Now >= mData.MutedUntil) { await Utils.UnmuteMemberAsync( diff --git a/EventHandler.cs b/EventHandler.cs index 23c6510..ba4ba79 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -88,7 +88,7 @@ public static class EventHandler { "op ??" => message.ReplyAsync( "некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"), "++++" => message.ReplyAsync("#"), - _ => new CommandProcessor(message).HandleCommandAsync() + _ => new CommandProcessor(message).HandleCommandAsync() }; return Task.CompletedTask; } @@ -129,8 +129,6 @@ public static class EventHandler { ? 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; From 1c13f0a31060b8d4a38f94d1433ea94eee6400bf Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 27 Jan 2023 09:53:04 +0500 Subject: [PATCH 068/329] Resync roles before removing them during a mute Signed-off-by: Octol1ttle --- Commands/MuteCommand.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Commands/MuteCommand.cs b/Commands/MuteCommand.cs index cdd7fe5..650ff02 100644 --- a/Commands/MuteCommand.cs +++ b/Commands/MuteCommand.cs @@ -28,16 +28,20 @@ public sealed class MuteCommand : ICommand { await MuteMemberAsync(cmd, toMute, duration, guildData, reason); } - private static async Task MuteMemberAsync(CommandProcessor cmd, IGuildUser toMute, - TimeSpan duration, GuildData data, string reason) { + private static async Task MuteMemberAsync( + CommandProcessor cmd, IGuildUser toMute, + TimeSpan duration, GuildData data, string reason) { var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); var role = data.MuteRole; var hasDuration = duration.TotalSeconds > 0; var memberData = data.MemberData[toMute.Id]; if (role is not null) { - if (data.Preferences["RemoveRolesOnMute"] is "true") + if (data.Preferences["RemoveRolesOnMute"] is "true") { + memberData.Roles = toMute.RoleIds.ToList(); + memberData.Roles.Remove(cmd.Context.Guild.Id); await toMute.RemoveRolesAsync(memberData.Roles, requestOptions); + } await toMute.AddRoleAsync(role, requestOptions); } else { @@ -57,7 +61,8 @@ public sealed class MuteCommand : ICommand { memberData.MutedUntil = DateTimeOffset.Now.Add(duration); cmd.ConfigWriteScheduled = true; - var feedback = string.Format(Messages.FeedbackMemberMuted, toMute.Mention, + var feedback = string.Format( + Messages.FeedbackMemberMuted, toMute.Mention, Utils.GetHumanizedTimeSpan(duration), Utils.Wrap(reason)); cmd.Reply(feedback, ReplyEmojis.Muted); From eaeacc12066a56fd3a9cdc0789be4d1fb210f134 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 2 Feb 2023 00:16:31 +0500 Subject: [PATCH 069/329] Fix a critical performance issue in GuildData, skip guild ticks if running behind Signed-off-by: Octol1ttle --- .editorconfig | 1 - Boyfriend.cs | 4 +++- Data/GuildData.cs | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index bb647a7..8d71c08 100644 --- a/.editorconfig +++ b/.editorconfig @@ -58,7 +58,6 @@ resharper_indent_nested_usings_stmt = true resharper_indent_nested_while_stmt = true resharper_indent_preprocessor_if = usual_indent resharper_indent_preprocessor_other = usual_indent -resharper_int_align_fields = true resharper_int_align_methods = true resharper_int_align_parameters = true resharper_int_align_properties = true diff --git a/Boyfriend.cs b/Boyfriend.cs index 4301e20..1b1577b 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -23,7 +23,7 @@ public static class Boyfriend { }; private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; - private static uint _nextSongIndex; + private static uint _nextSongIndex; private static readonly Tuple[] ActivityList = { Tuple.Create( @@ -72,6 +72,8 @@ public static class Boyfriend { } private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) { + if (GuildTickTasks.Count is not 0) return; + foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild)); try { Task.WaitAll(GuildTickTasks.ToArray()); } catch (AggregateException ex) { diff --git a/Data/GuildData.cs b/Data/GuildData.cs index 42959b0..1896ad5 100644 --- a/Data/GuildData.cs +++ b/Data/GuildData.cs @@ -74,7 +74,6 @@ public record GuildData { 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 From 233d4716009b04cfca3545cb4ed2414524b7b8bb Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Thu, 2 Feb 2023 07:48:18 +0300 Subject: [PATCH 070/329] Add reminder creation feedback (#24) --- Commands/RemindCommand.cs | 20 +-- Messages.Designer.cs | 255 ++++++++++++++++++++------------------ Messages.resx | 3 + Messages.ru.resx | 3 + Messages.tt-ru.resx | 3 + ReplyEmojis.cs | 25 ++-- 6 files changed, 167 insertions(+), 142 deletions(-) diff --git a/Commands/RemindCommand.cs b/Commands/RemindCommand.cs index 24b1a08..efc9fd2 100644 --- a/Commands/RemindCommand.cs +++ b/Commands/RemindCommand.cs @@ -9,14 +9,20 @@ public sealed class RemindCommand : ICommand { // 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 - }); + var reminderOffset = DateTimeOffset.Now.Add(remindIn); + if (reminderText is not null) { + GuildData.Get(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add( + new Reminder { + RemindAt = reminderOffset, + ReminderText = reminderText, + ReminderChannel = cmd.Context.Channel.Id + }); - cmd.ConfigWriteScheduled = true; + cmd.ConfigWriteScheduled = true; + + var feedback = string.Format(Messages.FeedbackReminderAdded, reminderOffset.ToUnixTimeSeconds().ToString()); + cmd.Reply(feedback, ReplyEmojis.Reminder); + } return Task.CompletedTask; } diff --git a/Messages.Designer.cs b/Messages.Designer.cs index 8f2be50..daeaa3a 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -9,8 +9,8 @@ namespace Boyfriend { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -22,15 +22,15 @@ namespace Boyfriend { [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// @@ -44,7 +44,7 @@ namespace Boyfriend { return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. @@ -58,7 +58,7 @@ namespace Boyfriend { resourceCulture = value; } } - + /// /// Looks up a localized string similar to Bah! . /// @@ -67,7 +67,7 @@ namespace Boyfriend { return ResourceManager.GetString("Beep1", resourceCulture); } } - + /// /// Looks up a localized string similar to Bop! . /// @@ -76,7 +76,7 @@ namespace Boyfriend { return ResourceManager.GetString("Beep2", resourceCulture); } } - + /// /// Looks up a localized string similar to Beep! . /// @@ -85,7 +85,7 @@ namespace Boyfriend { return ResourceManager.GetString("Beep3", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot ban users from this guild!. /// @@ -94,7 +94,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot ban this user!. /// @@ -103,7 +103,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot kick members from this guild!. /// @@ -112,7 +112,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot kick this member!. /// @@ -121,7 +121,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot manage this guild!. /// @@ -130,7 +130,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot manage messages in this guild!. /// @@ -139,7 +139,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot moderate members in this guild!. /// @@ -148,7 +148,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot mute this member!. /// @@ -157,7 +157,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot unmute this member!. /// @@ -166,7 +166,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. /// @@ -175,7 +175,7 @@ namespace Boyfriend { return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - + /// /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. /// @@ -184,7 +184,7 @@ namespace Boyfriend { return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. /// @@ -193,7 +193,7 @@ namespace Boyfriend { return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); } } - + /// /// Looks up a localized string similar to Not specified. /// @@ -202,7 +202,7 @@ namespace Boyfriend { return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify an integer from {0} to {1} instead of {2}!. /// @@ -211,7 +211,7 @@ namespace Boyfriend { return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to You specified more than {0} messages!. /// @@ -220,7 +220,7 @@ namespace Boyfriend { return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); } } - + /// /// Looks up a localized string similar to You specified less than {0} messages!. /// @@ -229,7 +229,7 @@ namespace Boyfriend { return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); } } - + /// /// Looks up a localized string similar to Bans a user. /// @@ -238,7 +238,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); } } - + /// /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. /// @@ -247,7 +247,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); } } - + /// /// Looks up a localized string similar to Shows this message. /// @@ -256,7 +256,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); } } - + /// /// Looks up a localized string similar to Kicks a member. /// @@ -265,7 +265,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); } } - + /// /// Looks up a localized string similar to Mutes a member. /// @@ -274,7 +274,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); } } - + /// /// Looks up a localized string similar to Shows (inaccurate) latency. /// @@ -283,7 +283,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); } } - + /// /// Looks up a localized string similar to Adds a reminder. /// @@ -292,7 +292,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionRemind", resourceCulture); } } - + /// /// Looks up a localized string similar to Allows you to change certain preferences for this guild. /// @@ -301,7 +301,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); } } - + /// /// Looks up a localized string similar to Unbans a user. /// @@ -310,7 +310,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); } } - + /// /// Looks up a localized string similar to Unmutes a member. /// @@ -319,7 +319,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); } } - + /// /// Looks up a localized string similar to Command help:. /// @@ -328,7 +328,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandHelp", resourceCulture); } } - + /// /// Looks up a localized string similar to I do not have permission to execute this command!. /// @@ -337,7 +337,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); } } - + /// /// Looks up a localized string similar to You do not have permission to execute this command!. /// @@ -346,7 +346,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); } } - + /// /// Looks up a localized string similar to Current settings:. /// @@ -355,7 +355,7 @@ namespace Boyfriend { return ResourceManager.GetString("CurrentSettings", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}, welcome to {1}. /// @@ -364,7 +364,7 @@ namespace Boyfriend { return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to 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. /// @@ -373,7 +373,7 @@ namespace Boyfriend { return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); } } - + /// /// Looks up a localized string similar to Event {0} is cancelled!{1}. /// @@ -382,7 +382,7 @@ namespace Boyfriend { return ResourceManager.GetString("EventCancelled", resourceCulture); } } - + /// /// Looks up a localized string similar to Event {0} has completed! Duration:{1}. /// @@ -391,7 +391,7 @@ namespace Boyfriend { return ResourceManager.GetString("EventCompleted", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4}. /// @@ -400,7 +400,7 @@ namespace Boyfriend { return ResourceManager.GetString("EventCreated", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}Event {1} will start <t:{2}:R>!. /// @@ -409,7 +409,7 @@ namespace Boyfriend { return ResourceManager.GetString("EventEarlyNotification", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. /// @@ -418,7 +418,7 @@ namespace Boyfriend { return ResourceManager.GetString("EventStarted", resourceCulture); } } - + /// /// Looks up a localized string similar to ever. /// @@ -427,7 +427,7 @@ namespace Boyfriend { return ResourceManager.GetString("Ever", resourceCulture); } } - + /// /// Looks up a localized string similar to Kicked {0}: {1}. /// @@ -436,7 +436,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); } } - + /// /// Looks up a localized string similar to Muted {0} for{1}: {2}. /// @@ -445,7 +445,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); } } - + /// /// Looks up a localized string similar to Unmuted {0}: {1}. /// @@ -454,7 +454,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); } } - + /// /// Looks up a localized string similar to Deleted {0} messages in {1}. /// @@ -463,7 +463,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); } } - + /// /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. /// @@ -472,7 +472,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); } } - + /// /// Looks up a localized string similar to Banned {0} for{1}: {2}. /// @@ -481,7 +481,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); } } - + /// /// Looks up a localized string similar to Unbanned {0}: {1}. /// @@ -490,7 +490,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); } } - + /// /// Looks up a localized string similar to This channel does not exist!. /// @@ -499,7 +499,7 @@ namespace Boyfriend { return ResourceManager.GetString("InvalidChannel", resourceCulture); } } - + /// /// Looks up a localized string similar to You did not specify a member of this guild!. /// @@ -508,7 +508,7 @@ namespace Boyfriend { return ResourceManager.GetString("InvalidMember", resourceCulture); } } - + /// /// Looks up a localized string similar to This role does not exist!. /// @@ -517,7 +517,7 @@ namespace Boyfriend { return ResourceManager.GetString("InvalidRole", resourceCulture); } } - + /// /// Looks up a localized string similar to Invalid setting value specified!. /// @@ -526,7 +526,7 @@ namespace Boyfriend { return ResourceManager.GetString("InvalidSettingValue", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify a user instead of {0}!. /// @@ -535,7 +535,7 @@ namespace Boyfriend { return ResourceManager.GetString("InvalidUser", resourceCulture); } } - + /// /// Looks up a localized string similar to Language not supported! Supported languages:. /// @@ -544,7 +544,7 @@ namespace Boyfriend { return ResourceManager.GetString("LanguageNotSupported", resourceCulture); } } - + /// /// Looks up a localized string similar to Member is already muted!. /// @@ -553,7 +553,7 @@ namespace Boyfriend { return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); } } - + /// /// Looks up a localized string similar to Member not muted!. /// @@ -562,7 +562,7 @@ namespace Boyfriend { return ResourceManager.GetString("MemberNotMuted", resourceCulture); } } - + /// /// Looks up a localized string similar to ms. /// @@ -571,7 +571,7 @@ namespace Boyfriend { return ResourceManager.GetString("Milliseconds", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify a reason to ban this user!. /// @@ -580,7 +580,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingBanReason", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify a reason to kick this member!. /// @@ -589,7 +589,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingKickReason", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify a guild member!. /// @@ -598,7 +598,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingMember", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify a reason to mute this member!. /// @@ -607,7 +607,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingMuteReason", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. /// @@ -616,7 +616,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingNumber", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify reminder text!. /// @@ -625,7 +625,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingReminderText", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify a reason to unban this user!. /// @@ -634,7 +634,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingUnbanReason", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify a reason for unmute this member!. /// @@ -643,7 +643,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify a user!. /// @@ -652,7 +652,7 @@ namespace Boyfriend { return ResourceManager.GetString("MissingUser", resourceCulture); } } - + /// /// Looks up a localized string similar to No. /// @@ -661,7 +661,7 @@ namespace Boyfriend { return ResourceManager.GetString("No", resourceCulture); } } - + /// /// Looks up a localized string similar to Punishment expired. /// @@ -670,7 +670,7 @@ namespace Boyfriend { return ResourceManager.GetString("PunishmentExpired", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}I'm ready!. /// @@ -679,7 +679,7 @@ namespace Boyfriend { return ResourceManager.GetString("Ready", resourceCulture); } } - + /// /// Looks up a localized string similar to Not specified. /// @@ -688,7 +688,7 @@ namespace Boyfriend { return ResourceManager.GetString("RoleNotSpecified", resourceCulture); } } - + /// /// Looks up a localized string similar to That setting doesn't exist!. /// @@ -697,7 +697,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingDoesntExist", resourceCulture); } } - + /// /// Looks up a localized string similar to Not specified. /// @@ -706,7 +706,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingNotDefined", resourceCulture); } } - + /// /// Looks up a localized string similar to Automatically start scheduled events. /// @@ -715,7 +715,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); } } - + /// /// Looks up a localized string similar to Early event start notification offset. /// @@ -724,7 +724,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); } } - + /// /// Looks up a localized string similar to Channel for event notifications. /// @@ -733,7 +733,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); } } - + /// /// Looks up a localized string similar to Role for event creation notifications. /// @@ -742,7 +742,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); } } - + /// /// Looks up a localized string similar to Event start notifications receivers. /// @@ -751,7 +751,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); } } - + /// /// Looks up a localized string similar to :(. /// @@ -760,7 +760,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); } } - + /// /// Looks up a localized string similar to Language. /// @@ -769,7 +769,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsLang", resourceCulture); } } - + /// /// Looks up a localized string similar to Mute role. /// @@ -778,7 +778,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsMuteRole", resourceCulture); } } - + /// /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. /// @@ -787,7 +787,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); } } - + /// /// Looks up a localized string similar to Prefix. /// @@ -796,7 +796,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsPrefix", resourceCulture); } } - + /// /// Looks up a localized string similar to Channel for private notifications. /// @@ -805,7 +805,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsPrivateFeedbackChannel", resourceCulture); } } - + /// /// Looks up a localized string similar to Channel for public notifications. /// @@ -814,7 +814,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture); } } - + /// /// Looks up a localized string similar to Receive startup messages. /// @@ -823,7 +823,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); } } - + /// /// Looks up a localized string similar to Remove roles on mute. /// @@ -832,7 +832,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); } } - + /// /// Looks up a localized string similar to Return roles on rejoin. /// @@ -841,7 +841,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsReturnRolesOnRejoin", resourceCulture); } } - + /// /// Looks up a localized string similar to Send welcome messages. /// @@ -850,7 +850,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); } } - + /// /// Looks up a localized string similar to Starter role. /// @@ -859,7 +859,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsStarterRole", resourceCulture); } } - + /// /// Looks up a localized string similar to Welcome message. /// @@ -868,7 +868,7 @@ namespace Boyfriend { return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot ban me!. /// @@ -877,7 +877,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotBanBot", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot ban users from this guild!. /// @@ -886,7 +886,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot ban the owner of this guild!. /// @@ -895,7 +895,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot ban this user!. /// @@ -904,7 +904,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot ban yourself!. /// @@ -913,7 +913,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot kick me!. /// @@ -922,7 +922,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotKickBot", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot kick members from this guild!. /// @@ -931,7 +931,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot kick the owner of this guild!. /// @@ -940,7 +940,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot kick this member!. /// @@ -949,7 +949,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot kick yourself!. /// @@ -958,7 +958,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot manage this guild!. /// @@ -967,7 +967,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot manage messages in this guild!. /// @@ -976,7 +976,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot moderate members in this guild!. /// @@ -985,7 +985,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot mute me!. /// @@ -994,7 +994,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot mute the owner of this guild!. /// @@ -1003,7 +1003,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot mute this member!. /// @@ -1012,7 +1012,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot mute yourself!. /// @@ -1021,7 +1021,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); } } - + /// /// Looks up a localized string similar to .... /// @@ -1030,7 +1030,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); } } - + /// /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. /// @@ -1039,7 +1039,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); } } - + /// /// Looks up a localized string similar to You cannot unmute this user!. /// @@ -1048,7 +1048,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to You are muted!. /// @@ -1057,7 +1057,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); } } - + /// /// Looks up a localized string similar to This user is not banned!. /// @@ -1066,7 +1066,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserNotBanned", resourceCulture); } } - + /// /// 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. /// @@ -1075,7 +1075,7 @@ namespace Boyfriend { return ResourceManager.GetString("UserNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Yes. /// @@ -1084,7 +1084,7 @@ namespace Boyfriend { return ResourceManager.GetString("Yes", resourceCulture); } } - + /// /// Looks up a localized string similar to You were banned by {0} in guild `{1}` for {2}. /// @@ -1093,7 +1093,7 @@ namespace Boyfriend { return ResourceManager.GetString("YouWereBanned", resourceCulture); } } - + /// /// Looks up a localized string similar to You were kicked by {0} in guild `{1}` for {2}. /// @@ -1102,5 +1102,14 @@ namespace Boyfriend { return ResourceManager.GetString("YouWereKicked", resourceCulture); } } + + /// + /// Looks up a localized string similar to OK, I'll mention you on <t:{0}:f>. + /// + internal static string FeedbackReminderAdded { + get { + return ResourceManager.GetString("FeedbackReminderAdded", resourceCulture); + } + } } } diff --git a/Messages.resx b/Messages.resx index e397d73..82beb50 100644 --- a/Messages.resx +++ b/Messages.resx @@ -465,4 +465,7 @@ You need to specify reminder text! + + OK, I'll mention you on <t:{0}:f> + diff --git a/Messages.ru.resx b/Messages.ru.resx index 55b7fb2..1d34695 100644 --- a/Messages.ru.resx +++ b/Messages.ru.resx @@ -465,4 +465,7 @@ Тебе нужно указать текст напоминания! + + Хорошо, я упомяну тебя <t:{0}:f> + diff --git a/Messages.tt-ru.resx b/Messages.tt-ru.resx index c2b67bc..0301ea7 100644 --- a/Messages.tt-ru.resx +++ b/Messages.tt-ru.resx @@ -465,4 +465,7 @@ для крафта напоминалки нужен текст + + вас понял, упоминание будет <t:{0}:f> + diff --git a/ReplyEmojis.cs b/ReplyEmojis.cs index a4e8c40..3ccb6a6 100644 --- a/ReplyEmojis.cs +++ b/ReplyEmojis.cs @@ -1,18 +1,19 @@ namespace Boyfriend; public static class ReplyEmojis { - public const string Success = ":white_check_mark:"; - public const string Error = ":x:"; + public const string Success = ":white_check_mark:"; + public const string Error = ":x:"; public const string MissingArgument = ":keyboard:"; public const string InvalidArgument = ":construction:"; - public const string NoPermission = ":no_entry_sign:"; - public const string CantInteract = ":vertical_traffic_light:"; - public const string Help = ":page_facing_up:"; - public const string SettingsList = ":gear:"; - public const string SettingsSet = ":control_knobs:"; - public const string Ping = ":signal_strength:"; - public const string Banned = ":hammer:"; - public const string Kicked = ":police_car:"; - public const string Muted = ":mute:"; - public const string Unmuted = ":loud_sound:"; + public const string NoPermission = ":no_entry_sign:"; + public const string CantInteract = ":vertical_traffic_light:"; + public const string Help = ":page_facing_up:"; + public const string SettingsList = ":gear:"; + public const string SettingsSet = ":control_knobs:"; + public const string Ping = ":signal_strength:"; + public const string Banned = ":hammer:"; + public const string Kicked = ":police_car:"; + public const string Muted = ":mute:"; + public const string Unmuted = ":loud_sound:"; + public const string Reminder = ":alarm_clock:"; } From 8518db5b2f086b38dcbd75ceb35b652afb852228 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 2 Feb 2023 10:04:37 +0500 Subject: [PATCH 071/329] Switch songs as part of guild tick loop Signed-off-by: Octol1ttle --- Boyfriend.cs | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index 1b1577b..e32a155 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -30,9 +30,10 @@ public static class Boyfriend { 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)), - Tuple.Create(new Game("Kurokotei - Scattered Faith", ActivityType.Listening), new TimeSpan(0, 8, 21)), + Tuple.Create( + new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)), Tuple.Create(new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), - Tuple.Create(new Game("RetroSpecter - Genocide", ActivityType.Listening), new TimeSpan(0, 5, 52)), + Tuple.Create(new Game("RetroSpecter - Overtime", ActivityType.Listening), new TimeSpan(0, 4, 33)), Tuple.Create(new Game("beatMARIO - Night of Knights", ActivityType.Listening), new TimeSpan(0, 4, 10)) }; @@ -61,20 +62,22 @@ public static class Boyfriend { 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; - } + await Task.Delay(-1); } private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) { if (GuildTickTasks.Count is not 0) return; - foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild)); + var now = DateTimeOffset.Now; + foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild, now)); + + if (now >= _nextSongAt) { + var nextSong = ActivityList[_nextSongIndex]; + await Client.SetActivityAsync(nextSong.Item1); + _nextSongAt = now.Add(nextSong.Item2); + _nextSongIndex++; + if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0; + } try { Task.WaitAll(GuildTickTasks.ToArray()); } catch (AggregateException ex) { foreach (var exc in ex.InnerExceptions) @@ -114,7 +117,7 @@ public static class Boyfriend { return Task.CompletedTask; } - private static async Task TickGuildAsync(SocketGuild guild) { + private static async Task TickGuildAsync(SocketGuild guild, DateTimeOffset now) { var data = GuildData.Get(guild); var config = data.Preferences; var saveData = false; @@ -126,7 +129,7 @@ public static class Boyfriend { .Now >= schEvent.StartTime) await schEvent.StartAsync(); else if (!data.EarlyNotifications.Contains(schEvent.Id) - && DateTimeOffset.Now >= schEvent.StartTime.Subtract(new TimeSpan(0, offset, 0))) { + && 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"])); @@ -150,11 +153,11 @@ public static class Boyfriend { _ = ulong.TryParse(config["StarterRole"], out var starterRoleId); foreach (var mData in data.MemberData.Values) { var user = guild.GetUser(mData.Id); - if (DateTimeOffset.Now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id); + if (now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id); if (!mData.IsInGuild) continue; if (!mData.Roles.Contains(starterRoleId) && mData.MutedUntil is null) _ = user.AddRoleAsync(starterRoleId); - if (DateTimeOffset.Now >= mData.MutedUntil) { + if (now >= mData.MutedUntil) { await Utils.UnmuteMemberAsync( data, Client.CurrentUser.ToString(), user, Messages.PunishmentExpired); @@ -163,7 +166,7 @@ public static class Boyfriend { for (var i = mData.Reminders.Count - 1; i >= 0; i--) { var reminder = mData.Reminders[i]; - if (DateTimeOffset.Now < reminder.RemindAt) continue; + if (now < reminder.RemindAt) continue; var channel = guild.GetTextChannel(reminder.ReminderChannel); if (channel is not null) From 8f9c71edf1f6be8d6e60d03adec3b9e0f4ccee7f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 2 Feb 2023 19:51:55 +0500 Subject: [PATCH 072/329] Do not delete messages from role-muted members Signed-off-by: Octol1ttle --- CommandProcessor.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/CommandProcessor.cs b/CommandProcessor.cs index 739ab4d..4f13f16 100644 --- a/CommandProcessor.cs +++ b/CommandProcessor.cs @@ -18,9 +18,9 @@ public sealed class CommandProcessor { }; private readonly StringBuilder _stackedPrivateFeedback = new(); - private readonly StringBuilder _stackedPublicFeedback = new(); - private readonly StringBuilder _stackedReplyMessage = new(); - private readonly List _tasks = new(); + private readonly StringBuilder _stackedPublicFeedback = new(); + private readonly StringBuilder _stackedReplyMessage = new(); + private readonly List _tasks = new(); public readonly SocketCommandContext Context; @@ -35,11 +35,6 @@ public sealed class CommandProcessor { var data = GuildData.Get(guild); Utils.SetCurrentLanguage(guild); - 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++) From a97341f9a9d606d2016d368f85ad1cdb1b420c3c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 2 Feb 2023 21:37:12 +0500 Subject: [PATCH 073/329] Do not allow setting reminders without specifying a valid TimeSpan Fixes #19 Signed-off-by: Octol1ttle --- Boyfriend.cs | 5 +++-- CommandProcessor.cs | 8 ++++---- Commands/RemindCommand.cs | 7 ++++++- Messages.Designer.cs | 9 +++++++++ Messages.resx | 5 ++++- Messages.ru.resx | 11 +++++++---- Messages.tt-ru.resx | 9 ++++++--- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index e32a155..58de254 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -150,12 +150,13 @@ public static class Boyfriend { mentions.Clear(); } - _ = ulong.TryParse(config["StarterRole"], out var starterRoleId); foreach (var mData in data.MemberData.Values) { var user = guild.GetUser(mData.Id); if (now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id); if (!mData.IsInGuild) continue; - if (!mData.Roles.Contains(starterRoleId) && mData.MutedUntil is null) _ = user.AddRoleAsync(starterRoleId); + if (mData.MutedUntil is null + && ulong.TryParse(config["StarterRole"], out var starterRoleId) + && !mData.Roles.Contains(starterRoleId)) _ = user.AddRoleAsync(starterRoleId); if (now >= mData.MutedUntil) { await Utils.UnmuteMemberAsync( diff --git a/CommandProcessor.cs b/CommandProcessor.cs index 4f13f16..38e1430 100644 --- a/CommandProcessor.cs +++ b/CommandProcessor.cs @@ -9,6 +9,7 @@ namespace Boyfriend; public sealed class CommandProcessor { private static readonly string Mention = $"<@{Boyfriend.Client.CurrentUser.Id}>"; + private static readonly TimeSpan Infinity = TimeSpan.FromMilliseconds(-1); public static readonly ICommand[] Commands = { new BanCommand(), new ClearCommand(), new HelpCommand(), @@ -246,14 +247,13 @@ public sealed class CommandProcessor { } public static TimeSpan GetTimeSpan(string[] args, int index) { - var infinity = TimeSpan.FromMilliseconds(-1); - if (index >= args.Length) return infinity; + if (index >= args.Length) return Infinity; var chars = args[index].AsSpan(); var numberBuilder = Boyfriend.StringBuilder; int days = 0, hours = 0, minutes = 0, seconds = 0; foreach (var c in chars) if (char.IsDigit(c)) { numberBuilder.Append(c); } else { - if (numberBuilder.Length is 0) return infinity; + if (numberBuilder.Length is 0) return Infinity; switch (c) { case 'd' or 'D' or 'д' or 'Д': days += int.Parse(numberBuilder.ToString()); @@ -271,7 +271,7 @@ public sealed class CommandProcessor { seconds += int.Parse(numberBuilder.ToString()); numberBuilder.Clear(); break; - default: return infinity; + default: return Infinity; } } diff --git a/Commands/RemindCommand.cs b/Commands/RemindCommand.cs index efc9fd2..5a116fc 100644 --- a/Commands/RemindCommand.cs +++ b/Commands/RemindCommand.cs @@ -8,9 +8,14 @@ public sealed class RemindCommand : ICommand { public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { // TODO: actually make this good var remindIn = CommandProcessor.GetTimeSpan(args, 0); + if (remindIn.TotalSeconds < 1) { + cmd.Reply(Messages.InvalidRemindIn, ReplyEmojis.InvalidArgument); + return Task.CompletedTask; + } + var reminderText = cmd.GetRemaining(cleanArgs, 1, "ReminderText"); - var reminderOffset = DateTimeOffset.Now.Add(remindIn); if (reminderText is not null) { + var reminderOffset = DateTimeOffset.Now.Add(remindIn); GuildData.Get(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add( new Reminder { RemindAt = reminderOffset, diff --git a/Messages.Designer.cs b/Messages.Designer.cs index daeaa3a..5e21849 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -1111,5 +1111,14 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackReminderAdded", resourceCulture); } } + + /// + /// Looks up a localized string similar to You need to specify when I should send you the reminder!. + /// + internal static string InvalidRemindIn { + get { + return ResourceManager.GetString("InvalidRemindIn", resourceCulture); + } + } } } diff --git a/Messages.resx b/Messages.resx index 82beb50..2db1a50 100644 --- a/Messages.resx +++ b/Messages.resx @@ -328,7 +328,7 @@ You need to specify a guild member! - You did not specify a member of this guild! + You need to specify a member of this guild! You cannot ban users from this guild! @@ -468,4 +468,7 @@ OK, I'll mention you on <t:{0}:f> + + You need to specify when I should send you the reminder! + diff --git a/Messages.ru.resx b/Messages.ru.resx index 1d34695..72c10ff 100644 --- a/Messages.ru.resx +++ b/Messages.ru.resx @@ -328,7 +328,7 @@ Надо указать участника сервера! - Тебе надо указать участника этого сервера! + Надо указать участника этого сервера! Ты не можешь банить пользователей на этом сервере! @@ -459,13 +459,16 @@ Возвращать роли при перезаходе - + Автоматически начинать события - + Тебе нужно указать текст напоминания! - + Хорошо, я упомяну тебя <t:{0}:f> + + Нужно указать время, через которое придёт напоминание! + diff --git a/Messages.tt-ru.resx b/Messages.tt-ru.resx index 0301ea7..fa4af35 100644 --- a/Messages.tt-ru.resx +++ b/Messages.tt-ru.resx @@ -459,13 +459,16 @@ вернуть звания при переподключении в дурку - + автоматом стартить квесты - + для крафта напоминалки нужен текст - + вас понял, упоминание будет <t:{0}:f> + + шизоид у меня на часах такого нету + From 620c706c973a4088a6adff188493cb33af6afa3e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 2 Feb 2023 23:01:19 +0500 Subject: [PATCH 074/329] Log originals of messages deleted through !clear Signed-off-by: Octol1ttle --- CommandProcessor.cs | 7 ++++--- Commands/ClearCommand.cs | 11 ++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CommandProcessor.cs b/CommandProcessor.cs index 38e1430..428f271 100644 --- a/CommandProcessor.cs +++ b/CommandProcessor.cs @@ -86,7 +86,8 @@ public sealed class CommandProcessor { } private void SendFeedbacks(bool reply = true) { - if (reply && _stackedReplyMessage.Length > 0) + var hasReply = _stackedReplyMessage.Length > 0; + if (reply && hasReply) _ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None); var data = GuildData.Get(Context.Guild); @@ -94,7 +95,7 @@ public sealed class CommandProcessor { var systemChannel = data.PublicFeedbackChannel; if (_stackedPrivateFeedback.Length > 0 && adminChannel is not null - && adminChannel.Id != Context.Message.Channel.Id) { + && (adminChannel.Id != Context.Message.Channel.Id || !hasReply)) { _ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString()); _stackedPrivateFeedback.Clear(); } @@ -102,7 +103,7 @@ public sealed class CommandProcessor { if (_stackedPublicFeedback.Length > 0 && systemChannel is not null && systemChannel.Id != adminChannel?.Id - && systemChannel.Id != Context.Message.Channel.Id) { + && (systemChannel.Id != Context.Message.Channel.Id || !hasReply)) { _ = Utils.SilentSendAsync(systemChannel, _stackedPublicFeedback.ToString()); _stackedPublicFeedback.Clear(); } diff --git a/Commands/ClearCommand.cs b/Commands/ClearCommand.cs index 141cc9a..b24af71 100644 --- a/Commands/ClearCommand.cs +++ b/Commands/ClearCommand.cs @@ -17,9 +17,14 @@ public sealed class ClearCommand : ICommand { var messages = await channel.GetMessagesAsync((int)(toDelete + 1)).FlattenAsync(); var user = (SocketGuildUser)cmd.Context.User; - await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(user.ToString()!)); + var msgArray = messages.ToArray(); + await channel.DeleteMessagesAsync(msgArray, Utils.GetRequestOptions(user.ToString()!)); - cmd.Audit(string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString(), - Utils.MentionChannel(channel.Id))); + foreach (var msg in msgArray.Where(m => !m.Author.IsBot)) + cmd.Audit( + string.Format( + Messages.CachedMessageDeleted, msg.Author.Mention, + Utils.MentionChannel(channel.Id), + Utils.Wrap(msg.CleanContent))); } } From 7f0fd6ffb59c8ad56040a45de4108872185d081d Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 2 Feb 2023 23:04:29 +0500 Subject: [PATCH 075/329] Fix removed roles store being overwritten Signed-off-by: Octol1ttle --- Commands/MuteCommand.cs | 2 +- EventHandler.cs | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Commands/MuteCommand.cs b/Commands/MuteCommand.cs index 650ff02..82a0e7e 100644 --- a/Commands/MuteCommand.cs +++ b/Commands/MuteCommand.cs @@ -37,6 +37,7 @@ public sealed class MuteCommand : ICommand { var memberData = data.MemberData[toMute.Id]; if (role is not null) { + memberData.MutedUntil = DateTimeOffset.Now.Add(duration); if (data.Preferences["RemoveRolesOnMute"] is "true") { memberData.Roles = toMute.RoleIds.ToList(); memberData.Roles.Remove(cmd.Context.Guild.Id); @@ -58,7 +59,6 @@ public sealed class MuteCommand : ICommand { await toMute.SetTimeOutAsync(duration, requestOptions); } - memberData.MutedUntil = DateTimeOffset.Now.Add(duration); cmd.ConfigWriteScheduled = true; var feedback = string.Format( diff --git a/EventHandler.cs b/EventHandler.cs index ba4ba79..2ab3664 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -7,8 +7,8 @@ using Discord.WebSocket; namespace Boyfriend; public static class EventHandler { - private static readonly DiscordSocketClient Client = Boyfriend.Client; - private static bool _sendReadyMessages = true; + private static readonly DiscordSocketClient Client = Boyfriend.Client; + private static bool _sendReadyMessages = true; public static void InitEvents() { Client.Ready += ReadyEvent; @@ -26,8 +26,11 @@ public static class EventHandler { private static Task MemberRolesUpdatedEvent(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); + if (data.MutedUntil is null) { + data.Roles = ((IGuildUser)newUser).RoleIds.ToList(); + data.Roles.Remove(newUser.Guild.Id); + } + return Task.CompletedTask; } From 3105229ad4cbf745cca365a181722eb4d69a13a5 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 2 Feb 2023 23:05:28 +0500 Subject: [PATCH 076/329] Include bots for storage in MemberData, download users when populating MemberData Signed-off-by: Octol1ttle --- Data/GuildData.cs | 4 +++- EventHandler.cs | 12 +++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Data/GuildData.cs b/Data/GuildData.cs index 1896ad5..08f9574 100644 --- a/Data/GuildData.cs +++ b/Data/GuildData.cs @@ -48,6 +48,7 @@ public record GuildData { [SuppressMessage("Performance", "CA1853:Unnecessary call to \'Dictionary.ContainsKey(key)\'")] // https://github.com/dotnet/roslyn-analyzers/issues/6377 private GuildData(SocketGuild guild) { + var downloaderTask = guild.DownloadUsersAsync(); _id = guild.Id; var idString = $"{_id}"; var memberDataDir = $"{_id}/MemberData"; @@ -74,7 +75,8 @@ public record GuildData { MemberData.Add(deserialised!.Id, deserialised); } - foreach (var member in guild.Users.Where(user => !user.IsBot)) { + downloaderTask.Wait(); + foreach (var member in guild.Users) { if (MemberData.TryGetValue(member.Id, out var memberData)) { if (!memberData.IsInGuild && DateTimeOffset.Now.ToUnixTimeSeconds() diff --git a/EventHandler.cs b/EventHandler.cs index 2ab3664..2e9690f 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -39,12 +39,13 @@ public static class EventHandler { var i = Random.Shared.Next(3); foreach (var guild in Client.Guilds) { + Boyfriend.Log(new LogMessage(LogSeverity.Info, nameof(EventHandler), $"Guild \"{guild.Name}\" is READY")); 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; + + Utils.SetCurrentLanguage(guild); _ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); } @@ -118,7 +119,6 @@ public static class EventHandler { } private static async Task UserJoinedEvent(SocketGuildUser user) { - if (user.IsBot) return; var guild = user.Guild; var data = GuildData.Get(guild); var config = data.Preferences; @@ -142,16 +142,14 @@ public static class EventHandler { memberData.JoinedAt.Add(user.JoinedAt!.Value); } - if (memberData.MutedUntil < DateTimeOffset.Now) { - if (data.MuteRole is not null && !user.TimedOutUntil.HasValue) - await user.AddRoleAsync(data.MuteRole); + if (DateTimeOffset.Now < memberData.MutedUntil) { + 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) { - if (user.IsBot) return Task.CompletedTask; var data = GuildData.Get(guild).MemberData[user.Id]; data.IsInGuild = false; data.LeftAt.Add(DateTimeOffset.Now); From f2e337153fead65429a43f8c2b631255563588ca Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 2 Feb 2023 23:11:17 +0500 Subject: [PATCH 077/329] Fix bulk message deleting logs being public bruh Signed-off-by: Octol1ttle --- Commands/ClearCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Commands/ClearCommand.cs b/Commands/ClearCommand.cs index b24af71..dc0e4aa 100644 --- a/Commands/ClearCommand.cs +++ b/Commands/ClearCommand.cs @@ -25,6 +25,6 @@ public sealed class ClearCommand : ICommand { string.Format( Messages.CachedMessageDeleted, msg.Author.Mention, Utils.MentionChannel(channel.Id), - Utils.Wrap(msg.CleanContent))); + Utils.Wrap(msg.CleanContent)), false); } } From 30a4a94f1b909f8e798aeef871a0cbfb555e7bf9 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Tue, 14 Feb 2023 17:44:29 +0300 Subject: [PATCH 078/329] Edit reminder message & add "!pardon" alias for "!unban" (#25) --- Boyfriend.cs | 5 +++-- Commands/UnbanCommand.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index 58de254..b8c406b 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -170,10 +170,11 @@ public static class Boyfriend { if (now < reminder.RemindAt) continue; var channel = guild.GetTextChannel(reminder.ReminderChannel); + var toSend = $"{ReplyEmojis.Reminder} {user.Mention} {Utils.Wrap(reminder.ReminderText)}"; if (channel is not null) - await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}"); + await channel.SendMessageAsync(toSend); else - await Utils.SendDirectMessage(user, reminder.ReminderText); + await Utils.SendDirectMessage(user, toSend); mData.Reminders.RemoveAt(i); saveData = true; diff --git a/Commands/UnbanCommand.cs b/Commands/UnbanCommand.cs index 70abfe1..fb8040e 100644 --- a/Commands/UnbanCommand.cs +++ b/Commands/UnbanCommand.cs @@ -3,7 +3,7 @@ namespace Boyfriend.Commands; public sealed class UnbanCommand : ICommand { - public string[] Aliases { get; } = { "unban", "разбан" }; + public string[] Aliases { get; } = { "unban", "pardon", "разбан" }; public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { if (!cmd.HasPermission(GuildPermission.BanMembers)) return; From f6f5543972597fd975f0a0c665830567b1ee5282 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 14 Feb 2023 22:32:29 +0500 Subject: [PATCH 079/329] Use modern Tuple syntax Signed-off-by: Octol1ttle --- Boyfriend.cs | 21 +++++++++------------ CommandProcessor.cs | 4 ++-- Commands/BanCommand.cs | 21 ++++++++++++--------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index b8c406b..fb462c4 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -25,16 +25,13 @@ public static class Boyfriend { 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)), - Tuple.Create( - new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)), - Tuple.Create(new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), - Tuple.Create(new Game("RetroSpecter - Overtime", ActivityType.Listening), new TimeSpan(0, 4, 33)), - Tuple.Create(new Game("beatMARIO - Night of Knights", ActivityType.Listening), new TimeSpan(0, 4, 10)) + private static readonly (Game Song, TimeSpan Duration)[] ActivityList = { + (new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening), new TimeSpan(0, 3, 40)), + (new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)), + (new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)), + (new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), + (new Game("RetroSpecter - Overtime", ActivityType.Listening), new TimeSpan(0, 4, 33)), + (new Game("beatMARIO - Night of Knights", ActivityType.Listening), new TimeSpan(0, 4, 10)) }; public static readonly DiscordSocketClient Client = new(Config); @@ -73,8 +70,8 @@ public static class Boyfriend { if (now >= _nextSongAt) { var nextSong = ActivityList[_nextSongIndex]; - await Client.SetActivityAsync(nextSong.Item1); - _nextSongAt = now.Add(nextSong.Item2); + await Client.SetActivityAsync(nextSong.Song); + _nextSongAt = now.Add(nextSong.Duration); _nextSongIndex++; if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0; } diff --git a/CommandProcessor.cs b/CommandProcessor.cs index 428f271..3ee91b6 100644 --- a/CommandProcessor.cs +++ b/CommandProcessor.cs @@ -118,7 +118,7 @@ public sealed class CommandProcessor { return null; } - public Tuple? GetUser(string[] args, string[] cleanArgs, int index) { + public (ulong Id, SocketUser? User)? GetUser(string[] args, string[] cleanArgs, int index) { if (index >= args.Length) { Utils.SafeAppendToBuilder( _stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", @@ -144,7 +144,7 @@ public sealed class CommandProcessor { return null; } - return Tuple.Create(mention, Boyfriend.Client.GetUser(mention))!; + return (mention, Boyfriend.Client.GetUser(mention)); } public bool HasPermission(GuildPermission permission) { diff --git a/Commands/BanCommand.cs b/Commands/BanCommand.cs index 9c00656..f984fd4 100644 --- a/Commands/BanCommand.cs +++ b/Commands/BanCommand.cs @@ -11,33 +11,36 @@ public sealed class BanCommand : ICommand { var toBan = cmd.GetUser(args, cleanArgs, 0); if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return; - var memberToBan = cmd.GetMember(toBan.Item1); + var memberToBan = cmd.GetMember(toBan.Value.Id); if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return; var duration = CommandProcessor.GetTimeSpan(args, 1); var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "BanReason"); - if (reason is not null) await BanUserAsync(cmd, toBan, duration, reason); + if (reason is not null) await BanUserAsync(cmd, toBan.Value, duration, reason); } - private static async Task BanUserAsync(CommandProcessor cmd, Tuple toBan, TimeSpan duration, - string reason) { + private static async Task BanUserAsync( + CommandProcessor cmd, (ulong Id, SocketUser? User) toBan, TimeSpan duration, + string reason) { var author = cmd.Context.User; var guild = cmd.Context.Guild; - if (toBan.Item2 is not null) - await Utils.SendDirectMessage(toBan.Item2, + if (toBan.User is not null) + await Utils.SendDirectMessage( + toBan.User, string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason))); var guildBanMessage = $"({author}) {reason}"; - await guild.AddBanAsync(toBan.Item1, 0, guildBanMessage); + await guild.AddBanAsync(toBan.Id, 0, guildBanMessage); - var memberData = GuildData.Get(guild).MemberData[toBan.Item1]; + var memberData = GuildData.Get(guild).MemberData[toBan.Id]; 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()}>", + var feedback = string.Format( + Messages.FeedbackUserBanned, $"<@{toBan.Id.ToString()}>", Utils.GetHumanizedTimeSpan(duration), Utils.Wrap(reason)); cmd.Reply(feedback, ReplyEmojis.Banned); cmd.Audit(feedback); From 4cc00e01daa0b534d0a19e9761f4f3b6e761a2bc Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 14 Feb 2023 23:17:20 +0500 Subject: [PATCH 080/329] Bugfixes: - Do not call RemoveUnbanAsync in guild tick loop if the user is not banned - Fix !clear message logs being reversed - Do not process MessageReceivedEvents by webhooks Signed-off-by: Octol1ttle --- Boyfriend.cs | 11 ++++------- CommandProcessor.cs | 6 +++--- Commands/ClearCommand.cs | 2 +- EventHandler.cs | 8 +++----- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index fb462c4..a10b1c5 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -31,18 +31,14 @@ public static class Boyfriend { (new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)), (new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), (new Game("RetroSpecter - Overtime", ActivityType.Listening), new TimeSpan(0, 4, 33)), - (new Game("beatMARIO - Night of Knights", ActivityType.Listening), new TimeSpan(0, 4, 10)) + (new Game("SOOOO - Happppy song", ActivityType.Listening), new TimeSpan(0, 5, 24)) }; public static readonly DiscordSocketClient Client = new(Config); private static readonly List GuildTickTasks = new(); - public static void Main() { - InitAsync().GetAwaiter().GetResult(); - } - - private static async Task InitAsync() { + private static async Task Main() { var token = (await File.ReadAllTextAsync("token.txt")).Trim(); Client.Log += Log; @@ -149,7 +145,8 @@ public static class Boyfriend { foreach (var mData in data.MemberData.Values) { var user = guild.GetUser(mData.Id); - if (now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id); + if (now >= mData.BannedUntil && await guild.GetBanAsync(mData.Id) is not null) + _ = guild.RemoveBanAsync(mData.Id); if (!mData.IsInGuild) continue; if (mData.MutedUntil is null && ulong.TryParse(config["StarterRole"], out var starterRoleId) diff --git a/CommandProcessor.cs b/CommandProcessor.cs index 3ee91b6..2492d38 100644 --- a/CommandProcessor.cs +++ b/CommandProcessor.cs @@ -8,7 +8,6 @@ using Discord.WebSocket; namespace Boyfriend; public sealed class CommandProcessor { - private static readonly string Mention = $"<@{Boyfriend.Client.CurrentUser.Id}>"; private static readonly TimeSpan Infinity = TimeSpan.FromMilliseconds(-1); public static readonly ICommand[] Commands = { @@ -58,9 +57,10 @@ public sealed class CommandProcessor { private async Task RunCommandOnLine(string line, string cleanLine, string prefix) { var prefixed = line.StartsWith(prefix); - if (!prefixed && !line.StartsWith(Mention)) return; + var mention = Boyfriend.Client.CurrentUser.Mention; + if (!prefixed && !line.StartsWith(mention)) return; foreach (var command in Commands) { - var lineNoMention = line.Remove(0, prefixed ? prefix.Length : Mention.Length); + var lineNoMention = line.Remove(0, prefixed ? prefix.Length : mention.Length); if (!command.Aliases.Contains(lineNoMention.Trim().Split()[0])) continue; var args = lineNoMention.Trim().Split().Skip(1).ToArray(); diff --git a/Commands/ClearCommand.cs b/Commands/ClearCommand.cs index dc0e4aa..065c08a 100644 --- a/Commands/ClearCommand.cs +++ b/Commands/ClearCommand.cs @@ -17,7 +17,7 @@ public sealed class ClearCommand : ICommand { var messages = await channel.GetMessagesAsync((int)(toDelete + 1)).FlattenAsync(); var user = (SocketGuildUser)cmd.Context.User; - var msgArray = messages.ToArray(); + var msgArray = messages.Reverse().ToArray(); await channel.DeleteMessagesAsync(msgArray, Utils.GetRequestOptions(user.ToString()!)); foreach (var msg in msgArray.Where(m => !m.Author.IsBot)) diff --git a/EventHandler.cs b/EventHandler.cs index 2e9690f..64bba20 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -83,16 +83,14 @@ public static class EventHandler { } private static Task MessageReceivedEvent(IDeletable messageParam) { - if (messageParam is not SocketUserMessage message) return Task.CompletedTask; + if (messageParam is not SocketUserMessage message || message.Author.IsWebhook) return Task.CompletedTask; _ = message.CleanContent.ToLower() switch { "whoami" => message.ReplyAsync("`nobody`"), "сука !!" => message.ReplyAsync("`root`"), "воооо" => message.ReplyAsync("`removing /...`"), - "op ??" => message.ReplyAsync( - "некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"), - "++++" => message.ReplyAsync("#"), - _ => new CommandProcessor(message).HandleCommandAsync() + "++++" => message.ReplyAsync("#"), + _ => new CommandProcessor(message).HandleCommandAsync() }; return Task.CompletedTask; } From a2e0707ade189659b31f94200bf3ba5d865ad60b Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Mon, 20 Mar 2023 09:19:17 +0300 Subject: [PATCH 081/329] we do a little trolling (#26) --- EventHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/EventHandler.cs b/EventHandler.cs index 64bba20..4866f97 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -89,6 +89,7 @@ public static class EventHandler { "whoami" => message.ReplyAsync("`nobody`"), "сука !!" => message.ReplyAsync("`root`"), "воооо" => message.ReplyAsync("`removing /...`"), + "пон" => message.ReplyAsync("https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg"), "++++" => message.ReplyAsync("#"), _ => new CommandProcessor(message).HandleCommandAsync() }; From cac0bdc80186bd42419c02c1fa87bcdfd4b2152b Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 20 Mar 2023 12:33:54 +0500 Subject: [PATCH 082/329] Fix calling commands with @mentions Signed-off-by: Octol1ttle --- CommandProcessor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CommandProcessor.cs b/CommandProcessor.cs index 2492d38..3ee91b6 100644 --- a/CommandProcessor.cs +++ b/CommandProcessor.cs @@ -8,6 +8,7 @@ using Discord.WebSocket; namespace Boyfriend; public sealed class CommandProcessor { + private static readonly string Mention = $"<@{Boyfriend.Client.CurrentUser.Id}>"; private static readonly TimeSpan Infinity = TimeSpan.FromMilliseconds(-1); public static readonly ICommand[] Commands = { @@ -57,10 +58,9 @@ public sealed class CommandProcessor { private async Task RunCommandOnLine(string line, string cleanLine, string prefix) { var prefixed = line.StartsWith(prefix); - var mention = Boyfriend.Client.CurrentUser.Mention; - if (!prefixed && !line.StartsWith(mention)) return; + if (!prefixed && !line.StartsWith(Mention)) return; foreach (var command in Commands) { - var lineNoMention = line.Remove(0, prefixed ? prefix.Length : mention.Length); + var lineNoMention = line.Remove(0, prefixed ? prefix.Length : Mention.Length); if (!command.Aliases.Contains(lineNoMention.Trim().Split()[0])) continue; var args = lineNoMention.Trim().Split().Skip(1).ToArray(); From 05ce6803730f79b432ae85611d54dea9b35e5aaf Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 28 Mar 2023 15:28:34 +0500 Subject: [PATCH 083/329] Bump Discord.Net from 3.9.0 to 3.10.0 Signed-off-by: Octol1ttle --- Boyfriend.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 69b6840..680a25f 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -28,7 +28,7 @@ - + From 41958deb0a541b08ec887e293602f343ef52b601 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 28 Mar 2023 22:47:01 +0500 Subject: [PATCH 084/329] Fixed a memory leak caused by hitting rate limits, caused by attempts to add unset starter roles Signed-off-by: Octol1ttle --- Boyfriend.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index a10b1c5..3403ddf 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -150,13 +150,13 @@ public static class Boyfriend { if (!mData.IsInGuild) continue; if (mData.MutedUntil is null && ulong.TryParse(config["StarterRole"], out var starterRoleId) + && guild.GetRole(starterRoleId) is not null && !mData.Roles.Contains(starterRoleId)) _ = user.AddRoleAsync(starterRoleId); if (now >= mData.MutedUntil) { - await Utils.UnmuteMemberAsync( + saveData = await Utils.UnmuteMemberAsync( data, Client.CurrentUser.ToString(), user, Messages.PunishmentExpired); - saveData = true; } for (var i = mData.Reminders.Count - 1; i >= 0; i--) { From f01ce4fe1cffc7bb2b68a0ec511a1ea2cdb87ecd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 11:19:01 +0500 Subject: [PATCH 085/329] Bump muno92/resharper_inspectcode from 1.6.6 to 1.6.11 (#27) Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.6.6 to 1.6.11. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/resharper.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index 7de5d34..c4a69fd 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -1,4 +1,4 @@ -name: "ReSharper" +name: "ReSharper" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -29,7 +29,7 @@ jobs: run: dotnet restore - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.6.6 + uses: muno92/resharper_inspectcode@1.6.11 with: solutionPath: ./Boyfriend.sln ignoreIssueType: InvertIf From 6dbfc1bfaea5080aae8813eba63a7f02a929b282 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 06:09:22 +0000 Subject: [PATCH 086/329] Bump muno92/resharper_inspectcode from 1.6.11 to 1.6.13 (#30) Signed-off-by: dependabot[bot] --- .github/workflows/resharper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index c4a69fd..a9cb03c 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.11 + uses: muno92/resharper_inspectcode@1.6.13 with: solutionPath: ./Boyfriend.sln ignoreIssueType: InvertIf From cdfa0e11f7cbf3836149a9e13fbc7bca27e0c6f6 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 27 Apr 2023 22:14:30 +0500 Subject: [PATCH 087/329] Use DateTimeOffset#UtcNow instead of DateTimeOffset#Now (UtcNow is actually faster lol) Signed-off-by: Octol1ttle --- Boyfriend.cs | 2 +- Commands/BanCommand.cs | 2 +- Commands/MuteCommand.cs | 4 ++-- Commands/PingCommand.cs | 3 ++- Commands/RemindCommand.cs | 2 +- Data/GuildData.cs | 2 +- EventHandler.cs | 15 ++++++++------- Utils.cs | 19 ++++++++++++------- 8 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Boyfriend.cs b/Boyfriend.cs index 3403ddf..aaad927 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -61,7 +61,7 @@ public static class Boyfriend { private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) { if (GuildTickTasks.Count is not 0) return; - var now = DateTimeOffset.Now; + var now = DateTimeOffset.UtcNow; foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild, now)); if (now >= _nextSongAt) { diff --git a/Commands/BanCommand.cs b/Commands/BanCommand.cs index f984fd4..f12dbc7 100644 --- a/Commands/BanCommand.cs +++ b/Commands/BanCommand.cs @@ -34,7 +34,7 @@ public sealed class BanCommand : ICommand { var memberData = GuildData.Get(guild).MemberData[toBan.Id]; memberData.BannedUntil - = duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.Now.Add(duration); + = duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.UtcNow.Add(duration); memberData.Roles.Clear(); cmd.ConfigWriteScheduled = true; diff --git a/Commands/MuteCommand.cs b/Commands/MuteCommand.cs index 82a0e7e..241c0e2 100644 --- a/Commands/MuteCommand.cs +++ b/Commands/MuteCommand.cs @@ -19,7 +19,7 @@ public sealed class MuteCommand : ICommand { if ((role is not null && toMute.Roles.Contains(role)) || (toMute.TimedOutUntil is not null && toMute.TimedOutUntil.Value - > DateTimeOffset.Now)) { + > DateTimeOffset.UtcNow)) { cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error); return; } @@ -37,7 +37,7 @@ public sealed class MuteCommand : ICommand { var memberData = data.MemberData[toMute.Id]; if (role is not null) { - memberData.MutedUntil = DateTimeOffset.Now.Add(duration); + memberData.MutedUntil = DateTimeOffset.UtcNow.Add(duration); if (data.Preferences["RemoveRolesOnMute"] is "true") { memberData.Roles = toMute.RoleIds.ToList(); memberData.Roles.Remove(cmd.Context.Guild.Id); diff --git a/Commands/PingCommand.cs b/Commands/PingCommand.cs index 67e0861..1678046 100644 --- a/Commands/PingCommand.cs +++ b/Commands/PingCommand.cs @@ -7,7 +7,8 @@ public sealed class PingCommand : ICommand { var builder = Boyfriend.StringBuilder; builder.Append(Utils.GetBeep()) - .Append(Math.Round(Math.Abs(DateTimeOffset.Now.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds))) + .Append( + Math.Round(Math.Abs(DateTimeOffset.UtcNow.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds))) .Append(Messages.Milliseconds); cmd.Reply(builder.ToString(), ReplyEmojis.Ping); diff --git a/Commands/RemindCommand.cs b/Commands/RemindCommand.cs index 5a116fc..1d58763 100644 --- a/Commands/RemindCommand.cs +++ b/Commands/RemindCommand.cs @@ -15,7 +15,7 @@ public sealed class RemindCommand : ICommand { var reminderText = cmd.GetRemaining(cleanArgs, 1, "ReminderText"); if (reminderText is not null) { - var reminderOffset = DateTimeOffset.Now.Add(remindIn); + var reminderOffset = DateTimeOffset.UtcNow.Add(remindIn); GuildData.Get(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add( new Reminder { RemindAt = reminderOffset, diff --git a/Data/GuildData.cs b/Data/GuildData.cs index 08f9574..8403e2c 100644 --- a/Data/GuildData.cs +++ b/Data/GuildData.cs @@ -79,7 +79,7 @@ public record GuildData { foreach (var member in guild.Users) { if (MemberData.TryGetValue(member.Id, out var memberData)) { if (!memberData.IsInGuild - && DateTimeOffset.Now.ToUnixTimeSeconds() + && DateTimeOffset.UtcNow.ToUnixTimeSeconds() - Math.Max( memberData.LeftAt.Last().ToUnixTimeSeconds(), memberData.BannedUntil?.ToUnixTimeSeconds() ?? 0) diff --git a/EventHandler.cs b/EventHandler.cs index 4866f97..3eb42dc 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -70,7 +70,7 @@ public static class EventHandler { await Task.Delay(500); var auditLogEntry = (await guild.GetAuditLogsAsync(1).FlattenAsync()).First(); - if (auditLogEntry.CreatedAt >= DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(1)) + if (auditLogEntry.CreatedAt >= DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(1)) && auditLogEntry.Data is MessageDeleteAuditLogData data && msg.Author.Id == data.Target.Id) mention = auditLogEntry.User.Mention; @@ -89,9 +89,10 @@ public static class EventHandler { "whoami" => message.ReplyAsync("`nobody`"), "сука !!" => message.ReplyAsync("`root`"), "воооо" => message.ReplyAsync("`removing /...`"), - "пон" => message.ReplyAsync("https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg"), - "++++" => message.ReplyAsync("#"), - _ => new CommandProcessor(message).HandleCommandAsync() + "пон" => message.ReplyAsync( + "https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg"), + "++++" => message.ReplyAsync("#"), + _ => new CommandProcessor(message).HandleCommandAsync() }; return Task.CompletedTask; } @@ -141,7 +142,7 @@ public static class EventHandler { memberData.JoinedAt.Add(user.JoinedAt!.Value); } - if (DateTimeOffset.Now < memberData.MutedUntil) { + if (DateTimeOffset.UtcNow < memberData.MutedUntil) { await user.AddRoleAsync(data.MuteRole); if (config["RemoveRolesOnMute"] is "false" && config["ReturnRolesOnRejoin"] is "true") await user.AddRolesAsync(memberData.Roles); @@ -151,7 +152,7 @@ public static class EventHandler { private static Task UserLeftEvent(SocketGuild guild, SocketUser user) { var data = GuildData.Get(guild).MemberData[user.Id]; data.IsInGuild = false; - data.LeftAt.Add(DateTimeOffset.Now); + data.LeftAt.Add(DateTimeOffset.UtcNow); return Task.CompletedTask; } @@ -226,6 +227,6 @@ public static class EventHandler { await channel.SendMessageAsync( string.Format( Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), - Utils.GetHumanizedTimeSpan(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); + Utils.GetHumanizedTimeSpan(DateTimeOffset.UtcNow.Subtract(scheduledEvent.StartTime)))); } } diff --git a/Utils.cs b/Utils.cs index 1417370..27c6ecb 100644 --- a/Utils.cs +++ b/Utils.cs @@ -58,8 +58,10 @@ public static partial class Utils { await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); } catch (Exception e) { - await Boyfriend.Log(new LogMessage(LogSeverity.Error, nameof(Utils), - "Exception while silently sending message", e)); + await Boyfriend.Log( + new LogMessage( + LogSeverity.Error, nameof(Utils), + "Exception while silently sending message", e)); } } @@ -126,8 +128,10 @@ public static partial class Utils { } public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) { - return guild.GetTextChannel(ParseMention(GuildData.Get(guild) - .Preferences["EventNotificationChannel"])); + return guild.GetTextChannel( + ParseMention( + GuildData.Get(guild) + .Preferences["EventNotificationChannel"])); } public static bool UserExists(ulong id) { @@ -138,8 +142,9 @@ public static partial class Utils { 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) { + public static async Task UnmuteMemberAsync( + GuildData data, string modDiscrim, SocketGuildUser toUnmute, + string reason) { var requestOptions = GetRequestOptions($"({modDiscrim}) {reason}"); var role = data.MuteRole; @@ -150,7 +155,7 @@ public static partial class Utils { await toUnmute.RemoveRoleAsync(role, requestOptions); data.MemberData[toUnmute.Id].MutedUntil = null; } else { - if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value < DateTimeOffset.Now) return false; + if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value < DateTimeOffset.UtcNow) return false; await toUnmute.RemoveTimeOutAsync(requestOptions); } From 926dc0f1b768cba7fe89e0458ccc60eee667ef7b Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 27 Apr 2023 22:53:28 +0500 Subject: [PATCH 088/329] Make sure the audit log enumerable is not empty when determining message deleter Signed-off-by: Octol1ttle --- EventHandler.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/EventHandler.cs b/EventHandler.cs index 3eb42dc..017c604 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -69,11 +69,17 @@ public static class EventHandler { await Task.Delay(500); - var auditLogEntry = (await guild.GetAuditLogsAsync(1).FlattenAsync()).First(); - if (auditLogEntry.CreatedAt >= DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(1)) - && auditLogEntry.Data is MessageDeleteAuditLogData data - && msg.Author.Id == data.Target.Id) - mention = auditLogEntry.User.Mention; + var auditLogEnumerator + = (await guild.GetAuditLogsAsync(1, actionType: ActionType.MessageDeleted).FlattenAsync()).GetEnumerator(); + if (auditLogEnumerator.MoveNext()) { + var auditLogEntry = auditLogEnumerator.Current!; + if (auditLogEntry.CreatedAt >= DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(1)) + && auditLogEntry.Data is MessageDeleteAuditLogData data + && msg.Author.Id == data.Target.Id) + mention = auditLogEntry.User.Mention; + } + + auditLogEnumerator.Dispose(); await Utils.SendFeedbackAsync( string.Format( From b0fd49fea28fa3ecd6317401ad480946fc9a9fce Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 27 Apr 2023 22:55:18 +0500 Subject: [PATCH 089/329] Remove pointless PropertyGroups Signed-off-by: Octol1ttle --- Boyfriend.csproj | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 680a25f..0192abe 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -18,19 +18,9 @@ A legacy-driven Discord bot written in C# - - x64 - - - - x64 - none - - - From 02949656bf17e95dadb73c51ad790e68f5117ed3 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Fri, 12 May 2023 17:56:14 +0300 Subject: [PATCH 090/329] Change audit output if a message was deleted because of '!clear' (#34) --- Commands/ClearCommand.cs | 2 +- Messages.Designer.cs | 9 +++++++++ Messages.resx | 3 +++ Messages.ru.resx | 3 +++ Messages.tt-ru.resx | 7 +++++-- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Commands/ClearCommand.cs b/Commands/ClearCommand.cs index 065c08a..4d10595 100644 --- a/Commands/ClearCommand.cs +++ b/Commands/ClearCommand.cs @@ -23,7 +23,7 @@ public sealed class ClearCommand : ICommand { foreach (var msg in msgArray.Where(m => !m.Author.IsBot)) cmd.Audit( string.Format( - Messages.CachedMessageDeleted, msg.Author.Mention, + Messages.CachedMessageCleared, msg.Author.Mention, Utils.MentionChannel(channel.Id), Utils.Wrap(msg.CleanContent)), false); } diff --git a/Messages.Designer.cs b/Messages.Designer.cs index 5e21849..54a7eab 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -1120,5 +1120,14 @@ namespace Boyfriend { return ResourceManager.GetString("InvalidRemindIn", resourceCulture); } } + + /// + /// Looks up a localized string similar to Deleted message using cleanup from {0} in channel {1}: {2}. + /// + internal static string CachedMessageCleared { + get { + return ResourceManager.GetString("CachedMessageCleared", resourceCulture); + } + } } } diff --git a/Messages.resx b/Messages.resx index 2db1a50..7e771ae 100644 --- a/Messages.resx +++ b/Messages.resx @@ -123,6 +123,9 @@ Deleted message from {0} in channel {1}: {2} + + Cleared message from {0} in channel {1}: {2} + Edited message in channel {0}: {1} -> {2} diff --git a/Messages.ru.resx b/Messages.ru.resx index 72c10ff..cd5fecd 100644 --- a/Messages.ru.resx +++ b/Messages.ru.resx @@ -123,6 +123,9 @@ Удалено сообщение от {0} в канале {1}: {2} + + Очищено сообщение от {0} в канале {1}: {2} + Отредактировано сообщение в канале {0}: {1} -> {2} diff --git a/Messages.tt-ru.resx b/Messages.tt-ru.resx index fa4af35..6f66300 100644 --- a/Messages.tt-ru.resx +++ b/Messages.tt-ru.resx @@ -121,10 +121,13 @@ {0}я родился! - вырезано {0} в канале {1}: {2} + вырезано сообщение от {0} в канале {1}: {2} + + + вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2} - переделано {0}: {1} -> {2} + переделано сообщение от {0}: {1} -> {2} {0}, добро пожаловать на сервер {1} From e80ced2d54021cab60c0da434186465a2f28d800 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Mon, 15 May 2023 18:30:58 +0300 Subject: [PATCH 091/329] Update mctaylors' Language (#36) coolest pull request fr --- Messages.tt-ru.resx | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Messages.tt-ru.resx b/Messages.tt-ru.resx index 6f66300..88364b2 100644 --- a/Messages.tt-ru.resx +++ b/Messages.tt-ru.resx @@ -175,8 +175,8 @@ *тут ничего нет* - *тут ничего нет* - + нъет + настройки: @@ -192,9 +192,9 @@ разглашать о том что пришел новый шизоид - - роль замученного - + + звание замученного + такого языка нету, ты шо, есть только такие: @@ -211,8 +211,8 @@ шизоид не замучен! - приветствие - + здравствуйте (типо настройка) + выбери число от {0} до {1} вместо {2}! @@ -222,9 +222,9 @@ такой прикол не существует - - получать инфу о рождении бота - + + получать инфу о старте бота + криво настроил прикол, давай по новой @@ -285,9 +285,9 @@ ты все сломал! значение прикола `{0}` и так {1} - - *тут ничего нет* - + + нъет + прикол для `{0}` теперь установлен на {1} @@ -423,9 +423,9 @@ мут этому шизику нельзя - - ты шо далбайоп шоле, админ замозамучался, не трожь - + + сильно + ты замучен. From c83a458633f17528303b595c0234aac324cbca64 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Tue, 16 May 2023 08:58:35 +0300 Subject: [PATCH 092/329] Rearrange commands in !help (#37) --- CommandProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CommandProcessor.cs b/CommandProcessor.cs index 3ee91b6..bd8b53e 100644 --- a/CommandProcessor.cs +++ b/CommandProcessor.cs @@ -14,8 +14,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 RemindCommand() + new RemindCommand(), new SettingsCommand(), new UnbanCommand(), + new UnmuteCommand() }; private readonly StringBuilder _stackedPrivateFeedback = new(); From 8d740e1c52be0a81cd9e7d0809bcc071f2121211 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 17 May 2023 13:16:42 +0500 Subject: [PATCH 093/329] Update CODEOWNERS Signed-off-by: Octol1ttle --- .github/CODEOWNERS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1915f8c..1c732eb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,4 @@ -* @TeamOctolings/boyfriend-admins +* @TeamOctolings/boyfriend +.github/CODEOWNERS @TeamOctolings/boyfriend-admins +*.md @mctaylors Messages.tt-ru.resx @mctaylors From 2ab7a07784d308e4fbe3534d36a24019ed10fa26 Mon Sep 17 00:00:00 2001 From: mctaylors <95250141+mctaylors@users.noreply.github.com> Date: Wed, 17 May 2023 13:41:23 +0300 Subject: [PATCH 094/329] Update README.md (#39) --- .github/README.md | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/.github/README.md b/.github/README.md index 15bc0ff..ee86305 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,17 +1,27 @@ -![Boyfriend-Dark](https://user-images.githubusercontent.com/95250141/206895339-ef5510c8-8b30-4887-b89c-5dc14a24b18a.png#gh-dark-mode-only) -![Boyfriend-Light](https://user-images.githubusercontent.com/95250141/206895340-3415d97d-91fd-4fb6-8c17-4e1bf340e1df.png#gh-light-mode-only) + + + + + + + Boyfriend Logo + + ![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master) ![GitHub last commit](https://img.shields.io/github/last-commit/TeamOctolings/Boyfriend) -## Building -To build Boyfriend, you need to clone this repo and compile it with [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0). -``` -git clone https://github.com/TeamOctolings/Boyfriend -cd Boyfriend -dotnet build -``` +Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and Discord.Net -## Initial setup -Create `token.txt` in `Boyfriend/bin/Debug/net7.0/` and enter your bot token. Then, run `Boyfriend.exe`. +# Features +* Banning, muting, kicking, etc. +* Reminding you about something if you wish +* Reminding everyone about that new event you made +* Log everything from joining the server to deleting messages + +*...and more!* + +# Getting Started + +You can read our [wiki](https://github.com/TeamOctolings/Boyfriend/wiki) in order to assemble your Boyfriend™ and moderate the server. From abbb58f80103e7f2487644ff59f8a74c05c3c271 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 9 Jul 2023 18:32:14 +0500 Subject: [PATCH 095/329] Switch to Remora.Discord (#41) result checks go brrr this also involves switching to using Discord's modern stuff like embeds and interactions and using brand-new for me programming concepts (dependency injection, results) --------- Signed-off-by: Octol1ttle Signed-off-by: mctaylors <95250141+mctaylors@users.noreply.github.com> Co-authored-by: mctaylors <95250141+mctaylors@users.noreply.github.com> Co-authored-by: nrdk Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .editorconfig | 1 + .github/CODEOWNERS | 2 +- .github/ISSUE_TEMPLATE/bug-report.yml | 76 + .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature-request.yml | 38 + .github/README.md | 27 - .github/workflows/codeql.yml | 44 - .github/workflows/resharper.yml | 7 +- .gitignore | 1 - Boyfriend.cs | 252 +-- Boyfriend.csproj | 33 +- ColorsList.cs | 18 + CommandProcessor.cs | 321 ---- Commands/AboutCommandGroup.cs | 75 + Commands/BanCommand.cs | 48 - Commands/BanCommandGroup.cs | 275 ++++ Commands/ClearCommand.cs | 30 - Commands/ClearCommandGroup.cs | 120 ++ Commands/ErrorLoggingEvents.cs | 62 + Commands/HelpCommand.cs | 21 - Commands/ICommand.cs | 7 - Commands/KickCommand.cs | 34 - Commands/KickCommandGroup.cs | 164 ++ Commands/MuteCommand.cs | 71 - Commands/MuteCommandGroup.cs | 258 +++ Commands/PingCommand.cs | 19 - Commands/PingCommandGroup.cs | 79 + Commands/RemindCommand.cs | 34 - Commands/RemindCommandGroup.cs | 66 + Commands/SettingsCommand.cs | 164 -- Commands/SettingsCommandGroup.cs | 158 ++ Commands/UnbanCommand.cs | 25 - Commands/UnmuteCommand.cs | 36 - Data/GuildConfiguration.cs | 90 ++ Data/GuildData.cs | 155 +- Data/MemberData.cs | 42 +- Data/Reminder.cs | 8 +- Data/ScheduledEventData.cs | 17 + EventHandler.cs | 238 --- EventResponders.cs | 335 ++++ Extensions.cs | 189 +++ InteractionResponders.cs | 36 + Messages.Designer.cs | 1670 ++++++++++++-------- Messages.resx | 528 ++++--- Messages.ru.resx | 530 ++++--- Messages.tt-ru.resx | 558 ++++--- ReplyEmojis.cs | 19 - Services/GuildDataService.cs | 109 ++ Services/GuildUpdateService.cs | 389 +++++ Services/UtilityService.cs | 140 ++ Utils.cs | 168 -- docs/CODE_OF_CONDUCT.md | 128 ++ docs/CONTRIBUTING.md | 68 + docs/README.md | 48 + 54 files changed, 5011 insertions(+), 3021 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml delete mode 100644 .github/README.md delete mode 100644 .github/workflows/codeql.yml create mode 100644 ColorsList.cs delete mode 100644 CommandProcessor.cs create mode 100644 Commands/AboutCommandGroup.cs delete mode 100644 Commands/BanCommand.cs create mode 100644 Commands/BanCommandGroup.cs delete mode 100644 Commands/ClearCommand.cs create mode 100644 Commands/ClearCommandGroup.cs create mode 100644 Commands/ErrorLoggingEvents.cs delete mode 100644 Commands/HelpCommand.cs delete mode 100644 Commands/ICommand.cs delete mode 100644 Commands/KickCommand.cs create mode 100644 Commands/KickCommandGroup.cs delete mode 100644 Commands/MuteCommand.cs create mode 100644 Commands/MuteCommandGroup.cs delete mode 100644 Commands/PingCommand.cs create mode 100644 Commands/PingCommandGroup.cs delete mode 100644 Commands/RemindCommand.cs create mode 100644 Commands/RemindCommandGroup.cs delete mode 100644 Commands/SettingsCommand.cs create mode 100644 Commands/SettingsCommandGroup.cs delete mode 100644 Commands/UnbanCommand.cs delete mode 100644 Commands/UnmuteCommand.cs create mode 100644 Data/GuildConfiguration.cs create mode 100644 Data/ScheduledEventData.cs delete mode 100644 EventHandler.cs create mode 100644 EventResponders.cs create mode 100644 Extensions.cs create mode 100644 InteractionResponders.cs delete mode 100644 ReplyEmojis.cs create mode 100644 Services/GuildDataService.cs create mode 100644 Services/GuildUpdateService.cs create mode 100644 Services/UtilityService.cs delete mode 100644 Utils.cs create mode 100644 docs/CODE_OF_CONDUCT.md create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/README.md diff --git a/.editorconfig b/.editorconfig index 8d71c08..bb647a7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -58,6 +58,7 @@ resharper_indent_nested_usings_stmt = true resharper_indent_nested_while_stmt = true resharper_indent_preprocessor_if = usual_indent resharper_indent_preprocessor_other = usual_indent +resharper_int_align_fields = true resharper_int_align_methods = true resharper_int_align_parameters = true resharper_int_align_properties = true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1c732eb..4c792a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ * @TeamOctolings/boyfriend .github/CODEOWNERS @TeamOctolings/boyfriend-admins -*.md @mctaylors +/docs/ @mctaylors Messages.tt-ru.resx @mctaylors diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..3f1083a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,76 @@ +name: Bug Report +description: Create a report to help us improve +labels: [ "bug" ] +body: + - type: markdown + attributes: + value: | + We welcome bug reports! Please see our [contribution guidelines](docs/CONTRIBUTING.md#reporting-bugs) for more information on writing a good bug report. This template will help us gather the information we need to start the triage process. + - type: textarea + id: background + attributes: + label: Description + description: Please share a clear and concise description of the problem. + placeholder: Description + validations: + required: true + - type: textarea + id: repro-steps + attributes: + label: Reproduction Steps + description: | + Please include minimal steps to reproduce the problem if possible. E.g.: the smallest possible command/action sequence. If possible include text as text rather than screenshots (so it shows up in searches). + placeholder: Minimal Reproduction + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: | + Provide a description of the expected behavior. + placeholder: Expected behavior + validations: + required: true + - type: textarea + id: actual-behavior + attributes: + label: Actual behavior + description: | + Provide a description of the actual behavior observed. If applicable please include any error messages, exception stacktraces or memory dumps. + placeholder: Actual behavior + validations: + required: true + - type: textarea + id: known-workarounds + attributes: + label: Known Workarounds + description: | + Please provide a description of any known workarounds. + placeholder: Known Workarounds + validations: + required: false + - type: textarea + id: configuration + attributes: + label: Configuration + description: | + Please provide more information on your configuration: + * Which version of .NET is the bot running on? + * What OS and version, and what distro if applicable? + * What is the architecture (x64, x86, ARM, ARM64)? + * Do you know whether it is specific to that configuration? + * If possible, please provide the Configuration.json for the affected guild + * If applicable, provide the member data JSON for the affected members + placeholder: Configuration + validations: + required: false + - type: textarea + id: other-info + attributes: + label: Other information + description: | + If you have an idea where the problem might lie, let us know that here. Please include any pointers to code, relevant changes, or related issues you know of. + placeholder: Other information + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..6dac200 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,38 @@ +name: Feature Request +description: Create a request for a feature you would like +labels: [ "type: enhancement" ] +body: + - type: textarea + id: background + attributes: + label: Description + description: Please share a clear and concise description of the feature you want. + placeholder: Description + validations: + required: true + - type: textarea + id: proposed-solution + attributes: + label: Proposed Solution + description: Please describe the solution you would like. + placeholder: Proposed Solution + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Considered Alternatives + description: | + Please provide a description of any alternative solutions or features you've considered. + placeholder: Considered Alternatives + validations: + required: false + - type: textarea + id: other-info + attributes: + label: Other Information + description: | + Please add any other context or screenshots about the feature request here. + placeholder: Other Information + validations: + required: false diff --git a/.github/README.md b/.github/README.md deleted file mode 100644 index ee86305..0000000 --- a/.github/README.md +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - Boyfriend Logo - - - -![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) -![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master) -![GitHub last commit](https://img.shields.io/github/last-commit/TeamOctolings/Boyfriend) - -Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and Discord.Net - -# Features -* Banning, muting, kicking, etc. -* Reminding you about something if you wish -* Reminding everyone about that new event you made -* Log everything from joining the server to deleting messages - -*...and more!* - -# Getting Started - -You can read our [wiki](https://github.com/TeamOctolings/Boyfriend/wiki) in order to assemble your Boyfriend™ and moderate the server. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 36ae7a7..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: "CodeQL" -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - schedule: - - cron: '45 7 * * 2' - -jobs: - analyze: - name: Analyze code - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'csharp' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - queries: +security-extended,security-and-quality - - - name: Build solution - uses: github/codeql-action/autobuild@v2 - - - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index a9cb03c..82b2562 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -18,9 +18,6 @@ jobs: contents: read security-events: write - strategy: - fail-fast: false - steps: - name: Checkout repository uses: actions/checkout@v3 @@ -29,8 +26,8 @@ jobs: run: dotnet restore - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.6.13 + uses: muno92/resharper_inspectcode@1.7.1 with: solutionPath: ./Boyfriend.sln - ignoreIssueType: InvertIf + ignoreIssueType: InvertIf, ConvertIfStatementToReturnStatement, ConvertIfStatementToSwitchStatement solutionWideAnalysis: true diff --git a/.gitignore b/.gitignore index 3816d9a..4529511 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .idea/ *.user -token.txt bin/ obj/ /packages/ diff --git a/Boyfriend.cs b/Boyfriend.cs index aaad927..6af4326 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -1,180 +1,96 @@ -using System.Text; -using System.Timers; -using Boyfriend.Data; -using Discord; -using Discord.Rest; -using Discord.WebSocket; -using Timer = System.Timers.Timer; +using Boyfriend.Commands; +using Boyfriend.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Remora.Commands.Extensions; +using Remora.Discord.API.Abstractions.Gateway.Commands; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Discord.Caching.Extensions; +using Remora.Discord.Caching.Services; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Services; +using Remora.Discord.Gateway; +using Remora.Discord.Gateway.Extensions; +using Remora.Discord.Hosting.Extensions; +using Remora.Discord.Interactivity.Extensions; +using Remora.Rest.Core; namespace Boyfriend; -public static class Boyfriend { - public static readonly StringBuilder StringBuilder = new(); +public class Boyfriend { + public static readonly AllowedMentions NoMentions = new( + Array.Empty(), Array.Empty(), Array.Empty()); - private static readonly DiscordSocketConfig Config = new() { - MessageCacheSize = 250, - GatewayIntents - = (GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers) - & ~GatewayIntents.GuildInvites, - AlwaysDownloadUsers = true, - AlwaysResolveStickers = false, - AlwaysDownloadDefaultStickers = false, - LargeThreshold = 500 - }; + public static async Task Main(string[] args) { + var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); + var services = host.Services; - private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; - private static uint _nextSongIndex; + var slashService = services.GetRequiredService(); + // Providing a guild ID to this call will result in command duplicates! + // To get rid of them, provide the ID of the guild containing duplicates, + // comment out calls to WithCommandGroup in CreateHostBuilder + // then launch the bot again and remove the guild ID + await slashService.UpdateSlashCommandsAsync(); - private static readonly (Game Song, TimeSpan Duration)[] ActivityList = { - (new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening), new TimeSpan(0, 3, 40)), - (new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)), - (new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)), - (new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)), - (new Game("RetroSpecter - Overtime", ActivityType.Listening), new TimeSpan(0, 4, 33)), - (new Game("SOOOO - Happppy song", ActivityType.Listening), new TimeSpan(0, 5, 24)) - }; - - public static readonly DiscordSocketClient Client = new(Config); - - private static readonly List GuildTickTasks = new(); - - private static async Task Main() { - var token = (await File.ReadAllTextAsync("token.txt")).Trim(); - - Client.Log += Log; - - await Client.LoginAsync(TokenType.Bot, token); - await Client.StartAsync(); - - EventHandler.InitEvents(); - - var timer = new Timer(); - timer.Interval = 1000; - timer.AutoReset = true; - timer.Elapsed += TickAllGuildsAsync; - if (ActivityList.Length is 0) timer.Dispose(); // CodeQL moment - timer.Start(); - - await Task.Delay(-1); + await host.RunAsync(); } - private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) { - if (GuildTickTasks.Count is not 0) return; + private static IHostBuilder CreateHostBuilder(string[] args) { + return Host.CreateDefaultBuilder(args) + .AddDiscordService( + services => { + var configuration = services.GetRequiredService(); - var now = DateTimeOffset.UtcNow; - foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild, now)); + return configuration.GetValue("BOT_TOKEN") + ?? throw new InvalidOperationException( + "No bot token has been provided. Set the " + + "BOT_TOKEN environment variable to a valid token."); + } + ).ConfigureServices( + (_, services) => { + services.Configure( + options => options.Intents |= GatewayIntents.MessageContents + | GatewayIntents.GuildMembers + | GatewayIntents.GuildScheduledEvents); + services.Configure( + settings => { + settings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); + settings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); + settings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); + settings.SetSlidingExpiration(TimeSpan.FromDays(7)); + }); - if (now >= _nextSongAt) { - var nextSong = ActivityList[_nextSongIndex]; - await Client.SetActivityAsync(nextSong.Song); - _nextSongAt = now.Add(nextSong.Duration); - _nextSongIndex++; - if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0; - } - - 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: - Console.ForegroundColor = ConsoleColor.DarkRed; - Console.Error.WriteLine(msg.ToString()); - break; - case LogSeverity.Error: - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine(msg.ToString()); - break; - case LogSeverity.Warning: - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine(msg.ToString()); - break; - case LogSeverity.Info: - Console.WriteLine(msg.ToString()); - break; - - case LogSeverity.Verbose: - case LogSeverity.Debug: - default: return Task.CompletedTask; - } - - Console.ResetColor(); - return Task.CompletedTask; - } - - private static async Task TickGuildAsync(SocketGuild guild, DateTimeOffset now) { - 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) - && 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 (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} ")); - - await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync( - string.Format( - Messages.EventEarlyNotification, - mentions, - Utils.Wrap(schEvent.Name), - schEvent.StartTime.ToUnixTimeSeconds().ToString()))!; - mentions.Clear(); - } - - foreach (var mData in data.MemberData.Values) { - var user = guild.GetUser(mData.Id); - if (now >= mData.BannedUntil && await guild.GetBanAsync(mData.Id) is not null) - _ = guild.RemoveBanAsync(mData.Id); - if (!mData.IsInGuild) continue; - if (mData.MutedUntil is null - && ulong.TryParse(config["StarterRole"], out var starterRoleId) - && guild.GetRole(starterRoleId) is not null - && !mData.Roles.Contains(starterRoleId)) _ = user.AddRoleAsync(starterRoleId); - - if (now >= mData.MutedUntil) { - saveData = await Utils.UnmuteMemberAsync( - data, Client.CurrentUser.ToString(), user, - Messages.PunishmentExpired); - } - - for (var i = mData.Reminders.Count - 1; i >= 0; i--) { - var reminder = mData.Reminders[i]; - if (now < reminder.RemindAt) continue; - - var channel = guild.GetTextChannel(reminder.ReminderChannel); - var toSend = $"{ReplyEmojis.Reminder} {user.Mention} {Utils.Wrap(reminder.ReminderText)}"; - if (channel is not null) - await channel.SendMessageAsync(toSend); - else - await Utils.SendDirectMessage(user, toSend); - - mData.Reminders.RemoveAt(i); - saveData = true; - } - } - - if (saveData) await data.Save(true); + services.AddTransient() + .AddDiscordCaching() + .AddDiscordCommands(true) + .AddPreparationErrorEvent() + .AddPostExecutionEvent() + .AddInteractivity() + .AddInteractionGroup() + .AddSingleton() + .AddSingleton() + .AddHostedService() + .AddCommandTree() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup(); + var responderTypes = typeof(Boyfriend).Assembly + .GetExportedTypes() + .Where(t => t.IsResponder()); + foreach (var responderType in responderTypes) services.AddResponder(responderType); + } + ).ConfigureLogging( + c => c.AddConsole() + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + ); } } diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 0192abe..29483d7 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -5,9 +5,9 @@ net7.0 enable enable - 1.0.0 + 2.0.0 Boyfriend - Octol1ttle, mctaylors + Octol1ttle, mctaylors, neroduckale AGPLv3 https://github.com/TeamOctolings/Boyfriend https://github.com/TeamOctolings/Boyfriend/blob/master/LICENSE @@ -19,8 +19,31 @@ - - - + + + + + + + + + + + True + True + Messages.resx + + + + + + + ResXFileCodeGenerator + Messages.Designer.cs + + + + + diff --git a/ColorsList.cs b/ColorsList.cs new file mode 100644 index 0000000..bdd5bce --- /dev/null +++ b/ColorsList.cs @@ -0,0 +1,18 @@ +using System.Drawing; + +namespace Boyfriend; + +/// +/// Contains all colors used in embeds. +/// +public static class ColorsList { + public static readonly Color Default = Color.Gray; + public static readonly Color Red = Color.Firebrick; + public static readonly Color Green = Color.PaleGreen; + public static readonly Color Yellow = Color.Gold; + public static readonly Color Blue = Color.RoyalBlue; + public static readonly Color Magenta = Color.Orchid; + public static readonly Color Cyan = Color.LightSkyBlue; + public static readonly Color Black = Color.Black; + public static readonly Color White = Color.WhiteSmoke; +} diff --git a/CommandProcessor.cs b/CommandProcessor.cs deleted file mode 100644 index bd8b53e..0000000 --- a/CommandProcessor.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System.Text; -using Boyfriend.Commands; -using Boyfriend.Data; -using Discord; -using Discord.Commands; -using Discord.WebSocket; - -namespace Boyfriend; - -public sealed class CommandProcessor { - private static readonly string Mention = $"<@{Boyfriend.Client.CurrentUser.Id}>"; - private static readonly TimeSpan Infinity = TimeSpan.FromMilliseconds(-1); - - public static readonly ICommand[] Commands = { - new BanCommand(), new ClearCommand(), new HelpCommand(), - new KickCommand(), new MuteCommand(), new PingCommand(), - new RemindCommand(), new SettingsCommand(), new UnbanCommand(), - new UnmuteCommand() - }; - - private readonly StringBuilder _stackedPrivateFeedback = new(); - private readonly StringBuilder _stackedPublicFeedback = new(); - private readonly StringBuilder _stackedReplyMessage = new(); - private readonly List _tasks = new(); - - public readonly SocketCommandContext Context; - - public bool ConfigWriteScheduled = false; - - public CommandProcessor(SocketUserMessage message) { - Context = new SocketCommandContext(Boyfriend.Client, message); - } - - public async Task HandleCommandAsync() { - var guild = Context.Guild; - var data = GuildData.Get(guild); - Utils.SetCurrentLanguage(guild); - - 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], data.Preferences["Prefix"])); - - try { Task.WaitAll(_tasks.ToArray()); } catch (AggregateException e) { - foreach (var ex in e.InnerExceptions) - await Boyfriend.Log( - new LogMessage( - LogSeverity.Error, nameof(CommandProcessor), - "Exception while executing commands", ex)); - } - - _tasks.Clear(); - - if (ConfigWriteScheduled) await data.Save(true); - - SendFeedbacks(); - } - - private async Task RunCommandOnLine(string line, string cleanLine, string prefix) { - var prefixed = line.StartsWith(prefix); - if (!prefixed && !line.StartsWith(Mention)) return; - foreach (var command in Commands) { - var lineNoMention = line.Remove(0, prefixed ? prefix.Length : Mention.Length); - if (!command.Aliases.Contains(lineNoMention.Trim().Split()[0])) continue; - - var args = lineNoMention.Trim().Split().Skip(1).ToArray(); - var cleanArgs = cleanLine.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray(); - await command.RunAsync(this, args, cleanArgs); - if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync(); - return; - } - } - - public void Reply(string response, string? customEmoji = null) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, $"{customEmoji ?? ReplyEmojis.Success} {response}", - Context.Message); - } - - public void Audit(string action, bool isPublic = true) { - var format = $"*[{Context.User.Mention}: {action}]*"; - 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); - } - - private void SendFeedbacks(bool reply = true) { - var hasReply = _stackedReplyMessage.Length > 0; - if (reply && hasReply) - _ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None); - - var data = GuildData.Get(Context.Guild); - var adminChannel = data.PrivateFeedbackChannel; - var systemChannel = data.PublicFeedbackChannel; - if (_stackedPrivateFeedback.Length > 0 - && adminChannel is not null - && (adminChannel.Id != Context.Message.Channel.Id || !hasReply)) { - _ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString()); - _stackedPrivateFeedback.Clear(); - } - - if (_stackedPublicFeedback.Length > 0 - && systemChannel is not null - && systemChannel.Id != adminChannel?.Id - && (systemChannel.Id != Context.Message.Channel.Id || !hasReply)) { - _ = Utils.SilentSendAsync(systemChannel, _stackedPublicFeedback.ToString()); - _stackedPublicFeedback.Clear(); - } - } - - public string? GetRemaining(string[] from, int startIndex, string? argument) { - if (startIndex >= from.Length && argument is not null) - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.MissingArgument} {Utils.GetMessage($"Missing{argument}")}", Context.Message); - else return string.Join(" ", from, startIndex, from.Length - startIndex); - return null; - } - - public (ulong Id, SocketUser? User)? GetUser(string[] args, string[] cleanArgs, int index) { - if (index >= args.Length) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", - Context.Message); - return 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 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 (mention, Boyfriend.Client.GetUser(mention)); - } - - public bool HasPermission(GuildPermission permission) { - if (!Context.Guild.CurrentUser.GuildPermissions.Has(permission)) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"BotCannot{permission}")}", - Context.Message); - return false; - } - - if (!GetMember().GuildPermissions.Has(permission) - && Context.Guild.OwnerId != Context.User.Id) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}", - Context.Message); - return false; - } - - return true; - } - - private SocketGuildUser GetMember() { - return GetMember(Context.User.Id)!; - } - - 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); - return null; - } - - var member = Context.Guild.GetUser(Utils.ParseMention(args[index])); - if (member is null) - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.InvalidArgument} {Messages.InvalidMember}", - Context.Message); - return member; - } - - public ulong? GetBan(string[] args, int index) { - if (index >= args.Length) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", - Context.Message); - return null; - } - - var id = Utils.ParseMention(args[index]); - if (Context.Guild.GetBanAsync(id) is null) { - Utils.SafeAppendToBuilder(_stackedReplyMessage, Messages.UserNotBanned, Context.Message); - return null; - } - - return id; - } - - public int? GetNumberRange(string[] args, int index, int min, int max, string? argument) { - if (index >= args.Length) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.MissingArgument} {string.Format(Messages.MissingNumber, min.ToString(), max.ToString())}", - Context.Message); - return null; - } - - if (!int.TryParse(args[index], out var i)) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}Invalid"), min.ToString(), max.ToString(), Utils.Wrap(args[index]))}", - Context.Message); - return null; - } - - if (argument is null) return i; - if (i < min) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooSmall"), min.ToString())}", - Context.Message); - return null; - } - - if (i > max) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooLarge"), max.ToString())}", - Context.Message); - return null; - } - - return i; - } - - public static TimeSpan GetTimeSpan(string[] args, int index) { - if (index >= args.Length) return Infinity; - var chars = args[index].AsSpan(); - var numberBuilder = Boyfriend.StringBuilder; - int days = 0, hours = 0, minutes = 0, seconds = 0; - foreach (var c in chars) - if (char.IsDigit(c)) { numberBuilder.Append(c); } else { - if (numberBuilder.Length is 0) return Infinity; - switch (c) { - case 'd' or 'D' or 'д' or 'Д': - days += int.Parse(numberBuilder.ToString()); - numberBuilder.Clear(); - break; - case 'h' or 'H' or 'ч' or 'Ч': - hours += int.Parse(numberBuilder.ToString()); - numberBuilder.Clear(); - break; - case 'm' or 'M' or 'м' or 'М': - minutes += int.Parse(numberBuilder.ToString()); - numberBuilder.Clear(); - break; - case 's' or 'S' or 'с' or 'С': - seconds += int.Parse(numberBuilder.ToString()); - numberBuilder.Clear(); - break; - default: return Infinity; - } - } - - numberBuilder.Clear(); - return new TimeSpan(days, hours, minutes, seconds); - } - - public bool CanInteractWith(SocketGuildUser user, string action) { - if (Context.User.Id == user.Id) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Themselves")}", Context.Message); - return false; - } - - if (Context.Guild.CurrentUser.Id == user.Id) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Bot")}", Context.Message); - return false; - } - - if (Context.Guild.Owner.Id == user.Id) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Owner")}", Context.Message); - return false; - } - - if (Context.Guild.CurrentUser.Hierarchy <= user.Hierarchy) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"BotCannot{action}Target")}", Context.Message); - return false; - } - - if (Context.Guild.Owner.Id != Context.User.Id && GetMember().Hierarchy <= user.Hierarchy) { - Utils.SafeAppendToBuilder( - _stackedReplyMessage, - $"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Target")}", Context.Message); - return false; - } - - return true; - } -} diff --git a/Commands/AboutCommandGroup.cs b/Commands/AboutCommandGroup.cs new file mode 100644 index 0000000..e4820c6 --- /dev/null +++ b/Commands/AboutCommandGroup.cs @@ -0,0 +1,75 @@ +using System.ComponentModel; +using System.Text; +using Boyfriend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +/// +/// Handles the command to show information about this bot: /about. +/// +public class AboutCommandGroup : CommandGroup { + private static readonly string[] Developers = { "Octol1ttle", "mctaylors", "neroduckale" }; + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestUserAPI _userApi; + + public AboutCommandGroup( + ICommandContext context, GuildDataService dataService, + FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + _context = context; + _dataService = dataService; + _feedbackService = feedbackService; + _userApi = userApi; + } + + /// + /// A slash command that shows information about this bot. + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("about")] + [Description("Shows Boyfriend's developers")] + public async Task SendAboutBotAsync() { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); + Messages.Culture = cfg.GetCulture(); + + var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers)); + foreach (var dev in Developers) + builder.AppendLine($"@{dev} — {$"AboutDeveloper@{dev}".Localized()}"); + + builder.AppendLine() + .AppendLine(Markdown.Bold(Messages.AboutTitleWiki)) + .AppendLine("https://github.com/TeamOctolings/Boyfriend/wiki"); + + var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser) + .WithDescription(builder.ToString()) + .WithColour(ColorsList.Cyan) + .WithImageUrl( + "https://media.discordapp.net/attachments/837385840946053181/1125009665592393738/boyfriend.png") + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } +} diff --git a/Commands/BanCommand.cs b/Commands/BanCommand.cs deleted file mode 100644 index f12dbc7..0000000 --- a/Commands/BanCommand.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Boyfriend.Data; -using Discord; -using Discord.WebSocket; - -namespace Boyfriend.Commands; - -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); - if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return; - - var memberToBan = cmd.GetMember(toBan.Value.Id); - if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return; - - var duration = CommandProcessor.GetTimeSpan(args, 1); - var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "BanReason"); - if (reason is not null) await BanUserAsync(cmd, toBan.Value, duration, reason); - } - - private static async Task BanUserAsync( - CommandProcessor cmd, (ulong Id, SocketUser? User) toBan, TimeSpan duration, - string reason) { - var author = cmd.Context.User; - var guild = cmd.Context.Guild; - if (toBan.User is not null) - await Utils.SendDirectMessage( - toBan.User, - string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason))); - - var guildBanMessage = $"({author}) {reason}"; - await guild.AddBanAsync(toBan.Id, 0, guildBanMessage); - - var memberData = GuildData.Get(guild).MemberData[toBan.Id]; - memberData.BannedUntil - = duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.UtcNow.Add(duration); - memberData.Roles.Clear(); - - cmd.ConfigWriteScheduled = true; - - var feedback = string.Format( - Messages.FeedbackUserBanned, $"<@{toBan.Id.ToString()}>", - Utils.GetHumanizedTimeSpan(duration), Utils.Wrap(reason)); - cmd.Reply(feedback, ReplyEmojis.Banned); - cmd.Audit(feedback); - } -} diff --git a/Commands/BanCommandGroup.cs b/Commands/BanCommandGroup.cs new file mode 100644 index 0000000..a525afb --- /dev/null +++ b/Commands/BanCommandGroup.cs @@ -0,0 +1,275 @@ +using System.ComponentModel; +using System.Text; +using Boyfriend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Conditions; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +/// +/// Handles commands related to ban management: /ban and /unban. +/// +public class BanCommandGroup : CommandGroup { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; + + public BanCommandGroup( + ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService, + FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + UtilityService utility) { + _context = context; + _channelApi = channelApi; + _dataService = dataService; + _feedbackService = feedbackService; + _guildApi = guildApi; + _userApi = userApi; + _utility = utility; + } + + /// + /// A slash command that bans a Discord user with the specified reason. + /// + /// The user to ban. + /// The duration for this ban. The user will be automatically unbanned after this duration. + /// + /// The reason for this ban. Must be encoded with when passed to + /// . + /// + /// + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user + /// was banned and vice-versa. + /// + /// + [Command("ban", "бан")] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.BanMembers)] + [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] + [Description("Ban user")] + public async Task BanUserAsync( + [Description("User to ban")] IUser target, + [Description("Ban reason")] string reason, + [Description("Ban duration")] + TimeSpan? duration = null) { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + // The current user's avatar is used when sending error messages + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var data = await _dataService.GetData(guildId.Value, CancellationToken); + var cfg = data.Configuration; + Messages.Culture = data.Culture; + + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken); + if (existingBanResult.IsDefined()) { + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser) + .WithColour(ColorsList.Red).Build(); + + if (!embed.IsDefined(out var alreadyBuilt)) + return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + } + + var interactionResult + = await _utility.CheckInteractionsAsync(guildId.Value, userId.Value, target.ID, "Ban", CancellationToken); + if (!interactionResult.IsSuccess) + return Result.FromError(interactionResult); + + Result responseEmbed; + if (interactionResult.Entity is not null) { + responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + .WithColour(ColorsList.Red).Build(); + } else { + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)); + if (duration is not null) + builder.Append( + string.Format( + Messages.DescriptionActionExpiresAt, + Markdown.Timestamp(DateTimeOffset.UtcNow.Add(duration.Value)))); + var description = builder.ToString(); + + var dmChannelResult = await _userApi.CreateDMAsync(target.ID, CancellationToken); + if (dmChannelResult.IsDefined(out var dmChannel)) { + var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + return Result.FromError(guildResult); + + var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) + .WithTitle(Messages.YouWereBanned) + .WithDescription(description) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Red) + .Build(); + + if (!dmEmbed.IsDefined(out var dmBuilt)) + return Result.FromError(dmEmbed); + await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken); + } + + var banResult = await _guildApi.CreateGuildBanAsync( + guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), + ct: CancellationToken); + if (!banResult.IsSuccess) + return Result.FromError(banResult.Error); + var memberData = data.GetMemberData(target.ID); + memberData.BannedUntil + = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; + memberData.Roles.Clear(); + + responseEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserBanned, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) + || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + var logEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserBanned, target.GetTag()), target) + .WithDescription(description) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Red) + .Build(); + + if (!logEmbed.IsDefined(out var logBuilt)) + return Result.FromError(logEmbed); + + var builtArray = new[] { logBuilt }; + // Not awaiting to reduce response time + if (cfg.PublicFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel + && cfg.PrivateFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + } + } + + if (!responseEmbed.IsDefined(out var built)) + return Result.FromError(responseEmbed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } + + /// + /// A slash command that unbans a Discord user with the specified reason. + /// + /// The user to unban. + /// + /// The reason for this unban. Must be encoded with when passed to + /// . + /// + /// + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user + /// was unbanned and vice-versa. + /// + /// + /// + [Command("unban")] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.BanMembers)] + [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] + [Description("Unban user")] + public async Task UnbanUserAsync( + [Description("User to unban")] IUser target, + [Description("Unban reason")] + string reason) { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + // The current user's avatar is used when sending error messages + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); + Messages.Culture = cfg.GetCulture(); + + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken); + if (!existingBanResult.IsDefined()) { + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser) + .WithColour(ColorsList.Red).Build(); + + if (!embed.IsDefined(out var alreadyBuilt)) + return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + } + + // Needed to get the tag and avatar + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var unbanResult = await _guildApi.RemoveGuildBanAsync( + guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + ct: CancellationToken); + if (!unbanResult.IsSuccess) + return Result.FromError(unbanResult.Error); + + var responseEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserUnbanned, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) + || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + var logEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserUnbanned, target.GetTag()), target) + .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Green) + .Build(); + + if (!logEmbed.IsDefined(out var logBuilt)) + return Result.FromError(logEmbed); + + var builtArray = new[] { logBuilt }; + + // Not awaiting to reduce response time + if (cfg.PublicFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel + && cfg.PrivateFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + } + + if (!responseEmbed.IsDefined(out var built)) + return Result.FromError(responseEmbed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } +} diff --git a/Commands/ClearCommand.cs b/Commands/ClearCommand.cs deleted file mode 100644 index 4d10595..0000000 --- a/Commands/ClearCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Diagnostics; -using Discord; -using Discord.WebSocket; - -namespace Boyfriend.Commands; - -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 UnreachableException(); - - if (!cmd.HasPermission(GuildPermission.ManageMessages)) return; - - var toDelete = cmd.GetNumberRange(cleanArgs, 0, 1, 200, "ClearAmount"); - if (toDelete is null) return; - var messages = await channel.GetMessagesAsync((int)(toDelete + 1)).FlattenAsync(); - - var user = (SocketGuildUser)cmd.Context.User; - var msgArray = messages.Reverse().ToArray(); - await channel.DeleteMessagesAsync(msgArray, Utils.GetRequestOptions(user.ToString()!)); - - foreach (var msg in msgArray.Where(m => !m.Author.IsBot)) - cmd.Audit( - string.Format( - Messages.CachedMessageCleared, msg.Author.Mention, - Utils.MentionChannel(channel.Id), - Utils.Wrap(msg.CleanContent)), false); - } -} diff --git a/Commands/ClearCommandGroup.cs b/Commands/ClearCommandGroup.cs new file mode 100644 index 0000000..de44fbb --- /dev/null +++ b/Commands/ClearCommandGroup.cs @@ -0,0 +1,120 @@ +using System.ComponentModel; +using System.Text; +using Boyfriend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Conditions; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +/// +/// Handles the command to clear messages in a channel: /clear. +/// +public class ClearCommandGroup : CommandGroup { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestUserAPI _userApi; + + public ClearCommandGroup( + IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService dataService, + FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + _channelApi = channelApi; + _context = context; + _dataService = dataService; + _feedbackService = feedbackService; + _userApi = userApi; + } + + /// + /// A slash command that clears messages in the channel it was executed. + /// + /// The amount of messages to clear. + /// + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages + /// were cleared and vice-versa. + /// + [Command("clear", "очистить")] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] + [RequireBotDiscordPermissions(DiscordPermission.ManageMessages)] + [Description("Remove multiple messages")] + public async Task ClearMessagesAsync( + [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] + int amount) { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + var messagesResult = await _channelApi.GetChannelMessagesAsync( + channelId.Value, limit: amount + 1, ct: CancellationToken); + if (!messagesResult.IsDefined(out var messages)) + return Result.FromError(messagesResult); + + var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); + Messages.Culture = cfg.GetCulture(); + + var idList = new List(messages.Count); + var builder = new StringBuilder().AppendLine(Mention.Channel(channelId.Value)).AppendLine(); + for (var i = messages.Count - 1; i >= 1; i--) { // '>= 1' to skip last message ('Boyfriend is thinking...') + var message = messages[i]; + idList.Add(message.ID); + builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author))); + builder.Append(message.Content.InBlockCode()); + } + + var description = builder.ToString(); + + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var deleteResult = await _channelApi.BulkDeleteMessagesAsync( + channelId.Value, idList, user.GetTag().EncodeHeader(), CancellationToken); + if (!deleteResult.IsSuccess) + return Result.FromError(deleteResult.Error); + + // The current user's avatar is used when sending messages + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var title = string.Format(Messages.MessagesCleared, amount.ToString()); + if (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value) { + var logEmbed = new EmbedBuilder().WithSmallTitle(title, currentUser) + .WithDescription(description) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Red) + .Build(); + + if (!logEmbed.IsDefined(out var logBuilt)) + return Result.FromError(logEmbed); + + // Not awaiting to reduce response time + if (cfg.PrivateFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { logBuilt }, + ct: CancellationToken); + } + + var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) + .WithColour(ColorsList.Green).Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } +} diff --git a/Commands/ErrorLoggingEvents.cs b/Commands/ErrorLoggingEvents.cs new file mode 100644 index 0000000..30869b4 --- /dev/null +++ b/Commands/ErrorLoggingEvents.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Logging; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Services; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global + +namespace Boyfriend.Commands; + +/// +/// Handles error logging for slash commands that couldn't be successfully prepared. +/// +public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent { + private readonly ILogger _logger; + + public ErrorLoggingPreparationErrorEvent(ILogger logger) { + _logger = logger; + } + + /// + /// Logs a warning using the injected if the has not + /// succeeded. + /// + /// The context of the slash command. Unused. + /// The result whose success is checked. + /// The cancellation token for this operation. Unused. + /// A result which has succeeded. + public Task PreparationFailed( + IOperationContext context, IResult preparationResult, CancellationToken ct = default) { + if (!preparationResult.IsSuccess) + _logger.LogWarning("Error in slash command preparation.\n{ErrorMessage}", preparationResult.Error.Message); + + return Task.FromResult(Result.FromSuccess()); + } +} + +/// +/// Handles error logging for slash command groups. +/// +public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { + private readonly ILogger _logger; + + public ErrorLoggingPostExecutionEvent(ILogger logger) { + _logger = logger; + } + + /// + /// Logs a warning using the injected if the has not + /// succeeded. + /// + /// The context of the slash command. Unused. + /// The result whose success is checked. + /// The cancellation token for this operation. Unused. + /// A result which has succeeded. + public Task AfterExecutionAsync( + ICommandContext context, IResult commandResult, CancellationToken ct = default) { + if (!commandResult.IsSuccess) + _logger.LogWarning("Error in slash command execution.\n{ErrorMessage}", commandResult.Error.Message); + + return Task.FromResult(Result.FromSuccess()); + } +} diff --git a/Commands/HelpCommand.cs b/Commands/HelpCommand.cs deleted file mode 100644 index 72b788b..0000000 --- a/Commands/HelpCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Boyfriend.Data; -using Humanizer; - -namespace Boyfriend.Commands; - -public sealed class HelpCommand : ICommand { - public string[] Aliases { get; } = { "help", "помощь", "справка" }; - - public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - var prefix = GuildData.Get(cmd.Context.Guild).Preferences["Prefix"]; - var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp); - - foreach (var command in CommandProcessor.Commands) - toSend.Append( - $"\n`{prefix}{command.Aliases[0]}`: {Utils.GetMessage($"CommandDescription{command.Aliases[0].Titleize()}")}"); - cmd.Reply(toSend.ToString(), ReplyEmojis.Help); - toSend.Clear(); - - return Task.CompletedTask; - } -} diff --git a/Commands/ICommand.cs b/Commands/ICommand.cs deleted file mode 100644 index 5dccb98..0000000 --- a/Commands/ICommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Boyfriend.Commands; - -public interface ICommand { - public string[] Aliases { get; } - - public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs); -} diff --git a/Commands/KickCommand.cs b/Commands/KickCommand.cs deleted file mode 100644 index 6d5689b..0000000 --- a/Commands/KickCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Boyfriend.Data; -using Discord; -using Discord.WebSocket; - -namespace Boyfriend.Commands; - -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, 0); - if (toKick is null || !cmd.HasPermission(GuildPermission.KickMembers)) return; - - if (cmd.CanInteractWith(toKick, "Kick")) - await KickMemberAsync(cmd, toKick, cmd.GetRemaining(args, 1, "KickReason")); - } - - private static async Task KickMemberAsync(CommandProcessor cmd, SocketGuildUser toKick, string? reason) { - if (reason is null) return; - var guildKickMessage = $"({cmd.Context.User}) {reason}"; - - await Utils.SendDirectMessage(toKick, - 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); - cmd.Audit(format); - } -} diff --git a/Commands/KickCommandGroup.cs b/Commands/KickCommandGroup.cs new file mode 100644 index 0000000..e70a903 --- /dev/null +++ b/Commands/KickCommandGroup.cs @@ -0,0 +1,164 @@ +using System.ComponentModel; +using Boyfriend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Conditions; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +/// +/// Handles the command to kick members of a guild: /kick. +/// +public class KickCommandGroup : CommandGroup { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; + + public KickCommandGroup( + ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService, + FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + UtilityService utility) { + _context = context; + _channelApi = channelApi; + _dataService = dataService; + _feedbackService = feedbackService; + _guildApi = guildApi; + _userApi = userApi; + _utility = utility; + } + + /// + /// A slash command that kicks a Discord user with the specified reason. + /// + /// The user to kick. + /// + /// The reason for this kick. Must be encoded with when passed to + /// . + /// + /// + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user + /// was kicked and vice-versa. + /// + [Command("kick", "кик")] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.KickMembers)] + [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] + [Description("Kick member")] + public async Task KickUserAsync( + [Description("Member to kick")] IUser target, + [Description("Kick reason")] string reason) { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + // The current user's avatar is used when sending error messages + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var data = await _dataService.GetData(guildId.Value, CancellationToken); + var cfg = data.Configuration; + Messages.Culture = cfg.GetCulture(); + + var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); + if (!memberResult.IsSuccess) { + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) + .WithColour(ColorsList.Red).Build(); + + if (!embed.IsDefined(out var alreadyBuilt)) + return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + } + + var interactionResult + = await _utility.CheckInteractionsAsync(guildId.Value, userId.Value, target.ID, "Kick", CancellationToken); + if (!interactionResult.IsSuccess) + return Result.FromError(interactionResult); + + Result responseEmbed; + if (interactionResult.Entity is not null) { + responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + .WithColour(ColorsList.Red).Build(); + } else { + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var dmChannelResult = await _userApi.CreateDMAsync(target.ID, CancellationToken); + if (dmChannelResult.IsDefined(out var dmChannel)) { + var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + return Result.FromError(guildResult); + + var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) + .WithTitle(Messages.YouWereKicked) + .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Red) + .Build(); + + if (!dmEmbed.IsDefined(out var dmBuilt)) + return Result.FromError(dmEmbed); + await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken); + } + + var kickResult = await _guildApi.RemoveGuildMemberAsync( + guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + ct: CancellationToken); + if (!kickResult.IsSuccess) + return Result.FromError(kickResult.Error); + data.GetMemberData(target.ID).Roles.Clear(); + + responseEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserKicked, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) + || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + var logEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserKicked, target.GetTag()), target) + .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Red) + .Build(); + + if (!logEmbed.IsDefined(out var logBuilt)) + return Result.FromError(logEmbed); + + var builtArray = new[] { logBuilt }; + // Not awaiting to reduce response time + if (cfg.PublicFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel + && cfg.PrivateFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + } + } + + if (!responseEmbed.IsDefined(out var built)) + return Result.FromError(responseEmbed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } +} diff --git a/Commands/MuteCommand.cs b/Commands/MuteCommand.cs deleted file mode 100644 index 241c0e2..0000000 --- a/Commands/MuteCommand.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Boyfriend.Data; -using Discord; - -namespace Boyfriend.Commands; - -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, 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 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 - > DateTimeOffset.UtcNow)) { - cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error); - return; - } - - if (cmd.HasPermission(GuildPermission.ModerateMembers) && cmd.CanInteractWith(toMute, "Mute")) - await MuteMemberAsync(cmd, toMute, duration, guildData, reason); - } - - private static async Task MuteMemberAsync( - CommandProcessor cmd, IGuildUser toMute, - TimeSpan duration, GuildData data, string reason) { - var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); - var role = data.MuteRole; - var hasDuration = duration.TotalSeconds > 0; - var memberData = data.MemberData[toMute.Id]; - - if (role is not null) { - memberData.MutedUntil = DateTimeOffset.UtcNow.Add(duration); - if (data.Preferences["RemoveRolesOnMute"] is "true") { - memberData.Roles = toMute.RoleIds.ToList(); - memberData.Roles.Remove(cmd.Context.Guild.Id); - await toMute.RemoveRolesAsync(memberData.Roles, requestOptions); - } - - await toMute.AddRoleAsync(role, requestOptions); - } else { - if (!hasDuration || duration.TotalDays > 28) { - cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error); - return; - } - - if (toMute.IsBot) { - cmd.Reply(Messages.CannotTimeOutBot, ReplyEmojis.Error); - return; - } - - await toMute.SetTimeOutAsync(duration, requestOptions); - } - - cmd.ConfigWriteScheduled = true; - - var feedback = string.Format( - Messages.FeedbackMemberMuted, toMute.Mention, - Utils.GetHumanizedTimeSpan(duration), - Utils.Wrap(reason)); - cmd.Reply(feedback, ReplyEmojis.Muted); - cmd.Audit(feedback); - } -} diff --git a/Commands/MuteCommandGroup.cs b/Commands/MuteCommandGroup.cs new file mode 100644 index 0000000..cdf750d --- /dev/null +++ b/Commands/MuteCommandGroup.cs @@ -0,0 +1,258 @@ +using System.ComponentModel; +using System.Text; +using Boyfriend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Conditions; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +/// +/// Handles commands related to mute management: /mute and /unmute. +/// +public class MuteCommandGroup : CommandGroup { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; + + public MuteCommandGroup( + ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService, + FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + UtilityService utility) { + _context = context; + _channelApi = channelApi; + _dataService = dataService; + _feedbackService = feedbackService; + _guildApi = guildApi; + _userApi = userApi; + _utility = utility; + } + + /// + /// A slash command that mutes a Discord user with the specified reason. + /// + /// The user to mute. + /// The duration for this mute. The user will be automatically unmuted after this duration. + /// + /// The reason for this mute. Must be encoded with when passed to + /// . + /// + /// + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user + /// was muted and vice-versa. + /// + /// + [Command("mute", "мут")] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.ModerateMembers)] + [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] + [Description("Mute member")] + public async Task MuteUserAsync( + [Description("Member to mute")] IUser target, + [Description("Mute reason")] string reason, + [Description("Mute duration")] + TimeSpan duration) { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + // The current user's avatar is used when sending error messages + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); + if (!memberResult.IsSuccess) { + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) + .WithColour(ColorsList.Red).Build(); + + if (!embed.IsDefined(out var alreadyBuilt)) + return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + } + + var interactionResult + = await _utility.CheckInteractionsAsync( + guildId.Value, userId.Value, target.ID, "Mute", CancellationToken); + if (!interactionResult.IsSuccess) + return Result.FromError(interactionResult); + + var data = await _dataService.GetData(guildId.Value, CancellationToken); + var cfg = data.Configuration; + Messages.Culture = data.Culture; + + Result responseEmbed; + if (interactionResult.Entity is not null) { + responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + .WithColour(ColorsList.Red).Build(); + } else { + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var until = DateTimeOffset.UtcNow.Add(duration); // >:) + var muteResult = await _guildApi.ModifyGuildMemberAsync( + guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), + communicationDisabledUntil: until, ct: CancellationToken); + if (!muteResult.IsSuccess) + return Result.FromError(muteResult.Error); + + responseEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserMuted, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) + || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)) + .Append( + string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))); + + var logEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserMuted, target.GetTag()), target) + .WithDescription(builder.ToString()) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Red) + .Build(); + + if (!logEmbed.IsDefined(out var logBuilt)) + return Result.FromError(logEmbed); + + var builtArray = new[] { logBuilt }; + // Not awaiting to reduce response time + if (cfg.PublicFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel + && cfg.PrivateFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + } + } + + if (!responseEmbed.IsDefined(out var built)) + return Result.FromError(responseEmbed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } + + /// + /// A slash command that unmutes a Discord user with the specified reason. + /// + /// The user to unmute. + /// + /// The reason for this unmute. Must be encoded with when passed to + /// . + /// + /// + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user + /// was unmuted and vice-versa. + /// + /// + /// + [Command("unmute", "размут")] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.ModerateMembers)] + [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] + [Description("Unmute member")] + public async Task UnmuteUserAsync( + [Description("Member to unmute")] + IUser target, + [Description("Unmute reason")] + string reason) { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + // The current user's avatar is used when sending error messages + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); + Messages.Culture = cfg.GetCulture(); + + var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); + if (!memberResult.IsSuccess) { + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) + .WithColour(ColorsList.Red).Build(); + + if (!embed.IsDefined(out var alreadyBuilt)) + return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + } + + var interactionResult + = await _utility.CheckInteractionsAsync( + guildId.Value, userId.Value, target.ID, "Unmute", CancellationToken); + if (!interactionResult.IsSuccess) + return Result.FromError(interactionResult); + + // Needed to get the tag and avatar + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var unmuteResult = await _guildApi.ModifyGuildMemberAsync( + guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + communicationDisabledUntil: null, ct: CancellationToken); + if (!unmuteResult.IsSuccess) + return Result.FromError(unmuteResult.Error); + + var responseEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserUnmuted, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) + || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + var logEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserUnmuted, target.GetTag()), target) + .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Green) + .Build(); + + if (!logEmbed.IsDefined(out var logBuilt)) + return Result.FromError(logEmbed); + + var builtArray = new[] { logBuilt }; + + // Not awaiting to reduce response time + if (cfg.PublicFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel + && cfg.PrivateFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + } + + if (!responseEmbed.IsDefined(out var built)) + return Result.FromError(responseEmbed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } +} diff --git a/Commands/PingCommand.cs b/Commands/PingCommand.cs deleted file mode 100644 index 1678046..0000000 --- a/Commands/PingCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Boyfriend.Commands; - -public sealed class PingCommand : ICommand { - public string[] Aliases { get; } = { "ping", "latency", "pong", "пинг", "задержка", "понг" }; - - public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - var builder = Boyfriend.StringBuilder; - - builder.Append(Utils.GetBeep()) - .Append( - Math.Round(Math.Abs(DateTimeOffset.UtcNow.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds))) - .Append(Messages.Milliseconds); - - cmd.Reply(builder.ToString(), ReplyEmojis.Ping); - builder.Clear(); - - return Task.CompletedTask; - } -} diff --git a/Commands/PingCommandGroup.cs b/Commands/PingCommandGroup.cs new file mode 100644 index 0000000..45d27e2 --- /dev/null +++ b/Commands/PingCommandGroup.cs @@ -0,0 +1,79 @@ +using System.ComponentModel; +using Boyfriend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +/// +/// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping +/// +public class PingCommandGroup : CommandGroup { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly DiscordGatewayClient _client; + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestUserAPI _userApi; + + public PingCommandGroup( + IDiscordRestChannelAPI channelApi, ICommandContext context, DiscordGatewayClient client, + GuildDataService dataService, FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + _channelApi = channelApi; + _context = context; + _client = client; + _dataService = dataService; + _feedbackService = feedbackService; + _userApi = userApi; + } + + /// + /// A slash command that shows time taken for the gateway to respond to the last heartbeat. + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("ping", "пинг")] + [Description("Get bot latency")] + public async Task SendPingAsync() { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); + Messages.Culture = cfg.GetCulture(); + + var latency = _client.Latency.TotalMilliseconds; + if (latency is 0) { + // No heartbeat has occurred, estimate latency from local time and "Boyfriend is thinking..." message + var lastMessageResult = await _channelApi.GetChannelMessagesAsync( + channelId.Value, limit: 1, ct: CancellationToken); + if (!lastMessageResult.IsDefined(out var lastMessage)) + return Result.FromError(lastMessageResult); + latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds; + } + + var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) + .WithTitle($"Beep{Random.Shared.Next(1, 4)}".Localized()) + .WithDescription($"{latency:F0}{Messages.Milliseconds}") + .WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red) + .WithCurrentTimestamp() + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } +} diff --git a/Commands/RemindCommand.cs b/Commands/RemindCommand.cs deleted file mode 100644 index 1d58763..0000000 --- a/Commands/RemindCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -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); - if (remindIn.TotalSeconds < 1) { - cmd.Reply(Messages.InvalidRemindIn, ReplyEmojis.InvalidArgument); - return Task.CompletedTask; - } - - var reminderText = cmd.GetRemaining(cleanArgs, 1, "ReminderText"); - if (reminderText is not null) { - var reminderOffset = DateTimeOffset.UtcNow.Add(remindIn); - GuildData.Get(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add( - new Reminder { - RemindAt = reminderOffset, - ReminderText = reminderText, - ReminderChannel = cmd.Context.Channel.Id - }); - - cmd.ConfigWriteScheduled = true; - - var feedback = string.Format(Messages.FeedbackReminderAdded, reminderOffset.ToUnixTimeSeconds().ToString()); - cmd.Reply(feedback, ReplyEmojis.Reminder); - } - - return Task.CompletedTask; - } -} diff --git a/Commands/RemindCommandGroup.cs b/Commands/RemindCommandGroup.cs new file mode 100644 index 0000000..5ee7a8c --- /dev/null +++ b/Commands/RemindCommandGroup.cs @@ -0,0 +1,66 @@ +using System.ComponentModel; +using Boyfriend.Data; +using Boyfriend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +/// +/// Handles the command to manage reminders: /remind +/// +public class RemindCommandGroup : CommandGroup { + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestUserAPI _userApi; + + public RemindCommandGroup( + ICommandContext context, GuildDataService dataService, FeedbackService feedbackService, + IDiscordRestUserAPI userApi) { + _context = context; + _dataService = dataService; + _feedbackService = feedbackService; + _userApi = userApi; + } + + [Command("remind")] + [Description("Create a reminder")] + public async Task AddReminderAsync(TimeSpan duration, string text) { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var remindAt = DateTimeOffset.UtcNow.Add(duration); + + (await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add( + new Reminder { + RemindAt = remindAt, + Channel = channelId.Value, + Text = text + }); + + var embed = new EmbedBuilder().WithSmallTitle(string.Format(Messages.ReminderCreated, user.GetTag()), user) + .WithDescription(string.Format(Messages.DescriptionReminderCreated, Markdown.Timestamp(remindAt))) + .WithColour(ColorsList.Green) + .Build(); + + if (!embed.IsDefined(out var built)) + return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } +} diff --git a/Commands/SettingsCommand.cs b/Commands/SettingsCommand.cs deleted file mode 100644 index b987394..0000000 --- a/Commands/SettingsCommand.cs +++ /dev/null @@ -1,164 +0,0 @@ -using Boyfriend.Data; -using Discord; - -namespace Boyfriend.Commands; - -public sealed class SettingsCommand : ICommand { - public string[] Aliases { get; } = { "settings", "config", "настройки", "конфиг" }; - - public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - if (!cmd.HasPermission(GuildPermission.ManageGuild)) return Task.CompletedTask; - - var guild = cmd.Context.Guild; - 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 GuildData.DefaultPreferences) { - var format = "{0}"; - 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}>"; - else currentValue = Messages.ChannelNotSpecified; - } else if (setting.Key.EndsWith("Role")) { - if (guild.GetRole(ulong.Parse(currentValue)) is not null) format = "<@&{0}>"; - else currentValue = Messages.RoleNotSpecified; - } else { - if (!IsBool(currentValue)) format = Utils.Wrap("{0}")!; - else currentValue = YesOrNo(currentValue is "true"); - } - - currentSettings.Append($"{Utils.GetMessage($"Settings{setting.Key}")} (`{setting.Key}`): ") - .AppendFormat(format, currentValue).AppendLine(); - } - - cmd.Reply(currentSettings.ToString(), ReplyEmojis.SettingsList); - currentSettings.Clear(); - return Task.CompletedTask; - } - - var selectedSetting = args[0].ToLower(); - - var exists = false; - foreach (var setting in GuildData.DefaultPreferences.Keys.Where(x => x.ToLower() == selectedSetting)) { - selectedSetting = setting; - exists = true; - break; - } - - if (!exists) { - cmd.Reply(Messages.SettingDoesntExist, ReplyEmojis.Error); - return Task.CompletedTask; - } - - string? value; - - if (args.Length >= 2) { - value = cmd.GetRemaining(args, 1, "Setting"); - if (value is null) return Task.CompletedTask; - if (selectedSetting is "EventStartedReceivers") { - value = value.Replace(" ", "").ToLower(); - if (value.StartsWith(",") - || value.Count(x => x is ',') > 1 - || (!value.Contains("interested") && !value.Contains("users") && !value.Contains("role"))) { - cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error); - return Task.CompletedTask; - } - } - } else { value = "reset"; } - - if (IsBool(GuildData.DefaultPreferences[selectedSetting]) && !IsBool(value)) { - value = value switch { - "y" or "yes" or "д" or "да" => "true", "n" or "no" or "н" or "нет" => "false", _ => value - }; - if (!IsBool(value)) { - cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error); - return Task.CompletedTask; - } - } - - var localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); - - var mention = Utils.ParseMention(value); - if (mention is not 0 && selectedSetting is not "WelcomeMessage") value = mention.ToString(); - - var formatting = Utils.Wrap("{0}")!; - if (selectedSetting is not "WelcomeMessage") { - if (selectedSetting.EndsWith("Channel")) formatting = "<#{0}>"; - if (selectedSetting.EndsWith("Role")) formatting = "<@&{0}>"; - } - - var formattedValue = selectedSetting switch { - "WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage), - "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] = GuildData.DefaultPreferences[selectedSetting]; - } else { - if (value == config[selectedSetting]) { - cmd.Reply( - string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), - ReplyEmojis.Error); - return Task.CompletedTask; - } - - if (selectedSetting is "Lang" && !Utils.CultureInfoCache.ContainsKey(value)) { - var langNotSupported = Boyfriend.StringBuilder.Append($"{Messages.LanguageNotSupported} "); - foreach (var lang in Utils.CultureInfoCache) langNotSupported.Append($"`{lang.Key}`, "); - langNotSupported.Remove(langNotSupported.Length - 2, 2); - cmd.Reply(langNotSupported.ToString(), ReplyEmojis.Error); - langNotSupported.Clear(); - return Task.CompletedTask; - } - - if (selectedSetting.EndsWith("Channel") && guild.GetTextChannel(mention) is null) { - cmd.Reply(Messages.InvalidChannel, ReplyEmojis.Error); - return Task.CompletedTask; - } - - if (selectedSetting.EndsWith("Role") && guild.GetRole(mention) is null) { - cmd.Reply(Messages.InvalidRole, ReplyEmojis.Error); - return Task.CompletedTask; - } - - if (selectedSetting.EndsWith("Offset") && !int.TryParse(value, out _)) { - cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error); - return Task.CompletedTask; - } - - if (selectedSetting is "MuteRole") - data.MuteRole = guild.GetRole(mention); - - config[selectedSetting] = value; - } - - if (selectedSetting is "Lang") { - Utils.SetCurrentLanguage(guild); - localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); - } - - cmd.ConfigWriteScheduled = true; - - var replyFormat = string.Format(Messages.FeedbackSettingsUpdated, localizedSelectedSetting, formattedValue); - cmd.Reply(replyFormat, ReplyEmojis.SettingsSet); - cmd.Audit(replyFormat, false); - return Task.CompletedTask; - } - - private static string YesOrNo(bool isYes) { - return isYes ? Messages.Yes : Messages.No; - } - - private static bool IsBool(string value) { - return value is "true" or "false"; - } -} diff --git a/Commands/SettingsCommandGroup.cs b/Commands/SettingsCommandGroup.cs new file mode 100644 index 0000000..7e4763f --- /dev/null +++ b/Commands/SettingsCommandGroup.cs @@ -0,0 +1,158 @@ +using System.ComponentModel; +using System.Reflection; +using System.Text; +using Boyfriend.Data; +using Boyfriend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Messages; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend.Commands; + +/// +/// Handles the commands to list and modify per-guild settings: /settings and /settings list. +/// +public class SettingsCommandGroup : CommandGroup { + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestUserAPI _userApi; + + public SettingsCommandGroup( + ICommandContext context, GuildDataService dataService, + FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + _context = context; + _dataService = dataService; + _feedbackService = feedbackService; + _userApi = userApi; + } + + /// + /// A slash command that lists current per-guild settings. + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("settings list")] + [Description("Shows settings list for this server")] + [SuppressInteractionResponse(suppress: true)] + public async Task SendSettingsListAsync() { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); + Messages.Culture = cfg.GetCulture(); + + var builder = new StringBuilder(); + + foreach (var setting in typeof(GuildConfiguration).GetProperties()) { + builder.Append(Markdown.InlineCode(setting.Name)) + .Append(": "); + var something = setting.GetValue(cfg); + if (something!.GetType() == typeof(List)) { + var list = (something as List); + builder.AppendLine(string.Join(", ", list!.Select(v => Markdown.InlineCode(v.ToString())))); + } else { builder.AppendLine(Markdown.InlineCode(something.ToString()!)); } + } + + var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) + .WithDescription(builder.ToString()) + .WithColour(ColorsList.Default) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync( + built, ct: CancellationToken, options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + } + + /// + /// A slash command that modifies per-guild settings. + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("settings")] + [Description("Change settings for this server")] + public async Task EditSettingsAsync( + [Description("настройка")] string setting, + [Description("значение")] string value) { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + return Result.FromError( + new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); + Messages.Culture = cfg.GetCulture(); + + PropertyInfo? property = null; + + try { + foreach (var prop in typeof(GuildConfiguration).GetProperties()) + if (string.Equals(setting, prop.Name, StringComparison.CurrentCultureIgnoreCase)) + property = prop; + if (property == null || !property.CanWrite) + throw new ApplicationException(Messages.SettingDoesntExist); + var type = property.PropertyType; + + if (value is "reset" or "default") { property.SetValue(cfg, null); } else if (type == typeof(string)) { + if (setting == "language" && value is not ("ru" or "en" or "mctaylors-ru")) + throw new ApplicationException(Messages.LanguageNotSupported); + property.SetValue(cfg, value); + } else { + try { + if (type == typeof(bool)) + property.SetValue(cfg, Convert.ToBoolean(value)); + + if (type == typeof(ulong)) { + var id = Convert.ToUInt64(value); + + property.SetValue(cfg, id); + } + } catch (Exception e) when (e is FormatException or OverflowException) { + throw new ApplicationException(Messages.InvalidSettingValue); + } + } + } catch (Exception e) { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser) + .WithDescription(e.Message) + .WithColour(ColorsList.Red) + .Build(); + if (!failedEmbed.IsDefined(out var failedBuilt)) return Result.FromError(failedEmbed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(failedBuilt, ct: CancellationToken); + } + + var builder = new StringBuilder(); + + builder.Append(Markdown.InlineCode(setting)) + .Append($" {Messages.SettingIsNow} ") + .Append(Markdown.InlineCode(value)); + + var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfulyChanged, currentUser) + .WithDescription(builder.ToString()) + .WithColour(ColorsList.Green) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + } +} diff --git a/Commands/UnbanCommand.cs b/Commands/UnbanCommand.cs deleted file mode 100644 index fb8040e..0000000 --- a/Commands/UnbanCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Discord; - -namespace Boyfriend.Commands; - -public sealed class UnbanCommand : ICommand { - public string[] Aliases { get; } = { "unban", "pardon", "разбан" }; - - public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - if (!cmd.HasPermission(GuildPermission.BanMembers)) return; - - var id = cmd.GetBan(args, 0); - if (id is null) return; - var reason = cmd.GetRemaining(args, 1, "UnbanReason"); - if (reason is not null) await UnbanUserAsync(cmd, id.Value, 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); - - var feedback = string.Format(Messages.FeedbackUserUnbanned, $"<@{id.ToString()}>", Utils.Wrap(reason)); - cmd.Reply(feedback); - cmd.Audit(feedback); - } -} diff --git a/Commands/UnmuteCommand.cs b/Commands/UnmuteCommand.cs deleted file mode 100644 index 6310c1d..0000000 --- a/Commands/UnmuteCommand.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Boyfriend.Data; -using Discord; -using Discord.WebSocket; - -namespace Boyfriend.Commands; - -public sealed class UnmuteCommand : ICommand { - public string[] Aliases { get; } = { "unmute", "размут" }; - - public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { - if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return; - - 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); - } - - private static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute, - string reason) { - var isMuted = await Utils.UnmuteMemberAsync(GuildData.Get(cmd.Context.Guild), cmd.Context.User.ToString(), - toUnmute, reason); - - 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/Data/GuildConfiguration.cs b/Data/GuildConfiguration.cs new file mode 100644 index 0000000..440e2b7 --- /dev/null +++ b/Data/GuildConfiguration.cs @@ -0,0 +1,90 @@ +using System.Globalization; +using Remora.Discord.API.Abstractions.Objects; + +namespace Boyfriend.Data; + +/// +/// Stores per-guild settings that can be set by a member +/// with using the /settings command +/// +public class GuildConfiguration { + /// + /// Represents a scheduled event notification receiver. + /// + /// + /// Used to selectively mention guild members when a scheduled event has started or is about to start. + /// + public enum NotificationReceiver { + Interested, + Role + } + + public static readonly Dictionary CultureInfoCache = new() { + { "en", new CultureInfo("en-US") }, + { "ru", new CultureInfo("ru-RU") }, + { "mctaylors-ru", new CultureInfo("tt-RU") } + }; + + public string Language { get; set; } = "en"; + + /// + /// Controls what message should be sent in when a new member joins the server. + /// + /// + /// + /// No message will be sent if set to "off", "disable" or "disabled". + /// will be sent if set to "default" or "reset" + /// + /// + /// + public string WelcomeMessage { get; set; } = "default"; + + /// + /// Controls whether or not the message should be sent + /// in on startup. + /// + /// + public bool ReceiveStartupMessages { get; set; } + + public bool RemoveRolesOnMute { get; set; } + + /// + /// Controls whether or not a guild member's roles are returned if he/she leaves and then joins back. + /// + /// Roles will not be returned if the member left the guild because of /ban or /kick. + public bool ReturnRolesOnRejoin { get; set; } + + public bool AutoStartEvents { get; set; } + + /// + /// Controls what channel should all public messages be sent to. + /// + public ulong PublicFeedbackChannel { get; set; } + + /// + /// Controls what channel should all private, moderator-only messages be sent to. + /// + public ulong PrivateFeedbackChannel { get; set; } + + public ulong EventNotificationChannel { get; set; } + public ulong DefaultRole { get; set; } + public ulong MuteRole { get; set; } + public ulong EventNotificationRole { get; set; } + + /// + /// Controls what guild members should be mentioned when a scheduled event has started or is about to start. + /// + /// + public List EventStartedReceivers { get; set; } + = new() { NotificationReceiver.Interested, NotificationReceiver.Role }; + + /// + /// Controls the amount of time before a scheduled event to send a reminder in . + /// + public TimeSpan EventEarlyNotificationOffset { get; set; } = TimeSpan.Zero; + + // Do not convert this to a property, else serialization will be attempted + public CultureInfo GetCulture() { + return CultureInfoCache[Language]; + } +} diff --git a/Data/GuildData.cs b/Data/GuildData.cs index 8403e2c..992adc0 100644 --- a/Data/GuildData.cs +++ b/Data/GuildData.cs @@ -1,142 +1,41 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using Discord; -using Discord.WebSocket; +using System.Globalization; +using Remora.Rest.Core; 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(); +/// +/// Stores information about a guild. This information is not accessible via the Discord API. +/// +/// This information is stored on disk as a JSON file. +public class GuildData { + public readonly GuildConfiguration Configuration; + public readonly string ConfigurationPath; public readonly Dictionary MemberData; + public readonly string MemberDataPath; - public readonly Dictionary Preferences; + public readonly Dictionary ScheduledEvents; + public readonly string ScheduledEventsPath; - private SocketRole? _cachedMuteRole; - - [SuppressMessage("Performance", "CA1853:Unnecessary call to \'Dictionary.ContainsKey(key)\'")] - // https://github.com/dotnet/roslyn-analyzers/issues/6377 - private GuildData(SocketGuild guild) { - var downloaderTask = guild.DownloadUsersAsync(); - _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); - } - - downloaderTask.Wait(); - foreach (var member in guild.Users) { - if (MemberData.TryGetValue(member.Id, out var memberData)) { - if (!memberData.IsInGuild - && DateTimeOffset.UtcNow.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); - } - - if (memberData.MutedUntil is null) { - memberData.Roles = ((IGuildUser)member).RoleIds.ToList(); - memberData.Roles.Remove(guild.Id); - } - - continue; - } - - MemberData.Add(member.Id, new MemberData(member)); - } - - MemberData.TrimExcess(); + public GuildData( + GuildConfiguration configuration, string configurationPath, + Dictionary scheduledEvents, string scheduledEventsPath, + Dictionary memberData, string memberDataPath) { + Configuration = configuration; + ConfigurationPath = configurationPath; + ScheduledEvents = scheduledEvents; + ScheduledEventsPath = scheduledEventsPath; + MemberData = memberData; + MemberDataPath = memberDataPath; } - 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 CultureInfo Culture => Configuration.GetCulture(); - public SocketTextChannel? PublicFeedbackChannel - => Boyfriend.Client.GetGuild(_id) - .GetTextChannel( - ulong.Parse(Preferences["PublicFeedbackChannel"])); + public MemberData GetMemberData(Snowflake userId) { + if (MemberData.TryGetValue(userId.Value, out var existing)) return existing; - public SocketTextChannel? PrivateFeedbackChannel => Boyfriend.Client.GetGuild(_id) - .GetTextChannel( - ulong.Parse( - Preferences["PrivateFeedbackChannel"])); - - 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); + var newData = new MemberData(userId.Value, null); + MemberData.Add(userId.Value, 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/Data/MemberData.cs b/Data/MemberData.cs index 137375a..72cbdec 100644 --- a/Data/MemberData.cs +++ b/Data/MemberData.cs @@ -1,38 +1,18 @@ -using System.Text.Json.Serialization; -using Discord; +using Remora.Rest.Core; 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; +/// +/// Stores information about a member +/// +public class MemberData { + public MemberData(ulong id, DateTimeOffset? bannedUntil) { Id = id; - IsInGuild = isInGuild; - JoinedAt = joinedAt; - LeftAt = leftAt; - MutedUntil = mutedUntil; - Reminders = reminders; - Roles = roles; + BannedUntil = bannedUntil; } - 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(); - } + public ulong Id { get; } + public DateTimeOffset? BannedUntil { get; set; } + public List Roles { get; set; } = new(); + public List Reminders { get; } = new(); } diff --git a/Data/Reminder.cs b/Data/Reminder.cs index c64ebbd..1d0410c 100644 --- a/Data/Reminder.cs +++ b/Data/Reminder.cs @@ -1,7 +1,9 @@ -namespace Boyfriend.Data; +using Remora.Rest.Core; + +namespace Boyfriend.Data; public struct Reminder { public DateTimeOffset RemindAt; - public string ReminderText; - public ulong ReminderChannel; + public string Text; + public Snowflake Channel; } diff --git a/Data/ScheduledEventData.cs b/Data/ScheduledEventData.cs new file mode 100644 index 0000000..661eed0 --- /dev/null +++ b/Data/ScheduledEventData.cs @@ -0,0 +1,17 @@ +using Remora.Discord.API.Abstractions.Objects; + +namespace Boyfriend.Data; + +/// +/// Stores information about scheduled events. This information is not provided by the Discord API. +/// +/// This information is stored on disk as a JSON file. +public class ScheduledEventData { + public ScheduledEventData(GuildScheduledEventStatus status) { + Status = status; + } + + public bool EarlyNotificationSent { get; set; } + public DateTimeOffset? ActualStartTime { get; set; } + public GuildScheduledEventStatus Status { get; set; } +} diff --git a/EventHandler.cs b/EventHandler.cs deleted file mode 100644 index 017c604..0000000 --- a/EventHandler.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System.Diagnostics; -using Boyfriend.Data; -using Discord; -using Discord.Rest; -using Discord.WebSocket; - -namespace Boyfriend; - -public static class EventHandler { - private static readonly DiscordSocketClient Client = Boyfriend.Client; - private static bool _sendReadyMessages = true; - - public static void InitEvents() { - Client.Ready += ReadyEvent; - Client.MessageDeleted += MessageDeletedEvent; - Client.MessageReceived += MessageReceivedEvent; - Client.MessageUpdated += MessageUpdatedEvent; - Client.UserJoined += UserJoinedEvent; - Client.UserLeft += UserLeftEvent; - Client.GuildMemberUpdated += MemberRolesUpdatedEvent; - Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; - Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; - Client.GuildScheduledEventStarted += ScheduledEventStartedEvent; - Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; - } - - private static Task MemberRolesUpdatedEvent(Cacheable oldUser, SocketGuildUser newUser) { - var data = GuildData.Get(newUser.Guild).MemberData[newUser.Id]; - if (data.MutedUntil is null) { - 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) { - Boyfriend.Log(new LogMessage(LogSeverity.Info, nameof(EventHandler), $"Guild \"{guild.Name}\" is READY")); - var data = GuildData.Get(guild); - var config = data.Preferences; - var channel = data.PrivateFeedbackChannel; - if (config["ReceiveStartupMessages"] is not "true" || channel is null) continue; - - Utils.SetCurrentLanguage(guild); - _ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); - } - - _sendReadyMessages = false; - return Task.CompletedTask; - } - - private static async Task MessageDeletedEvent( - Cacheable message, - Cacheable channel) { - var msg = message.Value; - if (channel.Value is not SocketGuildChannel gChannel - || msg is null or ISystemMessage - || msg.Author.IsBot) return; - - var guild = gChannel.Guild; - - Utils.SetCurrentLanguage(guild); - - var mention = msg.Author.Mention; - - await Task.Delay(500); - - var auditLogEnumerator - = (await guild.GetAuditLogsAsync(1, actionType: ActionType.MessageDeleted).FlattenAsync()).GetEnumerator(); - if (auditLogEnumerator.MoveNext()) { - var auditLogEntry = auditLogEnumerator.Current!; - if (auditLogEntry.CreatedAt >= DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(1)) - && auditLogEntry.Data is MessageDeleteAuditLogData data - && msg.Author.Id == data.Target.Id) - mention = auditLogEntry.User.Mention; - } - - auditLogEnumerator.Dispose(); - - await Utils.SendFeedbackAsync( - string.Format( - Messages.CachedMessageDeleted, msg.Author.Mention, - Utils.MentionChannel(channel.Id), - Utils.Wrap(msg.CleanContent)), guild, mention); - } - - private static Task MessageReceivedEvent(IDeletable messageParam) { - if (messageParam is not SocketUserMessage message || message.Author.IsWebhook) return Task.CompletedTask; - - _ = message.CleanContent.ToLower() switch { - "whoami" => message.ReplyAsync("`nobody`"), - "сука !!" => message.ReplyAsync("`root`"), - "воооо" => message.ReplyAsync("`removing /...`"), - "пон" => message.ReplyAsync( - "https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg"), - "++++" => message.ReplyAsync("#"), - _ => new CommandProcessor(message).HandleCommandAsync() - }; - return Task.CompletedTask; - } - - private static async Task MessageUpdatedEvent( - Cacheable messageCached, IMessage messageSocket, - ISocketMessageChannel channel) { - var msg = messageCached.Value; - if (channel is not SocketGuildChannel gChannel - || msg is null or ISystemMessage - || msg.CleanContent == messageSocket.CleanContent - || msg.Author.IsBot) return; - - var guild = gChannel.Guild; - 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, msg.Author.Mention); - } - - private static async Task UserJoinedEvent(SocketGuildUser user) { - var guild = user.Guild; - var data = GuildData.Get(guild); - var config = data.Preferences; - Utils.SetCurrentLanguage(guild); - - 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 (!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 (DateTimeOffset.UtcNow < memberData.MutedUntil) { - 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.UtcNow); - return Task.CompletedTask; - } - - private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) { - var guild = scheduledEvent.Guild; - var eventConfig = GuildData.Get(guild).Preferences; - var channel = Utils.GetEventNotificationChannel(guild); - Utils.SetCurrentLanguage(guild); - if (channel is null) return; - - var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); - var mentions = role is not null - ? $"{role.Mention} {scheduledEvent.Creator.Mention}" - : $"{scheduledEvent.Creator.Mention}"; - - var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id); - var descAndLink - = $"\n{Utils.Wrap(scheduledEvent.Description)}\nhttps://discord.com/events/{guild.Id}/{scheduledEvent.Id}"; - - await Utils.SilentSendAsync( - channel, - string.Format( - Messages.EventCreated, mentions, - Utils.Wrap(scheduledEvent.Name), location, - scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink), - true); - } - - private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { - var guild = scheduledEvent.Guild; - var eventConfig = GuildData.Get(guild).Preferences; - var channel = Utils.GetEventNotificationChannel(guild); - 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}" : "")); - } - - private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) { - var guild = scheduledEvent.Guild; - var eventConfig = GuildData.Get(guild).Preferences; - var channel = Utils.GetEventNotificationChannel(guild); - Utils.SetCurrentLanguage(guild); - - if (channel is null) return; - - 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)) - .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), - Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id))); - mentions.Clear(); - } - - private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { - var guild = scheduledEvent.Guild; - var channel = Utils.GetEventNotificationChannel(guild); - Utils.SetCurrentLanguage(guild); - if (channel is not null) - await channel.SendMessageAsync( - string.Format( - Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), - Utils.GetHumanizedTimeSpan(DateTimeOffset.UtcNow.Subtract(scheduledEvent.StartTime)))); - } -} diff --git a/EventResponders.cs b/EventResponders.cs new file mode 100644 index 0000000..708bbc1 --- /dev/null +++ b/EventResponders.cs @@ -0,0 +1,335 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using DiffPlex.DiffBuilder; +using Microsoft.Extensions.Logging; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Caching; +using Remora.Discord.Caching.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Discord.Gateway.Responders; +using Remora.Rest.Core; +using Remora.Results; + +// ReSharper disable UnusedType.Global + +namespace Boyfriend; + +/// +/// Handles sending a message to a guild that has just initialized if that guild +/// has enabled +/// +public class GuildCreateResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly ILogger _logger; + private readonly IDiscordRestUserAPI _userApi; + + public GuildCreateResponder( + IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger logger, + IDiscordRestUserAPI userApi) { + _channelApi = channelApi; + _dataService = dataService; + _logger = logger; + _userApi = userApi; + } + + public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild isn't IAvailableGuild + + var guild = gatewayEvent.Guild.AsT0; + _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); + + var guildConfig = await _dataService.GetConfiguration(guild.ID, ct); + if (!guildConfig.ReceiveStartupMessages) + return Result.FromSuccess(); + if (guildConfig.PrivateFeedbackChannel is 0) + return Result.FromSuccess(); + + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + Messages.Culture = guildConfig.GetCulture(); + var i = Random.Shared.Next(1, 4); + + var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) + .WithTitle($"Beep{i}".Localized()) + .WithDescription(Messages.Ready) + .WithCurrentTimestamp() + .WithColour(ColorsList.Blue) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + guildConfig.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, ct: ct); + } +} + +/// +/// Handles logging the contents of a deleted message and the user who deleted the message +/// to a guild's if one is set. +/// +public class MessageDeletedResponder : IResponder { + private readonly IDiscordRestAuditLogAPI _auditLogApi; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestUserAPI _userApi; + + public MessageDeletedResponder( + IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi, + GuildDataService dataService, IDiscordRestUserAPI userApi) { + _auditLogApi = auditLogApi; + _channelApi = channelApi; + _dataService = dataService; + _userApi = userApi; + } + + public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); + + var guildConfiguration = await _dataService.GetConfiguration(guildId, ct); + if (guildConfiguration.PrivateFeedbackChannel is 0) return Result.FromSuccess(); + + var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); + if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); + if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess(); + + var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( + guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); + if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); + + var auditLog = auditLogPage.AuditLogEntries.Single(); + if (!auditLog.Options.IsDefined(out var options)) + return Result.FromError(new ArgumentNullError(nameof(auditLog.Options))); + + var user = message.Author; + if (options.ChannelID == gatewayEvent.ChannelID + && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { + var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); + if (!userResult.IsDefined(out user)) return Result.FromError(userResult); + } + + Messages.Culture = guildConfiguration.GetCulture(); + + var embed = new EmbedBuilder() + .WithSmallTitle( + string.Format( + Messages.CachedMessageDeleted, + message.Author.GetTag()), message.Author) + .WithDescription( + $"{Mention.Channel(gatewayEvent.ChannelID)}\n{message.Content.InBlockCode()}") + .WithActionFooter(user) + .WithTimestamp(message.Timestamp) + .WithColour(ColorsList.Red) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} + +/// +/// Handles logging the difference between an edited message's old and new content +/// to a guild's if one is set. +/// +public class MessageEditedResponder : IResponder { + private readonly CacheService _cacheService; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestUserAPI _userApi; + + public MessageEditedResponder( + CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService, + IDiscordRestUserAPI userApi) { + _cacheService = cacheService; + _channelApi = channelApi; + _dataService = dataService; + _userApi = userApi; + } + + public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) + return Result.FromSuccess(); + var guildConfiguration = await _dataService.GetConfiguration(guildId, ct); + if (guildConfiguration.PrivateFeedbackChannel is 0) + return Result.FromSuccess(); + if (!gatewayEvent.Content.IsDefined(out var newContent)) + return Result.FromSuccess(); + if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) + return Result.FromSuccess(); // The message wasn't actually edited + + if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); + if (!gatewayEvent.ID.IsDefined(out var messageId)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID))); + + var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); + var messageResult = await _cacheService.TryGetValueAsync( + cacheKey, ct); + if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); + if (message.Content == newContent) return Result.FromSuccess(); + + // Custom event responders are called earlier than responders responsible for message caching + // This means that subsequent edit logs may contain the wrong content + // We can work around this by evicting the message from the cache + await _cacheService.EvictAsync(cacheKey, ct); + // However, since we evicted the message, subsequent edits won't have a cached instance to work with + // Getting the message will put it back in the cache, resolving all issues + // We don't need to await this since the result is not needed + // NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages + // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously + _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + var diff = InlineDiffBuilder.Diff(message.Content, newContent); + + Messages.Culture = guildConfiguration.GetCulture(); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) + .WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}") + .WithUserFooter(currentUser) + .WithTimestamp(timestamp.Value) + .WithColour(ColorsList.Yellow) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} + +/// +/// Handles sending a guild's if one is set. +/// If is enabled, roles will be returned. +/// +/// +public class GuildMemberAddResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestGuildAPI _guildApi; + + public GuildMemberAddResponder( + IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) { + _channelApi = channelApi; + _dataService = dataService; + _guildApi = guildApi; + } + + public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.User.IsDefined(out var user)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User))); + var data = await _dataService.GetData(gatewayEvent.GuildID, ct); + var cfg = data.Configuration; + if (cfg.PublicFeedbackChannel is 0 || cfg.WelcomeMessage is "off" or "disable" or "disabled") + return Result.FromSuccess(); + if (cfg.ReturnRolesOnRejoin) { + var result = await _guildApi.ModifyGuildMemberAsync( + gatewayEvent.GuildID, user.ID, roles: data.GetMemberData(user.ID).Roles, ct: ct); + if (!result.IsSuccess) return Result.FromError(result.Error); + } + + Messages.Culture = data.Culture; + var welcomeMessage = cfg.WelcomeMessage is "default" or "reset" + ? Messages.DefaultWelcomeMessage + : cfg.WelcomeMessage; + + var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); + if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(welcomeMessage, user.GetTag(), guild.Name), user) + .WithGuildFooter(guild) + .WithTimestamp(gatewayEvent.JoinedAt) + .WithColour(ColorsList.Green) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} + +/// +/// Handles sending a notification when a scheduled event has been cancelled +/// in a guild's if one is set. +/// +public class GuildScheduledEventDeleteResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + + public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) { + _channelApi = channelApi; + _dataService = dataService; + } + + public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) { + var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); + guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); + + if (guildData.Configuration.EventNotificationChannel is 0) + return Result.FromSuccess(); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name)) + .WithDescription(":(") + .WithColour(ColorsList.Red) + .WithCurrentTimestamp() + .Build(); + + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + guildData.Configuration.EventNotificationChannel.ToDiscordSnowflake(), embeds: new[] { built }, ct: ct); + } +} + +/// +/// Handles updating when a guild member is updated. +/// +public class GuildMemberUpdateResponder : IResponder { + private readonly GuildDataService _dataService; + + public GuildMemberUpdateResponder(GuildDataService dataService) { + _dataService = dataService; + } + + public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) { + var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); + memberData.Roles = gatewayEvent.Roles.ToList(); + return Result.FromSuccess(); + } +} + +/// +/// Handles sending replies to easter egg messages. +/// +public class MessageCreateResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + + public MessageCreateResponder(IDiscordRestChannelAPI channelApi) { + _channelApi = channelApi; + } + + public Task RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) { + _ = _channelApi.CreateMessageAsync( + gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content switch { + "whoami" => "`nobody`", + "сука !!" => "`root`", + "воооо" => "`removing /...`", + "пон" => + "https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg", + "++++" => "#", + _ => default(Optional) + }); + return Task.FromResult(Result.FromSuccess()); + } +} diff --git a/Extensions.cs b/Extensions.cs new file mode 100644 index 0000000..bb9ff20 --- /dev/null +++ b/Extensions.cs @@ -0,0 +1,189 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Text; +using DiffPlex.DiffBuilder.Model; +using Remora.Discord.API; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; + +namespace Boyfriend; + +public static class Extensions { + /// + /// Adds a footer with the 's avatar and tag (@username or username#0000). + /// + /// The builder to add the footer to. + /// The user whose tag and avatar to add. + /// The builder with the added footer. + public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) { + var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); + var avatarUrl = avatarUrlResult.IsSuccess + ? avatarUrlResult.Entity.AbsoluteUri + : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri; + + return builder.WithFooter(new EmbedFooter(user.GetTag(), avatarUrl)); + } + + /// + /// Adds a footer representing that an action was performed by a . + /// + /// The builder to add the footer to. + /// The user that performed the action whose tag and avatar to use. + /// The builder with the added footer. + public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) { + var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); + var avatarUrl = avatarUrlResult.IsSuccess + ? avatarUrlResult.Entity.AbsoluteUri + : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri; + + return builder.WithFooter( + new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl)); + } + + /// + /// Adds a title using the author field, making it smaller than using the title field. + /// + /// The builder to add the small title to. + /// The text of the small title. + /// The user whose avatar to use in the small title. + /// The URL that will be opened if a user clicks on the small title. + /// The builder with the added small title in the author field. + public static EmbedBuilder WithSmallTitle( + this EmbedBuilder builder, string text, IUser? avatarSource = null, string? url = default) { + Uri? avatarUrl = null; + if (avatarSource is not null) { + var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); + + avatarUrl = avatarUrlResult.IsSuccess + ? avatarUrlResult.Entity + : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; + } + + builder.Author = new EmbedAuthorBuilder(text, url, avatarUrl?.AbsoluteUri); + return builder; + } + + /// + /// Adds a footer representing that the action was performed in the . + /// + /// The builder to add the footer to. + /// The guild whose name and icon to use. + /// The builder with the added footer. + public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) { + var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); + var iconUrl = iconUrlResult.IsSuccess + ? iconUrlResult.Entity.AbsoluteUri + : default(Optional); + + return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl)); + } + + /// + /// Adds a title representing that the action happened in the . + /// + /// The builder to add the title to. + /// The guild whose name and icon to use. + /// The builder with the added title. + public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild) { + var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); + var iconUrl = iconUrlResult.IsSuccess + ? iconUrlResult.Entity.AbsoluteUri + : null; + + builder.Author = new EmbedAuthorBuilder(guild.Name, iconUrl: iconUrl); + return builder; + } + + /// + /// Adds a scheduled event's cover image. + /// + /// The builder to add the image to. + /// The ID of the scheduled event whose image to use. + /// The Optional containing the image hash. + /// The builder with the added cover image. + public static EmbedBuilder WithEventCover( + this EmbedBuilder builder, Snowflake eventId, Optional imageHashOptional) { + if (!imageHashOptional.IsDefined(out var imageHash)) return builder; + + var iconUrlResult = CDN.GetGuildScheduledEventCoverUrl(eventId, imageHash, imageSize: 1024); + return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder; + } + + /// + /// Sanitizes a string for use in by inserting zero-width spaces in between + /// symbols used to format the string with block code. + /// + /// The string to sanitize. + /// The sanitized string that can be safely used in . + private static string SanitizeForBlockCode(this string s) { + return s.Replace("```", "​`​`​`​"); + } + + /// + /// Sanitizes a string (see ) and formats the string with block code. + /// + /// The string to sanitize and format. + /// The sanitized string formatted with . + public static string InBlockCode(this string s) { + s = s.SanitizeForBlockCode(); + return $"```{s.SanitizeForBlockCode()}{(s.EndsWith("`") || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; + } + + public static string Localized(this string key) { + return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key; + } + + /// + /// Encodes a string to allow its transmission in request headers. + /// + /// Used when encountering "Request headers must contain only ASCII characters". + /// The string to encode. + /// An encoded string with spaces kept intact. + public static string EncodeHeader(this string s) { + return WebUtility.UrlEncode(s).Replace('+', ' '); + } + + public static string AsMarkdown(this DiffPaneModel model) { + var builder = new StringBuilder(); + foreach (var line in model.Lines) { + if (line.Type is ChangeType.Deleted) + builder.Append("-- "); + if (line.Type is ChangeType.Inserted) + builder.Append("++ "); + if (line.Type is not ChangeType.Imaginary) + builder.AppendLine(line.Text); + } + + return Markdown.BlockCode(builder.ToString().SanitizeForBlockCode(), "diff"); + } + + public static string GetTag(this IUser user) { + return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}"; + } + + public static Snowflake ToDiscordSnowflake(this ulong id) { + return DiscordSnowflake.New(id); + } + + public static TResult? MaxOrDefault( + this IEnumerable source, Func selector) { + var list = source.ToList(); + return list.Any() ? list.Max(selector) : default; + } + + public static bool TryGetContextIDs( + this ICommandContext context, [NotNullWhen(true)] out Snowflake? guildId, + [NotNullWhen(true)] out Snowflake? channelId, [NotNullWhen(true)] out Snowflake? userId) { + guildId = null; + channelId = null; + userId = null; + return context.TryGetGuildID(out guildId) + && context.TryGetChannelID(out channelId) + && context.TryGetUserID(out userId); + } +} diff --git a/InteractionResponders.cs b/InteractionResponders.cs new file mode 100644 index 0000000..231df31 --- /dev/null +++ b/InteractionResponders.cs @@ -0,0 +1,36 @@ +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.Commands.Feedback.Messages; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Interactivity; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend; + +/// +/// Handles responding to various interactions. +/// +public class InteractionResponders : InteractionGroup { + private readonly FeedbackService _feedbackService; + + public InteractionResponders(FeedbackService feedbackService) { + _feedbackService = feedbackService; + } + + /// + /// A button that will output an ephemeral embed containing the information about a scheduled event. + /// + /// The ID of the guild and scheduled event, encoded as "guildId:eventId". + /// An ephemeral feedback sending result which may or may not have succeeded. + [Button("scheduled-event-details")] + public async Task OnStatefulButtonClicked(string? state = null) { + if (state is null) return Result.FromError(new ArgumentNullError(nameof(state))); + + var idArray = state.Split(':'); + return (Result)await _feedbackService.SendContextualAsync( + $"https://discord.com/events/{idArray[0]}/{idArray[1]}", + options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + } +} diff --git a/Messages.Designer.cs b/Messages.Designer.cs index 54a7eab..9f39239 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -9,8 +9,8 @@ namespace Boyfriend { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -18,19 +18,19 @@ namespace Boyfriend { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// @@ -44,7 +44,7 @@ namespace Boyfriend { return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. @@ -58,34 +58,88 @@ namespace Boyfriend { resourceCulture = value; } } - + /// - /// Looks up a localized string similar to Bah! . + /// Looks up a localized string similar to About Boyfriend. + /// + internal static string AboutBot { + get { + return ResourceManager.GetString("AboutBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to logo and embed designer, Boyfriend's Wiki creator. + /// + internal static string AboutDeveloper_mctaylors { + get { + return ResourceManager.GetString("AboutDeveloper@mctaylors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to developer. + /// + internal static string AboutDeveloper_neroduckale { + get { + return ResourceManager.GetString("AboutDeveloper@neroduckale", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to main developer. + /// + internal static string AboutDeveloper_Octol1ttle { + get { + return ResourceManager.GetString("AboutDeveloper@Octol1ttle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Developers:. + /// + internal static string AboutTitleDevelopers { + get { + return ResourceManager.GetString("AboutTitleDevelopers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Boyfriend's Wiki Page:. + /// + internal static string AboutTitleWiki { + get { + return ResourceManager.GetString("AboutTitleWiki", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bah!. /// internal static string Beep1 { get { return ResourceManager.GetString("Beep1", resourceCulture); } } - + /// - /// Looks up a localized string similar to Bop! . + /// Looks up a localized string similar to Bop!. /// internal static string Beep2 { get { return ResourceManager.GetString("Beep2", resourceCulture); } } - + /// - /// Looks up a localized string similar to Beep! . + /// Looks up a localized string similar to Beep!. /// internal static string Beep3 { get { return ResourceManager.GetString("Beep3", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot ban users from this guild!. /// @@ -94,7 +148,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot ban this user!. /// @@ -103,7 +157,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot kick members from this guild!. /// @@ -112,7 +166,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot kick this member!. /// @@ -121,7 +175,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot manage this guild!. /// @@ -130,7 +184,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot manage messages in this guild!. /// @@ -139,7 +193,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot moderate members in this guild!. /// @@ -148,7 +202,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot mute this member!. /// @@ -157,7 +211,7 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot unmute this member!. /// @@ -166,25 +220,34 @@ namespace Boyfriend { return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); } } - + /// - /// Looks up a localized string similar to Deleted message from {0} in channel {1}: {2}. + /// Looks up a localized string similar to Cleared message from {0} in channel {1}: {2}. + /// + internal static string CachedMessageCleared { + get { + return ResourceManager.GetString("CachedMessageCleared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleted message by {0}:. /// internal static string CachedMessageDeleted { get { return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - + /// - /// Looks up a localized string similar to Edited message in channel {0}: {1} -> {2}. + /// Looks up a localized string similar to Edited message by {0}:. /// internal static string CachedMessageEdited { get { return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - + /// /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. /// @@ -193,7 +256,7 @@ namespace Boyfriend { return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); } } - + /// /// Looks up a localized string similar to Not specified. /// @@ -202,7 +265,7 @@ namespace Boyfriend { return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); } } - + /// /// Looks up a localized string similar to You need to specify an integer from {0} to {1} instead of {2}!. /// @@ -211,7 +274,7 @@ namespace Boyfriend { return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to You specified more than {0} messages!. /// @@ -220,7 +283,7 @@ namespace Boyfriend { return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); } } - + /// /// Looks up a localized string similar to You specified less than {0} messages!. /// @@ -229,7 +292,7 @@ namespace Boyfriend { return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); } } - + /// /// Looks up a localized string similar to Bans a user. /// @@ -238,7 +301,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); } } - + /// /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. /// @@ -247,7 +310,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); } } - + /// /// Looks up a localized string similar to Shows this message. /// @@ -256,7 +319,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); } } - + /// /// Looks up a localized string similar to Kicks a member. /// @@ -265,7 +328,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); } } - + /// /// Looks up a localized string similar to Mutes a member. /// @@ -274,7 +337,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); } } - + /// /// Looks up a localized string similar to Shows (inaccurate) latency. /// @@ -283,7 +346,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); } } - + /// /// Looks up a localized string similar to Adds a reminder. /// @@ -292,7 +355,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionRemind", resourceCulture); } } - + /// /// Looks up a localized string similar to Allows you to change certain preferences for this guild. /// @@ -301,7 +364,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); } } - + /// /// Looks up a localized string similar to Unbans a user. /// @@ -310,7 +373,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); } } - + /// /// Looks up a localized string similar to Unmutes a member. /// @@ -319,7 +382,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); } } - + /// /// Looks up a localized string similar to Command help:. /// @@ -328,7 +391,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandHelp", resourceCulture); } } - + /// /// Looks up a localized string similar to I do not have permission to execute this command!. /// @@ -337,7 +400,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); } } - + /// /// Looks up a localized string similar to You do not have permission to execute this command!. /// @@ -346,7 +409,7 @@ namespace Boyfriend { return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); } } - + /// /// Looks up a localized string similar to Current settings:. /// @@ -355,7 +418,7 @@ namespace Boyfriend { return ResourceManager.GetString("CurrentSettings", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}, welcome to {1}. /// @@ -364,7 +427,79 @@ namespace Boyfriend { return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Expires at: {0}. + /// + internal static string DescriptionActionExpiresAt { + get { + return ResourceManager.GetString("DescriptionActionExpiresAt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reason: {0}. + /// + internal static string DescriptionActionReason { + get { + return ResourceManager.GetString("DescriptionActionReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The event will start at {0} until {1} in {2}. + /// + internal static string DescriptionExternalEventCreated { + get { + return ResourceManager.GetString("DescriptionExternalEventCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The event is happening at {0} until {1}. + /// + internal static string DescriptionExternalEventStarted { + get { + return ResourceManager.GetString("DescriptionExternalEventStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The event will start at {0} in {1}. + /// + internal static string DescriptionLocalEventCreated { + get { + return ResourceManager.GetString("DescriptionLocalEventCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The event is happening at {0}. + /// + internal static string DescriptionLocalEventStarted { + get { + return ResourceManager.GetString("DescriptionLocalEventStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You asked me to remind you {0}. + /// + internal static string DescriptionReminder { + get { + return ResourceManager.GetString("DescriptionReminder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OK, I'll mention you on {0}. + /// + internal static string DescriptionReminderCreated { + get { + return ResourceManager.GetString("DescriptionReminderCreated", resourceCulture); + } + } + /// /// Looks up a localized string similar to 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. /// @@ -373,25 +508,25 @@ namespace Boyfriend { return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); } } - + /// - /// Looks up a localized string similar to Event {0} is cancelled!{1}. + /// Looks up a localized string similar to Event "{0}" is cancelled!. /// internal static string EventCancelled { get { return ResourceManager.GetString("EventCancelled", resourceCulture); } } - + /// - /// Looks up a localized string similar to Event {0} has completed! Duration:{1}. + /// Looks up a localized string similar to Event "{0}" has completed!. /// internal static string EventCompleted { get { return ResourceManager.GetString("EventCompleted", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4}. /// @@ -400,7 +535,34 @@ namespace Boyfriend { return ResourceManager.GetString("EventCreated", resourceCulture); } } - + + /// + /// Looks up a localized string similar to {0} has created a new event:. + /// + internal static string EventCreatedTitle { + get { + return ResourceManager.GetString("EventCreatedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event details. + /// + internal static string EventDetailsButton { + get { + return ResourceManager.GetString("EventDetailsButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The event has lasted for `{0}`. + /// + internal static string EventDuration { + get { + return ResourceManager.GetString("EventDuration", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0}Event {1} will start <t:{2}:R>!. /// @@ -409,16 +571,16 @@ namespace Boyfriend { return ResourceManager.GetString("EventEarlyNotification", resourceCulture); } } - + /// - /// Looks up a localized string similar to {0}Event {1} is starting at {2}!. + /// Looks up a localized string similar to Event "{0}" started. /// internal static string EventStarted { get { return ResourceManager.GetString("EventStarted", resourceCulture); } } - + /// /// Looks up a localized string similar to ever. /// @@ -427,7 +589,7 @@ namespace Boyfriend { return ResourceManager.GetString("Ever", resourceCulture); } } - + /// /// Looks up a localized string similar to Kicked {0}: {1}. /// @@ -436,7 +598,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); } } - + /// /// Looks up a localized string similar to Muted {0} for{1}: {2}. /// @@ -445,7 +607,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); } } - + /// /// Looks up a localized string similar to Unmuted {0}: {1}. /// @@ -454,16 +616,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); } } - - /// - /// Looks up a localized string similar to Deleted {0} messages in {1}. - /// - internal static string FeedbackMessagesCleared { - get { - return ResourceManager.GetString("FeedbackMessagesCleared", resourceCulture); - } - } - + /// /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. /// @@ -472,16 +625,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); } } - - /// - /// Looks up a localized string similar to Banned {0} for{1}: {2}. - /// - internal static string FeedbackUserBanned { - get { - return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); - } - } - + /// /// Looks up a localized string similar to Unbanned {0}: {1}. /// @@ -490,7 +634,7 @@ namespace Boyfriend { return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); } } - + /// /// Looks up a localized string similar to This channel does not exist!. /// @@ -499,619 +643,16 @@ namespace Boyfriend { return ResourceManager.GetString("InvalidChannel", resourceCulture); } } - + /// - /// Looks up a localized string similar to You did not specify a member of this guild!. + /// Looks up a localized string similar to You need to specify a member of this guild!. /// internal static string InvalidMember { get { return ResourceManager.GetString("InvalidMember", resourceCulture); } } - - /// - /// Looks up a localized string similar to This role does not exist!. - /// - internal static string InvalidRole { - get { - return ResourceManager.GetString("InvalidRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid setting value specified!. - /// - internal static string InvalidSettingValue { - get { - return ResourceManager.GetString("InvalidSettingValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a user instead of {0}!. - /// - internal static string InvalidUser { - get { - return ResourceManager.GetString("InvalidUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language not supported! Supported languages:. - /// - internal static string LanguageNotSupported { - get { - return ResourceManager.GetString("LanguageNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Member is already muted!. - /// - internal static string MemberAlreadyMuted { - get { - return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Member not muted!. - /// - internal static string MemberNotMuted { - get { - return ResourceManager.GetString("MemberNotMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ms. - /// - internal static string Milliseconds { - get { - return ResourceManager.GetString("Milliseconds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to ban this user!. - /// - internal static string MissingBanReason { - get { - return ResourceManager.GetString("MissingBanReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to kick this member!. - /// - internal static string MissingKickReason { - get { - return ResourceManager.GetString("MissingKickReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a guild member!. - /// - internal static string MissingMember { - get { - return ResourceManager.GetString("MissingMember", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to mute this member!. - /// - internal static string MissingMuteReason { - get { - return ResourceManager.GetString("MissingMuteReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. - /// - internal static string MissingNumber { - get { - return ResourceManager.GetString("MissingNumber", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify reminder text!. - /// - internal static string MissingReminderText { - get { - return ResourceManager.GetString("MissingReminderText", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to unban this user!. - /// - internal static string MissingUnbanReason { - get { - return ResourceManager.GetString("MissingUnbanReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason for unmute this member!. - /// - internal static string MissingUnmuteReason { - get { - return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a user!. - /// - internal static string MissingUser { - get { - return ResourceManager.GetString("MissingUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No. - /// - internal static string No { - get { - return ResourceManager.GetString("No", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Punishment expired. - /// - internal static string PunishmentExpired { - get { - return ResourceManager.GetString("PunishmentExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}I'm ready!. - /// - internal static string Ready { - get { - return ResourceManager.GetString("Ready", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string RoleNotSpecified { - get { - return ResourceManager.GetString("RoleNotSpecified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to That setting doesn't exist!. - /// - internal static string SettingDoesntExist { - get { - return ResourceManager.GetString("SettingDoesntExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string SettingNotDefined { - get { - return ResourceManager.GetString("SettingNotDefined", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically start scheduled events. - /// - internal static string SettingsAutoStartEvents { - get { - return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Early event start notification offset. - /// - internal static string SettingsEventEarlyNotificationOffset { - get { - return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event notifications. - /// - internal static string SettingsEventNotificationChannel { - get { - return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Role for event creation notifications. - /// - internal static string SettingsEventNotificationRole { - get { - return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event start notifications receivers. - /// - internal static string SettingsEventStartedReceivers { - get { - return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to :(. - /// - internal static string SettingsFrowningFace { - get { - return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language. - /// - internal static string SettingsLang { - get { - return ResourceManager.GetString("SettingsLang", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mute role. - /// - internal static string SettingsMuteRole { - get { - return ResourceManager.GetString("SettingsMuteRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. - /// - internal static string SettingsNothingChanged { - get { - return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prefix. - /// - internal static string SettingsPrefix { - get { - return ResourceManager.GetString("SettingsPrefix", resourceCulture); - } - } - - /// - /// 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. - /// - internal static string SettingsReceiveStartupMessages { - get { - return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remove roles on mute. - /// - internal static string SettingsRemoveRolesOnMute { - get { - return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); - } - } - - /// - /// 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. - /// - internal static string SettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Starter role. - /// - internal static string SettingsStarterRole { - get { - return ResourceManager.GetString("SettingsStarterRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Welcome message. - /// - internal static string SettingsWelcomeMessage { - get { - return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban me!. - /// - internal static string UserCannotBanBot { - get { - return ResourceManager.GetString("UserCannotBanBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban users from this guild!. - /// - internal static string UserCannotBanMembers { - get { - return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban the owner of this guild!. - /// - internal static string UserCannotBanOwner { - get { - return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban this user!. - /// - internal static string UserCannotBanTarget { - get { - return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban yourself!. - /// - internal static string UserCannotBanThemselves { - get { - return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick me!. - /// - internal static string UserCannotKickBot { - get { - return ResourceManager.GetString("UserCannotKickBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick members from this guild!. - /// - internal static string UserCannotKickMembers { - get { - return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick the owner of this guild!. - /// - internal static string UserCannotKickOwner { - get { - return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick this member!. - /// - internal static string UserCannotKickTarget { - get { - return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick yourself!. - /// - internal static string UserCannotKickThemselves { - get { - return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot manage this guild!. - /// - internal static string UserCannotManageGuild { - get { - return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot manage messages in this guild!. - /// - internal static string UserCannotManageMessages { - get { - return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot moderate members in this guild!. - /// - internal static string UserCannotModerateMembers { - get { - return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute me!. - /// - internal static string UserCannotMuteBot { - get { - return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute the owner of this guild!. - /// - internal static string UserCannotMuteOwner { - get { - return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute this member!. - /// - internal static string UserCannotMuteTarget { - get { - return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute yourself!. - /// - internal static string UserCannotMuteThemselves { - get { - return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to .... - /// - internal static string UserCannotUnmuteBot { - get { - return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. - /// - internal static string UserCannotUnmuteOwner { - get { - return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot unmute this user!. - /// - internal static string UserCannotUnmuteTarget { - get { - return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You are muted!. - /// - internal static string UserCannotUnmuteThemselves { - get { - return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This user is not banned!. - /// - internal static string UserNotBanned { - get { - return ResourceManager.GetString("UserNotBanned", resourceCulture); - } - } - - /// - /// 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 UserNotFound { - get { - return ResourceManager.GetString("UserNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yes. - /// - internal static string Yes { - get { - return ResourceManager.GetString("Yes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You were banned by {0} in guild `{1}` for {2}. - /// - internal static string YouWereBanned { - get { - return ResourceManager.GetString("YouWereBanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You were kicked by {0} in guild `{1}` for {2}. - /// - internal static string YouWereKicked { - get { - return ResourceManager.GetString("YouWereKicked", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to OK, I'll mention you on <t:{0}:f>. - /// - internal static string FeedbackReminderAdded { - get { - return ResourceManager.GetString("FeedbackReminderAdded", resourceCulture); - } - } - + /// /// Looks up a localized string similar to You need to specify when I should send you the reminder!. /// @@ -1120,13 +661,760 @@ namespace Boyfriend { return ResourceManager.GetString("InvalidRemindIn", resourceCulture); } } - + /// - /// Looks up a localized string similar to Deleted message using cleanup from {0} in channel {1}: {2}. + /// Looks up a localized string similar to This role does not exist!. /// - internal static string CachedMessageCleared { + internal static string InvalidRole { get { - return ResourceManager.GetString("CachedMessageCleared", resourceCulture); + return ResourceManager.GetString("InvalidRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid setting value specified!. + /// + internal static string InvalidSettingValue { + get { + return ResourceManager.GetString("InvalidSettingValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a user instead of {0}!. + /// + internal static string InvalidUser { + get { + return ResourceManager.GetString("InvalidUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Issued by. + /// + internal static string IssuedBy { + get { + return ResourceManager.GetString("IssuedBy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language not supported!. + /// + internal static string LanguageNotSupported { + get { + return ResourceManager.GetString("LanguageNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member is already muted!. + /// + internal static string MemberAlreadyMuted { + get { + return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member not muted!. + /// + internal static string MemberNotMuted { + get { + return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to From {0}:. + /// + internal static string MessageFrom { + get { + return ResourceManager.GetString("MessageFrom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cleared {0} messages. + /// + internal static string MessagesCleared { + get { + return ResourceManager.GetString("MessagesCleared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ms. + /// + internal static string Milliseconds { + get { + return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to ban this user!. + /// + internal static string MissingBanReason { + get { + return ResourceManager.GetString("MissingBanReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to kick this member!. + /// + internal static string MissingKickReason { + get { + return ResourceManager.GetString("MissingKickReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a guild member!. + /// + internal static string MissingMember { + get { + return ResourceManager.GetString("MissingMember", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to mute this member!. + /// + internal static string MissingMuteReason { + get { + return ResourceManager.GetString("MissingMuteReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. + /// + internal static string MissingNumber { + get { + return ResourceManager.GetString("MissingNumber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify reminder text!. + /// + internal static string MissingReminderText { + get { + return ResourceManager.GetString("MissingReminderText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason to unban this user!. + /// + internal static string MissingUnbanReason { + get { + return ResourceManager.GetString("MissingUnbanReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a reason for unmute this member!. + /// + internal static string MissingUnmuteReason { + get { + return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need to specify a user!. + /// + internal static string MissingUser { + get { + return ResourceManager.GetString("MissingUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No. + /// + internal static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Punishment expired. + /// + internal static string PunishmentExpired { + get { + return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I'm ready!. + /// + internal static string Ready { + get { + return ResourceManager.GetString("Ready", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reminder for {0}. + /// + internal static string Reminder { + get { + return ResourceManager.GetString("Reminder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reminder for {0} created. + /// + internal static string ReminderCreated { + get { + return ResourceManager.GetString("ReminderCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string RoleNotSpecified { + get { + return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to That setting doesn't exist!. + /// + internal static string SettingDoesntExist { + get { + return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to is now. + /// + internal static string SettingIsNow { + get { + return ResourceManager.GetString("SettingIsNow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Setting not changed. + /// + internal static string SettingNotChanged { + get { + return ResourceManager.GetString("SettingNotChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not specified. + /// + internal static string SettingNotDefined { + get { + return ResourceManager.GetString("SettingNotDefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically start scheduled events. + /// + internal static string SettingsAutoStartEvents { + get { + return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default role. + /// + internal static string SettingsDefaultRole { + get { + return ResourceManager.GetString("SettingsDefaultRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Early event start notification offset. + /// + internal static string SettingsEventEarlyNotificationOffset { + get { + return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel for event notifications. + /// + internal static string SettingsEventNotificationChannel { + get { + return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Role for event creation notifications. + /// + internal static string SettingsEventNotificationRole { + get { + return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event start notifications receivers. + /// + internal static string SettingsEventStartedReceivers { + get { + return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to :(. + /// + internal static string SettingsFrowningFace { + get { + return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language. + /// + internal static string SettingsLang { + get { + return ResourceManager.GetString("SettingsLang", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Boyfriend's Settings. + /// + internal static string SettingsListTitle { + get { + return ResourceManager.GetString("SettingsListTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mute role. + /// + internal static string SettingsMuteRole { + get { + return ResourceManager.GetString("SettingsMuteRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. + /// + internal static string SettingsNothingChanged { + get { + return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix. + /// + internal static string SettingsPrefix { + get { + return ResourceManager.GetString("SettingsPrefix", resourceCulture); + } + } + + /// + /// 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. + /// + internal static string SettingsReceiveStartupMessages { + get { + return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove roles on mute. + /// + internal static string SettingsRemoveRolesOnMute { + get { + return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); + } + } + + /// + /// 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. + /// + internal static string SettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Setting successfuly changed. + /// + internal static string SettingSuccessfulyChanged { + get { + return ResourceManager.GetString("SettingSuccessfulyChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome message. + /// + internal static string SettingsWelcomeMessage { + get { + return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This user is already banned!. + /// + internal static string UserAlreadyBanned { + get { + return ResourceManager.GetString("UserAlreadyBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This user is already muted!. + /// + internal static string UserAlreadyMuted { + get { + return ResourceManager.GetString("UserAlreadyMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} was banned. + /// + internal static string UserBanned { + get { + return ResourceManager.GetString("UserBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban me!. + /// + internal static string UserCannotBanBot { + get { + return ResourceManager.GetString("UserCannotBanBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban users from this guild!. + /// + internal static string UserCannotBanMembers { + get { + return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban the owner of this guild!. + /// + internal static string UserCannotBanOwner { + get { + return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban this user!. + /// + internal static string UserCannotBanTarget { + get { + return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot ban yourself!. + /// + internal static string UserCannotBanThemselves { + get { + return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick me!. + /// + internal static string UserCannotKickBot { + get { + return ResourceManager.GetString("UserCannotKickBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick members from this guild!. + /// + internal static string UserCannotKickMembers { + get { + return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick the owner of this guild!. + /// + internal static string UserCannotKickOwner { + get { + return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick this member!. + /// + internal static string UserCannotKickTarget { + get { + return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot kick yourself!. + /// + internal static string UserCannotKickThemselves { + get { + return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot manage this guild!. + /// + internal static string UserCannotManageGuild { + get { + return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot manage messages in this guild!. + /// + internal static string UserCannotManageMessages { + get { + return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot moderate members in this guild!. + /// + internal static string UserCannotModerateMembers { + get { + return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute me!. + /// + internal static string UserCannotMuteBot { + get { + return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute the owner of this guild!. + /// + internal static string UserCannotMuteOwner { + get { + return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute this member!. + /// + internal static string UserCannotMuteTarget { + get { + return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot mute yourself!. + /// + internal static string UserCannotMuteThemselves { + get { + return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to .... + /// + internal static string UserCannotUnmuteBot { + get { + return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. + /// + internal static string UserCannotUnmuteOwner { + get { + return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot unmute this user!. + /// + internal static string UserCannotUnmuteTarget { + get { + return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are muted!. + /// + internal static string UserCannotUnmuteThemselves { + get { + return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} was kicked. + /// + internal static string UserKicked { + get { + return ResourceManager.GetString("UserKicked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} was muted. + /// + internal static string UserMuted { + get { + return ResourceManager.GetString("UserMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This user is not banned!. + /// + internal static string UserNotBanned { + get { + return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + + /// + /// 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 UserNotFound { + get { + return ResourceManager.GetString("UserNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I could not find this user!. + /// + internal static string UserNotFoundShort { + get { + return ResourceManager.GetString("UserNotFoundShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This member is not muted!. + /// + internal static string UserNotMuted { + get { + return ResourceManager.GetString("UserNotMuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} was unbanned. + /// + internal static string UserUnbanned { + get { + return ResourceManager.GetString("UserUnbanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} was unmuted. + /// + internal static string UserUnmuted { + get { + return ResourceManager.GetString("UserUnmuted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yes. + /// + internal static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You were banned. + /// + internal static string YouWereBanned { + get { + return ResourceManager.GetString("YouWereBanned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You were kicked. + /// + internal static string YouWereKicked { + get { + return ResourceManager.GetString("YouWereKicked", resourceCulture); } } } diff --git a/Messages.resx b/Messages.resx index 7e771ae..0bb7ab5 100644 --- a/Messages.resx +++ b/Messages.resx @@ -1,145 +1,133 @@  - - - - - - + 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. + --> + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - {0}I'm ready! + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + I'm ready! - - Deleted message from {0} in channel {1}: {2} + + Deleted message by {0}: - + Cleared message from {0} in channel {1}: {2} - - Edited message in channel {0}: {1} -> {2} + + Edited message by {0}: - + {0}, welcome to {1} - - Bah! + + Bah! - - Bop! + + Bop! - Beep! + Beep! I do not have permission to execute this command! @@ -148,8 +136,8 @@ You do not have permission to execute this command! - You were banned by {0} in guild `{1}` for {2} - + You were banned + Punishment expired @@ -163,8 +151,8 @@ Command help: - You were kicked by {0} in guild `{1}` for {2} - + You were kicked + ms @@ -177,148 +165,148 @@ Not specified - + Current settings: - + Language - + Prefix - + Remove roles on mute - + Send welcome messages Mute role - Language not supported! Supported languages: - - + Language not supported! + + Yes - + No - + This user is not banned! - + Member not muted! Welcome message - + You need to specify an integer from {0} to {1} instead of {2}! - - Banned {0} for{1}: {2} + + {0} was banned - + That setting doesn't exist! - + Receive startup messages - + Invalid setting value specified! - + This role does not exist! - + This channel does not exist! 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 - + I cannot use time-outs on other bots! Try to set a mute role in settings - + {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4} - + Role for event creation notifications - + Channel for event notifications - + Event start notifications receivers - - {0}Event {1} is starting at {2}! + + Event "{0}" started - + :( - - Event {0} is cancelled!{1} + + Event "{0}" is cancelled! - - Event {0} has completed! Duration:{1} + + Event "{0}" has completed! - + ever - - Deleted {0} messages in {1} + + Cleared {0} messages - + Kicked {0}: {1} - + Muted {0} for{1}: {2} - + Unbanned {0}: {1} - + Unmuted {0}: {1} - + Nothing changed! `{0}` is already set to {1} - + Not specified - + Value of setting `{0}` is now set to {1} - + Bans a user - + Deletes a specified amount of messages in this channel - + Shows this message - + Kicks a member - + Mutes a member - + Shows (inaccurate) latency - + Allows you to change certain preferences for this guild - + Unbans a user - + Unmutes a member - + You need to specify an integer from {0} to {1}! @@ -331,8 +319,8 @@ You need to specify a guild member! - You need to specify a member of this guild! - + You need to specify a member of this guild! + You cannot ban users from this guild! @@ -345,94 +333,94 @@ You cannot moderate members in this guild! - + You cannot manage this guild! - + I cannot ban users from this guild! - + I cannot manage messages in this guild! - + I cannot kick members from this guild! - + I cannot moderate members in this guild! - + I cannot manage this guild! - + You need to specify a reason to ban this user! - + You need to specify a reason to kick this member! - + You need to specify a reason to mute this member! - + You need to specify a reason to unban this user! - + You need to specify a reason for unmute this member! You cannot ban the owner of this guild! - + You cannot ban yourself! - + You cannot ban me! - + I cannot ban this user! - + You cannot ban this user! - + You cannot kick the owner of this guild! - + You cannot kick yourself! - + You cannot kick me! - + I cannot kick this member! - + You cannot kick this member! - + You cannot mute the owner of this guild! - + You cannot mute yourself! - + You cannot mute me! - + I cannot mute this member! - + You cannot mute this member! - + You don't need to unmute the owner of this guild! - + You are muted! - + ... - + I cannot unmute this member! @@ -445,33 +433,129 @@ Early event start notification offset - 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 - + 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 + + + Default role + - Adds a reminder - + Adds a reminder + - Channel for public notifications - + Channel for public notifications + - Channel for private notifications - + Channel for private notifications + - Return roles on rejoin - + Return roles on rejoin + - Automatically start scheduled events - + Automatically start scheduled events + - You need to specify reminder text! - - - OK, I'll mention you on <t:{0}:f> + You need to specify reminder text! + + + OK, I'll mention you on {0} - You need to specify when I should send you the reminder! + You need to specify when I should send you the reminder! + + + Issued by + + + {0} has created a new event: + + + The event will start at {0} in {1} + + + The event will start at {0} until {1} in {2} + + + Event details + + + The event has lasted for `{0}` + + + The event is happening at {0} + + + The event is happening at {0} until {1} + + + This user is already banned! + + + {0} was unbanned + + + {0} was muted + + + {0} was unmuted + + + This member is not muted! + + + I could not find this user! + + + {0} was kicked + + + Reason: {0} + + + Expires at: {0} + + + This user is already muted! + + + From {0}: + + + Developers: + + + Boyfriend's Wiki Page: + + + About Boyfriend + + + logo and embed designer, Boyfriend's Wiki creator + + + main developer + + + developer + + + Reminder for {0} created + + + Reminder for {0} + + + You asked me to remind you {0} + + + Boyfriend's Settings + + + Setting successfuly changed + + + Setting not changed + + + is now diff --git a/Messages.ru.resx b/Messages.ru.resx index cd5fecd..1351ea1 100644 --- a/Messages.ru.resx +++ b/Messages.ru.resx @@ -1,145 +1,133 @@  - - - - - - + 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. + --> + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - {0}Я запустился! + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Я запустился! - - Удалено сообщение от {0} в канале {1}: {2} + + Сообщение {0} удалено: - + Очищено сообщение от {0} в канале {1}: {2} - - Отредактировано сообщение в канале {0}: {1} -> {2} + + Сообщение {0} отредактировано: - + {0}, добро пожаловать на сервер {1} - - Бап! + + Бап! - - Боп! + + Боп! - Бип! + Бип! У меня недостаточно прав для выполнения этой команды! @@ -147,9 +135,6 @@ У тебя недостаточно прав для выполнения этой команды! - - Тебя забанил {0} на сервере `{1}` за {2} - Время наказания истекло @@ -163,8 +148,8 @@ Справка по командам: - Тебя кикнул {0} на сервере `{1}` за {2} - + Вы были выгнаны + мс @@ -177,148 +162,148 @@ Не указана - + Текущие настройки: - + Язык - + Префикс - + Удалять роли при муте - + Отправлять приветствия - + Роль мута - Язык не поддерживается! Поддерживаемые языки: - - + Язык не поддерживается! + + Да - + Нет - + Этот пользователь не забанен! - + Участник не заглушен! Приветствие - + Надо указать целое число от {0} до {1} вместо {2}! - - Забанен {0} на{1}: {2} + + {0} был(-а) забанен(-а) - + Такая настройка не существует! - + Получать сообщения о запуске - + Указано недействительное значение для настройки! - + Эта роль не существует! - + Этот канал не существует! Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках - + Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4} - + Роль для уведомлений о создании событий - + Канал для уведомлений о событиях - + Получатели уведомлений о начале событий - - {0}Событие {1} начинается в {2}! + + Событие "{0}" началось - + :( - - Событие {0} отменено!{1} + + Событие "{0}" отменено! - - Событие {0} завершено! Продолжительность:{1} + + Событие "{0}" завершено! - + всегда - - Удалено {0} сообщений в {1} + + Очищено {0} сообщений - + Выгнан {0}: {1} - + Заглушен {0} на{1}: {2} - + Возвращён из бана {0}: {1} - + Разглушен {0}: {1} - + Ничего не изменилось! Значение настройки `{0}` уже {1} - + Не указано - + Значение настройки `{0}` теперь установлено на {1} - + Банит пользователя - + Удаляет указанное количество сообщений в этом канале - + Показывает эту справку - + Выгоняет участника - + Глушит участника - + Показывает (неточную) задержку - + Позволяет менять некоторые настройки под этот сервер - + Возвращает пользователя из бана - + Разглушает участника - + Надо указать целое число от {0} до {1}! @@ -331,8 +316,8 @@ Надо указать участника сервера! - Надо указать участника этого сервера! - + Надо указать участника этого сервера! + Ты не можешь банить пользователей на этом сервере! @@ -345,94 +330,94 @@ Ты не можешь модерировать участников этого сервера! - + Ты не можешь настраивать этот сервер! - + Я не могу банить пользователей на этом сервере! - + Я не могу управлять сообщениями этого сервера! - + Я не могу выгонять участников с этого сервера! - + Я не могу модерировать участников этого сервера! - + Я не могу настраивать этот сервер! - + Надо указать причину для бана этого участника! - + Надо указать причину для кика этого участника! - + Надо указать причину для мута этого участника! Надо указать причину для разбана этого пользователя! - + Надо указать причину для размута этого участника! - + Ты не можешь меня забанить! - + Ты не можешь забанить владельца этого сервера! - + Ты не можешь забанить этого участника! - + Ты не можешь себя забанить! - + Я не могу забанить этого пользователя! - + Ты не можешь выгнать владельца этого сервера! - + Ты не можешь себя выгнать! - + Ты не можешь меня выгнать! - + Я не могу выгнать этого участника - + Ты не можешь выгнать этого участника! - + Ты не можешь заглушить владельца этого сервера! - + Ты не можешь себя заглушить! - + Ты не можешь заглушить меня! - + Я не могу заглушить этого пользователя! - + Ты не можешь заглушить этого участника! - + Тебе не надо возвращать из мута владельца этого сервера! - + Ты заглушен! - + ... - + Ты не можешь вернуть из мута этого пользователя! @@ -445,33 +430,132 @@ Офсет отправки преждевременного уведомления о начале события - Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад - - - Начальная роль - + Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад + + + Общая роль + - Добавляет напоминание - + Добавляет напоминание + - Канал для публичных уведомлений - + Канал для публичных уведомлений + - Канал для приватных уведомлений - + Канал для приватных уведомлений + - Возвращать роли при перезаходе + Возвращать роли при перезаходе + + + Автоматически начинать события + + + Тебе нужно указать текст напоминания! + + + Хорошо, я упомяну тебя {0} - - Автоматически начинать события + + Нужно указать время, через которое придёт напоминание! + + + Ответственный + + + {0} создаёт новое событие: + + + Событие пройдёт {0} в канале {1} + + + Событие пройдёт с {0} до {1} в {2} + + + Подробнее о событии + + + Событие длилось `{0}` + + + Событие происходит в {0} + + + Событие происходит в {0} до {1} + + + Этот пользователь уже забанен! + + + {0} был(-а) разбанен(-а) + + + {0} был(-а) заглушен(-а) + + + Этот участник не заглушен! + + + {0} был(-а) разглушен(-а) + + + Я не смог найти этого пользователя! + + + {0} был(-а) выгнан(-а) + + + Причина: {0} + + + Закончится: {0} + + + Этот пользователь уже в муте! + + + Вы были забанены + + + От {0}: + + + Разработчики: + + + Страница Boyfriend's Wiki: + + + О Boyfriend + + + разрабочик + + + основной разработчик + + + дизайнер лого и эмбедов, создатель Boyfriend's Wiki + + + Напоминание для {0} создано - - Тебе нужно указать текст напоминания! + + Напоминание для {0} - - Хорошо, я упомяну тебя <t:{0}:f> + + Вы просили напомнить вам {0} - - Нужно указать время, через которое придёт напоминание! + + Настройки Boyfriend + + + Настройка успешно изменена + + + Настройка не редактирована + + + теперь имеет значение diff --git a/Messages.tt-ru.resx b/Messages.tt-ru.resx index 88364b2..e4a75be 100644 --- a/Messages.tt-ru.resx +++ b/Messages.tt-ru.resx @@ -1,145 +1,133 @@ - - - - - - + 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. + --> + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - {0}я родился! + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + я родился! - - вырезано сообщение от {0} в канале {1}: {2} + + сообщение {0} вырезано: - + вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2} - - переделано сообщение от {0}: {1} -> {2} + + сообщение {0} переделано: - + {0}, добро пожаловать на сервер {1} - - брах! + + брах! - - брох! + + брох! - брух! + брух! у меня прав нету, сделай что нибудь. @@ -148,8 +136,8 @@ у тебя прав нету, твои проблемы. - здарова, тебя крч забанил {0} на сервере `{1}` за {2} - + вы были забанены + время бана закончиловсь @@ -163,8 +151,8 @@ туториал по приколам: - здарова, тебя крч кикнул {0} на сервере `{1}` за {2} - + вы были кикнуты + мс @@ -175,150 +163,150 @@ *тут ничего нет* - нъет - - + нъет + + настройки: - + язык - + префикс - + удалять звание при муте - + разглашать о том что пришел новый шизоид - звание замученного - - - такого языка нету, ты шо, есть только такие: + звание замученного - + + такого языка нету... + + да - + нъет - + шизик не забанен - + шизоид не замучен! - здравствуйте (типо настройка) - - + здравствуйте (типо настройка) + + выбери число от {0} до {1} вместо {2}! - - забанен {0} на{1}: {2} + + {0} забанен - + такой прикол не существует - получать инфу о старте бота - - + получать инфу о старте бота + + криво настроил прикол, давай по новой - + этого звания нету, ты шо - + этого канала нету, ты шо ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим - + я не могу замутить ботов, сделай что нибудь - {0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!{4} + {0} приготовил новую движуху {1}! она пройдёт в {2} и начнётся <t:{3}:R>!{4} - - роль для уведомлений о создании квеста + + роль для уведомлений о создании движухи - - канал для уведомлений о квестах + + канал для уведомлений о движухах - - получатели уведомлений о начале квеста + + получатели уведомлений о начале движух - - {0}квест {1} начинается в {2}! + + движуха "{0}" начинается - - оъмъомоъемъъео(((( + + оъмъомоъемъъео(((( - - квест {0} отменен!{1} + + движуха "{0}" отменен! - - квест {0} завершен! все это длилось{1} + + движуха "{0}" завершен! - + всегда - - удалено {0} сообщений в {1} + + вырезано {0} забавных сообщений - + выгнан {0}: {1} - + замучен {0} на{1}: {2} - + раззабанен {0}: {1} - + раззамучен {0}: {1} - + ты все сломал! значение прикола `{0}` и так {1} - нъет - - + нъет + + прикол для `{0}` теперь установлен на {1} - + возводит великий банхаммер над шизоидом - + удаляет сообщения. сколько хош, столько и удалит - + показывает то, что ты сейчас видишь прямо сейчас - + выпинывает шизоида - + мутит шизоида - + показывает пинг (сверхмегаточный (нет)) - + настройки бота под этот сервер - + отводит великий банхаммер от шизоида - + раззамучивает шизоида - + укажи целое число от {0} до {1} @@ -331,8 +319,8 @@ укажи самого шизика - укажи шизоида сервера! - + укажи шизоида сервера! + бан @@ -345,133 +333,229 @@ тебе нельзя управлять шизоидами - + тебе нельзя редактировать дурку - + я не могу ваще никого банить чел. - + я не могу исправлять орфографический кринж участников, сделай что нибудь. - + я не могу ваще никого кикать чел. - + я не могу контроллировать за всеми ними, сделай что нибудь. - + я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь. - + укажи зачем банить шизика - + укажи зачем кикать шизика - + укажи зачем мутить шизика укажи зачем раззабанивать шизика - + укажи зачам размучивать шизика - + ээбля френдли фаер огонь по своим - + бан админу нельзя - + бан этому шизику нельзя - + самобан нельзя - + я не могу его забанить... - + кик админу нельзя - + самокик нельзя - + ээбля френдли фаер огонь по своим - + я не могу его кикнуть... - + кик этому шизику нельзя - + мут админу нельзя - + самомут нельзя - + ээбля френдли фаер огонь по своим - + я не могу его замутить... - + мут этому шизику нельзя - сильно - - + сильно + + ты замучен. - + ... - + тебе нельзя раззамучивать я не могу его раззамутить... - {0}квест {1} начнется <t:{2}:R>! + {0}движуха {1} начнется <t:{2}:R>! - заранее пнуть в минутах до начала квеста + заранее пнуть в минутах до начала движухи - у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад) - - - базовое звание - + у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад) + + + дефолтное звание + - крафтит напоминалку - + крафтит напоминалку + - канал для секретных уведомлений - + канал для секретных уведомлений + - канал для не секретных уведомлений - + канал для не секретных уведомлений + - вернуть звания при переподключении в дурку + вернуть звания при переподключении в дурку + + + автоматом стартить движухи + + + для крафта напоминалки нужен текст + + + вас понял, упоминание будет {0} - - автоматом стартить квесты + + шизоид у меня на часах такого нету + + + ответственный + + + {0} создает новое событие: + + + движуха произойдет {0} в канале {1} + + + движуха будет происходить с {0} до {1} в {2} + + + побольше о движухе + + + все это длилось `{0}` + + + движуха происходит в {0} + + + движуха происходит в {0} до {1} + + + этот шизоид уже лежит в бане + + + {0} раззабанен + + + {0} в муте + + + {0} в размуте + + + этого шизоида никто не мутил. + + + у нас такого шизоида нету... + + + {0} вышел с посторонней помощью + + + причина: {0} + + + до: {0} + + + этот шизоид УЖЕ замучился + + + от {0} + + + девелоперы: + + + страничка Boyfriend's Wiki: + + + немного о Boyfriend + + + скучный лого/эмбед дизайнер создавший Boyfriend's Wiki + + + ВАЖНЫЙ соучастник кодинг-стримов @Octol1ttle + + + САМЫЙ ВАЖНЫЙ чел написавший кода больше всех (99.99%) + + + напоминалка для {0} скрафченА - - для крафта напоминалки нужен текст + + напоминалка для {0} - - вас понял, упоминание будет <t:{0}:f> + + ты хотел чтоб я напомнил тебе {0} - - шизоид у меня на часах такого нету + + приколы Boyfriend + + + прикол редактирован + + + прикол сдох + + + стало diff --git a/ReplyEmojis.cs b/ReplyEmojis.cs deleted file mode 100644 index 3ccb6a6..0000000 --- a/ReplyEmojis.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Boyfriend; - -public static class ReplyEmojis { - public const string Success = ":white_check_mark:"; - public const string Error = ":x:"; - public const string MissingArgument = ":keyboard:"; - public const string InvalidArgument = ":construction:"; - public const string NoPermission = ":no_entry_sign:"; - public const string CantInteract = ":vertical_traffic_light:"; - public const string Help = ":page_facing_up:"; - public const string SettingsList = ":gear:"; - public const string SettingsSet = ":control_knobs:"; - public const string Ping = ":signal_strength:"; - public const string Banned = ":hammer:"; - public const string Kicked = ":police_car:"; - public const string Muted = ":mute:"; - public const string Unmuted = ":loud_sound:"; - public const string Reminder = ":alarm_clock:"; -} diff --git a/Services/GuildDataService.cs b/Services/GuildDataService.cs new file mode 100644 index 0000000..be873f1 --- /dev/null +++ b/Services/GuildDataService.cs @@ -0,0 +1,109 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using Boyfriend.Data; +using Microsoft.Extensions.Hosting; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Rest.Core; + +namespace Boyfriend.Services; + +/// +/// Handles saving, loading, initializing and providing . +/// +public class GuildDataService : IHostedService { + private readonly ConcurrentDictionary _datas = new(); + private readonly IDiscordRestGuildAPI _guildApi; + + // https://github.com/dotnet/aspnetcore/issues/39139 + public GuildDataService( + IHostApplicationLifetime lifetime, IDiscordRestGuildAPI guildApi) { + _guildApi = guildApi; + lifetime.ApplicationStopping.Register(ApplicationStopping); + } + + public Task StartAsync(CancellationToken ct) { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken ct) { + return Task.CompletedTask; + } + + private void ApplicationStopping() { + SaveAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + private async Task SaveAsync(CancellationToken ct) { + var tasks = new List(); + foreach (var data in _datas.Values) { + await using var configStream = File.OpenWrite(data.ConfigurationPath); + tasks.Add(JsonSerializer.SerializeAsync(configStream, data.Configuration, cancellationToken: ct)); + + await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath); + tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct)); + + foreach (var memberData in data.MemberData.Values) { + await using var memberDataStream = File.OpenWrite($"{data.MemberDataPath}/{memberData.Id}.json"); + tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct)); + } + } + + await Task.WhenAll(tasks); + } + + public async Task GetData(Snowflake guildId, CancellationToken ct = default) { + return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); + } + + private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) { + var idString = $"{guildId}"; + var memberDataPath = $"{guildId}/MemberData"; + var configurationPath = $"{guildId}/Configuration.json"; + var scheduledEventsPath = $"{guildId}/ScheduledEvents.json"; + if (!Directory.Exists(idString)) Directory.CreateDirectory(idString); + if (!Directory.Exists(memberDataPath)) Directory.CreateDirectory(memberDataPath); + if (!File.Exists(configurationPath)) await File.WriteAllTextAsync(configurationPath, "{}", ct); + if (!File.Exists(scheduledEventsPath)) await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); + + await using var configurationStream = File.OpenRead(configurationPath); + var configuration + = JsonSerializer.DeserializeAsync( + configurationStream, cancellationToken: ct); + + await using var eventsStream = File.OpenRead(scheduledEventsPath); + var events + = JsonSerializer.DeserializeAsync>( + eventsStream, cancellationToken: ct); + + var memberData = new Dictionary(); + foreach (var dataPath in Directory.GetFiles(memberDataPath)) { + await using var dataStream = File.OpenRead(dataPath); + var data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); + if (data is null) continue; + var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToDiscordSnowflake(), ct); + if (memberResult.IsSuccess) + data.Roles = memberResult.Entity.Roles.ToList(); + + memberData.Add(data.Id, data); + } + + var finalData = new GuildData( + await configuration ?? new GuildConfiguration(), configurationPath, + await events ?? new Dictionary(), scheduledEventsPath, + memberData, memberDataPath); + while (!_datas.ContainsKey(guildId)) _datas.TryAdd(guildId, finalData); + return finalData; + } + + public async Task GetConfiguration(Snowflake guildId, CancellationToken ct = default) { + return (await GetData(guildId, ct)).Configuration; + } + + public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) { + return (await GetData(guildId, ct)).GetMemberData(userId); + } + + public ICollection GetGuildIds() { + return _datas.Keys; + } +} diff --git a/Services/GuildUpdateService.cs b/Services/GuildUpdateService.cs new file mode 100644 index 0000000..3d55f07 --- /dev/null +++ b/Services/GuildUpdateService.cs @@ -0,0 +1,389 @@ +using Boyfriend.Data; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Gateway.Commands; +using Remora.Discord.API.Objects; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Discord.Gateway; +using Remora.Discord.Gateway.Responders; +using Remora.Discord.Interactivity; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Services; + +/// +/// Handles executing guild updates (also called "ticks") once per second. +/// +public class GuildUpdateService : BackgroundService { + private static readonly (string Name, TimeSpan Duration)[] SongList = { + ("UNDEAD CORPORATION - The Empress", new TimeSpan(0, 4, 34)), + ("UNDEAD CORPORATION - Everything will freeze", new TimeSpan(0, 3, 17)), + ("Splatoon 3 - Rockagilly Blues (Yoko & the Gold Bazookas)", new TimeSpan(0, 3, 37)), + ("Splatoon 3 - Seep and Destroy", new TimeSpan(0, 2, 42)), + ("IA - A Tale of Six Trillion Years and a Night", new TimeSpan(0, 3, 40)), + ("Manuel - Gas Gas Gas", new TimeSpan(0, 3, 17)), + ("Camellia - Flamewall", new TimeSpan(0, 6, 50)) + }; + + private readonly List _activityList = new(1) { new Activity("with Remora.Discord", ActivityType.Game) }; + + private readonly IDiscordRestChannelAPI _channelApi; + private readonly DiscordGatewayClient _client; + private readonly GuildDataService _dataService; + private readonly IDiscordRestGuildScheduledEventAPI _eventApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly ILogger _logger; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; + + private DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; + private uint _nextSongIndex; + + public GuildUpdateService( + IDiscordRestChannelAPI channelApi, DiscordGatewayClient client, GuildDataService dataService, + IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, ILogger logger, + IDiscordRestUserAPI userApi, UtilityService utility) { + _channelApi = channelApi; + _client = client; + _dataService = dataService; + _eventApi = eventApi; + _guildApi = guildApi; + _logger = logger; + _userApi = userApi; + _utility = utility; + } + + /// + /// Activates a periodic timer with a 1 second interval and adds guild update tasks on each timer tick. + /// Additionally, updates the current presence with songs from . + /// + /// If update tasks take longer than 1 second, the next timer tick will be skipped. + /// The cancellation token for this operation. + protected override async Task ExecuteAsync(CancellationToken ct) { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + var tasks = new List(); + + while (await timer.WaitForNextTickAsync(ct)) { + var guildIds = _dataService.GetGuildIds(); + if (guildIds.Count > 0 && DateTimeOffset.UtcNow >= _nextSongAt) { + var nextSong = SongList[_nextSongIndex]; + _activityList[0] = new Activity(nextSong.Name, ActivityType.Listening); + _client.SubmitCommand( + new UpdatePresence( + UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList)); + _nextSongAt = DateTimeOffset.UtcNow.Add(nextSong.Duration); + _nextSongIndex++; + if (_nextSongIndex >= SongList.Length) _nextSongIndex = 0; + } + + tasks.AddRange(guildIds.Select(id => TickGuildAsync(id, ct))); + + await Task.WhenAll(tasks); + tasks.Clear(); + } + } + + /// + /// Runs an update ("tick") for a guild with the provided . + /// + /// + /// This method does the following: + /// + /// Automatically unbans users once their ban period has expired. + /// Automatically grants members the guild's if one is set. + /// Sends reminders about an upcoming scheduled event. + /// Automatically starts scheduled events if is enabled. + /// Sends scheduled event start notifications. + /// Sends scheduled event completion notifications. + /// Sends reminders to members. + /// + /// This is done here and not in a for the following reasons: + /// + /// + /// Downtime would affect the reliability of notifications and automatic unbans if this logic were to be in a + /// . + /// + /// The Discord API doesn't provide necessary information about scheduled event updates. + /// + /// + /// The ID of the guild to update. + /// The cancellation token for this operation. + private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) { + var data = await _dataService.GetData(guildId, ct); + Messages.Culture = data.Culture; + var defaultRoleSnowflake = data.Configuration.DefaultRole.ToDiscordSnowflake(); + + foreach (var memberData in data.MemberData.Values) { + var userId = memberData.Id.ToDiscordSnowflake(); + + if (defaultRoleSnowflake.Value is not 0 && !memberData.Roles.Contains(defaultRoleSnowflake)) + _ = _guildApi.AddGuildMemberRoleAsync( + guildId, userId, defaultRoleSnowflake, ct: ct); + + if (DateTimeOffset.UtcNow > memberData.BannedUntil) { + var unbanResult = await _guildApi.RemoveGuildBanAsync( + guildId, userId, Messages.PunishmentExpired.EncodeHeader(), ct); + if (unbanResult.IsSuccess) + memberData.BannedUntil = null; + else + _logger.LogWarning( + "Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message); + } + + var userResult = await _userApi.GetUserAsync(userId, ct); + if (!userResult.IsDefined(out var user)) continue; + + for (var i = memberData.Reminders.Count - 1; i >= 0; i--) { + var reminder = memberData.Reminders[i]; + if (DateTimeOffset.UtcNow < reminder.RemindAt) continue; + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.Reminder, user.GetTag()), user) + .WithDescription( + string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) + .WithColour(ColorsList.Magenta) + .Build(); + + if (!embed.IsDefined(out var built)) continue; + + var messageResult = await _channelApi.CreateMessageAsync( + reminder.Channel, Mention.User(user), embeds: new[] { built }, ct: ct); + if (!messageResult.IsSuccess) + _logger.LogWarning( + "Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message); + + memberData.Reminders.Remove(reminder); + } + } + + var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); + if (!eventsResult.IsDefined(out var events)) return; + + if (data.Configuration.EventNotificationChannel is 0) return; + + foreach (var scheduledEvent in events) { + if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) { + data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status)); + } else { + var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; + if (storedEvent.Status == scheduledEvent.Status) { + if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) { + if (data.Configuration.AutoStartEvents + && scheduledEvent.Status is not GuildScheduledEventStatus.Active) { + var startResult = await _eventApi.ModifyGuildScheduledEventAsync( + guildId, scheduledEvent.ID, + status: GuildScheduledEventStatus.Active, ct: ct); + if (!startResult.IsSuccess) + _logger.LogWarning( + "Error in automatic scheduled event start request.\n{ErrorMessage}", + startResult.Error.Message); + } + } else if (data.Configuration.EventEarlyNotificationOffset != TimeSpan.Zero + && !storedEvent.EarlyNotificationSent + && DateTimeOffset.UtcNow + >= scheduledEvent.ScheduledStartTime - data.Configuration.EventEarlyNotificationOffset) { + var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct); + if (earlyResult.IsSuccess) + storedEvent.EarlyNotificationSent = true; + else + _logger.LogWarning( + "Error in scheduled event early notification sender.\n{ErrorMessage}", + earlyResult.Error.Message); + } + + continue; + } + + storedEvent.Status = scheduledEvent.Status; + } + + var result = scheduledEvent.Status switch { + GuildScheduledEventStatus.Scheduled => + await SendScheduledEventCreatedMessage(scheduledEvent, data.Configuration, ct), + GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => + await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct), + _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))) + }; + + if (!result.IsSuccess) + _logger.LogWarning("Error in guild update.\n{ErrorMessage}", result.Error.Message); + } + } + + /// + /// Handles sending a notification, mentioning the if one is + /// set, + /// when a scheduled event is created + /// in a guild's if one is set. + /// + /// The scheduled event that has just been created. + /// The configuration of the guild containing the scheduled event. + /// The cancellation token for this operation. + /// A notification sending result which may or may not have succeeded. + private async Task SendScheduledEventCreatedMessage( + IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) { + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + if (!scheduledEvent.CreatorID.IsDefined(out var creatorId)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID))); + var creatorResult = await _userApi.GetUserAsync(creatorId.Value, ct); + if (!creatorResult.IsDefined(out var creator)) return Result.FromError(creatorResult); + + string embedDescription; + var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null } + ? scheduledEvent.Description.Value + : string.Empty; + switch (scheduledEvent.EntityType) { + case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: + if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + + embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.DescriptionLocalEventCreated, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime), + Mention.Channel(channelId) + ))}"; + break; + case GuildScheduledEventEntityType.External: + if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); + if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); + if (!metadata.Location.IsDefined(out var location)) + return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + + embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.DescriptionExternalEventCreated, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime), + Markdown.Timestamp(endTime), + Markdown.InlineCode(location) + ))}"; + break; + default: + return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) + .WithTitle(scheduledEvent.Name) + .WithDescription(embedDescription) + .WithEventCover(scheduledEvent.ID, scheduledEvent.Image) + .WithUserFooter(currentUser) + .WithCurrentTimestamp() + .WithColour(ColorsList.White) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + var roleMention = config.EventNotificationRole is not 0 + ? Mention.Role(config.EventNotificationRole.ToDiscordSnowflake()) + : string.Empty; + + var button = new ButtonComponent( + ButtonComponentStyle.Primary, + Messages.EventDetailsButton, + new PartialEmoji(Name: "📋"), + CustomIDHelpers.CreateButtonIDWithState( + "scheduled-event-details", $"{scheduledEvent.GuildID}:{scheduledEvent.ID}") + ); + + return (Result)await _channelApi.CreateMessageAsync( + config.EventNotificationChannel.ToDiscordSnowflake(), roleMention, embeds: new[] { built }, + components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); + } + + /// + /// Handles sending a notification, mentioning the s, + /// when a scheduled event is about to start, has started or completed + /// in a guild's if one is set. + /// + /// The scheduled event that is about to start, has started or completed. + /// The data for the guild containing the scheduled event. + /// Controls whether or not a reminder for the scheduled event should be sent instead of the event started/completed notification + /// The cancellation token for this operation + /// A reminder/notification sending result which may or may not have succeeded. + private async Task SendScheduledEventUpdatedMessage( + IGuildScheduledEvent scheduledEvent, GuildData data, bool early, CancellationToken ct = default) { + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + var embed = new EmbedBuilder(); + string? content = null; + if (early) + embed.WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser) + .WithColour(ColorsList.Default); + else + switch (scheduledEvent.Status) { + case GuildScheduledEventStatus.Active: + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + + string embedDescription; + switch (scheduledEvent.EntityType) { + case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: + if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + + embedDescription = string.Format( + Messages.DescriptionLocalEventStarted, + Mention.Channel(channelId) + ); + break; + case GuildScheduledEventEntityType.External: + if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); + if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); + if (!metadata.Location.IsDefined(out var location)) + return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + + embedDescription = string.Format( + Messages.DescriptionExternalEventStarted, + Markdown.InlineCode(location), + Markdown.Timestamp(endTime) + ); + break; + default: + return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))); + } + + var contentResult = await _utility.GetEventNotificationMentions( + scheduledEvent, data.Configuration, ct); + if (!contentResult.IsDefined(out content)) + return Result.FromError(contentResult); + + embed.WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) + .WithDescription(embedDescription) + .WithColour(ColorsList.Green); + break; + case GuildScheduledEventStatus.Completed: + embed.WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) + .WithDescription( + string.Format( + Messages.EventDuration, + DateTimeOffset.UtcNow.Subtract( + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime + ?? scheduledEvent.ScheduledStartTime).ToString())) + .WithColour(ColorsList.Black); + + data.ScheduledEvents.Remove(scheduledEvent.ID.Value); + break; + case GuildScheduledEventStatus.Canceled: + case GuildScheduledEventStatus.Scheduled: + default: return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))); + } + + var result = embed.WithCurrentTimestamp().Build(); + + if (!result.IsDefined(out var built)) return Result.FromError(result); + + return (Result)await _channelApi.CreateMessageAsync( + data.Configuration.EventNotificationChannel.ToDiscordSnowflake(), + content ?? default(Optional), embeds: new[] { built }, ct: ct); + } +} diff --git a/Services/UtilityService.cs b/Services/UtilityService.cs new file mode 100644 index 0000000..b4ff6fb --- /dev/null +++ b/Services/UtilityService.cs @@ -0,0 +1,140 @@ +using System.Text; +using Boyfriend.Data; +using Microsoft.Extensions.Hosting; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Services; + +/// +/// Provides utility methods that cannot be transformed to extension methods because they require usage +/// of some Discord APIs. +/// +public class UtilityService : IHostedService { + private readonly IDiscordRestGuildScheduledEventAPI _eventApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; + + public UtilityService( + IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IDiscordRestGuildScheduledEventAPI eventApi) { + _guildApi = guildApi; + _userApi = userApi; + _eventApi = eventApi; + } + + public Task StartAsync(CancellationToken ct) { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken ct) { + return Task.CompletedTask; + } + + /// + /// Checks whether or not a member can interact with another member + /// + /// The ID of the guild in which an operation is being performed. + /// The executor of the operation. + /// The target of the operation. + /// The operation. + /// The cancellation token for this operation. + /// + /// + /// A result which has succeeded with a null string if the member can interact with the target. + /// + /// A result which has succeeded with a non-null string containing the error message if the member cannot + /// interact with the target. + /// + /// A result which has failed if an error occurred during the execution of this method. + /// + /// + public async Task> CheckInteractionsAsync( + Snowflake guildId, Snowflake interacterId, Snowflake targetId, string action, CancellationToken ct = default) { + if (interacterId == targetId) + return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); + + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + if (currentUser.ID == targetId) + return Result.FromSuccess($"UserCannot{action}Bot".Localized()); + + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); + if (!guildResult.IsDefined(out var guild)) + return Result.FromError(guildResult); + if (targetId == guild.OwnerID) return Result.FromSuccess($"UserCannot{action}Owner".Localized()); + + var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); + if (!targetMemberResult.IsDefined(out var targetMember)) + return Result.FromSuccess(null); + + var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, currentUser.ID, ct); + if (!currentMemberResult.IsDefined(out var currentMember)) + return Result.FromError(currentMemberResult); + + var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); + if (!rolesResult.IsDefined(out var roles)) + return Result.FromError(rolesResult); + + var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); + var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); + + var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); + if (targetBotRoleDiff >= 0) + return Result.FromSuccess($"BotCannot{action}Target".Localized()); + + if (interacterId == guild.OwnerID) + return Result.FromSuccess(null); + + var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId, ct); + if (!interacterResult.IsDefined(out var interacter)) + return Result.FromError(interacterResult); + + var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); + var targetInteracterRoleDiff + = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); + if (targetInteracterRoleDiff >= 0) + return Result.FromSuccess($"UserCannot{action}Target".Localized()); + + return Result.FromSuccess(null); + } + + /// + /// Gets the string mentioning all s related to a scheduled + /// event. + /// + /// + /// If the guild configuration enables , then the + /// will also be mentioned. + /// + /// + /// The scheduled event whose subscribers will be mentioned if the guild configuration enables + /// . + /// + /// The configuration of the guild containing the scheduled event + /// The cancellation token for this operation. + /// A result containing the string which may or may not have succeeded. + public async Task> GetEventNotificationMentions( + IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) { + var builder = new StringBuilder(); + var receivers = config.EventStartedReceivers; + var role = config.EventNotificationRole.ToDiscordSnowflake(); + var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync( + scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct); + if (!usersResult.IsDefined(out var users)) return Result.FromError(usersResult); + + if (receivers.Contains(GuildConfiguration.NotificationReceiver.Role) && role.Value is not 0) + builder.Append($"{Mention.Role(role)} "); + if (receivers.Contains(GuildConfiguration.NotificationReceiver.Interested)) + builder = users.Where( + user => { + if (!user.GuildMember.IsDefined(out var member)) return true; + return !member.Roles.Contains(role); + }) + .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} ")); + return builder.ToString(); + } +} diff --git a/Utils.cs b/Utils.cs deleted file mode 100644 index 27c6ecb..0000000 --- a/Utils.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using Boyfriend.Data; -using Discord; -using Discord.Net; -using Discord.WebSocket; -using Humanizer; -using Humanizer.Localisation; - -namespace Boyfriend; - -public static partial class Utils { - 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 AllowedMentions AllowRoles = new() { - AllowedTypes = AllowedMentionTypes.Roles - }; - - public static string GetBeep(int i = -1) { - return GetMessage($"Beep{(i < 0 ? Random.Shared.Next(3) + 1 : ++i)}"); - } - - public static string? Wrap(string? original, bool limitedSpace = false) { - if (original is null) return null; - var maxChars = limitedSpace ? 970 : 1940; - if (original.Length > maxChars) original = original[..maxChars]; - var style = original.Contains('\n') ? "```" : "`"; - return $"{style}{original}{(original.Equals("") ? " " : "")}{style}"; - } - - public static string MentionChannel(ulong id) { - return $"<#{id}>"; - } - - public static ulong ParseMention(string mention) { - return ulong.TryParse(NumbersOnlyRegex().Replace(mention, ""), out var id) ? id : 0; - } - - public static async Task SendDirectMessage(SocketUser user, string toSend) { - try { await user.SendMessageAsync(toSend); } catch (HttpException e) { - if (e.DiscordCode is not DiscordErrorCode.CannotSendMessageToUser) throw; - } - } - - 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 UnreachableException($"Message length is out of range: {text.Length}"); - - await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); - } catch (Exception e) { - await Boyfriend.Log( - new LogMessage( - LogSeverity.Error, nameof(Utils), - "Exception while silently sending message", e)); - } - } - - public static RequestOptions GetRequestOptions(string reason) { - var options = RequestOptions.Default; - options.AuditLogReason = reason; - return options; - } - - public static string GetMessage(string name) { - var propertyName = name; - name = $"{Messages.Culture}/{name}"; - if (ReflectionMessageCache.TryGetValue(name, out var cachedMessage)) return cachedMessage; - - var toReturn = - typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) - ?.ToString(); - if (toReturn is null) { - Console.Error.WriteLine($@"Could not find localized property: {propertyName}"); - return name; - } - - ReflectionMessageCache.Add(name, toReturn); - return toReturn; - } - - public static async Task - 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 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(SocketGuild guild) { - Messages.Culture = CultureInfoCache[GuildData.Get(guild).Preferences["Lang"]]; - } - - public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketTextChannel? channel) { - if (channel is null) return; - if (appendTo.Length + appendWhat.Length > 2000) { - _ = SilentSendAsync(channel, appendTo.ToString()); - appendTo.Clear(); - } - - appendTo.AppendLine(appendWhat); - } - - public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketUserMessage message) { - if (appendTo.Length + appendWhat.Length > 2000) { - _ = message.ReplyAsync(appendTo.ToString(), false, null, AllowedMentions.None); - appendTo.Clear(); - } - - appendTo.AppendLine(appendWhat); - } - - public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) { - 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.UtcNow) return false; - - await toUnmute.RemoveTimeOutAsync(requestOptions); - } - - return true; - } - - [GeneratedRegex("[^0-9]")] - private static partial Regex NumbersOnlyRegex(); -} diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0b5e0cd --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement via the "Report Content" feature or via email at +l1ttleofficial@outlook.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..bcaa25f --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing Guidelines + +Thank you for showing interest in the development of Boyfriend. We aim to provide a good collaborating environment for +everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. +Before starting, please read our [Code of Conduct](CODE_OF_CONDUCT.md) + +## Reporting bugs + +A **bug** is a situation in which there is something clearly wrong with the bot. Examples of applicable bug reports are: + +- The bot doesn't reply to a command +- The bot sends the same message twice +- The bot takes a long time to a respond if I use this specific command +- An embed the bot sent has incorrect information in it + +To track bug reports, we primarily use GitHub **issues**. When opening an issue, please keep in mind the following: + +- Before opening the issue, please search for any similar existing issues using the text search bar and the issue + labels. This includes both open and closed issues (we may have already fixed something, but the fix hasn't yet been + released). +- When opening the issue, please fill out as much of the issue template as you can. In particular, please make sure to + include console output and screenshots as much as possible. +- We may ask you for follow-up information to reproduce or debug the problem. Please look out for this and provide + follow-up info if we request it. + +## Submitting pull requests + +While pull requests from unaffiliated contributors are welcome, please note that the core team *may* be focused on +internal issues that haven't been published to the issue tracker yet. Reviewing PRs is done on a best-effort basis, so +please be aware that it may take a while before a core maintainer gets around to review your change. + +The [issue tracker](https://github.com/TeamOctolings/Boyfriend/issues) should provide plenty of issues to start with. +Make sure to check that an issue you're planning to resolve does not already have people working on it and that there +are no PRs associated with it + +In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't +seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the +correct direction on how to address it. + +If you'd like to propose a subjective change to one of the UI/UX aspects of the bot, or there is a bigger task you'd +like to work on, but there is no corresponding issue yet for it, **please open an issue first** to avoid wasted effort. + +Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes: + +- Make sure you're comfortable with the principles of object-oriented programming, the syntax of C\# and your + development environment. +- Make sure you are familiar with [git](https://git-scm.com/) + and [the pull request workflow](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests). +- Please do not make code changes via the GitHub web interface. +- Please make sure your development environment respects the .editorconfig file present in the repository. Our code + style differs from most C\# projects and is closer to something you see in Java projects. +- Please test your changes. We expect most new features and bugfixes to be tested in an environment similar to + production. + +After you're done with your changes and you wish to open the PR, please observe the following recommendations: + +- Please submit the pull request from + a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and + keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary. +- Please avoid pushing untested or incomplete code. +- Please do not force-push or rebase unless we ask you to. +- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change + is ready for merge. + +We are highly committed to quality when it comes to Boyfriend. This means that contributions from less experienced +community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never +conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please +consider our comments and requests a learning experience. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..45f6a92 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,48 @@ + + + + + + + Boyfriend Logo + + + +![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master) +![GitHub last commit](https://img.shields.io/github/last-commit/TeamOctolings/Boyfriend) +![CodeFactor](https://img.shields.io/codefactor/grade/github/TeamOctolings/Boyfriend) + +Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and +Discord.Net + +## Features + +* Banning, muting, kicking, etc. +* Reminding you about something if you wish +* Reminding everyone about that new event you made +* Log everything from joining the server to deleting messages + +*...and more!* + +## Installing and running Boyfriend + +You can read our [wiki](https://github.com/TeamOctolings/Boyfriend/wiki) in order to assemble your Boyfriend™ and +moderate the server. + +## Contributing + +When it comes to contributing to the project, the two main things you can do to help out are reporting issues and +submitting pull requests. Please refer to the [contributing guidelines](CONTRIBUTING.md) to understand how to help in +the most effective way possible. + +## Special Thanks + +![JetBrains Logo (Main) logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg) + +[JetBrains](https://www.jetbrains.com/), creators of [ReSharper](https://www.jetbrains.com/resharper) +and [Rider](https://www.jetbrains.com/rider), supports Boyfriend with one of +their [Open Source Licenses](https://jb.gg/OpenSourceSupport). +Rider is the recommended IDE when working with Boyfriend, and most of the Boyfriend team uses it. +Additionally, ReSharper command-line tools made by JetBrains are used for status checks on pull requests to ensure code +quality even when not using ReSharper or Rider. From 2dd9f023ef56cfa0a331f920d22b3da1d5050f23 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 9 Jul 2023 20:15:39 +0500 Subject: [PATCH 096/329] Fix some issues with slash commands & add missing docs (#46) mctaylors: "but I use Rider too..." --- Boyfriend.csproj | 25 +- Commands/KickCommandGroup.cs | 10 +- Commands/MuteCommandGroup.cs | 27 +- Commands/RemindCommandGroup.cs | 15 +- Commands/SettingsCommandGroup.cs | 30 +- Extensions.cs | 2 +- Messages.Designer.cs | 2195 ++++++++++++------------------ Messages.resx | 4 +- Messages.ru.resx | 2 +- Messages.tt-ru.resx | 2 +- docs/README.md | 2 +- 11 files changed, 916 insertions(+), 1398 deletions(-) diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 29483d7..a9af80e 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -19,31 +19,14 @@ - - - - - + + + + - - - - - True - True - Messages.resx - - - - - ResXFileCodeGenerator Messages.Designer.cs - - - - diff --git a/Commands/KickCommandGroup.cs b/Commands/KickCommandGroup.cs index e70a903..da7b2c5 100644 --- a/Commands/KickCommandGroup.cs +++ b/Commands/KickCommandGroup.cs @@ -42,15 +42,15 @@ public class KickCommandGroup : CommandGroup { } /// - /// A slash command that kicks a Discord user with the specified reason. + /// A slash command that kicks a Discord member with the specified reason. /// - /// The user to kick. + /// The member to kick. /// /// The reason for this kick. Must be encoded with when passed to /// . /// /// - /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member /// was kicked and vice-versa. /// [Command("kick", "кик")] @@ -59,8 +59,8 @@ public class KickCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] [Description("Kick member")] public async Task KickUserAsync( - [Description("Member to kick")] IUser target, - [Description("Kick reason")] string reason) { + [Description("Member to kick")] IUser target, + [Description("Kick reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); diff --git a/Commands/MuteCommandGroup.cs b/Commands/MuteCommandGroup.cs index cdf750d..764b4f4 100644 --- a/Commands/MuteCommandGroup.cs +++ b/Commands/MuteCommandGroup.cs @@ -44,16 +44,16 @@ public class MuteCommandGroup : CommandGroup { } /// - /// A slash command that mutes a Discord user with the specified reason. + /// A slash command that mutes a Discord member with the specified reason. /// - /// The user to mute. - /// The duration for this mute. The user will be automatically unmuted after this duration. + /// The member to mute. + /// The duration for this mute. The member will be automatically unmuted after this duration. /// /// The reason for this mute. Must be encoded with when passed to /// . /// /// - /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member /// was muted and vice-versa. /// /// @@ -63,10 +63,9 @@ public class MuteCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Mute member")] public async Task MuteUserAsync( - [Description("Member to mute")] IUser target, - [Description("Mute reason")] string reason, - [Description("Mute duration")] - TimeSpan duration) { + [Description("Member to mute")] IUser target, + [Description("Mute reason")] string reason, + [Description("Mute duration")] TimeSpan duration) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -156,15 +155,15 @@ public class MuteCommandGroup : CommandGroup { } /// - /// A slash command that unmutes a Discord user with the specified reason. + /// A slash command that unmutes a Discord member with the specified reason. /// - /// The user to unmute. + /// The member to unmute. /// /// The reason for this unmute. Must be encoded with when passed to /// . /// /// - /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user + /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member /// was unmuted and vice-versa. /// /// @@ -175,10 +174,8 @@ public class MuteCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Unmute member")] public async Task UnmuteUserAsync( - [Description("Member to unmute")] - IUser target, - [Description("Unmute reason")] - string reason) { + [Description("Member to unmute")] IUser target, + [Description("Unmute reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); diff --git a/Commands/RemindCommandGroup.cs b/Commands/RemindCommandGroup.cs index 5ee7a8c..d1c4519 100644 --- a/Commands/RemindCommandGroup.cs +++ b/Commands/RemindCommandGroup.cs @@ -33,9 +33,18 @@ public class RemindCommandGroup : CommandGroup { _userApi = userApi; } + /// + /// A slash command that schedules a reminder with the specified text. + /// + /// The period of time which must pass before the reminder will be sent. + /// The text of the reminder. + /// A feedback sending result which may or may not have succeeded. [Command("remind")] [Description("Create a reminder")] - public async Task AddReminderAsync(TimeSpan duration, string text) { + public async Task AddReminderAsync( + [Description("After what period of time mention the reminder")] + TimeSpan @in, + [Description("Reminder message")] string message) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -44,13 +53,13 @@ public class RemindCommandGroup : CommandGroup { if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); - var remindAt = DateTimeOffset.UtcNow.Add(duration); + var remindAt = DateTimeOffset.UtcNow.Add(@in); (await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add( new Reminder { RemindAt = remindAt, Channel = channelId.Value, - Text = text + Text = message }); var embed = new EmbedBuilder().WithSmallTitle(string.Format(Messages.ReminderCreated, user.GetTag()), user) diff --git a/Commands/SettingsCommandGroup.cs b/Commands/SettingsCommandGroup.cs index 7e4763f..e519d37 100644 --- a/Commands/SettingsCommandGroup.cs +++ b/Commands/SettingsCommandGroup.cs @@ -5,11 +5,8 @@ using Boyfriend.Data; using Boyfriend.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; -using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Contexts; -using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; @@ -44,10 +41,9 @@ public class SettingsCommandGroup : CommandGroup { /// /// A feedback sending result which may or may not have succeeded. /// - [Command("settings list")] + [Command("settingslist")] [Description("Shows settings list for this server")] - [SuppressInteractionResponse(suppress: true)] - public async Task SendSettingsListAsync() { + public async Task ListSettingsAsync() { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -77,21 +73,21 @@ public class SettingsCommandGroup : CommandGroup { .Build(); if (!embed.IsDefined(out var built)) return Result.FromError(embed); - return (Result)await _feedbackService.SendContextualEmbedAsync( - built, ct: CancellationToken, options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); } /// - /// A slash command that modifies per-guild settings. + /// A slash command that modifies per-guild settings. /// - /// - /// A feedback sending result which may or may not have succeeded. - /// + /// The setting to modify. + /// The new value of the setting. + /// A feedback sending result which may or may not have succeeded. [Command("settings")] [Description("Change settings for this server")] public async Task EditSettingsAsync( - [Description("настройка")] string setting, - [Description("значение")] string value) { + [Description("The setting whose value you want to change")] + string setting, + [Description("Setting value")] string value) { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -144,10 +140,10 @@ public class SettingsCommandGroup : CommandGroup { var builder = new StringBuilder(); builder.Append(Markdown.InlineCode(setting)) - .Append($" {Messages.SettingIsNow} ") - .Append(Markdown.InlineCode(value)); + .Append($" {Messages.SettingIsNow} ") + .Append(Markdown.InlineCode(value)); - var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfulyChanged, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser) .WithDescription(builder.ToString()) .WithColour(ColorsList.Green) .Build(); diff --git a/Extensions.cs b/Extensions.cs index bb9ff20..983aea2 100644 --- a/Extensions.cs +++ b/Extensions.cs @@ -159,7 +159,7 @@ public static class Extensions { builder.AppendLine(line.Text); } - return Markdown.BlockCode(builder.ToString().SanitizeForBlockCode(), "diff"); + return InBlockCode(builder.ToString()); } public static string GetTag(this IUser user) { diff --git a/Messages.Designer.cs b/Messages.Designer.cs index 9f39239..aaf5bc1 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -11,46 +11,32 @@ namespace Boyfriend { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -59,1363 +45,910 @@ namespace Boyfriend { } } - /// - /// Looks up a localized string similar to About Boyfriend. - /// - internal static string AboutBot { - get { - return ResourceManager.GetString("AboutBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to logo and embed designer, Boyfriend's Wiki creator. - /// - internal static string AboutDeveloper_mctaylors { - get { - return ResourceManager.GetString("AboutDeveloper@mctaylors", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to developer. - /// - internal static string AboutDeveloper_neroduckale { - get { - return ResourceManager.GetString("AboutDeveloper@neroduckale", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to main developer. - /// - internal static string AboutDeveloper_Octol1ttle { - get { - return ResourceManager.GetString("AboutDeveloper@Octol1ttle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Developers:. - /// - internal static string AboutTitleDevelopers { - get { - return ResourceManager.GetString("AboutTitleDevelopers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Boyfriend's Wiki Page:. - /// - internal static string AboutTitleWiki { - get { - return ResourceManager.GetString("AboutTitleWiki", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bah!. - /// - internal static string Beep1 { - get { - return ResourceManager.GetString("Beep1", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bop!. - /// - internal static string Beep2 { - get { - return ResourceManager.GetString("Beep2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Beep!. - /// - internal static string Beep3 { - get { - return ResourceManager.GetString("Beep3", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot ban users from this guild!. - /// - internal static string BotCannotBanMembers { - get { - return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot ban this user!. - /// - internal static string BotCannotBanTarget { - get { - return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot kick members from this guild!. - /// - internal static string BotCannotKickMembers { - get { - return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot kick this member!. - /// - internal static string BotCannotKickTarget { - get { - return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot manage this guild!. - /// - internal static string BotCannotManageGuild { - get { - return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot manage messages in this guild!. - /// - internal static string BotCannotManageMessages { - get { - return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot moderate members in this guild!. - /// - internal static string BotCannotModerateMembers { - get { - return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot mute this member!. - /// - internal static string BotCannotMuteTarget { - get { - return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot unmute this member!. - /// - internal static string BotCannotUnmuteTarget { - get { - return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cleared message from {0} in channel {1}: {2}. - /// - internal static string CachedMessageCleared { - get { - return ResourceManager.GetString("CachedMessageCleared", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deleted message by {0}:. - /// - internal static string CachedMessageDeleted { - get { - return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edited message by {0}:. - /// - internal static string CachedMessageEdited { - get { - return ResourceManager.GetString("CachedMessageEdited", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I cannot use time-outs on other bots! Try to set a mute role in settings. - /// - internal static string CannotTimeOutBot { - get { - return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string ChannelNotSpecified { - get { - return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify an integer from {0} to {1} instead of {2}!. - /// - internal static string ClearAmountInvalid { - get { - return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You specified more than {0} messages!. - /// - internal static string ClearAmountTooLarge { - get { - return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You specified less than {0} messages!. - /// - internal static string ClearAmountTooSmall { - get { - return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bans a user. - /// - internal static string CommandDescriptionBan { - get { - return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deletes a specified amount of messages in this channel. - /// - internal static string CommandDescriptionClear { - get { - return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Shows this message. - /// - internal static string CommandDescriptionHelp { - get { - return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Kicks a member. - /// - internal static string CommandDescriptionKick { - get { - return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mutes a member. - /// - internal static string CommandDescriptionMute { - get { - return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Shows (inaccurate) latency. - /// - internal static string CommandDescriptionPing { - get { - return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); - } - } - - /// - /// 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. - /// - internal static string CommandDescriptionSettings { - get { - return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unbans a user. - /// - internal static string CommandDescriptionUnban { - get { - return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unmutes a member. - /// - internal static string CommandDescriptionUnmute { - get { - return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Command help:. - /// - internal static string CommandHelp { - get { - return ResourceManager.GetString("CommandHelp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I do not have permission to execute this command!. - /// - internal static string CommandNoPermissionBot { - get { - return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You do not have permission to execute this command!. - /// - internal static string CommandNoPermissionUser { - get { - return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Current settings:. - /// - internal static string CurrentSettings { - get { - return ResourceManager.GetString("CurrentSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}, welcome to {1}. - /// - internal static string DefaultWelcomeMessage { - get { - return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Expires at: {0}. - /// - internal static string DescriptionActionExpiresAt { - get { - return ResourceManager.GetString("DescriptionActionExpiresAt", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reason: {0}. - /// - internal static string DescriptionActionReason { - get { - return ResourceManager.GetString("DescriptionActionReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The event will start at {0} until {1} in {2}. - /// - internal static string DescriptionExternalEventCreated { - get { - return ResourceManager.GetString("DescriptionExternalEventCreated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The event is happening at {0} until {1}. - /// - internal static string DescriptionExternalEventStarted { - get { - return ResourceManager.GetString("DescriptionExternalEventStarted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The event will start at {0} in {1}. - /// - internal static string DescriptionLocalEventCreated { - get { - return ResourceManager.GetString("DescriptionLocalEventCreated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The event is happening at {0}. - /// - internal static string DescriptionLocalEventStarted { - get { - return ResourceManager.GetString("DescriptionLocalEventStarted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You asked me to remind you {0}. - /// - internal static string DescriptionReminder { - get { - return ResourceManager.GetString("DescriptionReminder", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to OK, I'll mention you on {0}. - /// - internal static string DescriptionReminderCreated { - get { - return ResourceManager.GetString("DescriptionReminderCreated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to 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. - /// - internal static string DurationRequiredForTimeOuts { - get { - return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event "{0}" is cancelled!. - /// - internal static string EventCancelled { - get { - return ResourceManager.GetString("EventCancelled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event "{0}" has completed!. - /// - internal static string EventCompleted { - get { - return ResourceManager.GetString("EventCompleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4}. - /// - internal static string EventCreated { - get { - return ResourceManager.GetString("EventCreated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} has created a new event:. - /// - internal static string EventCreatedTitle { - get { - return ResourceManager.GetString("EventCreatedTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event details. - /// - internal static string EventDetailsButton { - get { - return ResourceManager.GetString("EventDetailsButton", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The event has lasted for `{0}`. - /// - internal static string EventDuration { - get { - return ResourceManager.GetString("EventDuration", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}Event {1} will start <t:{2}:R>!. - /// - internal static string EventEarlyNotification { - get { - return ResourceManager.GetString("EventEarlyNotification", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event "{0}" started. - /// - internal static string EventStarted { - get { - return ResourceManager.GetString("EventStarted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ever. - /// - internal static string Ever { - get { - return ResourceManager.GetString("Ever", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Kicked {0}: {1}. - /// - internal static string FeedbackMemberKicked { - get { - return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Muted {0} for{1}: {2}. - /// - internal static string FeedbackMemberMuted { - get { - return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unmuted {0}: {1}. - /// - internal static string FeedbackMemberUnmuted { - get { - return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value of setting `{0}` is now set to {1}. - /// - internal static string FeedbackSettingsUpdated { - get { - return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unbanned {0}: {1}. - /// - internal static string FeedbackUserUnbanned { - get { - return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This channel does not exist!. - /// - internal static string InvalidChannel { - get { - return ResourceManager.GetString("InvalidChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a member of this guild!. - /// - internal static string InvalidMember { - get { - return ResourceManager.GetString("InvalidMember", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify when I should send you the reminder!. - /// - internal static string InvalidRemindIn { - get { - return ResourceManager.GetString("InvalidRemindIn", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This role does not exist!. - /// - internal static string InvalidRole { - get { - return ResourceManager.GetString("InvalidRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid setting value specified!. - /// - internal static string InvalidSettingValue { - get { - return ResourceManager.GetString("InvalidSettingValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a user instead of {0}!. - /// - internal static string InvalidUser { - get { - return ResourceManager.GetString("InvalidUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Issued by. - /// - internal static string IssuedBy { - get { - return ResourceManager.GetString("IssuedBy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language not supported!. - /// - internal static string LanguageNotSupported { - get { - return ResourceManager.GetString("LanguageNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Member is already muted!. - /// - internal static string MemberAlreadyMuted { - get { - return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Member not muted!. - /// - internal static string MemberNotMuted { - get { - return ResourceManager.GetString("MemberNotMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to From {0}:. - /// - internal static string MessageFrom { - get { - return ResourceManager.GetString("MessageFrom", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cleared {0} messages. - /// - internal static string MessagesCleared { - get { - return ResourceManager.GetString("MessagesCleared", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ms. - /// - internal static string Milliseconds { - get { - return ResourceManager.GetString("Milliseconds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to ban this user!. - /// - internal static string MissingBanReason { - get { - return ResourceManager.GetString("MissingBanReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to kick this member!. - /// - internal static string MissingKickReason { - get { - return ResourceManager.GetString("MissingKickReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a guild member!. - /// - internal static string MissingMember { - get { - return ResourceManager.GetString("MissingMember", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to mute this member!. - /// - internal static string MissingMuteReason { - get { - return ResourceManager.GetString("MissingMuteReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify an integer from {0} to {1}!. - /// - internal static string MissingNumber { - get { - return ResourceManager.GetString("MissingNumber", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify reminder text!. - /// - internal static string MissingReminderText { - get { - return ResourceManager.GetString("MissingReminderText", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason to unban this user!. - /// - internal static string MissingUnbanReason { - get { - return ResourceManager.GetString("MissingUnbanReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a reason for unmute this member!. - /// - internal static string MissingUnmuteReason { - get { - return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You need to specify a user!. - /// - internal static string MissingUser { - get { - return ResourceManager.GetString("MissingUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No. - /// - internal static string No { - get { - return ResourceManager.GetString("No", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Punishment expired. - /// - internal static string PunishmentExpired { - get { - return ResourceManager.GetString("PunishmentExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I'm ready!. - /// internal static string Ready { get { return ResourceManager.GetString("Ready", resourceCulture); } } - /// - /// Looks up a localized string similar to Reminder for {0}. - /// - internal static string Reminder { + internal static string CachedMessageDeleted { get { - return ResourceManager.GetString("Reminder", resourceCulture); + return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - /// - /// Looks up a localized string similar to Reminder for {0} created. - /// - internal static string ReminderCreated { + internal static string CachedMessageCleared { get { - return ResourceManager.GetString("ReminderCreated", resourceCulture); + return ResourceManager.GetString("CachedMessageCleared", resourceCulture); } } - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string RoleNotSpecified { + internal static string CachedMessageEdited { get { - return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - /// - /// Looks up a localized string similar to That setting doesn't exist!. - /// - internal static string SettingDoesntExist { + internal static string DefaultWelcomeMessage { get { - return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); } } - /// - /// Looks up a localized string similar to is now. - /// - internal static string SettingIsNow { + internal static string Beep1 { get { - return ResourceManager.GetString("SettingIsNow", resourceCulture); + return ResourceManager.GetString("Beep1", resourceCulture); } } - /// - /// Looks up a localized string similar to Setting not changed. - /// - internal static string SettingNotChanged { + internal static string Beep2 { get { - return ResourceManager.GetString("SettingNotChanged", resourceCulture); + return ResourceManager.GetString("Beep2", resourceCulture); } } - /// - /// Looks up a localized string similar to Not specified. - /// - internal static string SettingNotDefined { + internal static string Beep3 { get { - return ResourceManager.GetString("SettingNotDefined", resourceCulture); + return ResourceManager.GetString("Beep3", resourceCulture); } } - /// - /// Looks up a localized string similar to Automatically start scheduled events. - /// - internal static string SettingsAutoStartEvents { + internal static string CommandNoPermissionBot { get { - return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); + return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); } } - /// - /// Looks up a localized string similar to Default role. - /// - internal static string SettingsDefaultRole { + internal static string CommandNoPermissionUser { get { - return ResourceManager.GetString("SettingsDefaultRole", resourceCulture); + return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); } } - /// - /// Looks up a localized string similar to Early event start notification offset. - /// - internal static string SettingsEventEarlyNotificationOffset { - get { - return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Channel for event notifications. - /// - internal static string SettingsEventNotificationChannel { - get { - return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Role for event creation notifications. - /// - internal static string SettingsEventNotificationRole { - get { - return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event start notifications receivers. - /// - internal static string SettingsEventStartedReceivers { - get { - return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to :(. - /// - internal static string SettingsFrowningFace { - get { - return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Language. - /// - internal static string SettingsLang { - get { - return ResourceManager.GetString("SettingsLang", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Boyfriend's Settings. - /// - internal static string SettingsListTitle { - get { - return ResourceManager.GetString("SettingsListTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mute role. - /// - internal static string SettingsMuteRole { - get { - return ResourceManager.GetString("SettingsMuteRole", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nothing changed! `{0}` is already set to {1}. - /// - internal static string SettingsNothingChanged { - get { - return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prefix. - /// - internal static string SettingsPrefix { - get { - return ResourceManager.GetString("SettingsPrefix", resourceCulture); - } - } - - /// - /// 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. - /// - internal static string SettingsReceiveStartupMessages { - get { - return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remove roles on mute. - /// - internal static string SettingsRemoveRolesOnMute { - get { - return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); - } - } - - /// - /// 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. - /// - internal static string SettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Setting successfuly changed. - /// - internal static string SettingSuccessfulyChanged { - get { - return ResourceManager.GetString("SettingSuccessfulyChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Welcome message. - /// - internal static string SettingsWelcomeMessage { - get { - return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This user is already banned!. - /// - internal static string UserAlreadyBanned { - get { - return ResourceManager.GetString("UserAlreadyBanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This user is already muted!. - /// - internal static string UserAlreadyMuted { - get { - return ResourceManager.GetString("UserAlreadyMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} was banned. - /// - internal static string UserBanned { - get { - return ResourceManager.GetString("UserBanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban me!. - /// - internal static string UserCannotBanBot { - get { - return ResourceManager.GetString("UserCannotBanBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban users from this guild!. - /// - internal static string UserCannotBanMembers { - get { - return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban the owner of this guild!. - /// - internal static string UserCannotBanOwner { - get { - return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban this user!. - /// - internal static string UserCannotBanTarget { - get { - return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot ban yourself!. - /// - internal static string UserCannotBanThemselves { - get { - return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick me!. - /// - internal static string UserCannotKickBot { - get { - return ResourceManager.GetString("UserCannotKickBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick members from this guild!. - /// - internal static string UserCannotKickMembers { - get { - return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick the owner of this guild!. - /// - internal static string UserCannotKickOwner { - get { - return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick this member!. - /// - internal static string UserCannotKickTarget { - get { - return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot kick yourself!. - /// - internal static string UserCannotKickThemselves { - get { - return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot manage this guild!. - /// - internal static string UserCannotManageGuild { - get { - return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot manage messages in this guild!. - /// - internal static string UserCannotManageMessages { - get { - return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot moderate members in this guild!. - /// - internal static string UserCannotModerateMembers { - get { - return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute me!. - /// - internal static string UserCannotMuteBot { - get { - return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute the owner of this guild!. - /// - internal static string UserCannotMuteOwner { - get { - return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute this member!. - /// - internal static string UserCannotMuteTarget { - get { - return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot mute yourself!. - /// - internal static string UserCannotMuteThemselves { - get { - return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to .... - /// - internal static string UserCannotUnmuteBot { - get { - return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You don't need to unmute the owner of this guild!. - /// - internal static string UserCannotUnmuteOwner { - get { - return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You cannot unmute this user!. - /// - internal static string UserCannotUnmuteTarget { - get { - return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You are muted!. - /// - internal static string UserCannotUnmuteThemselves { - get { - return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} was kicked. - /// - internal static string UserKicked { - get { - return ResourceManager.GetString("UserKicked", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} was muted. - /// - internal static string UserMuted { - get { - return ResourceManager.GetString("UserMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This user is not banned!. - /// - internal static string UserNotBanned { - get { - return ResourceManager.GetString("UserNotBanned", resourceCulture); - } - } - - /// - /// 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 UserNotFound { - get { - return ResourceManager.GetString("UserNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to I could not find this user!. - /// - internal static string UserNotFoundShort { - get { - return ResourceManager.GetString("UserNotFoundShort", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This member is not muted!. - /// - internal static string UserNotMuted { - get { - return ResourceManager.GetString("UserNotMuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} was unbanned. - /// - internal static string UserUnbanned { - get { - return ResourceManager.GetString("UserUnbanned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} was unmuted. - /// - internal static string UserUnmuted { - get { - return ResourceManager.GetString("UserUnmuted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yes. - /// - internal static string Yes { - get { - return ResourceManager.GetString("Yes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You were banned. - /// internal static string YouWereBanned { get { return ResourceManager.GetString("YouWereBanned", resourceCulture); } } - /// - /// Looks up a localized string similar to You were kicked. - /// + internal static string PunishmentExpired { + get { + return ResourceManager.GetString("PunishmentExpired", resourceCulture); + } + } + + internal static string ClearAmountTooSmall { + get { + return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); + } + } + + internal static string ClearAmountTooLarge { + get { + return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); + } + } + + internal static string CommandHelp { + get { + return ResourceManager.GetString("CommandHelp", resourceCulture); + } + } + internal static string YouWereKicked { get { return ResourceManager.GetString("YouWereKicked", resourceCulture); } } + + internal static string Milliseconds { + get { + return ResourceManager.GetString("Milliseconds", resourceCulture); + } + } + + internal static string MemberAlreadyMuted { + get { + return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); + } + } + + internal static string ChannelNotSpecified { + get { + return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); + } + } + + internal static string RoleNotSpecified { + get { + return ResourceManager.GetString("RoleNotSpecified", resourceCulture); + } + } + + internal static string CurrentSettings { + get { + return ResourceManager.GetString("CurrentSettings", resourceCulture); + } + } + + internal static string SettingsLang { + get { + return ResourceManager.GetString("SettingsLang", resourceCulture); + } + } + + internal static string SettingsPrefix { + get { + return ResourceManager.GetString("SettingsPrefix", resourceCulture); + } + } + + internal static string SettingsRemoveRolesOnMute { + get { + return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); + } + } + + internal static string SettingsSendWelcomeMessages { + get { + return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); + } + } + + internal static string SettingsMuteRole { + get { + return ResourceManager.GetString("SettingsMuteRole", resourceCulture); + } + } + + internal static string LanguageNotSupported { + get { + return ResourceManager.GetString("LanguageNotSupported", resourceCulture); + } + } + + internal static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } + + internal static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + + internal static string UserNotBanned { + get { + return ResourceManager.GetString("UserNotBanned", resourceCulture); + } + } + + internal static string MemberNotMuted { + get { + return ResourceManager.GetString("MemberNotMuted", resourceCulture); + } + } + + internal static string SettingsWelcomeMessage { + get { + return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); + } + } + + internal static string ClearAmountInvalid { + get { + return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); + } + } + + internal static string UserBanned { + get { + return ResourceManager.GetString("UserBanned", resourceCulture); + } + } + + internal static string SettingDoesntExist { + get { + return ResourceManager.GetString("SettingDoesntExist", resourceCulture); + } + } + + internal static string SettingsReceiveStartupMessages { + get { + return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); + } + } + + internal static string InvalidSettingValue { + get { + return ResourceManager.GetString("InvalidSettingValue", resourceCulture); + } + } + + internal static string InvalidRole { + get { + return ResourceManager.GetString("InvalidRole", resourceCulture); + } + } + + internal static string InvalidChannel { + get { + return ResourceManager.GetString("InvalidChannel", resourceCulture); + } + } + + internal static string DurationRequiredForTimeOuts { + get { + return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); + } + } + + internal static string CannotTimeOutBot { + get { + return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); + } + } + + internal static string EventCreated { + get { + return ResourceManager.GetString("EventCreated", resourceCulture); + } + } + + internal static string SettingsEventNotificationRole { + get { + return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); + } + } + + internal static string SettingsEventNotificationChannel { + get { + return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); + } + } + + internal static string SettingsEventStartedReceivers { + get { + return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); + } + } + + internal static string EventStarted { + get { + return ResourceManager.GetString("EventStarted", resourceCulture); + } + } + + internal static string SettingsFrowningFace { + get { + return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); + } + } + + internal static string EventCancelled { + get { + return ResourceManager.GetString("EventCancelled", resourceCulture); + } + } + + internal static string EventCompleted { + get { + return ResourceManager.GetString("EventCompleted", resourceCulture); + } + } + + internal static string Ever { + get { + return ResourceManager.GetString("Ever", resourceCulture); + } + } + + internal static string MessagesCleared { + get { + return ResourceManager.GetString("MessagesCleared", resourceCulture); + } + } + + internal static string FeedbackMemberKicked { + get { + return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); + } + } + + internal static string FeedbackMemberMuted { + get { + return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); + } + } + + internal static string FeedbackUserUnbanned { + get { + return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); + } + } + + internal static string FeedbackMemberUnmuted { + get { + return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); + } + } + + internal static string SettingsNothingChanged { + get { + return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); + } + } + + internal static string SettingNotDefined { + get { + return ResourceManager.GetString("SettingNotDefined", resourceCulture); + } + } + + internal static string FeedbackSettingsUpdated { + get { + return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); + } + } + + internal static string CommandDescriptionBan { + get { + return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); + } + } + + internal static string CommandDescriptionClear { + get { + return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); + } + } + + internal static string CommandDescriptionHelp { + get { + return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); + } + } + + internal static string CommandDescriptionKick { + get { + return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); + } + } + + internal static string CommandDescriptionMute { + get { + return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); + } + } + + internal static string CommandDescriptionPing { + get { + return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); + } + } + + internal static string CommandDescriptionSettings { + get { + return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); + } + } + + internal static string CommandDescriptionUnban { + get { + return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); + } + } + + internal static string CommandDescriptionUnmute { + get { + return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); + } + } + + internal static string MissingNumber { + get { + return ResourceManager.GetString("MissingNumber", resourceCulture); + } + } + + internal static string MissingUser { + get { + return ResourceManager.GetString("MissingUser", resourceCulture); + } + } + + internal static string InvalidUser { + get { + return ResourceManager.GetString("InvalidUser", resourceCulture); + } + } + + internal static string MissingMember { + get { + return ResourceManager.GetString("MissingMember", resourceCulture); + } + } + + internal static string InvalidMember { + get { + return ResourceManager.GetString("InvalidMember", resourceCulture); + } + } + + internal static string UserCannotBanMembers { + get { + return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); + } + } + + internal static string UserCannotManageMessages { + get { + return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); + } + } + + internal static string UserCannotKickMembers { + get { + return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); + } + } + + internal static string UserCannotModerateMembers { + get { + return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); + } + } + + internal static string UserCannotManageGuild { + get { + return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); + } + } + + internal static string BotCannotBanMembers { + get { + return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); + } + } + + internal static string BotCannotManageMessages { + get { + return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); + } + } + + internal static string BotCannotKickMembers { + get { + return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); + } + } + + internal static string BotCannotModerateMembers { + get { + return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); + } + } + + internal static string BotCannotManageGuild { + get { + return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); + } + } + + internal static string MissingBanReason { + get { + return ResourceManager.GetString("MissingBanReason", resourceCulture); + } + } + + internal static string MissingKickReason { + get { + return ResourceManager.GetString("MissingKickReason", resourceCulture); + } + } + + internal static string MissingMuteReason { + get { + return ResourceManager.GetString("MissingMuteReason", resourceCulture); + } + } + + internal static string MissingUnbanReason { + get { + return ResourceManager.GetString("MissingUnbanReason", resourceCulture); + } + } + + internal static string MissingUnmuteReason { + get { + return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); + } + } + + internal static string UserCannotBanOwner { + get { + return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); + } + } + + internal static string UserCannotBanThemselves { + get { + return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); + } + } + + internal static string UserCannotBanBot { + get { + return ResourceManager.GetString("UserCannotBanBot", resourceCulture); + } + } + + internal static string BotCannotBanTarget { + get { + return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); + } + } + + internal static string UserCannotBanTarget { + get { + return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); + } + } + + internal static string UserCannotKickOwner { + get { + return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); + } + } + + internal static string UserCannotKickThemselves { + get { + return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); + } + } + + internal static string UserCannotKickBot { + get { + return ResourceManager.GetString("UserCannotKickBot", resourceCulture); + } + } + + internal static string BotCannotKickTarget { + get { + return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); + } + } + + internal static string UserCannotKickTarget { + get { + return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); + } + } + + internal static string UserCannotMuteOwner { + get { + return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); + } + } + + internal static string UserCannotMuteThemselves { + get { + return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); + } + } + + internal static string UserCannotMuteBot { + get { + return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); + } + } + + internal static string BotCannotMuteTarget { + get { + return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); + } + } + + internal static string UserCannotMuteTarget { + get { + return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); + } + } + + internal static string UserCannotUnmuteOwner { + get { + return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); + } + } + + internal static string UserCannotUnmuteThemselves { + get { + return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); + } + } + + internal static string UserCannotUnmuteBot { + get { + return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); + } + } + + internal static string BotCannotUnmuteTarget { + get { + return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); + } + } + + internal static string UserCannotUnmuteTarget { + get { + return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); + } + } + + internal static string EventEarlyNotification { + get { + return ResourceManager.GetString("EventEarlyNotification", resourceCulture); + } + } + + internal static string SettingsEventEarlyNotificationOffset { + get { + return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); + } + } + + internal static string UserNotFound { + get { + return ResourceManager.GetString("UserNotFound", resourceCulture); + } + } + + internal static string SettingsDefaultRole { + get { + return ResourceManager.GetString("SettingsDefaultRole", resourceCulture); + } + } + + internal static string CommandDescriptionRemind { + get { + return ResourceManager.GetString("CommandDescriptionRemind", resourceCulture); + } + } + + internal static string SettingsPublicFeedbackChannel { + get { + return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture); + } + } + + internal static string SettingsPrivateFeedbackChannel { + get { + return ResourceManager.GetString("SettingsPrivateFeedbackChannel", resourceCulture); + } + } + + internal static string SettingsReturnRolesOnRejoin { + get { + return ResourceManager.GetString("SettingsReturnRolesOnRejoin", resourceCulture); + } + } + + internal static string SettingsAutoStartEvents { + get { + return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); + } + } + + internal static string MissingReminderText { + get { + return ResourceManager.GetString("MissingReminderText", resourceCulture); + } + } + + internal static string DescriptionReminderCreated { + get { + return ResourceManager.GetString("DescriptionReminderCreated", resourceCulture); + } + } + + internal static string InvalidRemindIn { + get { + return ResourceManager.GetString("InvalidRemindIn", resourceCulture); + } + } + + internal static string IssuedBy { + get { + return ResourceManager.GetString("IssuedBy", resourceCulture); + } + } + + internal static string EventCreatedTitle { + get { + return ResourceManager.GetString("EventCreatedTitle", resourceCulture); + } + } + + internal static string DescriptionLocalEventCreated { + get { + return ResourceManager.GetString("DescriptionLocalEventCreated", resourceCulture); + } + } + + internal static string DescriptionExternalEventCreated { + get { + return ResourceManager.GetString("DescriptionExternalEventCreated", resourceCulture); + } + } + + internal static string EventDetailsButton { + get { + return ResourceManager.GetString("EventDetailsButton", resourceCulture); + } + } + + internal static string EventDuration { + get { + return ResourceManager.GetString("EventDuration", resourceCulture); + } + } + + internal static string DescriptionLocalEventStarted { + get { + return ResourceManager.GetString("DescriptionLocalEventStarted", resourceCulture); + } + } + + internal static string DescriptionExternalEventStarted { + get { + return ResourceManager.GetString("DescriptionExternalEventStarted", resourceCulture); + } + } + + internal static string UserAlreadyBanned { + get { + return ResourceManager.GetString("UserAlreadyBanned", resourceCulture); + } + } + + internal static string UserUnbanned { + get { + return ResourceManager.GetString("UserUnbanned", resourceCulture); + } + } + + internal static string UserMuted { + get { + return ResourceManager.GetString("UserMuted", resourceCulture); + } + } + + internal static string UserUnmuted { + get { + return ResourceManager.GetString("UserUnmuted", resourceCulture); + } + } + + internal static string UserNotMuted { + get { + return ResourceManager.GetString("UserNotMuted", resourceCulture); + } + } + + internal static string UserNotFoundShort { + get { + return ResourceManager.GetString("UserNotFoundShort", resourceCulture); + } + } + + internal static string UserKicked { + get { + return ResourceManager.GetString("UserKicked", resourceCulture); + } + } + + internal static string DescriptionActionReason { + get { + return ResourceManager.GetString("DescriptionActionReason", resourceCulture); + } + } + + internal static string DescriptionActionExpiresAt { + get { + return ResourceManager.GetString("DescriptionActionExpiresAt", resourceCulture); + } + } + + internal static string UserAlreadyMuted { + get { + return ResourceManager.GetString("UserAlreadyMuted", resourceCulture); + } + } + + internal static string MessageFrom { + get { + return ResourceManager.GetString("MessageFrom", resourceCulture); + } + } + + internal static string AboutTitleDevelopers { + get { + return ResourceManager.GetString("AboutTitleDevelopers", resourceCulture); + } + } + + internal static string AboutTitleWiki { + get { + return ResourceManager.GetString("AboutTitleWiki", resourceCulture); + } + } + + internal static string AboutBot { + get { + return ResourceManager.GetString("AboutBot", resourceCulture); + } + } + + internal static string AboutDeveloper_mctaylors { + get { + return ResourceManager.GetString("AboutDeveloper@mctaylors", resourceCulture); + } + } + + internal static string AboutDeveloper_Octol1ttle { + get { + return ResourceManager.GetString("AboutDeveloper@Octol1ttle", resourceCulture); + } + } + + internal static string AboutDeveloper_neroduckale { + get { + return ResourceManager.GetString("AboutDeveloper@neroduckale", resourceCulture); + } + } + + internal static string ReminderCreated { + get { + return ResourceManager.GetString("ReminderCreated", resourceCulture); + } + } + + internal static string Reminder { + get { + return ResourceManager.GetString("Reminder", resourceCulture); + } + } + + internal static string DescriptionReminder { + get { + return ResourceManager.GetString("DescriptionReminder", resourceCulture); + } + } + + internal static string SettingsListTitle { + get { + return ResourceManager.GetString("SettingsListTitle", resourceCulture); + } + } + + internal static string SettingSuccessfullyChanged { + get { + return ResourceManager.GetString("SettingSuccessfullyChanged", resourceCulture); + } + } + + internal static string SettingNotChanged { + get { + return ResourceManager.GetString("SettingNotChanged", resourceCulture); + } + } + + internal static string SettingIsNow { + get { + return ResourceManager.GetString("SettingIsNow", resourceCulture); + } + } } } diff --git a/Messages.resx b/Messages.resx index 0bb7ab5..c2988e0 100644 --- a/Messages.resx +++ b/Messages.resx @@ -549,8 +549,8 @@ Boyfriend's Settings - - Setting successfuly changed + + Setting successfully changed Setting not changed diff --git a/Messages.ru.resx b/Messages.ru.resx index 1351ea1..2173cb0 100644 --- a/Messages.ru.resx +++ b/Messages.ru.resx @@ -549,7 +549,7 @@ Настройки Boyfriend - + Настройка успешно изменена diff --git a/Messages.tt-ru.resx b/Messages.tt-ru.resx index e4a75be..6e5666c 100644 --- a/Messages.tt-ru.resx +++ b/Messages.tt-ru.resx @@ -549,7 +549,7 @@ приколы Boyfriend - + прикол редактирован diff --git a/docs/README.md b/docs/README.md index 45f6a92..5a4fe26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -43,6 +43,6 @@ the most effective way possible. [JetBrains](https://www.jetbrains.com/), creators of [ReSharper](https://www.jetbrains.com/resharper) and [Rider](https://www.jetbrains.com/rider), supports Boyfriend with one of their [Open Source Licenses](https://jb.gg/OpenSourceSupport). -Rider is the recommended IDE when working with Boyfriend, and most of the Boyfriend team uses it. +Rider is the recommended IDE when working with Boyfriend, and everyone on the Boyfriend team uses it. Additionally, ReSharper command-line tools made by JetBrains are used for status checks on pull requests to ensure code quality even when not using ReSharper or Rider. From 3eb17b96c5817240f9861b1b323c4f608478f2fa Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 9 Jul 2023 22:36:44 +0500 Subject: [PATCH 097/329] Tidy up project structure, fix bug with edit logging (#47) The project structure has been changed because the previous one had everything in 1 folder. From this PR onwards, the following is true: - The source code is stored in `src/` - `*.resx` and `Messages.Designer.cs` is stored in `locale/` - Documentation is stored on the wiki and in `docs/` - Miscellaneous files, such as dotfiles, are stored in the root folder of the repository This PR additionally fixes an issue that would cause logs of edited messages to not be syntax highlighted. This happened because the responder of edited messages was changed to use the universal `InBlockCode` extension method which did not support syntax highlighting until this PR This PR additionally changes CODEOWNERS to be more reliable. Previously, it would be possible for some PRs to be unable to be approved because the only person who can approve them is the same person who opened the PR. --------- Signed-off-by: Octol1ttle --- .github/CODEOWNERS | 4 +- Boyfriend.csproj | 5 +- docs/README.md | 2 +- .../Messages.Designer.cs | 318 +++++++++--------- Messages.resx => locale/Messages.resx | 0 Messages.ru.resx => locale/Messages.ru.resx | 0 .../Messages.tt-ru.resx | 0 Boyfriend.cs => src/Boyfriend.cs | 0 ColorsList.cs => src/ColorsList.cs | 0 .../Commands}/AboutCommandGroup.cs | 4 +- {Commands => src/Commands}/BanCommandGroup.cs | 12 +- .../Commands}/ClearCommandGroup.cs | 0 .../Commands}/ErrorLoggingEvents.cs | 0 .../Commands}/KickCommandGroup.cs | 0 .../Commands}/MuteCommandGroup.cs | 0 .../Commands}/PingCommandGroup.cs | 0 .../Commands}/RemindCommandGroup.cs | 0 .../Commands}/SettingsCommandGroup.cs | 0 {Data => src/Data}/GuildConfiguration.cs | 0 {Data => src/Data}/GuildData.cs | 0 {Data => src/Data}/MemberData.cs | 0 {Data => src/Data}/Reminder.cs | 0 {Data => src/Data}/ScheduledEventData.cs | 0 EventResponders.cs => src/EventResponders.cs | 0 Extensions.cs => src/Extensions.cs | 14 +- .../InteractionResponders.cs | 0 .../Services}/GuildDataService.cs | 0 .../Services}/GuildUpdateService.cs | 0 {Services => src/Services}/UtilityService.cs | 0 29 files changed, 180 insertions(+), 179 deletions(-) rename Messages.Designer.cs => locale/Messages.Designer.cs (95%) rename Messages.resx => locale/Messages.resx (100%) rename Messages.ru.resx => locale/Messages.ru.resx (100%) rename Messages.tt-ru.resx => locale/Messages.tt-ru.resx (100%) rename Boyfriend.cs => src/Boyfriend.cs (100%) rename ColorsList.cs => src/ColorsList.cs (100%) rename {Commands => src/Commands}/AboutCommandGroup.cs (95%) rename {Commands => src/Commands}/BanCommandGroup.cs (97%) rename {Commands => src/Commands}/ClearCommandGroup.cs (100%) rename {Commands => src/Commands}/ErrorLoggingEvents.cs (100%) rename {Commands => src/Commands}/KickCommandGroup.cs (100%) rename {Commands => src/Commands}/MuteCommandGroup.cs (100%) rename {Commands => src/Commands}/PingCommandGroup.cs (100%) rename {Commands => src/Commands}/RemindCommandGroup.cs (100%) rename {Commands => src/Commands}/SettingsCommandGroup.cs (100%) rename {Data => src/Data}/GuildConfiguration.cs (100%) rename {Data => src/Data}/GuildData.cs (100%) rename {Data => src/Data}/MemberData.cs (100%) rename {Data => src/Data}/Reminder.cs (100%) rename {Data => src/Data}/ScheduledEventData.cs (100%) rename EventResponders.cs => src/EventResponders.cs (100%) rename Extensions.cs => src/Extensions.cs (92%) rename InteractionResponders.cs => src/InteractionResponders.cs (100%) rename {Services => src/Services}/GuildDataService.cs (100%) rename {Services => src/Services}/GuildUpdateService.cs (100%) rename {Services => src/Services}/UtilityService.cs (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4c792a0..eeccdc0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,2 @@ * @TeamOctolings/boyfriend -.github/CODEOWNERS @TeamOctolings/boyfriend-admins -/docs/ @mctaylors -Messages.tt-ru.resx @mctaylors +/docs/ @TeamOctolings/boyfriend-docs diff --git a/Boyfriend.csproj b/Boyfriend.csproj index a9af80e..f6cc7dc 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -23,8 +23,9 @@ - - + + + ResXFileCodeGenerator Messages.Designer.cs diff --git a/docs/README.md b/docs/README.md index 5a4fe26..21ffbbf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ ![CodeFactor](https://img.shields.io/codefactor/grade/github/TeamOctolings/Boyfriend) Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and -Discord.Net +Remora.Discord ## Features diff --git a/Messages.Designer.cs b/locale/Messages.Designer.cs similarity index 95% rename from Messages.Designer.cs rename to locale/Messages.Designer.cs index aaf5bc1..de6955b 100644 --- a/Messages.Designer.cs +++ b/locale/Messages.Designer.cs @@ -9,32 +9,32 @@ namespace Boyfriend { using System; - - + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - + private static System.Resources.ResourceManager resourceMan; - + private static System.Globalization.CultureInfo resourceCulture; - + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Resources.ResourceManager ResourceManager { get { if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.Messages", typeof(Messages).Assembly); + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.locale.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Globalization.CultureInfo Culture { get { @@ -44,907 +44,907 @@ namespace Boyfriend { resourceCulture = value; } } - + internal static string Ready { get { return ResourceManager.GetString("Ready", resourceCulture); } } - + internal static string CachedMessageDeleted { get { return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - + internal static string CachedMessageCleared { get { return ResourceManager.GetString("CachedMessageCleared", resourceCulture); } } - + internal static string CachedMessageEdited { get { return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - + internal static string DefaultWelcomeMessage { get { return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); } } - + internal static string Beep1 { get { return ResourceManager.GetString("Beep1", resourceCulture); } } - + internal static string Beep2 { get { return ResourceManager.GetString("Beep2", resourceCulture); } } - + internal static string Beep3 { get { return ResourceManager.GetString("Beep3", resourceCulture); } } - + internal static string CommandNoPermissionBot { get { return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); } } - + internal static string CommandNoPermissionUser { get { return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); } } - + internal static string YouWereBanned { get { return ResourceManager.GetString("YouWereBanned", resourceCulture); } } - + internal static string PunishmentExpired { get { return ResourceManager.GetString("PunishmentExpired", resourceCulture); } } - + internal static string ClearAmountTooSmall { get { return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); } } - + internal static string ClearAmountTooLarge { get { return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); } } - + internal static string CommandHelp { get { return ResourceManager.GetString("CommandHelp", resourceCulture); } } - + internal static string YouWereKicked { get { return ResourceManager.GetString("YouWereKicked", resourceCulture); } } - + internal static string Milliseconds { get { return ResourceManager.GetString("Milliseconds", resourceCulture); } } - + internal static string MemberAlreadyMuted { get { return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); } } - + internal static string ChannelNotSpecified { get { return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); } } - + internal static string RoleNotSpecified { get { return ResourceManager.GetString("RoleNotSpecified", resourceCulture); } } - + internal static string CurrentSettings { get { return ResourceManager.GetString("CurrentSettings", resourceCulture); } } - + internal static string SettingsLang { get { return ResourceManager.GetString("SettingsLang", resourceCulture); } } - + internal static string SettingsPrefix { get { return ResourceManager.GetString("SettingsPrefix", resourceCulture); } } - + internal static string SettingsRemoveRolesOnMute { get { return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); } } - + internal static string SettingsSendWelcomeMessages { get { return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); } } - + internal static string SettingsMuteRole { get { return ResourceManager.GetString("SettingsMuteRole", resourceCulture); } } - + internal static string LanguageNotSupported { get { return ResourceManager.GetString("LanguageNotSupported", resourceCulture); } } - + internal static string Yes { get { return ResourceManager.GetString("Yes", resourceCulture); } } - + internal static string No { get { return ResourceManager.GetString("No", resourceCulture); } } - + internal static string UserNotBanned { get { return ResourceManager.GetString("UserNotBanned", resourceCulture); } } - + internal static string MemberNotMuted { get { return ResourceManager.GetString("MemberNotMuted", resourceCulture); } } - + internal static string SettingsWelcomeMessage { get { return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); } } - + internal static string ClearAmountInvalid { get { return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); } } - + internal static string UserBanned { get { return ResourceManager.GetString("UserBanned", resourceCulture); } } - + internal static string SettingDoesntExist { get { return ResourceManager.GetString("SettingDoesntExist", resourceCulture); } } - + internal static string SettingsReceiveStartupMessages { get { return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); } } - + internal static string InvalidSettingValue { get { return ResourceManager.GetString("InvalidSettingValue", resourceCulture); } } - + internal static string InvalidRole { get { return ResourceManager.GetString("InvalidRole", resourceCulture); } } - + internal static string InvalidChannel { get { return ResourceManager.GetString("InvalidChannel", resourceCulture); } } - + internal static string DurationRequiredForTimeOuts { get { return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); } } - + internal static string CannotTimeOutBot { get { return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); } } - + internal static string EventCreated { get { return ResourceManager.GetString("EventCreated", resourceCulture); } } - + internal static string SettingsEventNotificationRole { get { return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); } } - + internal static string SettingsEventNotificationChannel { get { return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); } } - + internal static string SettingsEventStartedReceivers { get { return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); } } - + internal static string EventStarted { get { return ResourceManager.GetString("EventStarted", resourceCulture); } } - + internal static string SettingsFrowningFace { get { return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); } } - + internal static string EventCancelled { get { return ResourceManager.GetString("EventCancelled", resourceCulture); } } - + internal static string EventCompleted { get { return ResourceManager.GetString("EventCompleted", resourceCulture); } } - + internal static string Ever { get { return ResourceManager.GetString("Ever", resourceCulture); } } - + internal static string MessagesCleared { get { return ResourceManager.GetString("MessagesCleared", resourceCulture); } } - + internal static string FeedbackMemberKicked { get { return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); } } - + internal static string FeedbackMemberMuted { get { return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); } } - + internal static string FeedbackUserUnbanned { get { return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); } } - + internal static string FeedbackMemberUnmuted { get { return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); } } - + internal static string SettingsNothingChanged { get { return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); } } - + internal static string SettingNotDefined { get { return ResourceManager.GetString("SettingNotDefined", resourceCulture); } } - + internal static string FeedbackSettingsUpdated { get { return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); } } - + internal static string CommandDescriptionBan { get { return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); } } - + internal static string CommandDescriptionClear { get { return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); } } - + internal static string CommandDescriptionHelp { get { return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); } } - + internal static string CommandDescriptionKick { get { return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); } } - + internal static string CommandDescriptionMute { get { return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); } } - + internal static string CommandDescriptionPing { get { return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); } } - + internal static string CommandDescriptionSettings { get { return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); } } - + internal static string CommandDescriptionUnban { get { return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); } } - + internal static string CommandDescriptionUnmute { get { return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); } } - + internal static string MissingNumber { get { return ResourceManager.GetString("MissingNumber", resourceCulture); } } - + internal static string MissingUser { get { return ResourceManager.GetString("MissingUser", resourceCulture); } } - + internal static string InvalidUser { get { return ResourceManager.GetString("InvalidUser", resourceCulture); } } - + internal static string MissingMember { get { return ResourceManager.GetString("MissingMember", resourceCulture); } } - + internal static string InvalidMember { get { return ResourceManager.GetString("InvalidMember", resourceCulture); } } - + internal static string UserCannotBanMembers { get { return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); } } - + internal static string UserCannotManageMessages { get { return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); } } - + internal static string UserCannotKickMembers { get { return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); } } - + internal static string UserCannotModerateMembers { get { return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); } } - + internal static string UserCannotManageGuild { get { return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); } } - + internal static string BotCannotBanMembers { get { return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); } } - + internal static string BotCannotManageMessages { get { return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); } } - + internal static string BotCannotKickMembers { get { return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); } } - + internal static string BotCannotModerateMembers { get { return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); } } - + internal static string BotCannotManageGuild { get { return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); } } - + internal static string MissingBanReason { get { return ResourceManager.GetString("MissingBanReason", resourceCulture); } } - + internal static string MissingKickReason { get { return ResourceManager.GetString("MissingKickReason", resourceCulture); } } - + internal static string MissingMuteReason { get { return ResourceManager.GetString("MissingMuteReason", resourceCulture); } } - + internal static string MissingUnbanReason { get { return ResourceManager.GetString("MissingUnbanReason", resourceCulture); } } - + internal static string MissingUnmuteReason { get { return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); } } - + internal static string UserCannotBanOwner { get { return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); } } - + internal static string UserCannotBanThemselves { get { return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); } } - + internal static string UserCannotBanBot { get { return ResourceManager.GetString("UserCannotBanBot", resourceCulture); } } - + internal static string BotCannotBanTarget { get { return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); } } - + internal static string UserCannotBanTarget { get { return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); } } - + internal static string UserCannotKickOwner { get { return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); } } - + internal static string UserCannotKickThemselves { get { return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); } } - + internal static string UserCannotKickBot { get { return ResourceManager.GetString("UserCannotKickBot", resourceCulture); } } - + internal static string BotCannotKickTarget { get { return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); } } - + internal static string UserCannotKickTarget { get { return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); } } - + internal static string UserCannotMuteOwner { get { return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); } } - + internal static string UserCannotMuteThemselves { get { return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); } } - + internal static string UserCannotMuteBot { get { return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); } } - + internal static string BotCannotMuteTarget { get { return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); } } - + internal static string UserCannotMuteTarget { get { return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); } } - + internal static string UserCannotUnmuteOwner { get { return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); } } - + internal static string UserCannotUnmuteThemselves { get { return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); } } - + internal static string UserCannotUnmuteBot { get { return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); } } - + internal static string BotCannotUnmuteTarget { get { return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); } } - + internal static string UserCannotUnmuteTarget { get { return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); } } - + internal static string EventEarlyNotification { get { return ResourceManager.GetString("EventEarlyNotification", resourceCulture); } } - + internal static string SettingsEventEarlyNotificationOffset { get { return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); } } - + internal static string UserNotFound { get { return ResourceManager.GetString("UserNotFound", resourceCulture); } } - + internal static string SettingsDefaultRole { get { return ResourceManager.GetString("SettingsDefaultRole", resourceCulture); } } - + internal static string CommandDescriptionRemind { get { return ResourceManager.GetString("CommandDescriptionRemind", resourceCulture); } } - + internal static string SettingsPublicFeedbackChannel { get { return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture); } } - + internal static string SettingsPrivateFeedbackChannel { get { return ResourceManager.GetString("SettingsPrivateFeedbackChannel", resourceCulture); } } - + internal static string SettingsReturnRolesOnRejoin { get { return ResourceManager.GetString("SettingsReturnRolesOnRejoin", resourceCulture); } } - + internal static string SettingsAutoStartEvents { get { return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); } } - + internal static string MissingReminderText { get { return ResourceManager.GetString("MissingReminderText", resourceCulture); } } - + internal static string DescriptionReminderCreated { get { return ResourceManager.GetString("DescriptionReminderCreated", resourceCulture); } } - + internal static string InvalidRemindIn { get { return ResourceManager.GetString("InvalidRemindIn", resourceCulture); } } - + internal static string IssuedBy { get { return ResourceManager.GetString("IssuedBy", resourceCulture); } } - + internal static string EventCreatedTitle { get { return ResourceManager.GetString("EventCreatedTitle", resourceCulture); } } - + internal static string DescriptionLocalEventCreated { get { return ResourceManager.GetString("DescriptionLocalEventCreated", resourceCulture); } } - + internal static string DescriptionExternalEventCreated { get { return ResourceManager.GetString("DescriptionExternalEventCreated", resourceCulture); } } - + internal static string EventDetailsButton { get { return ResourceManager.GetString("EventDetailsButton", resourceCulture); } } - + internal static string EventDuration { get { return ResourceManager.GetString("EventDuration", resourceCulture); } } - + internal static string DescriptionLocalEventStarted { get { return ResourceManager.GetString("DescriptionLocalEventStarted", resourceCulture); } } - + internal static string DescriptionExternalEventStarted { get { return ResourceManager.GetString("DescriptionExternalEventStarted", resourceCulture); } } - + internal static string UserAlreadyBanned { get { return ResourceManager.GetString("UserAlreadyBanned", resourceCulture); } } - + internal static string UserUnbanned { get { return ResourceManager.GetString("UserUnbanned", resourceCulture); } } - + internal static string UserMuted { get { return ResourceManager.GetString("UserMuted", resourceCulture); } } - + internal static string UserUnmuted { get { return ResourceManager.GetString("UserUnmuted", resourceCulture); } } - + internal static string UserNotMuted { get { return ResourceManager.GetString("UserNotMuted", resourceCulture); } } - + internal static string UserNotFoundShort { get { return ResourceManager.GetString("UserNotFoundShort", resourceCulture); } } - + internal static string UserKicked { get { return ResourceManager.GetString("UserKicked", resourceCulture); } } - + internal static string DescriptionActionReason { get { return ResourceManager.GetString("DescriptionActionReason", resourceCulture); } } - + internal static string DescriptionActionExpiresAt { get { return ResourceManager.GetString("DescriptionActionExpiresAt", resourceCulture); } } - + internal static string UserAlreadyMuted { get { return ResourceManager.GetString("UserAlreadyMuted", resourceCulture); } } - + internal static string MessageFrom { get { return ResourceManager.GetString("MessageFrom", resourceCulture); } } - + internal static string AboutTitleDevelopers { get { return ResourceManager.GetString("AboutTitleDevelopers", resourceCulture); } } - + internal static string AboutTitleWiki { get { return ResourceManager.GetString("AboutTitleWiki", resourceCulture); } } - + internal static string AboutBot { get { return ResourceManager.GetString("AboutBot", resourceCulture); } } - + internal static string AboutDeveloper_mctaylors { get { return ResourceManager.GetString("AboutDeveloper@mctaylors", resourceCulture); } } - + internal static string AboutDeveloper_Octol1ttle { get { return ResourceManager.GetString("AboutDeveloper@Octol1ttle", resourceCulture); } } - + internal static string AboutDeveloper_neroduckale { get { return ResourceManager.GetString("AboutDeveloper@neroduckale", resourceCulture); } } - + internal static string ReminderCreated { get { return ResourceManager.GetString("ReminderCreated", resourceCulture); } } - + internal static string Reminder { get { return ResourceManager.GetString("Reminder", resourceCulture); } } - + internal static string DescriptionReminder { get { return ResourceManager.GetString("DescriptionReminder", resourceCulture); } } - + internal static string SettingsListTitle { get { return ResourceManager.GetString("SettingsListTitle", resourceCulture); } } - + internal static string SettingSuccessfullyChanged { get { return ResourceManager.GetString("SettingSuccessfullyChanged", resourceCulture); } } - + internal static string SettingNotChanged { get { return ResourceManager.GetString("SettingNotChanged", resourceCulture); } } - + internal static string SettingIsNow { get { return ResourceManager.GetString("SettingIsNow", resourceCulture); diff --git a/Messages.resx b/locale/Messages.resx similarity index 100% rename from Messages.resx rename to locale/Messages.resx diff --git a/Messages.ru.resx b/locale/Messages.ru.resx similarity index 100% rename from Messages.ru.resx rename to locale/Messages.ru.resx diff --git a/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx similarity index 100% rename from Messages.tt-ru.resx rename to locale/Messages.tt-ru.resx diff --git a/Boyfriend.cs b/src/Boyfriend.cs similarity index 100% rename from Boyfriend.cs rename to src/Boyfriend.cs diff --git a/ColorsList.cs b/src/ColorsList.cs similarity index 100% rename from ColorsList.cs rename to src/ColorsList.cs diff --git a/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs similarity index 95% rename from Commands/AboutCommandGroup.cs rename to src/Commands/AboutCommandGroup.cs index e4820c6..5ff757e 100644 --- a/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -59,8 +59,8 @@ public class AboutCommandGroup : CommandGroup { builder.AppendLine($"@{dev} — {$"AboutDeveloper@{dev}".Localized()}"); builder.AppendLine() - .AppendLine(Markdown.Bold(Messages.AboutTitleWiki)) - .AppendLine("https://github.com/TeamOctolings/Boyfriend/wiki"); + .AppendLine(Markdown.Bold(Messages.AboutTitleWiki)) + .AppendLine("https://github.com/TeamOctolings/Boyfriend/wiki"); var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser) .WithDescription(builder.ToString()) diff --git a/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs similarity index 97% rename from Commands/BanCommandGroup.cs rename to src/Commands/BanCommandGroup.cs index a525afb..02e0fa2 100644 --- a/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -63,10 +63,9 @@ public class BanCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Ban user")] public async Task BanUserAsync( - [Description("User to ban")] IUser target, - [Description("Ban reason")] string reason, - [Description("Ban duration")] - TimeSpan? duration = null) { + [Description("User to ban")] IUser target, + [Description("Ban reason")] string reason, + [Description("Ban duration")] TimeSpan? duration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -199,9 +198,8 @@ public class BanCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Unban user")] public async Task UnbanUserAsync( - [Description("User to unban")] IUser target, - [Description("Unban reason")] - string reason) { + [Description("User to unban")] IUser target, + [Description("Unban reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); diff --git a/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs similarity index 100% rename from Commands/ClearCommandGroup.cs rename to src/Commands/ClearCommandGroup.cs diff --git a/Commands/ErrorLoggingEvents.cs b/src/Commands/ErrorLoggingEvents.cs similarity index 100% rename from Commands/ErrorLoggingEvents.cs rename to src/Commands/ErrorLoggingEvents.cs diff --git a/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs similarity index 100% rename from Commands/KickCommandGroup.cs rename to src/Commands/KickCommandGroup.cs diff --git a/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs similarity index 100% rename from Commands/MuteCommandGroup.cs rename to src/Commands/MuteCommandGroup.cs diff --git a/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs similarity index 100% rename from Commands/PingCommandGroup.cs rename to src/Commands/PingCommandGroup.cs diff --git a/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs similarity index 100% rename from Commands/RemindCommandGroup.cs rename to src/Commands/RemindCommandGroup.cs diff --git a/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs similarity index 100% rename from Commands/SettingsCommandGroup.cs rename to src/Commands/SettingsCommandGroup.cs diff --git a/Data/GuildConfiguration.cs b/src/Data/GuildConfiguration.cs similarity index 100% rename from Data/GuildConfiguration.cs rename to src/Data/GuildConfiguration.cs diff --git a/Data/GuildData.cs b/src/Data/GuildData.cs similarity index 100% rename from Data/GuildData.cs rename to src/Data/GuildData.cs diff --git a/Data/MemberData.cs b/src/Data/MemberData.cs similarity index 100% rename from Data/MemberData.cs rename to src/Data/MemberData.cs diff --git a/Data/Reminder.cs b/src/Data/Reminder.cs similarity index 100% rename from Data/Reminder.cs rename to src/Data/Reminder.cs diff --git a/Data/ScheduledEventData.cs b/src/Data/ScheduledEventData.cs similarity index 100% rename from Data/ScheduledEventData.cs rename to src/Data/ScheduledEventData.cs diff --git a/EventResponders.cs b/src/EventResponders.cs similarity index 100% rename from EventResponders.cs rename to src/EventResponders.cs diff --git a/Extensions.cs b/src/Extensions.cs similarity index 92% rename from Extensions.cs rename to src/Extensions.cs index 983aea2..95500ac 100644 --- a/Extensions.cs +++ b/src/Extensions.cs @@ -125,13 +125,17 @@ public static class Extensions { } /// - /// Sanitizes a string (see ) and formats the string with block code. + /// Sanitizes a string (see ) and formats the string to use Markdown Block Code formatting with a specified + /// language for syntax highlighting. /// /// The string to sanitize and format. - /// The sanitized string formatted with . - public static string InBlockCode(this string s) { + /// + /// The sanitized string formatted to use Markdown Block Code with a specified + /// language for syntax highlighting. + public static string InBlockCode(this string s, string language = "") { s = s.SanitizeForBlockCode(); - return $"```{s.SanitizeForBlockCode()}{(s.EndsWith("`") || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; + return + $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith("`") || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; } public static string Localized(this string key) { @@ -159,7 +163,7 @@ public static class Extensions { builder.AppendLine(line.Text); } - return InBlockCode(builder.ToString()); + return InBlockCode(builder.ToString(), "diff"); } public static string GetTag(this IUser user) { diff --git a/InteractionResponders.cs b/src/InteractionResponders.cs similarity index 100% rename from InteractionResponders.cs rename to src/InteractionResponders.cs diff --git a/Services/GuildDataService.cs b/src/Services/GuildDataService.cs similarity index 100% rename from Services/GuildDataService.cs rename to src/Services/GuildDataService.cs diff --git a/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs similarity index 100% rename from Services/GuildUpdateService.cs rename to src/Services/GuildUpdateService.cs diff --git a/Services/UtilityService.cs b/src/Services/UtilityService.cs similarity index 100% rename from Services/UtilityService.cs rename to src/Services/UtilityService.cs From c6dd3727c393ecfa1a0f703880bd6749714a5ae0 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:25:02 +0300 Subject: [PATCH 098/329] The Milestone Commit (#48) mctaylors: - updated readme 7 times (and only adding new logo from /about) - [removed](https://github.com/TeamOctolings/Boyfriend/pull/48/commits/aeeb3d4399c31df70b47ccdf59f6963fdb68e9ec) bot footer from created event embed on the second try - [changed](https://github.com/TeamOctolings/Boyfriend/pull/48/commits/4b9b91d9e4d2289d9aad4e600f5ca6a424638a6e) cdn from discord to upload.systems Octol1ttle: - Guild settings code has been overhauled. Instead of instances of a `GuildConfiguration` class being (de-)serialized when used with listing and setting options provided by reflection, there are now multiple `Option` classes responsible for the type of option they are storing. The classes support getting a value, validating and setting values with Results, and getting a user-friendly representation of these values. This makes use of polymorphism, providing clean and easier to use and refactor code. - Gateway event responders have been split into their own separate files, which should make it easier to find and modify responders when needed. - Warning suppressions regarding unused and never instantiated classes have been replaced by `[ImplicitUse]` annotations provided by `JetBrains.Annotations`. This avoids hiding real issues and provides a better way to suppress false warnings while being explicit. - It is no longer possible to execute some slash commands if they are run without the correct permissions - Dependencies are now more explicitly defined neroduckale: - Made easter eggs case-insensitive --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> Signed-off-by: Octol1ttle Co-authored-by: Octol1ttle Co-authored-by: nrdk --- .github/workflows/resharper.yml | 6 +- Boyfriend.csproj | 6 +- docs/README.md | 12 +- docs/assets/boyfriend.png | Bin 0 -> 229373 bytes src/Boyfriend.cs | 19 +- src/Commands/AboutCommandGroup.cs | 14 +- src/Commands/BanCommandGroup.cs | 53 +-- src/Commands/ClearCommandGroup.cs | 20 +- src/Commands/ErrorLoggingEvents.cs | 16 +- src/Commands/KickCommandGroup.cs | 30 +- src/Commands/MuteCommandGroup.cs | 53 +-- src/Commands/PingCommandGroup.cs | 11 +- src/Commands/RemindCommandGroup.cs | 12 +- src/Commands/SettingsCommandGroup.cs | 96 ++--- src/Data/GuildConfiguration.cs | 90 ----- src/Data/GuildData.cs | 15 +- src/Data/GuildSettings.cs | 63 ++++ src/Data/MemberData.cs | 4 +- src/Data/Options/BoolOption.cs | 34 ++ src/Data/Options/IOption.cs | 10 + src/Data/Options/LanguageOption.cs | 35 ++ src/Data/Options/Option.cs | 46 +++ src/Data/Options/SnowflakeOption.cs | 27 ++ src/Data/Options/TimeSpanOption.cs | 28 ++ src/Data/Reminder.cs | 6 +- src/EventResponders.cs | 335 ------------------ src/Extensions.cs | 6 +- src/InteractionResponders.cs | 6 +- {locale => src}/Messages.Designer.cs | 3 - src/Responders/GuildLoadedResponder.cs | 63 ++++ src/Responders/GuildMemberJoinedResponder.cs | 65 ++++ .../GuildMemberRolesUpdatedResponder.cs | 26 ++ src/Responders/MessageDeletedResponder.cs | 78 ++++ src/Responders/MessageEditedResponder.cs | 89 +++++ src/Responders/MessageReceivedResponder.cs | 34 ++ .../ScheduledEventCancelledResponder.cs | 45 +++ src/Services/GuildDataService.cs | 26 +- src/Services/GuildUpdateService.cs | 55 ++- src/Services/UtilityService.cs | 33 +- 39 files changed, 912 insertions(+), 658 deletions(-) create mode 100644 docs/assets/boyfriend.png delete mode 100644 src/Data/GuildConfiguration.cs create mode 100644 src/Data/GuildSettings.cs create mode 100644 src/Data/Options/BoolOption.cs create mode 100644 src/Data/Options/IOption.cs create mode 100644 src/Data/Options/LanguageOption.cs create mode 100644 src/Data/Options/Option.cs create mode 100644 src/Data/Options/SnowflakeOption.cs create mode 100644 src/Data/Options/TimeSpanOption.cs delete mode 100644 src/EventResponders.cs rename {locale => src}/Messages.Designer.cs (99%) create mode 100644 src/Responders/GuildLoadedResponder.cs create mode 100644 src/Responders/GuildMemberJoinedResponder.cs create mode 100644 src/Responders/GuildMemberRolesUpdatedResponder.cs create mode 100644 src/Responders/MessageDeletedResponder.cs create mode 100644 src/Responders/MessageEditedResponder.cs create mode 100644 src/Responders/MessageReceivedResponder.cs create mode 100644 src/Responders/ScheduledEventCancelledResponder.cs diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index 82b2562..b0ec1a3 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -4,10 +4,12 @@ concurrency: cancel-in-progress: true on: - push: - branches: [ "master" ] pull_request: branches: [ "master" ] + merge_group: + types: [checks_requested] + push: + branches: [ "master" ] jobs: inspect-code: diff --git a/Boyfriend.csproj b/Boyfriend.csproj index f6cc7dc..a40b9a8 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -21,8 +21,12 @@ + - + + + + diff --git a/docs/README.md b/docs/README.md index 21ffbbf..0cde8ef 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,12 +1,6 @@ - - - - - - - Boyfriend Logo - - +

+ Boyfriend logo +

![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master) diff --git a/docs/assets/boyfriend.png b/docs/assets/boyfriend.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a5d16d00a9d1f89ed810d4df10114194e65cb0 GIT binary patch literal 229373 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

x*2w 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 z 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 z zgx1W+I4AHT)ARUB{;1_kmoDvmt!`D^{K9%yt&ghoqTH2opFZ~2*KB)cDZnzZ^+KA+e0n{QVe@X2Am#>Kh*T*PQEldIkGSx^>t5{^ zUblMp?%nTiWv~Bxa9i%}c?+IiezSbfo|=o>_y2u+dWVY@ztY9nXlm<`dFhQ}&rc8M*i>$c{Tx#?>Cc~<^-l~;_zf4ti7sdh%lFUPUi0&l|B@GzWKZYU z|Gw?M`275+FZM~y_s;|zm$4~V4R~1`)hTuH(v2G)C#A$*zIyeD*_B7~LW+ZI@d}@0 zA!~zwA}^{hK3Tk~si|p`N!(=RpNHlDDO4qkm@PPOk}~TL&r8=7c1Jr#pCsQPO>OPZ zqN1W_eGYH6n00ox`RPdvp`Uq!zc_NN@#nl`{o^m^?c!%=W-j~jo5j(hAwc7b?ER3J zn`Z3LOVgR$8>wY)R@Jw9{d)D;Czba9eVhMX``P-CGr2Qaj>+q8zPt2o-oBsDc52#u zpR-`+&dBe)R&F6>`#F5}^sPL$+yyjvr@G^|i@W>r`GWbi$4_xSVHe8npUm8OP-}M5 zoJWryP5Lr-nsH`c-mzQLjrOZvo9%xT#C+F||dd9I4b-d!-<9bV8bblbH){CP|Y=kHDTW$nKm)vx<_Q#`KX;eYP-!^x5bt7VU-PLKUI zGsADb(#pC12ZHl{buh6c%ROH+0x z^S(7{*3i&U_SSHzTQa*E;N?F6F+OwwDO_u%mBYXbFjT;9(pSQ0MG441o zuu*&2q)a#Mnx)L4lSG(*sb$6e{IcBM_WskNm%o%am_OQ8oN3DCzp2q-T+@;Gzb)sC&{fy_eokhSSr_HU*S%i5{mie; zl0|ukPlc;3`c!M!`R15cam zj9QaUG+50nT$tr7x2%k!K>AvPNF#L_mjFi+W-80`nL1P#*Re$#xpb3xP6jtIko!Ex2v7@^S{8s%U7=+ z-7gjRUweTR7XW(R-q@Fh>WJJZR3(SnR;iM!T27N1(=+P#85O6Avv1xM}FX9t&* zs!ingxZ<`=_V=0w+UAwF<9n>)}GP?Br*X#B31CB&5NcIUjE@Gr5)tz$u z=dO>*s;a7+&&Jvw{Cv*(eaF3wWs9df7d35~y7|e%{2U)YmEEiE&)5(j6JvAq*2-sQ z%F4>NcYd@@P0gq|+*ivoDaW%Q{o`z%puC>1ow*m)zs&9tR>35H`_{9qn?ivkqzG*CX ziHy7%Gs*CG#jBOe`3gJ@7qv*)U5wp2DP@`J4&%m&8pVe`oqW6KNoN1UTRkV=dV0S3 z|4IM?poPg3mKFOhQ%?M{s&HHWeYul|s#g{XeJ)LKuJ$>@@LR0hTJ@r<9Gi&}oIXU_7;>C-Z<@%4OKkc`kyJwHhKVHs&`lqGx6XSiC z$#>qHGARUfcVsbBql5C-N}IQD-`)nD?EKE;<2H}E*Z)aPyf;T@?)IJq%rhOTQw{!n zI<3F{_Ha8ZTz}$v&%UE)eynD2`5qlIf6LyKlaoH}*|R5U z665QZ!w)ZlMi1F@FC0{y8PJ!=x1)d4&hRU4PwvYsU$CGdsYr~2t@+Q7V>*5Le+qvn z+%*@uIpx7Q6+aem5pmyL9i~zV}yF2Djg@ z`Fyr~Uj4tH%?A@UsLyN3Vv)8mzdj1`~U26R1W=`5$cJKQ<_kGUV zwQFl@o}QXIe_O%c7RL40qrdKbU;Do6$84`3OTHbcZF#rw#XonRQXhrY6L=3YM21*O z9e7aoW%HITQ{J#iTh{#ekl6W8xu|dE)tQ?@{U`M<-}LQ5ME0HXi(K!%UAliiKjxt9 z+b2^OT>8IpKjY8zkoxJ2f3=Ma3@!x5#Q12}c^d3dWSf;P5zs$#_Yan`z`HG*%HG~8 z)!Y@bT&VKPMR#+iT{~Va44V2+ch=<@3;R+(?pexga(v|%!wq*u!(%SaX!~4dvM}Jq zk%P_b)dE)HVdlZL_c>R5(Z6VOKw#55P8O!(;F@=R%U7=C{OrbbHR9|Jt7it1=gLZI z0(>rhSDVt+#Z?u(6g08tT00^0+t!kAwmoMyELx-#b#SgiQNy%pVi~OE89c^G$17UD z=4#j8>U7Fi-{{x3|D#vh;}wr|18#F3pQalf_V8=f=}RTHeJ+m` z_3WvSyR_FWxOzKx(e~}%Pu#e1V*~G}hPl7h1ZoY`OT(udr4+A!VR1}O_P>sf&Jxi( zx(O8xxB5Qvu05T)Qpca`=ClWT%1JNwbmsObxI8+jsH}IC>z$xpVO#vs+M4R0pPr^K zeGtXjbRyC2+s6LC#Wp`|vpjOz)dJ+Kc_L?}a4K(l!uZ%s?c(J8*HOF6bS;nOs7Ua9 zTD|>rm(E37CY3uUM9SXWSU9^x^H<-Qhl<6gq)I(jNQCE42kk$$|9^;E-{i~;!^cU& z*IhVvd^p6tKE3C!SVU@kI&1mAea3!yE~Q2xeQYZapMCvR>_^SMtS9%C1aj`}`591i zdCg+Q=sEv)bvy~Y(pLD_<%{%$&u6F3pC6xe`KM6N#(n$t$+f)coRj@$V+bSb1%tkq zocDQKou)ik*AV?MZmP!N>(0ORoSk-ed*_uqo_re~TY9x8^YSv??H@nM_(wh9o_hXS?ll5d4=a~!nnrb$_be>|e_*0@&o$tf)^Y;J$WE6$E&RTbT z?mwTV@4r%O&H4D2)OXjEU4EQDXZ2RA3Hef);0fI6#PR})%E22W4jC&$o<}K!1{}?zHU?T zqthDOZBJdzoPYc6_WS?h7Y4j2VY^f39uN{@@;W>I_pP5@qG^VmvYZBeta8SEY=OT& zo?pFgSJte~&k78nal`j@-**dh$a*~KIPl8H?O1>0kcy+9C zOP<=K*F9Iy`Y24Q^qA*b>iKk0x1Lhvwi6E?Ja~C`cX@fyr;gem9^2M`IeF<-TxR2Z z?*(Sdem%Mx9`9RwLCM+K`Jk+I8B6NM$R~dqf9kb(`6+LrrrHHi`TBz)tDH$X0wIpi$B{=Z503g=GCi3Eaj|u+pYZtpIpAQy>Vyo zj-=fi=hS|?`P4Z5+@2btf2@(GA0BS6-<)}QnOSe>tgXP?6v{_2o2+(f`tGFM7q~VxGU=p!#j@t5>fo z&EmcM1Zz)}zrVLPmLt|X_4?{R5j;EI^GK}z@xH;xAoY|;)SPQ-W$*9lMjf0xb3!zlrJ}KDHSzuxF>&N5%?FvE}8#>>G30>N# zy?M6hq42q@{XXr|m?-bn!2bW6f8D3a-}&Y3{)Dl0G%BxHxiWHN$;(SzCr@p<@uVzr zFUL*8Wa$ift`>LsXDhG#5EtP3_3M}45p%;vrMV7`Z>n#0_-$y&*Z&pw2GrG8Id29! zweH-HWoHDJY4@i+2|vDW0;}bMIlzCkKzPeog_-(j#AAUpJqyCR+K|wPS;zvZq0f$JM8zuEWS%`CoA2KR7)4W6&@BARHWhfgyEz4^_!Mqj->(-_sx$j zm(#u;SFQVUx|n6v7md9iU)5jiG=F{L`@2t{in`3B7i`|V`R0-uql4S{R!Ov;(ND2C z=jG*g)fP(mSF)4vqp+?iC9_wOuZZZ;5v{d6zQk@|glOnuf>3+xL zSidG;m4;VK56;W@`NG{kYWDQ$;Xa>I`P0wMu{1F=Gt;>c8#FN``4~f~;_a3D_^&!P z)+KiynY?WSpXB5BJ(Ay!^L)Mdf8uND`#+9723`0azT@t@n#tGQ|FE8)TYj(d=Jb7E zmZn#zr|xOt{hfF+BH1NHUFDU5AIH?s^Yg7AMnpzNn)a4{yLj>9MXeW`Bc8K7e)S{y zL2uRjz2B82Yve;uO0U{>;l|96@X*ju*H1b6`FlRPwI3-|n-o3wmx{n99VzKlKHC-l zRlcr$e7t|Y+Rwn?n3y-2`Te@T*j`#4%boeYXTJMS|3pp}rhly5Vrv#wx2B(;XRH6_ z&Q9YMA2+Pdn2AZ?0cd zwbI`;x3Oe%6Ke$dZVfu&TEwm%iEh-MVjSd&9C3T{coCtMM@fkJf5sG?a=$( zpfjx+j_GHEE|UtAyO)&qd>hZs{D3=bU7Is*ez{n5*7Ukh`R==QlY&1Vk^huB%ZMwt zyYJ-!UE}BT>+4!?zx_6mCn7x~W5w>hd-vM!>z>M}ylJD4*RtD3K$qm5vP)>V^Xcn_ zJ33PQH+v+FUwLdhTm0t6M$gIz)&EnXxV@?YLG7R>u*51DB)0*bI z{f#vm!hNQurgOLbw2?je<>lqtw%E`Ql0F+IUHMr2riJA@XzoTH)Hj^h;(gxB&CRV! zzT!dSa}znas}~o~Try9xFWvUeo@KKphZIY=+4oHS^{Xl>^SV;k^|#$j_N%InUA5+% ztFZTl^`(vfr$y(@d|tFO#Y$38m{m1V3j}i%B0_^g6mJrPFt&(bAa5?}b<AjNK;nC67w!GILMSxPS{k~4w(?y~4)J4@*b{Mxf9kXUPC7bsO2dGuH75X%(b2_#F?{X=g!LgN{(*R0uHNRPY}ABdi;&npP$h`e=kxg z4HUB3@@QF+>bdvz|9|VdHb0b9;EOyzMKf3|rtqlfWuD}=yh*z=a)fGMG5lS&Y3=>@ z>y18s=V-Wg?b>Cg-9Aqf8n!L`cC%xhG4HQ+XMT#konQKGe$_ir|4#6J$WN(~S7%Qc zefwT2Q|oy=uKMlPGU4abilZDS*7vh&y6OA9-|t;k_n-IX z>IK=}T|0B4pYgLE?|JdcVCCLNJCX)26xr32KmJaL+*4tAaPX^dX&T^6!~QB_Ip{APUt$HN!`Q{6qa;Yn)A$+ z&AWD4dA-@-k)FOh=jjU;Bgth4(%2eaH2*R5nZy`kzwhpa3l97Ix>=eJ{#AP>aCMH- zs_mD;LyZ)_IRC7%gY@f}+4<&7u-TF0rk~c4Y_ni~)b;%ZcT2BdyzQy8=z@NzU0$H= z<5#UdZS@I4kI$yCM=sdEfB)5I>^vKPNjSf_$+P4AnIDm{v9ZaIE^#u-%F1r4U=@CI zIwwu-hpghvcbBi6nBJ5kp0)hb-wl^8U7C6AoLzaRU5c5NRafj02IGB(XAAos9&KSM zZwq;|k%^tP&0xRq$KQWl!5agZm{!Xq?&~jnAl$+eS-wYGI+V$=b%ToJ)AN%*8 zXP8*PQvCefUIC8`an)VMoyHA?#~0R021JL}zS+EC!vrDL+$UeJ$D6NX=-v3*CBstM z=Bfy4H!2EyzLl_z5pzj0 z`|>e|LHb2YpLg?v*Vot2_unU&4Vv{_>;Z)%N*?$f7F zr^IAWWM=0}5SkE@Y^r8wx=@e({yxK-4xc-gK3VnY)2GTztNPrI@Y$ zPR{*FstaeV`d+m+*k>NMqd?@5?Dc!Et?JnL;>E^)f>&T`0gmq4CU4g0XS%*uK6-1G zs48Ey%D-=^+TR2ozsmhwxqN2GvDvRL7yRp6x_NW(*M@~F_>y10IJNlHf8{Lc-#YsG z^OybH&e3prxxf0O{IqB4cdO^EE!%J^?7}wHg&XsZc8OlCtgJk$xliu65D#1FeLuy? zg>PbfW`LS`ilQnktkMk~61PstC`Pm=zuCNK(IPt`#hI!chvGm>u8;9bo4qj z_D|wj`6M=TsrN-(p^Ijp;9R(P&Sud4W(S`ZJ)IbH(>XmoeY48P+5RgU=d2De+M%@| zMg7?Q;NED#b-e4}@8p@^`D?==#X~Gc?;hnDGeth-PmvZEW->fV1 zR_uO0ulk&}uI^mtzcTLe@%K0We!u^JDK~#fS4m!4*!R9~>FxKeZ`Le-rG2RAT4efJ z^~&vTHKtBy4xjQVmJ{4M|Axlycd}315B7ZRxh+;#esfdmR905jrz)4P-PluUJf(r< zZK~^>XnIWc z`o)ts)^5KS<^NGUTgIxy<7f-K zk{MHPgudOp@I=_YBPze6V{_v|?28^9>5P?B5R2@;{r22{(Ben2W1TAt@;)Zq{&0Ij za?}6c!uyK9-Aunc`Ky_ciOH4LE$3anN=TpMESu-|fy=u7c%OdUo*DP{B^e*@m$z4) zm^qU_c~gz>u3Ej9yMF!toyl6xx^(H%I!9Bt=WSsb?S1|lppiZGOEoJa&%R#2e_!DD zT>BI30jcLo-G8a>beJgnO?}nAZQG{33F@9>ReI`7*7XTFi@$EmSfEiFKGn#e&a$Uh zStmTcwlulx{QZwTy}hk*RWFw&&-$c&`LWBdNh{`g@bA$*(qO0dOYXke%6G=|f1Zh% ze){Q`8982@?T4NH?f-sxX!6BTXWP#{Ni#V&3HcUm|Cs;Gr25+%&mZp$A5X~oSG4Qz>g43CqmP8ZbK;Zdc^9hP zQDWms^j~$%d_~^|k98Ko3J(uz{om!}b;wJ=<+DW%SZ#7=$2OmU<+t1$BW_MS z;gsW{^5FKOQ)?nOpLm#{?Cb0M+hk5snDFh$Sv)~^KOc(G|6)Jk?dMW2_OJK$R%;%- zt9H)%{hp&A3lHng{H?&wR3B3=ci2KL&cqMt5;{=cjmXr3{2%$gK<)6wX+ z`!%(dXXe>f$91h!ROe_vd>Ay?yzs}ma}`H-yi2JcG4r6i=uzHxw*AX{^=Wr$Cm4S&USh{`>t8qnk!eXobi3Q)ne8QB{u#2e~MPK*7}(4n<@F7 zqah|rmtpmd#2)4on)VMPDl2~~NuQgoz>&oC^XJch>+8O*ey-eOxzdQAAn8fmnWN+O>5h~@YnA%k19_H-8R(D>FPUv3e<|X_ivK0 zn3Q>W*<98h{Y^Xhi`tel-TWi9c-5+|rWs~NHa0bC`#3EnW+coBNWA7b{p5>Rbt{(i zc6D|A`V!mpxzj}{S%1$5r`ay^Hu4!;%Vt{r?QTO+krQY<@%{xk%6#jin-E63{=I@jvDJtDrw;UH$O^s7qwYXxl zYgJVhpS#jT!IpnVMQ+;|#c%ZqD>-iP?IC~t4lUo04#$nHtZp5*EjXo;Jh4w+J^O9) z6Qu>aHkz-Pr!QAExvMfK;)sJu-_uF;c55%Z{_s!yD#K}p#G`-s90gk5u8G)~B)Qy|9cm6urL+-s#Khq6CbeP z_L|S4zvg~CziO3M)WN%MZ0zjSs(h@0bNw!8NE=EoN$N1GImyk!H042_^54IIk1J0+ zu_DDUS@Ma;<&QHRzHU6fZJQY=ZW9w1>hD`={Ql$WP2I0rwRgrB9Av$^b?esYKA%$4 zd6)amt@^(I-|PMNo(qZn2)*O_OZm%#B|h6zSJl0HaP_v~u378kQ})Eve!be8xr5E! z*ubEm>ij(0Sg|+8FLq?y)?D#MB=-O5iVMbh)`DUT_xg^Dd}F!NmVey1?##pdhbPn& zZp($9I(<6xYe2V&xp}wTtV6fH?5YiD^Lc-uM*XCDNL<`I;hJ^@j&{&ukHeG9k956p zSk{qhu&$R!R$lSc_PTqO&o3U17CZUTJnrN(x4^yIV&yqlYFu4ijV~4G_Nhwnym}Qc zann?}d)e1DZEb8(dk(0zwYLlB2OR71t+9Q!&-&>3=3e*3$BoZThiv1ty#4s&kIOZ= zeFr~GkaX@Tj<~NDd{X~s#hHoi8Iw;t@OfVR*RpMTY*}Qeij$G2fJ@ej_3Qs%*m^zg z_bYGxy;l}VZg~3L^{wRfzLy#kO};&4(W*@BdK`50&$HS2^ZbiasuN zamly(`P}kLRm%E19x%;pK3t)vcX*$-(J@KcGkWi$4zuknm40<+#{c5*@aq>(o$~UU zwb3IkF7D;2soJZz=iDrcF*;^&aK@2_Z-U0f@(lKBTu(PIS+b<%n9c7OZ{NN(eY5PX z{A|td0ut=0pEs|WyGCDCcK_39(XV3lrcZxz(~f8Ty0nglWjY%YTq>JQ-X$J+azm51 zspi8$_T`>^PbzHW_!pg5D-l^N*PMC2e%Je*&*!~fw0hIUT1Ua3NA>GAUVL)uZA5JB zt@cTNCnCR}J!8YRb@DGS(AsRhtsd#=%QKe7uF*ZU=0#Ct+53<>OAr3Xw;MovLN>?f zg)5%iVmhz>U*%LEW7oMHcGsrdX?PdCYT{SjjOb_i9<{YTg|g8->OXDep~+Rki30ApS@si)0-6hS;nHkA^g9QlGOYZbLXYy zj>*!KPYA7EyH;0Sazp#Ug9qhK)*fq^+S(a^>cVgJj^mC-Yd*PtIvxPJDT>>3+u6%^ z?yT9}(ReQAjB#DhyP1|nrL5(*VWpT4o)eMi#X1-pMM%Fx7dYM{AQP~UArc7|A>n43$~2PW3MmGy)*B}>|;uq zS!xVw*6U`^5CLt%n)j&Q{anFG)#+!#&LmqkX2?C=)4rnp)=|5^b@iN|s#%&AE?mgC z&g)6egx=&^KF2EBU7kI-dX;_ZuP3eIaTQ^+)}Q})BtCZS;{;=+;w_zZ%4|vhjwFQD zchv6t`E0iAiT#r|i-?Jd9W^)nwe5_W$l7~$iK^dM1}~p>X14kJo!*ih~-O52k*VNQBWqQ<-ty@jga{^}{{1th@XkXG% zu1V?LF84h)l)sPLeb0lTb~@uH-z9Q&dX3u*%|b72+?stoNPY`wvC{WvXJ`8>J{L-J zmiGyd?>XsHSZKI*kB0!Cx(pvb=wcXkpM3|t&90pk__^)p<3HOp#B}C-K6UEUv)Mnw z1qySn>4)CF+0ZB3a_O*`ew>fh4A*-#pM7sCW%(q(nUt8LJeis8PJZO?7tJk;jwm-);D-IR8l)vH%ylbdD`0(Mwg+G>^*xAOIHT~n`d2R1f-8U$#ymI~efWo~lVgz3|#tIv2J^7qEs^=I@pC{8{F zT4>r|X(KmlwLr_^ha2VpJYesi<&kEtW;`SI;#}XP`}2Gh_OmYGPkOdnV;*crJ?NCg z&D%6s4xLZ3S@!At$K$T0oeQ4Z%+^y(fBEW_*HdXujcF#9FK+m6`q`rSOe8!xS^1}x z?xm|&TWd42HvL*48Xr>fk@?bg&O;AdoEEO=Pd{_$)Y}RD?A<5ik}lj~m66sjetvH5 zqt8Dx4!UzMiP(QeBd5#U`St?2u<&qm^V0CAHE!G~{a_L{Z z$iv6SR{rjeWXPHAn>O-W>D^MxX%M>ezsYNb3$u}lp?kczuM?>KZTWmo@w#8n%=cGH zI$eAyED^#r@sdW5vZeRpoSJf6tCiUBXoAKslRnDh|Q<@Vm zY6#s`d>oy+p`(udbw{6-zGmThwXCeHS1Nw~J<1_l_!nj|pPl@m&e73P@zHxB52u9z z9szT&GBR|(u)3Mz`b!|y)%B=l=03CIpY8vDwr77dJM7tqkj1m&4Zr>=b#YoKV9TMl zaKVBD()X{t++Pzfw(dsWo${SAtli@J`|^bJO;=}RW>$V(yZxTf+7s=*ImcDZJ~Y(c zV8e)?48 zwQeiN-iZe!GxmtGd}^MpWol}=<h{kMD^TZXzkL0A zdBB7R=We%8@Xa{VBgfP0d1Gf^zVz0a8F`yMva_>`YbJm3mYDcv(N~kI(+#G(cJJm_ z5Kl^&w9zX>|CZRoOUn(u6@T%yobn(KwBzprQ?SjS+G{)C`#GOh@9aGbI^rw+>C>kZ zc@rm{J9lnQ<%u(z0ZqFlHv4`1xNG_Rx?c?stXUidwte2%U;AeL|F`%5y^V4>`EaW2 z#je_#L(P7PP3KjFI^RusGw*B1J5v?$KZa~M{u-;p)~?dj&{z=OdebF7{{F>%`}Q&V zvdTW5^5FGmr%rd3oyo7(T1XwQU%Y+$_V5q4TR6MAx`ecko_MA_Q@Gwp;{Wc-%F213 zR_yLVT&--rEUQY7xAi!0ied>qbNo+XYYJb1&`Ww$5JV;=Fl>bcU=g*(5H~wiBx2z7!|Nm|K{@R__UoYL) ztMF^r&-|Ll(*Moneh91TCQQ0__Qi}d1vcRmU)C*qwxe5bSI0fCq>20X?NerEW^U%X zc}_Aqr0IlN;}@&o#tV-gCB+vOeq3V`+n;=MYxZ@!$yJhS$6Qj_HrbuH9Z*xf($B5C zG9vx|zrR+T9A}c`k9_gf{tyXVZsj7RMAu&=7*tt>zmkL2NhLBoyu|96p!N zEvB=8T|;-$<6ld4@_2HyPrhtocjtU( z20AN5W5u+)(ds|*j|Hx|$9MI$Sm3(rKQ@YLhw1npUTQV%@$vr4N^FsCacpy(mkB;( zlf8K4GUhCzpr??box2ag?RtoJ~=trwvfF; zBT+HuhmTCZ#4)?=iP0$*$9_T#tB z2NUE^?y{}edynhLk3%ZSV*TB-Z?94M`}<*mMe=WNGb1Z2tF%w67yaw~om6($KJtUt z^_$Pk&fNF*=zF_#_wLrybAO0{5<4YjW?~s@3aO^&aDV85gDW>4>oZ2kjln&m_`TBzHZ& zA>Hy-w{xHRq^%OIt*v#F1e*%Xbaiz%v6P=_KEG;Xue*i5=Nm=c`wu`@*IF!E*sqn?s(th;BXqtUcVIPb_e+ zk9td;blL>?OmS- z1q4i(VO=u!zQk|+uy?!P@B7@{-JS0DX=xfOXfg8lyXE&c$0RkK(|RY-ZfUkr@p=I3 z5yid!v-UWyH{5>i;of@rCsI7jI7s2OP~vDcifj z#LR3`Pt=pC;c~sqfvR-hZuHp7BT5>ClN2Ckl4`PZRr5S(1GD zkDYw6@VXm$YyMwow=>zgWy=;j-|NpU7Og&H^Yh8%O)^qBb9|p&SNY#}HlXRZO9*2` z&IHY{K6(3n+P=?y1y}@?f2~@ZU;8@xzfJC~Ekb9Pl-X@$oPFnr%I!t9ic_AEmy8+ zXN(E^Ee{P5sZ$M?PpG6+N!QCntG#H=+rE3jrcFf`RbuBScdmc4RQk%J=lN5c&%Tja zD%^2kPWGir(81IhpY9a3*0jc*yu040& zr`S|raQN_H^;xrKozuv1y?OY&kKwySftQnN_O6Hx+WF~}_VS~(pISVvtgRLQPO|jf zKS#w{Ln>d*`UDeb5!_tsayH*r@mlf)TAr8AXYSe5zj5QnE$Tbcg=VeZ{`!ad&lsJT zO-s!0mRxqt+!1s9(*67OTXye`{yIU@`B%Nf_VfJ{TYR6S90g^w=jwX&kmrsqYmyTZ7W~cs|1CfMV#M0H{!s_^w8_cxoDrNo%l||I)6eUiv-~wc zb6)QJ3Q@^9IXNF%#p5izwyh8|n%VqBGL)&2L02VV;%hi=tH9WGVTAQOR&Qsxav(>k*yFL?+|M~OhkICP=Ytm9uRJNp_pJ(s- ze)8$3T~jmmCR%TMU$*=BYvIz)ZO3wx=YFhaNlb`Q>-i*jf1X0{#iR`5ee!?bym(=e zQgK=Q%eM6$tuEz~X(gVSx4rfEPI+e5eL;HtvA~;-UX?9>l2cPtKL=IZ>s!8i_ioL7 znQs4+IyQQ?X{>8M>*X72NIKWLkBOy#qo ziM#V->6=2DD$9lLEasYhpmVB#!SqD=EN0Cqhb~>YQj+lU(o)S`QcVgRGhSX^9)D8e zceqQ_ZuW-~g_1(|ggf6JQ~BPJ!1MI=j0Ag5ucGrA2Iq8-)h=SP?JPRXYksC^_uYG- zV;?MwpPg|$b|a%E{Ao$UoJB^*B^REr_^+v}`_=#5N{jBcwl*c_rs}YX^XJD;VO;Kb zV8^DMGtHAC7Fy0+f7t5svHtjqhpng8A6m)!mbA_}JKKD_woK-#%DJA;$`#eV7R>y& z{+C?EJ?rv!Ho1>tirV(@@~ z`R<0xcb14)Tk%FF4#cJmo0Rrr=Ous$g*wfL~xd%E7* z4^?|bGk3`y-&FZIEs|-HSA25Aol6q)A`^ttx4d2b;`X{1S3h36xv$pR?BLv`JNdhg zB?ed6%wv*1cl={XY3bZwf1ca_-|}qs-qFx&B65-W@oS93F)~kWR!Ghclmp}dCOjd7AZaQj{kM((GQFF zh8lKqjYdxxU&T(!joTT-+SU8P?HG%|DaDDBFI0p&UQIgHC8`ZNq0Dctm1@wrnaeKs zaEcVK{502jwOiDo_unrcmN5&8jh*Wubev`BPbv_CUZMF9$ z(bunD>G?cNeWo&Z?%euob5=_lod0~><;A(%H@>Ac7i>KL_V)I2HzmQF(Tj_uw%?wc ze#lU-ctZblZ@rxM z*veC`rS?ND=c@K+3Qy;NT4!xL?zzoSk>TsN`S;_o?#I~J`D%;pUg)&7wN)|N&h}S$ zc3NY*u9kAsc|p%=t#f+HOfwIN#hp90W9n^J&v$~IZ%>HWiE%{cge`Gj_3F5HXsGDg zp4`R)3!5K*KA(U7OL~3D8-)}5*l)+y{d}5oJv!z6y}efX7ZPXvi$7ND-r~c4Tp-)L z`ddy^j!x+lpF1KQM^`>NzGu&?A0J$gCqLbGJMVV^m!9GB8{2ZD_3TroZGAE&SyjmK zbg+{_>>WGi7OR|yvlbg=EB}7I?qBmd`o7oMqXI0B8XWD1H(kALd*Wo(A%7-qrcWxz z{Qph`uXXp#BnBJjq8iPDYZ8TRL(cezE;z_b5?*tWvf{j z=W|)^O|wD6YzsH=t84`wE41)`=&8V7@0D!%H4}7hwB3JizWLS6GluK-SWYhdzhkEr zqkPGvtE(p&2JoByyBh!Z>dmd}#pXk+vUJ3#c4xjRSpFK;PsC!Ceo5udPDbXU@`;F?kwSH!t zn`8MTeWOQu`tv1wCpNr`y>;+Lgq7LU?30sJudwSDJv%dVnum(fqKO#{xzdhb50*G@ zYHGT^y7WuI;yn&lJG|T#i}-K;U^+SF6kkh9xZ2_g``f37-i$f+zWbR;+l>34yuG}n zGR_$JhlgKp+`(fo`IoBj31_nx)6?E61X{10bM5-|$$V+OCno#bb$S-0b5ye%*Ufab ze0KHPwO^iMZv5msshc0jIX#~EVn#-w=D*+_Rn;Zc|Ni`(KGA13Xe`e5 z^BH4zfyc8?`Azs)-SSxOu6g!_Q;fn7MeC;?ZZS8xGe_~!tM~8iLHh>EC#`E&=#Y7~ z^Z7jM9FG)pwXM?jl7}3Vj?8#6PvUoq#PO>~wtdWR=uEzNMAxh6e1rlsi)g~rSI4%W zK7IOSXO;BNyYK7RE#-Swh?L14xA@-pBtUtQ3e$>t<=Z-C3WWECM@4z9ci$p7|9p5u z$%Ay6!-o&oN)|}<9en-u8TYTuuv2Ynn^Nrl?a&fXh}KPNO3wZI>gx0*4{sIqot$+sg5g9*1rC>ZTn{{`zvl+&R*}W=i}p>w~VFSP;Yv7{K31qJNRqr`z5wXUYWmm z>Arn-7cN|ISbad@#hW*Ge(!x>`#$&V)!U_U*8HsOtxr1M>Rditnj7%2e7a6#kmKXN z(zx2MS8JsXDuyP^k?iZ-wwa0d_hinLsf?TSv^T!La^=c|`A3xw-2PbN#dOh*7pm6Yn_z+{Y?M#HzkAZhI!IUlkZi%UVCx-V9( zn|z;Lc5M+kUM*R7ysCnYo~$>Y_M$1+Zhn3gm;UvSb1aLWCH&#F_BmXBlIORN(Y}d) zCpc*ww4K?JxS*u)>#ELK{zc_qzkM?cZnAb1c$rpGvgQ7UChIA8Blg)~TvXNJ?6Mz80N7_ex1e=aiBY znXf`z=T48z*;^?8>n!JS=0=C9waa~v2;UJ-n5&$2TYBRq_KZ4jM!A0XCD(5_x>;PF zVP&8oHNB+k)WWo!+2+Z|dZuRXcrqsmw8~@p{y(YRypDQ;FBXXVhp4-=ELCdSx2TB2 ziP6f;z~F))2TO^car%kVr$1}`+W4`ayO(p1@ZH+~f4|qCd0SSk{_)bLw&nBds+!Y+ zdUYN;y4$Dk`g=VwfhlRz&I$MD2dvxo>lG_#H+w=xh6d==F@;0(TK>FQWVQS|^UV3I zlhXg~D6r#TVVZIyapJON%WAo9io7(Ou*bOd&7|HLl@6czPQBC8)}B56SaGRJJHPy~ zeRlKSXYAe*NmH-{RJP-f0p7tYpO~|fk z>)se0F_aD2RoQdtQqX?i`u>ECuHix-&G-H1N`AO${-bTmZRdL&C*H}s(|%0Z?%lGk zlP7Pv{+%Z8dpOlv>Vcy1vc&9Jn- zi;Ii92s-_&8g%;Ka`pvoOoi4KP^iw6v z4V~-Ot$X%EEzHndsd$#r)TVj796UjhMLZ$>rRzBR#l^*gn|Ak>l$9;37A?73x8PNH zi|>!W|Bi48rOD>pDi&0FdELw;th?ch!OY7(U$R`bC)kJW`*n5wyy^4j`{ys-Ff+km zNA$)dac8xH7c62yjj!IXoaC2ndwN0pL^#xk#tYgM0{3Ob}E7N? zYO(3t=F0nF6UWtNv!$zI%I}sM+sg5mm6|d=I4T}5qI%t^B~OjJY*F(9ms0{imu{?+ zTe)97aGC%7xJTcAXJ0PR?R(Z=|3`VsgS;c>cDjkbI-j{@!-fmm7dIZ~nZL+L>&KJR z9ZB=KVix?b-X?mn^kfR#FZ+&v>U(NV$+caRz24)IE9WfxMK*J$X|~u+AF-cr^Xs|y ze(ajr@!arT^`Ek36Zxz&+HSuE6%p=zG8>bwC;z$pa>B(KcU;78cptuhN^(!!`j7uL zyF(NEm+Nj@*8NOcqo;IJ)5+~Zzdk-cH#a&gI(oO3>-5>iGFBxVpBYRi-R}PHzIZk=W#SC+_W|r3+voDlAJk+{n)r?*l`#PE3y96(B%(J?VT>jh5htvRvn%kIO64=Zx-_bu2Fqjxp>)RbNB(-zO3l323lqn}xZ zeVE?fFPHj#E}2@*diLyDtV6)%H`%Ks)qbQF@r7G{N~sf7yJ(y=KQ=aY$^*U!WxH)1 z{!hJ`dMV{QXtzpHRfemticsgDFU#$phUbT?ZtMQjzN_(22^*ujZ1;LYclGoS#i_o& zyldSuCT`odZP^dCn~SPm{D^cZ^-R6GDs)Os%EZRT#>=2%m4jYHOWZt~BmB~RN&w?o z%|*2@=M+8eH3vd2ri4Ybz_Ol*`7w&(b;ZM*F#9$#blGC}O-&SG`5gL7wYynop+d_MEKi_kNpom3@V4 z$FsykX+>T?lm(~AxbiUjvE1;NkkUK6d}EK~cYax`FIVPR7Te9QBe_PBz^JxCxZ`yr(e}Dbw65GiZ2WKqGOKRrX-#({CT|n}*(PNpn9?f2* zrKJ z+uGw;!l_fIx_0f_RmJ-EM#l8cV{G?!%2luUd5BwoPusuySy9o^_5UAr>&rQ=yT_-h zf1;#F@ImiQQ0w}Y;DwsA=b!r&`<1v)HeDef@%Zoe`_GwO&$Q&8ntXMe26!6ol6Pv6 zo#VB|tFtHBL@(aGTY7f|!;<8g`xr~!U+Oz^Eos(g&oyV}+s}U{(p^$sZXW%jc~za| zP4o0a_vNLfr4=g!g*#nh=J>7kDhjvl$#T;#l!}m_arge&l9W0gS=sO{R=Zybf4Qsi z(;E%(bCddwfN&Hn}Z1gza2J6{tc>1R{Obc%kF$1H8JpN#j@_cSlf>+9&$N{-+xyZbDwy0X~~e-bA*nYy{Todc~Xp0ccelKku6g52`*hxqJ8SVX=LAq4Pp(J$~JN`0$})cl~^WfS4E_%Ns7W zL2Vx9Gfiq;4litz+^od1tbD{-Q;TY#N(muGY8azTf}C)nv^!@!ScPV#TVu-A|`Q zi}@_8zrrbFUpHs_^clfly;8E+N!43x(dx+MFRNty zO)THNxVYGsE1fx5eCJuOn}@5=>GEkyFBPnXonL!U(J}f zW}7RcOJktNbCcA1F;UT$)vHQ*4 z-PhmTo<4V3{IVl_pDJwbffi$C?h|vrc=KjwWiy}i2@R7vL)W=Hfp@}g-FyD|r|Ew- z#e=WE`gP`a{W7>Fuby@Of0)qA2!UW_vE`fCQwz=CGzE9QuMS%mvs3Tr^m)Qtwr)*) zp0F(Ne8^*I-J}HmDG&aMm#C%1KDM0p1a!R6zc0b_e_e5(SN(41?(c7JZ(q20adL5a z`S*3})~&m{c=6)DlPlh`I9kkEVYofWGHgO|wZM?yGnB zYe^&vCQsowJBQ>wf1p?q`(#(sXQF$eGtxoH8}lH_O$x+}~ZEAO2Y;O^hQ)Z-syJ*VotA+pk`;rl;u&mz%r0Gbj@UI~Xxf*`VN6a=mnx@!^ki7&czg ztM@T$?`F!Vsa<1YVlp8%YvQ?c=l(Xt7<qy<8gn`SWLQ$;8g*^Q!aglm1=! zT5S|+YsBcd{lC|3et!PwO5?YuYIAdSZ~9#8u=#o=*eZX|q@S#t0{0)e@!Y3i zPGV}+-(O#Ozg!E<-m+zj1HXjPlHS8Cn@%~G1x$PN`KOlPXSVttclBx0w(XeRS;KVf z?dnHG%TDe6esA}h9Xn=-8EjyD!hB^{owwBaOD>=}=crCAH6G?*6}uTO4UK}cRveD8 zp1zPzYDw~zEnDXNd{@5zw_I`#`vi+u`~LlU{X8TjWC{yYGeB5pR@k|W%K;MIpsBb43%b8xH_grF0jn0D9|!g z+r^r^tniHQpIC8oHRHV59`-X_nA8Fv&2+dZE@e~v{G96Y8ND*LRaee%C7($;V5U6Z z&Q)UZqS~1sS6Zo6?Py8veI(xmh6ej?tLdFna*%}XPE2q zcJ2lF5+REPMho9QI@&FLx#C~nlJ)D~U-sBGr}uQ9RgWB7Tt9vO z?ER;%K2bwNO7x5GMYG^9>1j9m3+&!5nGs_WH;qev!qFNgwjR$#S8r^6T(~-X{l1g$ zUY`w}SNH2>Ox+2c*C$u9eD`2c^*-jNw@xp~ z(p#cFZ9Z@Jd;8X{Tc5xFE6&k$A~kRS-*4;xz6#&}%gI3E%~QUQ%uBT2y)l0G`FQW` z&LkQB-sELEC-3e!uf0yVLu-2U_B_?23a&9F7hScQ`GamrF4(b{X~Wg4S3gfEHfc<_ zzAo1HqpvLM-A$hrC%;?hG;N`R+O@!E&$j)%-r?DM?R@RK;`H5j^W;zNsqN|M`QVqR zHZfBBe?{H9O;5{r@7ndtQ|`^-*I&c9ei=_b>+E1#@3U(029Xz<2O^e99S-3$?fJ}; zQ+!%?`x$+0ZR^=7*XnNFzP%l^06S&ERYVIN0|y6o*}^D7cwA-FN-Um6rX#w#=D0apF35xrztjvsQn0v;Wo-e^o9A{+#SmI-zdvjc5P)J0Az^ zotX1m-szh}+PmW-bB~lwou@PVwnWhMl%`7k%)IDrIgz1`j*gPOq0cVPxhfLCBKcWl$H(F+zn{EzxxMIb#LqYB_P-|w zXk0Pm{IA}muxSdYT{ZvjoAmETOVdEyDv4ZZ1c^(b@QIrrahZ`^JaP4y-&HH`SRJmt98A; zu+X`^;-?sk?DO;U^|NC{C95h+=Y6;4h?Ac9^L$%sVPRqGqyI`WF~%MxudZnBDx7?E z9%r(AZ`YrPqLHo-mm1If*Dbo^+^h2~Y+GmN?Rr_2AEWPd;@;ls?S*M|VWnyUopyne zo`((3?>u;_{mJCE)X-4TTQ&@D%67*lxQH*ixj8+)*=qWXxz^=%AwfYwD<5zETvA=e z@O8eyEv*}E&0m)0Uz?#@)Fgba;-*Ueu^!1xv2ND4PuL19WK2Q#0B@Tv`><|p_bZ36 z@~h80m+siHV_uw2c|gIN8;19m|GfL*6KBBj$ck?dkH|esN-*M|x6k#txwW35q2T^S z2@{0X{Z`c2%`@M&;)e6tjOI=Imw%Pp!s}_2a$-V7=dJ^j&$}A$-hAzKwBw@n?dRJ2 zgkPFWs9XH~bm+f%3bK7$TjEv-T=l+L*Yj@smcQLA)<66iv$RCTs#n^aZ@*vCgvZDG zPw#Q?w68bUwcPeoXyO`IiQl%Gc6M@K6_}??nUdo9?Qu(>f9fke##gJaZ@B$dao?Z) z2@jgM^s!sej~*pmoqmQR zT{e^PLc#z4iM(4T7&&TN*r?9%Th#E)O5x(~SKeM;r^468?A*foPP9p3?TjT$mURFB z&~E=Fon`B7)#lrw&39Z6%@v!MEcMZL&+{Y2zb+haczG*({oAa~HxJcT_%ixdR{k_> zKU^5+)F&vZx@AkpMMu%;LRtYXp;H2L_p`D#UAnzTA@Kail`B^st|_o?>N+NQC(?n< z^hLzAAl7+{xUJOsD`28sF;tUg)L3%)w@7c0fvf>X(43 zJMtC<3T)XDOSWwJ;<5PbtQRFM!ft=_MR%=Gn&5MCvb(EM*5oVu>;G>{Oibjo+cmEt z_4T#2?G07hlMXIn>J`}EusrnDH)7ulm5{p!gMEn1UFE*Gb-~jb3xj zdw<>ETjuMWN*h%BKQrBZsZe&!>iqln=N;d!QTSGM&^Ga|ape7`Rr*Q+!v5(`?waq4 z6FzIT`8~rg(}{ad-eQ-1?0I*0x%=Fc3if}$OwMVr{`h&LFTYV)*~B%hve!4wBX(EG%T<^E9s7J%@LZX3&6D=Yn(9y5AOE}; z@iFx5C%LK@3qc3wu5?>3{L{Xus3__+_Z1Iii3j)gY`WO2D;Sayw!nI=i<9HKJrBxO z*6Q#5a;fjop+kS|_QZ2Eu`p~-I~%n?-~=D*w#}bh4=ylb(tWgl1&h#zPqW@iTa{>> zyO*cb+S-~{>lA(R$Aw9Ub}DR`tD!OHR-N}0e$C|*Hr<+#xAW;V#)S(Peze=6>v;6% z=jYvxR%=?*W11(J$~Zq4nz}S`XYTE7PMw<+xW2r)x_a}I9WE9e@$WZvptLcO;_*UVRygg z+LBFdG7A?jT-fk2X4T&NrPpI0uL)aymtXJb`kQ+yKUe!L|GdYgVRse-vr50PTQ!H2 z&yOCwigZ zQLDIqbaHs=`^j(qZ~kQ_d2@!`{w+WE82YQ3{!ueD6s)ZT6@pvkYd##jc+59cwv(Z> zFW~pzy>CChT3jZ5OMlYA4SiBiPE1tRkv9%}vv|qZ%GTr%N88igC9kfW{A;xSRfSUW zb%iT-Z_9*xSawQt6YznMMcHL-T&*m9N5lt8(+M?zF;m z)=BirvD8I;N(gvg_3j{XC*L(WF68EB>0{<6;%& z+lsDJHoQ{4kUn#w)Vb=XQ^Vt)9*q{-l9BfPspia+|{gb%fia)iU>z5vzo144x z!JiJM&d$!cBFzFE8`fEwnJowp3p81E@oCD}M6p%!liqHgb+XK&dUyHHpU>xS*I+xk z%Hq(D$Gbcm8&{p<3jF^%^4as}$qm*H85tQIi|1~>RkeA~g$oxtxb=2CD6-$dv;WxF z(k1Qkbrp*n8_H%X2mCUN6}D_YwoO6X`_r6V6V}{X zVz+$h(ywMa-#E7S+nw$I!9JmOmccRZu(-`7ACHPBOPhE^M&8``>(?(S;Sxn%uQGqu zo!-afj%+qI)#>i;UbtRZNkrDFHDO=>LGqo^JIlLm7eO$-*)514U>cam1RzyJ{^1T z?N*scp-K9o$G2T;TEpX(-@G#a{=OC0rJu~s-zRvsz(=K2N!Lc2cbfZ7*jj*w7Z#xb z*FQ0C+_>>>Xinawf0j&71TSsT&uChu@AoMC!=C>sKOelfp+3=`k0Mj8rT~Dp|;9S~#z4GAOFI9&> z=I{Ue?cSa}d*<5hs^w^6;b>;(pJ!C@;Q?RMfwH&)>uk63zy-TKG+qCQpRhP)Y;ob| z*`VEJZ$I=_3E$jR`r6T}Z=zw(N1l{`xb6pGHl-}vPD>q%(0_KJYgJLg?rd9S<;Atf z`(#0l99C6TRn zRp>;e_daK_ICg1u`1&0iHf(T_kMVZ+e7F3*V_(6ci5Fe@6zV0fmnbUoccu8m8s#J< zDY2DFsMy=bKWpWk`D=~J{D<8>&lXwCY=8CR;HhknlP6E!VB-ynh?t>z^PGznYyMpx zg`K_A&rkX7H{UKct>`Axv?WWH=!J--{C}ou%yc)}XyX^*7g2MBykE!(9Sh=KKR?&k z&OYpBsG*jzKvwj#f87%{mU^9>Ia$s3Rodp8hf1a}WL{>otW zFXqwecj!B zOzYtco!8yLf23?e#OCSysZKk<_VdxN@9*#DJvh)9oU)E{;$hRy-ophg}#p4fNt7i48?mE4ol7su}E-yF36v1%^+va_2Fug%s8Umv$O>)aem@zrq( zO$rCkD)-xLdT=XyeQnRi)0Z+-k{RDl4qPGIu(-kQ-XUSO0P&BUHjeYAuAg&jOQx`t zq~y$|^Ot2}=3j{0oaVc3(Z`Tu1#S|@%?>UV$-BMn>=ky)oErwA&PL}2bd&d7l|G_3 z_1~Z8_VLf7l4{N82QT+~YP#;z6i-R#S#J&(X1x;3fAe_mde6w!t5$I>UbJXeai6rSNboeK9Lms`X!dPZR^hB=imH2Ja$O@)_)H= zqiwCIsAy>KS9Y$f>2n>Ycv$S3l_OoT&To~TmFZiCNuN_sKZ|^KXXj^~Yqn-)v(LJ1 zEuA*!xY3E*di#DnTK#d~CrvG_Ti+jb>%Y4sS)g>gY-@phN45>;qx54^{E@FO{5TY?tTSaPNemsOZ(Ho256UpH`VB<1cakw7i^1Zu6qIp1Y0*x|i=UE#;}Hw{tt6>(5IH?)`G1b0wp>O{O_) z3HvFuYIo4Knk&scfiW?2uI|vX&b#Ds)l;^1)tkzhzqPfswKvAiIA3}_w*26`ySvXD z)CAsoa^Z=!%55Xv_8kxVPQQClI?dis>7K)rcHy6QQvT+DNeMoq@Eug>&%2t%d$=lF zPWS7i$O*SU`>L;Cx}%z?S!HC}C%R?j-@U=YZ}gM}J}}*#tqVG(*w*|`!Qp+|a&N~a zeERDkpwf_eeO>H)(9INo@6~?48*h43%&)+DVT+B}mxS}hTEeULU!B+7(Gq&(^##As zLzR7B48LqV`zh_rjEQL*BXZ(&>H`-3Y%zN%anyxr$Gfs+$_jTijE+U0__*S3+S!^_ zB7aWa%mZcm!j~^!hF5-!V=;}4j4UzWy%_jD^%jF|+{qsQMV}_PDNCPwkf)TGnE1B( z%L~EkCnqL4u_{|+$XrjDSn=jXTc@WT}wqt-6mr=9g$K}ScYsYqzY!rUm| zUwxp-r|B*(E?aUNm9}ezt%>-!?UDNbh1;~U7B4&Xgt28!o@4)Yo!F`7$D%J?zI@qN zr#v7iD5z*fVd$}EXII@f`c$HzFv)P?E^m3}Wu3z6Wm|9NJha=htRdyJ#4_ zS4>ym()WYU<9L$nVOi@1BJ%R`f}*0X=kIQc2_L*LVP+FDwxR;^#Z{+Q&X zqb=?2+x`9g`p%s@r^m*Zc$Hbwc9#0-%g>W@e=~*MX}ad;`nqY)=ZyNtZU5Hx+5SSfqD_3J8(*5RVSJ0+7`_W449vlSoQtg-T4BaY(<~&{#?C9 zm^J(J+ynU>={?u?}_4D#reXc4%7$k@lrGU}z3LBatBldhQ_b#--Oi~r8d{Iclws`aa~vhA8tFCd3o8_NgI+CWn$u2#Oy9xdrDlZ@*juCJE5C3 z5f{D%+%w$t|87cTaBy%(Rlf|VV*Po^TmS6@o0x6?r>%_t^3&yiI?KeowOh}g{$f2L zcfMVldb;p}{V6Z!U#R~fyr!Dz_5aPe{mvzG%3AJLq zXN6+jtY7u?Crp`AvhWE@5ntoqS)woeEQ%(yt}?Hhr|*5gdD?{iz2#?jr4{h=^78Kg z_v>~3#X{SThO@KHKmRgU%6z==;J%~BS#le8pJq5$IOl@w;uXzT%J${Ga41*5nz))} zr?qO+os-S{c0cYvJlyWj&CUHf;oV&)0hI}z9UU3}4>0rh+}T_G{i9{^vpY97CLix! z;ry%o)s>ZrsTUSFa;Kf0_4OO*L=W?o$qV+K_R>{!mRNFNLBa#(6(0lEH;YEG2VY+o zd+|YnQaiuA-HMwnJm0k(*kY^Z>+@bUYKx9^ow9VssdE`RTmoO01TJJ16?9 zTsl8{`+CNcH+2SrZL2m8xbCuj4>o30x-)Ow@)~AKPPerFs4Ss50 z+xuDSMnnMr38au)tl39}P9a_*PQjI!tz6Ydor zm)*QJZ1veE$9qkJ0s{+u5>g&ZR=K*FzlaFj_olx3(!IUa{zdWLm6PqMq*Kwh zn0dLaQ_q`9g@uHONa&O~Tw3Nk+rZ&kBhTAw(s?@)=g5oPU4QcA$;ka>Z*R#o>iUcP z&@<||-0)T_A}yc&LGNar>ZVT?bx)^;mueu~=GoatL((&H?*?pdqD>a^%zr4hHdx7Jp&!0n^8C-u& zD3d>?cEnBy8-<4OZUE;;FqpSNva^G*+ zck-&Es#^cY?MHUWMar|Bp5}htT`=t=^8}rfTEgFVo}Xc;d^Txgg3m-ozqwYb*4qS6 zZtOLGR*}K^GiW)Fn?$A8uiB&}rC+mo142VfUwwIbS+wDup0VAfV0q{|jF67oR>nxjf@$yQn+EF_AMfjh{E&+glxf z-KpPbfu^pm?UU#F0m_qDuJXL|TOM+^$f!lPeBN&svDU6twcICf-f-MFZPQ6N(`jm( zcj(9L_;C5_>+Akp+}z$%_8D?CvDlXFwsqRnpR>TZo$s-@e%zeW*K4=0TkuO`h5u!) zx`6pyU)P@f6wCGL+S=&Agb#8$&T}&6rs^N~BYnElZcJ2BwJ-*KH z5lbVF@2#4(JuD}^wVs==?%7j1aT1s2jS>UvwNo~2o2?646mwcgNQf=j_PbpAxj7fv zb_jF2248!jc5B&jy@|}7t991L?fvy+@ArGJw?ycqZ4qq|5R#RhduzG>{2$Ehd?F0T zG;-&C``mFW^6uOV7T125-+J!*d}CjtqkVCPj%v?7Zlg^fdos5;_Wraya&ofz@~^~;}|QdsV7T(d{*dO%oMSVQSX%U=Bp zJAWIPi@lPpm^MAWu5vn`tksu;)trwDEY8_~zf=6)``oQ_x5GAm&dmH)@S)PQaXJ5o zIbm!5@@5|C5PVtD-y0YiS^4SAOk>?g9Qmo|`7VV=9zS7KID5&22@@`yIB`N`&E;c1 zembS18(fnY zRV|Qfz4Y|K&Elq(%XaLrxc^r*w#7duCnsum*;^%UZtl}8WzrdEo-$rl&sh0!+T2)TQ6C*EY6Li(*JRMvg%vA6@43P3ZrIk z554ewA75NG z-utqiJF4kdu6O4HkFcjxRz6r|yyNxElXK&$uG#}#P;vvjtkQU1L> z_K)BA+)Mra?d@vS^rmT?Me1|*ox6SFkNy?@OV1OYiHnIn;(5n&=+L1*bm!K5y4d4^FwgT5Q8#mdoCE54ZEDTTYK~@YDDuF>mj| z$w5IuAB>-|Y3k`cJL~qMZCYcbQK7+sO_h#cr`omIZHwIX^V#geGT&y4x#xbRo}PB` zpe)OZ+cvVnDxVehwO>7!^2KUeZO_l!-1>Vq)MjL4M9ateAJ~+3Rw_(DL9j>lOP$hA zz3#x@6Mf;0U;oUDEN?J!?#Ov# zQ*Cv=jgPg$(d_Fqj|sW*o0fk(b?S4VfZN{Yd5tqq^duYo^zA#iezHXR*H>5DE#5{P zy|z3j`~&lXUwX6kU)|SDKHm2>WBYC8+5%lhZ$H1jdDCk;{pOzZsj!MEYg&I|S)?_O|la_@-`2Uq{mNf$HOJo%nNxz&xmCx2zH-}~+I#l`OS zpCA5p5>VmTcrzyuw1P*&;lh$$n@w#Zhb-C*zA4{h+;x0HW8+rcR0(fT3yn$GvhdN7 z#LmMN=Y8BxHuG%1=b@8qs4f4h-o+0&7{7Bm^8P99bu>KNQHodRnNpX3@#-YKuN^{^h8#-}C0@ zPoK`s3O;h+P~P_2tVXlbZEbD$)_#9?SN~IU(r?|;($cA*Q-0mK9N+G7D4%NPZSq@C zLiDfjsr+d|LP7=+hZSz#yt!%q?dMmkBi(nX>odeLB%bWhvZ~sgY;0sy^?Sn~M$gE| zn;&o8x+QhgN_W-nH8pQON3%&<8~f`gKdrp<P@)B=seZ@ByW8uI5zw8q8*!SS( z^prO~RXNWZC-mGBuKao?r{a0oT5;c?m>8K~pLjrB!}1pw77DRSZc;sN7QbM-UDNSC z8B3wpD;&SQyIZ}XFf{Cb;HCSb-!uCYUxsZe-TQFIrAwE*PCgctjVU}T8m`UK+}(7u z^5+GK)1R(77J6OyxahM35*=h+6L?65_+rVC?#G#Sx|KH%nLo?7jf!bC7^w9+N!Fm)q4s~3$Qrae0h7j z+(see6vu?9J()KgEdq?6ZcMN&;86BIb@Q>Gr>AE|fx*Pu-`_48@5mOkmDY$~^2CuX zX5PV=^^P-^9#|#uAoAecHM#RH##x>C8~^{;_1?F)x68k1c9*|mA=CFtfBzq&aQ6nr zhZ?JOT-q#N?60VD+FZ-Z%6gn5NeiU%!oJ$y9-Vca*SCNCWpneNfBt>$ukA+1OvCDr z8SgRG0gbL*>FMd29rfcPt4(BNWHzVbR<6xXr>*ve&gHzuZ%|NB(vzi{mz;8Y&F?wBKC|cR>+6q?uyd)Xs`74&(8*wwAamW}sj z1x!2`aoqd0v!3|bb@DV*^0rNiy*OZFW9)$6l|F}7@)&4NElcV`$T?}{nC8p{3W zfgzjDI`I|!ayAh&`K+(0 zscEfl!F>T1PNAgaWOlnxCzO>%do3meHw3))IIWx=k?^!QI438^Uq06Pz~d6Dx!PfC zESyrE-*#tj2nqOC;rPOabxL`hI zw`hINfyPyrJM8*TZ8YjTqxO5#=FOW$^yDWV$l7}A|EKBud5mw=Z|F&%5FBuH!Lp&WI&$zIjMYC!%2a>wP~DKIV>);d6i6t-o(alv2htQ*V>)@)!T`USE)w^k7ZS ze9hNel&|b#+m>^)=-8Y`Y+_}*ZA0ADy1Tl%PMkO)@s8c~TXal(XvB_!hvJhqZ$G^y zw!MXC0c-DrvlA~`awgj^{MMLxtVhze;EZwG;fEIcYkq!O+&kN5XS7M&NvlK5vMcPo zRp-QBwm9A|-)?tyz3Y|fr>!h)e}7o2Rvb9vOtVLisnN=Z=ANFO5;3V0Cae4JdaJbQ zai@P2sV?O`wpd|)NJg>S91tNZn~pzd*O<>mur*P>|$8! zT-tW;{t}z(vE{OTxonn&k62XVKOHm3*t_(YnI_MPgn;_fr@t?0X<-4)Tr67jsG_!Z zul~LySLJB;(0_|(&f8Y~{aw(>|BfXI8zXYI-z~enbxvX6wf6N=$_FMDFDMkB#2xxb zAmZ`+u>^5^E`5*wyWnR24<;Don=xu$h3SNTs}c*MbC6X~ov zB}3ih{pMp|uS^nu!FTr2J(UNwmzH>T_sQG;TRA;Xwn>3w=bulf-|v6arER4t!OfYl z;^AB88uk>Q$!Eo-nveI%-quu6Q8~qaT}Wh(b-CX5HEY&%6?eRl+;~f8$%T*KBzYA~ zKhG&~uohv-%9>?SnDkvUc-fuAjS(BL6^n=gXJ%7jgsx{sC<}-g_VBpER4>y=*-MMpz zcjnBQ7uE>+wXJnHc7yF}^_PFs*tEXD!=wX2)g!f8M_T zuPidzz=ml<@$+;1S=rgERn?XX?5$wh8p(8}@KDx#i5DkaQk*AT^|n3EBVoWW_W_%x zzP|mnAFe_x_I0NeACi=2S}FUXseM6dhW%>^@A-DM1!ca%H@4;8c65xLw(_~jjpe_^ zC;Xk6`aMK4Jjc+`Fw<^VTSMymdwXXJ<@#RH%RH0Uys2th*W6Q+;{yX5H}B9kNI4;J z;@RS)>}ek}J8NQWB16UIy>Dgkj(c=-#)8g-1qaIF9_;es%6!OBZu?)~^kr|$+kW-i zC$I5eeeCjBYQ3E4o7XottM7BjoM2o1O(rL1s_c2D(0xx%CY0>!;z{f8>wEXE`4hWN zYiny{SLwuX_p=+^SO4)->7MxO*RRiCzkZ!+b-Kyp!s_t#+Mtp6^ApV@g!MP32u(b* z$o!ncmj3ViC!9I5OYh~EgY5Em6`$6%*Z>vZhHb7hdNay)CD( zPr{HXh;Po6fTwm7StFj$ZT`p4?ygy8R@ULN$nd6#_oB124U>;CsQ zHa-r$WnXUD$<57OZ(I5*WZUJJTi)E-s$Kl!!$a|}udnCJJdXm^sS#_ht+0N-=X3u3 zy5Db`j@fm;yPh&NXldiT>&umwF-fMU_Ut*6tiSusrjnYvXNAwsNNz9CGum;PEl+*x z+M^3C-NFjYxz2lVTkpZ-}ACDCV9ot~iRb6p z?$?rP2CdWD(4TRur!00^t;f{D!)?5kTeof%E!!;{&tNoh$BrF#;c=Cz*GpetOJ#P` zTE1+5@Z0YdY<%3$%cD|$8Q$VJZnUejCo91I+JyS!vgJIU-rlnt{!afMxiP8rjLt9R_q$zrVXIAf<}aK;Uj ziHXVecX#h@h*Vf2`S#XUX^WH-6OPDQm(5{ontW%Q+}gF#ulHZN)u`}$hw~zhHCB!+ z-}2;?ew#D?Qa(|4WOHkG)7}N2Pn~$S=JiMRicK18pJ%@Az4rg*f_336wRO*5X!=p3CTw_)MAXz35{xMo=HOg!AS zoKx|9(LJ-Q2))={CI5GozGiE0Z-2Z-pQFh`P*~XbMXPw+j%Vkr-^ZvX-cVvvZ8i86 zG;^P*A=~?zA9z;Xs?mJx?B&H(ckP19rp=rCIe2Do33_(rgi_Q`r`^6ROtywAeNTO1 zy|AsTP(H|KYg^TewS|cjejjLLwzm28VsU>bZ}%8eJboL zmpwO<$4ypLa^=pSPoF+*fBg6{`>Q>=90z(Njn_qP&%2w{w|H`D%4EZo6MJ}$ToTVb zW3qy?`R2>73!U55d}oFOB_wQ%7{yqyWjs>MfO}AZ*cWwT)Rc*%`Ik)xB z3D$+oQ(LcZi+Ig@de*F2n*##_@Agcyovfz?)UquJt86^dft0quez}z@o;so zaX>fI9oM&E{d*d%))^{DH-|X5*VNdYdyqE)bnIN8lH|=hy%{nKGJD^;9zDmD+x_KE zTdi&Nw>P;(H=Fw|UAp8vNnb)}LHzW!&pl2nFP^N)8WCIda%skElM_3OpBJ5*YaPDS zVqM3Vva1{2Shw%Q&i?wF8yg?{9$dm! z5+J&oKb?_v=jT76C36>i&pgi`#b@GfrV?fZQHg?v^V$O zu@cFg4-XDTxbuEG%x^CfvZjBL71vWo2P4rEoxOTl%I9XynziY}hYx;SsZJsKgN}lell)?tpD#M$@TwEUa=IN>E)IeH1FVFP;O2EZN1{XljS0G z-8)Ub?#DyHxyiZM=AP1C!P$90MU-88%6%PG6_p3+yuK;V&&_R@ z4o|%;zjjWfNY5Ru`TA39qa#I@aQ@vc_w=Hl?#r5cg=hRH?P|IWS~S1k-Ni*^TR|v? zmY27;@#|NwvKW5e>PU>>uM&NjrfF|)-~BQAh{DR1D;H0mJo&N8FOjObSMNJKfADqr zmtCt8d4Bna{a=1bJu24N!T!gC=I@WEGuIprkFWi@@X(tJRZJOMS4foa?d>_t7?mF} zZR%9f9}DKJ?V2!Qf|<pa$TcMmbEpjk2cJ0EqJxrU}xI( zFPBtVc`GX`jp9`%tUSFYSK!U>v#yU;E8MD8iZGui$U^#qEDHrkQO6bq>26Z`fsch7-Za?6OYzU#*faI` zFZr2IEWdSWuX~ZQ{q{%iDmD!bjT1p%LO&cc%lvtX|HC$(XPzfdp3IEXsJ{>pA0NNB z>gy}vE~|c@yQ)n!k||%bCjPm;!KVIS&BVU{?jjqa)~;L6a$fkyhlLUhlJav-_%GA? z?YO`FPtF7x)k%Ujna|G5tX1=!RkHmxtFL`U3gh{m#m_fI-{ij1!7%6G+{Pn0DYv+? z5}!n?Xl)b@uzBGhvh#fE)jxb*X2-1b`?u!&=ue+l`RsR3Pfwtsq2WoP?L3YGEk}+Y zk5^kcL+Dn(qR8dH!$mlv_~s=x8qAYT(OCT1Y0=`vyMHhEd|zpq_b z$D94irQa#NIsX{$nhW_^E|n2nx^!v!S?#!lgv~c^6{M&ZeLidcy|=gbsQ3qqHF0}? z<@yy^>tC3AJ&x1;+Jf(j1`}i(mwvx+`%pvg>-GEPm<4%*qNAg?B_3|O++}33+V;Z} zb*uGT7ppfk-#b4=`0=AhAG%rCPM$h-isjYn75(aSayev}qxep+9nH#LQ}b&^+ms0t z8uVvM95F54`DlmZikhnZA<`igCs{5PDnvJA%&q*hDs*+xzhBq)UyE3KE#PDkD`*v92~x7b8&WQvH67mUDNfMQ}mT=A{(Nf-<+_d`S1LiPbW7f zCMF8s2q@P5%$1^wZFeHi|Iz0y!v=t{{De3$CMv;?|gV7%f40Q<%>O^o&PU<)P4EsWNGWN zob8sy&vZ6LtwTeOYg-$$J@4+mGbc}KE|QshOZD{4 zlZO@+W<@nrd9Zx#cz1h&-5ITmZ@D!4`nqJFT}L|_zP`S$pMFJnW zW9b|Dl@|{l-PHY|@2REBjO*bsF+IjRf+N<3XY*4>?|IYMzS7$;pXHN_GmnN~Zk& z_I6WzpaR!7ulOz2>Wlq8zHwR<5+4^=XJTfyt)KstoWYmZ*YlI6>&0Gs^6Z%yx0>mN zUq?b;wMTzjp8H1ZnAO5RzqC5uJP4SyQMR+`_H342=jYj4*FHHhv0rl-9XJ?eKE_*ZQyxs2{&}7YdFShN%oray$ zcBZ^$d|&@!;{^{H#`0<39h=!ocl`hNTb_@Tv-4V^x8U3-&z}AI&3&aq$x$@nS;?xa zi_gwBH~;fs(`miQrI&VXH2756#d(^+iG6kE`PX3`3yLrH`iflf#a+@ zrwj2ExeK_cPf+e>sJvi!_Lu)vJK2k@?{{82#j3qNQpzB~!OG71vqkBvD}o3Awm03m zYq9eFuc~ilx&iYQjNSuc%;p8w!NO59WlSwKQ#2}OVjLY zDcouHVv_eO^mQBiq_gc5}Vf35m0utKYG|-n=MhUD}KJ0dISRs0Y`Icto5mF>41$(Pg4&-=UU$n`UsxBGomAJ|%RCti0w^f^DX;f;&SHhHEu zZeneBJ=AvWO{{%%q|h_+-@YD?}gO`6R(A_xi)$BcW@Q6*8b(E zFRkI7YgcQ<|GG1O!|k_${$S>uwnaWqn;XA-ozMfl zSp}yin-@Gdz<1u}^O?sB7cS(CV6Q#BE%)|<^(#7l*#z!C&N_ed_X9H-RSP7~-SzyW zej(wUu*X);S)yHjU)h3W)8i)>gO)L_e|vv_{o0e;MI9#`U^Tko;H3Qed&hJ`#R&~| zVOphcZ;9I9%vJ&|x6v}lie^~6ZEDc2ihP;vDVvyIUod=^YyEipnj==z%i<@eR#a46 zxwO>#zfIDSj>>fp%IsG#Ii5T@S-qU$lW}&eo2KZ~&jOO)rd)n`d3pWC!e{}`OP4OK zld>+$*>rC~!GiZ9k{cDCD~7Oz$?IPBKlNogZ|1c%k+sMB<^PM@SEx81KGrL3f5Fmv zOV7vN4|Ae)8&*B^KP5PE>eN=_9mX8|{M$FC9Gq#K-uJEf(NEpV%F1Za$?nb#bA7k< ze%g0$p-QmE4e4gydASZxG}^!1XcLi^?l#^btzv8(d@Rs~#n0XL)AsvHiRlZ!pS%^h z;7{9>IdgjSk4r6Cvc#qInUhk%BISSG?-aB$d`_M?@#5{_PwXnGsj2PKPYo6c)Hs;j zZZTdqcjZ@pq~F!qntNM}`Ht_C-|zP?XTDR} zRy@z}p0wApHA<(r&88deDrUOjdP+^?!@W1YS$T?|k$B_4@tqOdpyl^2F#b`}6aB{XF(B?W^4MjeF8JF@9V2 zb^T;@|96(v-`=#Z3Cr$~x0$ryz}Hal>14CdB}B&A1s0Z78a9L-o9Sp#t6b8j_S-D) zPQ^9QczN#aZM;jLzO}3UwZ-4td-nnUuIJM&=K39fbhLZARLw(|&~FM>seK77Tt6PL zHh*9;owYP@v72gZOUscTKXx(oowxh_Mqzn=`z=PsZILWV(T#7CZLGQH-q!*3VFG_L zPZL)6dolASchvN$g&$beCuc5P-!$ReTr)dfA80@URjsYBE~p@-5K@Xs*$%R^~}9H**yQ=o}cS}zhv*< zdUZqE*;%ra9Cs?E{9U=`v*RX}5}t^AFE1}I-yXHLtn-r+LuF-UWY@1rfqz#vPMe_b zJL%wp(kTb`xa8*MzWny~_UF zC0m?cSgNbsuih44|M%-&zV^p^4&^$E6r7d&cq6$#met~)0{g<8M_)JYv5P1b;|#Os zko;})Q&`>a&Ax<#P3~{Xs&~lSbS-%Jdj0-5tqVKnUI|K7`|i+ioxS?o)$7;ims`d~ zEvTvU&B@7;j{3Im?Y7%`&U-nZ{rLF!e$Ds0BRnHp{X%650pma@?BLBX`hgWv7tMvyIcw zG0fe(!g(W;(SFgNS1Sy8_xuRUmhw)S zK7IO+U-p75f$G}YuciB(XEJoHDvLfj!FjJ>-wbAb1&7_dZ=XvY&D-X6}ERFf9p z)z|T?rM30u)=yN@cg^QzzNe7Z^4mZ7L^Vp*qKp2WO5eQ6b) zsX@WPh6nB{96fq8(xG_!_UJk@@S(%S^HEqtA|hVz=>IzWE>M z)W}V+QPqUl6Cf0fA8bx=Jx(@@hgkY{hH6d%a+__JC*#f zx6s}7{KT!UhNTDg?Aa6Vqg8yN@S>}@@9vMsq^r4_4t`a+sPkb_sqN-Xr~RfJ*ljkE z_vGdh-;RREz2j5t+Z-%UvmXAu(W7A9=lR-u-IcY`=2vF&e7gVtU-*|+ zg-7biv&{uwelRkU_`c)a-QCCAT3a8UIl*R8`)kX*^%0U+XI7lJmO1l(PMJ??YHHt; zpMsMMY6B~;t%>~ZJKJonxx(rbKejD6`TonViy=t@5&uu}7S(?^$nJiAw)y*u6DCaX zu&-coJbiJo``>S@SuXYxD;}rbaeUX~;IrIKp?Ljn?R~A6Ib>!nF#W!5YoTWID&-Ta ze)^xjwA8!)?X9h^Uq3$9yW92Cf1Ztrhue7jZNJ_4=5P1&$@kpb+guez9sf-E_3Kv` zXzOIQ@3bDHQ_a`;mG(JaJLr|)4C;nuh!{_tGiOfnoQ=`PB$oZ{4HJ7P$-d{JUTDw0 zfcb}RZDYC}cC2c$Nx_2yE!FS$et!m9%>S(QwJXnulj`%=T(|fW!MITCV7kj@-i3d8 zL&76}E3is<`JS3RbLP+eg34|@7Yn-uICIRRZ!q)Q6g&sr)Mmc?a^&TT`;m=~I)|%m z7ijOCKY!}yKb6m)zDy37?`mxu$rpZmOXlTYpu4XadGfy7&aeOX(|Xdu%_aMfRVTKC zb|$^~e(a3kx?-d;)iuTNp&EKORwVJ)Ko8Nj$cxvy|>psV9Dn2~8 zw`I$g*FJj10S{&^N%{BZXL;VOEidOaH8pKImUH6W<2H5wOJ*gv_*i;5HzhZmU|Scv zTzz}u%{{J--yam6oTOTO=&ZN76R8{`=TpU!$R-@}zUA_w;wy-(322>&4R6O=q)e3;hnT zvtJdQmpk#>;m#*o^J+vtf!5DI+gJPB@6)5VE-IO>`T@#ZEK@I^ewnk=YFp-IHBkQ} zQ_;`bA)7r{D$MTg#O|g$kxVLr938$*x9>6^752B;sHvo+^lH;-z1b@(q?cG0J@I&! zy?*bu#OLScR<3*7Z}%(X@SmTb>tC<^?<{ac;{V_G_5TY$Jvr%hAmFg`*B@$IQug1Q zweXm-D7&_{zt#TY=jUGjICJLl^!PeUxBiPVt$j-mSg!myuas}%!vj1E>Ku;l(Dzz1 z^KB;U)k$;a$ZXHJxJVN;<(j@V%6Y@SU*e#K_O-Rqn@q8}zBT9O zrniUN`TtM8SSZxk+1dH>n+bDO4~H}RW3ZQ8ut`}_7z z@4mU~#qI6+#>aWKm70~7mWuM*|M_sd$?WBlX`6DVcx*YH#?LliR6Ocm`qS4JdEbC0 zVm{uJPCh@+wp}VbG^Jo-jY^Hizdv_;Hf?rzo$%_?Qtn%kJVoE$MDA^roxpQAD!u9a zx$F;LGBge}-V@t;xUDubGZVD2Gb&2UdYMBN|9i#?Z~r2ueX-N{XK2LCK5qBrob~%R zlID3ad$XT;?``EtJ9PN)mt5U7q?)-Dim#S{Kne%XS`uREyEv;8Sug6vArYD=-y>|DX z`OMCylqdSSMt{4#8mfhFuFAi^j}ugS#^2hOdt3hZ(rL@LZQFJ={P~ZFa05gBh-n6r`RMYPF)c*eVcAjbWHG9zU7d0>V^2$Lg z)ObvO?-yV(Y(L!2zrNKd@-{t6e^UrpRIcl#7yoz6V-*JB06w7m)YyNyZe%yDy-CswLp?BUbIP|dK3aC+# zuXRpH_wsbXzJ=3v-hXQH;oViE(-+pr%(1!mepXCyWMpLHMvK+nqaoTKl?X zvGrZGh2K1xw?)fuKYNWS*SX=7;_jvZ^}R2b&E6Hg_L|wgopTNxd022oKYH7njwN%> zN^>vxb~rn@vE$yaI}-}Zd>^Nzr#sJocKy(4b)m2G?BtF!PM)|y(AhD>W$D?AnwpxN zw--42&9!pf?x-XYp7M$Fg4o`xY@w$VQ?-jut_3aM^VLbdaB*Gi?#BGRU$4nCM)%46 z?w{v!`$^{Sy)Vr7Ja*YubI;Vw6SQ1>yLA4Zk8&p&a$KC_;_jvO`Cr!1`YZCJKK1SM zzFhM=1*s z-yL*_eR}i$i~qSn=gyotlQZ{!=ZuEaV&4 zd0)O<_Rl|e@}%Y*PNzTBG*>Zj!2-uAZr>gw?HcQ-aF_lwo#?|eF~(YET# zi?!eXa5Po)KYH}2c60jqI3|mQTR+UobDw@vC&3!(T>Gs{nn?(iIZ9VV&yH)I7gujKi@`-PvU(D9; z6I}2vvhgjO`=LKSKePA9T9?%%B|SPS*3BCK=SyOsu{Bo1{fAH1elzmnQe_ts%F-g_C?s-+>aqjIr6Ayf4da=xRwx97UFWaln z7K+EEq$xj?d>F+P7JtGz?1@dLTC<3V$egc1HzzVI2W^FzSN$$hHDmH7#`h(^RVE!P zRJryvZYu*=uM7___=oP+!L?Xy9CVVEa=I;cId~3 z4IB7CBfzP*x8<^{zBbC1XMCmg?ejm@)9*Yb-u9K+=tm|eB^AUNf-c`q-M5HgtMeoG z+zCtBSKeAX>%`1xKxOX{Yse=e)bL)Ehj4`F_`~ zU0J>7d4Dx+3f{c#yZ0BHcp2?FhZoa}YXw1Vq5sZBMV~a}?^wBj>Ot1C-f0Kf*mnGu zP&g55osrF+UFFG;*uT_Zw{_K`pSIdN?jC%xW9$Csyh7*r?S3eHKCQoh&-01yayzw5 zO}CaiIW=8a5FQg#vunqW9TvvMpKrABO84d8-)AeoYukhg6E)9>n*-@H9UUrf@RB|{o&T% z^WdLS@3#7dyxCtv>R&O;Tki8;Gdo~%Sp?`_A)Aj!gv*Z}JNAhCkS=I|zlrW&aG-|KcVX?X;@vrg zJ~N*7f`(IHU*r861WNEHSd@egXdXGG9#W^EcqyARbUSCj{h4nQ54CVgL}X9cwQJX@ z{Cz*w!X4X{Cw-m6@k#K#^LAB@`2O6Lim|m{ug1P9+x@O{{sgu;r>E;r=VVfuW9G*4 zn|HhYsr9c@u4rGY-*ieZTRSV_|BJ=_+v2NUF5NDvTfbrQ#EFJakBZ0JfR;|xEIPT( zpy~GOl}lR}YM)@)DeNGgaMkt1J71Psf%ASWH+yHX?%bYzUGG<~`MnAgC8b51Ik#KA zSg~e}&A)5W`LdTXcbhuLi@fyN^wT%!`MJ4)$4uH*{|~!3bHPSmmH1!UI(uWc*$K`3 zWtDPb0-vIy;zrP>?#D+ug*WL-2z8cT`?37!)zreI>$Mu)0f+jMO2c1+m*4@`xzi82-Pw#fWFFV`v zRrIlNc9`4q_mUc*10n2+pPjjPxQ+Lqd<2(Q1Sn-BUeUGJ%1_cLS)JRjhdGqFR&86L} zYiDfa`MBkCiwDQ2s@v0h7EXU!vE=jAo>;w`Dc9CSzC7H%!D^{$tI^DlT zz;F5GkH@6*YgQ(^g-S+F*N|}RNaMLAceuFd)x?w1>t46Z*PYPT)%|N~X7-IqI8XV| z!K|&fK+6KceSFTm?zj1LqN*)ec$VWKErWG}j`LR@U%+Oi^M2Rsb>5)qQE91bQPbYu z-oF3suQ_`kLy%|FJdcsEIh)0he1?Zce`1ttvLI;;a7d1JP?dKjpw0XVgR@qQ%?(FC# zkImj3zG>PU92QpQ5!A0TsY-?MmF1;RD{h!@Zn&3H{o=wxi{cfI=jK|MZ(6csNl^Rz zpetsa6VlbbSStQ)PygMhCXqeCv;6I?(rtV8{3$3YDVZgHNc8JDm+$QCJh;zC-MwG#?=0ot8|PW8 zD6crdr&G`QT4LJ#R_*9r*OQZz1JjKTfEF(KO5NV_BP++4dslyJpWw-)HMP^U*X>|Z zFEc!`ulBc*bhvBPgdEw{Paz9aHN*Z2zD!N9*4ELn>0o^K&oLw<HUA6>d(J&?V6MD9LH^uadS)7Z~mnecHjAQxQ}J_yE{9X*X{rJYj$!{(gwbt`s=2j z`SXu8%SGN%^ufE5U+%WIcN8uzzOo|F$T7}D=W&Ph+K)$0T1>lM@bdoyUXDHYJs)!= zfu{YArKP0Ac(8BzeY)65pv5;d^yiXS4Y_ygdX?Dv1@;?R9915mi#|Y&bs{FnTb3(+XHgjMDOTjFZpnc`HAHAiq{J+ z8`{{|)V#W~()QQ)_w(mXnsiBDPjB7&yLtTO^BGTV9iEZ*R}nFZp-h_2tIp3XZ3~->-kqm1Md8b2rx}K9OCu zO<$#cX9##ayZ)@RBjnBQCY>r3X;nR^!$HWVCVRMV?gwX^=b;FiLA+pc3$Og-8qbX8SGwT$8-bRp8FLTC**=cmM8Lu>PlKKX;d%#8ghf8IJFo+4=oyzFc${cXf9! zKV0V{zpnAm+U@s#U2Dj_$)OZEX)C|gHcu(5l8i}r^ZJigEu7!f)Z`#_J0xJ@!IZ!C zXLik=H0hG?>lJk|!d|tY@hz5bx{=kor{j2UZg4FP(YDX5%~y(+$$yhODaQD^y1IJ* z^Lq+Mrr-GW^>z8-RiUfha_;Tns@(q;(zeP!H>Ih&11#n z)n&3-TUss#R6b8!YrOvTzTfYp?dtyg=xll{!1B@g)TvWNX=i49tcZ+^T*{X!S|oAd z_=aCLZ}S(_ZIsMoXJ^~xeYp7Dg`b<+48^~#Sg}GbPOtjH10%aNk(-|_aXZ+QuvJrC zG(_vikqJzW?9W${!ydZjaKLW)`QRe`rDc{<_I_ zf4^M5vj6Yf{Pi+66&ofzyfB5gK)vagTmQmar7s!2AD63svnug$+t~w=x1;5*|M&yi z0&*#%Woh%R)U=}KUqzMf&b@GZtnL>(PY^7q;n!_$`_GaMsN4Gt=kY zTqSWPFUop}t6L}6SzjykrS{b~2nf5C+j@H-Nb+50jwa#3y)?t^r;FIlX6=h;8 z&RzaJ)9?C=7a4P6FDpbwM$Qzfl1`Lywzw@|!L~)lrdlm=i~Gv5851TnIGB$*?QyKAs5lfF9$WhR z!a`^D+?$)8ZU$XC6#bYb#K>s6$Gf1;+>a-s#JW|#fc9vV{P^(j`mrNN9-Lmbwq?;G zrHOT(7Rg48zr>IDA7Ci|-((&j{nu6{MZ!@f2zP@NJbyprq^6ch|SLC3P?3-}K>^GEd2KrQ|zGSAwoRyfpz-s;8{JD)s7c zHdn^0SFg_f|2+Tyo^weXO|CpYwlsM6+jGUa(QB`%9nZYRnSYnRrK0<89{EZ-4S*X5s{q#KOXmQ<~g%w)$xFwD<7UMIcE6c z|BEYcq|VK;3@&_jX6AoYHMO|31wI{0Z*OluU-9*7`27FhZs*^x`}K0UIb)mNyyVFo zucqcaU@_62zVu^p$Nv7lKEIC_a~)Y^EQ?aUrq8b}b1_^Io7(o`8e7WzhIOn(ekPIE zu3QmGXs}i~4eAW{Ez{j}Ye}&CZ50`1i@p38`B(o>=$-w=Y{suIFPr@enx{;kEB9>bmsTiz=pR^Xqx_j|kyOzi273O3M-0YB9r!O^a%9J zH^1iMdi415@4f$iy*|J9_q*NtI?>y5_AkrboR*uedQ*RiP^Tf|tMw7JQ+yhBxAJ9N zSNVO}-@f+j(&=$kn~x?P{2|bsb8}Ow>QvR_c(I#HzB*i2oW|qTV0Bez>5?T&q;>o+ ze16<-FE{1Q^UnFbxoL+dAAC`@YSI1OabJ!m1T%|>i8*}|pSY!V<&84i3x-nbau0e7 z<<0b3_SR!*=<2Xk{g@pa&g5-B&XF(js5mBmnxuW*pF@Vp$IjRkJUH;H{9fhr-=I=iqODmn&#Zh0R<67=GeOb$f<~GW=%`0#c0QS>?5>Ui>pwqim)A2^+;KO@+3@tl zmu;y9k1e&=@~}rv-ctAX*ZO7t^ZOXz*&Q=g1}*FSZ85iSL{LE>$ThCEb{K`I9XU&SX)ydE#MjBcpcOM4r-lwJMwSis=#?3*NoRIKI~9n zYF_A5H#s#lG}O_@hlg#Yl2cb#S6$Sml#_Btr~AAxTlJjPN^M2+zxow*hk5=6YwZtS z?subH;>nknm-pP{2$gYPlRJSS!@laZ<=*?7ZEJoMtU1rNa@{(+;ujYdCbjP3I+R%xBT;;7SUQ~@rHMYH}SvR$bE5B zhOlJG`+IxugZ8^J-|Lj z(3Dli($ccsvR3rr6Tt`Odwyv|htKV)Vt#u1Mf!p_p%D=~*wv?e%0G1I&@Fx`lZ;mj z9GmNcgMx}aJUH0=KYDxK-Se-ntvxN+vsPWZUsuTseZ%jWw@BRDT z@ArjHojO%?R!(63p;MQo?rmvmGV{M8zOkX_M2mV3OZI}@E((oh^It@6;1NCZ{N7S; zag#1Zjzv93-n{iw(C|HaQbmT{f^p7-!m}ai&-VZS``&i4n(wK;o*o~w=jW8Pwq##l zcmHnr{l8znyuAGX`$c#8wSWG6KEGL_)c?teC9fs@!?U05>FMn3-0^SfW#`B>u6&9)5Dw;mVI zIZ14iukD#4Yj0_}bGzqsfh|*}OnEW$ruVjgO8;0*>IAM;C>k&O^8UWO`ey@=%F3PE zJNEi5|LYdI{)6h4{Z1x%m$oTJnV0PO@u+*Ydyhn+be-bW7#*>0Q)T7kQ?IVB){om& z^76HO?Uz8ctJf;^56=>P!1wb}Ra#IxxwAl_3H!;W~K#_w>=tqbE__DH(&g1dwY6J zk*B}>6zc^yw`PZD|2fsrbJkHM+jNb*ZB>ar+m7v0I)#OWd-Zod5eolxcklOm(NQ5G zB`?0byzIVt^XAR^&Oi*;b_4WPz{mEWlUe`el ztlaDCV$ZKwvEtmmy(&kdE#{uP)o=I9<2?73mZ;_ZicK{~+!GqAxfKi#OEU7@yj5%S zw541|d;Y)piHi=~{C{$Ca;%$e_vM#ctYrGwT6Zm2upsU9G~MN~zm1KI*t}!Rn-uCC z($b#wJ)d7+cXpw3JD17kZO)BnzHF;@;|`qXn!IDH**y2%>*k)FcFc2XyP1seu`=VB zzlP_QTxYp=!-3_ztck;x{QGjuoB}L?M>S{co%=K<$n=!pS+nfB!YW#p-W$KkeA&3D zsBrE~PzT1evuZ}4m2=MFL`&TlH;7u+jt%nQKn!+U~Ss@vExy6z|Vp z@U$qb{zUY1s~=w8-rQ&Pjf65lN-u+a7IFNv+M)%bhYSmv7o5{=jus_o%|ooja?TtQVA?aP)d% zBO`p~=#uqK8@?5T&dma?1Niy#=OV+_rQbh%;+%T^hO;F&oReKR@1sc9uM4=abP$u;JEr_J43wnbSZoPMaG`!1%OJRxLLD>O+y<+s*E9Zz}Y zWzu;&6j|e!7`oNf*?I3%vC6IeXSVI*gP*<%vK@AAUh1!IZPk9|$W~w>V;W!c(REv6 zUH9_upFgE8tWRHXl$?r#{udlt8vnp9}?vbLdr-z3}PEyjNqw7ox9yl0fURu)j z=hv@auWsGC_375FTVCO&>({Sezv*U<;JPYdAt50_At9mU%{L2bxC^A>jaE;UDDF&% z*tQWTY1??|;s^bMpe; z<1HKSvTRpQIo!s3@P^|_DY1@2H!i3gJ#wU_y}iBs@IQG+0a3qsHb38jsw)pg24B^H zDXhlxFPCpTCT%X3u;;O*!OQcq=AQNcf8W2pK4#}9{<7V+LDzX*eUz=Nc3ppQadCC8 zlxbF8VBp0@W%s@}_ckUUZv;)Ib)RLcDEp=a?r{aD|5HkMBpb?d#F4(i?8+3oAU zwzB2%ih9tdhfmk{|NCm+bf9eS=53$nFebDXoZO-t5kF^^T|mlj0ZDaMkM7=Vh!) zRzw(tNfs)wdPy4{K6l7h?|4p1N{ZIC%w*_#Ca-$^OS-N6tn+8zSNODA&~2Xj(W6Jd z9!byaNj~0pa$m}!OWg|$ISy?;f8=@E6}=;yg)i<}^mz69eNm?6?_zigzROL!a^=eD z_}Z^mAI_XPGkbpZyPZ>y_sMEA#x;ai2F}gcw)6k_h)*?n7cN;PAM4o{yv%2&oV@(~ z=?``!Teb7c#|1}3L}Uh(1+10ViLeq5sRy;3T*YIraKG+0?31-#_Ni4ts$Syul8GIa zNhgC@R!k14+x%o!0PpLSb*E09ih85XY&Bn6H~(V}pV6ji#eaW&Re!(p`8?x=3m3|M z5#jh~za}g@A|qpkcH{Ac{enN9Jbiky=OD`q=ZK#zr_wJS`nO{NQ+mO7`HExt3mtux zA{%Y2KKA8Lnx-GW4>Y12w0V=dn%X1jM;w>93btOY|NA1%F4>tep>KX*8gQ;!1Ln&_hRMV=IhQ@~_+f`|&vc^_;!3Tpr3Yho=k9>9Gs4W^-&a;Wj9{ zbp0A=y<&Lfn;RSRtG>OdTzv48l3L=%h@7{#w(dSKwT(y8NlYgqp#I&Ros*wFeX8Ew z)pc%1+DtudZSD1-{r z`Y}A#eRRC$@7L@4PoF)TX2!NTJ3VXq%&oB^H}oDeWb1BP-4iIVYnoc^k4|CrbF<=Y zR3=!Iy}5CHwpp&#%Fn8nGtEvi<#ao)NC+A<969P_GdqPoIbzyTXJJ=)X{ayx2_#pt!*PYSvx!{ zc)DI}L0V38&b>X7Rh8b|@x8fmP2uvK0ql`KeLuz2#)gE5d@E$LD1O#+CY&whn5ks+ zdUoNQ158)s>J$85wPQyCL!Lv9+lUT(w(aS?i=V&IS|a&i(uK^LhLK&5zc+)jIgr zB_SufZT`uPHZx<%5R{y}+B9ey%878M! zPQK(BtEs7}d6#>-aLAKq&*ZXVrj}NHz7|=#N6bHt;YP-p4Vz6?-j|e=%t$x%aCJTU z5p;@=>+|I+-mS875qz)sx;;6U`}p+i^?SvRi!@)kdUa~<%}uKD8~0w^B<`>G_+~>5 zt2h6ojl7M=-`v=k{Pf1gWWxh@*(26on=sF|I&N>q#YL;dbs{$We?Gtd-iJ@8_4Bp0 zw5*l~PZz)HYHPH<_THXKaUNN#5|{0HciSF?%kMq^@$vEfb~Qf=G%wVBW!d#P$6n{f z#_p~2HyqRRll{DAciORQh0zn-Vv=t!^PT;PsV3*K<8^Bd`AhvRnYXrN-u>trpDbfv zXXCwmowLPPA*YCN&H1y1SIEDsSy33n4eco1i+}!#v8(UlT@KEdgz+>ux6Lw!qc$)G=xbb0xepIux?X`#%Hj>HG<=fWD zT9-X}{^ZFTZ$Ce|e5ad&EQSuC9WuA`_t$b71Y4U_TR-{H@=Ir$aCboEKb9vg39{}I z*WI5Ret&y=`}z~6ueGeU2#B%s$wcg||6ezA`jZ6=EHe{Cr8ek3dh_;_17CjFzBO^L z4vERi&VBZD6T8j+KcBcwF0b=ZYJ8z#bn+b2+UAws)Ab}`vL|d#KR@g1%dC4=kK`R3 z!`9@kESoWJUSIA71yxnmk^<4d2?t%wWaGCv-_l#+n$5qcdDESu<=4(X5;?gJ)XgsT}q@+g;7cN||(2p@Xbl_#lwY^_1dCSkvn5`CKC-~&z zi6c`yet%9%N;;&hq_j)(o|ua>LvL|KIP+RgWiB$ust; zAFx}}dVrx^eu451S)pflZhzrE{=Ai|fnH znmTps+dk`e5n+LWfd>utZMya{(tS&#hLXGb!oH_xlMY$Uu}TLz}o}8y93d#vX9h+5JGPL!?z+p{r>ZQ;>^-$V(k*}@=wp!8gD;mJOBC2=jZkqo6ns2dt33j@^cSrmpa`z&Yf1|`VMpk z-K)sWX@6N;<}y667IDpt*je=Sx2ULSSkr>JYI)C73&lmR2K`>kAbe>P(+-pN#o6=E zrfFy1-SzcpoK?7j!-m^$r%qP$jnbdEFg8uEagW7%4zq&&>T6@B3q-`q)qFT8r>LkX z7`I35@a+Jr?yJ|IQvPt#p*dBeyYPx&H1qMgnCLD#pgfksIREzkXj-=X>3* z;={?kFKSM(2%cJUOw8k1;+#K!YA&`fo8V%(Xp3w97ROof9V$8Mx)pbtEvAQxEY(w0 zO+5`-J36O*9dp8heSsUD+xf1qJiT`Ny{?emTb3+Qu{1X~w^dbD{rlixv!$k{rmBeb zwBOG+e!Va|f8Wl-U$4jC-|9c#&USa%+bYnyjP*TTU9a9WdNZ?l`1{|l`t;T+bw4>hU~xD$jtt!^jffshVonIDOTFeWpPjcL@%)v{bwIptesoGR--E@ zJ|v`MPw;ZT?b0^k3=Dr7y1KY78qRq7`(R=Ce6}5&0y}>5MHX7;CZ@(L*ImfORQ|~D zdTjaKryoClG_U#@$H?$N{maYC&--Va+tH{r z`DE;CAGelL7c2Rz4(62)I-mFm@V-;Xnfv4J>h=3})w;O2RO`p`GcYjvg@s)!2W{sy zYzmk%l~a;mWP{NYg+mf9CIOj$?Z2IJk$GF5v8e3UB?YCT@Xb4(KYdzze{1&j>LvLG z3=DY>ugBN_%{tm8nr?CPmMiPFMW0h9UDg#)U{&!rKH=I&qwCAhrfIV@S~nDbdUA5B zs;X+(n!O+IF*#SyW?I=hX@S))$0IAGB?H-3t~pwC+1LE*=SM$hPg=EV)tO(vs%EEb z=rC1xpYq8e-(01hGk>niw*}Xp$L+1!+S}8^QaIbO?$5{LCQg4-uExgIE}P1@&)Fr% zRqOFDx8&sH-xVMBG&fzj5@K^&H~-CopG5-8&%7=8<8J+)p>zia=TB0j$ zmQ$gkqqC;he4Y4$#c7xN`}_BY#-#$A{(YftQt06N3gW#1l?*IRN-!JaftzueijHukmv$Rx{CxGkS;Z^=&wlj%g43+}%S^lWJdj_tZk=9VZ*S@QRtCo2vuWB=_H}>E*`D#+ z6k6+FxXu&5&9k2EwYE!3yUIiTmlC(WOHF@wclY}|U;kJ8Mt=-rt+6Z7DCj@_9$FT*p_-3j(sz8OMv0i?-jdtFFn& z$k6#S?bwYinZa61bvtjktjW!-JneM;PhM2LK;HL=4Mw*X^_H6iY?Gee)z!7?@^b(6 z?Kx+pGR@ar6A|87aI} z4(2Fjzpr0>?~2{ezu)hJSE&@M+L&-KH2Qmab(zZ>6;C}p<4LT8$U{e#rae(9JiPNj z<-NGrrpeP(y{GNDveaAr#63O+2jwi#P6YWg74P?cf4BIH?G`(>_stQve>7YExc`Vv zj^mig*&Tdh6;4cVt#W6Z#67${?cUz%@1>u2GBYFu?fd<1cjeEYKc}`hG_DAkGA%Ro zNaUR}Or@WjeQlPT`Da{zRJH&Azu$AX#r1N^A6qgpC^Vc+)0VTZ`}6yTv|7s1w-vY3 zEgwxxyX~`KUAw42VePc()8juKsAOb3v(ULcOrtC5vSQZp+lp&rl`dAYByh|#m?8eh zZOPYfZ*MopW=&YLW=+Vsh*piaY=Zlot=dZV?0)`qo{P(szLFo2QCHsYtNra%Yvz%f znmX&q;fqzTEFaiUxV5ZxALByB1u<*|J)Eey&>`NFP6fX`?^b(E?sK)N@4XprBh}_?sHRa z{N3~UQ7m`vy{kJkx`O6Uo?Lw4#ZP922i5z2K9j!WoYrwsT;b5hj*H71*B|F+Xx!bN z&~ZD{-{4fo?MkKcX*1?d-?{hbrFF5pXQ+Bl`?ENFecXpGF$RWNf?{G~pd+B|_LjcB z7Ww^ddA$C?e;&*9l{%kh9;#fdRGze7_veO5@k#}}N{7m3F8x?N*Q&JX`cX!P1FGuk z>E}V0kjzk$O67F>n6MyY!hs2jrvz_Fa3(!)^m%1e{OpYAtJj6SG3*Qsk2Jb2fwuZZ zzI^#IwdqkA`@;8!T)DUWJN-3ZlkZR3kA*+{mz)>n>ytue{KKnWQ z6&kv_w%Ql>c|7hoe|yrBcn-sL{VDuG!g3bkrrp(1QMbbQ^ORB z;R|Vx$U6CJe`Y*Uzv#57RUq-sq3@IXHpbL`z4}&JNvUUVk-`bX!#AF#m2lt8EZtTA z|6c;Tw|;GSGTcX7{p{k5zxkV!`DH9N9PfS3!_dILV(r?!GbT<9 z)O)esrKeWubpt*%>n4j$bXcAdpW3D{{X}K=m(#j36OJA|+L~u!e?SG)ftLz1Mtla65LsN&fuj z%??V8%}bUon`Bho$zm^J2ldoUDetmlNhgl2?FF;48TP7dt`788o*}Xd7W%Y}W8b>;n z)$G1$lyX8qfKOV*-28e^vb10hpIY+j$){Z2Dtul0DZKLAsWZO0xw#9af|IVes_*%w zvFV+r%d-%!h5pOF-SwWX_t&>}_XPIyk_-&|!C_%+{4-fO6$-vHh&-3_y|Yd?x%2i% z|C?{#z7kRkGAnp+;2gJ@PDS0@TU-DCd@TR}$C_CSZyw&x!@w}P;O*AycDoV}w`CR< z78>@{bzTua$#p-I`(MC>XqR_sCm!`3MCFxxr~k-=Zy?)_1miPr0+BaCTO9w*JnCHH-`i z6DCjI{JYou-VK+RvnwKRXDYGA88CK!iD#U~8KUKJ{DpI%n;Tn70lU=76)QY`K4|9W z^GvxIZ1Fb07@?_*wn2$ky)s2eaH;U%p*3oiQ!G z{;%oU%+R!$i5r#w2wtpIw+ml-F1x#{YmIxqTue0mTmnUYgT|CrIv6oWMXUa- zajm(g^mk^*2HC)4<~6r3tq#}UFq5a~)s>Z3mR(Jdx}CD{=5HrZQM+)S)Pgl@e7@<1 zooF}XJH6at^L3BUno;(5-t2htrarAXJU1^&+QE8VX0v8*@`72upC24-?tZ`T_q**W@2nXZ z9<5lk543K=PBnUW&VJXTO{ZCeW-a{Y%xG{u>1*$;WRFJfw`H5pPgeIAJ7gmqaC1xM zWut?Z%(?##Wc8m2{@jtkGTVIT;eSh}np>;c*~z^LYJT$k`SQS9Ej|e{hPQ+!GDtp; zc(L~RlE1yD<^TUUzAs>Y@4|h%tZZ%f`Yc-ainBv0=x^KobB`b7c7^$MUoAX7CGl|E z%17VAK19EA7wu6~Q(Gpzp*5n|By8vH+X@HuU99|d#ZGNC%lKw_-Pz=|?jOfudYn+}*zyy*|pw&@gH8LlYrp1(>lHPU*FkTEH82U z%VLc#WkbV{HqXt=j^A+mb^qONfxjExNZzu_wSOt${r}(J$5C6euFAbyS@-GFr*md& zyUrbqc)h6f){5Bm38ja3`f4pdyYa)1sP5Erb1d)Ie!UugzO|)g%cbf?3=BW`I@`~D zxiL2+!RM3RgoAf`4dzJt6{t&{-;j`+liqpNb<_6U+d#LjSm)o{vpntmyuBKViVxTO zfy(NJxGQx}9=6N>`~G}>{XeB^?yZH_B5&TUym>qG=AFoM(nmN8SZtV_m}=y8Uo>lc z=hx4VS-0a6SH&zj1_muwR#tX3pBWpbDhs8u@`^Y;Xiy6%I3#*;$4{e7H=}nc%B;2? z*Y7;4^OW4Czf?*fqTDPz<$Z zpwX;O!Rfcs)U6SDJ07;##o5#cCvA-~7I9rEyrDCNg+nK?$^7A5_V-bqoiZ8!6rU&E z-;{cqNk3Mefra<#)vJ-o$NOTBe*A5*VVhUh{!QD|R{mdbRy6bX#3w(*E-mwwmblNe zpf8uN{ji$l@hisLetx!@k|uodmFl7^xk-EXe!o{OY*TX2@$k#b%L{kQNv>JE#VFR} z$CZ@xbFbK&M}PYKS-ZHn`1{V+Q495{h26 z$4*D<$Xb`N*mpY?6&F{2of+2N%i{G=QN#Rk+xDgh$GLQ4cbB!c`|t#Xgs6P_RdGZ> z>cTOJ`kZ8?$ExqdgEs8cKCgdeL&n8LlUrL`+iPmpG9*ZaEWC1RsV0^s%8~&NB zyb1;7!Xgiqy-iLOE!k+aeR9h?oy2O1%-zoId>iBIem=E-zvFS=b%kVW28MY{f|vW5 z>ql?P`E{U?`LX0*QQghES8SN8R+KJz)Gy=E*94(0m)~_3OmN=~IziOM)%EL_OJ5ln z1RQR>{dOvHbDHnkRjXDhrMOHJ?Cg?Q8a3nCa=C+y53NlWhadUr#+7E&JmcKc7Z(?c z`_HraIrYs?W(Ehh(qH+vx8**&9$!EAnBcZmm5md}bzV0Cn1*6f{cHhb-f=I5M0 zEG;c9fA9PGZ1(3#Oyv{XpH7%}K!!uS^sumoQWNia;r+X(RlnaW-t>TN)tBru(W&O&^$VkTY7?Yr1Y}?eA|N zCUiO)85w2p&r~?IW1(wU#)DhWSMHp(Al_qlzp2@&A8oU4&Mkg+=4b!mhbulFF=cYt zFmcYDJ!*!AhN0~n=Bh7pnX`|#eYy3)(-Fm0ByOa)&)FA%kl2=za?-?b4xwWI! z?Ok8^7}Vim<>KaksJK~yiRE3MgVP;7qlvZhx=XaqIAhx~_>==YA{T_kD6scvm-z;#rROj@m9e`IXKY zofh`DDV%yViSz4&pUeyb4H{iZ{ysi)z8&Vbe`08)B)oY_u~EcHtA~41XH1%tzv!IV z#D-#))jSrJg#}zb(QMff@~+=0>R_>w zB}ZkWZI*moWMpK{n>CFNu7M)Q)92T2b84Rlo5S}($L9aKj*hOfn8#HyF)<|}AtB-X@$D}yYkm|+E&i+6oO@Y*VQjPBN71wT zUp}9=7niXv%URjKUX+1p-l|orK7$M058=|4k2SL86fgH*Jf`zUV&2~?J3v<`Bp6FR zIWAxC^YqWLH0j$JA98BLY*Y0%ZrrF)V9xMu8sm`vd7X65Ap=McEoa@Uf9-sYwK;LM`eN& zcYDi#j>EiRntjb@-}`;P@8#XyRqD=vPmF<~L2N~S_m0==b{_+Yy<6fr`Jc{&gRw4e z`o8z>I3ZhK;NkK@F6RZ`hD+}rn_iD$_A_K)h$?t+ps|0NUTjs@Tdd=58=qJY;43mW+$-U5l>l{usre@WR8}`}eQ4+wa{vbMj8#_s}i>x~?6|JZBV? zuuzimjm2ck{CNx3e7u~sRczP8Jq-*`{`~xWn_u2;PQJqpJteCH3$G;>WPG0ZVXjhB z?Yh%yq4w70?|5!<@J^aOefqU6T1M~1blvCdyT|j^tNai{)ZBl6-`Dehy{Qnk+Vt?l zf&;5ppXK(pSbBHa+w^UR-Q{bytCyq4`*WO+Hu}1-yWV!U-ga}J_4_@W!7cdd>yA%(^5n^!sau|I?T~S{x+h#{ z{-?fso2lZ&-;-3m{X#=R<`h1*WM+7f$R}@irs~9*4fR4F^qaI^w=deUUG84(_q!(T zVkavcY?PanPNk&gE$6%wJ>~p7+v*pf!TBj#2BqI?85kHQC~9e~x<3Ekm*wH0E7{d2 zO`5cA&;NhFcXxMpYn#;0Z~oA!(YA?$cgp)ma~HY>=N+_@4rKmZ{p}6s3_lU+&UeeczOMIe+dpsGZf(51ombXs%lS55>3jP>9+P%o^{7n$J~smc zgVw9rDxf8w;^Awfw*GkBZ(p|yR4+3{-p%A*d0*x0)-?=lAL2duIHw(0a8pQJoPB*a zGsA(SKR-V=|NlDvf7IcVH}tgqgiB?Vr)@s#%&&WqML@@0@m-Fx)eVluscDX4rk_hd zO=A8SVTKvAK>hbGcZ$!S4HxDvVtM*@sHnZlq3W=)0CTZyX2i?+jt}|*_xP`T#5OY+UX$-|laOwdhLjjSUZ*ih~dBo4#>!P3rB-x8&yObZ_yEd3JvO|KiHZ%1Qj|SQ!{R zwY0TU+?$)`_{~*2LK&<){pI)WDtZOR0Di@}RZa3GPYhB(4+IeSxtVi;5pvdSWn^Hu(E97cSx~#U=IN=a-r{;OKQ1kBY<{GA4GR@**)KGe#6be?_ef%z-euU|jIqVQ3b?Aw4PzDkUOOcvX0+pSr;&Q7!c z^N?S`#l*wc*LT|0thEz4gf4!|RGi9Qb-QT${G~?=K^uBOi^29xUw3@VpD&mFGkbP* zTYQxYe5KXH`>%Dy4XfFf>#259JuW>!1 z?y}i4_S>r;A0L}LJYr@LIG(k2-qj!LJQtPC+i}-p6Z6b{r|y~w?|L>X8#Lkb`T6{hHe2 z{(uepB8Xoua0q9Jhr=OmlK7P0A_1Ya5 z=1$)mt1@|a$HyL%ryT)uFD|TWe^%qr^nHC{?THDB4@8(57EIi;XHP9?$6Sn?TS}N% z3j67kM+{shW-K^x*lmS}!Q_xOcB8GSOL$^+?Sg}Yy|?AwHap&YosD7H!aaNbT$$`| zXBiqOGTR~E_4JE#uNF&b{1utRnZ-8avX99N#m6o=f&w-rF9cEw_>KJLSZq9&wKa^J z{4?;iGQJq zp836s$4?(VeE4=xh3~SI-{0OY;dysr!2^egjt9vR-xSWPt4w<15dCb1LE@n~yLRmo zl8;emU=hi(o@rD0DWv^ypt3^VbJf&`Ki5>&ie&F!SlF}ez-`Z-&9%S3g}Bw9n`J*M zG3WB3O%Fd7U9?&)Bql3+_x9oEd0AZC+^P3Z9n*G?uoN`^btd8;>%^Zbne&du2nh=Z z>&5OWx#0VppP`}s#EBCE8}$X3%L~7~e&G6J_J#woLTx+dei2zzCZEfB@S11jZP%8! zt2j(@UR+qnsqEhOM?p)AORbuLfkh{FciG?1psO~1g7VawUv6BTIqy^ot$jcAX&fwc z(d15XXvzJ)zj%IjNw%tkiz-2iP@lD>=Wp6lU zmhn!SJh?cmfJb?&WYmP2ZQ1b*TlGI|W)ZN8i(TuwWFGUrRbG4uLXm%Lab*%f%y`=aNr@@xu@TjuDS@9>fuDkczcedHy^%`7X z{23e^1E=f)T^o28G-_s4b6x2qZ$u|w#a{`fHOieomUR4WnZ9#c+1p!=o3xm3C*m)0Lujc6P_xr3}0xtLo zZU6i$!9|lhqhwER%kvM5rkY1Tdi+@N0~doMpIYAXRF`RjyD&yA=l9kAezrDh>nk2*P40bq zubG{i_GL%icbsi}=~`sdBeBz>;W2{Jpskj^+l@6eb#+CzPXBQKe&@|+okqpFL6O~0 zk4fjZRBU$Sle6(yz1;85x_3g)LTu!jSKn9t$ujd%+1Jm%-|s)~>FfLV*5vD)Eb^kF zqSGf$o?Kt3mwtYp?Nj#Uq7!c9Y@4)X$&w__|Gg({dQI$^=l97AK4X9Wz_iD1P2G9h z?w8(K_iC;duty&ON;%AY$fZ<5_KRTTx++Log%xwp2wWPU8bz@mTi z=FQ!1JrW-URf~7`WQdfTJoM9;X3YJu$It&u@b2>WVJvT0uWJe%lYeV!wb3N^aamjQ zhIbca76<)*wR-)(_f4$acA(CdxOt2c!;Gd)n>NMIuX?pobH;ooyaJK_q*^ufnK+IZ`_O5AL6x zTsMtn#rb0%o}QjduXx{*X%UrhU9!vj7W@9$)e}3;E1sI6bm>`?%=L}Q$M=1RmQpr1 zH(#3>dcNY2u_I{SnNY43D zYVkaPch)c0sO!J}#NJ-N|KG1wywYZWR+hfL_Q60PVaBdqyIyTfKCXB6mM_c0sokZymXa{1k1^`b2~hin#Fo6fF2-Y1(3 z+5+)$!cJKR24i7K$;zi25)VJiyR+ltWBb1^{bM<1-pP!-8(FJ=_F!|yYoSN3_0B3Q zPH8>AXZ^LDlbgHy}+)AcTOFzz-;fHQ(Ypc6l?U#$5lOB~#+Yq~K zzEe-b)b+o$4s*=Ye>p3YF)mF0{*I8aFekR}3JKTO#hz9*dj64xV~N>`S>6rDZ+$+& zakF(ABQx8V&!0?Z^qo3&s-!o;FD=Kp>F@5y4?E^R?X!N@!OzW|{TMW)WnK10fz^KE zf?Um{>_Zg~t0R=O_QWiawff$^E%Wj-E&lntUs~4wGI{^?qf#<&-~FxDeZDSI;-0k= zSIiLG9_X3>qj=A^TiKhlva>(iRNQ0`2mty2^Xv8d=Vd4)?-radaOr0G#b;BN(ew#`K^8K#utF5l+?qk%_);{lfeZ{P@^^QB%FD+pCm4mTiP)&8uD;wS+Mk_IhNH8(WYYBM<=Zkt zx6})3x=-16HdHLb(0Vf?Q@Nvtu=%Z&KJlE3TzZi+Z+m)o|9!vr`#s}}7ccI$tpRnW z{xq58-irDD?RNh9*j*(rSIZlHm`<(rsh8{qoJ=)y76drr3Bu0`jwE|*|nQ9sow ztiDgevgk>=RN?NP4YBW6$GOfaIQwDCM@_*qS6){rTixbplnB4ME_SzG&Yc~FZHqMc zPE|5G1WcGd{rle!4-elD6*;<0a9elBZBPCQ9UU2k8Gol<|E(pjSnRv!_jL}AJ7@hX zD}UyLmW>H|Ha4F5(`a3Ofypn&(jb0S&jeY{HNkJM{`m2O<$P>tLBY#Qs*#2Uxp8`` zFAN=}neDsfgQBCW4M7u1NjwQxYwuY-h)t1GbDZ?p^YNzx%=}+CI5|5%JTjfh=Nc## zwl=Et9B&g-*zK>+za6)Zzqdo{wQ$xm=^$bGGjpxWyJj7|dipvqL!EnE+`WGjmEG&~ zUN9%ArYGK<+i@x0^X5FM45q|AK3P4lFD>Q1;R7CIj@|uj&AAz@kFJQiXnqd&C@k9( zQw7Q#9bH|o9zM-B6lG;)RVocR60a7#{BF{9;eTvz>pwpW*>Xf&iJ#Z(%zXR*1&)pf z85dt?W-)i~lexJ)>*^}wiob7eZcZ<^x7gdDGDFUY`5!mOox98@9osB~HwzrH((kV3 zp7>jL`yHYCyUX9V*Oc64U|_IYT>brBKj^^6$=d67D1`-FI487AS-AUFsy^f>wUe}3oHE0o-Aiu;la=>e&mYql@i_g z_5Xf;l&}9&xNrJ8afUyAU%q_NJ#X{*%_s9pY07tY`e+bj2+5ZBNLxq@5p+Nk3ex0NNgV zZp-brW+%%|Naz3AwpPlc=0J4CKE*7vSGLvPQr5-q|7YtO_$F=#E5n27BI4rmwcl>0 zKlk|2Tv4WRT6{_CmFXLQan7_<&+a=Z^F1j!`CyunLvC*Fw5wTb1!eX#)#WHVR6S@4 zSy!F+?a5^SxYUgi4yV%^zI^!-a&ni)=YzkL*OawQ?>u(B!|6Mxuv$%2czF1u!`ZA% z5~i1b9O)F^uex=A-rZe}Y~`{m_-C$--hOVyeIda<*9gl8=?9iL*l$@bAegJ;(b#p9=pPl(T=}}pfS&EH@f>w@r(Z4EfhJGr@04&ldItsyuH{u~{K#r}tj8fMdC_OTN%9_P3vX@d7Spu?9Wuzj zw_q*9gYp%RzNycvaQf|LKXJ|5O*3!1*4$XM;h4t4kKJ4El-v z7)&*uck!6Dac^!EXobz!>H`go&=odM_s--s{G}cB|Ec%xc~6b6t(z<+Ik9@Pg+p3e z+RHv!YrAicN}1FaE?c(j$ z@9VH{58<W#db91fvKYe)0%)s!&^zQER z{r$=r>8W|?tm0?6w0AUXJQf#{xPK@)DXFUZ;U7kZ56oHF*~Ruh9yAx8o~HXd)Ii$E z|BfHG$OA@Z^+kT$-2OPk959+z>1OT?nmgS7gBP^qa_aN*^Y6cTn$6BIf5E=Qr0eTq zHy`Vl-+#}S|KSFs(vNZ{9U@+S;2RA0OvtZFcF{JpX2Y^~X&% zy({LoiW_HyrEQEjaQ+e7q)C&4W{0I7V+ngOIgIUx*ew0%%Tg6ii$cDWL{M8J>Ak*PoxCUk^InE!sa;JHhUX zd4%s=tI|SOS69)@a|RV35>DJ(C*0V6#v@hfuh@^uKXaf4O!^P~HjmnWoa@iTU*FSy ze|vvF{=>7``Bl%j80Jr!J9lsT^K)}&S{a9T*%dT7=QgNi&)?VZ%>3y0i!E4t5KH2SWt4qtA3 zL~75+Mlk``2+NoCyWcx82${&My_C>&oLb!3)zt-B(9Bfloh)Np6>>gW#O1b{sQsV2 z9(D>U7rvV${MMN@gI(!LEPGkb=C>aoAFsEq|M%xb-QQol<`uRK559|tipqk{9jJU% zCV0_WuT7;>)$>D6K%mQUn}tiOzrO>`LEYGxY_RR4;??LSE4zP`ZC)C?yR3G>yjRB$ zm)Q#Q|IgErH(vKzA$EC~`K5+htLMJA{ICDgUccwl&sCwTe}Q(Ne|W;m)Y$*ZC2n8M z&riw6`<`B`S;Wl7^MbE-jvb4fQS#S(rQ;L&+J7|2Gf3X)Jvg_|M@Kc{Q2|u zpC5i@ZaBVT-MW1SrLV4p8y4<%{=ALzp0v8hUy-%Cj~IO?Df0^6vbi8LYq4PB_rTr$ z8*jfYE!%zfRqJQwJspcQN+jYTYa8%^{(osTMMPv!Bd3uzI(_{?GCsJ~9X1l6qPF zW25f0YF+l`#HEYRJnH0c?detb`oKA*(D%xG)mQRKKigLP zTeLCxc-_p26B+ZrGyC}Z`JKD>@Nm2T;UjOA{wWB~-^xrj_Lk|k(%08M#_bThJZb*?er|_Agif&~{V>baJ32es^z^H^(9qDopc~3w7g%K7sqAg|d1Gt#_3NMv>^sAF$F=G&FD~BY z_cK+nEPUkB)%P)chdlEiae*Du?avgFeeZZU`P3+O-j=ri{pDrx(&_6385j=S=Um8f zJJPAVyzBm7HXn}#>Ib^ywYQYKyu>;ui6J2gG)j1FZS?ZJ-)?1V3pjmcIjG_N#IJg< zymq#=ZomTSFgJ6RP;5-Pnb7v-`^V>lUbQ^ zCw>*^>bT&*y_J{E^Y<4srEltQE3OHFR>o}b=AATa)~sd0YZ7lcOqBkkb7)bS&y9=c z4^L04;^O9ZePt}P=KLK?*$2U(O7!iP`v3nXoH?uzw)*Ru7J(^d|I>d4GD+Urx~az5 z=Ht`?BbKjk>mnl~Z`W4*Wq4qC>GEasUJ1h_KBpCZkM|#IeY?r2eBw^lMLClnaJGLe zvi*Ga!;Bd-?oHY*av@=B)aBW)gfCfcQ*`^wF=b5{`>ttf7xEgpr9a^ z`$hsM%51amotWj^d2V%7P2K;$-y^HPzx#W^{W>=bgSy|G8-M@(elLF@$zAuE0O+tE z(Yk_NjP0`|(jOn|6)*Xg-*NG|qMzjh&?u+ev<0y#Np0!5(}lKg&%eLVcfMUMTjkGL zj0_BaWG5Yn^%pA-?zmIxB(+~)Ti^R~U0JIVhIVF#8BVx{5vg)sAW*T4j>bRA9gsmg6&*V4b`2#}hC%-Al zG<-C-{N7Df9UYxp-PcpxrhI*U{WM2YWUj{&e{*Y-#`X#GL6vOno{DwLRJ66br`_b{ zWHS?dYONCU`@utv?~j(X`?x$VUzhw~t->))4GoXQTTCS#nHJ>)f1Gai;@}Z>6ZwfN z&fd<@-v8kc_h-e))(K{QbFH?Pe0gzkzPW0~#LDjZD>lqcEw_63vo7+)4IhuBm~5N8 z7Z(=p)e-a7-npAA<6=*)(m%F|mKujnb^iGHC#WQ^EkAgAOAE`4W=ZGB-DPX7-&6(0 zuXR_vQadqsVx_3|HaBm%s|MBI-Y`EuH#hqI!_#a`psj^+b&q?^%>-YCZ??LzzyAM@ zj*boudyA(HhqJcMYi~c(BrdSyb-QZa65mTMN8W~PtOni5tP{B@CGGaM-1XlsnKCgj zuy9qxYfsuK`+r`f>syob>Dzjq$1IS&z5{f0zzonPny(*985ka66Y;fmvF3vgw&G{2OqSkI)9&x@Z?DQ~c=hVl9Z;j#{KnU)KWEp8&vc%6Dxz3x z;>U#>-z=RT_iNFL6)z_6uao`|ylM01`oG8R|9uni2{T~AhX@&zSH*&Te64egdb5TX&sPVlHj@w^ToUct5 zotY8xYw||T85_N${fc*dY?@is`tB=eKJoeTB}>+P{IHgRVf*5JpZ?7)zbEu7tCDaTU%TE@^=3I zy@3YRyFJ2eZ#io$X)sEy%!EwcTwvD&nhE{)ncUDwDp_9$d^D*05KUN#=?}~zs2SpdjB>%kozV5o6 zlG38Rpb5q|x3+eF6Jxz5!&`W3+J27JcUWJ~iu~U9RD99TM;+fj+}M~reXd2}BX0Q^ zYla=IyLRoGbhKN%f4*h$vp7z90sW7Rr`ylYl2?}fD{?+<(M^{<$x)!Goz{qXj^Ezh z^%mZd{jj&z0H@Te-G*@aGW@C z;z!z-7Z@^yH`L9ti0dRkgqO_eJw zE$aUKm<1ZP<5aZvys<8Ky@!H_JMTT#3pIz{|2Q50?@;!8li71pnm?V2^={{r6?$N9 z?B?g!=V9hFt%JGnSMH^cX&L#8>cnbh@A9wJwZD>{91;?;WaFk~o*k1u6uGRt{NI+N z&N|;gIy<5+e%gh-H-9}i*u0Qe%0+aJnyRYkYmxAar##BSy5&#Zud^g^mumNtJkkjgCd%#bLY~V$IKR1wXHpE zGbeH9yIrsK__?`v^SoMFS5#b_%Cf+y%$9TgjjwZdPWd*urS$@vw8?L&4gSv`U65(c zUp#BptXJFbRb`)=V_9sMb#+y!`}cTL0S*x{v1L6HhL7xhAC;+#t;vhpa&r0Z(%09R ze_7PNxxat@HwM#3J)76;>oMUow`Zu+-_KyQD?8_I6xb9SJ_Tc!0cb?tVw?LcKbl4cw4y0|4joO}f_XubY|KVpoExl4>4Ju?utF39I|v*^_;J z-OE?mwYD|h3=ZaHyZ6?-zP2{ltyk*n%0(Jmr)>8;k&-`Mbi1|e108lLr8T<+UI?Gz z(`|6St@bwi&z_2pNeh4b^M5cioBj6D63@x|riyHve9SHIBI9OWC+V*mI=?4}`|(a_ zZ)s_9cXw}oe?>vve_l#zm9S{`1D2BNLrpIhIh9@fTvt>^EUm|)YZNE`NEVH=a1{W?Md_Qx}!f$zC-nm zs&4PM&tG0%E?*nJ|DSC8;lQVhe={-!t?-PJt9&xC9+bQcUTy2xG56{IX>a%6xoCg# zhL2Lyqhnvo-rjO`cXKN%J^YhZ*Ox!wR9|k?8G~&XlU`Q6Stj=T-OlH78=84Q$HI4= zJ2;{9ZPl$io+);&v*HDhPna@g$|B3+XL~}!!^7XOU+1Zq3A%1+e&w^7{h`;*j!$2^ zHd`_G_O{lI5$t8V_ddF?&{dH-EOPObU$J7P`Ib_TNY!2TSu=AAPK+J)N+a)zp=Fb}!>u z%i?E$J{%Q~?+M<#w(><7LqndIw|772q|BPke}8_qZ>;OsqNklDUh1TD&>`)>*I+rf z{Dl6OIX>Rr%?wSPKRzCp-ybAg>$SyO@**SC@~!7DDrcp4wg~R~50Vku)GOs2IAzz@ z*VpGiXQ_8}wO#U4$NNR1TLu%~toUU&t8Q=072Z?Ow@l5{)O4!B^vROi8DD3x^r+rD zIG2CsgQsWz`8_{3_u%!+3EkrQr@9zYT{tb)8P#vyt|Ba-aoVHX;}P>T{S$s3Sr>Qc z#O(NR!(G1ii@zyn8+$%GgM^dsnZgeb4(`mFB-Va7@bE|Wk2Tk;CU5?b;~hDFf=>>g z;Hik)y|2N?i?7(|U$1Cyf4`&PMoIGQb2}w3`@OokS^dmGo&_3Re_yZP?>BwJ>%av! zzRvj}b-qpc>{1ukw`U}7@2ma2$^Q47&7G@$Tsyp-;e&EfadCa+yPeOUnV)rbk)Id* zZr|^BdRdeB?02*@EZ(z6M$U-4$l<{{`DurH%pxZXZ3kU*^>SV8?teGkud_2S2pI54 z7%VV8>fkoZ=*Y|6XZub#M=3o$`|iE3`Q0m;Ha35Z9xrESXwcsfwKfwptoGbnfA5!> zXVX?cOUV%z+}hpY9(m66NWlulL%X^e#CkWm2<=HwJI|@ERFp5s@axy>^}+Y6Uat*b z8}>R9v>Z)g5u1L$to61JACJq=pE7Aukk8A07v`Be7S9qra^$v563fCfcl=*AUfq6i zTP5Q@vEOySUaAYku~=Nadi8nwxj8%UH!PT|Zk2F#rTCL$tY4f}a(jT;-2+23Bhx+%GJ z-UE#XYAll~KU|o6<}j#BClD^FqN?hu%`~O*K#OS^OUZ8I2|pTC&m5|CR!Qv5Q2Tx? z_4&EElF7$`g-Q7yc}*I{J2t-Ra48 zig8N%S<H{rv?ReA}|c+&Tj+?H$AKdN;#C#O9Nf%GLBR_A#g2MZ;<#$=vcQZFIEZMhjpU>G6 zT5pv&7A-*MIX#o4tv@dGqFO zo0@2b22WSlqccJGu+B6{JjCYqf64OmN?${F6#GQ?Nu1Na{3Y0~Ma;(WhHd^f|Fb?z z`ZDL)+a3r8oq#j*z~RC*hW$^jxb>8Ezh~alzdNV+`oDFtyPIOOCNwoMm9umStUj+M z*zkCV|5Y4fcF}N;*crJXmOHmc^=Df~ajN9R-KV#mJ9SE{ z_RYrQ_x!xQqhBwrW?+cfv}w~mS-YAaH`YdPUl!c*cG14xu_!R7Mjf{zr=`iJ)G;`)mt&7Vl z9`UMN+|I7F=}_d#``(dN-JsZD>oTvQ;&t{ugPkzp>bT@q73EA(w6OH0l-!9eP|L4;r(B?kd z_QQdXTW>QnxV$N={%L-{#`x_~@%TR{vbIJ^?o*i7&ymlp^r-Ao^A(S4+YfFu{%W&E zwWy~1{kGiOVo8;30r%x5%%2~xX}Q0|X?F6jw>O@(x#V9tl6hnK>Ob?U-@O!*l9I~& z@Sn}}=FOX$lS)k_Ii9IErW2Rr-a(OxKcgl|Jc<%*ScIw?Nzgg@BY;p#}soV z{hTNBF8nL<(|*g>fJ@(=oSeL!SJLRonH7PH?{)R`w1mH4lv}=J$&(*@DnHxVmb?g% zTb%!>cGEAtJ@X%Z4ez)d|M0b*&o%$T!1d)H9voEq>~qOikaN+NAG~YAvI9#>zJyx& ziGMK6zP>J2&GNbeXO#<6u~K8I@r{aqZMW|&I%c&mdV5}O)YdFf{{8a}>;8On_h;$a z@pwsRt+r9p(zM&4!6*A}$4{R>2eZ6dcjCsuRhK#Rxwlo$=Pz_O;$M}&qx$>13v9eW z5fL*E&D`B-w?HFF-b7iT&YE#H=XcJHdS1umTmJK3+Esk}rF_t{Gc!LQ*5ChU66g}3 zKh{P@pS(`~XJ%;A(9*K9sr>Y$6133mnau`AZQ-w(pMPnpJP^-f%*<1GV61r5qSbio z$8jDh6Ai{!D-8K$t&VJZ z?fOcM>w?v)ghG=rAKt|4UbCDx{)%CL?NavkR_Vd4t^cwQwQ&Al{cF_`Klu;opmEY! zrrCBe`)VqKf(2K`J=_)GI{D3xi7#HfSg~S-#)ZaL3$|@Dvo|;29;0Nopf$9w;;-zV zxPQzk1?GJnj*2=J^X=>Z6&Ds3Zm)WIhmnEdOxudd?o00U?D#Ba;&*28qD2lqEDUw$ zmU>U;tN(J*T^}?=dF|id-A$;5Eb;_HeZ-xwTyKqCP2Y^&4wGnaQzUsh+<=%eT2Zr zy)CZp?#&e!6g)jWH6!k(*c!B2JZ3-C$M{FQ{71!9K3OY=w&><3PoF-_51deWgT<3; zb*v~5f6K0z#QuTp>({TT)!*OE z{dy(X-`BSMU5wySdzHuHNk6*^ZU^=6*z3Cc!*aWq^Qzx5&OQm6Dp=WKH+^gFwvE1O z@AQ7v>URB_u<(f`qm&Gv``34OcW;&~HVC@^ti|;Gf$g<%>Y8txN`2St6nA-o0#@&VyH^W?v+)Se5s$MRAEi5E7@9PIkW(I~CeV{`n_p5)| zy-D&RgD2>c9q<`bbp=`s3<3eh3i>fSK1>Cjy#K0C*4l{UQNXklbH7=IM?Taz|GE0jTwNhjsE%j z&CSiE}`~G}79ls`a_qVgVcI|TFzptdwaq`@`d$w6uRs?QN zJA3Q2;HJ%goVNVZpZ$a3X!nW1lSRQV0^qX(jYrjB#P4euEZ{CJjmAtrc{KWTW z+0|EPxqVpqa^u^x3D-COIlHM?^Wx6n%{!v1s;WLN?zj8({?DI3?`sSGHb|d1d)D^H z5n=y5pu;y~4&7-MuRrwK$NB%izX#7<@vN(>Th+La*RyZ`x>|w9pY&6H zzOL-)d7~3(t)Fe}+2Y2{)xpIzp`$TVtvSeGfu~lwgxX4@!yqF=+MyCw*Cp~li5^#dExKv<)vl8|M0@A71nVW6crVfy}!HLJlY}UKy^dk)wIGDUpa5(9B|=H+2!!MO{(70=ExW( z9r50|bLVc`n(hXL#oM--Ro~rJDjZ+)@u=m_ojaHE*l(NY!oJ0>^NPJd=}VE>o=4VaOxx&d(!JY5Hn(i8ShuQ3VZTXTEb+<8kfM zbG9kB4K*Eja<7!X5MSswZ}o2|{i zBgNTr{e7pM;=Hqtl=;<9xMtq+Uw77gzVFOiHMF&~Rt31)gEo@C`~7}@ea`D^Yj?le zQNxgNByW4Tx~l5hiqB`ww`cs~pJd+=e(aOz$C_JJH8_J=w>Da~UjBoSsj2BqZZVx73CH_n z#ZBI_1^&v{70U@U`TXhF`?41o808*aZ2UFh=BcUL=U;BnkK8YN@BGKQ_yhb6o=wjc zxNdH&)%*4BZT0+ee|`pr12$Xo?podA_fvk+0lII4eK{k8WACh+YL=Fj$N&8N{Qb$Z zXVZ+2B)EzHdH>LDu^hXkuf~IQ4;&UOPH-xVdD5rRXSsIA-Ji8l4#I(T@{3l?{qpmq z`g|ELA0MAthRMgcZL7XWByEgndYY@*;0J1DuMA#(&1Z(eL+Q{LUYvGS&NuqQpZ@xgxv#_$*?ZKVDcIS` z-CE$N7rX0-bg)MnhP zjA!aT9b}h(13GN{<+9IQUR_;Xmr8DUYjhkpn|JpzQ^ko37Q4&dDxE#pbHhGu!)sBm z7~bWK-ZD>fuJ76J8|6Oh_fwI*+Mp$c7nR-ndMbav-L8J*$dNzi4wy2^IRuJ4{qys4 zG-#vP(nT6(Zq|-B-uqr^ZA-Y%yYt_#*ZTbY{Otdi@~v9E+L(iX=F7K_GaefXFf=UW zWME)mv3YxY`}?y6Mx8868vZ-&PCu*GTv%ARR#{1@>cLE(&_UnQlH%g! zi`n0?CKz>fb@8o_*!bwQtaVvR!5=#}VFisI|G0RUUq`OTN_^pt(?Y+vancp>y)puAB?tc-MVvi`1*OX zXUypME#&emZ_LKZN>Bfrr{{=&s%3VL+{}mn; zRb^&n^{dL>LX?4lVe^B)=F{_!i^yJ>Q*z}KXh~r$@1CP6-x(SHNUgqF^$m10n3#Ut zpW5Is=}FCAZV&vd+rE{1_C?ty1wK5`aBBPgy45Vs@r&c0@BM!7b>76?twI+;ciZi}YDF8HJ&R}O3kx)un3!BywtRU#=*F75 zxf3S_9(yr4qtUZGC1<)|kisGLMLFg|I)A6kclgyS^Y!c3rJ&towH0v<4Ez6nyS?kr zpFdm&&3v=AE9FY6zj!WD*I?~(NF_S-<+ZiZ_CI)g1A~GlX(a@A^vv}LZi~o0U%KFD zz4gSplcoOce6n0$Zz`1SzPqdX`@5X3wc18~8;*(2n*ZiM*Mse!%{~6Hh5cXpy`9vHW~-&&6({K11Ii2akyQjE(FBkmJ+x>o1F+=|+pKIW&;Gm#upaU1y9)7r? z@D}%?E$lH5)`@Q0owQ+Z_4j>3zbtB>Jb9uL{H((5kIfeO!=Zfpr~hY^yz{r^^Yiob zUtC*zI~m+9U})sY4mvb<1&fgg`wnXbzkfnPLcgkC{AXlXa4>E2(`Cz-zrVjJ_4K{k z-`}1F$$U0qDxcQe{q)E6WDTXZgC}@-7N{MclKyT@eNw;^zr~Ejn-rW3CV##C@b-=! zdyoDC^$yO?wf_F0N77jC-TnRll|c(vOYR9NGz1!(u0He)bRRvl`;B$s7vA+9P+i&o z=xgLzj%{_P*g5XrzjfSTSOlbe`Z)Q@{|A4Ax_{kS?k|60UFL*Ut5%7eiV<3WUL{cI z^mLBo4*r74>nDHTy=xk1W8q%Zw#W^*=2xtgGN08*PGo2Fh9&G3Y8ChH zcq?{n_OM%c>@6#|m`v=3go7V7gO}a0E_iTY`Qbx{ZteVN#>nX9>U#8L?CvsEy{IiO z#2xJ?9O>uPkW}B%ukuRGC#TP<5>p2_QSifudmBFxsliKO|)*B^zDf2 z4qxw=yt!d$q@?tSOMmWSO;Ede-b0Buzy3Qfw7JoKeyzZ^dktsjdgLEgT&w)%U#2`y zPT020%gZ*;HqV#)ebHUscIJc$A66VqVtl(yTvJ!qc3;B5rvGnlZ`W^sXnf$ej?j(+ zvDW6-zZHL&#x)moJI_hBU*8 zfyHj)#*Nnt=CDOoOrDj{`YcCrTKTIhoO6;G9w=sIWf|$k>?rurE?;M{C-HEbN!R8n zYOk(r_Kixn%*bR@p2Wj7VOuQA>WnMSjJ_?FPd_B2aXhT}^PP7gi^<2@UdOYbdpejWJo7406ReVfZ>S(C#|Kb$4 zR_B-Sj?3YW_D`cf{F|UrvTDs5ot(?de4EPsI4>Oj`1pAE&9~p892=Q3?fBtU|ePK?#%^SST=cbsP+`jZ%TQdI_pEbR1!{zk-!IB+2emu|L_cN_D z_*hxk{R3WQ3bWS6UR&O)@_eiAhler_8GVw{($a#0f){VIKTnsE;1B5P$*rh$X0+J$ zF{k{_zUM!~qOV*&E?2E{&^N!1JL=Nq%brXDd+rNwY@Lucd&|4+9A_40d{%qAXN9A& zc~*jr!ivAQe^}Y&XDQ~D*J*WK`tWM?dcEJ5=l|Q1eq}|V{?_d4@7&Dqa5XfZO*?&a zXYunxpz>wKgFB7a%sf1g&iSgv@;lDM$LGw{xII3X9~4-u+Ma*E&T&J%@W+oVCHCTP z8VHXo;ACo+*Lmj`d3O|NV4Yf4ammNo|Mo(c*$Fr#M_P+Z5jm z2X2dH+3j$Eo6&btvfmf;W6ZObELfJUsjaBI(_#r zzi)Jaf5pm`Cv6HJ9a&a>zqWk+A`P>q%bvHtpGe6({@XAtUhP)8qSyYTQTc8gz6YCd z)K2e_Z~Zon^WDa&Q>VHL{b&DRWHwtj|I`%C`<~m%TQ97W)_5CnYFhKR*WnlHtl!N^ zInp5*9;Gua%+4x57&Q5}di_2xV~uMCe=ThvX9^r#dpR&L@Pivub4p6eha<&5YQ=fu zCO1^cT1==B)P*++e1p2+H7ci;>6 z%9H%}s$Q@C`taey+aU{ogH939v9Yl^$Ge^LL-G~Nw79r?jb~?@=ik_r>b-adwMbjO%aydOidQR_3+ae)zjhRBa1IOD2H%(5-&|S;5J9nm%BT5#A@f1_kv38x1vHqLJ#L^n~Gn(di7*e{(C{m>2q0J>e;riu)KTM zn3{5-FaP11o4d>3_odIT6=QkVa6ftb?d)@NEQ^<GR?dg_iS(5u!J$tXDg2iwBIF5`-Mm!n+*|QHa&){;rb1&!q z(Qa|=D>)*Cg@v#0)qFnt``7pP{~anSEP7NK`(~vrS+U~94SxGS8!8_jYF%>O=X1*| zCGL|KPrN$(;UBl*FTG3Zi*j~1?DLZ1PK{05-}|8;)&@PUtQ zz4eR0*0;<3=0=&Po|=;X`t@tid@J_!yg}KGzLk-^gJ}!FPYyR!)Bh}emhYmSi zx~=);hu%b={cIoY|Ff{X`xVII@^(w7^hO;+!;fz!``c9-+Su&bobE5n@MOd7w^zmW z<9tq^KY!k`>`lb2XJ=<$Rez_OeSKZ+X?{O`hB+tvq9TNYHakllj!2$T9H`!<*v!t~ z_U0{vLZek*SXkJy*j*)>x~i&AzkGUnx_9V!a=bH8^*Sh*;8)W0Xyu7sTRXpz9%CWFFf8Wn%Z_V$PTt0m9;>B8< zg5M1MLGkhNRiKXb{$H3=Fj|o zT1Zxwmp@5j!J0KXtpWC5Z+&IV|1VKtZPeek%>Uv}p?&sKHuzNu_gRJC5|WeqcXvzX z<$qlH`!BYiO*?H_`RPfUX7I8%K_acwOI#P8(H21UvaDGmW$|ryG$l=&}d!$`dzP9J-<`B zh(V$GY?}6&6DKU@&7C`U>dcvuHP6n>T=eA0lS#`OZ#30|ns%$DZ;LWWxE{+q@M>}7 z)Bm;#2CwbTN*>!Ce)ibvyt})28@BN?q{#5KZ(X%YYx9mB7RuJv)?ZIdRL%sgr<^E~ zb8%Z_U-eD@iT|}<&2Vh-ubI2$ZPM{RS*|bIDOakEQ%(q!vzg4zV4i(~*Qxxi)7e`~ zITx9onPXYZ@ATsL{=aYY_wRn(XWdrv^3v1)pc6l@Rjy{>U%W4A*Mg* z_U!5?B?dp~a>3{3SEiXJJ=(e7sZ&(p)ZXH|w}1Tjao@4l%q=rhQ-AaHjup*j@hMFE z=lU!58vdw@iZ1EZ1uC@Ba+kLjD!2L0{<-b>qh-RmoFx_4Jm1!= zZgn`mAb*A9Zcv+fF=$ZkXP@=E7vF9q_uF1r?A~wf?CgAd$H#Asa!b~&dv|GP@$<5^ zaeJ$-pSS-XbKt}06Tey_ev2McUtjMyr_Sw$`ICco?IzQg{0wV(Hp9@+&|shBfxPYU zHkF@JCN8jV4*fpC^SIe9o1S#`ozD`?@0JAbzHs5fxd8vK#V;={y)5+ASmRCA$r)1F z@0MgFrFaCFOV8ToD)cBz+{RMGG&1D!?fiY)B6OnF{N_|#=QY3c!6PLlg@5jL6^#Jx zl)}P~=iU3|?%LJ=`=jjp)+ue>!Ogw}1&zB~1JW;~PLI7N4_aAxYisxQ{hhaFtf`Fa zmS?Zi|IcEyV>kb#iHb=vCbN(HegFU8`&+Ht;(XkoZoolP2FDhywDl`j?wm4f*018M ztgN)8q$HzMfn(}ry`^6dvdgzzcV(<=*3u9yS(UZ$>%}zp+w(I0*lT*SuBjh6di3Ds zFAf2VrA~hR`t|76ty@{2K7G2gwY7Do7CWEJjtf%^`<_WCZPkyn%%2;o_Nm;Z*Zz;y zgahh}a?B5FO0mA!p}62Acb=I-t>alXMnmh&jTzg+*F;R*l617|eo}Js|Ifc(ufJb- z*7Q10UteF_8n(Kot69GP-fqADO)p|YLVaCb-O>ddbHz6tIbBiTf5rdgm*|d8{u7eiyg%$>xDhykg6jeUK)@=U$nY_s-wFPsVaX%E?Lbtbd&r)hPwW3*Kw* z;FuL*r*y5#*v97B`{eM2qPMr58{?)5jT<7Q8@6SHkC3;`ZzV44udY)p| z>!IM?5T>ig*#W!~b!@|i9hpZi#G#<&ON&I78eC~{B+fH$Tx!0p!{gAl+qNo4j z&-**;_pW%%5hwbgbZ?JBISY&9`o&2Gj3x&UGPWBiHcW6}dcyj|w>)lciTd1-x&K$0 zU7dID@9paU-z=A}dj0Y5n$Xv8Hoq=imGyqJ{PXL(-~PQZTfgCQ>HOD*vl1>`aIL-1 z5MVX^)^nfdmQx%WEE+j2g)KFw*Tw9*A1?T-ed}>A$9Ip_a@H93eOvVG#J_pA)oknb z{d!fsTK@a~KhyskeZSE~CKzP@JrsagKdgNKJF zH+{1_E;?lvhp=y@URc`BjF;B_JAXCIS#89{vi*7N%DZ*zK5xCYE_V0sU-4{SONBni z_&%w=|NCC~^oo^@RQ*RFHRw{H13<@~k)hY8hH`zO8H ze&t*2>YeSU>Xp7s+|_Djw2c32Z+^{V`+XmK^L4CWui2co59G?rk?C_wUp_fG*?-mH z@atbrJ^i#V_x3j3>OUWk%kTMkOnSQYslBW97tK{RnDW{)ZXQM z{Fk!vTPL=Cc8XCl;{Uo5&8uG4yv^OhxaaG&=={Goj@x~!3M*`1dn6Ay+@jltxX1TYvtPRmp)ec*8qt1fk=yB1E1omy=LA)PlG=9`vKIc;Ob$)Ia z2^pC?k2tz6l%&KoZ%vzdW)=I&yG4EmOVqc=f8TZ8_wDP~zXKigFAQoncpTa$o>CBN9?w$62K=aaRn`0#-7^Xg4ul2TGqGBPr2wykvD zUH)F~*3RPR0rmC&AH2G{n!T;9?VI&E;dQxZQkE{WmzXU+>&o}J;<1cc`#jfPxMuO~ zxI%)7^!b)Kg6Ce&;+~Z$y4pS?=={q~+d99_`Zi /// Handles the command to show information about this bot: /about. /// +[UsedImplicitly] public class AboutCommandGroup : CommandGroup { private static readonly string[] Developers = { "Octol1ttle", "mctaylors", "neroduckale" }; private readonly ICommandContext _context; @@ -42,6 +42,7 @@ public class AboutCommandGroup : CommandGroup { /// [Command("about")] [Description("Shows Boyfriend's developers")] + [UsedImplicitly] public async Task SendAboutBotAsync() { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) return Result.FromError( @@ -51,8 +52,8 @@ public class AboutCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); - Messages.Culture = cfg.GetCulture(); + var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers)); foreach (var dev in Developers) @@ -65,8 +66,7 @@ public class AboutCommandGroup : CommandGroup { var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl( - "https://media.discordapp.net/attachments/837385840946053181/1125009665592393738/boyfriend.png") + .WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png") .Build(); if (!embed.IsDefined(out var built)) return Result.FromError(embed); diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 02e0fa2..d2c1c76 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -1,11 +1,14 @@ using System.ComponentModel; using System.Text; +using Boyfriend.Data; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; @@ -13,14 +16,12 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// /// Handles commands related to ban management: /ban and /unban. /// +[UsedImplicitly] public class BanCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; @@ -58,10 +59,13 @@ public class BanCommandGroup : CommandGroup { /// /// [Command("ban", "бан")] + [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] + [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.BanMembers)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Ban user")] + [UsedImplicitly] public async Task BanUserAsync( [Description("User to ban")] IUser target, [Description("Ban reason")] string reason, @@ -76,8 +80,8 @@ public class BanCommandGroup : CommandGroup { return Result.FromError(currentUserResult); var data = await _dataService.GetData(guildId.Value, CancellationToken); - var cfg = data.Configuration; - Messages.Culture = data.Culture; + var cfg = data.Settings; + Messages.Culture = GuildSettings.Language.Get(cfg); var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken); if (existingBanResult.IsDefined()) { @@ -145,8 +149,10 @@ public class BanCommandGroup : CommandGroup { string.Format(Messages.UserBanned, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) - || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) + || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { var logEmbed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserBanned, target.GetTag()), target) .WithDescription(description) @@ -160,14 +166,14 @@ public class BanCommandGroup : CommandGroup { var builtArray = new[] { logBuilt }; // Not awaiting to reduce response time - if (cfg.PublicFeedbackChannel != channelId.Value) + if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); - if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel - && cfg.PrivateFeedbackChannel != channelId.Value) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); } } @@ -193,10 +199,13 @@ public class BanCommandGroup : CommandGroup { /// /// [Command("unban")] + [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] + [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.BanMembers)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Unban user")] + [UsedImplicitly] public async Task UnbanUserAsync( [Description("User to unban")] IUser target, [Description("Unban reason")] string reason) { @@ -209,8 +218,8 @@ public class BanCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); - Messages.Culture = cfg.GetCulture(); + var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken); if (!existingBanResult.IsDefined()) { @@ -238,8 +247,10 @@ public class BanCommandGroup : CommandGroup { string.Format(Messages.UserUnbanned, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) - || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) + || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { var logEmbed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnbanned, target.GetTag()), target) .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) @@ -254,14 +265,14 @@ public class BanCommandGroup : CommandGroup { var builtArray = new[] { logBuilt }; // Not awaiting to reduce response time - if (cfg.PublicFeedbackChannel != channelId.Value) + if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); - if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel - && cfg.PrivateFeedbackChannel != channelId.Value) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); } diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index de44fbb..ede4d0b 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -1,6 +1,8 @@ using System.ComponentModel; using System.Text; +using Boyfriend.Data; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -14,14 +16,12 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// /// Handles the command to clear messages in a channel: /clear. /// +[UsedImplicitly] public class ClearCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; @@ -48,10 +48,13 @@ public class ClearCommandGroup : CommandGroup { /// were cleared and vice-versa. /// [Command("clear", "очистить")] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] + [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.ManageMessages)] [Description("Remove multiple messages")] + [UsedImplicitly] public async Task ClearMessagesAsync( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] int amount) { @@ -64,8 +67,8 @@ public class ClearCommandGroup : CommandGroup { if (!messagesResult.IsDefined(out var messages)) return Result.FromError(messagesResult); - var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); - Messages.Culture = cfg.GetCulture(); + var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); var idList = new List(messages.Count); var builder = new StringBuilder().AppendLine(Mention.Channel(channelId.Value)).AppendLine(); @@ -93,7 +96,8 @@ public class ClearCommandGroup : CommandGroup { return Result.FromError(currentUserResult); var title = string.Format(Messages.MessagesCleared, amount.ToString()); - if (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value) { + if (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) { var logEmbed = new EmbedBuilder().WithSmallTitle(title, currentUser) .WithDescription(description) .WithActionFooter(user) @@ -105,9 +109,9 @@ public class ClearCommandGroup : CommandGroup { return Result.FromError(logEmbed); // Not awaiting to reduce response time - if (cfg.PrivateFeedbackChannel != channelId.Value) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { logBuilt }, + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { logBuilt }, ct: CancellationToken); } diff --git a/src/Commands/ErrorLoggingEvents.cs b/src/Commands/ErrorLoggingEvents.cs index 30869b4..c5eba21 100644 --- a/src/Commands/ErrorLoggingEvents.cs +++ b/src/Commands/ErrorLoggingEvents.cs @@ -1,15 +1,16 @@ +using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Services; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global - namespace Boyfriend.Commands; /// /// Handles error logging for slash commands that couldn't be successfully prepared. /// +[UsedImplicitly] public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent { private readonly ILogger _logger; @@ -27,8 +28,11 @@ public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent { /// A result which has succeeded. public Task PreparationFailed( IOperationContext context, IResult preparationResult, CancellationToken ct = default) { - if (!preparationResult.IsSuccess) + if (!preparationResult.IsSuccess && !preparationResult.Error.IsUserOrEnvironmentError()) { _logger.LogWarning("Error in slash command preparation.\n{ErrorMessage}", preparationResult.Error.Message); + if (preparationResult.Error is ExceptionError exerr) + _logger.LogError(exerr.Exception, "An exception has been thrown"); + } return Task.FromResult(Result.FromSuccess()); } @@ -37,6 +41,7 @@ public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent { /// /// Handles error logging for slash command groups. /// +[UsedImplicitly] public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { private readonly ILogger _logger; @@ -54,8 +59,11 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { /// A result which has succeeded. public Task AfterExecutionAsync( ICommandContext context, IResult commandResult, CancellationToken ct = default) { - if (!commandResult.IsSuccess) + if (!commandResult.IsSuccess && !commandResult.Error.IsUserOrEnvironmentError()) { _logger.LogWarning("Error in slash command execution.\n{ErrorMessage}", commandResult.Error.Message); + if (commandResult.Error is ExceptionError exerr) + _logger.LogError(exerr.Exception, "An exception has been thrown"); + } return Task.FromResult(Result.FromSuccess()); } diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index da7b2c5..5809677 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -1,24 +1,25 @@ using System.ComponentModel; +using Boyfriend.Data; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// /// Handles the command to kick members of a guild: /kick. /// +[UsedImplicitly] public class KickCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; @@ -54,10 +55,13 @@ public class KickCommandGroup : CommandGroup { /// was kicked and vice-versa. /// [Command("kick", "кик")] + [DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)] + [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.KickMembers)] [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] [Description("Kick member")] + [UsedImplicitly] public async Task KickUserAsync( [Description("Member to kick")] IUser target, [Description("Kick reason")] string reason) { @@ -71,8 +75,8 @@ public class KickCommandGroup : CommandGroup { return Result.FromError(currentUserResult); var data = await _dataService.GetData(guildId.Value, CancellationToken); - var cfg = data.Configuration; - Messages.Culture = cfg.GetCulture(); + var cfg = data.Settings; + Messages.Culture = GuildSettings.Language.Get(cfg); var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); if (!memberResult.IsSuccess) { @@ -129,8 +133,10 @@ public class KickCommandGroup : CommandGroup { string.Format(Messages.UserKicked, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) - || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) + || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { var logEmbed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserKicked, target.GetTag()), target) .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) @@ -144,14 +150,14 @@ public class KickCommandGroup : CommandGroup { var builtArray = new[] { logBuilt }; // Not awaiting to reduce response time - if (cfg.PublicFeedbackChannel != channelId.Value) + if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); - if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel - && cfg.PrivateFeedbackChannel != channelId.Value) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); } } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 764b4f4..4338d09 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -1,11 +1,14 @@ using System.ComponentModel; using System.Text; +using Boyfriend.Data; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; @@ -13,14 +16,12 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// /// Handles commands related to mute management: /mute and /unmute. /// +[UsedImplicitly] public class MuteCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; @@ -58,10 +59,13 @@ public class MuteCommandGroup : CommandGroup { /// /// [Command("mute", "мут")] + [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] + [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.ModerateMembers)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Mute member")] + [UsedImplicitly] public async Task MuteUserAsync( [Description("Member to mute")] IUser target, [Description("Mute reason")] string reason, @@ -93,8 +97,8 @@ public class MuteCommandGroup : CommandGroup { return Result.FromError(interactionResult); var data = await _dataService.GetData(guildId.Value, CancellationToken); - var cfg = data.Configuration; - Messages.Culture = data.Culture; + var cfg = data.Settings; + Messages.Culture = GuildSettings.Language.Get(cfg); Result responseEmbed; if (interactionResult.Entity is not null) { @@ -116,8 +120,10 @@ public class MuteCommandGroup : CommandGroup { string.Format(Messages.UserMuted, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) - || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) + || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)) .Append( string.Format( @@ -136,14 +142,14 @@ public class MuteCommandGroup : CommandGroup { var builtArray = new[] { logBuilt }; // Not awaiting to reduce response time - if (cfg.PublicFeedbackChannel != channelId.Value) + if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); - if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel - && cfg.PrivateFeedbackChannel != channelId.Value) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); } } @@ -169,10 +175,13 @@ public class MuteCommandGroup : CommandGroup { /// /// [Command("unmute", "размут")] + [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] + [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.ModerateMembers)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Unmute member")] + [UsedImplicitly] public async Task UnmuteUserAsync( [Description("Member to unmute")] IUser target, [Description("Unmute reason")] string reason) { @@ -185,8 +194,8 @@ public class MuteCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); - Messages.Culture = cfg.GetCulture(); + var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); if (!memberResult.IsSuccess) { @@ -220,8 +229,10 @@ public class MuteCommandGroup : CommandGroup { string.Format(Messages.UserUnmuted, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) - || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) + || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { var logEmbed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnmuted, target.GetTag()), target) .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) @@ -236,14 +247,14 @@ public class MuteCommandGroup : CommandGroup { var builtArray = new[] { logBuilt }; // Not awaiting to reduce response time - if (cfg.PublicFeedbackChannel != channelId.Value) + if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); - if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel - && cfg.PrivateFeedbackChannel != channelId.Value) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) + && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) _ = _channelApi.CreateMessageAsync( - cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, ct: CancellationToken); } diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 45d27e2..52d924f 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -1,5 +1,7 @@ using System.ComponentModel; +using Boyfriend.Data; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Rest; @@ -9,14 +11,12 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// /// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping /// +[UsedImplicitly] public class PingCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly DiscordGatewayClient _client; @@ -44,6 +44,7 @@ public class PingCommandGroup : CommandGroup { /// [Command("ping", "пинг")] [Description("Get bot latency")] + [UsedImplicitly] public async Task SendPingAsync() { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _)) return Result.FromError( @@ -53,8 +54,8 @@ public class PingCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); - Messages.Culture = cfg.GetCulture(); + var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); var latency = _client.Latency.TotalMilliseconds; if (latency is 0) { diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index d1c4519..7203fbd 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -1,23 +1,23 @@ using System.ComponentModel; using Boyfriend.Data; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// /// Handles the command to manage reminders: /remind /// +[UsedImplicitly] public class RemindCommandGroup : CommandGroup { private readonly ICommandContext _context; private readonly GuildDataService _dataService; @@ -40,7 +40,9 @@ public class RemindCommandGroup : CommandGroup { /// The text of the reminder. /// A feedback sending result which may or may not have succeeded. [Command("remind")] + [DiscordDefaultDMPermission(false)] [Description("Create a reminder")] + [UsedImplicitly] public async Task AddReminderAsync( [Description("After what period of time mention the reminder")] TimeSpan @in, @@ -57,8 +59,8 @@ public class RemindCommandGroup : CommandGroup { (await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add( new Reminder { - RemindAt = remindAt, - Channel = channelId.Value, + At = remindAt, + Channel = channelId.Value.Value, Text = message }); diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index e519d37..5c4a505 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -1,26 +1,44 @@ using System.ComponentModel; -using System.Reflection; using System.Text; using Boyfriend.Data; +using Boyfriend.Data.Options; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// /// Handles the commands to list and modify per-guild settings: /settings and /settings list. /// +[UsedImplicitly] public class SettingsCommandGroup : CommandGroup { + private static readonly IOption[] AllOptions = { + GuildSettings.Language, + GuildSettings.WelcomeMessage, + GuildSettings.ReceiveStartupMessages, + GuildSettings.RemoveRolesOnMute, + GuildSettings.ReturnRolesOnRejoin, + GuildSettings.AutoStartEvents, + GuildSettings.PublicFeedbackChannel, + GuildSettings.PrivateFeedbackChannel, + GuildSettings.EventNotificationChannel, + GuildSettings.DefaultRole, + GuildSettings.MuteRole, + GuildSettings.EventNotificationRole, + GuildSettings.EventEarlyNotificationOffset + }; + private readonly ICommandContext _context; private readonly GuildDataService _dataService; private readonly FeedbackService _feedbackService; @@ -36,13 +54,18 @@ public class SettingsCommandGroup : CommandGroup { } /// - /// A slash command that lists current per-guild settings. + /// A slash command that lists current per-guild GuildSettings. /// /// /// A feedback sending result which may or may not have succeeded. /// [Command("settingslist")] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.ManageGuild)] [Description("Shows settings list for this server")] + [UsedImplicitly] public async Task ListSettingsAsync() { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) return Result.FromError( @@ -52,19 +75,15 @@ public class SettingsCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); - Messages.Culture = cfg.GetCulture(); + var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); var builder = new StringBuilder(); - foreach (var setting in typeof(GuildConfiguration).GetProperties()) { - builder.Append(Markdown.InlineCode(setting.Name)) + foreach (var option in AllOptions) { + builder.Append(Markdown.InlineCode(option.Name)) .Append(": "); - var something = setting.GetValue(cfg); - if (something!.GetType() == typeof(List)) { - var list = (something as List); - builder.AppendLine(string.Join(", ", list!.Select(v => Markdown.InlineCode(v.ToString())))); - } else { builder.AppendLine(Markdown.InlineCode(something.ToString()!)); } + builder.AppendLine(option.Display(cfg)); } var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) @@ -77,13 +96,18 @@ public class SettingsCommandGroup : CommandGroup { } /// - /// A slash command that modifies per-guild settings. + /// A slash command that modifies per-guild GuildSettings. /// /// The setting to modify. /// The new value of the setting. /// A feedback sending result which may or may not have succeeded. [Command("settings")] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.ManageGuild)] [Description("Change settings for this server")] + [UsedImplicitly] public async Task EditSettingsAsync( [Description("The setting whose value you want to change")] string setting, @@ -96,40 +120,16 @@ public class SettingsCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); - Messages.Culture = cfg.GetCulture(); + var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); - PropertyInfo? property = null; + var option = AllOptions.Single( + o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase)); - try { - foreach (var prop in typeof(GuildConfiguration).GetProperties()) - if (string.Equals(setting, prop.Name, StringComparison.CurrentCultureIgnoreCase)) - property = prop; - if (property == null || !property.CanWrite) - throw new ApplicationException(Messages.SettingDoesntExist); - var type = property.PropertyType; - - if (value is "reset" or "default") { property.SetValue(cfg, null); } else if (type == typeof(string)) { - if (setting == "language" && value is not ("ru" or "en" or "mctaylors-ru")) - throw new ApplicationException(Messages.LanguageNotSupported); - property.SetValue(cfg, value); - } else { - try { - if (type == typeof(bool)) - property.SetValue(cfg, Convert.ToBoolean(value)); - - if (type == typeof(ulong)) { - var id = Convert.ToUInt64(value); - - property.SetValue(cfg, id); - } - } catch (Exception e) when (e is FormatException or OverflowException) { - throw new ApplicationException(Messages.InvalidSettingValue); - } - } - } catch (Exception e) { + var setResult = option.Set(cfg, value); + if (!setResult.IsSuccess) { var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser) - .WithDescription(e.Message) + .WithDescription(setResult.Error.Message) .WithColour(ColorsList.Red) .Build(); if (!failedEmbed.IsDefined(out var failedBuilt)) return Result.FromError(failedEmbed); @@ -139,9 +139,9 @@ public class SettingsCommandGroup : CommandGroup { var builder = new StringBuilder(); - builder.Append(Markdown.InlineCode(setting)) + builder.Append(Markdown.InlineCode(option.Name)) .Append($" {Messages.SettingIsNow} ") - .Append(Markdown.InlineCode(value)); + .Append(option.Display(cfg)); var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser) .WithDescription(builder.ToString()) diff --git a/src/Data/GuildConfiguration.cs b/src/Data/GuildConfiguration.cs deleted file mode 100644 index 440e2b7..0000000 --- a/src/Data/GuildConfiguration.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Globalization; -using Remora.Discord.API.Abstractions.Objects; - -namespace Boyfriend.Data; - -/// -/// Stores per-guild settings that can be set by a member -/// with using the /settings command -/// -public class GuildConfiguration { - /// - /// Represents a scheduled event notification receiver. - /// - /// - /// Used to selectively mention guild members when a scheduled event has started or is about to start. - /// - public enum NotificationReceiver { - Interested, - Role - } - - public static readonly Dictionary CultureInfoCache = new() { - { "en", new CultureInfo("en-US") }, - { "ru", new CultureInfo("ru-RU") }, - { "mctaylors-ru", new CultureInfo("tt-RU") } - }; - - public string Language { get; set; } = "en"; - - /// - /// Controls what message should be sent in when a new member joins the server. - /// - /// - /// - /// No message will be sent if set to "off", "disable" or "disabled". - /// will be sent if set to "default" or "reset" - /// - /// - /// - public string WelcomeMessage { get; set; } = "default"; - - /// - /// Controls whether or not the message should be sent - /// in on startup. - /// - /// - public bool ReceiveStartupMessages { get; set; } - - public bool RemoveRolesOnMute { get; set; } - - /// - /// Controls whether or not a guild member's roles are returned if he/she leaves and then joins back. - /// - /// Roles will not be returned if the member left the guild because of /ban or /kick. - public bool ReturnRolesOnRejoin { get; set; } - - public bool AutoStartEvents { get; set; } - - /// - /// Controls what channel should all public messages be sent to. - /// - public ulong PublicFeedbackChannel { get; set; } - - /// - /// Controls what channel should all private, moderator-only messages be sent to. - /// - public ulong PrivateFeedbackChannel { get; set; } - - public ulong EventNotificationChannel { get; set; } - public ulong DefaultRole { get; set; } - public ulong MuteRole { get; set; } - public ulong EventNotificationRole { get; set; } - - /// - /// Controls what guild members should be mentioned when a scheduled event has started or is about to start. - /// - /// - public List EventStartedReceivers { get; set; } - = new() { NotificationReceiver.Interested, NotificationReceiver.Role }; - - /// - /// Controls the amount of time before a scheduled event to send a reminder in . - /// - public TimeSpan EventEarlyNotificationOffset { get; set; } = TimeSpan.Zero; - - // Do not convert this to a property, else serialization will be attempted - public CultureInfo GetCulture() { - return CultureInfoCache[Language]; - } -} diff --git a/src/Data/GuildData.cs b/src/Data/GuildData.cs index 992adc0..7c81364 100644 --- a/src/Data/GuildData.cs +++ b/src/Data/GuildData.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Text.Json.Nodes; using Remora.Rest.Core; namespace Boyfriend.Data; @@ -8,29 +8,26 @@ namespace Boyfriend.Data; ///

50#I3;dtoumAry`~Cj^|FYu0 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 z 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 Y{CVzs>aNq*%mdl(>FVdQ&MBb@0O{!Q+W-In literal 0 HcmV?d00001 diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index 6af4326..0731ccd 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -56,23 +56,28 @@ public class Boyfriend { | GatewayIntents.GuildMembers | GatewayIntents.GuildScheduledEvents); services.Configure( - settings => { - settings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); - settings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); - settings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); - settings.SetSlidingExpiration(TimeSpan.FromDays(7)); + cSettings => { + cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); + cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); + cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); + cSettings.SetSlidingExpiration(TimeSpan.FromDays(7)); }); services.AddTransient() + // Init .AddDiscordCaching() .AddDiscordCommands(true) - .AddPreparationErrorEvent() - .AddPostExecutionEvent() + // Interactions .AddInteractivity() .AddInteractionGroup() + // Slash command event handlers + .AddPreparationErrorEvent() + .AddPostExecutionEvent() + // Services .AddSingleton() .AddSingleton() .AddHostedService() + // Slash commands .AddCommandTree() .WithCommandGroup() .WithCommandGroup() diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 5ff757e..edb34d2 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -1,6 +1,8 @@ using System.ComponentModel; using System.Text; +using Boyfriend.Data; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Rest; @@ -10,14 +12,12 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; ///

/// This information is stored on disk as a JSON file. public class GuildData { - public readonly GuildConfiguration Configuration; - public readonly string ConfigurationPath; - public readonly Dictionary MemberData; public readonly string MemberDataPath; public readonly Dictionary ScheduledEvents; public readonly string ScheduledEventsPath; + public readonly JsonNode Settings; + public readonly string SettingsPath; public GuildData( - GuildConfiguration configuration, string configurationPath, + JsonNode settings, string settingsPath, Dictionary scheduledEvents, string scheduledEventsPath, Dictionary memberData, string memberDataPath) { - Configuration = configuration; - ConfigurationPath = configurationPath; + Settings = settings; + SettingsPath = settingsPath; ScheduledEvents = scheduledEvents; ScheduledEventsPath = scheduledEventsPath; MemberData = memberData; MemberDataPath = memberDataPath; } - public CultureInfo Culture => Configuration.GetCulture(); - public MemberData GetMemberData(Snowflake userId) { if (MemberData.TryGetValue(userId.Value, out var existing)) return existing; diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs new file mode 100644 index 0000000..07e43a6 --- /dev/null +++ b/src/Data/GuildSettings.cs @@ -0,0 +1,63 @@ +using Boyfriend.Data.Options; +using Boyfriend.Responders; +using Remora.Discord.API.Abstractions.Objects; + +namespace Boyfriend.Data; + +/// +/// Contains all per-guild settings that can be set by a member +/// with using the /settings command +/// +public static class GuildSettings { + public static readonly LanguageOption Language = new("Language", "en"); + + /// + /// Controls what message should be sent in when a new member joins the server. + /// + /// + /// + /// No message will be sent if set to "off", "disable" or "disabled". + /// will be sent if set to "default" or "reset" + /// + /// + /// + public static readonly Option WelcomeMessage = new("WelcomeMessage", "default"); + + /// + /// Controls whether or not the message should be sent + /// in on startup. + /// + /// + public static readonly BoolOption ReceiveStartupMessages = new("ReceiveStartupMessages", false); + + public static readonly BoolOption RemoveRolesOnMute = new("RemoveRolesOnMute", false); + + /// + /// Controls whether or not a guild member's roles are returned if he/she leaves and then joins back. + /// + /// Roles will not be returned if the member left the guild because of /ban or /kick. + public static readonly BoolOption ReturnRolesOnRejoin = new("ReturnRolesOnRejoin", false); + + public static readonly BoolOption AutoStartEvents = new("AutoStartEvents", false); + + /// + /// Controls what channel should all public messages be sent to. + /// + public static readonly SnowflakeOption PublicFeedbackChannel = new("PublicFeedbackChannel"); + + /// + /// Controls what channel should all private, moderator-only messages be sent to. + /// + public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel"); + + public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel"); + public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); + public static readonly SnowflakeOption MuteRole = new("MuteRole"); + public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole"); + + /// + /// Controls the amount of time before a scheduled event to send a reminder in . + /// + public static readonly TimeSpanOption EventEarlyNotificationOffset = new( + "EventEarlyNotificationOffset", TimeSpan.Zero); +} diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 72cbdec..7d49ec7 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -1,5 +1,3 @@ -using Remora.Rest.Core; - namespace Boyfriend.Data; /// @@ -13,6 +11,6 @@ public class MemberData { public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } - public List Roles { get; set; } = new(); + public List Roles { get; set; } = new(); public List Reminders { get; } = new(); } diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs new file mode 100644 index 0000000..a8ee954 --- /dev/null +++ b/src/Data/Options/BoolOption.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Nodes; +using Remora.Results; + +namespace Boyfriend.Data.Options; + +public class BoolOption : Option { + public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { } + + public override string Display(JsonNode settings) { + return Get(settings) ? Messages.Yes : Messages.No; + } + + public override Result Set(JsonNode settings, string from) { + if (!TryParseBool(from, out var value)) + return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue)); + + settings[Name] = value; + return Result.FromSuccess(); + } + + private static bool TryParseBool(string from, out bool value) { + value = false; + switch (from) { + case "1" or "y" or "yes" or "д" or "да": + value = true; + return true; + case "0" or "n" or "no" or "н" or "не" or "нет": + value = false; + return true; + default: + return false; + } + } +} diff --git a/src/Data/Options/IOption.cs b/src/Data/Options/IOption.cs new file mode 100644 index 0000000..fc0f747 --- /dev/null +++ b/src/Data/Options/IOption.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Nodes; +using Remora.Results; + +namespace Boyfriend.Data.Options; + +public interface IOption { + string Name { get; } + string Display(JsonNode settings); + Result Set(JsonNode settings, string from); +} diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs new file mode 100644 index 0000000..6c4a49f --- /dev/null +++ b/src/Data/Options/LanguageOption.cs @@ -0,0 +1,35 @@ +using System.Globalization; +using System.Text.Json.Nodes; +using Remora.Discord.Extensions.Formatting; +using Remora.Results; + +namespace Boyfriend.Data.Options; + +/// +public class LanguageOption : Option { + private static readonly Dictionary CultureInfoCache = new() { + { "en", new CultureInfo("en-US") }, + { "ru", new CultureInfo("ru-RU") }, + { "mctaylors-ru", new CultureInfo("tt-RU") } + }; + + public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { } + + public override string Display(JsonNode settings) { + return Markdown.InlineCode(settings[Name]?.GetValue() ?? "en"); + } + + /// + public override CultureInfo Get(JsonNode settings) { + var property = settings[Name]; + return property != null ? CultureInfoCache[property.GetValue()] : DefaultValue; + } + + /// + public override Result Set(JsonNode settings, string from) { + if (!CultureInfoCache.ContainsKey(from.ToLowerInvariant())) + return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported)); + + return base.Set(settings, from.ToLowerInvariant()); + } +} diff --git a/src/Data/Options/Option.cs b/src/Data/Options/Option.cs new file mode 100644 index 0000000..742d3a9 --- /dev/null +++ b/src/Data/Options/Option.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Nodes; +using Remora.Discord.Extensions.Formatting; +using Remora.Results; + +namespace Boyfriend.Data.Options; + +/// +/// Represents an per-guild option. +/// +/// The type of the option. +public class Option : IOption +where T : notnull { + internal readonly T DefaultValue; + + public Option(string name, T defaultValue) { + Name = name; + DefaultValue = defaultValue; + } + + public string Name { get; } + + public virtual string Display(JsonNode settings) { + return Markdown.InlineCode(Get(settings).ToString()!); + } + + /// + /// Sets the value of the option from a to the provided JsonNode. + /// + /// The to set the value to. + /// The string from which the new value of the option will be parsed. + /// A value setting result which may or may not have succeeded. + public virtual Result Set(JsonNode settings, string from) { + settings[Name] = from; + return Result.FromSuccess(); + } + + /// + /// Gets the value of the option from the provided . + /// + /// The to get the value from. + /// The value of the option. + public virtual T Get(JsonNode settings) { + var property = settings[Name]; + return property != null ? property.GetValue() : DefaultValue; + } +} diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs new file mode 100644 index 0000000..f65065c --- /dev/null +++ b/src/Data/Options/SnowflakeOption.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Nodes; +using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Data.Options; + +public class SnowflakeOption : Option { + public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { } + + public override string Display(JsonNode settings) { + return Name.EndsWith("Channel") ? Mention.Channel(Get(settings)) : Mention.Role(Get(settings)); + } + + public override Snowflake Get(JsonNode settings) { + var property = settings[Name]; + return property != null ? property.GetValue().ToSnowflake() : DefaultValue; + } + + public override Result Set(JsonNode settings, string from) { + if (!ulong.TryParse(from, out var parsed)) + return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue)); + + settings[Name] = parsed; + return Result.FromSuccess(); + } +} diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs new file mode 100644 index 0000000..659d88c --- /dev/null +++ b/src/Data/Options/TimeSpanOption.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Nodes; +using Remora.Commands.Parsers; +using Remora.Results; + +namespace Boyfriend.Data.Options; + +public class TimeSpanOption : Option { + private static readonly TimeSpanParser Parser = new(); + + public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { } + + public override TimeSpan Get(JsonNode settings) { + var property = settings[Name]; + return property != null ? ParseTimeSpan(property.GetValue()).Entity : DefaultValue; + } + + public override Result Set(JsonNode settings, string from) { + if (!ParseTimeSpan(from).IsDefined(out var span)) + return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue)); + + settings[Name] = span.ToString(); + return Result.FromSuccess(); + } + + private static Result ParseTimeSpan(string from) { + return Parser.TryParseAsync(from).AsTask().GetAwaiter().GetResult(); + } +} diff --git a/src/Data/Reminder.cs b/src/Data/Reminder.cs index 1d0410c..2246b5e 100644 --- a/src/Data/Reminder.cs +++ b/src/Data/Reminder.cs @@ -1,9 +1,7 @@ -using Remora.Rest.Core; - namespace Boyfriend.Data; public struct Reminder { - public DateTimeOffset RemindAt; + public DateTimeOffset At; public string Text; - public Snowflake Channel; + public ulong Channel; } diff --git a/src/EventResponders.cs b/src/EventResponders.cs deleted file mode 100644 index 708bbc1..0000000 --- a/src/EventResponders.cs +++ /dev/null @@ -1,335 +0,0 @@ -using Boyfriend.Data; -using Boyfriend.Services; -using DiffPlex.DiffBuilder; -using Microsoft.Extensions.Logging; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.Caching; -using Remora.Discord.Caching.Services; -using Remora.Discord.Extensions.Embeds; -using Remora.Discord.Extensions.Formatting; -using Remora.Discord.Gateway.Responders; -using Remora.Rest.Core; -using Remora.Results; - -// ReSharper disable UnusedType.Global - -namespace Boyfriend; - -/// -/// Handles sending a message to a guild that has just initialized if that guild -/// has enabled -/// -public class GuildCreateResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly ILogger _logger; - private readonly IDiscordRestUserAPI _userApi; - - public GuildCreateResponder( - IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger logger, - IDiscordRestUserAPI userApi) { - _channelApi = channelApi; - _dataService = dataService; - _logger = logger; - _userApi = userApi; - } - - public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild isn't IAvailableGuild - - var guild = gatewayEvent.Guild.AsT0; - _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); - - var guildConfig = await _dataService.GetConfiguration(guild.ID, ct); - if (!guildConfig.ReceiveStartupMessages) - return Result.FromSuccess(); - if (guildConfig.PrivateFeedbackChannel is 0) - return Result.FromSuccess(); - - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - - Messages.Culture = guildConfig.GetCulture(); - var i = Random.Shared.Next(1, 4); - - var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) - .WithTitle($"Beep{i}".Localized()) - .WithDescription(Messages.Ready) - .WithCurrentTimestamp() - .WithColour(ColorsList.Blue) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - guildConfig.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, ct: ct); - } -} - -/// -/// Handles logging the contents of a deleted message and the user who deleted the message -/// to a guild's if one is set. -/// -public class MessageDeletedResponder : IResponder { - private readonly IDiscordRestAuditLogAPI _auditLogApi; - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestUserAPI _userApi; - - public MessageDeletedResponder( - IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi, - GuildDataService dataService, IDiscordRestUserAPI userApi) { - _auditLogApi = auditLogApi; - _channelApi = channelApi; - _dataService = dataService; - _userApi = userApi; - } - - public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); - - var guildConfiguration = await _dataService.GetConfiguration(guildId, ct); - if (guildConfiguration.PrivateFeedbackChannel is 0) return Result.FromSuccess(); - - var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); - if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); - if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess(); - - var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( - guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); - if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); - - var auditLog = auditLogPage.AuditLogEntries.Single(); - if (!auditLog.Options.IsDefined(out var options)) - return Result.FromError(new ArgumentNullError(nameof(auditLog.Options))); - - var user = message.Author; - if (options.ChannelID == gatewayEvent.ChannelID - && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { - var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); - if (!userResult.IsDefined(out user)) return Result.FromError(userResult); - } - - Messages.Culture = guildConfiguration.GetCulture(); - - var embed = new EmbedBuilder() - .WithSmallTitle( - string.Format( - Messages.CachedMessageDeleted, - message.Author.GetTag()), message.Author) - .WithDescription( - $"{Mention.Channel(gatewayEvent.ChannelID)}\n{message.Content.InBlockCode()}") - .WithActionFooter(user) - .WithTimestamp(message.Timestamp) - .WithColour(ColorsList.Red) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); - } -} - -/// -/// Handles logging the difference between an edited message's old and new content -/// to a guild's if one is set. -/// -public class MessageEditedResponder : IResponder { - private readonly CacheService _cacheService; - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestUserAPI _userApi; - - public MessageEditedResponder( - CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService, - IDiscordRestUserAPI userApi) { - _cacheService = cacheService; - _channelApi = channelApi; - _dataService = dataService; - _userApi = userApi; - } - - public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.GuildID.IsDefined(out var guildId)) - return Result.FromSuccess(); - var guildConfiguration = await _dataService.GetConfiguration(guildId, ct); - if (guildConfiguration.PrivateFeedbackChannel is 0) - return Result.FromSuccess(); - if (!gatewayEvent.Content.IsDefined(out var newContent)) - return Result.FromSuccess(); - if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) - return Result.FromSuccess(); // The message wasn't actually edited - - if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); - if (!gatewayEvent.ID.IsDefined(out var messageId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID))); - - var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); - var messageResult = await _cacheService.TryGetValueAsync( - cacheKey, ct); - if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); - if (message.Content == newContent) return Result.FromSuccess(); - - // Custom event responders are called earlier than responders responsible for message caching - // This means that subsequent edit logs may contain the wrong content - // We can work around this by evicting the message from the cache - await _cacheService.EvictAsync(cacheKey, ct); - // However, since we evicted the message, subsequent edits won't have a cached instance to work with - // Getting the message will put it back in the cache, resolving all issues - // We don't need to await this since the result is not needed - // NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages - // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously - _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); - - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - - var diff = InlineDiffBuilder.Diff(message.Content, newContent); - - Messages.Culture = guildConfiguration.GetCulture(); - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) - .WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}") - .WithUserFooter(currentUser) - .WithTimestamp(timestamp.Value) - .WithColour(ColorsList.Yellow) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); - } -} - -/// -/// Handles sending a guild's if one is set. -/// If is enabled, roles will be returned. -/// -/// -public class GuildMemberAddResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestGuildAPI _guildApi; - - public GuildMemberAddResponder( - IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) { - _channelApi = channelApi; - _dataService = dataService; - _guildApi = guildApi; - } - - public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.User.IsDefined(out var user)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User))); - var data = await _dataService.GetData(gatewayEvent.GuildID, ct); - var cfg = data.Configuration; - if (cfg.PublicFeedbackChannel is 0 || cfg.WelcomeMessage is "off" or "disable" or "disabled") - return Result.FromSuccess(); - if (cfg.ReturnRolesOnRejoin) { - var result = await _guildApi.ModifyGuildMemberAsync( - gatewayEvent.GuildID, user.ID, roles: data.GetMemberData(user.ID).Roles, ct: ct); - if (!result.IsSuccess) return Result.FromError(result.Error); - } - - Messages.Culture = data.Culture; - var welcomeMessage = cfg.WelcomeMessage is "default" or "reset" - ? Messages.DefaultWelcomeMessage - : cfg.WelcomeMessage; - - var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); - if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(welcomeMessage, user.GetTag(), guild.Name), user) - .WithGuildFooter(guild) - .WithTimestamp(gatewayEvent.JoinedAt) - .WithColour(ColorsList.Green) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); - } -} - -/// -/// Handles sending a notification when a scheduled event has been cancelled -/// in a guild's if one is set. -/// -public class GuildScheduledEventDeleteResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - - public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) { - _channelApi = channelApi; - _dataService = dataService; - } - - public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) { - var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); - guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); - - if (guildData.Configuration.EventNotificationChannel is 0) - return Result.FromSuccess(); - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name)) - .WithDescription(":(") - .WithColour(ColorsList.Red) - .WithCurrentTimestamp() - .Build(); - - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - guildData.Configuration.EventNotificationChannel.ToDiscordSnowflake(), embeds: new[] { built }, ct: ct); - } -} - -/// -/// Handles updating when a guild member is updated. -/// -public class GuildMemberUpdateResponder : IResponder { - private readonly GuildDataService _dataService; - - public GuildMemberUpdateResponder(GuildDataService dataService) { - _dataService = dataService; - } - - public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) { - var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); - memberData.Roles = gatewayEvent.Roles.ToList(); - return Result.FromSuccess(); - } -} - -/// -/// Handles sending replies to easter egg messages. -/// -public class MessageCreateResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - - public MessageCreateResponder(IDiscordRestChannelAPI channelApi) { - _channelApi = channelApi; - } - - public Task RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) { - _ = _channelApi.CreateMessageAsync( - gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content switch { - "whoami" => "`nobody`", - "сука !!" => "`root`", - "воооо" => "`removing /...`", - "пон" => - "https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg", - "++++" => "#", - _ => default(Optional) - }); - return Task.FromResult(Result.FromSuccess()); - } -} diff --git a/src/Extensions.cs b/src/Extensions.cs index 95500ac..18a3c06 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -170,7 +170,7 @@ public static class Extensions { return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}"; } - public static Snowflake ToDiscordSnowflake(this ulong id) { + public static Snowflake ToSnowflake(this ulong id) { return DiscordSnowflake.New(id); } @@ -190,4 +190,8 @@ public static class Extensions { && context.TryGetChannelID(out channelId) && context.TryGetUserID(out userId); } + + public static bool Empty(this Snowflake snowflake) { + return snowflake.Value is 0; + } } diff --git a/src/InteractionResponders.cs b/src/InteractionResponders.cs index 231df31..6f40d2a 100644 --- a/src/InteractionResponders.cs +++ b/src/InteractionResponders.cs @@ -1,17 +1,16 @@ +using JetBrains.Annotations; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Interactivity; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend; /// /// Handles responding to various interactions. /// +[UsedImplicitly] public class InteractionResponders : InteractionGroup { private readonly FeedbackService _feedbackService; @@ -25,6 +24,7 @@ public class InteractionResponders : InteractionGroup { /// The ID of the guild and scheduled event, encoded as "guildId:eventId". /// An ephemeral feedback sending result which may or may not have succeeded. [Button("scheduled-event-details")] + [UsedImplicitly] public async Task OnStatefulButtonClicked(string? state = null) { if (state is null) return Result.FromError(new ArgumentNullError(nameof(state))); diff --git a/locale/Messages.Designer.cs b/src/Messages.Designer.cs similarity index 99% rename from locale/Messages.Designer.cs rename to src/Messages.Designer.cs index de6955b..42a05be 100644 --- a/locale/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -8,9 +8,6 @@ //------------------------------------------------------------------------------ namespace Boyfriend { - using System; - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs new file mode 100644 index 0000000..081f526 --- /dev/null +++ b/src/Responders/GuildLoadedResponder.cs @@ -0,0 +1,63 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Gateway.Events; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles sending a message to a guild that has just initialized if that guild +/// has enabled +/// +[UsedImplicitly] +public class GuildLoadedResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly ILogger _logger; + private readonly IDiscordRestUserAPI _userApi; + + public GuildLoadedResponder( + IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger logger, + IDiscordRestUserAPI userApi) { + _channelApi = channelApi; + _dataService = dataService; + _logger = logger; + _userApi = userApi; + } + + public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild is not IAvailableGuild + + var guild = gatewayEvent.Guild.AsT0; + _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); + + var cfg = await _dataService.GetSettings(guild.ID, ct); + if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) + return Result.FromSuccess(); + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + return Result.FromSuccess(); + + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + Messages.Culture = GuildSettings.Language.Get(cfg); + var i = Random.Shared.Next(1, 4); + + var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) + .WithTitle($"Beep{i}".Localized()) + .WithDescription(Messages.Ready) + .WithCurrentTimestamp() + .WithColour(ColorsList.Blue) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct); + } +} diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs new file mode 100644 index 0000000..c61e500 --- /dev/null +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -0,0 +1,65 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles sending a guild's if one is set. +/// If is enabled, roles will be returned. +/// +/// +[UsedImplicitly] +public class GuildMemberJoinedResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestGuildAPI _guildApi; + + public GuildMemberJoinedResponder( + IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) { + _channelApi = channelApi; + _dataService = dataService; + _guildApi = guildApi; + } + + public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.User.IsDefined(out var user)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User))); + var data = await _dataService.GetData(gatewayEvent.GuildID, ct); + var cfg = data.Settings; + if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") + return Result.FromSuccess(); + if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { + var result = await _guildApi.ModifyGuildMemberAsync( + gatewayEvent.GuildID, user.ID, + roles: data.GetMemberData(user.ID).Roles.ConvertAll(r => r.ToSnowflake()), ct: ct); + if (!result.IsSuccess) return Result.FromError(result.Error); + } + + Messages.Culture = GuildSettings.Language.Get(cfg); + var welcomeMessage = GuildSettings.WelcomeMessage.Get(cfg) is "default" or "reset" + ? Messages.DefaultWelcomeMessage + : GuildSettings.WelcomeMessage.Get(cfg); + + var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); + if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(welcomeMessage, user.GetTag(), guild.Name), user) + .WithGuildFooter(guild) + .WithTimestamp(gatewayEvent.JoinedAt) + .WithColour(ColorsList.Green) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} diff --git a/src/Responders/GuildMemberRolesUpdatedResponder.cs b/src/Responders/GuildMemberRolesUpdatedResponder.cs new file mode 100644 index 0000000..b61ce32 --- /dev/null +++ b/src/Responders/GuildMemberRolesUpdatedResponder.cs @@ -0,0 +1,26 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles updating when a guild member is updated. +/// +[UsedImplicitly] +public class GuildMemberUpdateResponder : IResponder { + private readonly GuildDataService _dataService; + + public GuildMemberUpdateResponder(GuildDataService dataService) { + _dataService = dataService; + } + + public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) { + var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); + memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value); + return Result.FromSuccess(); + } +} diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs new file mode 100644 index 0000000..903db84 --- /dev/null +++ b/src/Responders/MessageDeletedResponder.cs @@ -0,0 +1,78 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles logging the contents of a deleted message and the user who deleted the message +/// to a guild's if one is set. +/// +[UsedImplicitly] +public class MessageDeletedResponder : IResponder { + private readonly IDiscordRestAuditLogAPI _auditLogApi; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestUserAPI _userApi; + + public MessageDeletedResponder( + IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi, + GuildDataService dataService, IDiscordRestUserAPI userApi) { + _auditLogApi = auditLogApi; + _channelApi = channelApi; + _dataService = dataService; + _userApi = userApi; + } + + public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); + + var cfg = await _dataService.GetSettings(guildId, ct); + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) return Result.FromSuccess(); + + var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); + if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); + if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess(); + + var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( + guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); + if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); + + var auditLog = auditLogPage.AuditLogEntries.Single(); + if (!auditLog.Options.IsDefined(out var options)) + return Result.FromError(new ArgumentNullError(nameof(auditLog.Options))); + + var user = message.Author; + if (options.ChannelID == gatewayEvent.ChannelID + && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { + var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); + if (!userResult.IsDefined(out user)) return Result.FromError(userResult); + } + + Messages.Culture = GuildSettings.Language.Get(cfg); + + var embed = new EmbedBuilder() + .WithSmallTitle( + string.Format( + Messages.CachedMessageDeleted, + message.Author.GetTag()), message.Author) + .WithDescription( + $"{Mention.Channel(gatewayEvent.ChannelID)}\n{message.Content.InBlockCode()}") + .WithActionFooter(user) + .WithTimestamp(message.Timestamp) + .WithColour(ColorsList.Red) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs new file mode 100644 index 0000000..0211170 --- /dev/null +++ b/src/Responders/MessageEditedResponder.cs @@ -0,0 +1,89 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using DiffPlex.DiffBuilder; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Caching; +using Remora.Discord.Caching.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles logging the difference between an edited message's old and new content +/// to a guild's if one is set. +/// +[UsedImplicitly] +public class MessageEditedResponder : IResponder { + private readonly CacheService _cacheService; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestUserAPI _userApi; + + public MessageEditedResponder( + CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService, + IDiscordRestUserAPI userApi) { + _cacheService = cacheService; + _channelApi = channelApi; + _dataService = dataService; + _userApi = userApi; + } + + public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) + return Result.FromSuccess(); + var cfg = await _dataService.GetSettings(guildId, ct); + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + return Result.FromSuccess(); + if (!gatewayEvent.Content.IsDefined(out var newContent)) + return Result.FromSuccess(); + if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) + return Result.FromSuccess(); // The message wasn't actually edited + + if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); + if (!gatewayEvent.ID.IsDefined(out var messageId)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID))); + + var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); + var messageResult = await _cacheService.TryGetValueAsync( + cacheKey, ct); + if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); + if (message.Content == newContent) return Result.FromSuccess(); + + // Custom event responders are called earlier than responders responsible for message caching + // This means that subsequent edit logs may contain the wrong content + // We can work around this by evicting the message from the cache + await _cacheService.EvictAsync(cacheKey, ct); + // However, since we evicted the message, subsequent edits won't have a cached instance to work with + // Getting the message will put it back in the cache, resolving all issues + // We don't need to await this since the result is not needed + // NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages + // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously + _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + var diff = InlineDiffBuilder.Diff(message.Content, newContent); + + Messages.Culture = GuildSettings.Language.Get(cfg); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) + .WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}") + .WithUserFooter(currentUser) + .WithTimestamp(timestamp.Value) + .WithColour(ColorsList.Yellow) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs new file mode 100644 index 0000000..a45a2f4 --- /dev/null +++ b/src/Responders/MessageReceivedResponder.cs @@ -0,0 +1,34 @@ +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Gateway.Responders; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles sending replies to easter egg messages. +/// +[UsedImplicitly] +public class MessageCreateResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + + public MessageCreateResponder(IDiscordRestChannelAPI channelApi) { + _channelApi = channelApi; + } + + public Task RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) { + _ = _channelApi.CreateMessageAsync( + gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content.ToLowerInvariant() switch { + "whoami" => "`nobody`", + "сука !!" => "`root`", + "воооо" => "`removing /...`", + "пон" => "https://cdn.upload.systems/uploads/2LNfUSwM.jpg", + "++++" => "#", + "осу" => "https://github.com/ppy/osu", + _ => default(Optional) + }); + return Task.FromResult(Result.FromSuccess()); + } +} diff --git a/src/Responders/ScheduledEventCancelledResponder.cs b/src/Responders/ScheduledEventCancelledResponder.cs new file mode 100644 index 0000000..86453ef --- /dev/null +++ b/src/Responders/ScheduledEventCancelledResponder.cs @@ -0,0 +1,45 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles sending a notification when a scheduled event has been cancelled +/// in a guild's if one is set. +/// +[UsedImplicitly] +public class GuildScheduledEventDeleteResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + + public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) { + _channelApi = channelApi; + _dataService = dataService; + } + + public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) { + var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); + guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); + + if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty()) + return Result.FromSuccess(); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name)) + .WithDescription(":(") + .WithColour(ColorsList.Red) + .WithCurrentTimestamp() + .Build(); + + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(guildData.Settings), embeds: new[] { built }, ct: ct); + } +} diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index be873f1..990e731 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Text.Json; +using System.Text.Json.Nodes; using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Remora.Discord.API.Abstractions.Rest; @@ -36,8 +37,8 @@ public class GuildDataService : IHostedService { private async Task SaveAsync(CancellationToken ct) { var tasks = new List(); foreach (var data in _datas.Values) { - await using var configStream = File.OpenWrite(data.ConfigurationPath); - tasks.Add(JsonSerializer.SerializeAsync(configStream, data.Configuration, cancellationToken: ct)); + await using var settingsStream = File.OpenWrite(data.SettingsPath); + tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct)); await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath); tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct)); @@ -58,17 +59,16 @@ public class GuildDataService : IHostedService { private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) { var idString = $"{guildId}"; var memberDataPath = $"{guildId}/MemberData"; - var configurationPath = $"{guildId}/Configuration.json"; + var settingsPath = $"{guildId}/Settings.json"; var scheduledEventsPath = $"{guildId}/ScheduledEvents.json"; if (!Directory.Exists(idString)) Directory.CreateDirectory(idString); if (!Directory.Exists(memberDataPath)) Directory.CreateDirectory(memberDataPath); - if (!File.Exists(configurationPath)) await File.WriteAllTextAsync(configurationPath, "{}", ct); + if (!File.Exists(settingsPath)) await File.WriteAllTextAsync(settingsPath, "{}", ct); if (!File.Exists(scheduledEventsPath)) await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); - await using var configurationStream = File.OpenRead(configurationPath); - var configuration - = JsonSerializer.DeserializeAsync( - configurationStream, cancellationToken: ct); + await using var settingsStream = File.OpenRead(settingsPath); + var jsonSettings + = JsonNode.Parse(settingsStream); await using var eventsStream = File.OpenRead(scheduledEventsPath); var events @@ -80,23 +80,23 @@ public class GuildDataService : IHostedService { await using var dataStream = File.OpenRead(dataPath); var data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); if (data is null) continue; - var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToDiscordSnowflake(), ct); + var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct); if (memberResult.IsSuccess) - data.Roles = memberResult.Entity.Roles.ToList(); + data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value); memberData.Add(data.Id, data); } var finalData = new GuildData( - await configuration ?? new GuildConfiguration(), configurationPath, + jsonSettings ?? new JsonObject(), settingsPath, await events ?? new Dictionary(), scheduledEventsPath, memberData, memberDataPath); while (!_datas.ContainsKey(guildId)) _datas.TryAdd(guildId, finalData); return finalData; } - public async Task GetConfiguration(Snowflake guildId, CancellationToken ct = default) { - return (await GetData(guildId, ct)).Configuration; + public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) { + return (await GetData(guildId, ct)).Settings; } public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) { diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs index 3d55f07..b119d90 100644 --- a/src/Services/GuildUpdateService.cs +++ b/src/Services/GuildUpdateService.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -94,9 +95,9 @@ public class GuildUpdateService : BackgroundService { /// This method does the following: /// /// Automatically unbans users once their ban period has expired. - /// Automatically grants members the guild's if one is set. + /// Automatically grants members the guild's if one is set. /// Sends reminders about an upcoming scheduled event. - /// Automatically starts scheduled events if is enabled. + /// Automatically starts scheduled events if is enabled. /// Sends scheduled event start notifications. /// Sends scheduled event completion notifications. /// Sends reminders to members. @@ -114,15 +115,15 @@ public class GuildUpdateService : BackgroundService { /// The cancellation token for this operation. private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) { var data = await _dataService.GetData(guildId, ct); - Messages.Culture = data.Culture; - var defaultRoleSnowflake = data.Configuration.DefaultRole.ToDiscordSnowflake(); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + var defaultRole = GuildSettings.DefaultRole.Get(data.Settings); foreach (var memberData in data.MemberData.Values) { - var userId = memberData.Id.ToDiscordSnowflake(); + var userId = memberData.Id.ToSnowflake(); - if (defaultRoleSnowflake.Value is not 0 && !memberData.Roles.Contains(defaultRoleSnowflake)) + if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value)) _ = _guildApi.AddGuildMemberRoleAsync( - guildId, userId, defaultRoleSnowflake, ct: ct); + guildId, userId, defaultRole, ct: ct); if (DateTimeOffset.UtcNow > memberData.BannedUntil) { var unbanResult = await _guildApi.RemoveGuildBanAsync( @@ -139,7 +140,7 @@ public class GuildUpdateService : BackgroundService { for (var i = memberData.Reminders.Count - 1; i >= 0; i--) { var reminder = memberData.Reminders[i]; - if (DateTimeOffset.UtcNow < reminder.RemindAt) continue; + if (DateTimeOffset.UtcNow < reminder.At) continue; var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.Reminder, user.GetTag()), user) @@ -151,7 +152,7 @@ public class GuildUpdateService : BackgroundService { if (!embed.IsDefined(out var built)) continue; var messageResult = await _channelApi.CreateMessageAsync( - reminder.Channel, Mention.User(user), embeds: new[] { built }, ct: ct); + reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); if (!messageResult.IsSuccess) _logger.LogWarning( "Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message); @@ -163,7 +164,7 @@ public class GuildUpdateService : BackgroundService { var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); if (!eventsResult.IsDefined(out var events)) return; - if (data.Configuration.EventNotificationChannel is 0) return; + if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) return; foreach (var scheduledEvent in events) { if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) { @@ -172,7 +173,7 @@ public class GuildUpdateService : BackgroundService { var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; if (storedEvent.Status == scheduledEvent.Status) { if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) { - if (data.Configuration.AutoStartEvents + if (GuildSettings.AutoStartEvents.Get(data.Settings) && scheduledEvent.Status is not GuildScheduledEventStatus.Active) { var startResult = await _eventApi.ModifyGuildScheduledEventAsync( guildId, scheduledEvent.ID, @@ -182,10 +183,11 @@ public class GuildUpdateService : BackgroundService { "Error in automatic scheduled event start request.\n{ErrorMessage}", startResult.Error.Message); } - } else if (data.Configuration.EventEarlyNotificationOffset != TimeSpan.Zero + } else if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) != TimeSpan.Zero && !storedEvent.EarlyNotificationSent && DateTimeOffset.UtcNow - >= scheduledEvent.ScheduledStartTime - data.Configuration.EventEarlyNotificationOffset) { + >= scheduledEvent.ScheduledStartTime + - GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) { var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct); if (earlyResult.IsSuccess) storedEvent.EarlyNotificationSent = true; @@ -203,7 +205,7 @@ public class GuildUpdateService : BackgroundService { var result = scheduledEvent.Status switch { GuildScheduledEventStatus.Scheduled => - await SendScheduledEventCreatedMessage(scheduledEvent, data.Configuration, ct), + await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct), _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))) @@ -215,19 +217,17 @@ public class GuildUpdateService : BackgroundService { } /// - /// Handles sending a notification, mentioning the if one is + /// Handles sending a notification, mentioning the if one is /// set, /// when a scheduled event is created - /// in a guild's if one is set. + /// in a guild's if one is set. /// /// The scheduled event that has just been created. - /// The configuration of the guild containing the scheduled event. + /// The settings of the guild containing the scheduled event. /// The cancellation token for this operation. /// A notification sending result which may or may not have succeeded. private async Task SendScheduledEventCreatedMessage( - IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) { - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { if (!scheduledEvent.CreatorID.IsDefined(out var creatorId)) return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID))); @@ -275,14 +275,13 @@ public class GuildUpdateService : BackgroundService { .WithTitle(scheduledEvent.Name) .WithDescription(embedDescription) .WithEventCover(scheduledEvent.ID, scheduledEvent.Image) - .WithUserFooter(currentUser) .WithCurrentTimestamp() .WithColour(ColorsList.White) .Build(); if (!embed.IsDefined(out var built)) return Result.FromError(embed); - var roleMention = config.EventNotificationRole is not 0 - ? Mention.Role(config.EventNotificationRole.ToDiscordSnowflake()) + var roleMention = !GuildSettings.EventNotificationRole.Get(settings).Empty() + ? Mention.Role(GuildSettings.EventNotificationRole.Get(settings)) : string.Empty; var button = new ButtonComponent( @@ -294,14 +293,14 @@ public class GuildUpdateService : BackgroundService { ); return (Result)await _channelApi.CreateMessageAsync( - config.EventNotificationChannel.ToDiscordSnowflake(), roleMention, embeds: new[] { built }, + GuildSettings.EventNotificationChannel.Get(settings), roleMention, embeds: new[] { built }, components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); } /// - /// Handles sending a notification, mentioning the s, + /// Handles sending a notification, mentioning the and event subscribers, /// when a scheduled event is about to start, has started or completed - /// in a guild's if one is set. + /// in a guild's if one is set. /// /// The scheduled event that is about to start, has started or completed. /// The data for the guild containing the scheduled event. @@ -353,7 +352,7 @@ public class GuildUpdateService : BackgroundService { } var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Configuration, ct); + scheduledEvent, data.Settings, ct); if (!contentResult.IsDefined(out content)) return Result.FromError(contentResult); @@ -383,7 +382,7 @@ public class GuildUpdateService : BackgroundService { if (!result.IsDefined(out var built)) return Result.FromError(result); return (Result)await _channelApi.CreateMessageAsync( - data.Configuration.EventNotificationChannel.ToDiscordSnowflake(), + GuildSettings.EventNotificationChannel.Get(data.Settings), content ?? default(Optional), embeds: new[] { built }, ct: ct); } } diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index b4ff6fb..3b6a2bf 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json.Nodes; using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Remora.Discord.API.Abstractions.Objects; @@ -103,38 +104,32 @@ public class UtilityService : IHostedService { } /// - /// Gets the string mentioning all s related to a scheduled + /// Gets the string mentioning the and event subscribers related to a scheduled /// event. /// - /// - /// If the guild configuration enables , then the - /// will also be mentioned. - /// /// - /// The scheduled event whose subscribers will be mentioned if the guild configuration enables - /// . + /// The scheduled event whose subscribers will be mentioned. /// - /// The configuration of the guild containing the scheduled event + /// The settings of the guild containing the scheduled event /// The cancellation token for this operation. /// A result containing the string which may or may not have succeeded. public async Task> GetEventNotificationMentions( - IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) { + IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { var builder = new StringBuilder(); - var receivers = config.EventStartedReceivers; - var role = config.EventNotificationRole.ToDiscordSnowflake(); + var role = GuildSettings.EventNotificationRole.Get(settings); var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync( scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct); if (!usersResult.IsDefined(out var users)) return Result.FromError(usersResult); - if (receivers.Contains(GuildConfiguration.NotificationReceiver.Role) && role.Value is not 0) + if (role.Value is not 0) builder.Append($"{Mention.Role(role)} "); - if (receivers.Contains(GuildConfiguration.NotificationReceiver.Interested)) - builder = users.Where( - user => { - if (!user.GuildMember.IsDefined(out var member)) return true; - return !member.Roles.Contains(role); - }) - .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} ")); + + builder = users.Where( + user => { + if (!user.GuildMember.IsDefined(out var member)) return true; + return !member.Roles.Contains(role); + }) + .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} ")); return builder.ToString(); } } From f75926c604d6ac4d4efaf3d92a99d98bda4b87ae Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 18 Jul 2023 19:18:35 +0500 Subject: [PATCH 099/329] Do not allow certain commands to be used in DMs (#49) This PR prevents usage of `/about`, `/ping` and `/remind` inside DMs. While it may look like these commands do not require a guild attached to them, the language system depends on having a guild to fetch settings from, so it is not possible to use these commands in DMs. Signed-off-by: Octol1ttle --- src/Commands/AboutCommandGroup.cs | 4 ++++ src/Commands/PingCommandGroup.cs | 4 ++++ src/Commands/RemindCommandGroup.cs | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index edb34d2..70f4bcf 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -6,6 +6,8 @@ using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; @@ -41,6 +43,8 @@ public class AboutCommandGroup : CommandGroup { /// A feedback sending result which may or may not have succeeded. /// [Command("about")] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] [Description("Shows Boyfriend's developers")] [UsedImplicitly] public async Task SendAboutBotAsync() { diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 52d924f..ad05451 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -5,6 +5,8 @@ using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; @@ -44,6 +46,8 @@ public class PingCommandGroup : CommandGroup { /// [Command("ping", "пинг")] [Description("Get bot latency")] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] [UsedImplicitly] public async Task SendPingAsync() { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _)) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 7203fbd..28fc0ed 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -6,6 +6,7 @@ using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; @@ -40,8 +41,9 @@ public class RemindCommandGroup : CommandGroup { /// The text of the reminder. /// A feedback sending result which may or may not have succeeded. [Command("remind")] - [DiscordDefaultDMPermission(false)] [Description("Create a reminder")] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] [UsedImplicitly] public async Task AddReminderAsync( [Description("After what period of time mention the reminder")] From f97e99d82b5ef6197782d91bd14795fc271fb060 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 20 Jul 2023 02:00:11 +0500 Subject: [PATCH 100/329] Increase thresholds for size labels (#52) This PR adds a `labels.yml` file to configure the `pull-request-size` bot. The new thresholds are: - 0-9 lines for XS - 10-49 lines for S - 50-99 lines for M - 100-499 lines for L - 500-999 lines for XL - \>1000 for XXL In the file, these thresholds are doubled to accomodate for the fact that both additions and deletions are counted (e.g. editing 10 lines will result in 10 additions and 10 deletions, 20 in total) Signed-off-by: Octol1ttle --- .github/labels.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/labels.yml diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..8187db4 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,24 @@ +XS: + name: size/XS + lines: 0 + color: 3CBF00 +S: + name: size/S + lines: 20 + color: 5D9801 +M: + name: size/M + lines: 100 + color: 7F7203 +L: + name: size/L + lines: 200 + color: A14C05 +XL: + name: size/XL + lines: 1000 + color: C32607 +XXL: + name: size/XXL + lines: 2000 + color: E50009 From c825848d7eb321743f858ea1812a279af4f35ca6 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 20 Jul 2023 02:01:53 +0500 Subject: [PATCH 101/329] Split error logging events into separate files (#51) This PR splits `LoggingPreparationErrorEvent` and `ErrorLoggingPostExecutionEvent` classes into separate files and puts these files in a separate namespace: `Boyfriend.Commands.Events`. This makes these classes easier to find and distinguish from commands groups. Signed-off-by: Octol1ttle --- src/Boyfriend.cs | 3 +- .../ErrorLoggingPostExecutionEvent.cs} | 33 +--------------- .../Events/LoggingPreparationErrorEvent.cs | 39 +++++++++++++++++++ 3 files changed, 42 insertions(+), 33 deletions(-) rename src/Commands/{ErrorLoggingEvents.cs => Events/ErrorLoggingPostExecutionEvent.cs} (51%) create mode 100644 src/Commands/Events/LoggingPreparationErrorEvent.cs diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index 0731ccd..d1c4f7a 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -1,4 +1,5 @@ using Boyfriend.Commands; +using Boyfriend.Commands.Events; using Boyfriend.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -71,7 +72,7 @@ public class Boyfriend { .AddInteractivity() .AddInteractionGroup() // Slash command event handlers - .AddPreparationErrorEvent() + .AddPreparationErrorEvent() .AddPostExecutionEvent() // Services .AddSingleton() diff --git a/src/Commands/ErrorLoggingEvents.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs similarity index 51% rename from src/Commands/ErrorLoggingEvents.cs rename to src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index c5eba21..51c2a8d 100644 --- a/src/Commands/ErrorLoggingEvents.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -5,38 +5,7 @@ using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Services; using Remora.Results; -namespace Boyfriend.Commands; - -/// -/// Handles error logging for slash commands that couldn't be successfully prepared. -/// -[UsedImplicitly] -public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent { - private readonly ILogger _logger; - - public ErrorLoggingPreparationErrorEvent(ILogger logger) { - _logger = logger; - } - - /// - /// Logs a warning using the injected if the has not - /// succeeded. - /// - /// The context of the slash command. Unused. - /// The result whose success is checked. - /// The cancellation token for this operation. Unused. - /// A result which has succeeded. - public Task PreparationFailed( - IOperationContext context, IResult preparationResult, CancellationToken ct = default) { - if (!preparationResult.IsSuccess && !preparationResult.Error.IsUserOrEnvironmentError()) { - _logger.LogWarning("Error in slash command preparation.\n{ErrorMessage}", preparationResult.Error.Message); - if (preparationResult.Error is ExceptionError exerr) - _logger.LogError(exerr.Exception, "An exception has been thrown"); - } - - return Task.FromResult(Result.FromSuccess()); - } -} +namespace Boyfriend.Commands.Events; /// /// Handles error logging for slash command groups. diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs new file mode 100644 index 0000000..7e8b2bb --- /dev/null +++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs @@ -0,0 +1,39 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Services; +using Remora.Results; + +namespace Boyfriend.Commands.Events; + +/// +/// Handles error logging for slash commands that couldn't be successfully prepared. +/// +[UsedImplicitly] +public class LoggingPreparationErrorEvent : IPreparationErrorEvent { + private readonly ILogger _logger; + + public LoggingPreparationErrorEvent(ILogger logger) { + _logger = logger; + } + + /// + /// Logs a warning using the injected if the has not + /// succeeded. + /// + /// The context of the slash command. Unused. + /// The result whose success is checked. + /// The cancellation token for this operation. Unused. + /// A result which has succeeded. + public Task PreparationFailed( + IOperationContext context, IResult preparationResult, CancellationToken ct = default) { + if (!preparationResult.IsSuccess && !preparationResult.Error.IsUserOrEnvironmentError()) { + _logger.LogWarning("Error in slash command preparation.\n{ErrorMessage}", preparationResult.Error.Message); + if (preparationResult.Error is ExceptionError exerr) + _logger.LogError(exerr.Exception, "An exception has been thrown"); + } + + return Task.FromResult(Result.FromSuccess()); + } +} From e2bf083189ac6584056749b128899b1da0b27ac6 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 20 Jul 2023 02:08:44 +0500 Subject: [PATCH 102/329] Reduce method complexity in /ban, /unban and some other commands (#50) This PR does numerous things to reduce method complexity: - Created an extension method `FeedbackService#SendContextualEmbedResultAsync`, which directly takes a `Result` and checks it once in the extension method instead of multiple times in all commands; - Split the command methods for `/ban` and `/unban` into 2 parts: `Execute(Un)Ban` and `(Un)BanUserAsync`. The former will check all the needed results and will pass the outputs into the latter; - Extracted the method for logging an action into Private- and PublicFeedbackChannels. It now resides in UtilityService. Right now, only `/ban` and `/unban` make use of that method; - Created an extension method `Snowflake#EmptyOrEqualTo`, that combines the task of checking if a Snowflake is empty and checking if that Snowflake is equal to another Snowflake. Similar changes will be made to other command groups in future PRs. This is not done here to make the reviewing process easier. Signed-off-by: Octol1ttle --- src/Commands/AboutCommandGroup.cs | 3 +- src/Commands/BanCommandGroup.cs | 229 ++++++++++----------------- src/Commands/ClearCommandGroup.cs | 3 +- src/Commands/KickCommandGroup.cs | 10 +- src/Commands/MuteCommandGroup.cs | 20 +-- src/Commands/PingCommandGroup.cs | 3 +- src/Commands/RemindCommandGroup.cs | 5 +- src/Commands/SettingsCommandGroup.cs | 9 +- src/Extensions.cs | 35 +++- src/Services/UtilityService.cs | 55 ++++++- 10 files changed, 186 insertions(+), 186 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 70f4bcf..760d96b 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -72,8 +72,7 @@ public class AboutCommandGroup : CommandGroup { .WithColour(ColorsList.Cyan) .WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png") .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } } diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index d2c1c76..902c753 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -7,13 +7,13 @@ using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Commands; @@ -57,7 +57,7 @@ public class BanCommandGroup : CommandGroup { /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user /// was banned and vice-versa. /// - /// + /// [Command("ban", "бан")] [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] [DiscordDefaultDMPermission(false)] @@ -66,122 +66,95 @@ public class BanCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Ban user")] [UsedImplicitly] - public async Task BanUserAsync( + public async Task ExecuteBan( [Description("User to ban")] IUser target, [Description("Ban reason")] string reason, [Description("Ban duration")] TimeSpan? duration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); - // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + return Result.FromError(guildResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); + return await BanUserAsync(target, reason, duration, guild, channelId.Value, user, currentUser); + } + + private async Task BanUserAsync( + IUser target, string reason, TimeSpan? duration, IGuild guild, Snowflake channelId, + IUser user, IUser currentUser) { + var data = await _dataService.GetData(guild.ID, CancellationToken); var cfg = data.Settings; Messages.Culture = GuildSettings.Language.Get(cfg); - var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken); + var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, CancellationToken); if (existingBanResult.IsDefined()) { - var embed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser) .WithColour(ColorsList.Red).Build(); - if (!embed.IsDefined(out var alreadyBuilt)) - return Result.FromError(embed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, CancellationToken); } var interactionResult - = await _utility.CheckInteractionsAsync(guildId.Value, userId.Value, target.ID, "Ban", CancellationToken); + = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Ban", CancellationToken); if (!interactionResult.IsSuccess) return Result.FromError(interactionResult); - Result responseEmbed; if (interactionResult.Entity is not null) { - responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) .WithColour(ColorsList.Red).Build(); - } else { - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); - if (!userResult.IsDefined(out var user)) - return Result.FromError(userResult); - var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)); - if (duration is not null) - builder.Append( - string.Format( - Messages.DescriptionActionExpiresAt, - Markdown.Timestamp(DateTimeOffset.UtcNow.Add(duration.Value)))); - var description = builder.ToString(); - - var dmChannelResult = await _userApi.CreateDMAsync(target.ID, CancellationToken); - if (dmChannelResult.IsDefined(out var dmChannel)) { - var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken); - if (!guildResult.IsDefined(out var guild)) - return Result.FromError(guildResult); - - var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) - .WithTitle(Messages.YouWereBanned) - .WithDescription(description) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Red) - .Build(); - - if (!dmEmbed.IsDefined(out var dmBuilt)) - return Result.FromError(dmEmbed); - await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken); - } - - var banResult = await _guildApi.CreateGuildBanAsync( - guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), - ct: CancellationToken); - if (!banResult.IsSuccess) - return Result.FromError(banResult.Error); - var memberData = data.GetMemberData(target.ID); - memberData.BannedUntil - = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; - memberData.Roles.Clear(); - - responseEmbed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserBanned, target.GetTag()), target) - .WithColour(ColorsList.Green).Build(); - - if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { - var logEmbed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserBanned, target.GetTag()), target) - .WithDescription(description) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Red) - .Build(); - - if (!logEmbed.IsDefined(out var logBuilt)) - return Result.FromError(logEmbed); - - var builtArray = new[] { logBuilt }; - // Not awaiting to reduce response time - if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - } + return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, CancellationToken); } - if (!responseEmbed.IsDefined(out var built)) - return Result.FromError(responseEmbed); + var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)); + if (duration is not null) + builder.Append( + string.Format( + Messages.DescriptionActionExpiresAt, + Markdown.Timestamp(DateTimeOffset.UtcNow.Add(duration.Value)))); + var title = string.Format(Messages.UserBanned, target.GetTag()); + var description = builder.ToString(); - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + var dmChannelResult = await _userApi.CreateDMAsync(target.ID, CancellationToken); + if (dmChannelResult.IsDefined(out var dmChannel)) { + var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) + .WithTitle(Messages.YouWereBanned) + .WithDescription(description) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Red) + .Build(); + + if (!dmEmbed.IsDefined(out var dmBuilt)) + return Result.FromError(dmEmbed); + await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken); + } + + var banResult = await _guildApi.CreateGuildBanAsync( + guild.ID, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), + ct: CancellationToken); + if (!banResult.IsSuccess) + return Result.FromError(banResult.Error); + var memberData = data.GetMemberData(target.ID); + memberData.BannedUntil + = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; + memberData.Roles.Clear(); + + var embed = new EmbedBuilder().WithSmallTitle( + title, target) + .WithColour(ColorsList.Green).Build(); + + _utility.LogActionAsync(cfg, channelId, title, target, description, user, CancellationToken); + + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } /// @@ -196,7 +169,7 @@ public class BanCommandGroup : CommandGroup { /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user /// was unbanned and vice-versa. /// - /// + /// /// [Command("unban")] [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] @@ -206,79 +179,53 @@ public class BanCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Unban user")] [UsedImplicitly] - public async Task UnbanUserAsync( + public async Task ExecuteUnban( [Description("User to unban")] IUser target, [Description("Unban reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); - // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - - var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(cfg); - - var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken); - if (!existingBanResult.IsDefined()) { - var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser) - .WithColour(ColorsList.Red).Build(); - - if (!embed.IsDefined(out var alreadyBuilt)) - return Result.FromError(embed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); - } - // Needed to get the tag and avatar var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); + return await UnbanUserAsync(target, reason, guildId.Value, channelId.Value, user, currentUser); + } + + private async Task UnbanUserAsync( + IUser target, string reason, Snowflake guildId, Snowflake channelId, IUser user, IUser currentUser) { + var cfg = await _dataService.GetSettings(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); + + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, CancellationToken); + if (!existingBanResult.IsDefined()) { + var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser) + .WithColour(ColorsList.Red).Build(); + + return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, CancellationToken); + } + var unbanResult = await _guildApi.RemoveGuildBanAsync( - guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), ct: CancellationToken); if (!unbanResult.IsSuccess) return Result.FromError(unbanResult.Error); - var responseEmbed = new EmbedBuilder().WithSmallTitle( + var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnbanned, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { - var logEmbed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserUnbanned, target.GetTag()), target) - .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Green) - .Build(); + var title = string.Format(Messages.UserUnbanned, target.GetTag()); + var description = string.Format(Messages.DescriptionActionReason, reason); + var logResult = _utility.LogActionAsync(cfg, channelId, title, target, description, user, CancellationToken); + if (!logResult.IsSuccess) + return Result.FromError(logResult.Error); - if (!logEmbed.IsDefined(out var logBuilt)) - return Result.FromError(logEmbed); - - var builtArray = new[] { logBuilt }; - - // Not awaiting to reduce response time - if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - } - - if (!responseEmbed.IsDefined(out var built)) - return Result.FromError(responseEmbed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } } diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index ede4d0b..306ab86 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -117,8 +117,7 @@ public class ClearCommandGroup : CommandGroup { var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) .WithColour(ColorsList.Green).Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } } diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 5809677..56154bd 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -83,10 +83,7 @@ public class KickCommandGroup : CommandGroup { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); - if (!embed.IsDefined(out var alreadyBuilt)) - return Result.FromError(embed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } var interactionResult @@ -162,9 +159,6 @@ public class KickCommandGroup : CommandGroup { } } - if (!responseEmbed.IsDefined(out var built)) - return Result.FromError(responseEmbed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken); } } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 4338d09..eff7029 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -84,10 +84,7 @@ public class MuteCommandGroup : CommandGroup { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); - if (!embed.IsDefined(out var alreadyBuilt)) - return Result.FromError(embed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } var interactionResult @@ -154,10 +151,7 @@ public class MuteCommandGroup : CommandGroup { } } - if (!responseEmbed.IsDefined(out var built)) - return Result.FromError(responseEmbed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken); } /// @@ -202,10 +196,7 @@ public class MuteCommandGroup : CommandGroup { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); - if (!embed.IsDefined(out var alreadyBuilt)) - return Result.FromError(embed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } var interactionResult @@ -258,9 +249,6 @@ public class MuteCommandGroup : CommandGroup { ct: CancellationToken); } - if (!responseEmbed.IsDefined(out var built)) - return Result.FromError(responseEmbed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken); } } diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index ad05451..5819336 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -77,8 +77,7 @@ public class PingCommandGroup : CommandGroup { .WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red) .WithCurrentTimestamp() .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } } diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 28fc0ed..954e84a 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -71,9 +71,6 @@ public class RemindCommandGroup : CommandGroup { .WithColour(ColorsList.Green) .Build(); - if (!embed.IsDefined(out var built)) - return Result.FromError(embed); - - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } } diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 5c4a505..d2d28be 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -90,9 +90,8 @@ public class SettingsCommandGroup : CommandGroup { .WithDescription(builder.ToString()) .WithColour(ColorsList.Default) .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } /// @@ -132,9 +131,8 @@ public class SettingsCommandGroup : CommandGroup { .WithDescription(setResult.Error.Message) .WithColour(ColorsList.Red) .Build(); - if (!failedEmbed.IsDefined(out var failedBuilt)) return Result.FromError(failedEmbed); - return (Result)await _feedbackService.SendContextualEmbedAsync(failedBuilt, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, CancellationToken); } var builder = new StringBuilder(); @@ -147,8 +145,7 @@ public class SettingsCommandGroup : CommandGroup { .WithDescription(builder.ToString()) .WithColour(ColorsList.Green) .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } } diff --git a/src/Extensions.cs b/src/Extensions.cs index 18a3c06..a9e0d48 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -7,9 +7,11 @@ using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; +using Remora.Results; namespace Boyfriend; @@ -51,10 +53,9 @@ public static class Extensions { /// The builder to add the small title to. /// The text of the small title. /// The user whose avatar to use in the small title. - /// The URL that will be opened if a user clicks on the small title. /// The builder with the added small title in the author field. public static EmbedBuilder WithSmallTitle( - this EmbedBuilder builder, string text, IUser? avatarSource = null, string? url = default) { + this EmbedBuilder builder, string text, IUser? avatarSource = null) { Uri? avatarUrl = null; if (avatarSource is not null) { var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); @@ -64,7 +65,7 @@ public static class Extensions { : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; } - builder.Author = new EmbedAuthorBuilder(text, url, avatarUrl?.AbsoluteUri); + builder.Author = new EmbedAuthorBuilder(text, iconUrl: avatarUrl?.AbsoluteUri); return builder; } @@ -191,7 +192,35 @@ public static class Extensions { && context.TryGetUserID(out userId); } + /// + /// Checks whether this Snowflake has any value set. + /// + /// The Snowflake to check. + /// true if the Snowflake has no value set or it's set to 0, false otherwise. public static bool Empty(this Snowflake snowflake) { return snowflake.Value is 0; } + + /// + /// Checks whether this snowflake is empty (see ) or it's equal to + /// + /// + /// The Snowflake to check for emptiness + /// The Snowflake to check for equality with . + /// + /// true if is empty or is equal to , false + /// otherwise. + /// + /// + public static bool EmptyOrEqualTo(this Snowflake snowflake, Snowflake anotherSnowflake) { + return snowflake.Empty() || snowflake == anotherSnowflake; + } + + public static async Task SendContextualEmbedResultAsync( + this FeedbackService feedback, Result embedResult, CancellationToken ct = default) { + if (!embedResult.IsDefined(out var embed)) + return Result.FromError(embedResult); + + return (Result)await feedback.SendContextualEmbedAsync(embed, ct: ct); + } } diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 3b6a2bf..b1ae347 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -4,6 +4,7 @@ using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; @@ -15,15 +16,18 @@ namespace Boyfriend.Services; /// of some Discord APIs. /// public class UtilityService : IHostedService { + private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestUserAPI _userApi; public UtilityService( - IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IDiscordRestGuildScheduledEventAPI eventApi) { + IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, + IDiscordRestUserAPI userApi) { + _channelApi = channelApi; + _eventApi = eventApi; _guildApi = guildApi; _userApi = userApi; - _eventApi = eventApi; } public Task StartAsync(CancellationToken ct) { @@ -132,4 +136,51 @@ public class UtilityService : IHostedService { .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} ")); return builder.ToString(); } + + /// + /// Logs an action in the and + /// . + /// + /// The guild configuration. + /// The ID of the channel where the action was executed. + /// The title for the embed. + /// The user whose avatar will be displayed next to the of the embed. + /// The description of the embed. + /// The user who performed the action. + /// The cancellation token for this operation. + /// + public Result LogActionAsync( + JsonNode cfg, Snowflake channelId, string title, IUser avatar, string description, + IUser user, CancellationToken ct = default) { + var publicChannel = GuildSettings.PublicFeedbackChannel.Get(cfg); + var privateChannel = GuildSettings.PrivateFeedbackChannel.Get(cfg); + if (GuildSettings.PublicFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId) + && GuildSettings.PrivateFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId)) + return Result.FromSuccess(); + + var logEmbed = new EmbedBuilder().WithSmallTitle(title, avatar) + .WithDescription(description) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Green) + .Build(); + + if (!logEmbed.IsDefined(out var logBuilt)) + return Result.FromError(logEmbed); + + var builtArray = new[] { logBuilt }; + + // Not awaiting to reduce response time + if (publicChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + publicChannel, embeds: builtArray, + ct: ct); + if (privateChannel != publicChannel + && privateChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + privateChannel, embeds: builtArray, + ct: ct); + + return Result.FromSuccess(); + } } From 685688bbe8a4ab477dc38ad565a4d5694bb2baf9 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 20 Jul 2023 12:25:03 +0500 Subject: [PATCH 103/329] Reduce method complexity of UtilityService#CheckInteractionsAsync (#55) This PR reduces method complexity in `UtilityService#CheckInteractionsAsync` by splitting the method into two parts, similar to `/ban` and `/unban`. Additionally, it converts the last `if` statement to a `return` with ternary. After this is merged, status checks on #54 should succeed and that PR should be merged. Signed-off-by: Octol1ttle --- src/Services/UtilityService.cs | 35 ++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index b1ae347..44168fa 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -64,13 +64,10 @@ public class UtilityService : IHostedService { var currentUserResult = await _userApi.GetCurrentUserAsync(ct); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - if (currentUser.ID == targetId) - return Result.FromSuccess($"UserCannot{action}Bot".Localized()); var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); - if (targetId == guild.OwnerID) return Result.FromSuccess($"UserCannot{action}Owner".Localized()); var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); if (!targetMemberResult.IsDefined(out var targetMember)) @@ -84,6 +81,25 @@ public class UtilityService : IHostedService { if (!rolesResult.IsDefined(out var roles)) return Result.FromError(rolesResult); + var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId, ct); + return interacterResult.IsDefined(out var interacter) + ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter) + : Result.FromError(interacterResult); + } + + private static Result CheckInteractions( + string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, + IGuildMember interacter) { + if (!targetMember.User.IsDefined(out var targetUser)) + return Result.FromError(new ArgumentNullError(nameof(targetMember.User))); + if (!interacter.User.IsDefined(out var interacterUser)) + return Result.FromError(new ArgumentNullError(nameof(interacter.User))); + + if (currentMember.User == targetMember.User) + return Result.FromSuccess($"UserCannot{action}Bot".Localized()); + + if (targetUser.ID == guild.OwnerID) return Result.FromSuccess($"UserCannot{action}Owner".Localized()); + var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); @@ -91,20 +107,15 @@ public class UtilityService : IHostedService { if (targetBotRoleDiff >= 0) return Result.FromSuccess($"BotCannot{action}Target".Localized()); - if (interacterId == guild.OwnerID) + if (interacterUser.ID == guild.OwnerID) return Result.FromSuccess(null); - var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId, ct); - if (!interacterResult.IsDefined(out var interacter)) - return Result.FromError(interacterResult); - var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); var targetInteracterRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); - if (targetInteracterRoleDiff >= 0) - return Result.FromSuccess($"UserCannot{action}Target".Localized()); - - return Result.FromSuccess(null); + return targetInteracterRoleDiff < 0 + ? Result.FromSuccess(null) + : Result.FromSuccess($"UserCannot{action}Target".Localized()); } /// From daa1dd41843e0c69cfe9ce2b07bc933e1da27cb6 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 20 Jul 2023 12:36:21 +0500 Subject: [PATCH 104/329] General fixes (#54) Depends on #55 These changes are really small and I don't know how to reasonably split them into multiple PRs, but here's the changelog: - The result of the `UtilityService#LogActionAsync` call in `BanCommandGroup#BanUserAsync` is no longer ignored and it's errors will now prevent feedback from being sent; - Converted `if` statement to a `return` with ternary in `LanguageOption`; - Slightly decreased method complexity of `MessageDeletedResponder#RespondAsync` by cleverly using Results; - Changed the order of parameters for `UtilityService#LogActionAsync` to flow better; - Removed the exemption for `ConvertIfToReturnStatement` for InspectCode. --------- Signed-off-by: Octol1ttle --- .github/workflows/resharper.yml | 2 +- src/Commands/BanCommandGroup.cs | 6 ++++-- src/Data/Options/LanguageOption.cs | 7 +++---- src/Responders/MessageDeletedResponder.cs | 14 ++++++-------- src/Services/UtilityService.cs | 10 +++++----- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index b0ec1a3..29302aa 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -31,5 +31,5 @@ jobs: uses: muno92/resharper_inspectcode@1.7.1 with: solutionPath: ./Boyfriend.sln - ignoreIssueType: InvertIf, ConvertIfStatementToReturnStatement, ConvertIfStatementToSwitchStatement + ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement solutionWideAnalysis: true diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 902c753..14f6d4f 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -152,7 +152,9 @@ public class BanCommandGroup : CommandGroup { title, target) .WithColour(ColorsList.Green).Build(); - _utility.LogActionAsync(cfg, channelId, title, target, description, user, CancellationToken); + var logResult = _utility.LogActionAsync(cfg, channelId, user, title, description, target, CancellationToken); + if (!logResult.IsSuccess) + return Result.FromError(logResult.Error); return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } @@ -222,7 +224,7 @@ public class BanCommandGroup : CommandGroup { var title = string.Format(Messages.UserUnbanned, target.GetTag()); var description = string.Format(Messages.DescriptionActionReason, reason); - var logResult = _utility.LogActionAsync(cfg, channelId, title, target, description, user, CancellationToken); + var logResult = _utility.LogActionAsync(cfg, channelId, user, title, description, target, CancellationToken); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs index 6c4a49f..dbe1b4f 100644 --- a/src/Data/Options/LanguageOption.cs +++ b/src/Data/Options/LanguageOption.cs @@ -27,9 +27,8 @@ public class LanguageOption : Option { /// public override Result Set(JsonNode settings, string from) { - if (!CultureInfoCache.ContainsKey(from.ToLowerInvariant())) - return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported)); - - return base.Set(settings, from.ToLowerInvariant()); + return CultureInfoCache.ContainsKey(from.ToLowerInvariant()) + ? base.Set(settings, from.ToLowerInvariant()) + : Result.FromError(new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported)); } } diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 903db84..652631e 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -46,15 +46,13 @@ public class MessageDeletedResponder : IResponder { if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); var auditLog = auditLogPage.AuditLogEntries.Single(); - if (!auditLog.Options.IsDefined(out var options)) - return Result.FromError(new ArgumentNullError(nameof(auditLog.Options))); - var user = message.Author; - if (options.ChannelID == gatewayEvent.ChannelID - && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { - var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); - if (!userResult.IsDefined(out user)) return Result.FromError(userResult); - } + var userResult = Result.FromSuccess(message.Author); + if (auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID + && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) + userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); + + if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); Messages.Culture = GuildSettings.Language.Get(cfg); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 44168fa..18808eb 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -154,15 +154,15 @@ public class UtilityService : IHostedService { /// /// The guild configuration. /// The ID of the channel where the action was executed. - /// The title for the embed. - /// The user whose avatar will be displayed next to the of the embed. - /// The description of the embed. /// The user who performed the action. + /// The title for the embed. + /// The description of the embed. + /// The user whose avatar will be displayed next to the of the embed. /// The cancellation token for this operation. /// public Result LogActionAsync( - JsonNode cfg, Snowflake channelId, string title, IUser avatar, string description, - IUser user, CancellationToken ct = default) { + JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar, + CancellationToken ct = default) { var publicChannel = GuildSettings.PublicFeedbackChannel.Get(cfg); var privateChannel = GuildSettings.PrivateFeedbackChannel.Get(cfg); if (GuildSettings.PublicFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId) From 4624b59a8abb2f80e4fc0fc8a3806d4ed736740c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 20 Jul 2023 12:44:07 +0500 Subject: [PATCH 105/329] Reduce method complexity of /clear (#56) Depends on #54 This PR reduces the method complexity of `ClearCommandGroup#ClearMessagesAsync` by doing the following: - Splitting the command method into 2 parts: ExecuteClear and ClearMessagesAsync. The former will check all the needed results and will pass the outputs into the latter; - Using the recently extracted method `UtilityService#LogActionAsync` to log deletion of messages. Signed-off-by: Octol1ttle --- src/Commands/ClearCommandGroup.cs | 58 +++++++++++++------------------ 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 306ab86..ce1787c 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -28,15 +28,17 @@ public class ClearCommandGroup : CommandGroup { private readonly GuildDataService _dataService; private readonly FeedbackService _feedbackService; private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public ClearCommandGroup( IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService dataService, - FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + FeedbackService feedbackService, IDiscordRestUserAPI userApi, UtilityService utility) { _channelApi = channelApi; _context = context; _dataService = dataService; _feedbackService = feedbackService; _userApi = userApi; + _utility = utility; } /// @@ -55,7 +57,7 @@ public class ClearCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.ManageMessages)] [Description("Remove multiple messages")] [UsedImplicitly] - public async Task ClearMessagesAsync( + public async Task ExecuteClear( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] int amount) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) @@ -66,12 +68,25 @@ public class ClearCommandGroup : CommandGroup { channelId.Value, limit: amount + 1, ct: CancellationToken); if (!messagesResult.IsDefined(out var messages)) return Result.FromError(messagesResult); + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + // The current user's avatar is used when sending messages + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); - var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + return await ClearMessagesAsync(amount, guildId.Value, channelId.Value, messages, user, currentUser); + } + + private async Task ClearMessagesAsync( + int amount, Snowflake guildId, Snowflake channelId, IReadOnlyList messages, + IUser user, IUser currentUser) { + var cfg = await _dataService.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); var idList = new List(messages.Count); - var builder = new StringBuilder().AppendLine(Mention.Channel(channelId.Value)).AppendLine(); + var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); for (var i = messages.Count - 1; i >= 1; i--) { // '>= 1' to skip last message ('Boyfriend is thinking...') var message = messages[i]; idList.Add(message.ID); @@ -79,41 +94,18 @@ public class ClearCommandGroup : CommandGroup { builder.Append(message.Content.InBlockCode()); } + var title = string.Format(Messages.MessagesCleared, amount.ToString()); var description = builder.ToString(); - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); - if (!userResult.IsDefined(out var user)) - return Result.FromError(userResult); - var deleteResult = await _channelApi.BulkDeleteMessagesAsync( - channelId.Value, idList, user.GetTag().EncodeHeader(), CancellationToken); + channelId, idList, user.GetTag().EncodeHeader(), CancellationToken); if (!deleteResult.IsSuccess) return Result.FromError(deleteResult.Error); - // The current user's avatar is used when sending messages - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) - return Result.FromError(currentUserResult); - - var title = string.Format(Messages.MessagesCleared, amount.ToString()); - if (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) { - var logEmbed = new EmbedBuilder().WithSmallTitle(title, currentUser) - .WithDescription(description) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Red) - .Build(); - - if (!logEmbed.IsDefined(out var logBuilt)) - return Result.FromError(logEmbed); - - // Not awaiting to reduce response time - if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { logBuilt }, - ct: CancellationToken); - } + var logResult = _utility.LogActionAsync( + cfg, channelId, user, title, description, currentUser, CancellationToken); + if (!logResult.IsSuccess) + return Result.FromError(logResult.Error); var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) .WithColour(ColorsList.Green).Build(); From d43ee59099b2d53bbf2f6bbbccf2c039f795a3ef Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:55:02 +0300 Subject: [PATCH 106/329] Remove "code quality" badge (#57) https://github.com/TeamOctolings/Boyfriend/assets/95250141/efdb0bef-930f-43e5-bba8-6554473f3580 Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- docs/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 0cde8ef..e6d35fb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,6 @@ ![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master) ![GitHub last commit](https://img.shields.io/github/last-commit/TeamOctolings/Boyfriend) -![CodeFactor](https://img.shields.io/codefactor/grade/github/TeamOctolings/Boyfriend) Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and Remora.Discord From 0938f518f86609e6a5cd438824d7ed41818b8160 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 18:44:10 +0500 Subject: [PATCH 107/329] Bump muno92/resharper_inspectcode from 1.7.1 to 1.8.0 (#58) Bumps muno92/resharper_inspectcode from 1.7.1 to 1.8.0. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/resharper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index 29302aa..f356672 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -28,7 +28,7 @@ jobs: run: dotnet restore - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.7.1 + uses: muno92/resharper_inspectcode@1.8.0 with: solutionPath: ./Boyfriend.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement From 8c72dc663e4a45c463310efe8e467781266a5361 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 21 Jul 2023 01:20:25 +0500 Subject: [PATCH 108/329] Do not assign Octol1ttle for Dependabot PRs (#59) This PR disables automatic assignation of all PRs created by Dependabot to me. There are now more people working on Boyfriend than just me. In fact, this project, in theory, can live without me as there are other members working on it. Signed-off-by: Octol1ttle --- .github/dependabot.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f573c5e..fd32dff 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,8 +12,6 @@ updates: allow: # Allow both direct and indirect updates for all packages - dependency-type: "all" - assignees: - - "Octol1ttle" labels: - "type: dependencies" @@ -24,8 +22,5 @@ updates: allow: # Allow both direct and indirect updates for all packages - dependency-type: "all" - # Add assignees - assignees: - - "Octol1ttle" labels: - "type: dependencies" From 27b8f15e3b5e16067cf25a9d2668ace850aeb350 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 21 Jul 2023 01:41:02 +0500 Subject: [PATCH 109/329] Force maximum Cognitive Complexity via InspectCode (#60) This PR uses a new feature of the InspectCode action: extensions. By adding the `Cognitive Complexity` extension, InspectCode can provide warnings and force the contributors to follow standards regarding complex methods. This functionality was previously provided by CodeFactor, but it is no longer used. Here's the full changelog of this PR: - Allowed Actions to run on push for any branch, not just `master`; - Added `Cognitive Complexity` plugin for InspectCode; - Significantly reduced complexity of KickCommandGroup, MuteCommandGroup and GuildUpdateService --------- Signed-off-by: Octol1ttle --- .github/workflows/resharper.yml | 5 +- src/Commands/AboutCommandGroup.cs | 9 +- src/Commands/BanCommandGroup.cs | 59 ++-- src/Commands/ClearCommandGroup.cs | 18 +- src/Commands/KickCommandGroup.cs | 122 ++++---- src/Commands/MuteCommandGroup.cs | 196 ++++++------ src/Commands/PingCommandGroup.cs | 13 +- src/Commands/RemindCommandGroup.cs | 19 +- src/Commands/SettingsCommandGroup.cs | 28 +- src/InteractionResponders.cs | 2 +- src/Services/GuildUpdateService.cs | 426 +++++++++++++++------------ 11 files changed, 476 insertions(+), 421 deletions(-) diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index f356672..860dd94 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -4,12 +4,12 @@ concurrency: cancel-in-progress: true on: + push: + branches: [ "*" ] pull_request: branches: [ "master" ] merge_group: types: [checks_requested] - push: - branches: [ "master" ] jobs: inspect-code: @@ -32,4 +32,5 @@ jobs: with: solutionPath: ./Boyfriend.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement + extensions: ReSharperPlugin.CognitiveComplexity solutionWideAnalysis: true diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 760d96b..50e7de7 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -5,6 +5,7 @@ using Boyfriend.Services; using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; @@ -47,7 +48,7 @@ public class AboutCommandGroup : CommandGroup { [RequireContext(ChannelContext.Guild)] [Description("Shows Boyfriend's developers")] [UsedImplicitly] - public async Task SendAboutBotAsync() { + public async Task ExecuteAboutAsync() { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -59,6 +60,10 @@ public class AboutCommandGroup : CommandGroup { var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); + return await SendAboutBotAsync(currentUser, CancellationToken); + } + + private async Task SendAboutBotAsync(IUser currentUser, CancellationToken ct = default) { var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers)); foreach (var dev in Developers) builder.AppendLine($"@{dev} — {$"AboutDeveloper@{dev}".Localized()}"); @@ -73,6 +78,6 @@ public class AboutCommandGroup : CommandGroup { .WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png") .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 14f6d4f..a8672a2 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -66,7 +66,7 @@ public class BanCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Ban user")] [UsedImplicitly] - public async Task ExecuteBan( + public async Task ExecuteBanAsync( [Description("User to ban")] IUser target, [Description("Ban reason")] string reason, [Description("Ban duration")] TimeSpan? duration = null) { @@ -84,26 +84,26 @@ public class BanCommandGroup : CommandGroup { if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); - return await BanUserAsync(target, reason, duration, guild, channelId.Value, user, currentUser); + var data = await _dataService.GetData(guild.ID, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await BanUserAsync( + target, reason, duration, guild, data, channelId.Value, user, currentUser, CancellationToken); } private async Task BanUserAsync( - IUser target, string reason, TimeSpan? duration, IGuild guild, Snowflake channelId, - IUser user, IUser currentUser) { - var data = await _dataService.GetData(guild.ID, CancellationToken); - var cfg = data.Settings; - Messages.Culture = GuildSettings.Language.Get(cfg); - - var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, CancellationToken); + IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, + IUser user, IUser currentUser, CancellationToken ct = default) { + var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct); if (existingBanResult.IsDefined()) { var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); } var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Ban", CancellationToken); + = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Ban", ct); if (!interactionResult.IsSuccess) return Result.FromError(interactionResult); @@ -111,7 +111,7 @@ public class BanCommandGroup : CommandGroup { var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, ct); } var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)); @@ -123,7 +123,7 @@ public class BanCommandGroup : CommandGroup { var title = string.Format(Messages.UserBanned, target.GetTag()); var description = builder.ToString(); - var dmChannelResult = await _userApi.CreateDMAsync(target.ID, CancellationToken); + var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct); if (dmChannelResult.IsDefined(out var dmChannel)) { var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereBanned) @@ -135,12 +135,12 @@ public class BanCommandGroup : CommandGroup { if (!dmEmbed.IsDefined(out var dmBuilt)) return Result.FromError(dmEmbed); - await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken); + await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct); } var banResult = await _guildApi.CreateGuildBanAsync( guild.ID, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), - ct: CancellationToken); + ct: ct); if (!banResult.IsSuccess) return Result.FromError(banResult.Error); var memberData = data.GetMemberData(target.ID); @@ -152,11 +152,12 @@ public class BanCommandGroup : CommandGroup { title, target) .WithColour(ColorsList.Green).Build(); - var logResult = _utility.LogActionAsync(cfg, channelId, user, title, description, target, CancellationToken); + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, target, ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } /// @@ -171,7 +172,7 @@ public class BanCommandGroup : CommandGroup { /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user /// was unbanned and vice-versa. /// - /// + /// /// [Command("unban")] [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] @@ -196,25 +197,27 @@ public class BanCommandGroup : CommandGroup { if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); - return await UnbanUserAsync(target, reason, guildId.Value, channelId.Value, user, currentUser); + var data = await _dataService.GetData(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await UnbanUserAsync( + target, reason, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken); } private async Task UnbanUserAsync( - IUser target, string reason, Snowflake guildId, Snowflake channelId, IUser user, IUser currentUser) { - var cfg = await _dataService.GetSettings(guildId, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(cfg); - - var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, CancellationToken); + IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, + IUser currentUser, CancellationToken ct = default) { + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct); if (!existingBanResult.IsDefined()) { var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, ct); } var unbanResult = await _guildApi.RemoveGuildBanAsync( guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), - ct: CancellationToken); + ct); if (!unbanResult.IsSuccess) return Result.FromError(unbanResult.Error); @@ -224,10 +227,10 @@ public class BanCommandGroup : CommandGroup { var title = string.Format(Messages.UserUnbanned, target.GetTag()); var description = string.Format(Messages.DescriptionActionReason, reason); - var logResult = _utility.LogActionAsync(cfg, channelId, user, title, description, target, CancellationToken); + var logResult = _utility.LogActionAsync(data.Settings, channelId, user, title, description, target, ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index ce1787c..46ddc24 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -76,15 +76,15 @@ public class ClearCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - return await ClearMessagesAsync(amount, guildId.Value, channelId.Value, messages, user, currentUser); + var data = await _dataService.GetData(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await ClearMessagesAsync(amount, data, channelId.Value, messages, user, currentUser, CancellationToken); } private async Task ClearMessagesAsync( - int amount, Snowflake guildId, Snowflake channelId, IReadOnlyList messages, - IUser user, IUser currentUser) { - var cfg = await _dataService.GetSettings(guildId, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(cfg); - + int amount, GuildData data, Snowflake channelId, IReadOnlyList messages, + IUser user, IUser currentUser, CancellationToken ct = default) { var idList = new List(messages.Count); var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); for (var i = messages.Count - 1; i >= 1; i--) { // '>= 1' to skip last message ('Boyfriend is thinking...') @@ -98,18 +98,18 @@ public class ClearCommandGroup : CommandGroup { var description = builder.ToString(); var deleteResult = await _channelApi.BulkDeleteMessagesAsync( - channelId, idList, user.GetTag().EncodeHeader(), CancellationToken); + channelId, idList, user.GetTag().EncodeHeader(), ct); if (!deleteResult.IsSuccess) return Result.FromError(deleteResult.Error); var logResult = _utility.LogActionAsync( - cfg, channelId, user, title, description, currentUser, CancellationToken); + data.Settings, channelId, user, title, description, currentUser, ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) .WithColour(ColorsList.Green).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 56154bd..9111b7f 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -6,12 +6,12 @@ using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; +using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Commands; @@ -62,21 +62,25 @@ public class KickCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] [Description("Kick member")] [UsedImplicitly] - public async Task KickUserAsync( + public async Task ExecuteKick( [Description("Member to kick")] IUser target, [Description("Kick reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); - // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + return Result.FromError(guildResult); var data = await _dataService.GetData(guildId.Value, CancellationToken); - var cfg = data.Settings; - Messages.Culture = GuildSettings.Language.Get(cfg); + Messages.Culture = GuildSettings.Language.Get(data.Settings); var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); if (!memberResult.IsSuccess) { @@ -86,79 +90,57 @@ public class KickCommandGroup : CommandGroup { return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } + return await KickUserAsync(target, reason, guild, channelId.Value, data, user, currentUser, CancellationToken); + } + + private async Task KickUserAsync( + IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser user, IUser currentUser, + CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync(guildId.Value, userId.Value, target.ID, "Kick", CancellationToken); + = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Kick", ct); if (!interactionResult.IsSuccess) return Result.FromError(interactionResult); - Result responseEmbed; if (interactionResult.Entity is not null) { - responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) .WithColour(ColorsList.Red).Build(); - } else { - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); - if (!userResult.IsDefined(out var user)) - return Result.FromError(userResult); - var dmChannelResult = await _userApi.CreateDMAsync(target.ID, CancellationToken); - if (dmChannelResult.IsDefined(out var dmChannel)) { - var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken); - if (!guildResult.IsDefined(out var guild)) - return Result.FromError(guildResult); - - var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) - .WithTitle(Messages.YouWereKicked) - .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Red) - .Build(); - - if (!dmEmbed.IsDefined(out var dmBuilt)) - return Result.FromError(dmEmbed); - await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken); - } - - var kickResult = await _guildApi.RemoveGuildMemberAsync( - guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), - ct: CancellationToken); - if (!kickResult.IsSuccess) - return Result.FromError(kickResult.Error); - data.GetMemberData(target.ID).Roles.Clear(); - - responseEmbed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserKicked, target.GetTag()), target) - .WithColour(ColorsList.Green).Build(); - - if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { - var logEmbed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserKicked, target.GetTag()), target) - .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Red) - .Build(); - - if (!logEmbed.IsDefined(out var logBuilt)) - return Result.FromError(logEmbed); - - var builtArray = new[] { logBuilt }; - // Not awaiting to reduce response time - if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - } + return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); } - return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken); + var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct); + if (dmChannelResult.IsDefined(out var dmChannel)) { + var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) + .WithTitle(Messages.YouWereKicked) + .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(ColorsList.Red) + .Build(); + + if (!dmEmbed.IsDefined(out var dmBuilt)) + return Result.FromError(dmEmbed); + await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct); + } + + var kickResult = await _guildApi.RemoveGuildMemberAsync( + guild.ID, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + ct); + if (!kickResult.IsSuccess) + return Result.FromError(kickResult.Error); + data.GetMemberData(target.ID).Roles.Clear(); + + var title = string.Format(Messages.UserKicked, target.GetTag()); + var description = string.Format(Messages.DescriptionActionReason, reason); + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, target, ct); + if (!logResult.IsSuccess) + return Result.FromError(logResult.Error); + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserKicked, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index eff7029..14895d5 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -7,13 +7,13 @@ using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Commands; @@ -23,20 +23,17 @@ namespace Boyfriend.Commands; /// [UsedImplicitly] public class MuteCommandGroup : CommandGroup { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; - private readonly IDiscordRestGuildAPI _guildApi; - private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; + private readonly FeedbackService _feedbackService; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public MuteCommandGroup( - ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService, - FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - UtilityService utility) { + ICommandContext context, GuildDataService dataService, FeedbackService feedbackService, + IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) { _context = context; - _channelApi = channelApi; _dataService = dataService; _feedbackService = feedbackService; _guildApi = guildApi; @@ -57,7 +54,7 @@ public class MuteCommandGroup : CommandGroup { /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member /// was muted and vice-versa. /// - /// + /// [Command("mute", "мут")] [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] [DiscordDefaultDMPermission(false)] @@ -66,7 +63,7 @@ public class MuteCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Mute member")] [UsedImplicitly] - public async Task MuteUserAsync( + public async Task ExecuteMute( [Description("Member to mute")] IUser target, [Description("Mute reason")] string reason, [Description("Mute duration")] TimeSpan duration) { @@ -79,6 +76,13 @@ public class MuteCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var data = await _dataService.GetData(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); if (!memberResult.IsSuccess) { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) @@ -87,71 +91,49 @@ public class MuteCommandGroup : CommandGroup { return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } + return await MuteUserAsync( + target, reason, duration, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken); + } + + private async Task MuteUserAsync( + IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, + IUser user, IUser currentUser, CancellationToken ct = default) { var interactionResult = await _utility.CheckInteractionsAsync( - guildId.Value, userId.Value, target.ID, "Mute", CancellationToken); + guildId, user.ID, target.ID, "Mute", ct); if (!interactionResult.IsSuccess) return Result.FromError(interactionResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); - var cfg = data.Settings; - Messages.Culture = GuildSettings.Language.Get(cfg); - - Result responseEmbed; if (interactionResult.Entity is not null) { - responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) .WithColour(ColorsList.Red).Build(); - } else { - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); - if (!userResult.IsDefined(out var user)) - return Result.FromError(userResult); - var until = DateTimeOffset.UtcNow.Add(duration); // >:) - var muteResult = await _guildApi.ModifyGuildMemberAsync( - guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), - communicationDisabledUntil: until, ct: CancellationToken); - if (!muteResult.IsSuccess) - return Result.FromError(muteResult.Error); - - responseEmbed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserMuted, target.GetTag()), target) - .WithColour(ColorsList.Green).Build(); - - if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { - var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)) - .Append( - string.Format( - Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))); - - var logEmbed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserMuted, target.GetTag()), target) - .WithDescription(builder.ToString()) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Red) - .Build(); - - if (!logEmbed.IsDefined(out var logBuilt)) - return Result.FromError(logEmbed); - - var builtArray = new[] { logBuilt }; - // Not awaiting to reduce response time - if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - } + return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); } - return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken); + var until = DateTimeOffset.UtcNow.Add(duration); // >:) + var muteResult = await _guildApi.ModifyGuildMemberAsync( + guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), + communicationDisabledUntil: until, ct: ct); + if (!muteResult.IsSuccess) + return Result.FromError(muteResult.Error); + + var title = string.Format(Messages.UserMuted, target.GetTag()); + var description = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)) + .Append( + string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); + + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, target, ct); + if (!logResult.IsSuccess) + return Result.FromError(logResult.Error); + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserMuted, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } /// @@ -166,7 +148,7 @@ public class MuteCommandGroup : CommandGroup { /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member /// was unmuted and vice-versa. /// - /// + /// /// [Command("unmute", "размут")] [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] @@ -176,7 +158,7 @@ public class MuteCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Unmute member")] [UsedImplicitly] - public async Task UnmuteUserAsync( + public async Task ExecuteUnmute( [Description("Member to unmute")] IUser target, [Description("Unmute reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) @@ -188,8 +170,13 @@ public class MuteCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(cfg); + // Needed to get the tag and avatar + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var data = await _dataService.GetData(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); if (!memberResult.IsSuccess) { @@ -199,56 +186,43 @@ public class MuteCommandGroup : CommandGroup { return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } + return await UnmuteUserAsync( + target, reason, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken); + } + + private async Task UnmuteUserAsync( + IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, + IUser currentUser, CancellationToken ct = default) { var interactionResult = await _utility.CheckInteractionsAsync( - guildId.Value, userId.Value, target.ID, "Unmute", CancellationToken); + guildId, user.ID, target.ID, "Unmute", ct); if (!interactionResult.IsSuccess) return Result.FromError(interactionResult); - // Needed to get the tag and avatar - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); - if (!userResult.IsDefined(out var user)) - return Result.FromError(userResult); + if (interactionResult.Entity is not null) { + var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + .WithColour(ColorsList.Red).Build(); + + return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); + } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( - guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), - communicationDisabledUntil: null, ct: CancellationToken); + guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + communicationDisabledUntil: null, ct: ct); if (!unmuteResult.IsSuccess) return Result.FromError(unmuteResult.Error); - var responseEmbed = new EmbedBuilder().WithSmallTitle( + var title = string.Format(Messages.UserUnmuted, target.GetTag()); + var description = string.Format(Messages.DescriptionActionReason, reason); + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, target, ct); + if (!logResult.IsSuccess) + return Result.FromError(logResult.Error); + + var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnmuted, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - || (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) { - var logEmbed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserUnmuted, target.GetTag()), target) - .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Green) - .Build(); - - if (!logEmbed.IsDefined(out var logBuilt)) - return Result.FromError(logEmbed); - - var builtArray = new[] { logBuilt }; - - // Not awaiting to reduce response time - if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg) - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray, - ct: CancellationToken); - } - - return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 5819336..83b04c9 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -4,6 +4,7 @@ using Boyfriend.Services; using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; @@ -11,6 +12,7 @@ using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway; +using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Commands; @@ -49,7 +51,7 @@ public class PingCommandGroup : CommandGroup { [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [UsedImplicitly] - public async Task SendPingAsync() { + public async Task ExecutePingAsync() { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -61,11 +63,16 @@ public class PingCommandGroup : CommandGroup { var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); + return await SendLatencyAsync(channelId.Value, currentUser, CancellationToken); + } + + private async Task SendLatencyAsync( + Snowflake channelId, IUser currentUser, CancellationToken ct = default) { var latency = _client.Latency.TotalMilliseconds; if (latency is 0) { // No heartbeat has occurred, estimate latency from local time and "Boyfriend is thinking..." message var lastMessageResult = await _channelApi.GetChannelMessagesAsync( - channelId.Value, limit: 1, ct: CancellationToken); + channelId, limit: 1, ct: ct); if (!lastMessageResult.IsDefined(out var lastMessage)) return Result.FromError(lastMessageResult); latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds; @@ -78,6 +85,6 @@ public class PingCommandGroup : CommandGroup { .WithCurrentTimestamp() .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 954e84a..90016f6 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -4,6 +4,7 @@ using Boyfriend.Services; using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; @@ -11,6 +12,7 @@ using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Commands; @@ -45,7 +47,7 @@ public class RemindCommandGroup : CommandGroup { [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [UsedImplicitly] - public async Task AddReminderAsync( + public async Task ExecuteReminderAsync( [Description("After what period of time mention the reminder")] TimeSpan @in, [Description("Reminder message")] string message) { @@ -57,12 +59,21 @@ public class RemindCommandGroup : CommandGroup { if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); + var data = await _dataService.GetData(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await AddReminderAsync(@in, message, data, channelId.Value, user, CancellationToken); + } + + private async Task AddReminderAsync( + TimeSpan @in, string message, GuildData data, + Snowflake channelId, IUser user, CancellationToken ct = default) { var remindAt = DateTimeOffset.UtcNow.Add(@in); - (await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add( + data.GetMemberData(user.ID).Reminders.Add( new Reminder { At = remindAt, - Channel = channelId.Value.Value, + Channel = channelId.Value, Text = message }); @@ -71,6 +82,6 @@ public class RemindCommandGroup : CommandGroup { .WithColour(ColorsList.Green) .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index d2d28be..741f10e 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Text; +using System.Text.Json.Nodes; using Boyfriend.Data; using Boyfriend.Data.Options; using Boyfriend.Services; @@ -66,7 +67,7 @@ public class SettingsCommandGroup : CommandGroup { [RequireDiscordPermission(DiscordPermission.ManageGuild)] [Description("Shows settings list for this server")] [UsedImplicitly] - public async Task ListSettingsAsync() { + public async Task ExecuteSettingsListAsync() { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -78,6 +79,10 @@ public class SettingsCommandGroup : CommandGroup { var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); + return await SendSettingsListAsync(cfg, currentUser, CancellationToken); + } + + private async Task SendSettingsListAsync(JsonNode cfg, IUser currentUser, CancellationToken ct = default) { var builder = new StringBuilder(); foreach (var option in AllOptions) { @@ -91,7 +96,7 @@ public class SettingsCommandGroup : CommandGroup { .WithColour(ColorsList.Default) .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } /// @@ -107,7 +112,7 @@ public class SettingsCommandGroup : CommandGroup { [RequireDiscordPermission(DiscordPermission.ManageGuild)] [Description("Change settings for this server")] [UsedImplicitly] - public async Task EditSettingsAsync( + public async Task ExecuteSettingsAsync( [Description("The setting whose value you want to change")] string setting, [Description("Setting value")] string value) { @@ -119,33 +124,38 @@ public class SettingsCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(cfg); + var data = await _dataService.GetData(guildId.Value, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + return await EditSettingAsync(setting, value, data, currentUser, CancellationToken); + } + + private async Task EditSettingAsync( + string setting, string value, GuildData data, IUser currentUser, CancellationToken ct = default) { var option = AllOptions.Single( o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase)); - var setResult = option.Set(cfg, value); + var setResult = option.Set(data.Settings, value); if (!setResult.IsSuccess) { var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser) .WithDescription(setResult.Error.Message) .WithColour(ColorsList.Red) .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); } var builder = new StringBuilder(); builder.Append(Markdown.InlineCode(option.Name)) .Append($" {Messages.SettingIsNow} ") - .Append(option.Display(cfg)); + .Append(option.Display(data.Settings)); var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser) .WithDescription(builder.ToString()) .WithColour(ColorsList.Green) .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/InteractionResponders.cs b/src/InteractionResponders.cs index 6f40d2a..00ef0b2 100644 --- a/src/InteractionResponders.cs +++ b/src/InteractionResponders.cs @@ -31,6 +31,6 @@ public class InteractionResponders : InteractionGroup { var idArray = state.Split(':'); return (Result)await _feedbackService.SendContextualAsync( $"https://discord.com/events/{idArray[0]}/{idArray[1]}", - options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral), ct: CancellationToken); } } diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs index b119d90..866fcb8 100644 --- a/src/Services/GuildUpdateService.cs +++ b/src/Services/GuildUpdateService.cs @@ -116,106 +116,131 @@ public class GuildUpdateService : BackgroundService { private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) { var data = await _dataService.GetData(guildId, ct); Messages.Culture = GuildSettings.Language.Get(data.Settings); + var defaultRole = GuildSettings.DefaultRole.Get(data.Settings); - foreach (var memberData in data.MemberData.Values) { - var userId = memberData.Id.ToSnowflake(); + var userResult = await _userApi.GetUserAsync(memberData.Id.ToSnowflake(), ct); + if (!userResult.IsDefined(out var user)) return; - if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value)) - _ = _guildApi.AddGuildMemberRoleAsync( - guildId, userId, defaultRole, ct: ct); - - if (DateTimeOffset.UtcNow > memberData.BannedUntil) { - var unbanResult = await _guildApi.RemoveGuildBanAsync( - guildId, userId, Messages.PunishmentExpired.EncodeHeader(), ct); - if (unbanResult.IsSuccess) - memberData.BannedUntil = null; - else - _logger.LogWarning( - "Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message); - } - - var userResult = await _userApi.GetUserAsync(userId, ct); - if (!userResult.IsDefined(out var user)) continue; - - for (var i = memberData.Reminders.Count - 1; i >= 0; i--) { - var reminder = memberData.Reminders[i]; - if (DateTimeOffset.UtcNow < reminder.At) continue; - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.Reminder, user.GetTag()), user) - .WithDescription( - string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) - .WithColour(ColorsList.Magenta) - .Build(); - - if (!embed.IsDefined(out var built)) continue; - - var messageResult = await _channelApi.CreateMessageAsync( - reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); - if (!messageResult.IsSuccess) - _logger.LogWarning( - "Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message); - - memberData.Reminders.Remove(reminder); - } + await TickMemberAsync(guildId, user, memberData, defaultRole, ct); } var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); - if (!eventsResult.IsDefined(out var events)) return; - - if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) return; + if (!eventsResult.IsSuccess) + _logger.LogWarning("Error retrieving scheduled events.\n{ErrorMessage}", eventsResult.Error.Message); + else if (!GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + await TickScheduledEventsAsync(guildId, data, eventsResult.Entity, ct); + } + private async Task TickScheduledEventsAsync( + Snowflake guildId, GuildData data, IEnumerable events, CancellationToken ct) { foreach (var scheduledEvent in events) { - if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) { + if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status)); - } else { - var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; - if (storedEvent.Status == scheduledEvent.Status) { - if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) { - if (GuildSettings.AutoStartEvents.Get(data.Settings) - && scheduledEvent.Status is not GuildScheduledEventStatus.Active) { - var startResult = await _eventApi.ModifyGuildScheduledEventAsync( - guildId, scheduledEvent.ID, - status: GuildScheduledEventStatus.Active, ct: ct); - if (!startResult.IsSuccess) - _logger.LogWarning( - "Error in automatic scheduled event start request.\n{ErrorMessage}", - startResult.Error.Message); - } - } else if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) != TimeSpan.Zero - && !storedEvent.EarlyNotificationSent - && DateTimeOffset.UtcNow - >= scheduledEvent.ScheduledStartTime - - GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) { - var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct); - if (earlyResult.IsSuccess) - storedEvent.EarlyNotificationSent = true; - else - _logger.LogWarning( - "Error in scheduled event early notification sender.\n{ErrorMessage}", - earlyResult.Error.Message); - } - continue; - } - - storedEvent.Status = scheduledEvent.Status; + var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; + if (storedEvent.Status == scheduledEvent.Status) { + await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct); + continue; } - var result = scheduledEvent.Status switch { + storedEvent.Status = scheduledEvent.Status; + + var statusChangedResponseResult = storedEvent.Status switch { GuildScheduledEventStatus.Scheduled => await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => - await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct), + await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct), _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))) }; - if (!result.IsSuccess) - _logger.LogWarning("Error in guild update.\n{ErrorMessage}", result.Error.Message); + if (!statusChangedResponseResult.IsSuccess) + _logger.LogWarning( + "Error handling scheduled event status update.\n{ErrorMessage}", + statusChangedResponseResult.Error.Message); } } + private async Task TickScheduledEventAsync( + Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, + CancellationToken ct) { + if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) { + await TryAutoStartEventAsync(guildId, data, scheduledEvent, ct); + return; + } + + if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) == TimeSpan.Zero + || eventData.EarlyNotificationSent + || DateTimeOffset.UtcNow + < scheduledEvent.ScheduledStartTime + - GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) return; + + var earlyResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct); + if (earlyResult.IsSuccess) { + eventData.EarlyNotificationSent = true; + return; + } + + _logger.LogWarning( + "Error in scheduled event early notification sender.\n{ErrorMessage}", + earlyResult.Error.Message); + } + + private async Task TryAutoStartEventAsync( + Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, CancellationToken ct) { + if (GuildSettings.AutoStartEvents.Get(data.Settings) + && scheduledEvent.Status is not GuildScheduledEventStatus.Active) { + var startResult = await _eventApi.ModifyGuildScheduledEventAsync( + guildId, scheduledEvent.ID, + status: GuildScheduledEventStatus.Active, ct: ct); + if (!startResult.IsSuccess) + _logger.LogWarning( + "Error in automatic scheduled event start request.\n{ErrorMessage}", + startResult.Error.Message); + } + } + + private async Task TickMemberAsync( + Snowflake guildId, IUser user, MemberData memberData, Snowflake defaultRole, CancellationToken ct) { + if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value)) + _ = _guildApi.AddGuildMemberRoleAsync( + guildId, user.ID, defaultRole, ct: ct); + + if (DateTimeOffset.UtcNow > memberData.BannedUntil) { + var unbanResult = await _guildApi.RemoveGuildBanAsync( + guildId, user.ID, Messages.PunishmentExpired.EncodeHeader(), ct); + if (unbanResult.IsSuccess) + memberData.BannedUntil = null; + else + _logger.LogWarning( + "Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message); + } + + for (var i = memberData.Reminders.Count - 1; i >= 0; i--) + await TickReminderAsync(memberData.Reminders[i], user, memberData, ct); + } + + private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData memberData, CancellationToken ct) { + if (DateTimeOffset.UtcNow < reminder.At) return; + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.Reminder, user.GetTag()), user) + .WithDescription( + string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) + .WithColour(ColorsList.Magenta) + .Build(); + + if (!embed.IsDefined(out var built)) return; + + var messageResult = await _channelApi.CreateMessageAsync( + reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); + if (!messageResult.IsSuccess) + _logger.LogWarning( + "Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message); + + memberData.Reminders.Remove(reminder); + } + /// /// Handles sending a notification, mentioning the if one is /// set, @@ -228,47 +253,23 @@ public class GuildUpdateService : BackgroundService { /// A notification sending result which may or may not have succeeded. private async Task SendScheduledEventCreatedMessage( IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { + if (!scheduledEvent.Creator.IsDefined(out var creator)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.Creator))); - if (!scheduledEvent.CreatorID.IsDefined(out var creatorId)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID))); - var creatorResult = await _userApi.GetUserAsync(creatorId.Value, ct); - if (!creatorResult.IsDefined(out var creator)) return Result.FromError(creatorResult); - - string embedDescription; + Result embedDescriptionResult; var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null } ? scheduledEvent.Description.Value : string.Empty; - switch (scheduledEvent.EntityType) { - case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: - if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + embedDescriptionResult = scheduledEvent.EntityType switch { + GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => + GetLocalEventCreatedEmbedDescription(scheduledEvent, eventDescription), + GuildScheduledEventEntityType.External => GetExternalScheduledEventCreatedEmbedDescription( + scheduledEvent, eventDescription), + _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))) + }; - embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( - string.Format( - Messages.DescriptionLocalEventCreated, - Markdown.Timestamp(scheduledEvent.ScheduledStartTime), - Mention.Channel(channelId) - ))}"; - break; - case GuildScheduledEventEntityType.External: - if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); - if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); - if (!metadata.Location.IsDefined(out var location)) - return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); - - embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( - string.Format( - Messages.DescriptionExternalEventCreated, - Markdown.Timestamp(scheduledEvent.ScheduledStartTime), - Markdown.Timestamp(endTime), - Markdown.InlineCode(location) - ))}"; - break; - default: - return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))); - } + if (!embedDescriptionResult.IsDefined(out var embedDescription)) + return Result.FromError(embedDescriptionResult); var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) @@ -297,92 +298,153 @@ public class GuildUpdateService : BackgroundService { components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); } + private static Result GetExternalScheduledEventCreatedEmbedDescription( + IGuildScheduledEvent scheduledEvent, string eventDescription) { + Result embedDescription; + if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); + if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); + if (!metadata.Location.IsDefined(out var location)) + return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + + embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.DescriptionExternalEventCreated, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime), + Markdown.Timestamp(endTime), + Markdown.InlineCode(location) + ))}"; + return embedDescription; + } + + private static Result GetLocalEventCreatedEmbedDescription( + IGuildScheduledEvent scheduledEvent, string eventDescription) { + if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + + return $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.DescriptionLocalEventCreated, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime), + Mention.Channel(channelId) + ))}"; + } + /// /// Handles sending a notification, mentioning the and event subscribers, - /// when a scheduled event is about to start, has started or completed + /// when a scheduled event has started or completed /// in a guild's if one is set. /// /// The scheduled event that is about to start, has started or completed. /// The data for the guild containing the scheduled event. - /// Controls whether or not a reminder for the scheduled event should be sent instead of the event started/completed notification /// The cancellation token for this operation /// A reminder/notification sending result which may or may not have succeeded. private async Task SendScheduledEventUpdatedMessage( - IGuildScheduledEvent scheduledEvent, GuildData data, bool early, CancellationToken ct = default) { - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) { + if (scheduledEvent.Status == GuildScheduledEventStatus.Active) { + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; - var embed = new EmbedBuilder(); - string? content = null; - if (early) - embed.WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser) - .WithColour(ColorsList.Default); - else - switch (scheduledEvent.Status) { - case GuildScheduledEventStatus.Active: - data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + var embedDescriptionResult = scheduledEvent.EntityType switch { + GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => + GetLocalEventStartedEmbedDescription(scheduledEvent), + GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), + _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))) + }; - string embedDescription; - switch (scheduledEvent.EntityType) { - case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: - if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + var contentResult = await _utility.GetEventNotificationMentions( + scheduledEvent, data.Settings, ct); + if (!contentResult.IsDefined(out var content)) + return Result.FromError(contentResult); + if (!embedDescriptionResult.IsDefined(out var embedDescription)) + return Result.FromError(embedDescriptionResult); - embedDescription = string.Format( - Messages.DescriptionLocalEventStarted, - Mention.Channel(channelId) - ); - break; - case GuildScheduledEventEntityType.External: - if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); - if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); - if (!metadata.Location.IsDefined(out var location)) - return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) + .WithDescription(embedDescription) + .WithColour(ColorsList.Green) + .WithCurrentTimestamp() + .Build(); - embedDescription = string.Format( - Messages.DescriptionExternalEventStarted, - Markdown.InlineCode(location), - Markdown.Timestamp(endTime) - ); - break; - default: - return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))); - } + if (!startedEmbed.IsDefined(out var startedBuilt)) return Result.FromError(startedEmbed); - var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); - if (!contentResult.IsDefined(out content)) - return Result.FromError(contentResult); + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), + content, embeds: new[] { startedBuilt }, ct: ct); + } - embed.WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) - .WithDescription(embedDescription) - .WithColour(ColorsList.Green); - break; - case GuildScheduledEventStatus.Completed: - embed.WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) - .WithDescription( - string.Format( - Messages.EventDuration, - DateTimeOffset.UtcNow.Subtract( - data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime - ?? scheduledEvent.ScheduledStartTime).ToString())) - .WithColour(ColorsList.Black); + if (scheduledEvent.Status != GuildScheduledEventStatus.Completed) + return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))); + data.ScheduledEvents.Remove(scheduledEvent.ID.Value); - data.ScheduledEvents.Remove(scheduledEvent.ID.Value); - break; - case GuildScheduledEventStatus.Canceled: - case GuildScheduledEventStatus.Scheduled: - default: return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))); - } + var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) + .WithDescription( + string.Format( + Messages.EventDuration, + DateTimeOffset.UtcNow.Subtract( + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime + ?? scheduledEvent.ScheduledStartTime).ToString())) + .WithColour(ColorsList.Black) + .WithCurrentTimestamp() + .Build(); - var result = embed.WithCurrentTimestamp().Build(); - - if (!result.IsDefined(out var built)) return Result.FromError(result); + if (!completedEmbed.IsDefined(out var completedBuilt)) + return Result.FromError(completedEmbed); return (Result)await _channelApi.CreateMessageAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), - content ?? default(Optional), embeds: new[] { built }, ct: ct); + embeds: new[] { completedBuilt }, ct: ct); + } + + private static Result GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) { + Result embedDescription; + if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + + embedDescription = string.Format( + Messages.DescriptionLocalEventStarted, + Mention.Channel(channelId) + ); + return embedDescription; + } + + private static Result GetExternalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) { + Result embedDescription; + if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); + if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); + if (!metadata.Location.IsDefined(out var location)) + return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + + embedDescription = string.Format( + Messages.DescriptionExternalEventStarted, + Markdown.InlineCode(location), + Markdown.Timestamp(endTime) + ); + return embedDescription; + } + + private async Task SendEarlyEventNotificationAsync( + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) { + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + var contentResult = await _utility.GetEventNotificationMentions( + scheduledEvent, data.Settings, ct); + if (!contentResult.IsDefined(out var content)) + return Result.FromError(contentResult); + + var earlyResult = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser) + .WithColour(ColorsList.Default) + .WithCurrentTimestamp() + .Build(); + + if (!earlyResult.IsDefined(out var earlyBuilt)) return Result.FromError(earlyResult); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), + content, + embeds: new[] { earlyBuilt }, ct: ct); } } From e31a9f73fa4b3add2b5cac9073904208ceaa65f1 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 24 Jul 2023 01:06:00 +0500 Subject: [PATCH 110/329] Remove non-numbers from SnowflakeOption inputs (#63) This PR allows passing mentions as an input to SnowflakeOption by removing non-number characters from the input string. Signed-off-by: Octol1ttle --- src/Data/Options/SnowflakeOption.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs index f65065c..05921ff 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -1,11 +1,12 @@ using System.Text.Json.Nodes; +using System.Text.RegularExpressions; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Data.Options; -public class SnowflakeOption : Option { +public partial class SnowflakeOption : Option { public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { } public override string Display(JsonNode settings) { @@ -18,10 +19,13 @@ public class SnowflakeOption : Option { } public override Result Set(JsonNode settings, string from) { - if (!ulong.TryParse(from, out var parsed)) + if (!ulong.TryParse(NonNumbers().Replace(from, ""), out var parsed)) return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue)); settings[Name] = parsed; return Result.FromSuccess(); } + + [GeneratedRegex("[^0-9]")] + private static partial Regex NonNumbers(); } From b4748a18c5744106a7d9ba8d7f5dc6fa74f9de0a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 24 Jul 2023 01:07:36 +0500 Subject: [PATCH 111/329] Add Color and Public parameters to LogActionAsync (#64) This PR adds the `color` and `isPublic` parameters to `UtilityService#LogActionAsync`. Previously, all embeds would be sent in both public and private feedback channels with the green embed color. It is now possible to change these fields, allowing for usage of the red color in punishment commands and allowing to hide the result of `/clear` from public eyes. Signed-off-by: Octol1ttle --- src/Commands/BanCommandGroup.cs | 5 +++-- src/Commands/ClearCommandGroup.cs | 2 +- src/Commands/KickCommandGroup.cs | 2 +- src/Commands/MuteCommandGroup.cs | 4 ++-- src/Services/UtilityService.cs | 13 ++++++++----- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index a8672a2..ff48156 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -153,7 +153,7 @@ public class BanCommandGroup : CommandGroup { .WithColour(ColorsList.Green).Build(); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ct); + data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); @@ -227,7 +227,8 @@ public class BanCommandGroup : CommandGroup { var title = string.Format(Messages.UserUnbanned, target.GetTag()); var description = string.Format(Messages.DescriptionActionReason, reason); - var logResult = _utility.LogActionAsync(data.Settings, channelId, user, title, description, target, ct); + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 46ddc24..0d35814 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -103,7 +103,7 @@ public class ClearCommandGroup : CommandGroup { return Result.FromError(deleteResult.Error); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, currentUser, ct); + data.Settings, channelId, user, title, description, currentUser, ColorsList.Red, false, ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 9111b7f..09dd833 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -133,7 +133,7 @@ public class KickCommandGroup : CommandGroup { var title = string.Format(Messages.UserKicked, target.GetTag()); var description = string.Format(Messages.DescriptionActionReason, reason); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ct); + data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 14895d5..be31ee8 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -125,7 +125,7 @@ public class MuteCommandGroup : CommandGroup { Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ct); + data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); @@ -215,7 +215,7 @@ public class MuteCommandGroup : CommandGroup { var title = string.Format(Messages.UserUnmuted, target.GetTag()); var description = string.Format(Messages.DescriptionActionReason, reason); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ct); + data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 18808eb..81acd85 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -1,3 +1,4 @@ +using System.Drawing; using System.Text; using System.Text.Json.Nodes; using Boyfriend.Data; @@ -158,11 +159,13 @@ public class UtilityService : IHostedService { /// The title for the embed. /// The description of the embed. /// The user whose avatar will be displayed next to the of the embed. + /// The color of the embed. + /// Whether or not the embed should be sent in /// The cancellation token for this operation. - /// + /// A result which has succeeded. public Result LogActionAsync( - JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar, - CancellationToken ct = default) { + JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar, + Color color, bool isPublic = true, CancellationToken ct = default) { var publicChannel = GuildSettings.PublicFeedbackChannel.Get(cfg); var privateChannel = GuildSettings.PrivateFeedbackChannel.Get(cfg); if (GuildSettings.PublicFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId) @@ -173,7 +176,7 @@ public class UtilityService : IHostedService { .WithDescription(description) .WithActionFooter(user) .WithCurrentTimestamp() - .WithColour(ColorsList.Green) + .WithColour(color) .Build(); if (!logEmbed.IsDefined(out var logBuilt)) @@ -182,7 +185,7 @@ public class UtilityService : IHostedService { var builtArray = new[] { logBuilt }; // Not awaiting to reduce response time - if (publicChannel != channelId.Value) + if (isPublic && publicChannel != channelId.Value) _ = _channelApi.CreateMessageAsync( publicChannel, embeds: builtArray, ct: ct); From 40527dad06f3e67614fd3ff07d9623604b820346 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:43:38 +0500 Subject: [PATCH 112/329] Bump Remora.Discord.Hosting from 6.0.6 to 6.0.7 (#66) Bumps Remora.Discord.Hosting from 6.0.6 to 6.0.7. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Boyfriend.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Boyfriend.csproj b/Boyfriend.csproj index a40b9a8..080edfb 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -25,7 +25,7 @@ - + From f752ebf1012bd096e67e2ba5700ef93bee476a49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:46:17 +0500 Subject: [PATCH 113/329] Bump Remora.Discord.Caching from 36.0.0 to 37.0.0 (#69) Bumps Remora.Discord.Caching from 36.0.0 to 37.0.0. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Boyfriend.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 080edfb..44fab7b 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -23,7 +23,7 @@ - + From abce09f26d1444146095ad26e68fba5529dd947a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:57:41 +0500 Subject: [PATCH 114/329] Bump Remora.Discord.Interactivity from 4.5.0 to 4.5.1 (#68) Bumps Remora.Discord.Interactivity from 4.5.0 to 4.5.1. Signed-off-by: dependabot[bot] Signed-off-by: Octol1ttle Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Octol1ttle --- Boyfriend.csproj | 2 +- src/Commands/AboutCommandGroup.cs | 2 +- src/Commands/BanCommandGroup.cs | 12 ++++++------ src/Commands/ClearCommandGroup.cs | 8 ++++---- src/Commands/KickCommandGroup.cs | 10 +++++----- src/Commands/MuteCommandGroup.cs | 16 ++++++++-------- src/Commands/PingCommandGroup.cs | 4 ++-- src/Commands/RemindCommandGroup.cs | 6 +++--- src/Commands/SettingsCommandGroup.cs | 4 ++-- src/Extensions.cs | 10 ++++------ src/Services/UtilityService.cs | 4 ++-- 11 files changed, 38 insertions(+), 40 deletions(-) diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 44fab7b..12cf9af 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 50e7de7..b479764 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -57,7 +57,7 @@ public class AboutCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + var cfg = await _dataService.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); return await SendAboutBotAsync(currentUser, CancellationToken); diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index ff48156..ad5ad5d 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -77,10 +77,10 @@ public class BanCommandGroup : CommandGroup { var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); - var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken); + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); @@ -88,7 +88,7 @@ public class BanCommandGroup : CommandGroup { Messages.Culture = GuildSettings.Language.Get(data.Settings); return await BanUserAsync( - target, reason, duration, guild, data, channelId.Value, user, currentUser, CancellationToken); + target, reason, duration, guild, data, channelId, user, currentUser, CancellationToken); } private async Task BanUserAsync( @@ -193,15 +193,15 @@ public class BanCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); // Needed to get the tag and avatar - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); + var data = await _dataService.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await UnbanUserAsync( - target, reason, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken); + target, reason, guildId, data, channelId, user, currentUser, CancellationToken); } private async Task UnbanUserAsync( diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 0d35814..9fae93c 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -65,10 +65,10 @@ public class ClearCommandGroup : CommandGroup { new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); var messagesResult = await _channelApi.GetChannelMessagesAsync( - channelId.Value, limit: amount + 1, ct: CancellationToken); + channelId, limit: amount + 1, ct: CancellationToken); if (!messagesResult.IsDefined(out var messages)) return Result.FromError(messagesResult); - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); // The current user's avatar is used when sending messages @@ -76,10 +76,10 @@ public class ClearCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); + var data = await _dataService.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ClearMessagesAsync(amount, data, channelId.Value, messages, user, currentUser, CancellationToken); + return await ClearMessagesAsync(amount, data, channelId, messages, user, currentUser, CancellationToken); } private async Task ClearMessagesAsync( diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 09dd833..62bbbad 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -72,17 +72,17 @@ public class KickCommandGroup : CommandGroup { var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); - var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken); + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); + var data = await _dataService.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); + var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); if (!memberResult.IsSuccess) { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); @@ -90,7 +90,7 @@ public class KickCommandGroup : CommandGroup { return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } - return await KickUserAsync(target, reason, guild, channelId.Value, data, user, currentUser, CancellationToken); + return await KickUserAsync(target, reason, guild, channelId, data, user, currentUser, CancellationToken); } private async Task KickUserAsync( diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index be31ee8..697e24e 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -76,14 +76,14 @@ public class MuteCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); + var data = await _dataService.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); + var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); if (!memberResult.IsSuccess) { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); @@ -92,7 +92,7 @@ public class MuteCommandGroup : CommandGroup { } return await MuteUserAsync( - target, reason, duration, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken); + target, reason, duration, guildId, data, channelId, user, currentUser, CancellationToken); } private async Task MuteUserAsync( @@ -171,14 +171,14 @@ public class MuteCommandGroup : CommandGroup { return Result.FromError(currentUserResult); // Needed to get the tag and avatar - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); + var data = await _dataService.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); + var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); if (!memberResult.IsSuccess) { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); @@ -187,7 +187,7 @@ public class MuteCommandGroup : CommandGroup { } return await UnmuteUserAsync( - target, reason, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken); + target, reason, guildId, data, channelId, user, currentUser, CancellationToken); } private async Task UnmuteUserAsync( diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 83b04c9..08e0c58 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -60,10 +60,10 @@ public class PingCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + var cfg = await _dataService.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); - return await SendLatencyAsync(channelId.Value, currentUser, CancellationToken); + return await SendLatencyAsync(channelId, currentUser, CancellationToken); } private async Task SendLatencyAsync( diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 90016f6..2d892bc 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -55,14 +55,14 @@ public class RemindCommandGroup : CommandGroup { return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); + var data = await _dataService.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await AddReminderAsync(@in, message, data, channelId.Value, user, CancellationToken); + return await AddReminderAsync(@in, message, data, channelId, user, CancellationToken); } private async Task AddReminderAsync( diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 741f10e..37dca6d 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -76,7 +76,7 @@ public class SettingsCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + var cfg = await _dataService.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); return await SendSettingsListAsync(cfg, currentUser, CancellationToken); @@ -124,7 +124,7 @@ public class SettingsCommandGroup : CommandGroup { if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - var data = await _dataService.GetData(guildId.Value, CancellationToken); + var data = await _dataService.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await EditSettingAsync(setting, value, data, currentUser, CancellationToken); diff --git a/src/Extensions.cs b/src/Extensions.cs index a9e0d48..2fa342c 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Net; using System.Text; using DiffPlex.DiffBuilder.Model; @@ -182,11 +181,10 @@ public static class Extensions { } public static bool TryGetContextIDs( - this ICommandContext context, [NotNullWhen(true)] out Snowflake? guildId, - [NotNullWhen(true)] out Snowflake? channelId, [NotNullWhen(true)] out Snowflake? userId) { - guildId = null; - channelId = null; - userId = null; + this ICommandContext context, out Snowflake guildId, + out Snowflake channelId, out Snowflake userId) { + channelId = default; + userId = default; return context.TryGetGuildID(out guildId) && context.TryGetChannelID(out channelId) && context.TryGetUserID(out userId); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 81acd85..6ddc204 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -185,12 +185,12 @@ public class UtilityService : IHostedService { var builtArray = new[] { logBuilt }; // Not awaiting to reduce response time - if (isPublic && publicChannel != channelId.Value) + if (isPublic && publicChannel != channelId) _ = _channelApi.CreateMessageAsync( publicChannel, embeds: builtArray, ct: ct); if (privateChannel != publicChannel - && privateChannel != channelId.Value) + && privateChannel != channelId) _ = _channelApi.CreateMessageAsync( privateChannel, embeds: builtArray, ct: ct); From fbe772406d587b24d0f2642fd2e27ecbfec3d1a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 17:00:24 +0500 Subject: [PATCH 115/329] Bump Remora.Discord.Extensions from 5.3.1 to 5.3.2 (#67) Bumps Remora.Discord.Extensions from 5.3.1 to 5.3.2. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Boyfriend.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 12cf9af..8c4cd81 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -24,7 +24,7 @@ - + From 7b722a45cbaa51d64fd8a693b33fa351f3be9603 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:51:05 +0300 Subject: [PATCH 116/329] Rename users who attempt to hoist themselves (#53) Signed-off-by: Octol1ttle Signed-off-by: mctaylors <95250141+mctaylors@users.noreply.github.com> Co-authored-by: nrdk Co-authored-by: Octol1ttle --- locale/Messages.resx | 3 ++ locale/Messages.ru.resx | 3 ++ locale/Messages.tt-ru.resx | 3 ++ src/Commands/SettingsCommandGroup.cs | 1 + src/Data/GuildSettings.cs | 5 +++ src/Services/GuildUpdateService.cs | 51 +++++++++++++++++++++++++--- 6 files changed, 61 insertions(+), 5 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index c2988e0..b701495 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -558,4 +558,7 @@ is now + + Rename members who attempt to hoist themselves + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 2173cb0..22bf3a9 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -558,4 +558,7 @@ теперь имеет значение + + Переименовывать участников, которые пытаются поднять себя + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 6e5666c..dca7f71 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -558,4 +558,7 @@ стало + + переобувать шизоидов пытающихся поднять себя в табе + diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 37dca6d..c4ea21b 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -31,6 +31,7 @@ public class SettingsCommandGroup : CommandGroup { GuildSettings.RemoveRolesOnMute, GuildSettings.ReturnRolesOnRejoin, GuildSettings.AutoStartEvents, + GuildSettings.RenameHoistedUsers, GuildSettings.PublicFeedbackChannel, GuildSettings.PrivateFeedbackChannel, GuildSettings.EventNotificationChannel, diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index 07e43a6..bd5757d 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -40,6 +40,11 @@ public static class GuildSettings { public static readonly BoolOption AutoStartEvents = new("AutoStartEvents", false); + /// + /// Controls whether or not users who try to hoist themselves should be renamed. + /// + public static readonly BoolOption RenameHoistedUsers = new("RenameHoistedUsers", false); + /// /// Controls what channel should all public messages be sent to. /// diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs index 866fcb8..c743b8e 100644 --- a/src/Services/GuildUpdateService.cs +++ b/src/Services/GuildUpdateService.cs @@ -1,4 +1,5 @@ using System.Text.Json.Nodes; +using System.Text.RegularExpressions; using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,7 +20,7 @@ namespace Boyfriend.Services; /// /// Handles executing guild updates (also called "ticks") once per second. /// -public class GuildUpdateService : BackgroundService { +public partial class GuildUpdateService : BackgroundService { private static readonly (string Name, TimeSpan Duration)[] SongList = { ("UNDEAD CORPORATION - The Empress", new TimeSpan(0, 4, 34)), ("UNDEAD CORPORATION - Everything will freeze", new TimeSpan(0, 3, 17)), @@ -30,6 +31,16 @@ public class GuildUpdateService : BackgroundService { ("Camellia - Flamewall", new TimeSpan(0, 6, 50)) }; + private static readonly string[] GenericNicknames = { + "Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary", + "Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck", + "Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane", + "Iceberg", "Iguana", "Kiwi", "Kite", "Lamb", "Lily", "Macaw", "Manatee", "Maple", "Mask", + "Nautilus", "Ostrich", "Octopus", "Pelican", "Puffin", "Pyramid", "Rattle", "Robin", "Rose", + "Salmon", "Seal", "Shark", "Sheep", "Snake", "Sonar", "Stump", "Sparrow", "Toaster", "Toucan", + "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag" + }; + private readonly List _activityList = new(1) { new Activity("with Remora.Discord", ActivityType.Game) }; private readonly IDiscordRestChannelAPI _channelApi; @@ -119,10 +130,11 @@ public class GuildUpdateService : BackgroundService { var defaultRole = GuildSettings.DefaultRole.Get(data.Settings); foreach (var memberData in data.MemberData.Values) { - var userResult = await _userApi.GetUserAsync(memberData.Id.ToSnowflake(), ct); - if (!userResult.IsDefined(out var user)) return; + var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, memberData.Id.ToSnowflake(), ct); + if (!guildMemberResult.IsDefined(out var guildMember)) return; + if (!guildMember.User.IsDefined(out var user)) return; - await TickMemberAsync(guildId, user, memberData, defaultRole, ct); + await TickMemberAsync(guildId, user, guildMember, memberData, defaultRole, data.Settings, ct); } var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); @@ -201,7 +213,8 @@ public class GuildUpdateService : BackgroundService { } private async Task TickMemberAsync( - Snowflake guildId, IUser user, MemberData memberData, Snowflake defaultRole, CancellationToken ct) { + Snowflake guildId, IUser user, IGuildMember member, MemberData memberData, Snowflake defaultRole, + JsonNode cfg, CancellationToken ct) { if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value)) _ = _guildApi.AddGuildMemberRoleAsync( guildId, user.ID, defaultRole, ct: ct); @@ -218,8 +231,36 @@ public class GuildUpdateService : BackgroundService { for (var i = memberData.Reminders.Count - 1; i >= 0; i--) await TickReminderAsync(memberData.Reminders[i], user, memberData, ct); + if (GuildSettings.RenameHoistedUsers.Get(cfg)) await FilterNicknameAsync(guildId, user, member, ct); } + private Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, CancellationToken ct) { + var currentNickname = member.Nickname.IsDefined(out var nickname) + ? nickname + : user.GlobalName ?? user.Username; + var characterList = currentNickname.ToList(); + var usernameChanged = false; + foreach (var character in currentNickname) + if (IllegalCharsRegex().IsMatch(character.ToString())) { + characterList.Remove(character); + usernameChanged = true; + } else { break; } + + if (!usernameChanged) return Task.CompletedTask; + var newNickname = string.Concat(characterList.ToArray()); + + _ = _guildApi.ModifyGuildMemberAsync( + guildId, user.ID, + !string.IsNullOrWhiteSpace(newNickname) + ? newNickname + : GenericNicknames[Random.Shared.Next(GenericNicknames.Length)], + ct: ct); + return Task.CompletedTask; + } + + [GeneratedRegex("[^0-9A-zЁА-яё]")] + private static partial Regex IllegalCharsRegex(); + private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData memberData, CancellationToken ct) { if (DateTimeOffset.UtcNow < reminder.At) return; From 397bb83ba8b617dcae6adfcd8038ee118c2301dd Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 24 Jul 2023 22:35:45 +0500 Subject: [PATCH 117/329] Add code analysis to prohibit using bad methods and properties (#70) This PR adds the package `Microsoft.CodeAnalysis.BannedApiAnalyzers` to scan for banned code (defined by `CodeAnalysis/BannedSymbols.txt`) and provide warnings if it is used. The list of banned symbols is borrowed from osu! and other projects Signed-off-by: Octol1ttle --- Boyfriend.csproj | 4 ++++ CodeAnalysis/BannedSymbols.txt | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 CodeAnalysis/BannedSymbols.txt diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 8c4cd81..366d1c5 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -22,6 +22,7 @@ + @@ -34,4 +35,7 @@ Messages.Designer.cs + + + diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt new file mode 100644 index 0000000..f664b89 --- /dev/null +++ b/CodeAnalysis/BannedSymbols.txt @@ -0,0 +1,19 @@ +M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable or EqualityComparer.Default instead. +M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable or EqualityComparer.Default instead. +M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. +T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. +M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty. +M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. +P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. +M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever. +M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture. +M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture. +M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. +M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. +M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead. +M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead. +M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead. +M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead. +P:System.DateTime.Now;Use System.DateTime.UtcNow instead. +P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead. +P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead. From 05fd343dce7c5ae92a8e007b18cf3deb4baa4a3b Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Tue, 25 Jul 2023 17:53:11 +0300 Subject: [PATCH 118/329] Improved /settingslist + fixed /settings pt.1 (#65) This PR is mainly aimed at improving /settingslist and fixing /settings List of things to do before merging: - [x] #62 - [x] Add the pages feature to /settingslist - [x] Add bullets like these -> ![](https://github.com/TeamOctolings/Boyfriend/assets/95250141/fdf1a3b8-bb64-473d-9f57-bc6e34812811) And since the development has already been taking more than 2 days, I suggest splitting the PR into 2 parts. List of other things that will be in the future PR: - mctaylors#1 - Fix bot not answering when an invalid setting is specified in /settings - Options list for /settings --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- locale/Messages.resx | 17 +++++++++++++- locale/Messages.ru.resx | 21 ++++++++++++++--- locale/Messages.tt-ru.resx | 17 +++++++++++++- src/Commands/SettingsCommandGroup.cs | 34 +++++++++++++++++++++------- src/Data/Options/BoolOption.cs | 5 ++-- src/Messages.Designer.cs | 30 ++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 15 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index b701495..72ad974 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -168,7 +168,7 @@ Current settings: - + Language @@ -561,4 +561,19 @@ Rename members who attempt to hoist themselves + + page + + + Page not found! + + + There are total pages + + + Next + + + Previous + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 22bf3a9..1d703ed 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -165,7 +165,7 @@ Текущие настройки: - + Язык @@ -433,8 +433,8 @@ Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад - Общая роль - + Роль по умолчанию + Добавляет напоминание @@ -561,4 +561,19 @@ Переименовывать участников, которые пытаются поднять себя + + страница + + + Страница не найдена! + + + Всего страниц существует + + + Далее + + + Назад + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index dca7f71..0fd4ebc 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -168,7 +168,7 @@ настройки: - + язык @@ -561,4 +561,19 @@ переобувать шизоидов пытающихся поднять себя в табе + + это страница + + + если я был бы html, я бы сказал 404 + + + ну а если быть точнее, тут всего страниц + + + следующее + + + предыдущее + diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index c4ea21b..48d7b0d 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -68,7 +68,8 @@ public class SettingsCommandGroup : CommandGroup { [RequireDiscordPermission(DiscordPermission.ManageGuild)] [Description("Shows settings list for this server")] [UsedImplicitly] - public async Task ExecuteSettingsListAsync() { + public async Task ExecuteSettingsListAsync( + [Description("Settings list page")] int page) { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) return Result.FromError( new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); @@ -80,23 +81,40 @@ public class SettingsCommandGroup : CommandGroup { var cfg = await _dataService.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); - return await SendSettingsListAsync(cfg, currentUser, CancellationToken); + return await SendSettingsListAsync(cfg, currentUser, page, CancellationToken); } - private async Task SendSettingsListAsync(JsonNode cfg, IUser currentUser, CancellationToken ct = default) { + private async Task SendSettingsListAsync(JsonNode cfg, IUser currentUser, int page, CancellationToken ct = default) { var builder = new StringBuilder(); - - foreach (var option in AllOptions) { - builder.Append(Markdown.InlineCode(option.Name)) - .Append(": "); - builder.AppendLine(option.Display(cfg)); + var footer = new StringBuilder(); + const int optionsPerList = 7; + var totalPages = (AllOptions.Length + optionsPerList - 1)/optionsPerList; + for (var i = optionsPerList * page - optionsPerList; i <= optionsPerList * page - 1; i++) { + try { + builder.AppendLine($"Settings{AllOptions[i].Name}".Localized()) + .Append(Markdown.InlineCode(AllOptions[i].Name)) + .Append(": ") + .AppendLine(AllOptions[i].Display(cfg)) + .AppendLine(); + } catch { /* hilariously ignored */ } } + footer.Append($"{Messages.Page} {page}/{totalPages} "); + for (var i = 1; i <= totalPages; i++) footer.Append(i == page ? "●" : "○"); + var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) .WithDescription(builder.ToString()) .WithColour(ColorsList.Default) + .WithFooter(footer.ToString()) .Build(); + if (optionsPerList * page - optionsPerList >= AllOptions.Length) { + embed = new EmbedBuilder().WithSmallTitle(Messages.PageNotFound, currentUser) + .WithDescription($"{Messages.PagesAllowed}: {Markdown.Bold(totalPages.ToString())}") + .WithColour(ColorsList.Red) + .Build(); + } + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); } diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs index a8ee954..55b846e 100644 --- a/src/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -19,12 +19,13 @@ public class BoolOption : Option { } private static bool TryParseBool(string from, out bool value) { + from = from.ToLower(); value = false; switch (from) { - case "1" or "y" or "yes" or "д" or "да": + case "true" or "1" or "y" or "yes" or "д" or "да": value = true; return true; - case "0" or "n" or "no" or "н" or "не" or "нет": + case "false" or "0" or "n" or "no" or "н" or "не" or "нет" or "нъет": value = false; return true; default: diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 42a05be..1f6fbc7 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -947,5 +947,35 @@ namespace Boyfriend { return ResourceManager.GetString("SettingIsNow", resourceCulture); } } + + internal static string Page { + get { + return ResourceManager.GetString("Page", resourceCulture); + } + } + + internal static string PageNotFound { + get { + return ResourceManager.GetString("PageNotFound", resourceCulture); + } + } + + internal static string PagesAllowed { + get { + return ResourceManager.GetString("PagesAllowed", resourceCulture); + } + } + + internal static string Next { + get { + return ResourceManager.GetString("Next", resourceCulture); + } + } + + internal static string Previous { + get { + return ResourceManager.GetString("Previous", resourceCulture); + } + } } } From 8f95e4cd2c3fbc7a8937aa39c26aee2eb4fd4c7c Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Tue, 25 Jul 2023 19:20:09 +0300 Subject: [PATCH 119/329] Replace "ToLower" with "ToLowerInvariant" to fix a missed warning (#71) --- src/Data/Options/BoolOption.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs index 55b846e..e8be77a 100644 --- a/src/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -19,7 +19,7 @@ public class BoolOption : Option { } private static bool TryParseBool(string from, out bool value) { - from = from.ToLower(); + from = from.ToLowerInvariant(); value = false; switch (from) { case "true" or "1" or "y" or "yes" or "д" or "да": From a6df26af677ffb3ca0796a0160dee252ba3333ea Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 26 Jul 2023 21:07:05 +0300 Subject: [PATCH 120/329] Resolving issues on #65 (#72) Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- locale/Messages.resx | 4 +-- locale/Messages.ru.resx | 4 +-- locale/Messages.tt-ru.resx | 2 +- src/Commands/SettingsCommandGroup.cs | 54 +++++++++++++++------------- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 72ad974..974d87f 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -562,13 +562,13 @@ Rename members who attempt to hoist themselves - page + Page Page not found! - There are total pages + There are {0} total pages Next diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 1d703ed..b5eacac 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -562,13 +562,13 @@ Переименовывать участников, которые пытаются поднять себя - страница + Страница Страница не найдена! - Всего страниц существует + Всего есть {0} страниц(-ы) Далее diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 0fd4ebc..d7d7520 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -568,7 +568,7 @@ если я был бы html, я бы сказал 404 - ну а если быть точнее, тут всего страниц + ну а если быть точнее, тут всего {0} страниц(-ы) следующее diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 48d7b0d..9bfbc95 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -85,37 +85,43 @@ public class SettingsCommandGroup : CommandGroup { } private async Task SendSettingsListAsync(JsonNode cfg, IUser currentUser, int page, CancellationToken ct = default) { - var builder = new StringBuilder(); + var description = new StringBuilder(); var footer = new StringBuilder(); - const int optionsPerList = 7; - var totalPages = (AllOptions.Length + optionsPerList - 1)/optionsPerList; - for (var i = optionsPerList * page - optionsPerList; i <= optionsPerList * page - 1; i++) { - try { - builder.AppendLine($"Settings{AllOptions[i].Name}".Localized()) - .Append(Markdown.InlineCode(AllOptions[i].Name)) - .Append(": ") - .AppendLine(AllOptions[i].Display(cfg)) - .AppendLine(); - } catch { /* hilariously ignored */ } - } - footer.Append($"{Messages.Page} {page}/{totalPages} "); - for (var i = 1; i <= totalPages; i++) footer.Append(i == page ? "●" : "○"); + const int optionsPerPage = 10; - var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) - .WithDescription(builder.ToString()) - .WithColour(ColorsList.Default) - .WithFooter(footer.ToString()) - .Build(); + var totalPages = (AllOptions.Length + optionsPerPage - 1) / optionsPerPage; + var lastOptionOnPage = Math.Min(optionsPerPage * page, AllOptions.Length); + var firstOptionOnPage = optionsPerPage * page - optionsPerPage; - if (optionsPerList * page - optionsPerList >= AllOptions.Length) { - embed = new EmbedBuilder().WithSmallTitle(Messages.PageNotFound, currentUser) - .WithDescription($"{Messages.PagesAllowed}: {Markdown.Bold(totalPages.ToString())}") + if (firstOptionOnPage >= AllOptions.Length) { + var embed = new EmbedBuilder().WithSmallTitle(Messages.PageNotFound, currentUser) + .WithDescription(string.Format(Messages.PagesAllowed, Markdown.Bold(totalPages.ToString()))) .WithColour(ColorsList.Red) .Build(); - } - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + } else { + footer.Append($"{Messages.Page} {page}/{totalPages} "); + for (var i = 0; i < totalPages; i++) footer.Append(i + 1 == page ? "●" : "○"); + + for (var i = firstOptionOnPage; i < lastOptionOnPage; i++) { + var optionName = AllOptions[i].Name; + var optionValue = AllOptions[i].Display(cfg); + + description.AppendLine($"- {$"Settings{optionName}".Localized()}") + .Append($" - {Markdown.InlineCode(optionName)}: ") + .AppendLine(optionValue); + } + + var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) + .WithDescription(description.ToString()) + .WithColour(ColorsList.Default) + .WithFooter(footer.ToString()) + .Build(); + + return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + } } /// From f47ebe81c573a4b4cbd14c3c4b1561d0b0e4b90c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 28 Jul 2023 21:58:55 +0500 Subject: [PATCH 121/329] Do not wrap result errors when returning them (#73) This PR removes the wrapping of in-house created result errors with `Result.FromError` (I did not know this was possible until today, lol), which reduces code verbosity and makes it easier to read, and replaces `ArgumentNullError` with `ArgumentInvalidError` when retrieving IDs from a command context, which, in my opinion, is more correct. --------- Signed-off-by: Octol1ttle --- src/Commands/AboutCommandGroup.cs | 3 +-- src/Commands/BanCommandGroup.cs | 6 ++--- src/Commands/ClearCommandGroup.cs | 3 +-- src/Commands/KickCommandGroup.cs | 3 +-- src/Commands/MuteCommandGroup.cs | 6 ++--- src/Commands/PingCommandGroup.cs | 3 +-- src/Commands/RemindCommandGroup.cs | 3 +-- src/Commands/SettingsCommandGroup.cs | 6 ++--- src/Data/Options/BoolOption.cs | 2 +- src/Data/Options/LanguageOption.cs | 2 +- src/Data/Options/SnowflakeOption.cs | 2 +- src/Data/Options/TimeSpanOption.cs | 2 +- src/InteractionResponders.cs | 2 +- src/Responders/GuildMemberJoinedResponder.cs | 2 +- src/Responders/MessageEditedResponder.cs | 4 +-- src/Services/GuildUpdateService.cs | 26 ++++++++++---------- src/Services/UtilityService.cs | 4 +-- 17 files changed, 34 insertions(+), 45 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index b479764..a41cf11 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -50,8 +50,7 @@ public class AboutCommandGroup : CommandGroup { [UsedImplicitly] public async Task ExecuteAboutAsync() { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index ad5ad5d..465f8d1 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -71,8 +71,7 @@ public class BanCommandGroup : CommandGroup { [Description("Ban reason")] string reason, [Description("Ban duration")] TimeSpan? duration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) @@ -186,8 +185,7 @@ public class BanCommandGroup : CommandGroup { [Description("User to unban")] IUser target, [Description("Unban reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 9fae93c..0c241e3 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -61,8 +61,7 @@ public class ClearCommandGroup : CommandGroup { [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] int amount) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); var messagesResult = await _channelApi.GetChannelMessagesAsync( channelId, limit: amount + 1, ct: CancellationToken); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 62bbbad..5d7909f 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -66,8 +66,7 @@ public class KickCommandGroup : CommandGroup { [Description("Member to kick")] IUser target, [Description("Kick reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 697e24e..af967b9 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -68,8 +68,7 @@ public class MuteCommandGroup : CommandGroup { [Description("Mute reason")] string reason, [Description("Mute duration")] TimeSpan duration) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); @@ -162,8 +161,7 @@ public class MuteCommandGroup : CommandGroup { [Description("Member to unmute")] IUser target, [Description("Unmute reason")] string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 08e0c58..f301edb 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -53,8 +53,7 @@ public class PingCommandGroup : CommandGroup { [UsedImplicitly] public async Task ExecutePingAsync() { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 2d892bc..9734ab5 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -52,8 +52,7 @@ public class RemindCommandGroup : CommandGroup { TimeSpan @in, [Description("Reminder message")] string message) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 9bfbc95..5a9efad 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -71,8 +71,7 @@ public class SettingsCommandGroup : CommandGroup { public async Task ExecuteSettingsListAsync( [Description("Settings list page")] int page) { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) @@ -142,8 +141,7 @@ public class SettingsCommandGroup : CommandGroup { string setting, [Description("Setting value")] string value) { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) - return Result.FromError( - new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs index e8be77a..edf6b26 100644 --- a/src/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -12,7 +12,7 @@ public class BoolOption : Option { public override Result Set(JsonNode settings, string from) { if (!TryParseBool(from, out var value)) - return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue)); + return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); settings[Name] = value; return Result.FromSuccess(); diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs index dbe1b4f..5c50899 100644 --- a/src/Data/Options/LanguageOption.cs +++ b/src/Data/Options/LanguageOption.cs @@ -29,6 +29,6 @@ public class LanguageOption : Option { public override Result Set(JsonNode settings, string from) { return CultureInfoCache.ContainsKey(from.ToLowerInvariant()) ? base.Set(settings, from.ToLowerInvariant()) - : Result.FromError(new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported)); + : new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported); } } diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs index 05921ff..66dfa8c 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -20,7 +20,7 @@ public partial class SnowflakeOption : Option { public override Result Set(JsonNode settings, string from) { if (!ulong.TryParse(NonNumbers().Replace(from, ""), out var parsed)) - return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue)); + return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); settings[Name] = parsed; return Result.FromSuccess(); diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index 659d88c..3aa1fd5 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -16,7 +16,7 @@ public class TimeSpanOption : Option { public override Result Set(JsonNode settings, string from) { if (!ParseTimeSpan(from).IsDefined(out var span)) - return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue)); + return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); settings[Name] = span.ToString(); return Result.FromSuccess(); diff --git a/src/InteractionResponders.cs b/src/InteractionResponders.cs index 00ef0b2..977f00f 100644 --- a/src/InteractionResponders.cs +++ b/src/InteractionResponders.cs @@ -26,7 +26,7 @@ public class InteractionResponders : InteractionGroup { [Button("scheduled-event-details")] [UsedImplicitly] public async Task OnStatefulButtonClicked(string? state = null) { - if (state is null) return Result.FromError(new ArgumentNullError(nameof(state))); + if (state is null) return new ArgumentNullError(nameof(state)); var idArray = state.Split(':'); return (Result)await _feedbackService.SendContextualAsync( diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index c61e500..5357363 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -29,7 +29,7 @@ public class GuildMemberJoinedResponder : IResponder { public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) { if (!gatewayEvent.User.IsDefined(out var user)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User))); + return new ArgumentNullError(nameof(gatewayEvent.User)); var data = await _dataService.GetData(gatewayEvent.GuildID, ct); var cfg = data.Settings; if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index 0211170..e7ec215 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -45,9 +45,9 @@ public class MessageEditedResponder : IResponder { return Result.FromSuccess(); // The message wasn't actually edited if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); + return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); if (!gatewayEvent.ID.IsDefined(out var messageId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID))); + return new ArgumentNullError(nameof(gatewayEvent.ID)); var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); var messageResult = await _cacheService.TryGetValueAsync( diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs index c743b8e..c5d5659 100644 --- a/src/Services/GuildUpdateService.cs +++ b/src/Services/GuildUpdateService.cs @@ -163,7 +163,7 @@ public partial class GuildUpdateService : BackgroundService { await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct), - _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))) + _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)) }; if (!statusChangedResponseResult.IsSuccess) @@ -295,7 +295,7 @@ public partial class GuildUpdateService : BackgroundService { private async Task SendScheduledEventCreatedMessage( IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { if (!scheduledEvent.Creator.IsDefined(out var creator)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.Creator))); + return new ArgumentNullError(nameof(scheduledEvent.Creator)); Result embedDescriptionResult; var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null } @@ -306,7 +306,7 @@ public partial class GuildUpdateService : BackgroundService { GetLocalEventCreatedEmbedDescription(scheduledEvent, eventDescription), GuildScheduledEventEntityType.External => GetExternalScheduledEventCreatedEmbedDescription( scheduledEvent, eventDescription), - _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))) + _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) }; if (!embedDescriptionResult.IsDefined(out var embedDescription)) @@ -343,11 +343,11 @@ public partial class GuildUpdateService : BackgroundService { IGuildScheduledEvent scheduledEvent, string eventDescription) { Result embedDescription; if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); + return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); + return new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); if (!metadata.Location.IsDefined(out var location)) - return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + return new ArgumentNullError(nameof(metadata.Location)); embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( string.Format( @@ -362,7 +362,7 @@ public partial class GuildUpdateService : BackgroundService { private static Result GetLocalEventCreatedEmbedDescription( IGuildScheduledEvent scheduledEvent, string eventDescription) { if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + return new ArgumentNullError(nameof(scheduledEvent.ChannelID)); return $"{eventDescription}\n\n{Markdown.BlockQuote( string.Format( @@ -390,7 +390,7 @@ public partial class GuildUpdateService : BackgroundService { GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => GetLocalEventStartedEmbedDescription(scheduledEvent), GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), - _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))) + _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) }; var contentResult = await _utility.GetEventNotificationMentions( @@ -414,7 +414,7 @@ public partial class GuildUpdateService : BackgroundService { } if (scheduledEvent.Status != GuildScheduledEventStatus.Completed) - return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))); + return new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)); data.ScheduledEvents.Remove(scheduledEvent.ID.Value); var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) @@ -439,7 +439,7 @@ public partial class GuildUpdateService : BackgroundService { private static Result GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) { Result embedDescription; if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + return new ArgumentNullError(nameof(scheduledEvent.ChannelID)); embedDescription = string.Format( Messages.DescriptionLocalEventStarted, @@ -451,11 +451,11 @@ public partial class GuildUpdateService : BackgroundService { private static Result GetExternalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) { Result embedDescription; if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); + return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) - return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); + return new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); if (!metadata.Location.IsDefined(out var location)) - return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + return new ArgumentNullError(nameof(metadata.Location)); embedDescription = string.Format( Messages.DescriptionExternalEventStarted, diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 6ddc204..21aade1 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -92,9 +92,9 @@ public class UtilityService : IHostedService { string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, IGuildMember interacter) { if (!targetMember.User.IsDefined(out var targetUser)) - return Result.FromError(new ArgumentNullError(nameof(targetMember.User))); + return new ArgumentNullError(nameof(targetMember.User)); if (!interacter.User.IsDefined(out var interacterUser)) - return Result.FromError(new ArgumentNullError(nameof(interacter.User))); + return new ArgumentNullError(nameof(interacter.User)); if (currentMember.User == targetMember.User) return Result.FromSuccess($"UserCannot{action}Bot".Localized()); From 940f2e64a0bf065acf07add976e27a61daef730d Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Sun, 30 Jul 2023 12:29:08 +0300 Subject: [PATCH 122/329] Add soundtracks from different games (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds some soundtracks from five different games to the bot's listening activity. List of soundtracks added: - Jukio Kallio, Daniel Hagström - Fall 'n' Roll (Fall Guys) - SCATTLE - Hypertension (Super Meat Boy) - KEYGEN CHURCH - Tenebre Rosso Sangue (ULTRAKILL) - Chipzel - Swing Me Another 6 (Dicey Dungeons) - ~Floex - The Glasshouse With Butterfly (Machinarium)~ - Noisecream - Mist of Rage (My Friend Pedro) --- src/Services/GuildUpdateService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs index c5d5659..da06a99 100644 --- a/src/Services/GuildUpdateService.cs +++ b/src/Services/GuildUpdateService.cs @@ -28,7 +28,12 @@ public partial class GuildUpdateService : BackgroundService { ("Splatoon 3 - Seep and Destroy", new TimeSpan(0, 2, 42)), ("IA - A Tale of Six Trillion Years and a Night", new TimeSpan(0, 3, 40)), ("Manuel - Gas Gas Gas", new TimeSpan(0, 3, 17)), - ("Camellia - Flamewall", new TimeSpan(0, 6, 50)) + ("Camellia - Flamewall", new TimeSpan(0, 6, 50)), + ("Jukio Kallio, Daniel Hagström - Fall 'n' Roll", new TimeSpan(0, 3, 14)), + ("SCATTLE - Hypertension", new TimeSpan(0, 3, 18)), + ("KEYGEN CHURCH - Tenebre Rosso Sangue", new TimeSpan(0, 3, 53)), + ("Chipzel - Swing Me Another 6", new TimeSpan(0, 5, 32)), + ("Noisecream - Mist of Rage", new TimeSpan(0, 2, 25)) }; private static readonly string[] GenericNicknames = { From 4cb39a34b57f9b392f27e3935c697436f088d59f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 2 Aug 2023 18:25:41 +0500 Subject: [PATCH 123/329] Add logging to file (#75) This PR adds the package `Serilog.Extensions.Logging.File` to add logging to file. I decided this was necessary after the bot unexpectedly went down in a tmux session, leaving no traces behind. ![image](https://github.com/TeamOctolings/Boyfriend/assets/61277953/b6ff9e69-b370-4844-b552-db4a39933f62) --------- Signed-off-by: Octol1ttle --- Boyfriend.csproj | 1 + src/Boyfriend.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 366d1c5..6550e3e 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -28,6 +28,7 @@ + diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index d1c4f7a..322b65a 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -18,6 +18,7 @@ using Remora.Discord.Gateway.Extensions; using Remora.Discord.Hosting.Extensions; using Remora.Discord.Interactivity.Extensions; using Remora.Rest.Core; +using Serilog.Extensions.Logging; namespace Boyfriend; @@ -95,8 +96,12 @@ public class Boyfriend { } ).ConfigureLogging( c => c.AddConsole() + .AddFile("Logs/Boyfriend-{Date}.log", + outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) ); } } From 84e730838b3af9aa9a0cfbac8315acd18040e447 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 3 Aug 2023 01:51:16 +0500 Subject: [PATCH 124/329] Add a new .editorconfig and reformat code (#76) *I'll start working on features and bugfixes after this PR, I promise* very short summary: - no more braceless statements - braces are on new lines now - `sealed` on everything that can be `sealed` - no more awkwardly looking alignment of fields/parameters - no more `Service` suffix on service fields. yeah. - no more `else`s. who needs them? - code style is now enforced by CI --------- Signed-off-by: Octol1ttle --- .editorconfig | 1806 ++++++++++++++++- locale/Messages.resx | 44 +- locale/Messages.ru.resx | 44 +- locale/Messages.tt-ru.resx | 44 +- src/Boyfriend.cs | 34 +- src/ColorsList.cs | 17 +- src/Commands/AboutCommandGroup.cs | 38 +- src/Commands/BanCommandGroup.cs | 112 +- src/Commands/ClearCommandGroup.cs | 51 +- .../Events/ErrorLoggingPostExecutionEvent.cs | 14 +- .../Events/LoggingPreparationErrorEvent.cs | 14 +- src/Commands/KickCommandGroup.cs | 72 +- src/Commands/MuteCommandGroup.cs | 98 +- src/Commands/PingCommandGroup.cs | 42 +- src/Commands/RemindCommandGroup.cs | 37 +- src/Commands/SettingsCommandGroup.cs | 106 +- src/Data/GuildData.cs | 24 +- src/Data/GuildSettings.cs | 9 +- src/Data/MemberData.cs | 12 +- src/Data/Options/BoolOption.cs | 18 +- src/Data/Options/IOption.cs | 5 +- src/Data/Options/LanguageOption.cs | 15 +- src/Data/Options/Option.cs | 19 +- src/Data/Options/SnowflakeOption.cs | 18 +- src/Data/Options/TimeSpanOption.cs | 14 +- src/Data/Reminder.cs | 7 +- src/Data/ScheduledEventData.cs | 12 +- src/Extensions.cs | 93 +- src/InteractionResponders.cs | 20 +- src/Responders/GuildLoadedResponder.cs | 41 +- src/Responders/GuildMemberJoinedResponder.cs | 41 +- .../GuildMemberRolesUpdatedResponder.cs | 15 +- src/Responders/MessageDeletedResponder.cs | 62 +- src/Responders/MessageEditedResponder.cs | 58 +- src/Responders/MessageReceivedResponder.cs | 20 +- .../ScheduledEventCancelledResponder.cs | 22 +- src/Services/GuildDataService.cs | 81 +- src/Services/GuildUpdateService.cs | 273 ++- src/Services/UtilityService.cs | 88 +- 39 files changed, 2917 insertions(+), 623 deletions(-) diff --git a/.editorconfig b/.editorconfig index bb647a7..5d54b45 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,82 +1,1734 @@ [*] -charset = utf-8 -end_of_line = lf -trim_trailing_whitespace = true -insert_final_newline = true -indent_style = space -indent_size = 4 -tab_width = 4 +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 # Microsoft .NET properties -csharp_new_line_before_catch = false -csharp_new_line_before_else = false -csharp_new_line_before_finally = false -csharp_new_line_before_members_in_object_initializers = false -csharp_new_line_before_open_brace = none -csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async -csharp_style_var_elsewhere = true : suggestion -csharp_style_var_for_built_in_types = true : suggestion -csharp_style_var_when_type_is_apparent = true : suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary : none -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity : none -dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary : none -dotnet_style_predefined_type_for_locals_parameters_members = true : suggestion -dotnet_style_predefined_type_for_member_access = true : suggestion -dotnet_style_qualification_for_event = false : suggestion -dotnet_style_qualification_for_field = false : suggestion -dotnet_style_qualification_for_method = false : suggestion -dotnet_style_qualification_for_property = false : suggestion -dotnet_style_require_accessibility_modifiers = for_non_interface_members : suggestion +csharp_indent_braces = false +csharp_indent_switch_labels = true +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:warning +csharp_prefer_braces = true:warning +csharp_preserve_single_line_blocks = true +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_style_expression_bodied_accessors = false:warning +csharp_style_expression_bodied_constructors = false:warning +csharp_style_expression_bodied_methods = false:warning +csharp_style_expression_bodied_properties = false:warning +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_utf8_string_literals = true:warning +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_using_directive_placement = outside_namespace:warning +dotnet_diagnostic.bc40000.severity = warning +dotnet_diagnostic.bc400005.severity = warning +dotnet_diagnostic.bc40008.severity = warning +dotnet_diagnostic.bc40056.severity = warning +dotnet_diagnostic.bc42016.severity = warning +dotnet_diagnostic.bc42024.severity = warning +dotnet_diagnostic.bc42025.severity = warning +dotnet_diagnostic.bc42104.severity = warning +dotnet_diagnostic.bc42105.severity = warning +dotnet_diagnostic.bc42106.severity = warning +dotnet_diagnostic.bc42107.severity = warning +dotnet_diagnostic.bc42304.severity = warning +dotnet_diagnostic.bc42309.severity = warning +dotnet_diagnostic.bc42322.severity = warning +dotnet_diagnostic.bc42349.severity = warning +dotnet_diagnostic.bc42353.severity = warning +dotnet_diagnostic.bc42354.severity = warning +dotnet_diagnostic.bc42355.severity = warning +dotnet_diagnostic.bc42356.severity = warning +dotnet_diagnostic.bc42358.severity = warning +dotnet_diagnostic.bc42504.severity = warning +dotnet_diagnostic.bc42505.severity = warning +dotnet_diagnostic.ca2252.severity = error +dotnet_diagnostic.cs0067.severity = warning +dotnet_diagnostic.cs0078.severity = warning +dotnet_diagnostic.cs0108.severity = warning +dotnet_diagnostic.cs0109.severity = warning +dotnet_diagnostic.cs0114.severity = warning +dotnet_diagnostic.cs0162.severity = warning +dotnet_diagnostic.cs0164.severity = warning +dotnet_diagnostic.cs0168.severity = warning +dotnet_diagnostic.cs0169.severity = warning +dotnet_diagnostic.cs0183.severity = warning +dotnet_diagnostic.cs0184.severity = warning +dotnet_diagnostic.cs0197.severity = warning +dotnet_diagnostic.cs0219.severity = warning +dotnet_diagnostic.cs0252.severity = warning +dotnet_diagnostic.cs0253.severity = warning +dotnet_diagnostic.cs0414.severity = warning +dotnet_diagnostic.cs0420.severity = warning +dotnet_diagnostic.cs0458.severity = warning +dotnet_diagnostic.cs0464.severity = warning +dotnet_diagnostic.cs0465.severity = warning +dotnet_diagnostic.cs0469.severity = warning +dotnet_diagnostic.cs0472.severity = warning +dotnet_diagnostic.cs0612.severity = warning +dotnet_diagnostic.cs0618.severity = warning +dotnet_diagnostic.cs0628.severity = warning +dotnet_diagnostic.cs0642.severity = warning +dotnet_diagnostic.cs0649.severity = warning +dotnet_diagnostic.cs0652.severity = warning +dotnet_diagnostic.cs0657.severity = warning +dotnet_diagnostic.cs0658.severity = warning +dotnet_diagnostic.cs0659.severity = warning +dotnet_diagnostic.cs0660.severity = warning +dotnet_diagnostic.cs0661.severity = warning +dotnet_diagnostic.cs0665.severity = warning +dotnet_diagnostic.cs0672.severity = warning +dotnet_diagnostic.cs0675.severity = warning +dotnet_diagnostic.cs0693.severity = warning +dotnet_diagnostic.cs1030.severity = warning +dotnet_diagnostic.cs1058.severity = warning +dotnet_diagnostic.cs1066.severity = warning +dotnet_diagnostic.cs1522.severity = warning +dotnet_diagnostic.cs1570.severity = warning +dotnet_diagnostic.cs1571.severity = warning +dotnet_diagnostic.cs1572.severity = warning +dotnet_diagnostic.cs1573.severity = warning +dotnet_diagnostic.cs1574.severity = warning +dotnet_diagnostic.cs1580.severity = warning +dotnet_diagnostic.cs1581.severity = warning +dotnet_diagnostic.cs1584.severity = warning +dotnet_diagnostic.cs1587.severity = warning +dotnet_diagnostic.cs1589.severity = warning +dotnet_diagnostic.cs1590.severity = warning +dotnet_diagnostic.cs1591.severity = warning +dotnet_diagnostic.cs1592.severity = warning +dotnet_diagnostic.cs1710.severity = warning +dotnet_diagnostic.cs1711.severity = warning +dotnet_diagnostic.cs1712.severity = warning +dotnet_diagnostic.cs1717.severity = warning +dotnet_diagnostic.cs1723.severity = warning +dotnet_diagnostic.cs1911.severity = warning +dotnet_diagnostic.cs1957.severity = warning +dotnet_diagnostic.cs1981.severity = warning +dotnet_diagnostic.cs1998.severity = warning +dotnet_diagnostic.cs4014.severity = warning +dotnet_diagnostic.cs7022.severity = warning +dotnet_diagnostic.cs7023.severity = warning +dotnet_diagnostic.cs7095.severity = warning +dotnet_diagnostic.cs8073.severity = warning +dotnet_diagnostic.cs8094.severity = warning +dotnet_diagnostic.cs8123.severity = warning +dotnet_diagnostic.cs8321.severity = warning +dotnet_diagnostic.cs8383.severity = warning +dotnet_diagnostic.cs8416.severity = warning +dotnet_diagnostic.cs8417.severity = warning +dotnet_diagnostic.cs8424.severity = warning +dotnet_diagnostic.cs8425.severity = warning +dotnet_diagnostic.cs8500.severity = warning +dotnet_diagnostic.cs8509.severity = warning +dotnet_diagnostic.cs8524.severity = warning +dotnet_diagnostic.cs8597.severity = warning +dotnet_diagnostic.cs8600.severity = warning +dotnet_diagnostic.cs8601.severity = warning +dotnet_diagnostic.cs8602.severity = warning +dotnet_diagnostic.cs8603.severity = warning +dotnet_diagnostic.cs8604.severity = warning +dotnet_diagnostic.cs8605.severity = warning +dotnet_diagnostic.cs8607.severity = warning +dotnet_diagnostic.cs8608.severity = warning +dotnet_diagnostic.cs8609.severity = warning +dotnet_diagnostic.cs8610.severity = warning +dotnet_diagnostic.cs8611.severity = warning +dotnet_diagnostic.cs8612.severity = warning +dotnet_diagnostic.cs8613.severity = warning +dotnet_diagnostic.cs8614.severity = warning +dotnet_diagnostic.cs8615.severity = warning +dotnet_diagnostic.cs8616.severity = warning +dotnet_diagnostic.cs8617.severity = warning +dotnet_diagnostic.cs8618.severity = warning +dotnet_diagnostic.cs8619.severity = warning +dotnet_diagnostic.cs8620.severity = warning +dotnet_diagnostic.cs8621.severity = warning +dotnet_diagnostic.cs8622.severity = warning +dotnet_diagnostic.cs8624.severity = warning +dotnet_diagnostic.cs8625.severity = warning +dotnet_diagnostic.cs8629.severity = warning +dotnet_diagnostic.cs8631.severity = warning +dotnet_diagnostic.cs8632.severity = warning +dotnet_diagnostic.cs8633.severity = warning +dotnet_diagnostic.cs8634.severity = warning +dotnet_diagnostic.cs8643.severity = warning +dotnet_diagnostic.cs8644.severity = warning +dotnet_diagnostic.cs8645.severity = warning +dotnet_diagnostic.cs8655.severity = warning +dotnet_diagnostic.cs8656.severity = warning +dotnet_diagnostic.cs8667.severity = warning +dotnet_diagnostic.cs8669.severity = warning +dotnet_diagnostic.cs8670.severity = warning +dotnet_diagnostic.cs8714.severity = warning +dotnet_diagnostic.cs8762.severity = warning +dotnet_diagnostic.cs8763.severity = warning +dotnet_diagnostic.cs8764.severity = warning +dotnet_diagnostic.cs8765.severity = warning +dotnet_diagnostic.cs8766.severity = warning +dotnet_diagnostic.cs8767.severity = warning +dotnet_diagnostic.cs8768.severity = warning +dotnet_diagnostic.cs8769.severity = warning +dotnet_diagnostic.cs8770.severity = warning +dotnet_diagnostic.cs8774.severity = warning +dotnet_diagnostic.cs8775.severity = warning +dotnet_diagnostic.cs8776.severity = warning +dotnet_diagnostic.cs8777.severity = warning +dotnet_diagnostic.cs8794.severity = warning +dotnet_diagnostic.cs8819.severity = warning +dotnet_diagnostic.cs8824.severity = warning +dotnet_diagnostic.cs8825.severity = warning +dotnet_diagnostic.cs8846.severity = warning +dotnet_diagnostic.cs8847.severity = warning +dotnet_diagnostic.cs8851.severity = warning +dotnet_diagnostic.cs8860.severity = warning +dotnet_diagnostic.cs8892.severity = warning +dotnet_diagnostic.cs8907.severity = warning +dotnet_diagnostic.cs8947.severity = warning +dotnet_diagnostic.cs8960.severity = warning +dotnet_diagnostic.cs8961.severity = warning +dotnet_diagnostic.cs8962.severity = warning +dotnet_diagnostic.cs8963.severity = warning +dotnet_diagnostic.cs8965.severity = warning +dotnet_diagnostic.cs8966.severity = warning +dotnet_diagnostic.cs8971.severity = warning +dotnet_diagnostic.cs8981.severity = warning +dotnet_diagnostic.cs9042.severity = warning +dotnet_diagnostic.cs9073.severity = warning +dotnet_diagnostic.cs9074.severity = warning +dotnet_diagnostic.cs9080.severity = warning +dotnet_diagnostic.cs9081.severity = warning +dotnet_diagnostic.cs9082.severity = warning +dotnet_diagnostic.cs9083.severity = warning +dotnet_diagnostic.cs9084.severity = warning +dotnet_diagnostic.cs9085.severity = warning +dotnet_diagnostic.cs9086.severity = warning +dotnet_diagnostic.cs9087.severity = warning +dotnet_diagnostic.cs9088.severity = warning +dotnet_diagnostic.cs9089.severity = warning +dotnet_diagnostic.cs9090.severity = warning +dotnet_diagnostic.cs9091.severity = warning +dotnet_diagnostic.cs9092.severity = warning +dotnet_diagnostic.cs9093.severity = warning +dotnet_diagnostic.cs9094.severity = warning +dotnet_diagnostic.cs9095.severity = warning +dotnet_diagnostic.cs9097.severity = warning +dotnet_diagnostic.wme006.severity = warning +dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols = constants_symbols +dotnet_naming_rule.event_rule.import_to_resharper = as_predefined +dotnet_naming_rule.event_rule.severity = warning +dotnet_naming_rule.event_rule.style = upper_camel_case_style +dotnet_naming_rule.event_rule.symbols = event_symbols +dotnet_naming_rule.interfaces_rule.import_to_resharper = as_predefined +dotnet_naming_rule.interfaces_rule.severity = warning +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols +dotnet_naming_rule.locals_rule.import_to_resharper = as_predefined +dotnet_naming_rule.locals_rule.severity = warning +dotnet_naming_rule.locals_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.locals_rule.symbols = locals_symbols +dotnet_naming_rule.local_constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.local_constants_rule.severity = warning +dotnet_naming_rule.local_constants_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols +dotnet_naming_rule.local_functions_rule.import_to_resharper = as_predefined +dotnet_naming_rule.local_functions_rule.severity = warning +dotnet_naming_rule.local_functions_rule.style = upper_camel_case_style +dotnet_naming_rule.local_functions_rule.symbols = local_functions_symbols +dotnet_naming_rule.method_rule.import_to_resharper = as_predefined +dotnet_naming_rule.method_rule.severity = warning +dotnet_naming_rule.method_rule.style = upper_camel_case_style +dotnet_naming_rule.method_rule.symbols = method_symbols +dotnet_naming_rule.parameters_rule.import_to_resharper = as_predefined +dotnet_naming_rule.parameters_rule.severity = warning +dotnet_naming_rule.parameters_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.parameters_rule.symbols = parameters_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_rule.property_rule.import_to_resharper = as_predefined +dotnet_naming_rule.property_rule.severity = warning +dotnet_naming_rule.property_rule.style = upper_camel_case_style +dotnet_naming_rule.property_rule.symbols = property_symbols +dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols +dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols +dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper = as_predefined +dotnet_naming_rule.types_and_namespaces_rule.severity = warning +dotnet_naming_rule.types_and_namespaces_rule.style = upper_camel_case_style +dotnet_naming_rule.types_and_namespaces_rule.symbols = types_and_namespaces_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper = as_predefined +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols +dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix = I +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.lower_camel_case_style.required_prefix = _ +dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case +dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix = T +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds = field +dotnet_naming_symbols.constants_symbols.required_modifiers = const +dotnet_naming_symbols.event_symbols.applicable_accessibilities = * +dotnet_naming_symbols.event_symbols.applicable_kinds = event +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = * +dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface +dotnet_naming_symbols.locals_symbols.applicable_accessibilities = * +dotnet_naming_symbols.locals_symbols.applicable_kinds = local +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = * +dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local +dotnet_naming_symbols.local_constants_symbols.required_modifiers = const +dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities = * +dotnet_naming_symbols.local_functions_symbols.applicable_kinds = local_function +dotnet_naming_symbols.method_symbols.applicable_accessibilities = * +dotnet_naming_symbols.method_symbols.applicable_kinds = method +dotnet_naming_symbols.parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.parameters_symbols.applicable_kinds = parameter +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly +dotnet_naming_symbols.property_symbols.applicable_accessibilities = * +dotnet_naming_symbols.property_symbols.applicable_kinds = property +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = * +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace, class, struct, enum, delegate +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:warning +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning # ReSharper properties -resharper_align_linq_query = true -resharper_align_multiline_argument = true -resharper_align_multiline_binary_patterns = true -resharper_align_multiline_extends_list = true -resharper_align_multiline_parameter = true -resharper_align_multiple_declaration = true -resharper_align_multline_type_parameter_constrains = true -resharper_align_multline_type_parameter_list = true -resharper_align_tuple_components = true -resharper_allow_comment_after_lbrace = true -resharper_csharp_empty_block_style = together_same_line -resharper_csharp_indent_type_constraints = false -resharper_csharp_int_align_comments = true -resharper_csharp_outdent_commas = true -resharper_csharp_stick_comment = false -resharper_csharp_wrap_after_declaration_lpar = true -resharper_csharp_wrap_after_invocation_lpar = true -resharper_csharp_wrap_before_binary_opsign = true -resharper_csharp_wrap_before_first_type_parameter_constraint = true -resharper_csharp_wrap_multiple_declaration_style = wrap_if_long -resharper_csharp_wrap_multiple_type_parameter_constraints_style = chop_always -resharper_indent_nested_fixed_stmt = true -resharper_indent_nested_foreach_stmt = true -resharper_indent_nested_for_stmt = true -resharper_indent_nested_lock_stmt = true -resharper_indent_nested_usings_stmt = true -resharper_indent_nested_while_stmt = true -resharper_indent_preprocessor_if = usual_indent -resharper_indent_preprocessor_other = usual_indent -resharper_int_align_fields = true -resharper_int_align_methods = true -resharper_int_align_parameters = true -resharper_int_align_properties = true -resharper_int_align_switch_expressions = true -resharper_int_align_switch_sections = true -resharper_keep_existing_switch_expression_arrangement = false -resharper_outdent_statement_labels = true -resharper_place_field_attribute_on_same_line = if_owner_is_single_line -resharper_place_simple_accessorholder_on_single_line = true -resharper_place_simple_accessor_on_single_line = false -resharper_place_simple_case_statement_on_same_line = true -resharper_place_simple_embedded_block_on_same_line = true -resharper_place_simple_switch_expression_on_single_line = true -resharper_space_around_arrow_op = true -resharper_wrap_before_arrow_with_expressions = true -resharper_wrap_before_eq = true -resharper_wrap_before_extends_colon = true -resharper_wrap_before_linq_expression = true -resharper_wrap_chained_binary_expressions = chop_if_long -resharper_wrap_for_stmt_header_style = wrap_if_long -resharper_wrap_switch_expression = chop_if_long +resharper_alignment_tab_fill_style = use_spaces +resharper_align_first_arg_by_paren = false +resharper_align_linq_query = false +resharper_align_multiline_array_and_object_initializer = false +resharper_align_multiline_array_initializer = true +resharper_align_multiline_binary_patterns = false +resharper_align_multiline_ctor_init = true +resharper_align_multiline_expression_braces = false +resharper_align_multiline_implements_list = true +resharper_align_multiline_list_pattern = false +resharper_align_multiline_property_pattern = false +resharper_align_multiline_statement_conditions = true +resharper_align_multiline_switch_expression = false +resharper_align_multiline_type_argument = true +resharper_align_multiline_type_parameter = true +resharper_align_multline_type_parameter_constrains = false +resharper_align_multline_type_parameter_list = false +resharper_align_ternary = align_not_nested +resharper_align_tuple_components = false +resharper_allow_alias = true +resharper_allow_comment_after_lbrace = false +resharper_allow_far_alignment = false +resharper_always_use_end_of_line_brace_style = false +resharper_apply_auto_detected_rules = true +resharper_apply_on_completion = false +resharper_arguments_anonymous_function = positional +resharper_arguments_literal = positional +resharper_arguments_named = positional +resharper_arguments_other = positional +resharper_arguments_skip_single = true +resharper_arguments_string_literal = positional +resharper_attribute_style = do_not_touch +resharper_autodetect_indent_settings = true +resharper_blank_lines_after_access_specifier = 0 +resharper_blank_lines_after_block_statements = 1 +resharper_blank_lines_after_case = 0 +resharper_blank_lines_after_control_transfer_statements = 0 +resharper_blank_lines_after_file_scoped_namespace_directive = 1 +resharper_blank_lines_after_imports = 1 +resharper_blank_lines_after_multiline_statements = 0 +resharper_blank_lines_after_options = 1 +resharper_blank_lines_after_start_comment = 1 +resharper_blank_lines_after_using_list = 1 +resharper_blank_lines_around_accessor = 0 +resharper_blank_lines_around_auto_property = 1 +resharper_blank_lines_around_block_case_section = 0 +resharper_blank_lines_around_class_definition = 1 +resharper_blank_lines_around_field = 1 +resharper_blank_lines_around_function_declaration = 0 +resharper_blank_lines_around_function_definition = 1 +resharper_blank_lines_around_global_attribute = 0 +resharper_blank_lines_around_invocable = 1 +resharper_blank_lines_around_local_method = 1 +resharper_blank_lines_around_multiline_case_section = 0 +resharper_blank_lines_around_namespace = 1 +resharper_blank_lines_around_other_declaration = 0 +resharper_blank_lines_around_property = 1 +resharper_blank_lines_around_razor_functions = 1 +resharper_blank_lines_around_razor_helpers = 1 +resharper_blank_lines_around_razor_sections = 1 +resharper_blank_lines_around_region = 1 +resharper_blank_lines_around_single_line_accessor = 0 +resharper_blank_lines_around_single_line_auto_property = 0 +resharper_blank_lines_around_single_line_field = 0 +resharper_blank_lines_around_single_line_function_definition = 0 +resharper_blank_lines_around_single_line_invocable = 0 +resharper_blank_lines_around_single_line_local_method = 0 +resharper_blank_lines_around_single_line_property = 0 +resharper_blank_lines_around_single_line_type = 1 +resharper_blank_lines_around_type = 1 +resharper_blank_lines_before_access_specifier = 1 +resharper_blank_lines_before_block_statements = 0 +resharper_blank_lines_before_case = 0 +resharper_blank_lines_before_control_transfer_statements = 0 +resharper_blank_lines_before_multiline_statements = 0 +resharper_blank_lines_before_single_line_comment = 0 +resharper_blank_lines_inside_namespace = 0 +resharper_blank_lines_inside_region = 1 +resharper_blank_lines_inside_type = 0 +resharper_blank_line_after_pi = true +resharper_braces_redundant = false +resharper_break_template_declaration = line_break +resharper_builtin_type_apply_to_native_integer = false +resharper_can_use_global_alias = true +resharper_configure_await_analysis_mode = disabled +resharper_continuous_indent_multiplier = 1 +resharper_continuous_line_indent = single +resharper_csharp_align_multiline_argument = false +resharper_csharp_align_multiline_binary_expressions_chain = true +resharper_csharp_align_multiline_calls_chain = false +resharper_csharp_align_multiline_expression = false +resharper_csharp_align_multiline_extends_list = false +resharper_csharp_align_multiline_for_stmt = false +resharper_csharp_align_multiline_parameter = false +resharper_csharp_align_multiple_declaration = false +resharper_csharp_insert_final_newline = true +resharper_csharp_keep_blank_lines_in_code = 1 +resharper_csharp_max_line_length = 120 +resharper_csharp_naming_rule.enum_member = AaBb +resharper_csharp_naming_rule.method_property_event = AaBb +resharper_csharp_naming_rule.other = AaBb +resharper_csharp_new_line_before_while = false +resharper_csharp_prefer_qualified_reference = false +resharper_csharp_space_after_unary_operator = false +resharper_csharp_wrap_before_first_type_parameter_constraint = true +resharper_csharp_wrap_lines = true +resharper_default_exception_variable_name = e +resharper_default_value_when_type_evident = default_literal +resharper_default_value_when_type_not_evident = default_literal +resharper_delete_quotes_from_solid_values = false +resharper_disable_blank_line_changes = false +resharper_disable_formatter = false +resharper_disable_indenter = false +resharper_disable_int_align = false +resharper_disable_line_break_changes = false +resharper_disable_line_break_removal = false +resharper_disable_space_changes = false +resharper_disable_space_changes_before_trailing_comment = false +resharper_dont_remove_extra_blank_lines = false +resharper_empty_block_style = multiline +resharper_enable_wrapping = false +resharper_enforce_line_ending_style = false +resharper_event_handler_pattern_long = $object$On$event$ +resharper_event_handler_pattern_short = On$event$ +resharper_export_declaration_braces = next_line +resharper_expression_braces = inside +resharper_expression_pars = inside +resharper_extra_spaces = remove_all +resharper_force_attribute_style = separate +resharper_force_chop_compound_do_expression = false +resharper_force_chop_compound_if_expression = false +resharper_force_chop_compound_while_expression = false +resharper_formatter_off_tag = @formatter:off +resharper_formatter_on_tag = @formatter:on +resharper_formatter_tags_accept_regexp = false +resharper_formatter_tags_enabled = true +resharper_format_leading_spaces_decl = false +resharper_free_block_braces = next_line +resharper_function_declaration_return_type_style = do_not_change +resharper_function_definition_return_type_style = do_not_change +resharper_generator_mode = false +resharper_ignore_space_preservation = false +resharper_include_prefix_comment_in_indent = false +resharper_indent_access_specifiers_from_class = false +resharper_indent_aligned_ternary = true +resharper_indent_anonymous_method_block = false +resharper_indent_braces_inside_statement_conditions = true +resharper_indent_case_from_select = true +resharper_indent_child_elements = OneIndent +resharper_indent_class_members_from_access_specifiers = false +resharper_indent_comment = true +resharper_indent_export_declaration_members = true +resharper_indent_inside_namespace = true +resharper_indent_invocation_pars = inside +resharper_indent_method_decl_pars = inside +resharper_indent_nested_fixed_stmt = false +resharper_indent_nested_foreach_stmt = false +resharper_indent_nested_for_stmt = false +resharper_indent_nested_lock_stmt = false +resharper_indent_nested_usings_stmt = false +resharper_indent_nested_while_stmt = false +resharper_indent_pars = inside +resharper_indent_preprocessor_directives = none +resharper_indent_preprocessor_if = no_indent +resharper_indent_preprocessor_other = no_indent +resharper_indent_preprocessor_region = usual_indent +resharper_indent_statement_pars = inside +resharper_indent_text = OneIndent +resharper_indent_typearg_angles = inside +resharper_indent_typeparam_angles = inside +resharper_indent_type_constraints = true +resharper_indent_wrapped_function_names = false +resharper_instance_members_qualify_declared_in = this_class, base_class +resharper_int_align = false +resharper_int_align_comments = false +resharper_int_align_declaration_names = false +resharper_int_align_enum_initializers = false +resharper_int_align_eq = false +resharper_int_align_fix_in_adjacent = true +resharper_keep_blank_lines_in_declarations = 2 +resharper_keep_existing_arrangement = true +resharper_keep_nontrivial_alias = true +resharper_keep_user_linebreaks = true +resharper_keep_user_wrapping = true +resharper_linebreaks_around_razor_statements = true +resharper_linebreaks_inside_tags_for_elements_longer_than = 2147483647 +resharper_linebreaks_inside_tags_for_elements_with_child_elements = true +resharper_linebreaks_inside_tags_for_multiline_elements = true +resharper_linebreak_before_all_elements = false +resharper_linebreak_before_multiline_elements = true +resharper_linebreak_before_singleline_elements = false +resharper_line_break_after_colon_in_member_initializer_lists = do_not_change +resharper_line_break_after_comma_in_member_initializer_lists = false +resharper_line_break_after_init_statement = do_not_change +resharper_line_break_before_comma_in_member_initializer_lists = false +resharper_line_break_before_requires_clause = do_not_change +resharper_linkage_specification_braces = end_of_line +resharper_linkage_specification_indentation = none +resharper_local_function_body = expression_body +resharper_macro_block_begin = +resharper_macro_block_end = +resharper_max_array_initializer_elements_on_line = 10000 +resharper_max_attribute_length_for_same_line = 38 +resharper_max_enum_members_on_line = 3 +resharper_max_formal_parameters_on_line = 10000 +resharper_max_initializer_elements_on_line = 4 +resharper_max_invocation_arguments_on_line = 10000 +resharper_member_initializer_list_style = do_not_change +resharper_namespace_declaration_braces = next_line +resharper_namespace_indentation = all +resharper_nested_ternary_style = autodetect +resharper_new_line_before_catch = true +resharper_new_line_before_else = true +resharper_new_line_before_enumerators = true +resharper_normalize_tag_names = false +resharper_no_indent_inside_elements = html, body, thead, tbody, tfoot +resharper_no_indent_inside_if_element_longer_than = 200 +resharper_null_checking_pattern_style = not_null_pattern +resharper_object_creation_when_type_evident = target_typed +resharper_object_creation_when_type_not_evident = explicitly_typed +resharper_old_engine = false +resharper_outdent_binary_ops = false +resharper_outdent_binary_pattern_ops = false +resharper_outdent_commas = false +resharper_outdent_dots = false +resharper_outdent_namespace_member = false +resharper_outdent_statement_labels = false +resharper_outdent_ternary_ops = false +resharper_parentheses_non_obvious_operations = none, shift, bitwise_and, bitwise_exclusive_or, bitwise_inclusive_or, bitwise +resharper_parentheses_redundancy_style = remove_if_not_clarifies_precedence +resharper_parentheses_same_type_operations = false +resharper_pi_attributes_indent = align_by_first_attribute +resharper_place_attribute_on_same_line = false +resharper_place_comments_at_first_column = false +resharper_place_constructor_initializer_on_same_line = true +resharper_place_event_attribute_on_same_line = false +resharper_place_expr_accessor_on_single_line = true +resharper_place_expr_method_on_single_line = true +resharper_place_expr_property_on_single_line = true +resharper_place_linq_into_on_new_line = true +resharper_place_namespace_definitions_on_same_line = false +resharper_place_property_attribute_on_same_line = false +resharper_place_simple_case_statement_on_same_line = false +resharper_place_simple_embedded_statement_on_same_line = true +resharper_place_simple_initializer_on_single_line = true +resharper_place_simple_list_pattern_on_single_line = true +resharper_place_simple_property_pattern_on_single_line = true +resharper_place_simple_switch_expression_on_single_line = true +resharper_place_type_constraints_on_same_line = true +resharper_prefer_explicit_discard_declaration = false +resharper_prefer_separate_deconstructed_variables_declaration = true +resharper_preserve_spaces_inside_tags = pre, textarea +resharper_qualified_using_at_nested_scope = false +resharper_quote_style = doublequoted +resharper_razor_prefer_qualified_reference = true +resharper_remove_blank_lines_near_braces = false +resharper_remove_blank_lines_near_braces_in_code = true +resharper_remove_blank_lines_near_braces_in_declarations = true +resharper_remove_this_qualifier = true +resharper_requires_expression_braces = next_line +resharper_resx_attribute_indent = single_indent +resharper_resx_insert_final_newline = true +resharper_resx_linebreak_before_elements = +resharper_resx_max_blank_lines_between_tags = 0 +resharper_resx_max_line_length = 2147483647 +resharper_resx_pi_attribute_style = do_not_touch +resharper_resx_space_before_self_closing = false +resharper_resx_wrap_lines = false +resharper_resx_wrap_tags_and_pi = false +resharper_resx_wrap_text = false +resharper_show_autodetect_configure_formatting_tip = false +resharper_simple_block_style = do_not_change +resharper_simple_case_statement_style = do_not_change +resharper_simple_embedded_statement_style = do_not_change +resharper_sort_attributes = false +resharper_sort_class_selectors = false +resharper_sort_usings = true +resharper_sort_usings_lowercase_first = false +resharper_spaces_around_eq_in_attribute = false +resharper_spaces_around_eq_in_pi_attribute = false +resharper_spaces_inside_tags = false +resharper_space_after_attributes = true +resharper_space_after_attribute_target_colon = true +resharper_space_after_cast = false +resharper_space_after_colon = true +resharper_space_after_colon_in_case = true +resharper_space_after_colon_in_inheritance_clause = true +resharper_space_after_comma = true +resharper_space_after_ellipsis_in_parameter_pack = true +resharper_space_after_for_colon = true +resharper_space_after_keywords_in_control_flow_statements = true +resharper_space_after_last_attribute = false +resharper_space_after_last_pi_attribute = false +resharper_space_after_operator_keyword = true +resharper_space_after_operator_not = false +resharper_space_after_ptr_in_data_member = true +resharper_space_after_ptr_in_data_members = false +resharper_space_after_ptr_in_method = true +resharper_space_after_ptr_in_nested_declarator = false +resharper_space_after_ref_in_data_member = true +resharper_space_after_ref_in_data_members = false +resharper_space_after_ref_in_method = true +resharper_space_after_semicolon_in_for_statement = true +resharper_space_after_ternary_colon = true +resharper_space_after_ternary_quest = true +resharper_space_after_triple_slash = true +resharper_space_after_type_parameter_constraint_colon = true +resharper_space_around_additive_op = true +resharper_space_around_alias_eq = true +resharper_space_around_assignment_op = true +resharper_space_around_assignment_operator = true +resharper_space_around_deref_in_trailing_return_type = true +resharper_space_around_lambda_arrow = true +resharper_space_around_member_access_operator = false +resharper_space_around_relational_op = true +resharper_space_around_shift_op = true +resharper_space_around_stmt_colon = true +resharper_space_around_ternary_operator = true +resharper_space_before_array_rank_parentheses = false +resharper_space_before_attribute_target_colon = false +resharper_space_before_checked_parentheses = false +resharper_space_before_colon = false +resharper_space_before_colon_in_case = false +resharper_space_before_colon_in_inheritance_clause = true +resharper_space_before_comma = false +resharper_space_before_default_parentheses = false +resharper_space_before_ellipsis_in_parameter_pack = false +resharper_space_before_empty_invocation_parentheses = false +resharper_space_before_empty_method_parentheses = false +resharper_space_before_for_colon = true +resharper_space_before_initializer_braces = false +resharper_space_before_invocation_parentheses = false +resharper_space_before_label_colon = false +resharper_space_before_lambda_parentheses = false +resharper_space_before_method_parentheses = false +resharper_space_before_nameof_parentheses = false +resharper_space_before_new_parentheses = false +resharper_space_before_nullable_mark = false +resharper_space_before_open_square_brackets = false +resharper_space_before_pointer_asterik_declaration = false +resharper_space_before_postfix_operator = false +resharper_space_before_ptr_in_abstract_decl = false +resharper_space_before_ptr_in_data_member = false +resharper_space_before_ptr_in_data_members = true +resharper_space_before_ptr_in_method = false +resharper_space_before_ref_in_abstract_decl = false +resharper_space_before_ref_in_data_member = false +resharper_space_before_ref_in_data_members = true +resharper_space_before_ref_in_method = false +resharper_space_before_semicolon = false +resharper_space_before_semicolon_in_for_statement = false +resharper_space_before_singleline_accessorholder = true +resharper_space_before_sizeof_parentheses = false +resharper_space_before_template_args = false +resharper_space_before_template_params = true +resharper_space_before_ternary_colon = true +resharper_space_before_ternary_quest = true +resharper_space_before_trailing_comment = true +resharper_space_before_typeof_parentheses = false +resharper_space_before_type_argument_angle = false +resharper_space_before_type_parameter_angle = false +resharper_space_before_type_parameter_constraint_colon = true +resharper_space_before_type_parameter_parentheses = true +resharper_space_between_accessors_in_singleline_property = true +resharper_space_between_attribute_sections = true +resharper_space_between_closing_angle_brackets_in_template_args = false +resharper_space_between_keyword_and_expression = true +resharper_space_between_keyword_and_type = true +resharper_space_between_method_call_empty_parameter_list_parentheses = false +resharper_space_between_method_call_name_and_opening_parenthesis = false +resharper_space_between_method_call_parameter_list_parentheses = false +resharper_space_between_method_declaration_empty_parameter_list_parentheses = false +resharper_space_between_method_declaration_name_and_open_parenthesis = false +resharper_space_between_method_declaration_parameter_list_parentheses = false +resharper_space_between_parentheses_of_control_flow_statements = false +resharper_space_between_square_brackets = false +resharper_space_between_typecast_parentheses = false +resharper_space_in_singleline_accessorholder = true +resharper_space_in_singleline_anonymous_method = true +resharper_space_in_singleline_method = true +resharper_space_near_postfix_and_prefix_op = false +resharper_space_within_array_initialization_braces = false +resharper_space_within_array_rank_empty_parentheses = false +resharper_space_within_array_rank_parentheses = false +resharper_space_within_attribute_angles = false +resharper_space_within_checked_parentheses = false +resharper_space_within_declaration_parentheses = false +resharper_space_within_default_parentheses = false +resharper_space_within_empty_braces = true +resharper_space_within_empty_initializer_braces = false +resharper_space_within_empty_invocation_parentheses = false +resharper_space_within_empty_method_parentheses = false +resharper_space_within_empty_template_params = false +resharper_space_within_expression_parentheses = false +resharper_space_within_initializer_braces = false +resharper_space_within_invocation_parentheses = false +resharper_space_within_method_parentheses = false +resharper_space_within_nameof_parentheses = false +resharper_space_within_new_parentheses = false +resharper_space_within_parentheses = false +resharper_space_within_single_line_array_initializer_braces = true +resharper_space_within_sizeof_parentheses = false +resharper_space_within_slice_pattern = true +resharper_space_within_template_args = false +resharper_space_within_template_params = false +resharper_space_within_tuple_parentheses = false +resharper_space_within_typeof_parentheses = false +resharper_space_within_type_argument_angles = false +resharper_space_within_type_parameter_angles = false +resharper_space_within_type_parameter_parentheses = false +resharper_special_else_if_treatment = true +resharper_static_members_qualify_members = none +resharper_static_members_qualify_with = declared_type +resharper_stick_comment = true +resharper_support_vs_event_naming_pattern = true +resharper_toplevel_function_declaration_return_type_style = do_not_change +resharper_toplevel_function_definition_return_type_style = do_not_change +resharper_trailing_comma_in_multiline_lists = false +resharper_trailing_comma_in_singleline_lists = false +resharper_use_continuous_indent_inside_initializer_braces = true +resharper_use_continuous_indent_inside_parens = true +resharper_use_continuous_line_indent_in_expression_braces = false +resharper_use_continuous_line_indent_in_method_pars = false +resharper_use_indents_from_main_language_in_file = true +resharper_use_indent_from_previous_element = true +resharper_use_indent_from_vs = false +resharper_use_roslyn_logic_for_evident_types = false +resharper_wrap_after_binary_opsign = true +resharper_wrap_after_declaration_lpar = false +resharper_wrap_after_dot = false +resharper_wrap_after_dot_in_method_calls = false +resharper_wrap_after_expression_lbrace = true +resharper_wrap_after_invocation_lpar = false +resharper_wrap_arguments_style = wrap_if_long +resharper_wrap_around_elements = true +resharper_wrap_array_initializer_style = wrap_if_long +resharper_wrap_base_clause_style = wrap_if_long +resharper_wrap_before_arrow_with_expressions = false +resharper_wrap_before_binary_opsign = false +resharper_wrap_before_binary_pattern_op = true +resharper_wrap_before_colon = false +resharper_wrap_before_comma = false +resharper_wrap_before_comma_in_base_clause = false +resharper_wrap_before_declaration_lpar = false +resharper_wrap_before_declaration_rpar = false +resharper_wrap_before_eq = false +resharper_wrap_before_expression_rbrace = true +resharper_wrap_before_extends_colon = false +resharper_wrap_before_invocation_lpar = false +resharper_wrap_before_invocation_rpar = false +resharper_wrap_before_linq_expression = false +resharper_wrap_before_ternary_opsigns = true +resharper_wrap_before_type_parameter_langle = false +resharper_wrap_braced_init_list_style = wrap_if_long +resharper_wrap_chained_binary_expressions = wrap_if_long +resharper_wrap_chained_binary_patterns = wrap_if_long +resharper_wrap_chained_method_calls = wrap_if_long +resharper_wrap_ctor_initializer_style = wrap_if_long +resharper_wrap_enumeration_style = chop_if_long +resharper_wrap_enum_declaration = chop_always +resharper_wrap_extends_list_style = wrap_if_long +resharper_wrap_for_stmt_header_style = chop_if_long +resharper_wrap_list_pattern = wrap_if_long +resharper_wrap_multiple_declaration_style = chop_if_long +resharper_wrap_multiple_type_parameter_constraints_style = chop_if_long +resharper_wrap_object_and_collection_initializer_style = chop_always +resharper_wrap_parameters_style = wrap_if_long +resharper_wrap_property_pattern = chop_if_long +resharper_wrap_switch_expression = chop_always +resharper_wrap_ternary_expr_style = chop_if_long +resharper_wrap_verbatim_interpolated_strings = no_wrap +resharper_xmldoc_attribute_indent = single_indent +resharper_xmldoc_insert_final_newline = true +resharper_xmldoc_linebreak_before_elements = summary, remarks, example, returns, param, typeparam, value, para +resharper_xmldoc_max_blank_lines_between_tags = 0 +resharper_xmldoc_max_line_length = 120 +resharper_xmldoc_pi_attribute_style = do_not_touch +resharper_xmldoc_space_before_self_closing = true +resharper_xmldoc_wrap_lines = true +resharper_xmldoc_wrap_tags_and_pi = true +resharper_xmldoc_wrap_text = true + +# ReSharper inspection severities +resharper_access_rights_in_text_highlighting = warning +resharper_access_to_disposed_closure_highlighting = warning +resharper_access_to_for_each_variable_in_closure_highlighting = warning +resharper_access_to_modified_closure_highlighting = warning +resharper_access_to_static_member_via_derived_type_highlighting = warning +resharper_address_of_marshal_by_ref_object_highlighting = warning +resharper_angular_html_banana_highlighting = warning +resharper_annotate_can_be_null_parameter_highlighting = warning +resharper_annotate_can_be_null_type_member_highlighting = warning +resharper_annotate_not_null_parameter_highlighting = warning +resharper_annotate_not_null_type_member_highlighting = warning +resharper_annotation_conflict_in_hierarchy_highlighting = warning +resharper_annotation_redundancy_at_value_type_highlighting = warning +resharper_annotation_redundancy_in_hierarchy_highlighting = warning +resharper_anonymous_object_destructuring_problem_highlighting = warning +resharper_arguments_style_anonymous_function_highlighting = warning +resharper_arguments_style_literal_highlighting = warning +resharper_arguments_style_named_expression_highlighting = warning +resharper_arguments_style_other_highlighting = warning +resharper_arguments_style_string_literal_highlighting = warning +resharper_arrange_attributes_highlighting = warning +resharper_arrange_default_value_when_type_evident_highlighting = warning +resharper_arrange_default_value_when_type_not_evident_highlighting = warning +resharper_arrange_local_function_body_highlighting = warning +resharper_arrange_namespace_body_highlighting = warning +resharper_arrange_null_checking_pattern_highlighting = warning +resharper_arrange_object_creation_when_type_evident_highlighting = warning +resharper_arrange_object_creation_when_type_not_evident_highlighting = warning +resharper_arrange_redundant_parentheses_highlighting = warning +resharper_arrange_static_member_qualifier_highlighting = warning +resharper_arrange_this_qualifier_highlighting = warning +resharper_arrange_trailing_comma_in_multiline_lists_highlighting = warning +resharper_arrange_trailing_comma_in_singleline_lists_highlighting = warning +resharper_arrange_type_member_modifiers_highlighting = warning +resharper_arrange_type_modifiers_highlighting = warning +resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting = warning +resharper_asp_content_placeholder_not_resolved_highlighting = error +resharper_asp_custom_page_parser_filter_type_highlighting = warning +resharper_asp_dead_code_highlighting = warning +resharper_asp_entity_highlighting = warning +resharper_asp_image_highlighting = warning +resharper_asp_invalid_control_type_highlighting = error +resharper_asp_not_resolved_highlighting = error +resharper_asp_ods_method_reference_resolve_error_highlighting = error +resharper_asp_resolve_warning_highlighting = warning +resharper_asp_skin_not_resolved_highlighting = error +resharper_asp_tag_attribute_with_optional_value_highlighting = warning +resharper_asp_theme_not_resolved_highlighting = error +resharper_asp_unused_register_directive_highlighting_highlighting = warning +resharper_asp_warning_highlighting = warning +resharper_assignment_in_conditional_expression_highlighting = warning +resharper_assignment_is_fully_discarded_highlighting = warning +resharper_assign_null_to_not_null_attribute_highlighting = warning +resharper_asxx_path_error_highlighting = warning +resharper_async_iterator_invocation_without_await_foreach_highlighting = warning +resharper_async_void_lambda_highlighting = warning +resharper_async_void_method_highlighting = warning +resharper_auto_property_can_be_made_get_only_global_highlighting = warning +resharper_auto_property_can_be_made_get_only_local_highlighting = warning +resharper_bad_attribute_brackets_spaces_highlighting = warning +resharper_bad_braces_spaces_highlighting = warning +resharper_bad_child_statement_indent_highlighting = warning +resharper_bad_colon_spaces_highlighting = warning +resharper_bad_comma_spaces_highlighting = warning +resharper_bad_control_braces_indent_highlighting = warning +resharper_bad_control_braces_line_breaks_highlighting = warning +resharper_bad_declaration_braces_indent_highlighting = warning +resharper_bad_declaration_braces_line_breaks_highlighting = warning +resharper_bad_empty_braces_line_breaks_highlighting = warning +resharper_bad_expression_braces_indent_highlighting = warning +resharper_bad_expression_braces_line_breaks_highlighting = warning +resharper_bad_generic_brackets_spaces_highlighting = warning +resharper_bad_indent_highlighting = warning +resharper_bad_linq_line_breaks_highlighting = warning +resharper_bad_list_line_breaks_highlighting = warning +resharper_bad_member_access_spaces_highlighting = warning +resharper_bad_namespace_braces_indent_highlighting = warning +resharper_bad_parens_line_breaks_highlighting = warning +resharper_bad_parens_spaces_highlighting = warning +resharper_bad_preprocessor_indent_highlighting = warning +resharper_bad_semicolon_spaces_highlighting = warning +resharper_bad_spaces_after_keyword_highlighting = warning +resharper_bad_square_brackets_spaces_highlighting = warning +resharper_bad_switch_braces_indent_highlighting = warning +resharper_bad_symbol_spaces_highlighting = warning +resharper_base_member_has_params_highlighting = warning +resharper_base_method_call_with_default_parameter_highlighting = warning +resharper_base_object_equals_is_object_equals_highlighting = warning +resharper_base_object_get_hash_code_call_in_get_hash_code_highlighting = warning +resharper_bitwise_operator_on_enum_without_flags_highlighting = warning +resharper_blazor_editor_required_highlighting = warning +resharper_built_in_type_reference_style_for_member_access_highlighting = warning +resharper_built_in_type_reference_style_highlighting = warning +resharper_by_ref_argument_is_volatile_field_highlighting = warning +resharper_cannot_apply_equality_operator_to_type_highlighting = warning +resharper_can_simplify_dictionary_lookup_with_try_add_highlighting = warning +resharper_can_simplify_dictionary_lookup_with_try_get_value_highlighting = warning +resharper_center_tag_is_obsolete_highlighting = warning +resharper_check_for_reference_equality_instead_1_highlighting = warning +resharper_check_for_reference_equality_instead_2_highlighting = warning +resharper_check_for_reference_equality_instead_3_highlighting = warning +resharper_check_for_reference_equality_instead_4_highlighting = warning +resharper_check_namespace_highlighting = warning +resharper_class_cannot_be_instantiated_highlighting = warning +resharper_class_can_be_sealed_global_highlighting = warning +resharper_class_can_be_sealed_local_highlighting = warning +resharper_class_never_instantiated_global_highlighting = warning +resharper_class_never_instantiated_local_highlighting = warning +resharper_class_with_virtual_members_never_inherited_global_highlighting = warning +resharper_class_with_virtual_members_never_inherited_local_highlighting = warning +resharper_clear_attribute_is_obsolete_all_highlighting = warning +resharper_clear_attribute_is_obsolete_highlighting = warning +resharper_cognitive_complexity_highlighting = warning +resharper_collection_never_queried_global_highlighting = warning +resharper_collection_never_queried_local_highlighting = warning +resharper_collection_never_updated_global_highlighting = warning +resharper_collection_never_updated_local_highlighting = warning +resharper_comment_typo_highlighting = none +resharper_compare_non_constrained_generic_with_null_highlighting = warning +resharper_compare_of_floats_by_equality_operator_highlighting = warning +resharper_complex_object_destructuring_problem_highlighting = warning +resharper_complex_object_in_context_destructuring_problem_highlighting = warning +resharper_conditional_access_qualifier_is_non_nullable_according_to_api_contract_highlighting = warning +resharper_conditional_ternary_equal_branch_highlighting = warning +resharper_condition_is_always_true_or_false_according_to_nullable_api_contract_highlighting = warning +resharper_condition_is_always_true_or_false_highlighting = warning +resharper_confusing_char_as_integer_in_constructor_highlighting = warning +resharper_constant_conditional_access_qualifier_highlighting = warning +resharper_constant_null_coalescing_condition_highlighting = warning +resharper_constructor_initializer_loop_highlighting = warning +resharper_container_annotation_redundancy_highlighting = warning +resharper_contextual_logger_problem_highlighting = warning +resharper_context_value_is_provided_highlighting = warning +resharper_contract_annotation_not_parsed_highlighting = warning +resharper_convert_closure_to_method_group_highlighting = warning +resharper_convert_conditional_ternary_expression_to_switch_expression_highlighting = warning +resharper_convert_if_do_to_while_highlighting = warning +resharper_convert_if_statement_to_conditional_ternary_expression_highlighting = warning +resharper_convert_if_statement_to_null_coalescing_assignment_highlighting = warning +resharper_convert_if_statement_to_null_coalescing_expression_highlighting = warning +resharper_convert_if_statement_to_return_statement_highlighting = warning +resharper_convert_if_statement_to_switch_statement_highlighting = hint +resharper_convert_if_to_or_expression_highlighting = warning +resharper_convert_nullable_to_short_form_highlighting = warning +resharper_convert_switch_statement_to_switch_expression_highlighting = warning +resharper_convert_to_auto_property_highlighting = warning +resharper_convert_to_auto_property_when_possible_highlighting = warning +resharper_convert_to_auto_property_with_private_setter_highlighting = warning +resharper_convert_to_compound_assignment_highlighting = warning +resharper_convert_to_constant_global_highlighting = warning +resharper_convert_to_constant_local_highlighting = warning +resharper_convert_to_lambda_expression_highlighting = warning +resharper_convert_to_local_function_highlighting = warning +resharper_convert_to_null_coalescing_compound_assignment_highlighting = warning +resharper_convert_to_primary_constructor_highlighting = warning +resharper_convert_to_static_class_highlighting = warning +resharper_convert_to_using_declaration_highlighting = warning +resharper_convert_type_check_pattern_to_null_check_highlighting = warning +resharper_convert_type_check_to_null_check_highlighting = warning +resharper_co_variant_array_conversion_highlighting = warning +resharper_default_value_attribute_for_optional_parameter_highlighting = warning +resharper_double_negation_in_pattern_highlighting = warning +resharper_double_negation_operator_highlighting = warning +resharper_duplicate_resource_highlighting = warning +resharper_empty_constructor_highlighting = warning +resharper_empty_destructor_highlighting = warning +resharper_empty_embedded_statement_highlighting = warning +resharper_empty_for_statement_highlighting = warning +resharper_empty_general_catch_clause_highlighting = warning +resharper_empty_namespace_highlighting = warning +resharper_empty_region_highlighting = warning +resharper_empty_statement_highlighting = warning +resharper_empty_title_tag_highlighting = warning +resharper_entity_name_captured_only_global_highlighting = warning +resharper_entity_name_captured_only_local_highlighting = warning +resharper_enumerable_sum_in_explicit_unchecked_context_highlighting = warning +resharper_enum_underlying_type_is_int_highlighting = warning +resharper_equal_expression_comparison_highlighting = warning +resharper_event_never_invoked_global_highlighting = warning +resharper_event_never_subscribed_to_global_highlighting = warning +resharper_event_never_subscribed_to_local_highlighting = warning +resharper_event_unsubscription_via_anonymous_delegate_highlighting = warning +resharper_exception_passed_as_template_argument_problem_highlighting = warning +resharper_explicit_caller_info_argument_highlighting = warning +resharper_expression_is_always_null_highlighting = warning +resharper_extract_common_property_pattern_highlighting = warning +resharper_field_can_be_made_read_only_global_highlighting = warning +resharper_field_can_be_made_read_only_local_highlighting = warning +resharper_field_hides_interface_property_with_default_implementation_highlighting = warning +resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting = warning +resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = warning +resharper_format_string_placeholders_mismatch_highlighting = warning +resharper_format_string_problem_highlighting = warning +resharper_for_can_be_converted_to_foreach_highlighting = warning +resharper_for_statement_condition_is_true_highlighting = warning +resharper_function_complexity_overflow_highlighting = warning +resharper_function_never_returns_highlighting = warning +resharper_function_recursive_on_all_paths_highlighting = warning +resharper_gc_suppress_finalize_for_type_without_destructor_highlighting = warning +resharper_generic_enumerator_not_disposed_highlighting = warning +resharper_heap_view_boxing_allocation_highlighting = hint +resharper_heap_view_can_avoid_closure_highlighting = suggestion +resharper_heap_view_closure_allocation_highlighting = hint +resharper_heap_view_delegate_allocation_highlighting = hint +resharper_heap_view_implicit_capture_highlighting = hint +resharper_heap_view_object_allocation_evident_highlighting = hint +resharper_heap_view_object_allocation_highlighting = hint +resharper_heap_view_object_allocation_possible_highlighting = hint +resharper_heap_view_possible_boxing_allocation_highlighting = hint +resharper_heuristic_unreachable_code_highlighting = warning +resharper_html_attributes_quotes_highlighting = warning +resharper_html_attribute_not_resolved_highlighting = warning +resharper_html_attribute_value_not_resolved_highlighting = warning +resharper_html_dead_code_highlighting = warning +resharper_html_event_not_resolved_highlighting = warning +resharper_html_id_duplication_highlighting = warning +resharper_html_id_not_resolved_highlighting = warning +resharper_html_obsolete_highlighting = warning +resharper_html_path_error_highlighting = warning +resharper_html_tag_not_closed_highlighting = error +resharper_html_tag_not_resolved_highlighting = warning +resharper_html_tag_should_be_self_closed_highlighting = warning +resharper_html_tag_should_not_be_self_closed_highlighting = warning +resharper_html_warning_highlighting = warning +resharper_identifier_typo_highlighting = none +resharper_inactive_preprocessor_branch_highlighting = warning +resharper_inconsistently_synchronized_field_highlighting = warning +resharper_inconsistent_context_log_property_naming_highlighting = warning +resharper_inconsistent_log_property_naming_highlighting = warning +resharper_inconsistent_naming_highlighting = warning +resharper_inconsistent_order_of_locks_highlighting = warning +resharper_incorrect_blank_lines_near_braces_highlighting = warning +resharper_indexing_by_invalid_range_highlighting = warning +resharper_inheritdoc_consider_usage_highlighting = none +resharper_inheritdoc_invalid_usage_highlighting = warning +resharper_inline_out_variable_declaration_highlighting = warning +resharper_inline_temporary_variable_highlighting = warning +resharper_internal_or_private_member_not_documented_highlighting = warning +resharper_interpolated_string_expression_is_not_i_formattable_highlighting = warning +resharper_introduce_optional_parameters_global_highlighting = warning +resharper_introduce_optional_parameters_local_highlighting = warning +resharper_int_division_by_zero_highlighting = warning +resharper_int_variable_overflow_highlighting = warning +resharper_int_variable_overflow_in_checked_context_highlighting = warning +resharper_int_variable_overflow_in_unchecked_context_highlighting = warning +resharper_invalid_value_type_highlighting = warning +resharper_invalid_xml_doc_comment_highlighting = warning +resharper_invert_condition_1_highlighting = warning +resharper_invert_if_highlighting = hint +resharper_invocation_is_skipped_highlighting = warning +resharper_invoke_as_extension_method_highlighting = warning +resharper_is_expression_always_false_highlighting = warning +resharper_is_expression_always_true_highlighting = warning +resharper_iterator_method_result_is_ignored_highlighting = warning +resharper_iterator_never_returns_highlighting = warning +resharper_join_declaration_and_initializer_highlighting = warning +resharper_join_null_check_with_usage_highlighting = warning +resharper_lambda_expression_can_be_made_static_highlighting = none +resharper_lambda_expression_must_be_static_highlighting = warning +resharper_lambda_should_not_capture_context_highlighting = warning +resharper_localizable_element_highlighting = warning +resharper_local_function_can_be_made_static_highlighting = warning +resharper_local_function_hides_method_highlighting = warning +resharper_local_variable_hides_member_highlighting = warning +resharper_log_message_is_sentence_problem_highlighting = warning +resharper_long_literal_ending_lower_l_highlighting = warning +resharper_loop_can_be_converted_to_query_highlighting = warning +resharper_loop_can_be_partly_converted_to_query_highlighting = warning +resharper_loop_variable_is_never_changed_inside_loop_highlighting = warning +resharper_markup_attribute_typo_highlighting = none +resharper_markup_text_typo_highlighting = none +resharper_math_abs_method_is_redundant_highlighting = warning +resharper_math_clamp_min_greater_than_max_highlighting = warning +resharper_meaningless_default_parameter_value_highlighting = warning +resharper_member_can_be_file_local_highlighting = warning +resharper_member_can_be_internal_highlighting = none +resharper_member_can_be_made_static_global_highlighting = warning +resharper_member_can_be_made_static_local_highlighting = warning +resharper_member_can_be_private_global_highlighting = warning +resharper_member_can_be_private_local_highlighting = warning +resharper_member_can_be_protected_global_highlighting = warning +resharper_member_can_be_protected_local_highlighting = warning +resharper_member_hides_interface_member_with_default_implementation_highlighting = warning +resharper_member_hides_static_from_outer_class_highlighting = warning +resharper_member_initializer_value_ignored_highlighting = warning +resharper_merge_and_pattern_highlighting = warning +resharper_merge_cast_with_type_check_highlighting = warning +resharper_merge_conditional_expression_highlighting = warning +resharper_merge_into_logical_pattern_highlighting = warning +resharper_merge_into_negated_pattern_highlighting = warning +resharper_merge_into_pattern_highlighting = warning +resharper_merge_nested_property_patterns_highlighting = warning +resharper_merge_sequential_checks_highlighting = warning +resharper_method_has_async_overload_highlighting = warning +resharper_method_has_async_overload_with_cancellation_highlighting = warning +resharper_method_overload_with_optional_parameter_highlighting = warning +resharper_method_supports_cancellation_highlighting = warning +resharper_missing_alt_attribute_in_img_tag_highlighting = warning +resharper_missing_blank_lines_highlighting = warning +resharper_missing_body_tag_highlighting = warning +resharper_missing_head_and_body_tags_highlighting = warning +resharper_missing_head_tag_highlighting = warning +resharper_missing_indent_highlighting = warning +resharper_missing_linebreak_highlighting = warning +resharper_missing_space_highlighting = warning +resharper_more_specific_foreach_variable_type_available_highlighting = warning +resharper_move_to_existing_positional_deconstruction_pattern_highlighting = warning +resharper_move_variable_declaration_inside_loop_condition_highlighting = warning +resharper_multiple_nullable_attributes_usage_highlighting = warning +resharper_multiple_order_by_highlighting = warning +resharper_multiple_resolve_candidates_in_text_highlighting = warning +resharper_multiple_spaces_highlighting = warning +resharper_multiple_statements_on_one_line_highlighting = warning +resharper_multiple_type_members_on_one_line_highlighting = warning +resharper_must_use_return_value_highlighting = warning +resharper_mvc_action_not_resolved_highlighting = error +resharper_mvc_area_not_resolved_highlighting = error +resharper_mvc_controller_not_resolved_highlighting = error +resharper_mvc_invalid_model_type_highlighting = error +resharper_mvc_masterpage_not_resolved_highlighting = error +resharper_mvc_partial_view_not_resolved_highlighting = error +resharper_mvc_template_not_resolved_highlighting = error +resharper_mvc_view_component_not_resolved_highlighting = error +resharper_mvc_view_component_view_not_resolved_highlighting = error +resharper_mvc_view_not_resolved_highlighting = error +resharper_negation_of_relational_pattern_highlighting = warning +resharper_negative_equality_expression_highlighting = warning +resharper_negative_index_highlighting = warning +resharper_nested_string_interpolation_highlighting = warning +resharper_non_atomic_compound_operator_highlighting = warning +resharper_non_constant_equality_expression_has_constant_result_highlighting = warning +resharper_non_parsable_element_highlighting = warning +resharper_non_readonly_member_in_get_hash_code_highlighting = warning +resharper_non_volatile_field_in_double_check_locking_highlighting = warning +resharper_not_accessed_field_global_highlighting = warning +resharper_not_accessed_field_local_highlighting = warning +resharper_not_accessed_out_parameter_variable_highlighting = warning +resharper_not_accessed_positional_property_global_highlighting = warning +resharper_not_accessed_positional_property_local_highlighting = warning +resharper_not_accessed_variable_highlighting = warning +resharper_not_assigned_out_parameter_highlighting = warning +resharper_not_declared_in_parent_culture_highlighting = warning +resharper_not_null_or_required_member_is_not_initialized_highlighting = warning +resharper_not_observable_annotation_redundancy_highlighting = warning +resharper_not_overridden_in_specific_culture_highlighting = warning +resharper_not_resolved_in_text_highlighting = warning +resharper_nullable_warning_suppression_is_used_highlighting = warning +resharper_null_coalescing_condition_is_always_not_null_according_to_api_contract_highlighting = warning +resharper_n_unit_async_method_must_be_task_highlighting = warning +resharper_n_unit_attribute_produces_too_many_tests_highlighting = warning +resharper_n_unit_auto_fixture_incorrect_argument_type_highlighting = warning +resharper_n_unit_auto_fixture_missed_test_attribute_highlighting = warning +resharper_n_unit_auto_fixture_missed_test_or_test_fixture_attribute_highlighting = warning +resharper_n_unit_auto_fixture_redundant_argument_in_inline_auto_data_attribute_highlighting = warning +resharper_n_unit_duplicate_values_highlighting = warning +resharper_n_unit_ignored_parameter_attribute_highlighting = warning +resharper_n_unit_implicit_unspecified_null_values_highlighting = warning +resharper_n_unit_incorrect_argument_type_highlighting = warning +resharper_n_unit_incorrect_expected_result_type_highlighting = warning +resharper_n_unit_incorrect_range_bounds_highlighting = warning +resharper_n_unit_method_with_parameters_and_test_attribute_highlighting = warning +resharper_n_unit_missing_arguments_in_test_case_attribute_highlighting = warning +resharper_n_unit_non_public_method_with_test_attribute_highlighting = warning +resharper_n_unit_no_values_provided_highlighting = warning +resharper_n_unit_parameter_type_is_not_compatible_with_attribute_highlighting = warning +resharper_n_unit_range_attribute_bounds_are_out_of_range_highlighting = warning +resharper_n_unit_range_step_sign_mismatch_highlighting = warning +resharper_n_unit_range_step_value_must_not_be_zero_highlighting = warning +resharper_n_unit_range_to_value_is_not_reachable_highlighting = warning +resharper_n_unit_redundant_argument_instead_of_expected_result_highlighting = warning +resharper_n_unit_redundant_argument_in_test_case_attribute_highlighting = warning +resharper_n_unit_redundant_expected_result_in_test_case_attribute_highlighting = warning +resharper_n_unit_test_case_attribute_requires_expected_result_highlighting = warning +resharper_n_unit_test_case_result_property_duplicates_expected_result_highlighting = warning +resharper_n_unit_test_case_result_property_is_obsolete_highlighting = warning +resharper_n_unit_test_case_source_cannot_be_resolved_highlighting = warning +resharper_n_unit_test_case_source_must_be_field_property_method_highlighting = warning +resharper_n_unit_test_case_source_must_be_static_highlighting = warning +resharper_n_unit_test_case_source_should_implement_i_enumerable_highlighting = warning +resharper_object_creation_as_statement_highlighting = warning +resharper_obsolete_element_error_highlighting = error +resharper_obsolete_element_highlighting = warning +resharper_one_way_operation_contract_with_return_type_highlighting = warning +resharper_operation_contract_without_service_contract_highlighting = warning +resharper_operator_is_can_be_used_highlighting = warning +resharper_operator_without_matched_checked_operator_highlighting = warning +resharper_optional_parameter_hierarchy_mismatch_highlighting = warning +resharper_optional_parameter_ref_out_highlighting = warning +resharper_other_tags_inside_script1_highlighting = error +resharper_other_tags_inside_script2_highlighting = error +resharper_other_tags_inside_unclosed_script_highlighting = error +resharper_outdent_is_off_prev_level_highlighting = warning +resharper_out_parameter_value_is_always_discarded_global_highlighting = warning +resharper_out_parameter_value_is_always_discarded_local_highlighting = warning +resharper_overridden_with_empty_value_highlighting = warning +resharper_overridden_with_same_value_highlighting = warning +resharper_parameter_hides_member_highlighting = warning +resharper_parameter_only_used_for_precondition_check_global_highlighting = warning +resharper_parameter_only_used_for_precondition_check_local_highlighting = warning +resharper_parameter_type_can_be_enumerable_global_highlighting = warning +resharper_parameter_type_can_be_enumerable_local_highlighting = warning +resharper_partial_method_parameter_name_mismatch_highlighting = warning +resharper_partial_method_with_single_part_highlighting = warning +resharper_partial_type_with_single_part_highlighting = warning +resharper_pass_string_interpolation_highlighting = warning +resharper_pattern_always_matches_highlighting = warning +resharper_pattern_is_always_true_or_false_highlighting = warning +resharper_pattern_is_redundant_highlighting = warning +resharper_pattern_never_matches_highlighting = warning +resharper_place_assignment_expression_into_block_highlighting = warning +resharper_polymorphic_field_like_event_invocation_highlighting = warning +resharper_positional_property_used_problem_highlighting = warning +resharper_possible_infinite_inheritance_highlighting = warning +resharper_possible_intended_rethrow_highlighting = warning +resharper_possible_interface_member_ambiguity_highlighting = warning +resharper_possible_invalid_cast_exception_highlighting = warning +resharper_possible_invalid_cast_exception_in_foreach_loop_highlighting = warning +resharper_possible_invalid_operation_exception_highlighting = warning +resharper_possible_loss_of_fraction_highlighting = warning +resharper_possible_mistaken_argument_highlighting = warning +resharper_possible_mistaken_call_to_get_type_1_highlighting = warning +resharper_possible_mistaken_call_to_get_type_2_highlighting = warning +resharper_possible_multiple_enumeration_highlighting = warning +resharper_possible_multiple_write_access_in_double_check_locking_highlighting = warning +resharper_possible_null_reference_exception_highlighting = warning +resharper_possible_struct_member_modification_of_non_variable_struct_highlighting = warning +resharper_possible_unintended_linear_search_in_set_highlighting = warning +resharper_possible_unintended_queryable_as_enumerable_highlighting = warning +resharper_possible_unintended_reference_comparison_highlighting = warning +resharper_possible_write_to_me_highlighting = warning +resharper_possibly_impure_method_call_on_readonly_variable_highlighting = warning +resharper_possibly_missing_indexer_initializer_comma_highlighting = warning +resharper_possibly_mistaken_use_of_interpolated_string_insert_highlighting = warning +resharper_private_field_can_be_converted_to_local_variable_highlighting = warning +resharper_property_can_be_made_init_only_global_highlighting = warning +resharper_property_can_be_made_init_only_local_highlighting = warning +resharper_property_field_keyword_is_never_assigned_highlighting = warning +resharper_property_field_keyword_is_never_used_highlighting = warning +resharper_property_not_resolved_highlighting = error +resharper_public_constructor_in_abstract_class_highlighting = warning +resharper_pure_attribute_on_void_method_highlighting = warning +resharper_razor_layout_not_resolved_highlighting = error +resharper_razor_section_not_resolved_highlighting = error +resharper_read_access_in_double_check_locking_highlighting = warning +resharper_redundant_abstract_modifier_highlighting = warning +resharper_redundant_accessor_body_highlighting = warning +resharper_redundant_always_match_subpattern_highlighting = warning +resharper_redundant_anonymous_type_property_name_highlighting = warning +resharper_redundant_argument_default_value_highlighting = warning +resharper_redundant_array_creation_expression_highlighting = warning +resharper_redundant_array_lower_bound_specification_highlighting = warning +resharper_redundant_assignment_highlighting = warning +resharper_redundant_attribute_parentheses_highlighting = warning +resharper_redundant_attribute_suffix_highlighting = warning +resharper_redundant_attribute_usage_property_highlighting = warning +resharper_redundant_base_constructor_call_highlighting = warning +resharper_redundant_base_qualifier_highlighting = warning +resharper_redundant_blank_lines_highlighting = warning +resharper_redundant_bool_compare_highlighting = warning +resharper_redundant_caller_argument_expression_default_value_highlighting = warning +resharper_redundant_case_label_highlighting = warning +resharper_redundant_cast_highlighting = warning +resharper_redundant_catch_clause_highlighting = warning +resharper_redundant_check_before_assignment_highlighting = warning +resharper_redundant_collection_initializer_element_braces_highlighting = warning +resharper_redundant_configure_await_highlighting = warning +resharper_redundant_declaration_semicolon_highlighting = warning +resharper_redundant_default_member_initializer_highlighting = warning +resharper_redundant_delegate_creation_highlighting = warning +resharper_redundant_dictionary_contains_key_before_adding_highlighting = warning +resharper_redundant_disable_warning_comment_highlighting = warning +resharper_redundant_discard_designation_highlighting = warning +resharper_redundant_empty_case_else_highlighting = warning +resharper_redundant_empty_finally_block_highlighting = warning +resharper_redundant_empty_object_creation_argument_list_highlighting = warning +resharper_redundant_empty_object_or_collection_initializer_highlighting = warning +resharper_redundant_empty_switch_section_highlighting = warning +resharper_redundant_enumerable_cast_call_highlighting = warning +resharper_redundant_enum_case_label_for_default_section_highlighting = warning +resharper_redundant_explicit_array_creation_highlighting = warning +resharper_redundant_explicit_array_size_highlighting = warning +resharper_redundant_explicit_nullable_creation_highlighting = warning +resharper_redundant_explicit_params_array_creation_highlighting = warning +resharper_redundant_explicit_positional_property_declaration_highlighting = warning +resharper_redundant_explicit_tuple_component_name_highlighting = warning +resharper_redundant_extends_list_entry_highlighting = warning +resharper_redundant_fixed_pointer_declaration_highlighting = warning +resharper_redundant_if_else_block_highlighting = warning +resharper_redundant_if_statement_then_keyword_highlighting = warning +resharper_redundant_immediate_delegate_invocation_highlighting = warning +resharper_redundant_is_before_relational_pattern_highlighting = warning +resharper_redundant_iterator_keyword_highlighting = warning +resharper_redundant_jump_statement_highlighting = warning +resharper_redundant_lambda_parameter_type_highlighting = warning +resharper_redundant_lambda_signature_parentheses_highlighting = warning +resharper_redundant_linebreak_highlighting = warning +resharper_redundant_logical_conditional_expression_operand_highlighting = warning +resharper_redundant_me_qualifier_highlighting = warning +resharper_redundant_my_base_qualifier_highlighting = warning +resharper_redundant_my_class_qualifier_highlighting = warning +resharper_redundant_name_qualifier_highlighting = warning +resharper_redundant_not_null_constraint_highlighting = warning +resharper_redundant_nullable_annotation_on_reference_type_constraint_highlighting = warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_base_type_highlighting = warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_type_kind_highlighting = warning +resharper_redundant_nullable_flow_attribute_highlighting = warning +resharper_redundant_nullable_type_mark_highlighting = warning +resharper_redundant_nullness_attribute_with_nullable_reference_types_highlighting = warning +resharper_redundant_overflow_checking_context_highlighting = warning +resharper_redundant_overload_global_highlighting = warning +resharper_redundant_overload_local_highlighting = warning +resharper_redundant_overridden_member_highlighting = warning +resharper_redundant_params_highlighting = warning +resharper_redundant_parentheses_highlighting = warning +resharper_redundant_pattern_parentheses_highlighting = warning +resharper_redundant_property_parentheses_highlighting = warning +resharper_redundant_property_pattern_clause_highlighting = warning +resharper_redundant_qualifier_highlighting = warning +resharper_redundant_query_order_by_ascending_keyword_highlighting = warning +resharper_redundant_range_bound_highlighting = warning +resharper_redundant_readonly_modifier_highlighting = warning +resharper_redundant_record_body_highlighting = warning +resharper_redundant_record_class_keyword_highlighting = warning +resharper_redundant_scoped_parameter_modifier_highlighting = warning +resharper_redundant_setter_value_parameter_declaration_highlighting = warning +resharper_redundant_set_contains_before_adding_highlighting = warning +resharper_redundant_space_highlighting = warning +resharper_redundant_string_format_call_highlighting = warning +resharper_redundant_string_interpolation_highlighting = warning +resharper_redundant_string_to_char_array_call_highlighting = warning +resharper_redundant_string_type_highlighting = warning +resharper_redundant_suppress_nullable_warning_expression_highlighting = warning +resharper_redundant_ternary_expression_highlighting = warning +resharper_redundant_to_string_call_for_value_type_highlighting = warning +resharper_redundant_to_string_call_highlighting = warning +resharper_redundant_type_arguments_of_method_highlighting = warning +resharper_redundant_type_check_in_pattern_highlighting = warning +resharper_redundant_unsafe_context_highlighting = warning +resharper_redundant_using_directive_global_highlighting = warning +resharper_redundant_using_directive_highlighting = warning +resharper_redundant_verbatim_prefix_highlighting = warning +resharper_redundant_verbatim_string_prefix_highlighting = warning +resharper_redundant_virtual_modifier_highlighting = warning +resharper_redundant_with_expression_highlighting = warning +resharper_reference_equals_with_value_type_highlighting = warning +resharper_reg_exp_inspections_highlighting = warning +resharper_remove_constructor_invocation_highlighting = warning +resharper_remove_redundant_or_statement_false_highlighting = warning +resharper_remove_redundant_or_statement_true_highlighting = warning +resharper_remove_to_list_1_highlighting = warning +resharper_remove_to_list_2_highlighting = warning +resharper_replace_auto_property_with_computed_property_highlighting = warning +resharper_replace_conditional_expression_with_null_coalescing_highlighting = warning +resharper_replace_object_pattern_with_var_pattern_highlighting = warning +resharper_replace_sequence_equal_with_constant_pattern_highlighting = warning +resharper_replace_slice_with_range_indexer_highlighting = warning +resharper_replace_substring_with_range_indexer_highlighting = warning +resharper_replace_with_field_keyword_highlighting = warning +resharper_replace_with_first_or_default_1_highlighting = warning +resharper_replace_with_first_or_default_2_highlighting = warning +resharper_replace_with_first_or_default_3_highlighting = warning +resharper_replace_with_first_or_default_4_highlighting = warning +resharper_replace_with_last_or_default_1_highlighting = warning +resharper_replace_with_last_or_default_2_highlighting = warning +resharper_replace_with_last_or_default_3_highlighting = warning +resharper_replace_with_last_or_default_4_highlighting = warning +resharper_replace_with_of_type_1_highlighting = warning +resharper_replace_with_of_type_2_highlighting = warning +resharper_replace_with_of_type_3_highlighting = warning +resharper_replace_with_of_type_any_1_highlighting = warning +resharper_replace_with_of_type_any_2_highlighting = warning +resharper_replace_with_of_type_count_1_highlighting = warning +resharper_replace_with_of_type_count_2_highlighting = warning +resharper_replace_with_of_type_first_1_highlighting = warning +resharper_replace_with_of_type_first_2_highlighting = warning +resharper_replace_with_of_type_first_or_default_1_highlighting = warning +resharper_replace_with_of_type_first_or_default_2_highlighting = warning +resharper_replace_with_of_type_last_1_highlighting = warning +resharper_replace_with_of_type_last_2_highlighting = warning +resharper_replace_with_of_type_last_or_default_1_highlighting = warning +resharper_replace_with_of_type_last_or_default_2_highlighting = warning +resharper_replace_with_of_type_long_count_highlighting = warning +resharper_replace_with_of_type_single_1_highlighting = warning +resharper_replace_with_of_type_single_2_highlighting = warning +resharper_replace_with_of_type_single_or_default_1_highlighting = warning +resharper_replace_with_of_type_single_or_default_2_highlighting = warning +resharper_replace_with_of_type_where_highlighting = warning +resharper_replace_with_simple_assignment_false_highlighting = warning +resharper_replace_with_simple_assignment_true_highlighting = warning +resharper_replace_with_single_assignment_false_highlighting = warning +resharper_replace_with_single_assignment_true_highlighting = warning +resharper_replace_with_single_call_to_any_highlighting = warning +resharper_replace_with_single_call_to_count_highlighting = warning +resharper_replace_with_single_call_to_first_highlighting = warning +resharper_replace_with_single_call_to_first_or_default_highlighting = warning +resharper_replace_with_single_call_to_last_highlighting = warning +resharper_replace_with_single_call_to_last_or_default_highlighting = warning +resharper_replace_with_single_call_to_single_highlighting = warning +resharper_replace_with_single_call_to_single_or_default_highlighting = warning +resharper_replace_with_single_or_default_1_highlighting = warning +resharper_replace_with_single_or_default_2_highlighting = warning +resharper_replace_with_single_or_default_3_highlighting = warning +resharper_replace_with_single_or_default_4_highlighting = warning +resharper_replace_with_string_is_null_or_empty_highlighting = warning +resharper_required_base_types_conflict_highlighting = warning +resharper_required_base_types_direct_conflict_highlighting = warning +resharper_required_base_types_is_not_inherited_highlighting = warning +resharper_resource_item_not_resolved_highlighting = error +resharper_resource_not_resolved_highlighting = error +resharper_resx_not_resolved_highlighting = warning +resharper_return_type_can_be_enumerable_global_highlighting = warning +resharper_return_type_can_be_enumerable_local_highlighting = warning +resharper_return_type_can_be_not_nullable_highlighting = warning +resharper_return_value_of_pure_method_is_not_used_highlighting = warning +resharper_route_templates_action_route_prefix_can_be_extracted_to_controller_route_highlighting = warning +resharper_route_templates_ambiguous_matching_constraint_constructor_highlighting = warning +resharper_route_templates_constraint_argument_cannot_be_converted_highlighting = warning +resharper_route_templates_controller_route_parameter_is_not_passed_to_methods_highlighting = warning +resharper_route_templates_duplicated_parameter_highlighting = warning +resharper_route_templates_matching_constraint_constructor_not_resolved_highlighting = warning +resharper_route_templates_method_missing_route_parameters_highlighting = warning +resharper_route_templates_optional_parameter_can_be_preceded_only_by_single_period_highlighting = warning +resharper_route_templates_optional_parameter_must_be_at_the_end_of_segment_highlighting = warning +resharper_route_templates_parameter_constraint_can_be_specified_highlighting = warning +resharper_route_templates_parameter_type_and_constraints_mismatch_highlighting = warning +resharper_route_templates_parameter_type_can_be_made_stricter_highlighting = warning +resharper_route_templates_route_parameter_constraint_not_resolved_highlighting = warning +resharper_route_templates_route_parameter_is_not_passed_to_method_highlighting = warning +resharper_route_templates_route_token_not_resolved_highlighting = warning +resharper_route_templates_symbol_not_resolved_highlighting = warning +resharper_route_templates_syntax_error_highlighting = warning +resharper_safe_cast_is_used_as_type_check_highlighting = warning +resharper_script_tag_has_both_src_and_content_attributes_highlighting = error +resharper_sealed_member_in_sealed_class_highlighting = warning +resharper_separate_control_transfer_statement_highlighting = warning +resharper_service_contract_without_operations_highlighting = warning +resharper_shift_expression_real_shift_count_is_zero_highlighting = warning +resharper_shift_expression_result_equals_zero_highlighting = warning +resharper_shift_expression_right_operand_not_equal_real_count_highlighting = warning +resharper_shift_expression_zero_left_operand_highlighting = warning +resharper_similar_anonymous_type_nearby_highlighting = warning +resharper_simplify_conditional_operator_highlighting = warning +resharper_simplify_conditional_ternary_expression_highlighting = warning +resharper_simplify_i_if_highlighting = warning +resharper_simplify_linq_expression_use_all_highlighting = warning +resharper_simplify_linq_expression_use_any_highlighting = warning +resharper_simplify_linq_expression_use_min_by_and_max_by_highlighting = warning +resharper_simplify_string_interpolation_highlighting = warning +resharper_specify_a_culture_in_string_conversion_explicitly_highlighting = warning +resharper_specify_string_comparison_highlighting = warning +resharper_spin_lock_in_readonly_field_highlighting = warning +resharper_stack_alloc_inside_loop_highlighting = warning +resharper_static_member_initializer_referes_to_member_below_highlighting = warning +resharper_static_member_in_generic_type_highlighting = warning +resharper_static_problem_in_text_highlighting = warning +resharper_string_compare_is_culture_specific_1_highlighting = warning +resharper_string_compare_is_culture_specific_2_highlighting = warning +resharper_string_compare_is_culture_specific_3_highlighting = warning +resharper_string_compare_is_culture_specific_4_highlighting = warning +resharper_string_compare_is_culture_specific_5_highlighting = warning +resharper_string_compare_is_culture_specific_6_highlighting = warning +resharper_string_compare_to_is_culture_specific_highlighting = warning +resharper_string_ends_with_is_culture_specific_highlighting = warning +resharper_string_index_of_is_culture_specific_1_highlighting = warning +resharper_string_index_of_is_culture_specific_2_highlighting = warning +resharper_string_index_of_is_culture_specific_3_highlighting = warning +resharper_string_last_index_of_is_culture_specific_1_highlighting = warning +resharper_string_last_index_of_is_culture_specific_2_highlighting = warning +resharper_string_last_index_of_is_culture_specific_3_highlighting = warning +resharper_string_literal_as_interpolation_argument_highlighting = warning +resharper_string_literal_typo_highlighting = none +resharper_string_starts_with_is_culture_specific_highlighting = warning +resharper_structured_message_template_problem_highlighting = warning +resharper_struct_can_be_made_read_only_highlighting = warning +resharper_struct_member_can_be_made_read_only_highlighting = warning +resharper_suggest_base_type_for_parameter_highlighting = warning +resharper_suggest_base_type_for_parameter_in_constructor_highlighting = warning +resharper_suggest_discard_declaration_var_style_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = warning +resharper_suggest_var_or_type_deconstruction_declarations_highlighting = warning +resharper_suggest_var_or_type_elsewhere_highlighting = warning +resharper_suggest_var_or_type_simple_types_highlighting = warning +resharper_suppress_nullable_warning_expression_as_inverted_is_expression_highlighting = warning +resharper_suspicious_lock_over_synchronization_primitive_highlighting = warning +resharper_suspicious_math_sign_method_highlighting = warning +resharper_suspicious_parameter_name_in_argument_null_exception_highlighting = warning +resharper_suspicious_type_conversion_global_highlighting = warning +resharper_swap_via_deconstruction_highlighting = warning +resharper_switch_expression_handles_some_known_enum_values_with_exception_in_default_highlighting = warning +resharper_switch_statement_for_enum_misses_default_section_highlighting = warning +resharper_switch_statement_handles_some_known_enum_values_with_default_highlighting = warning +resharper_switch_statement_missing_some_enum_cases_no_default_highlighting = warning +resharper_symbol_from_not_copied_locally_reference_used_warning_highlighting = warning +resharper_tabs_and_spaces_mismatch_highlighting = warning +resharper_tabs_are_disallowed_highlighting = warning +resharper_tabs_outside_indent_highlighting = warning +resharper_tail_recursive_call_highlighting = warning +resharper_template_duplicate_property_problem_highlighting = warning +resharper_template_format_string_problem_highlighting = warning +resharper_template_is_not_compile_time_constant_problem_highlighting = warning +resharper_thread_static_at_instance_field_highlighting = warning +resharper_thread_static_field_has_initializer_highlighting = warning +resharper_too_wide_local_variable_scope_highlighting = warning +resharper_try_cast_always_succeeds_highlighting = warning +resharper_try_statements_can_be_merged_highlighting = warning +resharper_type_parameter_can_be_variant_highlighting = warning +resharper_unassigned_field_global_highlighting = warning +resharper_unassigned_field_local_highlighting = warning +resharper_unassigned_get_only_auto_property_highlighting = warning +resharper_unassigned_readonly_field_highlighting = warning +resharper_unclosed_script_highlighting = error +resharper_unnecessary_whitespace_highlighting = warning +resharper_unreachable_switch_arm_due_to_integer_analysis_highlighting = warning +resharper_unreachable_switch_case_due_to_integer_analysis_highlighting = warning +resharper_unreal_header_tool_error_highlighting = error +resharper_unreal_header_tool_warning_highlighting = warning +resharper_unsupported_required_base_type_highlighting = warning +resharper_unused_anonymous_method_signature_highlighting = warning +resharper_unused_auto_property_accessor_global_highlighting = warning +resharper_unused_auto_property_accessor_local_highlighting = warning +resharper_unused_import_clause_highlighting = warning +resharper_unused_local_function_highlighting = warning +resharper_unused_local_function_parameter_highlighting = warning +resharper_unused_local_function_return_value_highlighting = warning +resharper_unused_member_global_highlighting = warning +resharper_unused_member_hierarchy_global_highlighting = warning +resharper_unused_member_hierarchy_local_highlighting = warning +resharper_unused_member_in_super_global_highlighting = warning +resharper_unused_member_in_super_local_highlighting = warning +resharper_unused_member_local_highlighting = warning +resharper_unused_method_return_value_global_highlighting = warning +resharper_unused_method_return_value_local_highlighting = warning +resharper_unused_parameter_global_highlighting = warning +resharper_unused_parameter_in_partial_method_highlighting = warning +resharper_unused_parameter_local_highlighting = warning +resharper_unused_tuple_component_in_return_value_highlighting = warning +resharper_unused_type_global_highlighting = warning +resharper_unused_type_local_highlighting = warning +resharper_unused_type_parameter_highlighting = warning +resharper_unused_variable_highlighting = warning +resharper_useless_binary_operation_highlighting = warning +resharper_useless_comparison_to_integral_constant_highlighting = warning +resharper_use_array_creation_expression_1_highlighting = warning +resharper_use_array_creation_expression_2_highlighting = warning +resharper_use_array_empty_method_highlighting = warning +resharper_use_await_using_highlighting = warning +resharper_use_cancellation_token_for_i_async_enumerable_highlighting = warning +resharper_use_collection_count_property_highlighting = warning +resharper_use_configure_await_false_for_async_disposable_highlighting = warning +resharper_use_configure_await_false_highlighting = warning +resharper_use_deconstruction_highlighting = warning +resharper_use_empty_types_field_highlighting = warning +resharper_use_event_args_empty_field_highlighting = warning +resharper_use_format_specifier_in_format_string_highlighting = warning +resharper_use_implicitly_typed_variable_evident_highlighting = warning +resharper_use_implicitly_typed_variable_highlighting = warning +resharper_use_implicit_by_val_modifier_highlighting = warning +resharper_use_indexed_property_highlighting = warning +resharper_use_index_from_end_expression_highlighting = warning +resharper_use_is_operator_1_highlighting = warning +resharper_use_is_operator_2_highlighting = warning +resharper_use_method_any_0_highlighting = warning +resharper_use_method_any_1_highlighting = warning +resharper_use_method_any_2_highlighting = warning +resharper_use_method_any_3_highlighting = warning +resharper_use_method_any_4_highlighting = warning +resharper_use_method_is_instance_of_type_highlighting = warning +resharper_use_nameof_expression_for_part_of_the_string_highlighting = warning +resharper_use_nameof_expression_highlighting = warning +resharper_use_nameof_for_dependency_property_highlighting = warning +resharper_use_name_of_instead_of_type_of_highlighting = warning +resharper_use_negated_pattern_in_is_expression_highlighting = warning +resharper_use_negated_pattern_matching_highlighting = warning +resharper_use_nullable_annotation_instead_of_attribute_highlighting = warning +resharper_use_nullable_attributes_supported_by_compiler_highlighting = warning +resharper_use_nullable_reference_types_annotation_syntax_highlighting = warning +resharper_use_null_propagation_highlighting = warning +resharper_use_object_or_collection_initializer_highlighting = warning +resharper_use_pattern_matching_highlighting = warning +resharper_use_positional_deconstruction_pattern_highlighting = warning +resharper_use_string_interpolation_highlighting = warning +resharper_use_string_interpolation_when_possible_highlighting = warning +resharper_use_switch_case_pattern_variable_highlighting = warning +resharper_use_throw_if_null_method_highlighting = warning +resharper_use_unsigned_right_shift_operator_highlighting = warning +resharper_use_verbatim_string_highlighting = warning +resharper_use_with_expression_to_copy_anonymous_object_highlighting = warning +resharper_use_with_expression_to_copy_record_highlighting = warning +resharper_use_with_expression_to_copy_struct_highlighting = warning +resharper_use_with_expression_to_copy_tuple_highlighting = warning +resharper_value_parameter_not_used_highlighting = warning +resharper_value_range_attribute_violation_highlighting = warning +resharper_variable_can_be_not_nullable_highlighting = warning +resharper_variable_hides_outer_variable_highlighting = warning +resharper_virtual_member_call_in_constructor_highlighting = warning +resharper_virtual_member_never_overridden_global_highlighting = warning +resharper_virtual_member_never_overridden_local_highlighting = warning +resharper_void_method_with_must_use_return_value_attribute_highlighting = warning +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_module_qualification_resolve_highlighting = warning +resharper_web_config_redundant_add_namespace_tag_highlighting = warning +resharper_web_config_redundant_location_tag_highlighting = warning +resharper_web_config_tag_prefix_redundand_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_unused_add_tag_highlighting = warning +resharper_web_config_unused_element_due_to_config_source_attribute_highlighting = warning +resharper_web_config_unused_remove_or_clear_tag_highlighting = warning +resharper_web_config_web_config_path_warning_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning +resharper_web_ignored_path_highlighting = warning +resharper_web_mapped_path_highlighting = warning +resharper_with_expression_instead_of_initializer_highlighting = warning +resharper_with_expression_modifies_all_members_highlighting = warning +resharper_wrong_indent_size_highlighting = warning +resharper_xaml_assign_null_to_not_null_attribute_highlighting = warning +resharper_xaml_avalonia_wrong_binding_mode_for_stream_binding_operator_highlighting = warning +resharper_xaml_binding_without_context_not_resolved_highlighting = warning +resharper_xaml_binding_with_context_not_resolved_highlighting = warning +resharper_xaml_compiled_binding_missing_data_type_error_highlighting_highlighting = error +resharper_xaml_constructor_warning_highlighting = warning +resharper_xaml_decimal_parsing_is_culture_dependent_highlighting = warning +resharper_xaml_dependency_property_resolve_error_highlighting = warning +resharper_xaml_duplicate_style_setter_highlighting = warning +resharper_xaml_dynamic_resource_error_highlighting = error +resharper_xaml_element_name_reference_not_resolved_highlighting = error +resharper_xaml_empty_grid_length_definition_highlighting = error +resharper_xaml_field_modifier_requires_name_attribute_highlighting = warning +resharper_xaml_grid_definitions_can_be_converted_to_attribute_highlighting = warning +resharper_xaml_ignored_path_highlighting_highlighting = warning +resharper_xaml_index_out_of_grid_definition_highlighting = warning +resharper_xaml_invalid_member_type_highlighting = error +resharper_xaml_invalid_resource_target_type_highlighting = error +resharper_xaml_invalid_resource_type_highlighting = error +resharper_xaml_invalid_type_highlighting = error +resharper_xaml_language_level_highlighting = error +resharper_xaml_mapped_path_highlighting_highlighting = warning +resharper_xaml_method_arguments_will_be_ignored_highlighting = warning +resharper_xaml_missing_grid_index_highlighting = warning +resharper_xaml_overloads_collision_highlighting = warning +resharper_xaml_parent_is_out_of_current_component_tree_highlighting = warning +resharper_xaml_path_error_highlighting = warning +resharper_xaml_possible_null_reference_exception_highlighting = warning +resharper_xaml_redundant_attached_property_highlighting = warning +resharper_xaml_redundant_binding_mode_attribute_highlighting = warning +resharper_xaml_redundant_collection_property_highlighting = warning +resharper_xaml_redundant_freeze_attribute_highlighting = warning +resharper_xaml_redundant_grid_definitions_highlighting = warning +resharper_xaml_redundant_grid_span_highlighting = warning +resharper_xaml_redundant_modifiers_attribute_highlighting = warning +resharper_xaml_redundant_namespace_alias_highlighting = warning +resharper_xaml_redundant_name_attribute_highlighting = warning +resharper_xaml_redundant_property_type_qualifier_highlighting = warning +resharper_xaml_redundant_resource_highlighting = warning +resharper_xaml_redundant_styled_value_highlighting = warning +resharper_xaml_redundant_update_source_trigger_attribute_highlighting = warning +resharper_xaml_redundant_xamarin_forms_class_declaration_highlighting = warning +resharper_xaml_resource_file_path_case_mismatch_highlighting = warning +resharper_xaml_routed_event_resolve_error_highlighting = warning +resharper_xaml_static_resource_not_resolved_highlighting = warning +resharper_xaml_style_class_not_found_highlighting = warning +resharper_xaml_style_invalid_target_type_highlighting = error +resharper_xaml_unexpected_element_highlighting = error +resharper_xaml_unexpected_text_token_highlighting = error +resharper_xaml_xaml_duplicate_device_family_type_view_highlighting_highlighting = error +resharper_xaml_xaml_mismatched_device_family_view_clr_name_highlighting_highlighting = warning +resharper_xaml_xaml_relative_source_default_mode_warning_highlighting_highlighting = warning +resharper_xaml_xaml_unknown_device_family_type_highlighting_highlighting = warning +resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_highlighting_highlighting = warning +resharper_xaml_x_key_attribute_disallowed_highlighting = error +resharper_xunit_xunit_test_with_console_output_highlighting = warning +resharper_zero_index_from_end_highlighting = warning + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fx,fxh,hlsl,hlsli,hlslinc,master,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/locale/Messages.resx b/locale/Messages.resx index 974d87f..f463a3b 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -47,46 +47,48 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + @@ -100,10 +102,14 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + I'm ready! diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index b5eacac..d045bc3 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -47,46 +47,48 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + @@ -100,10 +102,14 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + Я запустился! diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index d7d7520..1909d27 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -47,46 +47,48 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + @@ -100,10 +102,14 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + я родился! diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index 322b65a..a1b6fbd 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -22,11 +22,13 @@ using Serilog.Extensions.Logging; namespace Boyfriend; -public class Boyfriend { +public sealed class Boyfriend +{ public static readonly AllowedMentions NoMentions = new( Array.Empty(), Array.Empty(), Array.Empty()); - public static async Task Main(string[] args) { + public static async Task Main(string[] args) + { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); var services = host.Services; @@ -40,10 +42,12 @@ public class Boyfriend { await host.RunAsync(); } - private static IHostBuilder CreateHostBuilder(string[] args) { + private static IHostBuilder CreateHostBuilder(string[] args) + { return Host.CreateDefaultBuilder(args) .AddDiscordService( - services => { + services => + { var configuration = services.GetRequiredService(); return configuration.GetValue("BOT_TOKEN") @@ -52,13 +56,18 @@ public class Boyfriend { + "BOT_TOKEN environment variable to a valid token."); } ).ConfigureServices( - (_, services) => { + (_, services) => + { services.Configure( - options => options.Intents |= GatewayIntents.MessageContents - | GatewayIntents.GuildMembers - | GatewayIntents.GuildScheduledEvents); + options => + { + options.Intents |= GatewayIntents.MessageContents + | GatewayIntents.GuildMembers + | GatewayIntents.GuildScheduledEvents; + }); services.Configure( - cSettings => { + cSettings => + { cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); @@ -92,12 +101,15 @@ public class Boyfriend { var responderTypes = typeof(Boyfriend).Assembly .GetExportedTypes() .Where(t => t.IsResponder()); - foreach (var responderType in responderTypes) services.AddResponder(responderType); + foreach (var responderType in responderTypes) + { + services.AddResponder(responderType); + } } ).ConfigureLogging( c => c.AddConsole() .AddFile("Logs/Boyfriend-{Date}.log", - outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") + outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) diff --git a/src/ColorsList.cs b/src/ColorsList.cs index bdd5bce..fc32274 100644 --- a/src/ColorsList.cs +++ b/src/ColorsList.cs @@ -5,14 +5,15 @@ namespace Boyfriend; /// /// Contains all colors used in embeds. /// -public static class ColorsList { +public static class ColorsList +{ public static readonly Color Default = Color.Gray; - public static readonly Color Red = Color.Firebrick; - public static readonly Color Green = Color.PaleGreen; - public static readonly Color Yellow = Color.Gold; - public static readonly Color Blue = Color.RoyalBlue; + public static readonly Color Red = Color.Firebrick; + public static readonly Color Green = Color.PaleGreen; + public static readonly Color Yellow = Color.Gold; + public static readonly Color Blue = Color.RoyalBlue; public static readonly Color Magenta = Color.Orchid; - public static readonly Color Cyan = Color.LightSkyBlue; - public static readonly Color Black = Color.Black; - public static readonly Color White = Color.WhiteSmoke; + public static readonly Color Cyan = Color.LightSkyBlue; + public static readonly Color Black = Color.Black; + public static readonly Color White = Color.WhiteSmoke; } diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index a41cf11..34ba68d 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -21,19 +21,21 @@ namespace Boyfriend.Commands; /// Handles the command to show information about this bot: /about. /// [UsedImplicitly] -public class AboutCommandGroup : CommandGroup { - private static readonly string[] Developers = { "Octol1ttle", "mctaylors", "neroduckale" }; - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; - private readonly IDiscordRestUserAPI _userApi; +public class AboutCommandGroup : CommandGroup +{ + private static readonly string[] Developers = { "Octol1ttle", "mctaylors", "neroduckale" }; + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; public AboutCommandGroup( - ICommandContext context, GuildDataService dataService, - FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + ICommandContext context, GuildDataService guildData, + FeedbackService feedback, IDiscordRestUserAPI userApi) + { _context = context; - _dataService = dataService; - _feedbackService = feedbackService; + _guildData = guildData; + _feedback = feedback; _userApi = userApi; } @@ -48,24 +50,32 @@ public class AboutCommandGroup : CommandGroup { [RequireContext(ChannelContext.Guild)] [Description("Shows Boyfriend's developers")] [UsedImplicitly] - public async Task ExecuteAboutAsync() { + public async Task ExecuteAboutAsync() + { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } - var cfg = await _dataService.GetSettings(guildId, CancellationToken); + var cfg = await _guildData.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); return await SendAboutBotAsync(currentUser, CancellationToken); } - private async Task SendAboutBotAsync(IUser currentUser, CancellationToken ct = default) { + private async Task SendAboutBotAsync(IUser currentUser, CancellationToken ct = default) + { var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers)); foreach (var dev in Developers) + { builder.AppendLine($"@{dev} — {$"AboutDeveloper@{dev}".Localized()}"); + } builder.AppendLine() .AppendLine(Markdown.Bold(Messages.AboutTitleWiki)) @@ -77,6 +87,6 @@ public class AboutCommandGroup : CommandGroup { .WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png") .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 465f8d1..ea03009 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -22,23 +22,25 @@ namespace Boyfriend.Commands; /// Handles commands related to ban management: /ban and /unban. /// [UsedImplicitly] -public class BanCommandGroup : CommandGroup { +public class BanCommandGroup : CommandGroup +{ private readonly IDiscordRestChannelAPI _channelApi; - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; - private readonly IDiscordRestGuildAPI _guildApi; - private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public BanCommandGroup( - ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService, - FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - UtilityService utility) { + ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, + FeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + UtilityService utility) + { _context = context; _channelApi = channelApi; - _dataService = dataService; - _feedbackService = feedbackService; + _guildData = guildData; + _feedback = feedback; _guildApi = guildApi; _userApi = userApi; _utility = utility; @@ -67,23 +69,35 @@ public class BanCommandGroup : CommandGroup { [Description("Ban user")] [UsedImplicitly] public async Task ExecuteBanAsync( - [Description("User to ban")] IUser target, - [Description("Ban reason")] string reason, - [Description("Ban duration")] TimeSpan? duration = null) { + [Description("User to ban")] IUser target, + [Description("Ban reason")] string reason, + [Description("Ban duration")] TimeSpan? duration = null) + { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) + { return Result.FromError(userResult); + } + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) + { return Result.FromError(guildResult); + } - var data = await _dataService.GetData(guild.ID, CancellationToken); + var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await BanUserAsync( @@ -91,39 +105,48 @@ public class BanCommandGroup : CommandGroup { } private async Task BanUserAsync( - IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, - IUser user, IUser currentUser, CancellationToken ct = default) { + IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, + IUser user, IUser currentUser, CancellationToken ct = default) + { var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct); - if (existingBanResult.IsDefined()) { + if (existingBanResult.IsDefined()) + { var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var interactionResult = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Ban", ct); if (!interactionResult.IsSuccess) + { return Result.FromError(interactionResult); + } - if (interactionResult.Entity is not null) { + if (interactionResult.Entity is not null) + { var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); } var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)); if (duration is not null) + { builder.Append( string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(DateTimeOffset.UtcNow.Add(duration.Value)))); + } + var title = string.Format(Messages.UserBanned, target.GetTag()); var description = builder.ToString(); var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct); - if (dmChannelResult.IsDefined(out var dmChannel)) { + if (dmChannelResult.IsDefined(out var dmChannel)) + { var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereBanned) .WithDescription(description) @@ -133,7 +156,10 @@ public class BanCommandGroup : CommandGroup { .Build(); if (!dmEmbed.IsDefined(out var dmBuilt)) + { return Result.FromError(dmEmbed); + } + await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct); } @@ -141,7 +167,10 @@ public class BanCommandGroup : CommandGroup { guild.ID, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), ct: ct); if (!banResult.IsSuccess) + { return Result.FromError(banResult.Error); + } + var memberData = data.GetMemberData(target.ID); memberData.BannedUntil = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; @@ -154,9 +183,11 @@ public class BanCommandGroup : CommandGroup { var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) + { return Result.FromError(logResult.Error); + } - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } /// @@ -172,7 +203,7 @@ public class BanCommandGroup : CommandGroup { /// was unbanned and vice-versa. /// /// - /// + /// [Command("unban")] [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] [DiscordDefaultDMPermission(false)] @@ -182,20 +213,29 @@ public class BanCommandGroup : CommandGroup { [Description("Unban user")] [UsedImplicitly] public async Task ExecuteUnban( - [Description("User to unban")] IUser target, - [Description("Unban reason")] string reason) { + [Description("User to unban")] IUser target, + [Description("Unban reason")] string reason) + { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } + // Needed to get the tag and avatar var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) + { return Result.FromError(userResult); + } - var data = await _dataService.GetData(guildId, CancellationToken); + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await UnbanUserAsync( @@ -203,21 +243,25 @@ public class BanCommandGroup : CommandGroup { } private async Task UnbanUserAsync( - IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, - IUser currentUser, CancellationToken ct = default) { + IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, + IUser currentUser, CancellationToken ct = default) + { var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct); - if (!existingBanResult.IsDefined()) { + if (!existingBanResult.IsDefined()) + { var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); } var unbanResult = await _guildApi.RemoveGuildBanAsync( guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), ct); if (!unbanResult.IsSuccess) + { return Result.FromError(unbanResult.Error); + } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnbanned, target.GetTag()), target) @@ -228,8 +272,10 @@ public class BanCommandGroup : CommandGroup { var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) + { return Result.FromError(logResult.Error); + } - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 0c241e3..f9ac630 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -22,21 +22,23 @@ namespace Boyfriend.Commands; /// Handles the command to clear messages in a channel: /clear. /// [UsedImplicitly] -public class ClearCommandGroup : CommandGroup { +public class ClearCommandGroup : CommandGroup +{ private readonly IDiscordRestChannelAPI _channelApi; - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; - private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public ClearCommandGroup( - IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService dataService, - FeedbackService feedbackService, IDiscordRestUserAPI userApi, UtilityService utility) { + IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService guildData, + FeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) + { _channelApi = channelApi; _context = context; - _dataService = dataService; - _feedbackService = feedbackService; + _guildData = guildData; + _feedback = feedback; _userApi = userApi; _utility = utility; } @@ -59,34 +61,47 @@ public class ClearCommandGroup : CommandGroup { [UsedImplicitly] public async Task ExecuteClear( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] - int amount) { + int amount) + { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } var messagesResult = await _channelApi.GetChannelMessagesAsync( channelId, limit: amount + 1, ct: CancellationToken); if (!messagesResult.IsDefined(out var messages)) + { return Result.FromError(messagesResult); + } + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) + { return Result.FromError(userResult); + } + // The current user's avatar is used when sending messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } - var data = await _dataService.GetData(guildId, CancellationToken); + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await ClearMessagesAsync(amount, data, channelId, messages, user, currentUser, CancellationToken); } private async Task ClearMessagesAsync( - int amount, GuildData data, Snowflake channelId, IReadOnlyList messages, - IUser user, IUser currentUser, CancellationToken ct = default) { + int amount, GuildData data, Snowflake channelId, IReadOnlyList messages, + IUser user, IUser currentUser, CancellationToken ct = default) + { var idList = new List(messages.Count); var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); - for (var i = messages.Count - 1; i >= 1; i--) { // '>= 1' to skip last message ('Boyfriend is thinking...') + for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Boyfriend is thinking...') + { var message = messages[i]; idList.Add(message.ID); builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author))); @@ -99,16 +114,20 @@ public class ClearCommandGroup : CommandGroup { var deleteResult = await _channelApi.BulkDeleteMessagesAsync( channelId, idList, user.GetTag().EncodeHeader(), ct); if (!deleteResult.IsSuccess) + { return Result.FromError(deleteResult.Error); + } var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, currentUser, ColorsList.Red, false, ct); if (!logResult.IsSuccess) + { return Result.FromError(logResult.Error); + } var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) .WithColour(ColorsList.Green).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 51c2a8d..009bfa1 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -11,10 +11,12 @@ namespace Boyfriend.Commands.Events; /// Handles error logging for slash command groups. /// [UsedImplicitly] -public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { +public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent +{ private readonly ILogger _logger; - public ErrorLoggingPostExecutionEvent(ILogger logger) { + public ErrorLoggingPostExecutionEvent(ILogger logger) + { _logger = logger; } @@ -27,11 +29,15 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { /// The cancellation token for this operation. Unused. /// A result which has succeeded. public Task AfterExecutionAsync( - ICommandContext context, IResult commandResult, CancellationToken ct = default) { - if (!commandResult.IsSuccess && !commandResult.Error.IsUserOrEnvironmentError()) { + ICommandContext context, IResult commandResult, CancellationToken ct = default) + { + if (!commandResult.IsSuccess && !commandResult.Error.IsUserOrEnvironmentError()) + { _logger.LogWarning("Error in slash command execution.\n{ErrorMessage}", commandResult.Error.Message); if (commandResult.Error is ExceptionError exerr) + { _logger.LogError(exerr.Exception, "An exception has been thrown"); + } } return Task.FromResult(Result.FromSuccess()); diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs index 7e8b2bb..f6c1e1f 100644 --- a/src/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs @@ -11,10 +11,12 @@ namespace Boyfriend.Commands.Events; /// Handles error logging for slash commands that couldn't be successfully prepared. /// [UsedImplicitly] -public class LoggingPreparationErrorEvent : IPreparationErrorEvent { +public class LoggingPreparationErrorEvent : IPreparationErrorEvent +{ private readonly ILogger _logger; - public LoggingPreparationErrorEvent(ILogger logger) { + public LoggingPreparationErrorEvent(ILogger logger) + { _logger = logger; } @@ -27,11 +29,15 @@ public class LoggingPreparationErrorEvent : IPreparationErrorEvent { /// The cancellation token for this operation. Unused. /// A result which has succeeded. public Task PreparationFailed( - IOperationContext context, IResult preparationResult, CancellationToken ct = default) { - if (!preparationResult.IsSuccess && !preparationResult.Error.IsUserOrEnvironmentError()) { + IOperationContext context, IResult preparationResult, CancellationToken ct = default) + { + if (!preparationResult.IsSuccess && !preparationResult.Error.IsUserOrEnvironmentError()) + { _logger.LogWarning("Error in slash command preparation.\n{ErrorMessage}", preparationResult.Error.Message); if (preparationResult.Error is ExceptionError exerr) + { _logger.LogError(exerr.Exception, "An exception has been thrown"); + } } return Task.FromResult(Result.FromSuccess()); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 5d7909f..45da191 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -20,23 +20,25 @@ namespace Boyfriend.Commands; /// Handles the command to kick members of a guild: /kick. /// [UsedImplicitly] -public class KickCommandGroup : CommandGroup { +public class KickCommandGroup : CommandGroup +{ private readonly IDiscordRestChannelAPI _channelApi; - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; - private readonly IDiscordRestGuildAPI _guildApi; - private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public KickCommandGroup( - ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService, - FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - UtilityService utility) { + ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, + FeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + UtilityService utility) + { _context = context; _channelApi = channelApi; - _dataService = dataService; - _feedbackService = feedbackService; + _guildData = guildData; + _feedback = feedback; _guildApi = guildApi; _userApi = userApi; _utility = utility; @@ -63,30 +65,43 @@ public class KickCommandGroup : CommandGroup { [Description("Kick member")] [UsedImplicitly] public async Task ExecuteKick( - [Description("Member to kick")] IUser target, - [Description("Kick reason")] string reason) { + [Description("Member to kick")] IUser target, + [Description("Kick reason")] string reason) + { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) + { return Result.FromError(userResult); + } + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) + { return Result.FromError(guildResult); + } - var data = await _dataService.GetData(guildId, CancellationToken); + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); - if (!memberResult.IsSuccess) { + if (!memberResult.IsSuccess) + { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } return await KickUserAsync(target, reason, guild, channelId, data, user, currentUser, CancellationToken); @@ -94,21 +109,26 @@ public class KickCommandGroup : CommandGroup { private async Task KickUserAsync( IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser user, IUser currentUser, - CancellationToken ct = default) { + CancellationToken ct = default) + { var interactionResult = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Kick", ct); if (!interactionResult.IsSuccess) + { return Result.FromError(interactionResult); + } - if (interactionResult.Entity is not null) { + if (interactionResult.Entity is not null) + { var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct); - if (dmChannelResult.IsDefined(out var dmChannel)) { + if (dmChannelResult.IsDefined(out var dmChannel)) + { var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereKicked) .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) @@ -118,7 +138,10 @@ public class KickCommandGroup : CommandGroup { .Build(); if (!dmEmbed.IsDefined(out var dmBuilt)) + { return Result.FromError(dmEmbed); + } + await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct); } @@ -126,7 +149,10 @@ public class KickCommandGroup : CommandGroup { guild.ID, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), ct); if (!kickResult.IsSuccess) + { return Result.FromError(kickResult.Error); + } + data.GetMemberData(target.ID).Roles.Clear(); var title = string.Format(Messages.UserKicked, target.GetTag()); @@ -134,12 +160,14 @@ public class KickCommandGroup : CommandGroup { var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) + { return Result.FromError(logResult.Error); + } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserKicked, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index af967b9..a35699b 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -22,20 +22,22 @@ namespace Boyfriend.Commands; /// Handles commands related to mute management: /mute and /unmute. /// [UsedImplicitly] -public class MuteCommandGroup : CommandGroup { - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; +public class MuteCommandGroup : CommandGroup +{ + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; private readonly IDiscordRestGuildAPI _guildApi; - private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public MuteCommandGroup( - ICommandContext context, GuildDataService dataService, FeedbackService feedbackService, - IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) { + ICommandContext context, GuildDataService guildData, FeedbackService feedback, + IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) + { _context = context; - _dataService = dataService; - _feedbackService = feedbackService; + _guildData = guildData; + _feedback = feedback; _guildApi = guildApi; _userApi = userApi; _utility = utility; @@ -64,30 +66,38 @@ public class MuteCommandGroup : CommandGroup { [Description("Mute member")] [UsedImplicitly] public async Task ExecuteMute( - [Description("Member to mute")] IUser target, - [Description("Mute reason")] string reason, - [Description("Mute duration")] TimeSpan duration) { + [Description("Member to mute")] IUser target, + [Description("Mute reason")] string reason, + [Description("Mute duration")] TimeSpan duration) + { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) + { return Result.FromError(userResult); + } - var data = await _dataService.GetData(guildId, CancellationToken); + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); - if (!memberResult.IsSuccess) { + if (!memberResult.IsSuccess) + { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } return await MuteUserAsync( @@ -95,19 +105,23 @@ public class MuteCommandGroup : CommandGroup { } private async Task MuteUserAsync( - IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, - IUser user, IUser currentUser, CancellationToken ct = default) { + IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, + IUser user, IUser currentUser, CancellationToken ct = default) + { var interactionResult = await _utility.CheckInteractionsAsync( guildId, user.ID, target.ID, "Mute", ct); if (!interactionResult.IsSuccess) + { return Result.FromError(interactionResult); + } - if (interactionResult.Entity is not null) { + if (interactionResult.Entity is not null) + { var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var until = DateTimeOffset.UtcNow.Add(duration); // >:) @@ -115,7 +129,9 @@ public class MuteCommandGroup : CommandGroup { guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), communicationDisabledUntil: until, ct: ct); if (!muteResult.IsSuccess) + { return Result.FromError(muteResult.Error); + } var title = string.Format(Messages.UserMuted, target.GetTag()); var description = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)) @@ -126,13 +142,15 @@ public class MuteCommandGroup : CommandGroup { var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) + { return Result.FromError(logResult.Error); + } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserMuted, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } /// @@ -148,7 +166,7 @@ public class MuteCommandGroup : CommandGroup { /// was unmuted and vice-versa. /// /// - /// + /// [Command("unmute", "размут")] [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] [DiscordDefaultDMPermission(false)] @@ -158,30 +176,38 @@ public class MuteCommandGroup : CommandGroup { [Description("Unmute member")] [UsedImplicitly] public async Task ExecuteUnmute( - [Description("Member to unmute")] IUser target, - [Description("Unmute reason")] string reason) { + [Description("Member to unmute")] IUser target, + [Description("Unmute reason")] string reason) + { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } // The current user's avatar is used when sending error messages var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } // Needed to get the tag and avatar var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) + { return Result.FromError(userResult); + } - var data = await _dataService.GetData(guildId, CancellationToken); + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); - if (!memberResult.IsSuccess) { + if (!memberResult.IsSuccess) + { var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } return await UnmuteUserAsync( @@ -189,38 +215,46 @@ public class MuteCommandGroup : CommandGroup { } private async Task UnmuteUserAsync( - IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, - IUser currentUser, CancellationToken ct = default) { + IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, + IUser currentUser, CancellationToken ct = default) + { var interactionResult = await _utility.CheckInteractionsAsync( guildId, user.ID, target.ID, "Unmute", ct); if (!interactionResult.IsSuccess) + { return Result.FromError(interactionResult); + } - if (interactionResult.Entity is not null) { + if (interactionResult.Entity is not null) + { var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) .WithColour(ColorsList.Red).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), communicationDisabledUntil: null, ct: ct); if (!unmuteResult.IsSuccess) + { return Result.FromError(unmuteResult.Error); + } var title = string.Format(Messages.UserUnmuted, target.GetTag()); var description = string.Format(Messages.DescriptionActionReason, reason); var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) + { return Result.FromError(logResult.Error); + } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnmuted, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index f301edb..e52f22c 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -21,22 +21,24 @@ namespace Boyfriend.Commands; /// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping /// [UsedImplicitly] -public class PingCommandGroup : CommandGroup { +public class PingCommandGroup : CommandGroup +{ private readonly IDiscordRestChannelAPI _channelApi; - private readonly DiscordGatewayClient _client; - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; - private readonly IDiscordRestUserAPI _userApi; + private readonly DiscordGatewayClient _client; + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; public PingCommandGroup( - IDiscordRestChannelAPI channelApi, ICommandContext context, DiscordGatewayClient client, - GuildDataService dataService, FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + IDiscordRestChannelAPI channelApi, ICommandContext context, DiscordGatewayClient client, + GuildDataService guildData, FeedbackService feedback, IDiscordRestUserAPI userApi) + { _channelApi = channelApi; _context = context; _client = client; - _dataService = dataService; - _feedbackService = feedbackService; + _guildData = guildData; + _feedback = feedback; _userApi = userApi; } @@ -51,29 +53,39 @@ public class PingCommandGroup : CommandGroup { [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [UsedImplicitly] - public async Task ExecutePingAsync() { + public async Task ExecutePingAsync() + { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } - var cfg = await _dataService.GetSettings(guildId, CancellationToken); + var cfg = await _guildData.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); return await SendLatencyAsync(channelId, currentUser, CancellationToken); } private async Task SendLatencyAsync( - Snowflake channelId, IUser currentUser, CancellationToken ct = default) { + Snowflake channelId, IUser currentUser, CancellationToken ct = default) + { var latency = _client.Latency.TotalMilliseconds; - if (latency is 0) { + if (latency is 0) + { // No heartbeat has occurred, estimate latency from local time and "Boyfriend is thinking..." message var lastMessageResult = await _channelApi.GetChannelMessagesAsync( channelId, limit: 1, ct: ct); if (!lastMessageResult.IsDefined(out var lastMessage)) + { return Result.FromError(lastMessageResult); + } + latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds; } @@ -84,6 +96,6 @@ public class PingCommandGroup : CommandGroup { .WithCurrentTimestamp() .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 9734ab5..3f588aa 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -21,18 +21,20 @@ namespace Boyfriend.Commands; /// Handles the command to manage reminders: /remind /// [UsedImplicitly] -public class RemindCommandGroup : CommandGroup { - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; +public class RemindCommandGroup : CommandGroup +{ + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; + private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; public RemindCommandGroup( - ICommandContext context, GuildDataService dataService, FeedbackService feedbackService, - IDiscordRestUserAPI userApi) { + ICommandContext context, GuildDataService guildData, FeedbackService feedback, + IDiscordRestUserAPI userApi) + { _context = context; - _dataService = dataService; - _feedbackService = feedbackService; + _guildData = guildData; + _feedback = feedback; _userApi = userApi; } @@ -50,27 +52,34 @@ public class RemindCommandGroup : CommandGroup { public async Task ExecuteReminderAsync( [Description("After what period of time mention the reminder")] TimeSpan @in, - [Description("Reminder message")] string message) { + [Description("Reminder message")] string message) + { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) + { return Result.FromError(userResult); + } - var data = await _dataService.GetData(guildId, CancellationToken); + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await AddReminderAsync(@in, message, data, channelId, user, CancellationToken); } private async Task AddReminderAsync( - TimeSpan @in, string message, GuildData data, - Snowflake channelId, IUser user, CancellationToken ct = default) { + TimeSpan @in, string message, GuildData data, + Snowflake channelId, IUser user, CancellationToken ct = default) + { var remindAt = DateTimeOffset.UtcNow.Add(@in); data.GetMemberData(user.ID).Reminders.Add( - new Reminder { + new Reminder + { At = remindAt, Channel = channelId.Value, Text = message @@ -81,6 +90,6 @@ public class RemindCommandGroup : CommandGroup { .WithColour(ColorsList.Green) .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 5a9efad..687d49f 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -23,8 +23,10 @@ namespace Boyfriend.Commands; /// Handles the commands to list and modify per-guild settings: /settings and /settings list. /// [UsedImplicitly] -public class SettingsCommandGroup : CommandGroup { - private static readonly IOption[] AllOptions = { +public class SettingsCommandGroup : CommandGroup +{ + private static readonly IOption[] AllOptions = + { GuildSettings.Language, GuildSettings.WelcomeMessage, GuildSettings.ReceiveStartupMessages, @@ -41,17 +43,18 @@ public class SettingsCommandGroup : CommandGroup { GuildSettings.EventEarlyNotificationOffset }; - private readonly ICommandContext _context; - private readonly GuildDataService _dataService; - private readonly FeedbackService _feedbackService; + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; + private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; public SettingsCommandGroup( - ICommandContext context, GuildDataService dataService, - FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + ICommandContext context, GuildDataService guildData, + FeedbackService feedback, IDiscordRestUserAPI userApi) + { _context = context; - _dataService = dataService; - _feedbackService = feedbackService; + _guildData = guildData; + _feedback = feedback; _userApi = userApi; } @@ -69,21 +72,28 @@ public class SettingsCommandGroup : CommandGroup { [Description("Shows settings list for this server")] [UsedImplicitly] public async Task ExecuteSettingsListAsync( - [Description("Settings list page")] int page) { + [Description("Settings list page")] int page) + { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } - var cfg = await _dataService.GetSettings(guildId, CancellationToken); + var cfg = await _guildData.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); return await SendSettingsListAsync(cfg, currentUser, page, CancellationToken); } - private async Task SendSettingsListAsync(JsonNode cfg, IUser currentUser, int page, CancellationToken ct = default) { + private async Task SendSettingsListAsync(JsonNode cfg, IUser currentUser, int page, + CancellationToken ct = default) + { var description = new StringBuilder(); var footer = new StringBuilder(); @@ -93,38 +103,43 @@ public class SettingsCommandGroup : CommandGroup { var lastOptionOnPage = Math.Min(optionsPerPage * page, AllOptions.Length); var firstOptionOnPage = optionsPerPage * page - optionsPerPage; - if (firstOptionOnPage >= AllOptions.Length) { - var embed = new EmbedBuilder().WithSmallTitle(Messages.PageNotFound, currentUser) + if (firstOptionOnPage >= AllOptions.Length) + { + var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.PageNotFound, currentUser) .WithDescription(string.Format(Messages.PagesAllowed, Markdown.Bold(totalPages.ToString()))) .WithColour(ColorsList.Red) .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); - } else { - footer.Append($"{Messages.Page} {page}/{totalPages} "); - for (var i = 0; i < totalPages; i++) footer.Append(i + 1 == page ? "●" : "○"); - - for (var i = firstOptionOnPage; i < lastOptionOnPage; i++) { - var optionName = AllOptions[i].Name; - var optionValue = AllOptions[i].Display(cfg); - - description.AppendLine($"- {$"Settings{optionName}".Localized()}") - .Append($" - {Markdown.InlineCode(optionName)}: ") - .AppendLine(optionValue); - } - - var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) - .WithDescription(description.ToString()) - .WithColour(ColorsList.Default) - .WithFooter(footer.ToString()) - .Build(); - - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); } + + footer.Append($"{Messages.Page} {page}/{totalPages} "); + for (var i = 0; i < totalPages; i++) + { + footer.Append(i + 1 == page ? "●" : "○"); + } + + for (var i = firstOptionOnPage; i < lastOptionOnPage; i++) + { + var optionName = AllOptions[i].Name; + var optionValue = AllOptions[i].Display(cfg); + + description.AppendLine($"- {$"Settings{optionName}".Localized()}") + .Append($" - {Markdown.InlineCode(optionName)}: ") + .AppendLine(optionValue); + } + + var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) + .WithDescription(description.ToString()) + .WithColour(ColorsList.Default) + .WithFooter(footer.ToString()) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } /// - /// A slash command that modifies per-guild GuildSettings. + /// A slash command that modifies per-guild GuildSettings. /// /// The setting to modify. /// The new value of the setting. @@ -139,33 +154,40 @@ public class SettingsCommandGroup : CommandGroup { public async Task ExecuteSettingsAsync( [Description("The setting whose value you want to change")] string setting, - [Description("Setting value")] string value) { + [Description("Setting value")] string value) + { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } - var data = await _dataService.GetData(guildId, CancellationToken); + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await EditSettingAsync(setting, value, data, currentUser, CancellationToken); } private async Task EditSettingAsync( - string setting, string value, GuildData data, IUser currentUser, CancellationToken ct = default) { + string setting, string value, GuildData data, IUser currentUser, CancellationToken ct = default) + { var option = AllOptions.Single( o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase)); var setResult = option.Set(data.Settings, value); - if (!setResult.IsSuccess) { + if (!setResult.IsSuccess) + { var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser) .WithDescription(setResult.Error.Message) .WithColour(ColorsList.Red) .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var builder = new StringBuilder(); @@ -179,6 +201,6 @@ public class SettingsCommandGroup : CommandGroup { .WithColour(ColorsList.Green) .Build(); - return await _feedbackService.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Data/GuildData.cs b/src/Data/GuildData.cs index 7c81364..5adb869 100644 --- a/src/Data/GuildData.cs +++ b/src/Data/GuildData.cs @@ -7,19 +7,21 @@ namespace Boyfriend.Data; /// Stores information about a guild. This information is not accessible via the Discord API. /// /// This information is stored on disk as a JSON file. -public class GuildData { +public sealed class GuildData +{ public readonly Dictionary MemberData; - public readonly string MemberDataPath; + public readonly string MemberDataPath; public readonly Dictionary ScheduledEvents; - public readonly string ScheduledEventsPath; - public readonly JsonNode Settings; - public readonly string SettingsPath; + public readonly string ScheduledEventsPath; + public readonly JsonNode Settings; + public readonly string SettingsPath; public GuildData( - JsonNode settings, string settingsPath, + JsonNode settings, string settingsPath, Dictionary scheduledEvents, string scheduledEventsPath, - Dictionary memberData, string memberDataPath) { + Dictionary memberData, string memberDataPath) + { Settings = settings; SettingsPath = settingsPath; ScheduledEvents = scheduledEvents; @@ -28,8 +30,12 @@ public class GuildData { MemberDataPath = memberDataPath; } - public MemberData GetMemberData(Snowflake userId) { - if (MemberData.TryGetValue(userId.Value, out var existing)) return existing; + public MemberData GetMemberData(Snowflake userId) + { + if (MemberData.TryGetValue(userId.Value, out var existing)) + { + return existing; + } var newData = new MemberData(userId.Value, null); MemberData.Add(userId.Value, newData); diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index bd5757d..1c94835 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -8,7 +8,8 @@ namespace Boyfriend.Data; /// Contains all per-guild settings that can be set by a member /// with using the /settings command ///
-public static class GuildSettings { +public static class GuildSettings +{ public static readonly LanguageOption Language = new("Language", "en"); /// @@ -56,9 +57,9 @@ public static class GuildSettings { public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel"); public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel"); - public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); - public static readonly SnowflakeOption MuteRole = new("MuteRole"); - public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole"); + public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); + public static readonly SnowflakeOption MuteRole = new("MuteRole"); + public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole"); /// /// Controls the amount of time before a scheduled event to send a reminder in . diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 7d49ec7..f028938 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -3,14 +3,16 @@ namespace Boyfriend.Data; /// /// Stores information about a member /// -public class MemberData { - public MemberData(ulong id, DateTimeOffset? bannedUntil) { +public sealed class MemberData +{ + public MemberData(ulong id, DateTimeOffset? bannedUntil) + { Id = id; BannedUntil = bannedUntil; } - public ulong Id { get; } + public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } - public List Roles { get; set; } = new(); - public List Reminders { get; } = new(); + public List Roles { get; set; } = new(); + public List Reminders { get; } = new(); } diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs index edf6b26..51cd3a1 100644 --- a/src/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -3,25 +3,31 @@ using Remora.Results; namespace Boyfriend.Data.Options; -public class BoolOption : Option { +public sealed class BoolOption : Option +{ public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { } - public override string Display(JsonNode settings) { + public override string Display(JsonNode settings) + { return Get(settings) ? Messages.Yes : Messages.No; } - public override Result Set(JsonNode settings, string from) { + public override Result Set(JsonNode settings, string from) + { if (!TryParseBool(from, out var value)) + { return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); + } settings[Name] = value; return Result.FromSuccess(); } - private static bool TryParseBool(string from, out bool value) { - from = from.ToLowerInvariant(); + private static bool TryParseBool(string from, out bool value) + { value = false; - switch (from) { + switch (from.ToLowerInvariant()) + { case "true" or "1" or "y" or "yes" or "д" or "да": value = true; return true; diff --git a/src/Data/Options/IOption.cs b/src/Data/Options/IOption.cs index fc0f747..6f435e5 100644 --- a/src/Data/Options/IOption.cs +++ b/src/Data/Options/IOption.cs @@ -3,8 +3,9 @@ using Remora.Results; namespace Boyfriend.Data.Options; -public interface IOption { +public interface IOption +{ string Name { get; } string Display(JsonNode settings); - Result Set(JsonNode settings, string from); + Result Set(JsonNode settings, string from); } diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs index 5c50899..a82d7f6 100644 --- a/src/Data/Options/LanguageOption.cs +++ b/src/Data/Options/LanguageOption.cs @@ -6,8 +6,10 @@ using Remora.Results; namespace Boyfriend.Data.Options; /// -public class LanguageOption : Option { - private static readonly Dictionary CultureInfoCache = new() { +public sealed class LanguageOption : Option +{ + private static readonly Dictionary CultureInfoCache = new() + { { "en", new CultureInfo("en-US") }, { "ru", new CultureInfo("ru-RU") }, { "mctaylors-ru", new CultureInfo("tt-RU") } @@ -15,18 +17,21 @@ public class LanguageOption : Option { public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { } - public override string Display(JsonNode settings) { + public override string Display(JsonNode settings) + { return Markdown.InlineCode(settings[Name]?.GetValue() ?? "en"); } /// - public override CultureInfo Get(JsonNode settings) { + public override CultureInfo Get(JsonNode settings) + { var property = settings[Name]; return property != null ? CultureInfoCache[property.GetValue()] : DefaultValue; } /// - public override Result Set(JsonNode settings, string from) { + public override Result Set(JsonNode settings, string from) + { return CultureInfoCache.ContainsKey(from.ToLowerInvariant()) ? base.Set(settings, from.ToLowerInvariant()) : new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported); diff --git a/src/Data/Options/Option.cs b/src/Data/Options/Option.cs index 742d3a9..c96b6ac 100644 --- a/src/Data/Options/Option.cs +++ b/src/Data/Options/Option.cs @@ -9,18 +9,21 @@ namespace Boyfriend.Data.Options; /// /// The type of the option. public class Option : IOption -where T : notnull { - internal readonly T DefaultValue; + where T : notnull +{ + protected readonly T DefaultValue; - public Option(string name, T defaultValue) { + public Option(string name, T defaultValue) + { Name = name; DefaultValue = defaultValue; } public string Name { get; } - public virtual string Display(JsonNode settings) { - return Markdown.InlineCode(Get(settings).ToString()!); + public virtual string Display(JsonNode settings) + { + return Markdown.InlineCode(Get(settings).ToString() ?? throw new InvalidOperationException()); } /// @@ -29,7 +32,8 @@ where T : notnull { /// The to set the value to. /// The string from which the new value of the option will be parsed. /// A value setting result which may or may not have succeeded. - public virtual Result Set(JsonNode settings, string from) { + public virtual Result Set(JsonNode settings, string from) + { settings[Name] = from; return Result.FromSuccess(); } @@ -39,7 +43,8 @@ where T : notnull { /// /// The to get the value from. /// The value of the option. - public virtual T Get(JsonNode settings) { + public virtual T Get(JsonNode settings) + { var property = settings[Name]; return property != null ? property.GetValue() : DefaultValue; } diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs index 66dfa8c..7391b00 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -6,21 +6,29 @@ using Remora.Results; namespace Boyfriend.Data.Options; -public partial class SnowflakeOption : Option { +public sealed partial class SnowflakeOption : Option +{ public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { } - public override string Display(JsonNode settings) { - return Name.EndsWith("Channel") ? Mention.Channel(Get(settings)) : Mention.Role(Get(settings)); + public override string Display(JsonNode settings) + { + return Name.EndsWith("Channel", StringComparison.Ordinal) + ? Mention.Channel(Get(settings)) + : Mention.Role(Get(settings)); } - public override Snowflake Get(JsonNode settings) { + public override Snowflake Get(JsonNode settings) + { var property = settings[Name]; return property != null ? property.GetValue().ToSnowflake() : DefaultValue; } - public override Result Set(JsonNode settings, string from) { + public override Result Set(JsonNode settings, string from) + { if (!ulong.TryParse(NonNumbers().Replace(from, ""), out var parsed)) + { return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); + } settings[Name] = parsed; return Result.FromSuccess(); diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index 3aa1fd5..5e39cf0 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -4,25 +4,31 @@ using Remora.Results; namespace Boyfriend.Data.Options; -public class TimeSpanOption : Option { +public sealed class TimeSpanOption : Option +{ private static readonly TimeSpanParser Parser = new(); public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { } - public override TimeSpan Get(JsonNode settings) { + public override TimeSpan Get(JsonNode settings) + { var property = settings[Name]; return property != null ? ParseTimeSpan(property.GetValue()).Entity : DefaultValue; } - public override Result Set(JsonNode settings, string from) { + public override Result Set(JsonNode settings, string from) + { if (!ParseTimeSpan(from).IsDefined(out var span)) + { return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); + } settings[Name] = span.ToString(); return Result.FromSuccess(); } - private static Result ParseTimeSpan(string from) { + private static Result ParseTimeSpan(string from) + { return Parser.TryParseAsync(from).AsTask().GetAwaiter().GetResult(); } } diff --git a/src/Data/Reminder.cs b/src/Data/Reminder.cs index 2246b5e..a332003 100644 --- a/src/Data/Reminder.cs +++ b/src/Data/Reminder.cs @@ -1,7 +1,8 @@ namespace Boyfriend.Data; -public struct Reminder { +public struct Reminder +{ public DateTimeOffset At; - public string Text; - public ulong Channel; + public string Text; + public ulong Channel; } diff --git a/src/Data/ScheduledEventData.cs b/src/Data/ScheduledEventData.cs index 661eed0..29075f5 100644 --- a/src/Data/ScheduledEventData.cs +++ b/src/Data/ScheduledEventData.cs @@ -6,12 +6,14 @@ namespace Boyfriend.Data; /// Stores information about scheduled events. This information is not provided by the Discord API. /// /// This information is stored on disk as a JSON file. -public class ScheduledEventData { - public ScheduledEventData(GuildScheduledEventStatus status) { +public sealed class ScheduledEventData +{ + public ScheduledEventData(GuildScheduledEventStatus status) + { Status = status; } - public bool EarlyNotificationSent { get; set; } - public DateTimeOffset? ActualStartTime { get; set; } - public GuildScheduledEventStatus Status { get; set; } + public bool EarlyNotificationSent { get; set; } + public DateTimeOffset? ActualStartTime { get; set; } + public GuildScheduledEventStatus Status { get; set; } } diff --git a/src/Extensions.cs b/src/Extensions.cs index 2fa342c..df2e548 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -14,14 +14,16 @@ using Remora.Results; namespace Boyfriend; -public static class Extensions { +public static class Extensions +{ /// /// Adds a footer with the 's avatar and tag (@username or username#0000). /// /// The builder to add the footer to. /// The user whose tag and avatar to add. /// The builder with the added footer. - public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) { + public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) + { var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); var avatarUrl = avatarUrlResult.IsSuccess ? avatarUrlResult.Entity.AbsoluteUri @@ -36,7 +38,8 @@ public static class Extensions { /// The builder to add the footer to. /// The user that performed the action whose tag and avatar to use. /// The builder with the added footer. - public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) { + public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) + { var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); var avatarUrl = avatarUrlResult.IsSuccess ? avatarUrlResult.Entity.AbsoluteUri @@ -54,9 +57,11 @@ public static class Extensions { /// The user whose avatar to use in the small title. /// The builder with the added small title in the author field. public static EmbedBuilder WithSmallTitle( - this EmbedBuilder builder, string text, IUser? avatarSource = null) { + this EmbedBuilder builder, string text, IUser? avatarSource = null) + { Uri? avatarUrl = null; - if (avatarSource is not null) { + if (avatarSource is not null) + { var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); avatarUrl = avatarUrlResult.IsSuccess @@ -74,7 +79,8 @@ public static class Extensions { /// The builder to add the footer to. /// The guild whose name and icon to use. /// The builder with the added footer. - public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) { + public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) + { var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); var iconUrl = iconUrlResult.IsSuccess ? iconUrlResult.Entity.AbsoluteUri @@ -89,7 +95,8 @@ public static class Extensions { /// The builder to add the title to. /// The guild whose name and icon to use. /// The builder with the added title. - public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild) { + public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild) + { var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); var iconUrl = iconUrlResult.IsSuccess ? iconUrlResult.Entity.AbsoluteUri @@ -107,8 +114,12 @@ public static class Extensions { /// The Optional containing the image hash. /// The builder with the added cover image. public static EmbedBuilder WithEventCover( - this EmbedBuilder builder, Snowflake eventId, Optional imageHashOptional) { - if (!imageHashOptional.IsDefined(out var imageHash)) return builder; + this EmbedBuilder builder, Snowflake eventId, Optional imageHashOptional) + { + if (!imageHashOptional.IsDefined(out var imageHash)) + { + return builder; + } var iconUrlResult = CDN.GetGuildScheduledEventCoverUrl(eventId, imageHash, imageSize: 1024); return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder; @@ -120,25 +131,31 @@ public static class Extensions { /// /// The string to sanitize. /// The sanitized string that can be safely used in . - private static string SanitizeForBlockCode(this string s) { + private static string SanitizeForBlockCode(this string s) + { return s.Replace("```", "​`​`​`​"); } /// - /// Sanitizes a string (see ) and formats the string to use Markdown Block Code formatting with a specified - /// language for syntax highlighting. + /// Sanitizes a string (see ) and formats the string to use Markdown Block Code + /// formatting with a specified + /// language for syntax highlighting. /// /// The string to sanitize and format. /// - /// The sanitized string formatted to use Markdown Block Code with a specified - /// language for syntax highlighting. - public static string InBlockCode(this string s, string language = "") { + /// + /// The sanitized string formatted to use Markdown Block Code with a specified + /// language for syntax highlighting. + /// + public static string InBlockCode(this string s, string language = "") + { s = s.SanitizeForBlockCode(); return - $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith("`") || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; + $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith("`", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; } - public static string Localized(this string key) { + public static string Localized(this string key) + { return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key; } @@ -148,41 +165,56 @@ public static class Extensions { /// Used when encountering "Request headers must contain only ASCII characters". /// The string to encode. /// An encoded string with spaces kept intact. - public static string EncodeHeader(this string s) { + public static string EncodeHeader(this string s) + { return WebUtility.UrlEncode(s).Replace('+', ' '); } - public static string AsMarkdown(this DiffPaneModel model) { + public static string AsMarkdown(this DiffPaneModel model) + { var builder = new StringBuilder(); - foreach (var line in model.Lines) { + foreach (var line in model.Lines) + { if (line.Type is ChangeType.Deleted) + { builder.Append("-- "); + } + if (line.Type is ChangeType.Inserted) + { builder.Append("++ "); + } + if (line.Type is not ChangeType.Imaginary) + { builder.AppendLine(line.Text); + } } return InBlockCode(builder.ToString(), "diff"); } - public static string GetTag(this IUser user) { + public static string GetTag(this IUser user) + { return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}"; } - public static Snowflake ToSnowflake(this ulong id) { + public static Snowflake ToSnowflake(this ulong id) + { return DiscordSnowflake.New(id); } public static TResult? MaxOrDefault( - this IEnumerable source, Func selector) { + this IEnumerable source, Func selector) + { var list = source.ToList(); return list.Any() ? list.Max(selector) : default; } public static bool TryGetContextIDs( - this ICommandContext context, out Snowflake guildId, - out Snowflake channelId, out Snowflake userId) { + this ICommandContext context, out Snowflake guildId, + out Snowflake channelId, out Snowflake userId) + { channelId = default; userId = default; return context.TryGetGuildID(out guildId) @@ -195,7 +227,8 @@ public static class Extensions { /// /// The Snowflake to check. /// true if the Snowflake has no value set or it's set to 0, false otherwise. - public static bool Empty(this Snowflake snowflake) { + public static bool Empty(this Snowflake snowflake) + { return snowflake.Value is 0; } @@ -210,14 +243,18 @@ public static class Extensions { /// otherwise. /// /// - public static bool EmptyOrEqualTo(this Snowflake snowflake, Snowflake anotherSnowflake) { + public static bool EmptyOrEqualTo(this Snowflake snowflake, Snowflake anotherSnowflake) + { return snowflake.Empty() || snowflake == anotherSnowflake; } public static async Task SendContextualEmbedResultAsync( - this FeedbackService feedback, Result embedResult, CancellationToken ct = default) { + this FeedbackService feedback, Result embedResult, CancellationToken ct = default) + { if (!embedResult.IsDefined(out var embed)) + { return Result.FromError(embedResult); + } return (Result)await feedback.SendContextualEmbedAsync(embed, ct: ct); } diff --git a/src/InteractionResponders.cs b/src/InteractionResponders.cs index 977f00f..d3916b0 100644 --- a/src/InteractionResponders.cs +++ b/src/InteractionResponders.cs @@ -11,11 +11,13 @@ namespace Boyfriend; /// Handles responding to various interactions. /// [UsedImplicitly] -public class InteractionResponders : InteractionGroup { - private readonly FeedbackService _feedbackService; +public class InteractionResponders : InteractionGroup +{ + private readonly FeedbackService _feedback; - public InteractionResponders(FeedbackService feedbackService) { - _feedbackService = feedbackService; + public InteractionResponders(FeedbackService feedback) + { + _feedback = feedback; } /// @@ -25,11 +27,15 @@ public class InteractionResponders : InteractionGroup { /// An ephemeral feedback sending result which may or may not have succeeded. [Button("scheduled-event-details")] [UsedImplicitly] - public async Task OnStatefulButtonClicked(string? state = null) { - if (state is null) return new ArgumentNullError(nameof(state)); + public async Task OnStatefulButtonClicked(string? state = null) + { + if (state is null) + { + return new ArgumentNullError(nameof(state)); + } var idArray = state.Split(':'); - return (Result)await _feedbackService.SendContextualAsync( + return (Result)await _feedback.SendContextualAsync( $"https://discord.com/events/{idArray[0]}/{idArray[1]}", options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral), ct: CancellationToken); } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 081f526..547e5ac 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -16,35 +16,49 @@ namespace Boyfriend.Responders; /// has enabled /// [UsedImplicitly] -public class GuildLoadedResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; +public class GuildLoadedResponder : IResponder +{ + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _guildData; private readonly ILogger _logger; - private readonly IDiscordRestUserAPI _userApi; + private readonly IDiscordRestUserAPI _userApi; public GuildLoadedResponder( - IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger logger, - IDiscordRestUserAPI userApi) { + IDiscordRestChannelAPI channelApi, GuildDataService guildData, ILogger logger, + IDiscordRestUserAPI userApi) + { _channelApi = channelApi; - _dataService = dataService; + _guildData = guildData; _logger = logger; _userApi = userApi; } - public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild is not IAvailableGuild + public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) + { + if (!gatewayEvent.Guild.IsT0) // Guild is not IAvailableGuild + { + return Result.FromSuccess(); + } var guild = gatewayEvent.Guild.AsT0; _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); - var cfg = await _dataService.GetSettings(guild.ID, ct); + var cfg = await _guildData.GetSettings(guild.ID, ct); if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) + { return Result.FromSuccess(); + } + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + { return Result.FromSuccess(); + } var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } Messages.Culture = GuildSettings.Language.Get(cfg); var i = Random.Shared.Next(1, 4); @@ -55,7 +69,10 @@ public class GuildLoadedResponder : IResponder { .WithCurrentTimestamp() .WithColour(ColorsList.Blue) .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } return (Result)await _channelApi.CreateMessageAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct); diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 5357363..bb60b0b 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -15,31 +15,44 @@ namespace Boyfriend.Responders; /// /// [UsedImplicitly] -public class GuildMemberJoinedResponder : IResponder { +public class GuildMemberJoinedResponder : IResponder +{ private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; public GuildMemberJoinedResponder( - IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) { + IDiscordRestChannelAPI channelApi, GuildDataService guildData, IDiscordRestGuildAPI guildApi) + { _channelApi = channelApi; - _dataService = dataService; + _guildData = guildData; _guildApi = guildApi; } - public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) { + public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) + { if (!gatewayEvent.User.IsDefined(out var user)) + { return new ArgumentNullError(nameof(gatewayEvent.User)); - var data = await _dataService.GetData(gatewayEvent.GuildID, ct); + } + + var data = await _guildData.GetData(gatewayEvent.GuildID, ct); var cfg = data.Settings; if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") + { return Result.FromSuccess(); - if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { + } + + if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) + { var result = await _guildApi.ModifyGuildMemberAsync( gatewayEvent.GuildID, user.ID, roles: data.GetMemberData(user.ID).Roles.ConvertAll(r => r.ToSnowflake()), ct: ct); - if (!result.IsSuccess) return Result.FromError(result.Error); + if (!result.IsSuccess) + { + return Result.FromError(result.Error); + } } Messages.Culture = GuildSettings.Language.Get(cfg); @@ -48,7 +61,10 @@ public class GuildMemberJoinedResponder : IResponder { : GuildSettings.WelcomeMessage.Get(cfg); var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); - if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } var embed = new EmbedBuilder() .WithSmallTitle(string.Format(welcomeMessage, user.GetTag(), guild.Name), user) @@ -56,7 +72,10 @@ public class GuildMemberJoinedResponder : IResponder { .WithTimestamp(gatewayEvent.JoinedAt) .WithColour(ColorsList.Green) .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } return (Result)await _channelApi.CreateMessageAsync( GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built }, diff --git a/src/Responders/GuildMemberRolesUpdatedResponder.cs b/src/Responders/GuildMemberRolesUpdatedResponder.cs index b61ce32..dbd8b3a 100644 --- a/src/Responders/GuildMemberRolesUpdatedResponder.cs +++ b/src/Responders/GuildMemberRolesUpdatedResponder.cs @@ -11,15 +11,18 @@ namespace Boyfriend.Responders; /// Handles updating when a guild member is updated. /// [UsedImplicitly] -public class GuildMemberUpdateResponder : IResponder { - private readonly GuildDataService _dataService; +public class GuildMemberUpdateResponder : IResponder +{ + private readonly GuildDataService _guildData; - public GuildMemberUpdateResponder(GuildDataService dataService) { - _dataService = dataService; + public GuildMemberUpdateResponder(GuildDataService guildData) + { + _guildData = guildData; } - public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) { - var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); + public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) + { + var memberData = await _guildData.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value); return Result.FromSuccess(); } diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 652631e..3611f80 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -16,43 +16,68 @@ namespace Boyfriend.Responders; /// to a guild's if one is set. /// [UsedImplicitly] -public class MessageDeletedResponder : IResponder { +public class MessageDeletedResponder : IResponder +{ private readonly IDiscordRestAuditLogAPI _auditLogApi; - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestUserAPI _userApi; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; public MessageDeletedResponder( IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi, - GuildDataService dataService, IDiscordRestUserAPI userApi) { + GuildDataService guildData, IDiscordRestUserAPI userApi) + { _auditLogApi = auditLogApi; _channelApi = channelApi; - _dataService = dataService; + _guildData = guildData; _userApi = userApi; } - public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); + public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) + { + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) + { + return Result.FromSuccess(); + } - var cfg = await _dataService.GetSettings(guildId, ct); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) return Result.FromSuccess(); + var cfg = await _guildData.GetSettings(guildId, ct); + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + { + return Result.FromSuccess(); + } var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); - if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); - if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess(); + if (!messageResult.IsDefined(out var message)) + { + return Result.FromError(messageResult); + } + + if (string.IsNullOrWhiteSpace(message.Content)) + { + return Result.FromSuccess(); + } var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); - if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); + if (!auditLogResult.IsDefined(out var auditLogPage)) + { + return Result.FromError(auditLogResult); + } var auditLog = auditLogPage.AuditLogEntries.Single(); var userResult = Result.FromSuccess(message.Author); - if (auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID + if (auditLog.UserID is not null + && auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) - userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); + { + userResult = await _userApi.GetUserAsync(auditLog.UserID.Value, ct); + } - if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); + if (!userResult.IsDefined(out var user)) + { + return Result.FromError(userResult); + } Messages.Culture = GuildSettings.Language.Get(cfg); @@ -67,7 +92,10 @@ public class MessageDeletedResponder : IResponder { .WithTimestamp(message.Timestamp) .WithColour(ColorsList.Red) .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } return (Result)await _channelApi.CreateMessageAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index e7ec215..7e02f9f 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -18,42 +18,68 @@ namespace Boyfriend.Responders; /// to a guild's if one is set. /// [UsedImplicitly] -public class MessageEditedResponder : IResponder { - private readonly CacheService _cacheService; +public class MessageEditedResponder : IResponder +{ + private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestUserAPI _userApi; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; public MessageEditedResponder( - CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService, - IDiscordRestUserAPI userApi) { + CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService guildData, + IDiscordRestUserAPI userApi) + { _cacheService = cacheService; _channelApi = channelApi; - _dataService = dataService; + _guildData = guildData; _userApi = userApi; } - public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { + public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) + { if (!gatewayEvent.GuildID.IsDefined(out var guildId)) + { return Result.FromSuccess(); - var cfg = await _dataService.GetSettings(guildId, ct); + } + + var cfg = await _guildData.GetSettings(guildId, ct); if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + { return Result.FromSuccess(); + } + if (!gatewayEvent.Content.IsDefined(out var newContent)) + { return Result.FromSuccess(); + } + if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) + { return Result.FromSuccess(); // The message wasn't actually edited + } if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) + { return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); + } + if (!gatewayEvent.ID.IsDefined(out var messageId)) + { return new ArgumentNullError(nameof(gatewayEvent.ID)); + } var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); var messageResult = await _cacheService.TryGetValueAsync( cacheKey, ct); - if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); - if (message.Content == newContent) return Result.FromSuccess(); + if (!messageResult.IsDefined(out var message)) + { + return Result.FromError(messageResult); + } + + if (message.Content == newContent) + { + return Result.FromSuccess(); + } // Custom event responders are called earlier than responders responsible for message caching // This means that subsequent edit logs may contain the wrong content @@ -67,7 +93,10 @@ public class MessageEditedResponder : IResponder { _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } var diff = InlineDiffBuilder.Diff(message.Content, newContent); @@ -80,7 +109,10 @@ public class MessageEditedResponder : IResponder { .WithTimestamp(timestamp.Value) .WithColour(ColorsList.Yellow) .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } return (Result)await _channelApi.CreateMessageAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs index a45a2f4..cadef5f 100644 --- a/src/Responders/MessageReceivedResponder.cs +++ b/src/Responders/MessageReceivedResponder.cs @@ -11,23 +11,27 @@ namespace Boyfriend.Responders; /// Handles sending replies to easter egg messages. /// [UsedImplicitly] -public class MessageCreateResponder : IResponder { +public class MessageCreateResponder : IResponder +{ private readonly IDiscordRestChannelAPI _channelApi; - public MessageCreateResponder(IDiscordRestChannelAPI channelApi) { + public MessageCreateResponder(IDiscordRestChannelAPI channelApi) + { _channelApi = channelApi; } - public Task RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) { + public Task RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) + { _ = _channelApi.CreateMessageAsync( - gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content.ToLowerInvariant() switch { - "whoami" => "`nobody`", + gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content.ToLowerInvariant() switch + { + "whoami" => "`nobody`", "сука !!" => "`root`", - "воооо" => "`removing /...`", + "воооо" => "`removing /...`", "пон" => "https://cdn.upload.systems/uploads/2LNfUSwM.jpg", "++++" => "#", - "осу" => "https://github.com/ppy/osu", - _ => default(Optional) + "осу" => "https://github.com/ppy/osu", + _ => default(Optional) }); return Task.FromResult(Result.FromSuccess()); } diff --git a/src/Responders/ScheduledEventCancelledResponder.cs b/src/Responders/ScheduledEventCancelledResponder.cs index 86453ef..c35128a 100644 --- a/src/Responders/ScheduledEventCancelledResponder.cs +++ b/src/Responders/ScheduledEventCancelledResponder.cs @@ -14,21 +14,26 @@ namespace Boyfriend.Responders; /// in a guild's if one is set. /// [UsedImplicitly] -public class GuildScheduledEventDeleteResponder : IResponder { +public class GuildScheduledEventDeleteResponder : IResponder +{ private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; + private readonly GuildDataService _guildData; - public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) { + public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService guildData) + { _channelApi = channelApi; - _dataService = dataService; + _guildData = guildData; } - public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) { - var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); + public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) + { + var guildData = await _guildData.GetData(gatewayEvent.GuildID, ct); guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty()) + { return Result.FromSuccess(); + } var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name)) @@ -37,7 +42,10 @@ public class GuildScheduledEventDeleteResponder : IResponder /// Handles saving, loading, initializing and providing . /// -public class GuildDataService : IHostedService { +public sealed class GuildDataService : IHostedService +{ private readonly ConcurrentDictionary _datas = new(); - private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestGuildAPI _guildApi; // https://github.com/dotnet/aspnetcore/issues/39139 public GuildDataService( - IHostApplicationLifetime lifetime, IDiscordRestGuildAPI guildApi) { + IHostApplicationLifetime lifetime, IDiscordRestGuildAPI guildApi) + { _guildApi = guildApi; lifetime.ApplicationStopping.Register(ApplicationStopping); } - public Task StartAsync(CancellationToken ct) { + public Task StartAsync(CancellationToken ct) + { return Task.CompletedTask; } - public Task StopAsync(CancellationToken ct) { + public Task StopAsync(CancellationToken ct) + { return Task.CompletedTask; } - private void ApplicationStopping() { + private void ApplicationStopping() + { SaveAsync(CancellationToken.None).GetAwaiter().GetResult(); } - private async Task SaveAsync(CancellationToken ct) { + private async Task SaveAsync(CancellationToken ct) + { var tasks = new List(); - foreach (var data in _datas.Values) { + foreach (var data in _datas.Values) + { await using var settingsStream = File.OpenWrite(data.SettingsPath); tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct)); await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath); tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct)); - foreach (var memberData in data.MemberData.Values) { + foreach (var memberData in data.MemberData.Values) + { await using var memberDataStream = File.OpenWrite($"{data.MemberDataPath}/{memberData.Id}.json"); tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct)); } @@ -52,19 +60,36 @@ public class GuildDataService : IHostedService { await Task.WhenAll(tasks); } - public async Task GetData(Snowflake guildId, CancellationToken ct = default) { + public async Task GetData(Snowflake guildId, CancellationToken ct = default) + { return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); } - private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) { + private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) + { var idString = $"{guildId}"; var memberDataPath = $"{guildId}/MemberData"; var settingsPath = $"{guildId}/Settings.json"; var scheduledEventsPath = $"{guildId}/ScheduledEvents.json"; - if (!Directory.Exists(idString)) Directory.CreateDirectory(idString); - if (!Directory.Exists(memberDataPath)) Directory.CreateDirectory(memberDataPath); - if (!File.Exists(settingsPath)) await File.WriteAllTextAsync(settingsPath, "{}", ct); - if (!File.Exists(scheduledEventsPath)) await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); + if (!Directory.Exists(idString)) + { + Directory.CreateDirectory(idString); + } + + if (!Directory.Exists(memberDataPath)) + { + Directory.CreateDirectory(memberDataPath); + } + + if (!File.Exists(settingsPath)) + { + await File.WriteAllTextAsync(settingsPath, "{}", ct); + } + + if (!File.Exists(scheduledEventsPath)) + { + await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); + } await using var settingsStream = File.OpenRead(settingsPath); var jsonSettings @@ -76,13 +101,20 @@ public class GuildDataService : IHostedService { eventsStream, cancellationToken: ct); var memberData = new Dictionary(); - foreach (var dataPath in Directory.GetFiles(memberDataPath)) { + foreach (var dataPath in Directory.GetFiles(memberDataPath)) + { await using var dataStream = File.OpenRead(dataPath); var data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); - if (data is null) continue; + if (data is null) + { + continue; + } + var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct); if (memberResult.IsSuccess) + { data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value); + } memberData.Add(data.Id, data); } @@ -91,19 +123,26 @@ public class GuildDataService : IHostedService { jsonSettings ?? new JsonObject(), settingsPath, await events ?? new Dictionary(), scheduledEventsPath, memberData, memberDataPath); - while (!_datas.ContainsKey(guildId)) _datas.TryAdd(guildId, finalData); + while (!_datas.ContainsKey(guildId)) + { + _datas.TryAdd(guildId, finalData); + } + return finalData; } - public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) { + public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) + { return (await GetData(guildId, ct)).Settings; } - public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) { + public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) + { return (await GetData(guildId, ct)).GetMemberData(userId); } - public ICollection GetGuildIds() { + public ICollection GetGuildIds() + { return _datas.Keys; } } diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs index da06a99..752f3c2 100644 --- a/src/Services/GuildUpdateService.cs +++ b/src/Services/GuildUpdateService.cs @@ -20,8 +20,10 @@ namespace Boyfriend.Services; /// /// Handles executing guild updates (also called "ticks") once per second. /// -public partial class GuildUpdateService : BackgroundService { - private static readonly (string Name, TimeSpan Duration)[] SongList = { +public sealed partial class GuildUpdateService : BackgroundService +{ + private static readonly (string Name, TimeSpan Duration)[] SongList = + { ("UNDEAD CORPORATION - The Empress", new TimeSpan(0, 4, 34)), ("UNDEAD CORPORATION - Everything will freeze", new TimeSpan(0, 3, 17)), ("Splatoon 3 - Rockagilly Blues (Yoko & the Gold Bazookas)", new TimeSpan(0, 3, 37)), @@ -36,7 +38,8 @@ public partial class GuildUpdateService : BackgroundService { ("Noisecream - Mist of Rage", new TimeSpan(0, 2, 25)) }; - private static readonly string[] GenericNicknames = { + private static readonly string[] GenericNicknames = + { "Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary", "Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck", "Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane", @@ -48,25 +51,26 @@ public partial class GuildUpdateService : BackgroundService { private readonly List _activityList = new(1) { new Activity("with Remora.Discord", ActivityType.Game) }; - private readonly IDiscordRestChannelAPI _channelApi; - private readonly DiscordGatewayClient _client; - private readonly GuildDataService _dataService; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly DiscordGatewayClient _client; private readonly IDiscordRestGuildScheduledEventAPI _eventApi; - private readonly IDiscordRestGuildAPI _guildApi; - private readonly ILogger _logger; - private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + private readonly ILogger _logger; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; private DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; - private uint _nextSongIndex; + private uint _nextSongIndex; public GuildUpdateService( - IDiscordRestChannelAPI channelApi, DiscordGatewayClient client, GuildDataService dataService, + IDiscordRestChannelAPI channelApi, DiscordGatewayClient client, GuildDataService guildData, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, ILogger logger, - IDiscordRestUserAPI userApi, UtilityService utility) { + IDiscordRestUserAPI userApi, UtilityService utility) + { _channelApi = channelApi; _client = client; - _dataService = dataService; + _guildData = guildData; _eventApi = eventApi; _guildApi = guildApi; _logger = logger; @@ -76,17 +80,20 @@ public partial class GuildUpdateService : BackgroundService { /// /// Activates a periodic timer with a 1 second interval and adds guild update tasks on each timer tick. - /// Additionally, updates the current presence with songs from . + /// Additionally, updates the current presence with songs from . /// /// If update tasks take longer than 1 second, the next timer tick will be skipped. /// The cancellation token for this operation. - protected override async Task ExecuteAsync(CancellationToken ct) { + protected override async Task ExecuteAsync(CancellationToken ct) + { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); var tasks = new List(); - while (await timer.WaitForNextTickAsync(ct)) { - var guildIds = _dataService.GetGuildIds(); - if (guildIds.Count > 0 && DateTimeOffset.UtcNow >= _nextSongAt) { + while (await timer.WaitForNextTickAsync(ct)) + { + var guildIds = _guildData.GetGuildIds(); + if (guildIds.Count > 0 && DateTimeOffset.UtcNow >= _nextSongAt) + { var nextSong = SongList[_nextSongIndex]; _activityList[0] = new Activity(nextSong.Name, ActivityType.Listening); _client.SubmitCommand( @@ -94,7 +101,10 @@ public partial class GuildUpdateService : BackgroundService { UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList)); _nextSongAt = DateTimeOffset.UtcNow.Add(nextSong.Duration); _nextSongIndex++; - if (_nextSongIndex >= SongList.Length) _nextSongIndex = 0; + if (_nextSongIndex >= SongList.Length) + { + _nextSongIndex = 0; + } } tasks.AddRange(guildIds.Select(id => TickGuildAsync(id, ct))); @@ -111,9 +121,9 @@ public partial class GuildUpdateService : BackgroundService { /// This method does the following: /// /// Automatically unbans users once their ban period has expired. - /// Automatically grants members the guild's if one is set. + /// Automatically grants members the guild's if one is set. /// Sends reminders about an upcoming scheduled event. - /// Automatically starts scheduled events if is enabled. + /// Automatically starts scheduled events if is enabled. /// Sends scheduled event start notifications. /// Sends scheduled event completion notifications. /// Sends reminders to members. @@ -129,41 +139,62 @@ public partial class GuildUpdateService : BackgroundService { /// /// The ID of the guild to update. /// The cancellation token for this operation. - private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) { - var data = await _dataService.GetData(guildId, ct); + private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) + { + var data = await _guildData.GetData(guildId, ct); Messages.Culture = GuildSettings.Language.Get(data.Settings); var defaultRole = GuildSettings.DefaultRole.Get(data.Settings); - foreach (var memberData in data.MemberData.Values) { + foreach (var memberData in data.MemberData.Values) + { var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, memberData.Id.ToSnowflake(), ct); - if (!guildMemberResult.IsDefined(out var guildMember)) return; - if (!guildMember.User.IsDefined(out var user)) return; + if (!guildMemberResult.IsDefined(out var guildMember)) + { + return; + } + + if (!guildMember.User.IsDefined(out var user)) + { + return; + } await TickMemberAsync(guildId, user, guildMember, memberData, defaultRole, data.Settings, ct); } var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); if (!eventsResult.IsSuccess) + { _logger.LogWarning("Error retrieving scheduled events.\n{ErrorMessage}", eventsResult.Error.Message); - else if (!GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + return; + } + + if (!GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + { await TickScheduledEventsAsync(guildId, data, eventsResult.Entity, ct); + } } private async Task TickScheduledEventsAsync( - Snowflake guildId, GuildData data, IEnumerable events, CancellationToken ct) { - foreach (var scheduledEvent in events) { + Snowflake guildId, GuildData data, IEnumerable events, CancellationToken ct) + { + foreach (var scheduledEvent in events) + { if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) + { data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status)); + } var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; - if (storedEvent.Status == scheduledEvent.Status) { + if (storedEvent.Status == scheduledEvent.Status) + { await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct); continue; } storedEvent.Status = scheduledEvent.Status; - var statusChangedResponseResult = storedEvent.Status switch { + var statusChangedResponseResult = storedEvent.Status switch + { GuildScheduledEventStatus.Scheduled => await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => @@ -172,16 +203,20 @@ public partial class GuildUpdateService : BackgroundService { }; if (!statusChangedResponseResult.IsSuccess) + { _logger.LogWarning( "Error handling scheduled event status update.\n{ErrorMessage}", statusChangedResponseResult.Error.Message); + } } } private async Task TickScheduledEventAsync( - Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, - CancellationToken ct) { - if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) { + Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, + CancellationToken ct) + { + if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) + { await TryAutoStartEventAsync(guildId, data, scheduledEvent, ct); return; } @@ -190,10 +225,14 @@ public partial class GuildUpdateService : BackgroundService { || eventData.EarlyNotificationSent || DateTimeOffset.UtcNow < scheduledEvent.ScheduledStartTime - - GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) return; + - GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) + { + return; + } var earlyResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct); - if (earlyResult.IsSuccess) { + if (earlyResult.IsSuccess) + { eventData.EarlyNotificationSent = true; return; } @@ -204,54 +243,82 @@ public partial class GuildUpdateService : BackgroundService { } private async Task TryAutoStartEventAsync( - Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, CancellationToken ct) { + Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, CancellationToken ct) + { if (GuildSettings.AutoStartEvents.Get(data.Settings) - && scheduledEvent.Status is not GuildScheduledEventStatus.Active) { + && scheduledEvent.Status is not GuildScheduledEventStatus.Active) + { var startResult = await _eventApi.ModifyGuildScheduledEventAsync( guildId, scheduledEvent.ID, status: GuildScheduledEventStatus.Active, ct: ct); if (!startResult.IsSuccess) + { _logger.LogWarning( "Error in automatic scheduled event start request.\n{ErrorMessage}", startResult.Error.Message); + } } } private async Task TickMemberAsync( - Snowflake guildId, IUser user, IGuildMember member, MemberData memberData, Snowflake defaultRole, - JsonNode cfg, CancellationToken ct) { + Snowflake guildId, IUser user, IGuildMember member, MemberData memberData, Snowflake defaultRole, + JsonNode cfg, CancellationToken ct) + { if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value)) + { _ = _guildApi.AddGuildMemberRoleAsync( guildId, user.ID, defaultRole, ct: ct); + } - if (DateTimeOffset.UtcNow > memberData.BannedUntil) { + if (DateTimeOffset.UtcNow > memberData.BannedUntil) + { var unbanResult = await _guildApi.RemoveGuildBanAsync( guildId, user.ID, Messages.PunishmentExpired.EncodeHeader(), ct); - if (unbanResult.IsSuccess) - memberData.BannedUntil = null; - else + if (!unbanResult.IsSuccess) + { _logger.LogWarning( "Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message); + return; + } + + memberData.BannedUntil = null; } for (var i = memberData.Reminders.Count - 1; i >= 0; i--) + { await TickReminderAsync(memberData.Reminders[i], user, memberData, ct); - if (GuildSettings.RenameHoistedUsers.Get(cfg)) await FilterNicknameAsync(guildId, user, member, ct); + } + + if (GuildSettings.RenameHoistedUsers.Get(cfg)) + { + await FilterNicknameAsync(guildId, user, member, ct); + } } - private Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, CancellationToken ct) { + private Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, CancellationToken ct) + { var currentNickname = member.Nickname.IsDefined(out var nickname) ? nickname : user.GlobalName ?? user.Username; var characterList = currentNickname.ToList(); var usernameChanged = false; foreach (var character in currentNickname) - if (IllegalCharsRegex().IsMatch(character.ToString())) { + { + if (IllegalChars().IsMatch(character.ToString())) + { characterList.Remove(character); usernameChanged = true; - } else { break; } + continue; + } + + break; + } + + if (!usernameChanged) + { + return Task.CompletedTask; + } - if (!usernameChanged) return Task.CompletedTask; var newNickname = string.Concat(characterList.ToArray()); _ = _guildApi.ModifyGuildMemberAsync( @@ -264,10 +331,14 @@ public partial class GuildUpdateService : BackgroundService { } [GeneratedRegex("[^0-9A-zЁА-яё]")] - private static partial Regex IllegalCharsRegex(); + private static partial Regex IllegalChars(); - private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData memberData, CancellationToken ct) { - if (DateTimeOffset.UtcNow < reminder.At) return; + private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData memberData, CancellationToken ct) + { + if (DateTimeOffset.UtcNow < reminder.At) + { + return; + } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.Reminder, user.GetTag()), user) @@ -276,13 +347,18 @@ public partial class GuildUpdateService : BackgroundService { .WithColour(ColorsList.Magenta) .Build(); - if (!embed.IsDefined(out var built)) return; + if (!embed.IsDefined(out var built)) + { + return; + } var messageResult = await _channelApi.CreateMessageAsync( reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); if (!messageResult.IsSuccess) + { _logger.LogWarning( "Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message); + } memberData.Reminders.Remove(reminder); } @@ -298,15 +374,19 @@ public partial class GuildUpdateService : BackgroundService { /// The cancellation token for this operation. /// A notification sending result which may or may not have succeeded. private async Task SendScheduledEventCreatedMessage( - IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { + IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) + { if (!scheduledEvent.Creator.IsDefined(out var creator)) + { return new ArgumentNullError(nameof(scheduledEvent.Creator)); + } Result embedDescriptionResult; var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null } ? scheduledEvent.Description.Value : string.Empty; - embedDescriptionResult = scheduledEvent.EntityType switch { + embedDescriptionResult = scheduledEvent.EntityType switch + { GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => GetLocalEventCreatedEmbedDescription(scheduledEvent, eventDescription), GuildScheduledEventEntityType.External => GetExternalScheduledEventCreatedEmbedDescription( @@ -315,7 +395,9 @@ public partial class GuildUpdateService : BackgroundService { }; if (!embedDescriptionResult.IsDefined(out var embedDescription)) + { return Result.FromError(embedDescriptionResult); + } var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) @@ -325,7 +407,10 @@ public partial class GuildUpdateService : BackgroundService { .WithCurrentTimestamp() .WithColour(ColorsList.White) .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } var roleMention = !GuildSettings.EventNotificationRole.Get(settings).Empty() ? Mention.Role(GuildSettings.EventNotificationRole.Get(settings)) @@ -345,14 +430,23 @@ public partial class GuildUpdateService : BackgroundService { } private static Result GetExternalScheduledEventCreatedEmbedDescription( - IGuildScheduledEvent scheduledEvent, string eventDescription) { + IGuildScheduledEvent scheduledEvent, string eventDescription) + { Result embedDescription; if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + { return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); + } + if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + { return new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); + } + if (!metadata.Location.IsDefined(out var location)) + { return new ArgumentNullError(nameof(metadata.Location)); + } embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( string.Format( @@ -365,9 +459,12 @@ public partial class GuildUpdateService : BackgroundService { } private static Result GetLocalEventCreatedEmbedDescription( - IGuildScheduledEvent scheduledEvent, string eventDescription) { + IGuildScheduledEvent scheduledEvent, string eventDescription) + { if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + { return new ArgumentNullError(nameof(scheduledEvent.ChannelID)); + } return $"{eventDescription}\n\n{Markdown.BlockQuote( string.Format( @@ -378,7 +475,8 @@ public partial class GuildUpdateService : BackgroundService { } /// - /// Handles sending a notification, mentioning the and event subscribers, + /// Handles sending a notification, mentioning the and event + /// subscribers, /// when a scheduled event has started or completed /// in a guild's if one is set. /// @@ -387,11 +485,14 @@ public partial class GuildUpdateService : BackgroundService { /// The cancellation token for this operation /// A reminder/notification sending result which may or may not have succeeded. private async Task SendScheduledEventUpdatedMessage( - IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) { - if (scheduledEvent.Status == GuildScheduledEventStatus.Active) { + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) + { + if (scheduledEvent.Status == GuildScheduledEventStatus.Active) + { data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; - var embedDescriptionResult = scheduledEvent.EntityType switch { + var embedDescriptionResult = scheduledEvent.EntityType switch + { GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => GetLocalEventStartedEmbedDescription(scheduledEvent), GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), @@ -401,9 +502,14 @@ public partial class GuildUpdateService : BackgroundService { var contentResult = await _utility.GetEventNotificationMentions( scheduledEvent, data.Settings, ct); if (!contentResult.IsDefined(out var content)) + { return Result.FromError(contentResult); + } + if (!embedDescriptionResult.IsDefined(out var embedDescription)) + { return Result.FromError(embedDescriptionResult); + } var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) .WithDescription(embedDescription) @@ -411,7 +517,10 @@ public partial class GuildUpdateService : BackgroundService { .WithCurrentTimestamp() .Build(); - if (!startedEmbed.IsDefined(out var startedBuilt)) return Result.FromError(startedEmbed); + if (!startedEmbed.IsDefined(out var startedBuilt)) + { + return Result.FromError(startedEmbed); + } return (Result)await _channelApi.CreateMessageAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), @@ -419,7 +528,10 @@ public partial class GuildUpdateService : BackgroundService { } if (scheduledEvent.Status != GuildScheduledEventStatus.Completed) + { return new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)); + } + data.ScheduledEvents.Remove(scheduledEvent.ID.Value); var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) @@ -434,17 +546,22 @@ public partial class GuildUpdateService : BackgroundService { .Build(); if (!completedEmbed.IsDefined(out var completedBuilt)) + { return Result.FromError(completedEmbed); + } return (Result)await _channelApi.CreateMessageAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), embeds: new[] { completedBuilt }, ct: ct); } - private static Result GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) { + private static Result GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) + { Result embedDescription; if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + { return new ArgumentNullError(nameof(scheduledEvent.ChannelID)); + } embedDescription = string.Format( Messages.DescriptionLocalEventStarted, @@ -453,14 +570,23 @@ public partial class GuildUpdateService : BackgroundService { return embedDescription; } - private static Result GetExternalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) { + private static Result GetExternalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) + { Result embedDescription; if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + { return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); + } + if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + { return new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); + } + if (!metadata.Location.IsDefined(out var location)) + { return new ArgumentNullError(nameof(metadata.Location)); + } embedDescription = string.Format( Messages.DescriptionExternalEventStarted, @@ -471,14 +597,20 @@ public partial class GuildUpdateService : BackgroundService { } private async Task SendEarlyEventNotificationAsync( - IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) { + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) + { var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } var contentResult = await _utility.GetEventNotificationMentions( scheduledEvent, data.Settings, ct); if (!contentResult.IsDefined(out var content)) + { return Result.FromError(contentResult); + } var earlyResult = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser) @@ -486,7 +618,10 @@ public partial class GuildUpdateService : BackgroundService { .WithCurrentTimestamp() .Build(); - if (!earlyResult.IsDefined(out var earlyBuilt)) return Result.FromError(earlyResult); + if (!earlyResult.IsDefined(out var earlyBuilt)) + { + return Result.FromError(earlyResult); + } return (Result)await _channelApi.CreateMessageAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 21aade1..35a11dd 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -16,26 +16,30 @@ namespace Boyfriend.Services; /// Provides utility methods that cannot be transformed to extension methods because they require usage /// of some Discord APIs. /// -public class UtilityService : IHostedService { - private readonly IDiscordRestChannelAPI _channelApi; +public sealed class UtilityService : IHostedService +{ + private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi; - private readonly IDiscordRestGuildAPI _guildApi; - private readonly IDiscordRestUserAPI _userApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; public UtilityService( IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, - IDiscordRestUserAPI userApi) { + IDiscordRestUserAPI userApi) + { _channelApi = channelApi; _eventApi = eventApi; _guildApi = guildApi; _userApi = userApi; } - public Task StartAsync(CancellationToken ct) { + public Task StartAsync(CancellationToken ct) + { return Task.CompletedTask; } - public Task StopAsync(CancellationToken ct) { + public Task StopAsync(CancellationToken ct) + { return Task.CompletedTask; } @@ -58,29 +62,42 @@ public class UtilityService : IHostedService { /// /// public async Task> CheckInteractionsAsync( - Snowflake guildId, Snowflake interacterId, Snowflake targetId, string action, CancellationToken ct = default) { + Snowflake guildId, Snowflake interacterId, Snowflake targetId, string action, CancellationToken ct = default) + { if (interacterId == targetId) + { return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); + } var currentUserResult = await _userApi.GetCurrentUserAsync(ct); if (!currentUserResult.IsDefined(out var currentUser)) + { return Result.FromError(currentUserResult); + } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); if (!guildResult.IsDefined(out var guild)) + { return Result.FromError(guildResult); + } var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); if (!targetMemberResult.IsDefined(out var targetMember)) + { return Result.FromSuccess(null); + } var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, currentUser.ID, ct); if (!currentMemberResult.IsDefined(out var currentMember)) + { return Result.FromError(currentMemberResult); + } var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); if (!rolesResult.IsDefined(out var roles)) + { return Result.FromError(rolesResult); + } var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId, ct); return interacterResult.IsDefined(out var interacter) @@ -90,26 +107,41 @@ public class UtilityService : IHostedService { private static Result CheckInteractions( string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, - IGuildMember interacter) { + IGuildMember interacter) + { if (!targetMember.User.IsDefined(out var targetUser)) + { return new ArgumentNullError(nameof(targetMember.User)); + } + if (!interacter.User.IsDefined(out var interacterUser)) + { return new ArgumentNullError(nameof(interacter.User)); + } if (currentMember.User == targetMember.User) + { return Result.FromSuccess($"UserCannot{action}Bot".Localized()); + } - if (targetUser.ID == guild.OwnerID) return Result.FromSuccess($"UserCannot{action}Owner".Localized()); + if (targetUser.ID == guild.OwnerID) + { + return Result.FromSuccess($"UserCannot{action}Owner".Localized()); + } var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); if (targetBotRoleDiff >= 0) + { return Result.FromSuccess($"BotCannot{action}Target".Localized()); + } if (interacterUser.ID == guild.OwnerID) + { return Result.FromSuccess(null); + } var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); var targetInteracterRoleDiff @@ -120,7 +152,8 @@ public class UtilityService : IHostedService { } /// - /// Gets the string mentioning the and event subscribers related to a scheduled + /// Gets the string mentioning the and event subscribers related to + /// a scheduled /// event. /// /// @@ -130,21 +163,24 @@ public class UtilityService : IHostedService { /// The cancellation token for this operation. /// A result containing the string which may or may not have succeeded. public async Task> GetEventNotificationMentions( - IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { + IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) + { var builder = new StringBuilder(); var role = GuildSettings.EventNotificationRole.Get(settings); var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync( scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct); - if (!usersResult.IsDefined(out var users)) return Result.FromError(usersResult); + if (!usersResult.IsDefined(out var users)) + { + return Result.FromError(usersResult); + } if (role.Value is not 0) + { builder.Append($"{Mention.Role(role)} "); + } builder = users.Where( - user => { - if (!user.GuildMember.IsDefined(out var member)) return true; - return !member.Roles.Contains(role); - }) + user => user.GuildMember.IsDefined(out var member) && !member.Roles.Contains(role)) .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} ")); return builder.ToString(); } @@ -160,17 +196,22 @@ public class UtilityService : IHostedService { /// The description of the embed. /// The user whose avatar will be displayed next to the of the embed. /// The color of the embed. - /// Whether or not the embed should be sent in + /// + /// Whether or not the embed should be sent in + /// /// The cancellation token for this operation. /// A result which has succeeded. public Result LogActionAsync( - JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar, - Color color, bool isPublic = true, CancellationToken ct = default) { + JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar, + Color color, bool isPublic = true, CancellationToken ct = default) + { var publicChannel = GuildSettings.PublicFeedbackChannel.Get(cfg); var privateChannel = GuildSettings.PrivateFeedbackChannel.Get(cfg); if (GuildSettings.PublicFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId) && GuildSettings.PrivateFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId)) + { return Result.FromSuccess(); + } var logEmbed = new EmbedBuilder().WithSmallTitle(title, avatar) .WithDescription(description) @@ -180,20 +221,27 @@ public class UtilityService : IHostedService { .Build(); if (!logEmbed.IsDefined(out var logBuilt)) + { return Result.FromError(logEmbed); + } var builtArray = new[] { logBuilt }; // Not awaiting to reduce response time if (isPublic && publicChannel != channelId) + { _ = _channelApi.CreateMessageAsync( publicChannel, embeds: builtArray, ct: ct); + } + if (privateChannel != publicChannel && privateChannel != channelId) + { _ = _channelApi.CreateMessageAsync( privateChannel, embeds: builtArray, ct: ct); + } return Result.FromSuccess(); } From c9a7fb79ab301fc25628293ed0617fe6a4e76466 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 4 Aug 2023 01:32:38 +0500 Subject: [PATCH 125/329] Log guild setting changes to PrivateFeedbackChannel (#79) This PR makes the bot log guild settings to the Private Feedback Channel. The functionality was present in pre-Remora Boyfriend, but was missing afterwards, so I will call this a bug, making this PR a bugfix. cc @mctaylors - embed design needs review ![image](https://github.com/TeamOctolings/Boyfriend/assets/61277953/5680f0d6-82fc-4c0e-b4f0-815b082997ec) Signed-off-by: Octol1ttle --- src/Commands/SettingsCommandGroup.cs | 31 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 687d49f..ad0d7f4 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -15,6 +15,7 @@ using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Commands; @@ -47,15 +48,17 @@ public class SettingsCommandGroup : CommandGroup private readonly FeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public SettingsCommandGroup( ICommandContext context, GuildDataService guildData, - FeedbackService feedback, IDiscordRestUserAPI userApi) + FeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) { _context = context; _guildData = guildData; _feedback = feedback; _userApi = userApi; + _utility = utility; } /// @@ -156,7 +159,7 @@ public class SettingsCommandGroup : CommandGroup string setting, [Description("Setting value")] string value) { - if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } @@ -167,14 +170,21 @@ public class SettingsCommandGroup : CommandGroup return Result.FromError(currentUserResult); } + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); + if (!userResult.IsDefined(out var user)) + { + return Result.FromError(userResult); + } + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await EditSettingAsync(setting, value, data, currentUser, CancellationToken); + return await EditSettingAsync(setting, value, data, channelId, user, currentUser, CancellationToken); } private async Task EditSettingAsync( - string setting, string value, GuildData data, IUser currentUser, CancellationToken ct = default) + string setting, string value, GuildData data, Snowflake channelId, IUser user, IUser currentUser, + CancellationToken ct = default) { var option = AllOptions.Single( o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase)); @@ -195,9 +205,18 @@ public class SettingsCommandGroup : CommandGroup builder.Append(Markdown.InlineCode(option.Name)) .Append($" {Messages.SettingIsNow} ") .Append(option.Display(data.Settings)); + var title = Messages.SettingSuccessfullyChanged; + var description = builder.ToString(); - var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser) - .WithDescription(builder.ToString()) + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, currentUser, ColorsList.Magenta, false, ct); + if (!logResult.IsSuccess) + { + return Result.FromError(logResult.Error); + } + + var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) + .WithDescription(description) .WithColour(ColorsList.Green) .Build(); From d023033ed47ed2bbb9880e213ac6abad22553f2f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 4 Aug 2023 01:34:01 +0500 Subject: [PATCH 126/329] Fix roles not returning on rejoin if welcome messages were disabled (#78) This PR fixes an issue where, if the `PublicFeedbackChannel` wasn't set or the welcome message was disabled, `GuildMemberJoinedResponder` would `return` early, causing roles to not be granted back if `ReturnRolesOnRejoin` was enabled, by moving the `if return` after the code that grants back roles Signed-off-by: Octol1ttle --- src/Responders/GuildMemberJoinedResponder.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index bb60b0b..b2069bf 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -38,11 +38,6 @@ public class GuildMemberJoinedResponder : IResponder var data = await _guildData.GetData(gatewayEvent.GuildID, ct); var cfg = data.Settings; - if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() - || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") - { - return Result.FromSuccess(); - } if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { @@ -55,6 +50,12 @@ public class GuildMemberJoinedResponder : IResponder } } + if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") + { + return Result.FromSuccess(); + } + Messages.Culture = GuildSettings.Language.Get(cfg); var welcomeMessage = GuildSettings.WelcomeMessage.Get(cfg) is "default" or "reset" ? Messages.DefaultWelcomeMessage From e9f7825e4ade8a758a0c347ef26e2f040a85d015 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Fri, 4 Aug 2023 18:52:54 +0500 Subject: [PATCH 127/329] Backfill member data when a guild is loaded or a new member joins it (#77) This PR backfills member data when a guild is loaded or a new member joins it. The reason for that is some actions that happen on member tick (default role grant, nickname filtering) would only occur if a member had data related to them (due to being banned or setting a reminder). In addition, the `.editorconfig` was updated with new inspections provided by a new release of Rider, 2023.2 See explanations for some changes in comments. --------- Signed-off-by: Octol1ttle --- .editorconfig | 50 +++++++++++++++----- src/Boyfriend.cs | 1 + src/Commands/BanCommandGroup.cs | 2 +- src/Commands/KickCommandGroup.cs | 2 +- src/Commands/RemindCommandGroup.cs | 2 +- src/Data/GuildData.cs | 4 +- src/Data/MemberData.cs | 2 +- src/Responders/GuildLoadedResponder.cs | 8 +++- src/Responders/GuildMemberJoinedResponder.cs | 3 +- src/Services/GuildDataService.cs | 22 +++------ 10 files changed, 61 insertions(+), 35 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5d54b45..7548c96 100644 --- a/.editorconfig +++ b/.editorconfig @@ -87,6 +87,7 @@ dotnet_diagnostic.cs0197.severity = warning dotnet_diagnostic.cs0219.severity = warning dotnet_diagnostic.cs0252.severity = warning dotnet_diagnostic.cs0253.severity = warning +dotnet_diagnostic.cs0282.severity = warning dotnet_diagnostic.cs0414.severity = warning dotnet_diagnostic.cs0420.severity = warning dotnet_diagnostic.cs0458.severity = warning @@ -150,6 +151,8 @@ dotnet_diagnostic.cs8424.severity = warning dotnet_diagnostic.cs8425.severity = warning dotnet_diagnostic.cs8500.severity = warning dotnet_diagnostic.cs8509.severity = warning +dotnet_diagnostic.cs8519.severity = warning +dotnet_diagnostic.cs8520.severity = warning dotnet_diagnostic.cs8524.severity = warning dotnet_diagnostic.cs8597.severity = warning dotnet_diagnostic.cs8600.severity = warning @@ -221,6 +224,7 @@ dotnet_diagnostic.cs8963.severity = warning dotnet_diagnostic.cs8965.severity = warning dotnet_diagnostic.cs8966.severity = warning dotnet_diagnostic.cs8971.severity = warning +dotnet_diagnostic.cs8974.severity = warning dotnet_diagnostic.cs8981.severity = warning dotnet_diagnostic.cs9042.severity = warning dotnet_diagnostic.cs9073.severity = warning @@ -242,6 +246,9 @@ dotnet_diagnostic.cs9093.severity = warning dotnet_diagnostic.cs9094.severity = warning dotnet_diagnostic.cs9095.severity = warning dotnet_diagnostic.cs9097.severity = warning +dotnet_diagnostic.cs9099.severity = warning +dotnet_diagnostic.cs9100.severity = warning +dotnet_diagnostic.cs9113.severity = warning dotnet_diagnostic.wme006.severity = warning dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined dotnet_naming_rule.constants_rule.severity = warning @@ -379,6 +386,7 @@ resharper_align_linq_query = false resharper_align_multiline_array_and_object_initializer = false resharper_align_multiline_array_initializer = true resharper_align_multiline_binary_patterns = false +resharper_align_multiline_comments = true resharper_align_multiline_ctor_init = true resharper_align_multiline_expression_braces = false resharper_align_multiline_implements_list = true @@ -527,6 +535,7 @@ resharper_indent_comment = true resharper_indent_export_declaration_members = true resharper_indent_inside_namespace = true resharper_indent_invocation_pars = inside +resharper_indent_member_initializer_list = true resharper_indent_method_decl_pars = inside resharper_indent_nested_fixed_stmt = false resharper_indent_nested_foreach_stmt = false @@ -539,6 +548,7 @@ resharper_indent_preprocessor_directives = none resharper_indent_preprocessor_if = no_indent resharper_indent_preprocessor_other = no_indent resharper_indent_preprocessor_region = usual_indent +resharper_indent_raw_literal_string = align resharper_indent_statement_pars = inside resharper_indent_text = OneIndent resharper_indent_typearg_angles = inside @@ -547,6 +557,7 @@ resharper_indent_type_constraints = true resharper_indent_wrapped_function_names = false resharper_instance_members_qualify_declared_in = this_class, base_class resharper_int_align = false +resharper_int_align_bitfield_sizes = false resharper_int_align_comments = false resharper_int_align_declaration_names = false resharper_int_align_enum_initializers = false @@ -658,6 +669,7 @@ resharper_space_after_attributes = true resharper_space_after_attribute_target_colon = true resharper_space_after_cast = false resharper_space_after_colon = true +resharper_space_after_colon_in_bitfield_declarator = true resharper_space_after_colon_in_case = true resharper_space_after_colon_in_inheritance_clause = true resharper_space_after_comma = true @@ -695,6 +707,7 @@ resharper_space_before_array_rank_parentheses = false resharper_space_before_attribute_target_colon = false resharper_space_before_checked_parentheses = false resharper_space_before_colon = false +resharper_space_before_colon_in_bitfield_declarator = true resharper_space_before_colon_in_case = false resharper_space_before_colon_in_inheritance_clause = true resharper_space_before_comma = false @@ -806,6 +819,7 @@ resharper_wrap_after_dot = false resharper_wrap_after_dot_in_method_calls = false resharper_wrap_after_expression_lbrace = true resharper_wrap_after_invocation_lpar = false +resharper_wrap_after_property_in_chained_method_calls = false resharper_wrap_arguments_style = wrap_if_long resharper_wrap_around_elements = true resharper_wrap_array_initializer_style = wrap_if_long @@ -821,6 +835,7 @@ resharper_wrap_before_declaration_rpar = false resharper_wrap_before_eq = false resharper_wrap_before_expression_rbrace = true resharper_wrap_before_extends_colon = false +resharper_wrap_before_first_method_call = false resharper_wrap_before_invocation_lpar = false resharper_wrap_before_invocation_rpar = false resharper_wrap_before_linq_expression = false @@ -862,6 +877,7 @@ resharper_access_to_for_each_variable_in_closure_highlighting = warning resharper_access_to_modified_closure_highlighting = warning resharper_access_to_static_member_via_derived_type_highlighting = warning resharper_address_of_marshal_by_ref_object_highlighting = warning +resharper_all_underscore_local_parameter_name_highlighting = warning resharper_angular_html_banana_highlighting = warning resharper_annotate_can_be_null_parameter_highlighting = warning resharper_annotate_can_be_null_type_member_highlighting = warning @@ -880,17 +896,12 @@ resharper_arrange_attributes_highlighting = warning resharper_arrange_default_value_when_type_evident_highlighting = warning resharper_arrange_default_value_when_type_not_evident_highlighting = warning resharper_arrange_local_function_body_highlighting = warning -resharper_arrange_namespace_body_highlighting = warning resharper_arrange_null_checking_pattern_highlighting = warning resharper_arrange_object_creation_when_type_evident_highlighting = warning resharper_arrange_object_creation_when_type_not_evident_highlighting = warning -resharper_arrange_redundant_parentheses_highlighting = warning resharper_arrange_static_member_qualifier_highlighting = warning -resharper_arrange_this_qualifier_highlighting = warning resharper_arrange_trailing_comma_in_multiline_lists_highlighting = warning resharper_arrange_trailing_comma_in_singleline_lists_highlighting = warning -resharper_arrange_type_member_modifiers_highlighting = warning -resharper_arrange_type_modifiers_highlighting = warning resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting = warning resharper_asp_content_placeholder_not_resolved_highlighting = error resharper_asp_custom_page_parser_filter_type_highlighting = warning @@ -906,6 +917,7 @@ resharper_asp_tag_attribute_with_optional_value_highlighting = warning resharper_asp_theme_not_resolved_highlighting = error resharper_asp_unused_register_directive_highlighting_highlighting = warning resharper_asp_warning_highlighting = warning +resharper_assignment_instead_of_discard_highlighting = warning resharper_assignment_in_conditional_expression_highlighting = warning resharper_assignment_is_fully_discarded_highlighting = warning resharper_assign_null_to_not_null_attribute_highlighting = warning @@ -947,8 +959,6 @@ resharper_base_object_equals_is_object_equals_highlighting = warning resharper_base_object_get_hash_code_call_in_get_hash_code_highlighting = warning resharper_bitwise_operator_on_enum_without_flags_highlighting = warning resharper_blazor_editor_required_highlighting = warning -resharper_built_in_type_reference_style_for_member_access_highlighting = warning -resharper_built_in_type_reference_style_highlighting = warning resharper_by_ref_argument_is_volatile_field_highlighting = warning resharper_cannot_apply_equality_operator_to_type_highlighting = warning resharper_can_simplify_dictionary_lookup_with_try_add_highlighting = warning @@ -985,6 +995,7 @@ resharper_condition_is_always_true_or_false_highlighting = warning resharper_confusing_char_as_integer_in_constructor_highlighting = warning resharper_constant_conditional_access_qualifier_highlighting = warning resharper_constant_null_coalescing_condition_highlighting = warning +resharper_consteval_if_is_always_constant_highlighting = warning resharper_constructor_initializer_loop_highlighting = warning resharper_container_annotation_redundancy_highlighting = warning resharper_contextual_logger_problem_highlighting = warning @@ -992,6 +1003,7 @@ resharper_context_value_is_provided_highlighting = warning resharper_contract_annotation_not_parsed_highlighting = warning resharper_convert_closure_to_method_group_highlighting = warning resharper_convert_conditional_ternary_expression_to_switch_expression_highlighting = warning +resharper_convert_constructor_to_member_initializers_highlighting = warning resharper_convert_if_do_to_while_highlighting = warning resharper_convert_if_statement_to_conditional_ternary_expression_highlighting = warning resharper_convert_if_statement_to_null_coalescing_assignment_highlighting = warning @@ -1016,6 +1028,8 @@ resharper_convert_to_using_declaration_highlighting = warning resharper_convert_type_check_pattern_to_null_check_highlighting = warning resharper_convert_type_check_to_null_check_highlighting = warning resharper_co_variant_array_conversion_highlighting = warning +resharper_c_sharp_build_cs_invalid_module_name_highlighting = warning +resharper_c_sharp_missing_plugin_dependency_highlighting = warning resharper_default_value_attribute_for_optional_parameter_highlighting = warning resharper_double_negation_in_pattern_highlighting = warning resharper_double_negation_operator_highlighting = warning @@ -1081,6 +1095,7 @@ resharper_html_tag_should_be_self_closed_highlighting = warning resharper_html_tag_should_not_be_self_closed_highlighting = warning resharper_html_warning_highlighting = warning resharper_identifier_typo_highlighting = none +resharper_if_std_is_constant_evaluated_can_be_replaced_highlighting = warning resharper_inactive_preprocessor_branch_highlighting = warning resharper_inconsistently_synchronized_field_highlighting = warning resharper_inconsistent_context_log_property_naming_highlighting = warning @@ -1162,6 +1177,7 @@ resharper_missing_indent_highlighting = warning resharper_missing_linebreak_highlighting = warning resharper_missing_space_highlighting = warning resharper_more_specific_foreach_variable_type_available_highlighting = warning +resharper_move_local_function_after_jump_statement_highlighting = warning resharper_move_to_existing_positional_deconstruction_pattern_highlighting = warning resharper_move_variable_declaration_inside_loop_condition_highlighting = warning resharper_multiple_nullable_attributes_usage_highlighting = warning @@ -1203,6 +1219,7 @@ resharper_not_observable_annotation_redundancy_highlighting = warning resharper_not_overridden_in_specific_culture_highlighting = warning resharper_not_resolved_in_text_highlighting = warning resharper_nullable_warning_suppression_is_used_highlighting = warning +resharper_nullness_annotation_conflict_with_jet_brains_annotations_highlighting = warning resharper_null_coalescing_condition_is_always_not_null_according_to_api_contract_highlighting = warning resharper_n_unit_async_method_must_be_task_highlighting = warning resharper_n_unit_attribute_produces_too_many_tests_highlighting = warning @@ -1289,6 +1306,7 @@ resharper_possible_write_to_me_highlighting = warning resharper_possibly_impure_method_call_on_readonly_variable_highlighting = warning resharper_possibly_missing_indexer_initializer_comma_highlighting = warning resharper_possibly_mistaken_use_of_interpolated_string_insert_highlighting = warning +resharper_possibly_unintended_usage_parameterless_get_expression_type_highlighting = error resharper_private_field_can_be_converted_to_local_variable_highlighting = warning resharper_property_can_be_made_init_only_global_highlighting = warning resharper_property_can_be_made_init_only_local_highlighting = warning @@ -1297,6 +1315,7 @@ resharper_property_field_keyword_is_never_used_highlighting = warning resharper_property_not_resolved_highlighting = error resharper_public_constructor_in_abstract_class_highlighting = warning resharper_pure_attribute_on_void_method_highlighting = warning +resharper_raw_string_can_be_simplified_highlighting = warning resharper_razor_layout_not_resolved_highlighting = error resharper_razor_section_not_resolved_highlighting = error resharper_read_access_in_double_check_locking_highlighting = warning @@ -1312,7 +1331,6 @@ resharper_redundant_attribute_parentheses_highlighting = warning resharper_redundant_attribute_suffix_highlighting = warning resharper_redundant_attribute_usage_property_highlighting = warning resharper_redundant_base_constructor_call_highlighting = warning -resharper_redundant_base_qualifier_highlighting = warning resharper_redundant_blank_lines_highlighting = warning resharper_redundant_bool_compare_highlighting = warning resharper_redundant_caller_argument_expression_default_value_highlighting = warning @@ -1361,6 +1379,7 @@ resharper_redundant_not_null_constraint_highlighting = warning resharper_redundant_nullable_annotation_on_reference_type_constraint_highlighting = warning resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_base_type_highlighting = warning resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_type_kind_highlighting = warning +resharper_redundant_nullable_directive_highlighting = warning resharper_redundant_nullable_flow_attribute_highlighting = warning resharper_redundant_nullable_type_mark_highlighting = warning resharper_redundant_nullness_attribute_with_nullable_reference_types_highlighting = warning @@ -1393,12 +1412,14 @@ resharper_redundant_to_string_call_for_value_type_highlighting = warning resharper_redundant_to_string_call_highlighting = warning resharper_redundant_type_arguments_of_method_highlighting = warning resharper_redundant_type_check_in_pattern_highlighting = warning +resharper_redundant_type_declaration_body_highlighting = warning resharper_redundant_unsafe_context_highlighting = warning resharper_redundant_using_directive_global_highlighting = warning resharper_redundant_using_directive_highlighting = warning resharper_redundant_verbatim_prefix_highlighting = warning resharper_redundant_verbatim_string_prefix_highlighting = warning resharper_redundant_virtual_modifier_highlighting = warning +resharper_redundant_with_cancellation_highlighting = warning resharper_redundant_with_expression_highlighting = warning resharper_reference_equals_with_value_type_highlighting = warning resharper_reg_exp_inspections_highlighting = warning @@ -1443,6 +1464,7 @@ resharper_replace_with_of_type_single_2_highlighting = warning resharper_replace_with_of_type_single_or_default_1_highlighting = warning resharper_replace_with_of_type_single_or_default_2_highlighting = warning resharper_replace_with_of_type_where_highlighting = warning +resharper_replace_with_primary_constructor_parameter_highlighting = warning resharper_replace_with_simple_assignment_false_highlighting = warning resharper_replace_with_simple_assignment_true_highlighting = warning resharper_replace_with_single_assignment_false_highlighting = warning @@ -1466,6 +1488,8 @@ resharper_required_base_types_is_not_inherited_highlighting = warning resharper_resource_item_not_resolved_highlighting = error resharper_resource_not_resolved_highlighting = error resharper_resx_not_resolved_highlighting = warning +resharper_return_of_task_produced_by_using_variable_highlighting = warning +resharper_return_of_using_variable_highlighting = warning resharper_return_type_can_be_enumerable_global_highlighting = warning resharper_return_type_can_be_enumerable_local_highlighting = warning resharper_return_type_can_be_not_nullable_highlighting = warning @@ -1491,6 +1515,7 @@ resharper_safe_cast_is_used_as_type_check_highlighting = warning resharper_script_tag_has_both_src_and_content_attributes_highlighting = error resharper_sealed_member_in_sealed_class_highlighting = warning resharper_separate_control_transfer_statement_highlighting = warning +resharper_separate_local_functions_with_jump_statement_highlighting = warning resharper_service_contract_without_operations_highlighting = warning resharper_shift_expression_real_shift_count_is_zero_highlighting = warning resharper_shift_expression_result_equals_zero_highlighting = warning @@ -1511,6 +1536,7 @@ resharper_stack_alloc_inside_loop_highlighting = warning resharper_static_member_initializer_referes_to_member_below_highlighting = warning resharper_static_member_in_generic_type_highlighting = warning resharper_static_problem_in_text_highlighting = warning +resharper_std_is_constant_evaluated_will_always_evaluate_to_constant_highlighting = warning resharper_string_compare_is_culture_specific_1_highlighting = warning resharper_string_compare_is_culture_specific_2_highlighting = warning resharper_string_compare_is_culture_specific_3_highlighting = warning @@ -1534,10 +1560,7 @@ resharper_struct_member_can_be_made_read_only_highlighting = warning resharper_suggest_base_type_for_parameter_highlighting = warning resharper_suggest_base_type_for_parameter_in_constructor_highlighting = warning resharper_suggest_discard_declaration_var_style_highlighting = warning -resharper_suggest_var_or_type_built_in_types_highlighting = warning resharper_suggest_var_or_type_deconstruction_declarations_highlighting = warning -resharper_suggest_var_or_type_elsewhere_highlighting = warning -resharper_suggest_var_or_type_simple_types_highlighting = warning resharper_suppress_nullable_warning_expression_as_inverted_is_expression_highlighting = warning resharper_suspicious_lock_over_synchronization_primitive_highlighting = warning resharper_suspicious_math_sign_method_highlighting = warning @@ -1588,6 +1611,7 @@ resharper_unused_member_in_super_local_highlighting = warning resharper_unused_member_local_highlighting = warning resharper_unused_method_return_value_global_highlighting = warning resharper_unused_method_return_value_local_highlighting = warning +resharper_unused_nullable_directive_highlighting = warning resharper_unused_parameter_global_highlighting = warning resharper_unused_parameter_in_partial_method_highlighting = warning resharper_unused_parameter_local_highlighting = warning @@ -1607,6 +1631,7 @@ resharper_use_collection_count_property_highlighting = warning resharper_use_configure_await_false_for_async_disposable_highlighting = warning resharper_use_configure_await_false_highlighting = warning resharper_use_deconstruction_highlighting = warning +resharper_use_discard_assignment_highlighting = warning resharper_use_empty_types_field_highlighting = warning resharper_use_event_args_empty_field_highlighting = warning resharper_use_format_specifier_in_format_string_highlighting = warning @@ -1636,6 +1661,7 @@ resharper_use_null_propagation_highlighting = warning resharper_use_object_or_collection_initializer_highlighting = warning resharper_use_pattern_matching_highlighting = warning resharper_use_positional_deconstruction_pattern_highlighting = warning +resharper_use_raw_string_highlighting = warning resharper_use_string_interpolation_highlighting = warning resharper_use_string_interpolation_when_possible_highlighting = warning resharper_use_switch_case_pattern_variable_highlighting = warning @@ -1646,6 +1672,8 @@ resharper_use_with_expression_to_copy_anonymous_object_highlighting = warning resharper_use_with_expression_to_copy_record_highlighting = warning resharper_use_with_expression_to_copy_struct_highlighting = warning resharper_use_with_expression_to_copy_tuple_highlighting = warning +resharper_using_statement_resource_initialization_expression_highlighting = warning +resharper_using_statement_resource_initialization_highlighting = warning resharper_value_parameter_not_used_highlighting = warning resharper_value_range_attribute_violation_highlighting = warning resharper_variable_can_be_not_nullable_highlighting = warning diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index a1b6fbd..b4e9a63 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -63,6 +63,7 @@ public sealed class Boyfriend { options.Intents |= GatewayIntents.MessageContents | GatewayIntents.GuildMembers + | GatewayIntents.GuildPresences | GatewayIntents.GuildScheduledEvents; }); services.Configure( diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index ea03009..df1d18f 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -171,7 +171,7 @@ public class BanCommandGroup : CommandGroup return Result.FromError(banResult.Error); } - var memberData = data.GetMemberData(target.ID); + var memberData = data.GetOrCreateMemberData(target.ID); memberData.BannedUntil = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; memberData.Roles.Clear(); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 45da191..88d746f 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -153,7 +153,7 @@ public class KickCommandGroup : CommandGroup return Result.FromError(kickResult.Error); } - data.GetMemberData(target.ID).Roles.Clear(); + data.GetOrCreateMemberData(target.ID).Roles.Clear(); var title = string.Format(Messages.UserKicked, target.GetTag()); var description = string.Format(Messages.DescriptionActionReason, reason); diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 3f588aa..cabbbeb 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -77,7 +77,7 @@ public class RemindCommandGroup : CommandGroup { var remindAt = DateTimeOffset.UtcNow.Add(@in); - data.GetMemberData(user.ID).Reminders.Add( + data.GetOrCreateMemberData(user.ID).Reminders.Add( new Reminder { At = remindAt, diff --git a/src/Data/GuildData.cs b/src/Data/GuildData.cs index 5adb869..2e14e17 100644 --- a/src/Data/GuildData.cs +++ b/src/Data/GuildData.cs @@ -30,14 +30,14 @@ public sealed class GuildData MemberDataPath = memberDataPath; } - public MemberData GetMemberData(Snowflake userId) + public MemberData GetOrCreateMemberData(Snowflake userId) { if (MemberData.TryGetValue(userId.Value, out var existing)) { return existing; } - var newData = new MemberData(userId.Value, null); + var newData = new MemberData(userId.Value); MemberData.Add(userId.Value, newData); return newData; } diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index f028938..1ff2bc0 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -5,7 +5,7 @@ namespace Boyfriend.Data; /// public sealed class MemberData { - public MemberData(ulong id, DateTimeOffset? bannedUntil) + public MemberData(ulong id, DateTimeOffset? bannedUntil = null) { Id = id; BannedUntil = bannedUntil; diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 547e5ac..9dd2f97 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -43,7 +43,13 @@ public class GuildLoadedResponder : IResponder var guild = gatewayEvent.Guild.AsT0; _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); - var cfg = await _guildData.GetSettings(guild.ID, ct); + var data = await _guildData.GetData(guild.ID, ct); + var cfg = data.Settings; + foreach (var member in guild.Members.Where(m => m.User.HasValue)) + { + data.GetOrCreateMemberData(member.User.Value.ID); + } + if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) { return Result.FromSuccess(); diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index b2069bf..5699008 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -38,12 +38,13 @@ public class GuildMemberJoinedResponder : IResponder var data = await _guildData.GetData(gatewayEvent.GuildID, ct); var cfg = data.Settings; + var memberData = data.GetOrCreateMemberData(user.ID); if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { var result = await _guildApi.ModifyGuildMemberAsync( gatewayEvent.GuildID, user.ID, - roles: data.GetMemberData(user.ID).Roles.ConvertAll(r => r.ToSnowflake()), ct: ct); + roles: memberData.Roles.ConvertAll(r => r.ToSnowflake()), ct: ct); if (!result.IsSuccess) { return Result.FromError(result.Error); diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index fac1b0a..0ded110 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -71,15 +71,7 @@ public sealed class GuildDataService : IHostedService var memberDataPath = $"{guildId}/MemberData"; var settingsPath = $"{guildId}/Settings.json"; var scheduledEventsPath = $"{guildId}/ScheduledEvents.json"; - if (!Directory.Exists(idString)) - { - Directory.CreateDirectory(idString); - } - - if (!Directory.Exists(memberDataPath)) - { - Directory.CreateDirectory(memberDataPath); - } + Directory.CreateDirectory(idString); if (!File.Exists(settingsPath)) { @@ -101,9 +93,9 @@ public sealed class GuildDataService : IHostedService eventsStream, cancellationToken: ct); var memberData = new Dictionary(); - foreach (var dataPath in Directory.GetFiles(memberDataPath)) + foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles()) { - await using var dataStream = File.OpenRead(dataPath); + await using var dataStream = dataFileInfo.OpenRead(); var data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); if (data is null) { @@ -123,10 +115,8 @@ public sealed class GuildDataService : IHostedService jsonSettings ?? new JsonObject(), settingsPath, await events ?? new Dictionary(), scheduledEventsPath, memberData, memberDataPath); - while (!_datas.ContainsKey(guildId)) - { - _datas.TryAdd(guildId, finalData); - } + + _datas.TryAdd(guildId, finalData); return finalData; } @@ -138,7 +128,7 @@ public sealed class GuildDataService : IHostedService public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) { - return (await GetData(guildId, ct)).GetMemberData(userId); + return (await GetData(guildId, ct)).GetOrCreateMemberData(userId); } public ICollection GetGuildIds() From f260681b393b0136f161aaf84739ee90791398eb Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 5 Aug 2023 23:02:40 +0500 Subject: [PATCH 128/329] Split GuildUpdateService into separate services (#80) GuildUpdateService is a service that contains way too many responsibilities with everything strictly coupled to each other. The code is buggy, hard to refactor and swallows errors. This prompted me to make this PR, which splits it into three independant services: - SongUpdateService (responsible for changing songs presence); - MemberUpdateService (responsible for updating member datas: unbanning users, adding the default role, sending reminders, filtering nicknames); - ScheduledEventUpdateService (responsible for updating scheduled events: sending notifications, automatically starting events). All of these services and their methods use Results to push errors all the way up in the stack, making sure no error is missed. To make logging and debugging easier, an extension method for `ILogger` was created - `LogResult`. The method checks if the result was successful or if its failure was caused by a user or environment error before logging anything - providing cleaner code and logs. `ExceptionError`s will also have their exception stacktrace and type logged (except in Remora code). This PR also fixes an issue that prevented banned users from being unbanned when their punishment was over. --------- Signed-off-by: Octol1ttle --- src/Boyfriend.cs | 5 +- src/Commands/BanCommandGroup.cs | 3 +- .../Events/ErrorLoggingPostExecutionEvent.cs | 12 +- .../Events/LoggingPreparationErrorEvent.cs | 10 +- src/Commands/MuteCommandGroup.cs | 3 +- src/Extensions.cs | 75 +++ src/Services/GuildUpdateService.cs | 631 ------------------ src/Services/Update/MemberUpdateService.cs | 194 ++++++ .../Update/ScheduledEventUpdateService.cs | 374 +++++++++++ src/Services/Update/SongUpdateService.cs | 66 ++ 10 files changed, 720 insertions(+), 653 deletions(-) delete mode 100644 src/Services/GuildUpdateService.cs create mode 100644 src/Services/Update/MemberUpdateService.cs create mode 100644 src/Services/Update/ScheduledEventUpdateService.cs create mode 100644 src/Services/Update/SongUpdateService.cs diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index b4e9a63..7fdbbb2 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -1,6 +1,7 @@ using Boyfriend.Commands; using Boyfriend.Commands.Events; using Boyfriend.Services; +using Boyfriend.Services.Update; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -88,7 +89,9 @@ public sealed class Boyfriend // Services .AddSingleton() .AddSingleton() - .AddHostedService() + .AddHostedService() + .AddHostedService() + .AddHostedService() // Slash commands .AddCommandTree() .WithCommandGroup() diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index df1d18f..6d662c6 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Text; using Boyfriend.Data; using Boyfriend.Services; +using Boyfriend.Services.Update; using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -203,7 +204,7 @@ public class BanCommandGroup : CommandGroup /// was unbanned and vice-versa. /// /// - /// + /// [Command("unban")] [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] [DiscordDefaultDMPermission(false)] diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 009bfa1..b0fbccf 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -1,7 +1,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Remora.Discord.Commands.Contexts; -using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Services; using Remora.Results; @@ -24,21 +23,14 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent /// Logs a warning using the injected if the has not /// succeeded. /// - /// The context of the slash command. Unused. + /// The context of the slash command. /// The result whose success is checked. /// The cancellation token for this operation. Unused. /// A result which has succeeded. public Task AfterExecutionAsync( ICommandContext context, IResult commandResult, CancellationToken ct = default) { - if (!commandResult.IsSuccess && !commandResult.Error.IsUserOrEnvironmentError()) - { - _logger.LogWarning("Error in slash command execution.\n{ErrorMessage}", commandResult.Error.Message); - if (commandResult.Error is ExceptionError exerr) - { - _logger.LogError(exerr.Exception, "An exception has been thrown"); - } - } + _logger.LogResult(commandResult, $"Error in slash command execution for /{context.Command.Command.Node.Key}."); return Task.FromResult(Result.FromSuccess()); } diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs index f6c1e1f..6d80513 100644 --- a/src/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs @@ -1,7 +1,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Remora.Discord.Commands.Contexts; -using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Services; using Remora.Results; @@ -31,14 +30,7 @@ public class LoggingPreparationErrorEvent : IPreparationErrorEvent public Task PreparationFailed( IOperationContext context, IResult preparationResult, CancellationToken ct = default) { - if (!preparationResult.IsSuccess && !preparationResult.Error.IsUserOrEnvironmentError()) - { - _logger.LogWarning("Error in slash command preparation.\n{ErrorMessage}", preparationResult.Error.Message); - if (preparationResult.Error is ExceptionError exerr) - { - _logger.LogError(exerr.Exception, "An exception has been thrown"); - } - } + _logger.LogResult(preparationResult, "Error in slash command preparation."); return Task.FromResult(Result.FromSuccess()); } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index a35699b..ce70d68 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Text; using Boyfriend.Data; using Boyfriend.Services; +using Boyfriend.Services.Update; using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -166,7 +167,7 @@ public class MuteCommandGroup : CommandGroup /// was unmuted and vice-versa. /// /// - /// + /// [Command("unmute", "размут")] [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] [DiscordDefaultDMPermission(false)] diff --git a/src/Extensions.cs b/src/Extensions.cs index df2e548..9a05e80 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -1,6 +1,7 @@ using System.Net; using System.Text; using DiffPlex.DiffBuilder.Model; +using Microsoft.Extensions.Logging; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Objects; @@ -258,4 +259,78 @@ public static class Extensions return (Result)await feedback.SendContextualEmbedAsync(embed, ct: ct); } + + /// + /// Checks if the has failed due to an error that has resulted from neither invalid user + /// input nor the execution environment and logs the error using the provided . + /// + /// + /// This has special behavior for - its exception will be passed to the + /// + /// + /// The logger to use. + /// The Result whose error check. + /// The message to use if this result has failed. + public static void LogResult(this ILogger logger, IResult result, string? message = "") + { + if (result.IsSuccess || result.Error.IsUserOrEnvironmentError()) + { + return; + } + + if (result.Error is ExceptionError exe) + { + logger.LogError(exe.Exception, "{ErrorMessage}", message); + return; + } + + logger.LogWarning("{UserMessage}\n{ResultErrorMessage}", message, result.Error.Message); + } + + public static void AddIfFailed(this List list, Result result) + { + if (!result.IsSuccess) + { + list.Add(result); + } + } + + /// + /// Return an appropriate result for a list of failed results. The list must only contain failed results. + /// + /// The list of failed results. + /// + /// A successful result if the list is empty, the only Result in the list, or + /// containing all results from the list. + /// + /// + public static Result AggregateErrors(this List list) + { + return list.Count switch + { + 0 => Result.FromSuccess(), + 1 => list[0], + _ => new AggregateError(list.Cast().ToArray()) + }; + } + + public static Result TryGetExternalEventData(this IGuildScheduledEvent scheduledEvent, out DateTimeOffset endTime, + out string? location) + { + endTime = default; + location = default; + if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + { + return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); + } + + if (!metadata.Location.IsDefined(out location)) + { + return new ArgumentNullError(nameof(metadata.Location)); + } + + return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime) + ? Result.FromSuccess() + : new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); + } } diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs deleted file mode 100644 index 752f3c2..0000000 --- a/src/Services/GuildUpdateService.cs +++ /dev/null @@ -1,631 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; -using Boyfriend.Data; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Gateway.Commands; -using Remora.Discord.API.Objects; -using Remora.Discord.Extensions.Embeds; -using Remora.Discord.Extensions.Formatting; -using Remora.Discord.Gateway; -using Remora.Discord.Gateway.Responders; -using Remora.Discord.Interactivity; -using Remora.Rest.Core; -using Remora.Results; - -namespace Boyfriend.Services; - -/// -/// Handles executing guild updates (also called "ticks") once per second. -/// -public sealed partial class GuildUpdateService : BackgroundService -{ - private static readonly (string Name, TimeSpan Duration)[] SongList = - { - ("UNDEAD CORPORATION - The Empress", new TimeSpan(0, 4, 34)), - ("UNDEAD CORPORATION - Everything will freeze", new TimeSpan(0, 3, 17)), - ("Splatoon 3 - Rockagilly Blues (Yoko & the Gold Bazookas)", new TimeSpan(0, 3, 37)), - ("Splatoon 3 - Seep and Destroy", new TimeSpan(0, 2, 42)), - ("IA - A Tale of Six Trillion Years and a Night", new TimeSpan(0, 3, 40)), - ("Manuel - Gas Gas Gas", new TimeSpan(0, 3, 17)), - ("Camellia - Flamewall", new TimeSpan(0, 6, 50)), - ("Jukio Kallio, Daniel Hagström - Fall 'n' Roll", new TimeSpan(0, 3, 14)), - ("SCATTLE - Hypertension", new TimeSpan(0, 3, 18)), - ("KEYGEN CHURCH - Tenebre Rosso Sangue", new TimeSpan(0, 3, 53)), - ("Chipzel - Swing Me Another 6", new TimeSpan(0, 5, 32)), - ("Noisecream - Mist of Rage", new TimeSpan(0, 2, 25)) - }; - - private static readonly string[] GenericNicknames = - { - "Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary", - "Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck", - "Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane", - "Iceberg", "Iguana", "Kiwi", "Kite", "Lamb", "Lily", "Macaw", "Manatee", "Maple", "Mask", - "Nautilus", "Ostrich", "Octopus", "Pelican", "Puffin", "Pyramid", "Rattle", "Robin", "Rose", - "Salmon", "Seal", "Shark", "Sheep", "Snake", "Sonar", "Stump", "Sparrow", "Toaster", "Toucan", - "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag" - }; - - private readonly List _activityList = new(1) { new Activity("with Remora.Discord", ActivityType.Game) }; - - private readonly IDiscordRestChannelAPI _channelApi; - private readonly DiscordGatewayClient _client; - private readonly IDiscordRestGuildScheduledEventAPI _eventApi; - private readonly IDiscordRestGuildAPI _guildApi; - private readonly GuildDataService _guildData; - private readonly ILogger _logger; - private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; - - private DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; - private uint _nextSongIndex; - - public GuildUpdateService( - IDiscordRestChannelAPI channelApi, DiscordGatewayClient client, GuildDataService guildData, - IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, ILogger logger, - IDiscordRestUserAPI userApi, UtilityService utility) - { - _channelApi = channelApi; - _client = client; - _guildData = guildData; - _eventApi = eventApi; - _guildApi = guildApi; - _logger = logger; - _userApi = userApi; - _utility = utility; - } - - /// - /// Activates a periodic timer with a 1 second interval and adds guild update tasks on each timer tick. - /// Additionally, updates the current presence with songs from . - /// - /// If update tasks take longer than 1 second, the next timer tick will be skipped. - /// The cancellation token for this operation. - protected override async Task ExecuteAsync(CancellationToken ct) - { - using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); - var tasks = new List(); - - while (await timer.WaitForNextTickAsync(ct)) - { - var guildIds = _guildData.GetGuildIds(); - if (guildIds.Count > 0 && DateTimeOffset.UtcNow >= _nextSongAt) - { - var nextSong = SongList[_nextSongIndex]; - _activityList[0] = new Activity(nextSong.Name, ActivityType.Listening); - _client.SubmitCommand( - new UpdatePresence( - UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList)); - _nextSongAt = DateTimeOffset.UtcNow.Add(nextSong.Duration); - _nextSongIndex++; - if (_nextSongIndex >= SongList.Length) - { - _nextSongIndex = 0; - } - } - - tasks.AddRange(guildIds.Select(id => TickGuildAsync(id, ct))); - - await Task.WhenAll(tasks); - tasks.Clear(); - } - } - - /// - /// Runs an update ("tick") for a guild with the provided . - /// - /// - /// This method does the following: - /// - /// Automatically unbans users once their ban period has expired. - /// Automatically grants members the guild's if one is set. - /// Sends reminders about an upcoming scheduled event. - /// Automatically starts scheduled events if is enabled. - /// Sends scheduled event start notifications. - /// Sends scheduled event completion notifications. - /// Sends reminders to members. - /// - /// This is done here and not in a for the following reasons: - /// - /// - /// Downtime would affect the reliability of notifications and automatic unbans if this logic were to be in a - /// . - /// - /// The Discord API doesn't provide necessary information about scheduled event updates. - /// - /// - /// The ID of the guild to update. - /// The cancellation token for this operation. - private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) - { - var data = await _guildData.GetData(guildId, ct); - Messages.Culture = GuildSettings.Language.Get(data.Settings); - - var defaultRole = GuildSettings.DefaultRole.Get(data.Settings); - foreach (var memberData in data.MemberData.Values) - { - var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, memberData.Id.ToSnowflake(), ct); - if (!guildMemberResult.IsDefined(out var guildMember)) - { - return; - } - - if (!guildMember.User.IsDefined(out var user)) - { - return; - } - - await TickMemberAsync(guildId, user, guildMember, memberData, defaultRole, data.Settings, ct); - } - - var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); - if (!eventsResult.IsSuccess) - { - _logger.LogWarning("Error retrieving scheduled events.\n{ErrorMessage}", eventsResult.Error.Message); - return; - } - - if (!GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) - { - await TickScheduledEventsAsync(guildId, data, eventsResult.Entity, ct); - } - } - - private async Task TickScheduledEventsAsync( - Snowflake guildId, GuildData data, IEnumerable events, CancellationToken ct) - { - foreach (var scheduledEvent in events) - { - if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) - { - data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status)); - } - - var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; - if (storedEvent.Status == scheduledEvent.Status) - { - await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct); - continue; - } - - storedEvent.Status = scheduledEvent.Status; - - var statusChangedResponseResult = storedEvent.Status switch - { - GuildScheduledEventStatus.Scheduled => - await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), - GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => - await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct), - _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)) - }; - - if (!statusChangedResponseResult.IsSuccess) - { - _logger.LogWarning( - "Error handling scheduled event status update.\n{ErrorMessage}", - statusChangedResponseResult.Error.Message); - } - } - } - - private async Task TickScheduledEventAsync( - Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, - CancellationToken ct) - { - if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) - { - await TryAutoStartEventAsync(guildId, data, scheduledEvent, ct); - return; - } - - if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) == TimeSpan.Zero - || eventData.EarlyNotificationSent - || DateTimeOffset.UtcNow - < scheduledEvent.ScheduledStartTime - - GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) - { - return; - } - - var earlyResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct); - if (earlyResult.IsSuccess) - { - eventData.EarlyNotificationSent = true; - return; - } - - _logger.LogWarning( - "Error in scheduled event early notification sender.\n{ErrorMessage}", - earlyResult.Error.Message); - } - - private async Task TryAutoStartEventAsync( - Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, CancellationToken ct) - { - if (GuildSettings.AutoStartEvents.Get(data.Settings) - && scheduledEvent.Status is not GuildScheduledEventStatus.Active) - { - var startResult = await _eventApi.ModifyGuildScheduledEventAsync( - guildId, scheduledEvent.ID, - status: GuildScheduledEventStatus.Active, ct: ct); - if (!startResult.IsSuccess) - { - _logger.LogWarning( - "Error in automatic scheduled event start request.\n{ErrorMessage}", - startResult.Error.Message); - } - } - } - - private async Task TickMemberAsync( - Snowflake guildId, IUser user, IGuildMember member, MemberData memberData, Snowflake defaultRole, - JsonNode cfg, CancellationToken ct) - { - if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value)) - { - _ = _guildApi.AddGuildMemberRoleAsync( - guildId, user.ID, defaultRole, ct: ct); - } - - if (DateTimeOffset.UtcNow > memberData.BannedUntil) - { - var unbanResult = await _guildApi.RemoveGuildBanAsync( - guildId, user.ID, Messages.PunishmentExpired.EncodeHeader(), ct); - if (!unbanResult.IsSuccess) - { - _logger.LogWarning( - "Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message); - return; - } - - memberData.BannedUntil = null; - } - - for (var i = memberData.Reminders.Count - 1; i >= 0; i--) - { - await TickReminderAsync(memberData.Reminders[i], user, memberData, ct); - } - - if (GuildSettings.RenameHoistedUsers.Get(cfg)) - { - await FilterNicknameAsync(guildId, user, member, ct); - } - } - - private Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, CancellationToken ct) - { - var currentNickname = member.Nickname.IsDefined(out var nickname) - ? nickname - : user.GlobalName ?? user.Username; - var characterList = currentNickname.ToList(); - var usernameChanged = false; - foreach (var character in currentNickname) - { - if (IllegalChars().IsMatch(character.ToString())) - { - characterList.Remove(character); - usernameChanged = true; - continue; - } - - break; - } - - if (!usernameChanged) - { - return Task.CompletedTask; - } - - var newNickname = string.Concat(characterList.ToArray()); - - _ = _guildApi.ModifyGuildMemberAsync( - guildId, user.ID, - !string.IsNullOrWhiteSpace(newNickname) - ? newNickname - : GenericNicknames[Random.Shared.Next(GenericNicknames.Length)], - ct: ct); - return Task.CompletedTask; - } - - [GeneratedRegex("[^0-9A-zЁА-яё]")] - private static partial Regex IllegalChars(); - - private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData memberData, CancellationToken ct) - { - if (DateTimeOffset.UtcNow < reminder.At) - { - return; - } - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.Reminder, user.GetTag()), user) - .WithDescription( - string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) - .WithColour(ColorsList.Magenta) - .Build(); - - if (!embed.IsDefined(out var built)) - { - return; - } - - var messageResult = await _channelApi.CreateMessageAsync( - reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); - if (!messageResult.IsSuccess) - { - _logger.LogWarning( - "Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message); - } - - memberData.Reminders.Remove(reminder); - } - - /// - /// Handles sending a notification, mentioning the if one is - /// set, - /// when a scheduled event is created - /// in a guild's if one is set. - /// - /// The scheduled event that has just been created. - /// The settings of the guild containing the scheduled event. - /// The cancellation token for this operation. - /// A notification sending result which may or may not have succeeded. - private async Task SendScheduledEventCreatedMessage( - IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) - { - if (!scheduledEvent.Creator.IsDefined(out var creator)) - { - return new ArgumentNullError(nameof(scheduledEvent.Creator)); - } - - Result embedDescriptionResult; - var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null } - ? scheduledEvent.Description.Value - : string.Empty; - embedDescriptionResult = scheduledEvent.EntityType switch - { - GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => - GetLocalEventCreatedEmbedDescription(scheduledEvent, eventDescription), - GuildScheduledEventEntityType.External => GetExternalScheduledEventCreatedEmbedDescription( - scheduledEvent, eventDescription), - _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) - }; - - if (!embedDescriptionResult.IsDefined(out var embedDescription)) - { - return Result.FromError(embedDescriptionResult); - } - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) - .WithTitle(scheduledEvent.Name) - .WithDescription(embedDescription) - .WithEventCover(scheduledEvent.ID, scheduledEvent.Image) - .WithCurrentTimestamp() - .WithColour(ColorsList.White) - .Build(); - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - - var roleMention = !GuildSettings.EventNotificationRole.Get(settings).Empty() - ? Mention.Role(GuildSettings.EventNotificationRole.Get(settings)) - : string.Empty; - - var button = new ButtonComponent( - ButtonComponentStyle.Primary, - Messages.EventDetailsButton, - new PartialEmoji(Name: "📋"), - CustomIDHelpers.CreateButtonIDWithState( - "scheduled-event-details", $"{scheduledEvent.GuildID}:{scheduledEvent.ID}") - ); - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(settings), roleMention, embeds: new[] { built }, - components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); - } - - private static Result GetExternalScheduledEventCreatedEmbedDescription( - IGuildScheduledEvent scheduledEvent, string eventDescription) - { - Result embedDescription; - if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - { - return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); - } - - if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) - { - return new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); - } - - if (!metadata.Location.IsDefined(out var location)) - { - return new ArgumentNullError(nameof(metadata.Location)); - } - - embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( - string.Format( - Messages.DescriptionExternalEventCreated, - Markdown.Timestamp(scheduledEvent.ScheduledStartTime), - Markdown.Timestamp(endTime), - Markdown.InlineCode(location) - ))}"; - return embedDescription; - } - - private static Result GetLocalEventCreatedEmbedDescription( - IGuildScheduledEvent scheduledEvent, string eventDescription) - { - if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) - { - return new ArgumentNullError(nameof(scheduledEvent.ChannelID)); - } - - return $"{eventDescription}\n\n{Markdown.BlockQuote( - string.Format( - Messages.DescriptionLocalEventCreated, - Markdown.Timestamp(scheduledEvent.ScheduledStartTime), - Mention.Channel(channelId) - ))}"; - } - - /// - /// Handles sending a notification, mentioning the and event - /// subscribers, - /// when a scheduled event has started or completed - /// in a guild's if one is set. - /// - /// The scheduled event that is about to start, has started or completed. - /// The data for the guild containing the scheduled event. - /// The cancellation token for this operation - /// A reminder/notification sending result which may or may not have succeeded. - private async Task SendScheduledEventUpdatedMessage( - IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) - { - if (scheduledEvent.Status == GuildScheduledEventStatus.Active) - { - data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; - - var embedDescriptionResult = scheduledEvent.EntityType switch - { - GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => - GetLocalEventStartedEmbedDescription(scheduledEvent), - GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), - _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) - }; - - var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); - if (!contentResult.IsDefined(out var content)) - { - return Result.FromError(contentResult); - } - - if (!embedDescriptionResult.IsDefined(out var embedDescription)) - { - return Result.FromError(embedDescriptionResult); - } - - var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) - .WithDescription(embedDescription) - .WithColour(ColorsList.Green) - .WithCurrentTimestamp() - .Build(); - - if (!startedEmbed.IsDefined(out var startedBuilt)) - { - return Result.FromError(startedEmbed); - } - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(data.Settings), - content, embeds: new[] { startedBuilt }, ct: ct); - } - - if (scheduledEvent.Status != GuildScheduledEventStatus.Completed) - { - return new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)); - } - - data.ScheduledEvents.Remove(scheduledEvent.ID.Value); - - var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) - .WithDescription( - string.Format( - Messages.EventDuration, - DateTimeOffset.UtcNow.Subtract( - data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime - ?? scheduledEvent.ScheduledStartTime).ToString())) - .WithColour(ColorsList.Black) - .WithCurrentTimestamp() - .Build(); - - if (!completedEmbed.IsDefined(out var completedBuilt)) - { - return Result.FromError(completedEmbed); - } - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(data.Settings), - embeds: new[] { completedBuilt }, ct: ct); - } - - private static Result GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) - { - Result embedDescription; - if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) - { - return new ArgumentNullError(nameof(scheduledEvent.ChannelID)); - } - - embedDescription = string.Format( - Messages.DescriptionLocalEventStarted, - Mention.Channel(channelId) - ); - return embedDescription; - } - - private static Result GetExternalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) - { - Result embedDescription; - if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - { - return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); - } - - if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) - { - return new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); - } - - if (!metadata.Location.IsDefined(out var location)) - { - return new ArgumentNullError(nameof(metadata.Location)); - } - - embedDescription = string.Format( - Messages.DescriptionExternalEventStarted, - Markdown.InlineCode(location), - Markdown.Timestamp(endTime) - ); - return embedDescription; - } - - private async Task SendEarlyEventNotificationAsync( - IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) - { - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) - { - return Result.FromError(currentUserResult); - } - - var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); - if (!contentResult.IsDefined(out var content)) - { - return Result.FromError(contentResult); - } - - var earlyResult = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser) - .WithColour(ColorsList.Default) - .WithCurrentTimestamp() - .Build(); - - if (!earlyResult.IsDefined(out var earlyBuilt)) - { - return Result.FromError(earlyResult); - } - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(data.Settings), - content, - embeds: new[] { earlyBuilt }, ct: ct); - } -} diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs new file mode 100644 index 0000000..472f844 --- /dev/null +++ b/src/Services/Update/MemberUpdateService.cs @@ -0,0 +1,194 @@ +using System.Text.RegularExpressions; +using Boyfriend.Data; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Services.Update; + +public sealed partial class MemberUpdateService : BackgroundService +{ + private static readonly string[] GenericNicknames = + { + "Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary", + "Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck", + "Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane", + "Iceberg", "Iguana", "Kiwi", "Kite", "Lamb", "Lily", "Macaw", "Manatee", "Maple", "Mask", + "Nautilus", "Ostrich", "Octopus", "Pelican", "Puffin", "Pyramid", "Rattle", "Robin", "Rose", + "Salmon", "Seal", "Shark", "Sheep", "Snake", "Sonar", "Stump", "Sparrow", "Toaster", "Toucan", + "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag" + }; + + private readonly IDiscordRestChannelAPI _channelApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + private readonly ILogger _logger; + + public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, + GuildDataService guildData, ILogger logger) + { + _channelApi = channelApi; + _guildApi = guildApi; + _guildData = guildData; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + var tasks = new List(); + + while (await timer.WaitForNextTickAsync(ct)) + { + var guildIds = _guildData.GetGuildIds(); + + tasks.AddRange(guildIds.Select(async id => + { + var tickResult = await TickMemberDatasAsync(id, ct); + _logger.LogResult(tickResult, $"Error in member data update for guild {id}."); + })); + + await Task.WhenAll(tasks); + tasks.Clear(); + } + } + + private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct) + { + var guildData = await _guildData.GetData(guildId, ct); + var defaultRole = GuildSettings.DefaultRole.Get(guildData.Settings); + var failedResults = new List(); + foreach (var data in guildData.MemberData.Values) + { + var tickResult = await TickMemberDataAsync(guildId, guildData, defaultRole, data, ct); + failedResults.AddIfFailed(tickResult); + } + + return failedResults.AggregateErrors(); + } + + private async Task TickMemberDataAsync(Snowflake guildId, GuildData guildData, Snowflake defaultRole, + MemberData data, + CancellationToken ct) + { + var failedResults = new List(); + var id = data.Id.ToSnowflake(); + if (DateTimeOffset.UtcNow > data.BannedUntil) + { + var unbanResult = await _guildApi.RemoveGuildBanAsync( + guildId, id, Messages.PunishmentExpired.EncodeHeader(), ct); + if (unbanResult.IsSuccess) + { + data.BannedUntil = null; + } + + return unbanResult; + } + + if (defaultRole.Value is not 0 && !data.Roles.Contains(defaultRole.Value)) + { + var addResult = await _guildApi.AddGuildMemberRoleAsync( + guildId, id, defaultRole, ct: ct); + failedResults.AddIfFailed(addResult); + } + + var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, id, ct); + if (!guildMemberResult.IsDefined(out var guildMember)) + { + return failedResults.AggregateErrors(); + } + + if (!guildMember.User.IsDefined(out var user)) + { + failedResults.AddIfFailed(new ArgumentNullError(nameof(guildMember.User))); + return failedResults.AggregateErrors(); + } + + for (var i = data.Reminders.Count - 1; i >= 0; i--) + { + var reminderTickResult = await TickReminderAsync(data.Reminders[i], user, data, ct); + failedResults.AddIfFailed(reminderTickResult); + } + + if (GuildSettings.RenameHoistedUsers.Get(guildData.Settings)) + { + var filterResult = await FilterNicknameAsync(guildId, user, guildMember, ct); + failedResults.AddIfFailed(filterResult); + } + + return failedResults.AggregateErrors(); + } + + private async Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, + CancellationToken ct) + { + var currentNickname = member.Nickname.IsDefined(out var nickname) + ? nickname + : user.GlobalName ?? user.Username; + var characterList = currentNickname.ToList(); + var usernameChanged = false; + foreach (var character in currentNickname) + { + if (IllegalChars().IsMatch(character.ToString())) + { + characterList.Remove(character); + usernameChanged = true; + continue; + } + + break; + } + + if (!usernameChanged) + { + return Result.FromSuccess(); + } + + var newNickname = string.Concat(characterList.ToArray()); + + return await _guildApi.ModifyGuildMemberAsync( + guildId, user.ID, + !string.IsNullOrWhiteSpace(newNickname) + ? newNickname + : GenericNicknames[Random.Shared.Next(GenericNicknames.Length)], + ct: ct); + } + + [GeneratedRegex("[^0-9A-zЁА-яё]")] + private static partial Regex IllegalChars(); + + private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData data, CancellationToken ct) + { + if (DateTimeOffset.UtcNow < reminder.At) + { + return Result.FromSuccess(); + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.Reminder, user.GetTag()), user) + .WithDescription( + string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) + .WithColour(ColorsList.Magenta) + .Build(); + + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } + + var messageResult = await _channelApi.CreateMessageAsync( + reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); + if (!messageResult.IsSuccess) + { + return Result.FromError(messageResult); + } + + data.Reminders.Remove(reminder); + return Result.FromSuccess(); + } +} diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs new file mode 100644 index 0000000..83094e9 --- /dev/null +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -0,0 +1,374 @@ +using System.Text.Json.Nodes; +using Boyfriend.Data; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Discord.Interactivity; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Services.Update; + +public sealed class ScheduledEventUpdateService : BackgroundService +{ + private readonly IDiscordRestChannelAPI _channelApi; + private readonly IDiscordRestGuildScheduledEventAPI _eventApi; + private readonly GuildDataService _guildData; + private readonly ILogger _logger; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; + + public ScheduledEventUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, + GuildDataService guildData, ILogger logger, IDiscordRestUserAPI userApi, + UtilityService utility) + { + _channelApi = channelApi; + _eventApi = eventApi; + _guildData = guildData; + _logger = logger; + _userApi = userApi; + _utility = utility; + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + var tasks = new List(); + + while (await timer.WaitForNextTickAsync(ct)) + { + var guildIds = _guildData.GetGuildIds(); + + tasks.AddRange(guildIds.Select(async id => + { + var tickResult = await TickScheduledEventsAsync(id, ct); + _logger.LogResult(tickResult, $"Error in scheduled events update for guild {id}."); + })); + + await Task.WhenAll(tasks); + tasks.Clear(); + } + } + + private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct) + { + var failedResults = new List(); + var data = await _guildData.GetData(guildId, ct); + var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); + if (!eventsResult.IsDefined(out var events)) + { + return Result.FromError(eventsResult); + } + + foreach (var scheduledEvent in events) + { + if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) + { + data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status)); + } + + var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; + if (storedEvent.Status == scheduledEvent.Status) + { + var tickResult = await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct); + failedResults.AddIfFailed(tickResult); + continue; + } + + storedEvent.Status = scheduledEvent.Status; + + var statusChangedResponseResult = storedEvent.Status switch + { + GuildScheduledEventStatus.Scheduled => + await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), + GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => + await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct), + _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)) + }; + failedResults.AddIfFailed(statusChangedResponseResult); + } + + return failedResults.AggregateErrors(); + } + + private async Task TickScheduledEventAsync( + Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, + CancellationToken ct) + { + if (GuildSettings.AutoStartEvents.Get(data.Settings) + && DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime + && scheduledEvent.Status is not GuildScheduledEventStatus.Active) + { + return await AutoStartEventAsync(guildId, scheduledEvent, ct); + } + + var offset = GuildSettings.EventEarlyNotificationOffset.Get(data.Settings); + if (offset == TimeSpan.Zero + || eventData.EarlyNotificationSent + || DateTimeOffset.UtcNow < scheduledEvent.ScheduledStartTime - offset) + { + return Result.FromSuccess(); + } + + var sendResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct); + if (sendResult.IsSuccess) + { + eventData.EarlyNotificationSent = true; + } + + return sendResult; + } + + private async Task AutoStartEventAsync( + Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct) + { + return (Result)await _eventApi.ModifyGuildScheduledEventAsync( + guildId, scheduledEvent.ID, + status: GuildScheduledEventStatus.Active, ct: ct); + } + + /// + /// Handles sending a notification, mentioning the if one is + /// set, + /// when a scheduled event is created + /// in a guild's if one is set. + /// + /// The scheduled event that has just been created. + /// The settings of the guild containing the scheduled event. + /// The cancellation token for this operation. + /// A notification sending result which may or may not have succeeded. + private async Task SendScheduledEventCreatedMessage( + IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) + { + if (!scheduledEvent.Creator.IsDefined(out var creator)) + { + return new ArgumentNullError(nameof(scheduledEvent.Creator)); + } + + var eventDescription = scheduledEvent.Description.IsDefined(out var description) + ? description + : string.Empty; + var embedDescriptionResult = scheduledEvent.EntityType switch + { + GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => + GetLocalEventCreatedEmbedDescription(scheduledEvent, eventDescription), + GuildScheduledEventEntityType.External => GetExternalScheduledEventCreatedEmbedDescription( + scheduledEvent, eventDescription), + _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) + }; + + if (!embedDescriptionResult.IsDefined(out var embedDescription)) + { + return Result.FromError(embedDescriptionResult); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) + .WithTitle(scheduledEvent.Name) + .WithDescription(embedDescription) + .WithEventCover(scheduledEvent.ID, scheduledEvent.Image) + .WithCurrentTimestamp() + .WithColour(ColorsList.White) + .Build(); + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } + + var roleMention = !GuildSettings.EventNotificationRole.Get(settings).Empty() + ? Mention.Role(GuildSettings.EventNotificationRole.Get(settings)) + : string.Empty; + + var button = new ButtonComponent( + ButtonComponentStyle.Primary, + Messages.EventDetailsButton, + new PartialEmoji(Name: "📋"), + CustomIDHelpers.CreateButtonIDWithState( + "scheduled-event-details", $"{scheduledEvent.GuildID}:{scheduledEvent.ID}") + ); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(settings), roleMention, embeds: new[] { built }, + components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); + } + + private static Result GetExternalScheduledEventCreatedEmbedDescription( + IGuildScheduledEvent scheduledEvent, string eventDescription) + { + var dataResult = scheduledEvent.TryGetExternalEventData(out var endTime, out var location); + if (!dataResult.IsSuccess) + { + return Result.FromError(dataResult); + } + + return $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.DescriptionExternalEventCreated, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime), + Markdown.Timestamp(endTime), + Markdown.InlineCode(location ?? string.Empty) + ))}"; + } + + private static Result GetLocalEventCreatedEmbedDescription( + IGuildScheduledEvent scheduledEvent, string eventDescription) + { + if (scheduledEvent.ChannelID is null) + { + return new ArgumentNullError(nameof(scheduledEvent.ChannelID)); + } + + return $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.DescriptionLocalEventCreated, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime), + Mention.Channel(scheduledEvent.ChannelID.Value) + ))}"; + } + + /// + /// Handles sending a notification, mentioning the and event + /// subscribers, + /// when a scheduled event has started or completed + /// in a guild's if one is set. + /// + /// The scheduled event that is about to start, has started or completed. + /// The data for the guild containing the scheduled event. + /// The cancellation token for this operation + /// A reminder/notification sending result which may or may not have succeeded. + private async Task SendScheduledEventUpdatedMessage( + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) + { + if (scheduledEvent.Status == GuildScheduledEventStatus.Active) + { + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + + var embedDescriptionResult = scheduledEvent.EntityType switch + { + GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => + GetLocalEventStartedEmbedDescription(scheduledEvent), + GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), + _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) + }; + + var contentResult = await _utility.GetEventNotificationMentions( + scheduledEvent, data.Settings, ct); + if (!contentResult.IsDefined(out var content)) + { + return Result.FromError(contentResult); + } + + if (!embedDescriptionResult.IsDefined(out var embedDescription)) + { + return Result.FromError(embedDescriptionResult); + } + + var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) + .WithDescription(embedDescription) + .WithColour(ColorsList.Green) + .WithCurrentTimestamp() + .Build(); + + if (!startedEmbed.IsDefined(out var startedBuilt)) + { + return Result.FromError(startedEmbed); + } + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), + content, embeds: new[] { startedBuilt }, ct: ct); + } + + if (scheduledEvent.Status != GuildScheduledEventStatus.Completed) + { + return new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)); + } + + data.ScheduledEvents.Remove(scheduledEvent.ID.Value); + + var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) + .WithDescription( + string.Format( + Messages.EventDuration, + DateTimeOffset.UtcNow.Subtract( + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime + ?? scheduledEvent.ScheduledStartTime).ToString())) + .WithColour(ColorsList.Black) + .WithCurrentTimestamp() + .Build(); + + if (!completedEmbed.IsDefined(out var completedBuilt)) + { + return Result.FromError(completedEmbed); + } + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), + embeds: new[] { completedBuilt }, ct: ct); + } + + private static Result GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) + { + if (scheduledEvent.ChannelID is null) + { + return new ArgumentNullError(nameof(scheduledEvent.ChannelID)); + } + + return string.Format( + Messages.DescriptionLocalEventStarted, + Mention.Channel(scheduledEvent.ChannelID.Value) + ); + } + + private static Result GetExternalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) + { + var dataResult = scheduledEvent.TryGetExternalEventData(out var endTime, out var location); + if (!dataResult.IsSuccess) + { + return Result.FromError(dataResult); + } + + return string.Format( + Messages.DescriptionExternalEventStarted, + Markdown.InlineCode(location ?? string.Empty), + Markdown.Timestamp(endTime) + ); + } + + private async Task SendEarlyEventNotificationAsync( + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) + { + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } + + var contentResult = await _utility.GetEventNotificationMentions( + scheduledEvent, data.Settings, ct); + if (!contentResult.IsDefined(out var content)) + { + return Result.FromError(contentResult); + } + + var earlyResult = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser) + .WithColour(ColorsList.Default) + .WithCurrentTimestamp() + .Build(); + + if (!earlyResult.IsDefined(out var earlyBuilt)) + { + return Result.FromError(earlyResult); + } + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), + content, + embeds: new[] { earlyBuilt }, ct: ct); + } +} diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs new file mode 100644 index 0000000..f369c0f --- /dev/null +++ b/src/Services/Update/SongUpdateService.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.Hosting; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Gateway.Commands; +using Remora.Discord.API.Objects; +using Remora.Discord.Gateway; + +namespace Boyfriend.Services.Update; + +public sealed class SongUpdateService : BackgroundService +{ + private static readonly (string Name, TimeSpan Duration)[] SongList = + { + ("UNDEAD CORPORATION - The Empress", new TimeSpan(0, 4, 34)), + ("UNDEAD CORPORATION - Everything will freeze", new TimeSpan(0, 3, 17)), + ("Splatoon 3 - Rockagilly Blues (Yoko & the Gold Bazookas)", new TimeSpan(0, 3, 37)), + ("Splatoon 3 - Seep and Destroy", new TimeSpan(0, 2, 42)), + ("IA - A Tale of Six Trillion Years and a Night", new TimeSpan(0, 3, 40)), + ("Manuel - Gas Gas Gas", new TimeSpan(0, 3, 17)), + ("Camellia - Flamewall", new TimeSpan(0, 6, 50)), + ("Jukio Kallio, Daniel Hagström - Fall 'n' Roll", new TimeSpan(0, 3, 14)), + ("SCATTLE - Hypertension", new TimeSpan(0, 3, 18)), + ("KEYGEN CHURCH - Tenebre Rosso Sangue", new TimeSpan(0, 3, 53)), + ("Chipzel - Swing Me Another 6", new TimeSpan(0, 5, 32)), + ("Noisecream - Mist of Rage", new TimeSpan(0, 2, 25)) + }; + + private readonly List _activityList = new(1) + { + new Activity("with Remora.Discord", ActivityType.Game) + }; + + private readonly DiscordGatewayClient _client; + private readonly GuildDataService _guildData; + + private uint _nextSongIndex; + + public SongUpdateService(DiscordGatewayClient client, GuildDataService guildData) + { + _client = client; + _guildData = guildData; + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + while (_guildData.GetGuildIds().Count is 0) + { + await Task.Delay(TimeSpan.FromSeconds(5), ct); + } + + while (!ct.IsCancellationRequested) + { + var nextSong = SongList[_nextSongIndex]; + _activityList[0] = new Activity(nextSong.Name, ActivityType.Listening); + _client.SubmitCommand( + new UpdatePresence( + UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList)); + _nextSongIndex++; + if (_nextSongIndex >= SongList.Length) + { + _nextSongIndex = 0; + } + + await Task.Delay(nextSong.Duration, ct); + } + } +} From cac3ee9bf74de1c07ff5a7020372ea82970efb94 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 12 Aug 2023 18:47:14 +0500 Subject: [PATCH 129/329] Scheduled event update bugfixes (#81) This PR fixes the following bugs in ScheduledEventUpdateService: - When a scheduled event is first added into ScheduledEventData, its status update code will be skipped. This results in some messages not being sent to the EventNotificationChannel - When the status update code returns an unsuccessful Result, the status in ScheduledEventData will still be updated, meaning that the code will not retry. --------- Signed-off-by: Octol1ttle --- src/Data/ScheduledEventData.cs | 4 ++-- src/Services/Update/ScheduledEventUpdateService.cs | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Data/ScheduledEventData.cs b/src/Data/ScheduledEventData.cs index 29075f5..7cd0578 100644 --- a/src/Data/ScheduledEventData.cs +++ b/src/Data/ScheduledEventData.cs @@ -8,12 +8,12 @@ namespace Boyfriend.Data; /// This information is stored on disk as a JSON file. public sealed class ScheduledEventData { - public ScheduledEventData(GuildScheduledEventStatus status) + public ScheduledEventData(GuildScheduledEventStatus? status) { Status = status; } public bool EarlyNotificationSent { get; set; } public DateTimeOffset? ActualStartTime { get; set; } - public GuildScheduledEventStatus Status { get; set; } + public GuildScheduledEventStatus? Status { get; set; } } diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 83094e9..3d57f09 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -68,7 +68,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService { if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) { - data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status)); + data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(null)); } var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; @@ -79,9 +79,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService continue; } - storedEvent.Status = scheduledEvent.Status; - - var statusChangedResponseResult = storedEvent.Status switch + var statusChangedResponseResult = scheduledEvent.Status switch { GuildScheduledEventStatus.Scheduled => await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), @@ -89,6 +87,11 @@ public sealed class ScheduledEventUpdateService : BackgroundService await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct), _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)) }; + if (statusChangedResponseResult.IsSuccess) + { + storedEvent.Status = scheduledEvent.Status; + } + failedResults.AddIfFailed(statusChangedResponseResult); } From 488a1eec0ce5925802995af7644f2f8b7cead269 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 12 Aug 2023 18:54:51 +0500 Subject: [PATCH 130/329] Add /listremind and /delremind (#82) This PR closes #61. This PR adds the `/listremind` command and the `/delremind` command. The `/delremind` command requires an index to determine which reminder to delete. cc @mctaylors review embed design and feature experience --------- Signed-off-by: Octol1ttle --- locale/Messages.resx | 9 +++ locale/Messages.ru.resx | 9 +++ locale/Messages.tt-ru.resx | 9 +++ src/Commands/RemindCommandGroup.cs | 101 +++++++++++++++++++++++++++++ src/Messages.Designer.cs | 18 +++++ 5 files changed, 146 insertions(+) diff --git a/locale/Messages.resx b/locale/Messages.resx index f463a3b..ddb0f06 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -582,4 +582,13 @@ Previous + + {0}'s reminders + + + There's no reminder with that index! + + + Reminder deleted + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index d045bc3..0cb87fb 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -582,4 +582,13 @@ Назад + + Напоминания {0} + + + У тебя нет напоминания с указанным индексом! + + + Напоминание удалено + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 1909d27..5b0b798 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -582,4 +582,13 @@ предыдущее + + напоминалки {0} + + + у тебя нет напоминалки с этим индексом! + + + напоминалка уничтожена + diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index cabbbeb..d5110e0 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Text; using Boyfriend.Data; using Boyfriend.Services; using JetBrains.Annotations; @@ -38,6 +39,54 @@ public class RemindCommandGroup : CommandGroup _userApi = userApi; } + /// + /// A slash command that lists reminders of the user that called it. + /// + /// A feedback sending result which may or may not have succeeded. + [Command("listremind")] + [Description("List your reminders")] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [UsedImplicitly] + public async Task ExecuteListReminderAsync() + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); + if (!userResult.IsDefined(out var user)) + { + return Result.FromError(userResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await ListRemindersAsync(data.GetOrCreateMemberData(userId), user, CancellationToken); + } + + private async Task ListRemindersAsync(MemberData data, IUser user, CancellationToken ct) + { + var builder = new StringBuilder(); + for (var i = 0; i < data.Reminders.Count; i++) + { + var reminder = data.Reminders[i]; + builder.AppendLine( + $"- {Markdown.InlineCode(i.ToString())} - {Markdown.InlineCode(reminder.Text)} - {Markdown.Timestamp(reminder.At)}"); + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.ReminderList, user.GetTag()), user) + .WithDescription(builder.ToString()) + .WithColour(ColorsList.Cyan) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync( + embed, ct); + } + /// /// A slash command that schedules a reminder with the specified text. /// @@ -92,4 +141,56 @@ public class RemindCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct); } + + /// + /// A slash command that deletes a reminder using its index. + /// + /// The index of the reminder to delete. + /// A feedback sending result which may or may not have succeeded. + [Command("delremind")] + [Description("Delete one of your reminders")] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [UsedImplicitly] + public async Task ExecuteDeleteReminderAsync( + [MinValue(0)] int index) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await DeleteReminderAsync(data.GetOrCreateMemberData(userId), index, currentUser, CancellationToken); + } + + private async Task DeleteReminderAsync(MemberData data, int index, IUser currentUser, + CancellationToken ct) + { + if (index >= data.Reminders.Count) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderIndex, currentUser) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + } + + data.Reminders.RemoveAt(index); + + var embed = new EmbedBuilder().WithSmallTitle(Messages.ReminderDeleted, currentUser) + .WithColour(ColorsList.Green) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync( + embed, ct); + } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 1f6fbc7..b4509e8 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -977,5 +977,23 @@ namespace Boyfriend { return ResourceManager.GetString("Previous", resourceCulture); } } + + internal static string ReminderList { + get { + return ResourceManager.GetString("ReminderList", resourceCulture); + } + } + + internal static string InvalidReminderIndex { + get { + return ResourceManager.GetString("InvalidReminderIndex", resourceCulture); + } + } + + internal static string ReminderDeleted { + get { + return ResourceManager.GetString("ReminderDeleted", resourceCulture); + } + } } } From d0d663d2bbf58440b958e23eaceb220071f4a54f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 12 Aug 2023 19:18:30 +0500 Subject: [PATCH 131/329] Fix a crash in scheduled event early notification (#83) This PR fixes a fatal crash that occurs when the bot tries to send an early notification for a scheduled event. An exception is thrown by `string#Format` when the argument list provided doesn't match the argument list in the template. This is fixed by correcting the template and the argument list Signed-off-by: Octol1ttle --- locale/Messages.resx | 2 +- locale/Messages.ru.resx | 2 +- locale/Messages.tt-ru.resx | 2 +- src/Services/Update/ScheduledEventUpdateService.cs | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index ddb0f06..cf3de28 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -433,7 +433,7 @@ You cannot unmute this user!
- {0}Event {1} will start <t:{2}:R>! + Event "{0}" will start {1}! Early event start notification offset diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 0cb87fb..d4729d1 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -430,7 +430,7 @@ Я не могу вернуть из мута этого пользователя! - {0}Событие {1} начнется <t:{2}:R>! + Событие "{0}" начнется {1}! Офсет отправки преждевременного уведомления о начале события diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 5b0b798..212ad16 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -433,7 +433,7 @@ я не могу его раззамутить... - {0}движуха {1} начнется <t:{2}:R>! + движуха "{0}" начнется {1}! заранее пнуть в минутах до начала движухи diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 3d57f09..faa8bab 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -359,7 +359,9 @@ public sealed class ScheduledEventUpdateService : BackgroundService } var earlyResult = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser) + .WithSmallTitle( + string.Format(Messages.EventEarlyNotification, scheduledEvent.Name, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime, TimestampStyle.RelativeTime)), currentUser) .WithColour(ColorsList.Default) .WithCurrentTimestamp() .Build(); From 87dbb07dece13adbcfb8f19394506d5073f72f9b Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:19:07 +0300 Subject: [PATCH 132/329] EventEarlyNotification timestamp bugfix and other changes (#84) This PR fixes EventEarlyNotification timestamp not displaying correctly and also has some other changes: - Add xmldocs for `/settingslist`'s `page` option in SettingsCommandGroup.cs - Add option description for `index` for `/delreminder` - Some corrections of grammatical errors in Messages.tt-ru.resx - Fix `ArgumentOutOfRangeException` in `/settingslist` that appears if the user uses a negative integer in `page` --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> Co-authored-by: Octol1ttle --- locale/Messages.tt-ru.resx | 8 ++++---- src/Commands/RemindCommandGroup.cs | 4 +++- src/Commands/SettingsCommandGroup.cs | 7 +++++-- .../Update/ScheduledEventUpdateService.cs | 16 +++------------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 212ad16..ec1c886 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -253,11 +253,11 @@ оъмъомоъемъъео(((( - движуха "{0}" отменен! - + движуха "{0}" отменена! +
- движуха "{0}" завершен! - + движуха "{0}" завершена! +
всегда diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index d5110e0..8e8b820 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -153,7 +153,9 @@ public class RemindCommandGroup : CommandGroup [RequireContext(ChannelContext.Guild)] [UsedImplicitly] public async Task ExecuteDeleteReminderAsync( - [MinValue(0)] int index) + [Description("Index of reminder to delete")] + [MinValue(0)] + int index) { if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) { diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index ad0d7f4..61be402 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -62,8 +62,9 @@ public class SettingsCommandGroup : CommandGroup } /// - /// A slash command that lists current per-guild GuildSettings. + /// A slash command that sends a page from the list of current GuildSettings. /// + /// The number of the page to send. /// /// A feedback sending result which may or may not have succeeded. /// @@ -75,7 +76,9 @@ public class SettingsCommandGroup : CommandGroup [Description("Shows settings list for this server")] [UsedImplicitly] public async Task ExecuteSettingsListAsync( - [Description("Settings list page")] int page) + [Description("Settings list page")] + [MinValue(1)] + int page) { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) { diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index faa8bab..24b68da 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -19,18 +19,15 @@ public sealed class ScheduledEventUpdateService : BackgroundService private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly GuildDataService _guildData; private readonly ILogger _logger; - private readonly IDiscordRestUserAPI _userApi; private readonly UtilityService _utility; public ScheduledEventUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, - GuildDataService guildData, ILogger logger, IDiscordRestUserAPI userApi, - UtilityService utility) + GuildDataService guildData, ILogger logger, UtilityService utility) { _channelApi = channelApi; _eventApi = eventApi; _guildData = guildData; _logger = logger; - _userApi = userApi; _utility = utility; } @@ -345,12 +342,6 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task SendEarlyEventNotificationAsync( IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) { - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) - { - return Result.FromError(currentUserResult); - } - var contentResult = await _utility.GetEventNotificationMentions( scheduledEvent, data.Settings, ct); if (!contentResult.IsDefined(out var content)) @@ -359,11 +350,10 @@ public sealed class ScheduledEventUpdateService : BackgroundService } var earlyResult = new EmbedBuilder() - .WithSmallTitle( + .WithDescription( string.Format(Messages.EventEarlyNotification, scheduledEvent.Name, - Markdown.Timestamp(scheduledEvent.ScheduledStartTime, TimestampStyle.RelativeTime)), currentUser) + Markdown.Timestamp(scheduledEvent.ScheduledStartTime, TimestampStyle.RelativeTime))) .WithColour(ColorsList.Default) - .WithCurrentTimestamp() .Build(); if (!earlyResult.IsDefined(out var earlyBuilt)) From da2a88246c1aadc57141726af3a8ea848d28d638 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:45:56 +0300 Subject: [PATCH 133/329] Add failed embed for /listremind (#88) Without this embed, if there are no reminders created by the user, the bot will endlessly think because StringBuilder will be empty and normal embed will not be shown. --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ locale/Messages.tt-ru.resx | 3 +++ src/Commands/RemindCommandGroup.cs | 19 +++++++++++++++++-- src/Messages.Designer.cs | 6 ++++++ 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index cf3de28..82392a8 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -591,4 +591,7 @@ Reminder deleted + + You don't have any reminders created! + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index d4729d1..7b5b3d4 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -591,4 +591,7 @@ Напоминание удалено + + У вас нет созданных напоминаний! + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index ec1c886..bb8c204 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -591,4 +591,7 @@ напоминалка уничтожена + + ты еще не крафтил напоминалки + diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 8e8b820..263d1ba 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -61,14 +61,29 @@ public class RemindCommandGroup : CommandGroup return Result.FromError(userResult); } + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } + var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ListRemindersAsync(data.GetOrCreateMemberData(userId), user, CancellationToken); + return await ListRemindersAsync(data.GetOrCreateMemberData(userId), user, currentUser, CancellationToken); } - private async Task ListRemindersAsync(MemberData data, IUser user, CancellationToken ct) + private async Task ListRemindersAsync(MemberData data, IUser user, IUser currentUser, CancellationToken ct) { + if (data.Reminders.Count == 0) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.NoRemindersFound, currentUser) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + } + var builder = new StringBuilder(); for (var i = 0; i < data.Reminders.Count; i++) { diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index b4509e8..0fee435 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -995,5 +995,11 @@ namespace Boyfriend { return ResourceManager.GetString("ReminderDeleted", resourceCulture); } } + + internal static string NoRemindersFound { + get { + return ResourceManager.GetString("NoRemindersFound", resourceCulture); + } + } } } From 501c51b865b92196ef347f61d2d2ef7b44739ceb Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 14 Aug 2023 18:24:20 +0500 Subject: [PATCH 134/329] Add automatic deployment (#90) This PR closes #87. This PR splits the workflow into 2 files: `build-pr.yml` and `build-push.yml`. The former runs InspectCode to make sure issues don't get through, while the latter builds and uploads Boyfriend and publishes it to a remote production server via SSH. --------- Signed-off-by: Octol1ttle --- .editorconfig | 4 +++ .github/workflows/build-pr.yml | 31 ++++++++++++++++ .github/workflows/build-push.yml | 61 ++++++++++++++++++++++++++++++++ .github/workflows/resharper.yml | 36 ------------------- 4 files changed, 96 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/build-pr.yml create mode 100644 .github/workflows/build-push.yml delete mode 100644 .github/workflows/resharper.yml diff --git a/.editorconfig b/.editorconfig index 7548c96..4982036 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1760,3 +1760,7 @@ resharper_zero_index_from_end_highlighting = warning indent_style = space indent_size = 4 tab_width = 4 + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 0000000..dc91660 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,31 @@ +name: "ReSharper" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + branches: [ "master" ] + merge_group: + types: [ checks_requested ] + +jobs: + inspect-code: + name: Inspect code + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: ReSharper CLI InspectCode + uses: muno92/resharper_inspectcode@1.8.0 + with: + solutionPath: ./Boyfriend.sln + ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement + extensions: ReSharperPlugin.CognitiveComplexity + solutionWideAnalysis: true diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml new file mode 100644 index 0000000..731201d --- /dev/null +++ b/.github/workflows/build-push.yml @@ -0,0 +1,61 @@ +name: "Publish and deploy" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [ "master" ] + +jobs: + upload-solution: + name: Upload Boyfriend to production + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + environment: production + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Publish solution + run: dotnet publish -c Release -r linux-x64 --no-self-contained -p:PublishReadyToRun=true + + - name: Setup SSH key + run: | + install -m 600 -D /dev/null ~/.ssh/id_rsa + echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts + shell: bash + env: + SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}} + SSH_HOST: ${{secrets.SSH_HOST}} + + - name: Quit currently running instance + continue-on-error: true + run: | + ssh $SSH_USER@$SSH_HOST pkill --signal SIGQUIT Boyfriend + shell: bash + env: + SSH_USER: ${{secrets.SSH_USER}} + SSH_HOST: ${{secrets.SSH_HOST}} + + - name: Upload published solution + run: | + scp -r bin/Release/net7.0/linux-x64/publish/* $SSH_USER@$SSH_HOST:$UPLOAD_DESTINATION + shell: bash + env: + SSH_USER: ${{secrets.SSH_USER}} + SSH_HOST: ${{secrets.SSH_HOST}} + UPLOAD_DESTINATION: ${{secrets.UPLOAD_DESTINATION}} + + - name: Start uploaded solution + run: | + ssh $SSH_USER@$SSH_HOST $COMMAND + shell: bash + env: + SSH_USER: ${{secrets.SSH_USER}} + SSH_HOST: ${{secrets.SSH_HOST}} + COMMAND: ${{secrets.COMMAND}} diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml deleted file mode 100644 index 860dd94..0000000 --- a/.github/workflows/resharper.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: "ReSharper" -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - push: - branches: [ "*" ] - pull_request: - branches: [ "master" ] - merge_group: - types: [checks_requested] - -jobs: - inspect-code: - name: Inspect code - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Restore dependencies and tools - run: dotnet restore - - - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.8.0 - with: - solutionPath: ./Boyfriend.sln - ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement - extensions: ReSharperPlugin.CognitiveComplexity - solutionWideAnalysis: true From 4252613dd3971bb2a29b6b98334cce7d241c89a8 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 14 Aug 2023 18:24:22 +0500 Subject: [PATCH 135/329] Fix various issues with ScheduledEventUpdateService (#89) This PR closes #85. This PR fixes many issues related to scheduled events. Most importantly, scheduled events that are no longer present in the guild, but still have data related to them, won't be left rotting. This requires deletion of `ScheduledEvents.json` files in all guilds. Maybe I'll start writing datafixers one day... Signed-off-by: Octol1ttle --- src/Data/ScheduledEventData.cs | 10 +- src/Responders/GuildLoadedResponder.cs | 15 ++ .../ScheduledEventCancelledResponder.cs | 53 ------ .../ScheduledEventCreatedResponder.cs | 32 ++++ .../ScheduledEventUpdatedResponder.cs | 30 ++++ .../Update/ScheduledEventUpdateService.cs | 162 +++++++++++------- 6 files changed, 190 insertions(+), 112 deletions(-) delete mode 100644 src/Responders/ScheduledEventCancelledResponder.cs create mode 100644 src/Responders/ScheduledEventCreatedResponder.cs create mode 100644 src/Responders/ScheduledEventUpdatedResponder.cs diff --git a/src/Data/ScheduledEventData.cs b/src/Data/ScheduledEventData.cs index 7cd0578..8af9c92 100644 --- a/src/Data/ScheduledEventData.cs +++ b/src/Data/ScheduledEventData.cs @@ -8,12 +8,20 @@ namespace Boyfriend.Data; /// This information is stored on disk as a JSON file. public sealed class ScheduledEventData { - public ScheduledEventData(GuildScheduledEventStatus? status) + public ScheduledEventData(ulong id, string name, GuildScheduledEventStatus status, + DateTimeOffset scheduledStartTime) { + Id = id; + Name = name; Status = status; + ScheduledStartTime = scheduledStartTime; } + public ulong Id { get; } + public string Name { get; set; } public bool EarlyNotificationSent { get; set; } + public DateTimeOffset ScheduledStartTime { get; set; } public DateTimeOffset? ActualStartTime { get; set; } public GuildScheduledEventStatus? Status { get; set; } + public bool ScheduleOnStatusUpdated { get; set; } = true; } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 9dd2f97..a5b8b19 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -50,6 +50,21 @@ public class GuildLoadedResponder : IResponder data.GetOrCreateMemberData(member.User.Value.ID); } + foreach (var schEvent in guild.GuildScheduledEvents) + { + if (!data.ScheduledEvents.TryGetValue(schEvent.ID.Value, out var eventData)) + { + data.ScheduledEvents.Add(schEvent.ID.Value, new ScheduledEventData(schEvent.ID.Value, + schEvent.Name, schEvent.Status, schEvent.ScheduledStartTime)); + continue; + } + + eventData.Name = schEvent.Name; + eventData.ScheduledStartTime = schEvent.ScheduledStartTime; + eventData.ScheduleOnStatusUpdated = eventData.Status != schEvent.Status; + eventData.Status = schEvent.Status; + } + if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) { return Result.FromSuccess(); diff --git a/src/Responders/ScheduledEventCancelledResponder.cs b/src/Responders/ScheduledEventCancelledResponder.cs deleted file mode 100644 index c35128a..0000000 --- a/src/Responders/ScheduledEventCancelledResponder.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Boyfriend.Data; -using Boyfriend.Services; -using JetBrains.Annotations; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.Extensions.Embeds; -using Remora.Discord.Gateway.Responders; -using Remora.Results; - -namespace Boyfriend.Responders; - -/// -/// Handles sending a notification when a scheduled event has been cancelled -/// in a guild's if one is set. -/// -[UsedImplicitly] -public class GuildScheduledEventDeleteResponder : IResponder -{ - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _guildData; - - public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService guildData) - { - _channelApi = channelApi; - _guildData = guildData; - } - - public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) - { - var guildData = await _guildData.GetData(gatewayEvent.GuildID, ct); - guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); - - if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty()) - { - return Result.FromSuccess(); - } - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name)) - .WithDescription(":(") - .WithColour(ColorsList.Red) - .WithCurrentTimestamp() - .Build(); - - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(guildData.Settings), embeds: new[] { built }, ct: ct); - } -} diff --git a/src/Responders/ScheduledEventCreatedResponder.cs b/src/Responders/ScheduledEventCreatedResponder.cs new file mode 100644 index 0000000..36f313a --- /dev/null +++ b/src/Responders/ScheduledEventCreatedResponder.cs @@ -0,0 +1,32 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles adding a scheduled event to a guild's ScheduledEventData. +/// +[UsedImplicitly] +public class ScheduledEventCreatedResponder : IResponder +{ + private readonly GuildDataService _guildData; + + public ScheduledEventCreatedResponder(GuildDataService guildData) + { + _guildData = guildData; + } + + public async Task RespondAsync(IGuildScheduledEventCreate gatewayEvent, CancellationToken ct = default) + { + var data = await _guildData.GetData(gatewayEvent.GuildID, ct); + data.ScheduledEvents.Add(gatewayEvent.ID.Value, + new ScheduledEventData(gatewayEvent.ID.Value, + gatewayEvent.Name, gatewayEvent.Status, gatewayEvent.ScheduledStartTime)); + + return Result.FromSuccess(); + } +} diff --git a/src/Responders/ScheduledEventUpdatedResponder.cs b/src/Responders/ScheduledEventUpdatedResponder.cs new file mode 100644 index 0000000..7db7edd --- /dev/null +++ b/src/Responders/ScheduledEventUpdatedResponder.cs @@ -0,0 +1,30 @@ +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +[UsedImplicitly] +public class ScheduledEventUpdatedResponder : IResponder +{ + private readonly GuildDataService _guildData; + + public ScheduledEventUpdatedResponder(GuildDataService guildData) + { + _guildData = guildData; + } + + public async Task RespondAsync(IGuildScheduledEventUpdate gatewayEvent, CancellationToken ct = default) + { + var data = await _guildData.GetData(gatewayEvent.GuildID, ct); + var eventData = data.ScheduledEvents[gatewayEvent.ID.Value]; + eventData.Name = gatewayEvent.Name; + eventData.ScheduledStartTime = gatewayEvent.ScheduledStartTime; + eventData.ScheduleOnStatusUpdated = eventData.Status != gatewayEvent.Status; + eventData.Status = gatewayEvent.Status; + + return Result.FromSuccess(); + } +} diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 24b68da..de03fc5 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -61,40 +61,56 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromError(eventsResult); } - foreach (var scheduledEvent in events) + foreach (var storedEvent in data.ScheduledEvents.Values) { - if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) + var scheduledEvent = TryGetScheduledEvent(events, storedEvent.Id); + if (!scheduledEvent.IsSuccess) { - data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(null)); + storedEvent.ScheduleOnStatusUpdated = true; + storedEvent.Status = storedEvent.ActualStartTime != null + ? GuildScheduledEventStatus.Completed + : GuildScheduledEventStatus.Canceled; } - var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; - if (storedEvent.Status == scheduledEvent.Status) + if (!storedEvent.ScheduleOnStatusUpdated) { - var tickResult = await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct); + var tickResult = await TickScheduledEventAsync(guildId, data, scheduledEvent.Entity, storedEvent, ct); failedResults.AddIfFailed(tickResult); continue; } - var statusChangedResponseResult = scheduledEvent.Status switch + var statusUpdatedResponseResult = storedEvent.Status switch { GuildScheduledEventStatus.Scheduled => - await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), - GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => - await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct), - _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)) + await SendScheduledEventCreatedMessage(scheduledEvent.Entity, data.Settings, ct), + GuildScheduledEventStatus.Canceled => + await SendScheduledEventCancelledMessage(storedEvent, data, ct), + GuildScheduledEventStatus.Active => + await SendScheduledEventStartedMessage(scheduledEvent.Entity, data, ct), + GuildScheduledEventStatus.Completed => + await SendScheduledEventCompletedMessage(storedEvent, data, ct), + _ => new ArgumentOutOfRangeError(nameof(storedEvent.Status)) }; - if (statusChangedResponseResult.IsSuccess) + if (statusUpdatedResponseResult.IsSuccess) { - storedEvent.Status = scheduledEvent.Status; + storedEvent.ScheduleOnStatusUpdated = false; } - failedResults.AddIfFailed(statusChangedResponseResult); + failedResults.AddIfFailed(statusUpdatedResponseResult); } return failedResults.AggregateErrors(); } + private static Result TryGetScheduledEvent(IEnumerable from, ulong id) + { + var filtered = from.Where(schEvent => schEvent.ID == id); + var filteredArray = filtered.ToArray(); + return filteredArray.Any() + ? Result.FromSuccess(filteredArray.Single()) + : new NotFoundError(); + } + private async Task TickScheduledEventAsync( Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, CancellationToken ct) @@ -240,63 +256,57 @@ public sealed class ScheduledEventUpdateService : BackgroundService /// The data for the guild containing the scheduled event. /// The cancellation token for this operation /// A reminder/notification sending result which may or may not have succeeded. - private async Task SendScheduledEventUpdatedMessage( + private async Task SendScheduledEventStartedMessage( IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) { - if (scheduledEvent.Status == GuildScheduledEventStatus.Active) + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + + var embedDescriptionResult = scheduledEvent.EntityType switch { - data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => + GetLocalEventStartedEmbedDescription(scheduledEvent), + GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), + _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) + }; - var embedDescriptionResult = scheduledEvent.EntityType switch - { - GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => - GetLocalEventStartedEmbedDescription(scheduledEvent), - GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), - _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) - }; - - var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); - if (!contentResult.IsDefined(out var content)) - { - return Result.FromError(contentResult); - } - - if (!embedDescriptionResult.IsDefined(out var embedDescription)) - { - return Result.FromError(embedDescriptionResult); - } - - var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) - .WithDescription(embedDescription) - .WithColour(ColorsList.Green) - .WithCurrentTimestamp() - .Build(); - - if (!startedEmbed.IsDefined(out var startedBuilt)) - { - return Result.FromError(startedEmbed); - } - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(data.Settings), - content, embeds: new[] { startedBuilt }, ct: ct); + var contentResult = await _utility.GetEventNotificationMentions( + scheduledEvent, data.Settings, ct); + if (!contentResult.IsDefined(out var content)) + { + return Result.FromError(contentResult); } - if (scheduledEvent.Status != GuildScheduledEventStatus.Completed) + if (!embedDescriptionResult.IsDefined(out var embedDescription)) { - return new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)); + return Result.FromError(embedDescriptionResult); } - data.ScheduledEvents.Remove(scheduledEvent.ID.Value); + var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) + .WithDescription(embedDescription) + .WithColour(ColorsList.Green) + .WithCurrentTimestamp() + .Build(); - var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) + if (!startedEmbed.IsDefined(out var startedBuilt)) + { + return Result.FromError(startedEmbed); + } + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), + content, embeds: new[] { startedBuilt }, ct: ct); + } + + private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data, + CancellationToken ct) + { + var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, eventData.Name)) .WithDescription( string.Format( Messages.EventDuration, DateTimeOffset.UtcNow.Subtract( - data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime - ?? scheduledEvent.ScheduledStartTime).ToString())) + eventData.ActualStartTime + ?? eventData.ScheduledStartTime).ToString())) .WithColour(ColorsList.Black) .WithCurrentTimestamp() .Build(); @@ -306,9 +316,45 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromError(completedEmbed); } - return (Result)await _channelApi.CreateMessageAsync( + var createResult = (Result)await _channelApi.CreateMessageAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), embeds: new[] { completedBuilt }, ct: ct); + if (createResult.IsSuccess) + { + data.ScheduledEvents.Remove(eventData.Id); + } + + return createResult; + } + + private async Task SendScheduledEventCancelledMessage(ScheduledEventData eventData, GuildData data, + CancellationToken ct) + { + if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + { + return Result.FromSuccess(); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventCancelled, eventData.Name)) + .WithDescription(":(") + .WithColour(ColorsList.Red) + .WithCurrentTimestamp() + .Build(); + + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } + + var createResult = (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), embeds: new[] { built }, ct: ct); + if (createResult.IsSuccess) + { + data.ScheduledEvents.Remove(eventData.Id); + } + + return createResult; } private static Result GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) From ef5410b7bbc0c4cdcdcf467fb1d62cc6d17f4285 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Mon, 14 Aug 2023 19:39:03 +0300 Subject: [PATCH 136/329] Update README.md due to #90 (#91) completely forgot about README.md while i was reviewing #90 Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index e6d35fb..aaa0782 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@

![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) -![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/build-push.yml?branch=master) ![GitHub last commit](https://img.shields.io/github/last-commit/TeamOctolings/Boyfriend) Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and From 0bf61ecf397769706a1287316db1689bc1ccfc7a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 15 Aug 2023 10:23:58 +0500 Subject: [PATCH 137/329] Add a JSON deserialization constructor for ScheduledEventData (#92) This PR fixes an exception that would occur when deserialization of ScheduledEventData would be attempted. The exception is fixed by providing a constructor containing all properties and adding the `[JsonConstructor]` attribute. Signed-off-by: Octol1ttle --- src/Data/ScheduledEventData.cs | 20 ++++++++++++++++--- src/Responders/GuildLoadedResponder.cs | 2 +- .../ScheduledEventCreatedResponder.cs | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Data/ScheduledEventData.cs b/src/Data/ScheduledEventData.cs index 8af9c92..f03daee 100644 --- a/src/Data/ScheduledEventData.cs +++ b/src/Data/ScheduledEventData.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using Remora.Discord.API.Abstractions.Objects; namespace Boyfriend.Data; @@ -8,13 +9,26 @@ namespace Boyfriend.Data; /// This information is stored on disk as a JSON file. public sealed class ScheduledEventData { - public ScheduledEventData(ulong id, string name, GuildScheduledEventStatus status, - DateTimeOffset scheduledStartTime) + public ScheduledEventData(ulong id, string name, DateTimeOffset scheduledStartTime, + GuildScheduledEventStatus status) { Id = id; Name = name; - Status = status; ScheduledStartTime = scheduledStartTime; + Status = status; + } + + [JsonConstructor] + public ScheduledEventData(ulong id, string name, bool earlyNotificationSent, DateTimeOffset scheduledStartTime, + DateTimeOffset? actualStartTime, GuildScheduledEventStatus? status, bool scheduleOnStatusUpdated) + { + Id = id; + Name = name; + EarlyNotificationSent = earlyNotificationSent; + ScheduledStartTime = scheduledStartTime; + ActualStartTime = actualStartTime; + Status = status; + ScheduleOnStatusUpdated = scheduleOnStatusUpdated; } public ulong Id { get; } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index a5b8b19..b3288c0 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -55,7 +55,7 @@ public class GuildLoadedResponder : IResponder if (!data.ScheduledEvents.TryGetValue(schEvent.ID.Value, out var eventData)) { data.ScheduledEvents.Add(schEvent.ID.Value, new ScheduledEventData(schEvent.ID.Value, - schEvent.Name, schEvent.Status, schEvent.ScheduledStartTime)); + schEvent.Name, schEvent.ScheduledStartTime, schEvent.Status)); continue; } diff --git a/src/Responders/ScheduledEventCreatedResponder.cs b/src/Responders/ScheduledEventCreatedResponder.cs index 36f313a..3541237 100644 --- a/src/Responders/ScheduledEventCreatedResponder.cs +++ b/src/Responders/ScheduledEventCreatedResponder.cs @@ -25,7 +25,7 @@ public class ScheduledEventCreatedResponder : IResponder Date: Sun, 20 Aug 2023 23:27:16 +0300 Subject: [PATCH 138/329] Change organization name references from TeamOctolings to LabsDevelopment (#93) we are moving! (6 commits because I was too lazy to open the IDE) --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- .github/CODEOWNERS | 4 ++-- Boyfriend.csproj | 6 +++--- docs/CONTRIBUTING.md | 2 +- docs/README.md | 11 +++++------ src/Commands/AboutCommandGroup.cs | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index eeccdc0..7c34266 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -* @TeamOctolings/boyfriend -/docs/ @TeamOctolings/boyfriend-docs +* @LabsDevelopment/boyfriend +/docs/ @LabsDevelopment/boyfriend-docs diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 6550e3e..b99a311 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -9,9 +9,9 @@ Boyfriend Octol1ttle, mctaylors, neroduckale AGPLv3 - https://github.com/TeamOctolings/Boyfriend - https://github.com/TeamOctolings/Boyfriend/blob/master/LICENSE - https://github.com/TeamOctolings/Boyfriend + https://github.com/LabsDevelopment/Boyfriend + https://github.com/LabsDevelopment/Boyfriend/blob/master/LICENSE + https://github.com/LabsDevelopment/Boyfriend github TeamOctolings en diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index bcaa25f..602e8de 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -29,7 +29,7 @@ While pull requests from unaffiliated contributors are welcome, please note that internal issues that haven't been published to the issue tracker yet. Reviewing PRs is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change. -The [issue tracker](https://github.com/TeamOctolings/Boyfriend/issues) should provide plenty of issues to start with. +The [issue tracker](https://github.com/LabsDevelopment/Boyfriend/issues) should provide plenty of issues to start with. Make sure to check that an issue you're planning to resolve does not already have people working on it and that there are no PRs associated with it diff --git a/docs/README.md b/docs/README.md index aaa0782..8544779 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,12 +2,11 @@ Boyfriend logo

-![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) -![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/build-push.yml?branch=master) -![GitHub last commit](https://img.shields.io/github/last-commit/TeamOctolings/Boyfriend) +![License](https://img.shields.io/github/license/LabsDevelopment/Boyfriend) +![Workflow Status](https://img.shields.io/github/actions/workflow/status/LabsDevelopment/Boyfriend/.github/workflows/build-push.yml?branch=master&logo=ReSharper) +![Last Commit](https://img.shields.io/github/last-commit/LabsDevelopment/Boyfriend) -Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and -Remora.Discord +Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and Remora.Discord ## Features @@ -20,7 +19,7 @@ Remora.Discord ## Installing and running Boyfriend -You can read our [wiki](https://github.com/TeamOctolings/Boyfriend/wiki) in order to assemble your Boyfriend™ and +You can read our [wiki](https://github.com/LabsDevelopment/Boyfriend/wiki) in order to assemble your Boyfriend™ and moderate the server. ## Contributing diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 34ba68d..d9f659e 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -79,7 +79,7 @@ public class AboutCommandGroup : CommandGroup builder.AppendLine() .AppendLine(Markdown.Bold(Messages.AboutTitleWiki)) - .AppendLine("https://github.com/TeamOctolings/Boyfriend/wiki"); + .AppendLine("https://github.com/LabsDevelopment/Boyfriend/wiki"); var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser) .WithDescription(builder.ToString()) From 324f4554041a0df460fb09273fa283292da91989 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 22 Aug 2023 04:18:58 +0500 Subject: [PATCH 139/329] Rename settings commands (#94) This PR renames commands `/settingslist` and `/settings` to `/listsettings` and `/editsettings` respectively. This helps avoid confusion and accidental use of the wrong command while conforming to a common naming style, similar to remind commands. cc @mctaylors wiki needs updating --------- Signed-off-by: Octol1ttle --- src/Commands/SettingsCommandGroup.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 61be402..fab90c4 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -68,16 +68,15 @@ public class SettingsCommandGroup : CommandGroup /// /// A feedback sending result which may or may not have succeeded. /// - [Command("settingslist")] + [Command("listsettings")] [DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.ManageGuild)] [Description("Shows settings list for this server")] [UsedImplicitly] - public async Task ExecuteSettingsListAsync( - [Description("Settings list page")] - [MinValue(1)] + public async Task ExecuteListSettingsAsync( + [Description("Settings list page")] [MinValue(1)] int page) { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) @@ -150,14 +149,14 @@ public class SettingsCommandGroup : CommandGroup /// The setting to modify. /// The new value of the setting. /// A feedback sending result which may or may not have succeeded. - [Command("settings")] + [Command("editsettings")] [DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.ManageGuild)] [Description("Change settings for this server")] [UsedImplicitly] - public async Task ExecuteSettingsAsync( + public async Task ExecuteEditSettingsAsync( [Description("The setting whose value you want to change")] string setting, [Description("Setting value")] string value) From 37ebf0ffa041f1020dce5f6dfd2e6a99ef6c7853 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 22 Aug 2023 12:44:05 +0500 Subject: [PATCH 140/329] Add periodic guild data saving (every 5 minutes) (#99) This PR adds a BackgroundGuildDataSaverService which will save all guild data to disk every 5 minutes using a PeriodicTimer. Closes #96 Signed-off-by: Octol1ttle --- src/Boyfriend.cs | 1 + .../BackgroundGuildDataSaverService.cs | 23 +++++++++++++++++++ src/Services/GuildDataService.cs | 12 ++++++---- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/Services/BackgroundGuildDataSaverService.cs diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index 7fdbbb2..246869e 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -92,6 +92,7 @@ public sealed class Boyfriend .AddHostedService() .AddHostedService() .AddHostedService() + .AddHostedService() // Slash commands .AddCommandTree() .WithCommandGroup() diff --git a/src/Services/BackgroundGuildDataSaverService.cs b/src/Services/BackgroundGuildDataSaverService.cs new file mode 100644 index 0000000..76a7ddb --- /dev/null +++ b/src/Services/BackgroundGuildDataSaverService.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Hosting; + +namespace Boyfriend.Services; + +public sealed class BackgroundGuildDataSaverService : BackgroundService +{ + private readonly GuildDataService _guildData; + + public BackgroundGuildDataSaverService(GuildDataService guildData) + { + _guildData = guildData; + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); + + while (await timer.WaitForNextTickAsync(ct)) + { + await _guildData.SaveAsync(ct); + } + } +} diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 0ded110..26147d2 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using Boyfriend.Data; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Remora.Discord.API.Abstractions.Rest; using Remora.Rest.Core; @@ -15,12 +16,14 @@ public sealed class GuildDataService : IHostedService { private readonly ConcurrentDictionary _datas = new(); private readonly IDiscordRestGuildAPI _guildApi; + private readonly ILogger _logger; // https://github.com/dotnet/aspnetcore/issues/39139 public GuildDataService( - IHostApplicationLifetime lifetime, IDiscordRestGuildAPI guildApi) + IHostApplicationLifetime lifetime, IDiscordRestGuildAPI guildApi, ILogger logger) { _guildApi = guildApi; + _logger = logger; lifetime.ApplicationStopping.Register(ApplicationStopping); } @@ -39,8 +42,9 @@ public sealed class GuildDataService : IHostedService SaveAsync(CancellationToken.None).GetAwaiter().GetResult(); } - private async Task SaveAsync(CancellationToken ct) + public async Task SaveAsync(CancellationToken ct) { + _logger.LogInformation("Saving guild data..."); var tasks = new List(); foreach (var data in _datas.Values) { @@ -89,7 +93,7 @@ public sealed class GuildDataService : IHostedService await using var eventsStream = File.OpenRead(scheduledEventsPath); var events - = JsonSerializer.DeserializeAsync>( + = await JsonSerializer.DeserializeAsync>( eventsStream, cancellationToken: ct); var memberData = new Dictionary(); @@ -113,7 +117,7 @@ public sealed class GuildDataService : IHostedService var finalData = new GuildData( jsonSettings ?? new JsonObject(), settingsPath, - await events ?? new Dictionary(), scheduledEventsPath, + events ?? new Dictionary(), scheduledEventsPath, memberData, memberDataPath); _datas.TryAdd(guildId, finalData); From 5831f5205ca148ebc3cbfb01cd1371b800a6524f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 22 Aug 2023 12:45:46 +0500 Subject: [PATCH 141/329] Add autocomplete for /editsettings setting keys (#98) This PR adds autocomplete for setting keys in `/editsetting` slash command. The usage of options provided by auto-complete is enforced client-side. Closes #97 Closes #95 Signed-off-by: Octol1ttle --- src/Commands/SettingsCommandGroup.cs | 17 ++++++++++------ src/Data/Options/AllOptionsEnum.cs | 29 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 src/Data/Options/AllOptionsEnum.cs diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index fab90c4..dcb0a5f 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -26,6 +26,13 @@ namespace Boyfriend.Commands; [UsedImplicitly] public class SettingsCommandGroup : CommandGroup { + /// + /// Represents all options as an array of objects implementing . + /// + /// + /// WARNING: If you update this array in any way, you must also update and make sure + /// that the orders match. + /// private static readonly IOption[] AllOptions = { GuildSettings.Language, @@ -158,7 +165,7 @@ public class SettingsCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteEditSettingsAsync( [Description("The setting whose value you want to change")] - string setting, + AllOptionsEnum setting, [Description("Setting value")] string value) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) @@ -181,16 +188,14 @@ public class SettingsCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await EditSettingAsync(setting, value, data, channelId, user, currentUser, CancellationToken); + return await EditSettingAsync(AllOptions[(int)setting], value, data, channelId, user, currentUser, + CancellationToken); } private async Task EditSettingAsync( - string setting, string value, GuildData data, Snowflake channelId, IUser user, IUser currentUser, + IOption option, string value, GuildData data, Snowflake channelId, IUser user, IUser currentUser, CancellationToken ct = default) { - var option = AllOptions.Single( - o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase)); - var setResult = option.Set(data.Settings, value); if (!setResult.IsSuccess) { diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs new file mode 100644 index 0000000..d0b1ae7 --- /dev/null +++ b/src/Data/Options/AllOptionsEnum.cs @@ -0,0 +1,29 @@ +using Boyfriend.Commands; +using JetBrains.Annotations; + +namespace Boyfriend.Data.Options; + +/// +/// Represents all options as enums. +/// +/// +/// WARNING: This enum is order-dependent! It's values are used as indexes for +/// . +/// +public enum AllOptionsEnum +{ + [UsedImplicitly] Language, + [UsedImplicitly] WelcomeMessage, + [UsedImplicitly] ReceiveStartupMessages, + [UsedImplicitly] RemoveRolesOnMute, + [UsedImplicitly] ReturnRolesOnRejoin, + [UsedImplicitly] AutoStartEvents, + [UsedImplicitly] RenameHoistedUsers, + [UsedImplicitly] PublicFeedbackChannel, + [UsedImplicitly] PrivateFeedbackChannel, + [UsedImplicitly] EventNotificationChannel, + [UsedImplicitly] DefaultRole, + [UsedImplicitly] MuteRole, + [UsedImplicitly] EventNotificationRole, + [UsedImplicitly] EventEarlyNotificationOffset +} From 31968837e51516e1df3fdad4833d78e0c668a5cf Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 22 Aug 2023 12:46:57 +0500 Subject: [PATCH 142/329] Fix JSON corruption when saving (#100) This PR fixes an issue that caused guild data JSON files to be corrupted upon saving. As it turns out `File.OpenWrite(string)` does not clear the file before writing to it. That means, if a file contains `{"MyKey": "MyValue"}` and I write `{}` to it using `File.OpenWrite(string)`, the contents of the file will be `{}MyKey": "MyValue"}`. This is a malformed JSON and will cause an error upon next bot startup. In addition, this PR blacklists the `File.OpenWrite` method using `CodeAnalysis/BannedSymbols.txt` to prevent its accidental use in the future. Signed-off-by: Octol1ttle --- CodeAnalysis/BannedSymbols.txt | 1 + src/Services/GuildDataService.cs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index f664b89..0a1ec81 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -17,3 +17,4 @@ M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberiz P:System.DateTime.Now;Use System.DateTime.UtcNow instead. P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead. P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead. +M:System.IO.File.OpenWrite(System.String);File.OpenWrite(string) does not clear the file before writing to it. Use File.Create(string) instead. diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 26147d2..796b598 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -48,15 +48,15 @@ public sealed class GuildDataService : IHostedService var tasks = new List(); foreach (var data in _datas.Values) { - await using var settingsStream = File.OpenWrite(data.SettingsPath); + await using var settingsStream = File.Create(data.SettingsPath); tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct)); - await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath); + await using var eventsStream = File.Create(data.ScheduledEventsPath); tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct)); foreach (var memberData in data.MemberData.Values) { - await using var memberDataStream = File.OpenWrite($"{data.MemberDataPath}/{memberData.Id}.json"); + await using var memberDataStream = File.Create($"{data.MemberDataPath}/{memberData.Id}.json"); tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct)); } } From 5fd116d0e2e442150630e2e312e6eff42008b5b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 11:35:49 +0500 Subject: [PATCH 143/329] Bump muno92/resharper_inspectcode from 1.8.0 to 1.8.2 (#101) --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index dc91660..7008227 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v3 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.8.0 + uses: muno92/resharper_inspectcode@1.8.2 with: solutionPath: ./Boyfriend.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement From 4ccf40bf350b5735e8d6fd339c7f9adf99448444 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 4 Sep 2023 22:59:30 +0500 Subject: [PATCH 144/329] Allow better customisation of autodeploy through secrets and variables (#102) The production environment and host are ready for this PR Signed-off-by: Octol1ttle --- .github/workflows/build-push.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 731201d..36ae3de 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -21,7 +21,9 @@ jobs: uses: actions/checkout@v3 - name: Publish solution - run: dotnet publish -c Release -r linux-x64 --no-self-contained -p:PublishReadyToRun=true + run: dotnet publish $PUBLISH_FLAGS + env: + PUBLISH_FLAGS: ${{vars.PUBLISH_FLAGS}} - name: Setup SSH key run: | @@ -33,29 +35,30 @@ jobs: SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}} SSH_HOST: ${{secrets.SSH_HOST}} - - name: Quit currently running instance - continue-on-error: true + - name: Stop currently running instance run: | - ssh $SSH_USER@$SSH_HOST pkill --signal SIGQUIT Boyfriend + ssh $SSH_USER@$SSH_HOST $STOP_COMMAND shell: bash env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} + STOP_COMMAND: ${{vars.STOP_COMMAND}} - name: Upload published solution run: | - scp -r bin/Release/net7.0/linux-x64/publish/* $SSH_USER@$SSH_HOST:$UPLOAD_DESTINATION + scp -r $UPLOAD_FROM $SSH_USER@$SSH_HOST:$UPLOAD_TO shell: bash env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} - UPLOAD_DESTINATION: ${{secrets.UPLOAD_DESTINATION}} + UPLOAD_FROM: ${{vars.UPLOAD_FROM}} + UPLOAD_TO: ${{vars.UPLOAD_TO}} - - name: Start uploaded solution + - name: Start new instance run: | - ssh $SSH_USER@$SSH_HOST $COMMAND + ssh $SSH_USER@$SSH_HOST $START_COMMAND shell: bash env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} - COMMAND: ${{secrets.COMMAND}} + START_COMMAND: ${{vars.START_COMMAND}} From f0a449d26c33c0ad5eb0557b65cd2d3d24e6cd22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:18:03 +0300 Subject: [PATCH 145/329] Bump muno92/resharper_inspectcode from 1.8.2 to 1.8.3 (#103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.8.2 to 1.8.3.
Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.8.3 - 2023-09-07

Commits
  • 35b207e Merge pull request #414 from muno92/tagpr-from-1.8.2
  • 67310c4 Compile
  • 27edfc6 [tagpr] update CHANGELOG.md
  • aa84292 [tagpr] prepare for the next release
  • e3bdd8b Merge pull request #413 from muno92/renovate/all-minor-patch
  • 9b9a73f Update dependency @​vercel/ncc to ^0.38.0
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.8.2&new-version=1.8.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 7008227..0de52fc 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v3 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.8.2 + uses: muno92/resharper_inspectcode@1.8.3 with: solutionPath: ./Boyfriend.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement From 81595e58d364eda31df8339956d455ae8ec8afa3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:20:03 +0300 Subject: [PATCH 146/329] Bump actions/checkout from 3 to 4 (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
Release notes

Sourced from actions/checkout's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v4.0.0

v3.6.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3.5.3...v3.6.0

v3.5.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v3.5.3

v3.5.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v3.5.1...v3.5.2

v3.5.1

What's Changed

New Contributors

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v4.0.0

v3.6.0

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

v3.0.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- .github/workflows/build-push.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 0de52fc..c806834 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: ReSharper CLI InspectCode uses: muno92/resharper_inspectcode@1.8.3 diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 36ae3de..13597b2 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Publish solution run: dotnet publish $PUBLISH_FLAGS From 438ecfb41be09a9ea7aa3177a0e48468ea77cab0 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 12 Sep 2023 18:28:46 +0500 Subject: [PATCH 147/329] Force enumeration of global collections before using them (#106) This PR closes #105 This PR fixes exceptions caused by changing a collection's contents while it is being enumerated. This can often happen with Guild- and MemberDatas. By using `ToArray()` on these global collections and using it in the `foreach` loop, we create a new copy of the collection, preventing any modification to it. While this does introduce a lot of memory allocations, there is no fixing that. Usually, the fix to these exceptions would be to convert the `foreach` to a reverse-`for`. However, because indices cannot be used on these collections, that is not possible. Signed-off-by: Octol1ttle --- src/Commands/RemindCommandGroup.cs | 5 ++--- src/Services/GuildDataService.cs | 6 ++++-- src/Services/Update/MemberUpdateService.cs | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 263d1ba..d437b5d 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -85,7 +85,7 @@ public class RemindCommandGroup : CommandGroup } var builder = new StringBuilder(); - for (var i = 0; i < data.Reminders.Count; i++) + for (var i = data.Reminders.Count - 1; i >= 0; i--) { var reminder = data.Reminders[i]; builder.AppendLine( @@ -168,8 +168,7 @@ public class RemindCommandGroup : CommandGroup [RequireContext(ChannelContext.Guild)] [UsedImplicitly] public async Task ExecuteDeleteReminderAsync( - [Description("Index of reminder to delete")] - [MinValue(0)] + [Description("Index of reminder to delete")] [MinValue(0)] int index) { if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 796b598..296377a 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -46,7 +46,8 @@ public sealed class GuildDataService : IHostedService { _logger.LogInformation("Saving guild data..."); var tasks = new List(); - foreach (var data in _datas.Values) + var datas = _datas.Values.ToArray(); + foreach (var data in datas) { await using var settingsStream = File.Create(data.SettingsPath); tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct)); @@ -54,7 +55,8 @@ public sealed class GuildDataService : IHostedService await using var eventsStream = File.Create(data.ScheduledEventsPath); tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct)); - foreach (var memberData in data.MemberData.Values) + var memberDatas = data.MemberData.Values.ToArray(); + foreach (var memberData in memberDatas) { await using var memberDataStream = File.Create($"{data.MemberDataPath}/{memberData.Id}.json"); tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct)); diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 472f844..631cc50 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -63,7 +63,8 @@ public sealed partial class MemberUpdateService : BackgroundService var guildData = await _guildData.GetData(guildId, ct); var defaultRole = GuildSettings.DefaultRole.Get(guildData.Settings); var failedResults = new List(); - foreach (var data in guildData.MemberData.Values) + var memberDatas = guildData.MemberData.Values.ToArray(); + foreach (var data in memberDatas) { var tickResult = await TickMemberDataAsync(guildId, guildData, defaultRole, data, ct); failedResults.AddIfFailed(tickResult); From 1225ff83d4743882c20e540e3b5dd4c81219480f Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:24:55 +0300 Subject: [PATCH 148/329] Add MORE music to the bot (#107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Music added: - EDWXRDX - CONSCIENCE (2:16) - dontleaveme - afterward (2:29) - Clams Casino - I'm God (4:37) - Jupyter - Starboy (2:35) - VØJ, Narvent - Memory Reboot (3:29) Timecode source: [Yandex.Music](https://music.yandex.ru) --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- src/Services/Update/SongUpdateService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index f369c0f..f807655 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -21,7 +21,12 @@ public sealed class SongUpdateService : BackgroundService ("SCATTLE - Hypertension", new TimeSpan(0, 3, 18)), ("KEYGEN CHURCH - Tenebre Rosso Sangue", new TimeSpan(0, 3, 53)), ("Chipzel - Swing Me Another 6", new TimeSpan(0, 5, 32)), - ("Noisecream - Mist of Rage", new TimeSpan(0, 2, 25)) + ("Noisecream - Mist of Rage", new TimeSpan(0, 2, 25)), + ("EDWXRDX - CONSCIENCE", new TimeSpan(0, 2, 16)), + ("dontleaveme - afterward", new TimeSpan(0, 2, 29)), + ("Ferdous - Gravity", new TimeSpan(0, 4, 37)), + ("The Drums - Money", new TimeSpan(0, 2, 35)), + ("Derek Pope - War Machine", new TimeSpan(0, 3, 29)) }; private readonly List _activityList = new(1) From b796b885a17fa2b592709583b8132c5195c75d2d Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Tue, 19 Sep 2023 04:23:02 +0300 Subject: [PATCH 149/329] Change logo CDN (#108) Signed-off-by: Macintosh II --- docs/README.md | 2 +- src/Commands/AboutCommandGroup.cs | 2 +- src/Services/Update/SongUpdateService.cs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index 8544779..381d541 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@

- Boyfriend logo + Boyfriend banner

![License](https://img.shields.io/github/license/LabsDevelopment/Boyfriend) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index d9f659e..4ad428a 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -84,7 +84,7 @@ public class AboutCommandGroup : CommandGroup var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png") + .WithImageUrl("https://mctaylors.ddns.net/cdn/boyfriend-banner-light.png") .Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct); diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index f807655..61bda52 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -24,9 +24,9 @@ public sealed class SongUpdateService : BackgroundService ("Noisecream - Mist of Rage", new TimeSpan(0, 2, 25)), ("EDWXRDX - CONSCIENCE", new TimeSpan(0, 2, 16)), ("dontleaveme - afterward", new TimeSpan(0, 2, 29)), - ("Ferdous - Gravity", new TimeSpan(0, 4, 37)), - ("The Drums - Money", new TimeSpan(0, 2, 35)), - ("Derek Pope - War Machine", new TimeSpan(0, 3, 29)) + ("Ferdous - Gravity", new TimeSpan(0, 2, 38)), + ("The Drums - Money", new TimeSpan(0, 3, 53)), + ("Derek Pope - War Machine", new TimeSpan(0, 3, 39)) }; private readonly List _activityList = new(1) From 1e8b7e53737dab676d5e99550cf549c53fe19d35 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:16:09 +0300 Subject: [PATCH 150/329] Add mute role support & fix /unmute (#109) - Added support for `MuteRole`, now if you add any role to this setting, then try to mute a member, all his roles will be removed except for the one you set in this setting. - Fixed `/unmute`, that tried to set target's display name to unmute reason. --------- Signed-off-by: Macintosh II --- src/Commands/MuteCommandGroup.cs | 138 +++++++++++++++++- src/Data/MemberData.cs | 1 + .../GuildMemberRolesUpdatedResponder.cs | 6 +- src/Services/Update/MemberUpdateService.cs | 43 ++++-- 4 files changed, 172 insertions(+), 16 deletions(-) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index ce70d68..bc64a64 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -101,11 +101,17 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } - return await MuteUserAsync( + if (GuildSettings.MuteRole.Get(data.Settings) != 0) + { + return await RoleMuteUserAsync( + target, reason, duration, guildId, data, channelId, user, currentUser, CancellationToken); + } + + return await TimeoutUserAsync( target, reason, duration, guildId, data, channelId, user, currentUser, CancellationToken); } - private async Task MuteUserAsync( + private async Task RoleMuteUserAsync( IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, IUser currentUser, CancellationToken ct = default) { @@ -125,10 +131,80 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } + var until = DateTimeOffset.UtcNow.Add(duration); // >:) + var memberData = data.GetOrCreateMemberData(target.ID); + memberData.MutedUntil = until; + var assignRoles = new List + { + GuildSettings.MuteRole.Get(data.Settings) + }; + if (!GuildSettings.RemoveRolesOnMute.Get(data.Settings)) + { + assignRoles.AddRange(memberData.Roles.ConvertAll(r => r.ToSnowflake())); + } + + var muteResult = await _guildApi.ModifyGuildMemberAsync( + guildId, target.ID, roles: assignRoles, + reason: $"({user.GetTag()}) {reason}".EncodeHeader(), ct: ct); + if (!muteResult.IsSuccess) + { + return Result.FromError(muteResult.Error); + } + + var title = string.Format(Messages.UserMuted, target.GetTag()); + var description = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)) + .Append( + string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); + + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); + if (!logResult.IsSuccess) + { + return Result.FromError(logResult.Error); + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserMuted, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } + + private async Task TimeoutUserAsync( + IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, + IUser user, IUser currentUser, CancellationToken ct = default) + { + if (duration.TotalDays >= 28) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.BotCannotMuteTarget, currentUser) + .WithDescription(Messages.DurationRequiredForTimeOuts) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, CancellationToken); + } + + var interactionResult + = await _utility.CheckInteractionsAsync( + guildId, user.ID, target.ID, "Mute", ct); + if (!interactionResult.IsSuccess) + { + return Result.FromError(interactionResult); + } + + if (interactionResult.Entity is not null) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + } + var until = DateTimeOffset.UtcNow.Add(duration); // >:) var muteResult = await _guildApi.ModifyGuildMemberAsync( guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), communicationDisabledUntil: until, ct: ct); + if (!muteResult.IsSuccess) { return Result.FromError(muteResult.Error); @@ -211,11 +287,63 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } - return await UnmuteUserAsync( + if (data.GetOrCreateMemberData(target.ID).MutedUntil is not null) + { + return await RemoveMuteRoleUserAsync( + target, reason, guildId, data, channelId, user, currentUser, CancellationToken); + } + + return await RemoveTimeoutUserAsync( target, reason, guildId, data, channelId, user, currentUser, CancellationToken); } - private async Task UnmuteUserAsync( + private async Task RemoveMuteRoleUserAsync( + IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, + IUser currentUser, CancellationToken ct = default) + { + var interactionResult + = await _utility.CheckInteractionsAsync( + guildId, user.ID, target.ID, "Unmute", ct); + if (!interactionResult.IsSuccess) + { + return Result.FromError(interactionResult); + } + + if (interactionResult.Entity is not null) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + } + + var memberData = data.GetOrCreateMemberData(target.ID); + var unmuteResult = await _guildApi.ModifyGuildMemberAsync( + guildId, target.ID, roles: memberData.Roles.ConvertAll(r => r.ToSnowflake()), + reason: $"({user.GetTag()}) {reason}".EncodeHeader(), ct: ct); + memberData.MutedUntil = null; + if (!unmuteResult.IsSuccess) + { + return Result.FromError(unmuteResult.Error); + } + + var title = string.Format(Messages.UserUnmuted, target.GetTag()); + var description = string.Format(Messages.DescriptionActionReason, reason); + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); + if (!logResult.IsSuccess) + { + return Result.FromError(logResult.Error); + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserUnmuted, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } + + private async Task RemoveTimeoutUserAsync( IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, IUser currentUser, CancellationToken ct = default) { @@ -236,7 +364,7 @@ public class MuteCommandGroup : CommandGroup } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( - guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), communicationDisabledUntil: null, ct: ct); if (!unmuteResult.IsSuccess) { diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 1ff2bc0..72a9ee1 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -13,6 +13,7 @@ public sealed class MemberData public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } + public DateTimeOffset? MutedUntil { get; set; } public List Roles { get; set; } = new(); public List Reminders { get; } = new(); } diff --git a/src/Responders/GuildMemberRolesUpdatedResponder.cs b/src/Responders/GuildMemberRolesUpdatedResponder.cs index dbd8b3a..eade781 100644 --- a/src/Responders/GuildMemberRolesUpdatedResponder.cs +++ b/src/Responders/GuildMemberRolesUpdatedResponder.cs @@ -23,7 +23,11 @@ public class GuildMemberUpdateResponder : IResponder public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) { var memberData = await _guildData.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); - memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value); + if (memberData.MutedUntil is null) + { + memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value); + } + return Result.FromSuccess(); } } diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 631cc50..d4424ec 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -79,17 +79,9 @@ public sealed partial class MemberUpdateService : BackgroundService { var failedResults = new List(); var id = data.Id.ToSnowflake(); - if (DateTimeOffset.UtcNow > data.BannedUntil) - { - var unbanResult = await _guildApi.RemoveGuildBanAsync( - guildId, id, Messages.PunishmentExpired.EncodeHeader(), ct); - if (unbanResult.IsSuccess) - { - data.BannedUntil = null; - } - return unbanResult; - } + var punishmentsResult = await CheckMemberPunishmentsAsync(guildId, id, data, ct); + failedResults.AddIfFailed(punishmentsResult); if (defaultRole.Value is not 0 && !data.Roles.Contains(defaultRole.Value)) { @@ -125,6 +117,37 @@ public sealed partial class MemberUpdateService : BackgroundService return failedResults.AggregateErrors(); } + private async Task CheckMemberPunishmentsAsync( + Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) + { + if (DateTimeOffset.UtcNow > data.BannedUntil) + { + var unbanResult = await _guildApi.RemoveGuildBanAsync( + guildId, id, Messages.PunishmentExpired.EncodeHeader(), ct); + if (unbanResult.IsSuccess) + { + data.BannedUntil = null; + } + + return unbanResult; + } + + if (DateTimeOffset.UtcNow > data.MutedUntil) + { + var unmuteResult = await _guildApi.ModifyGuildMemberAsync( + guildId, id, roles: data.Roles.ConvertAll(r => r.ToSnowflake()), + reason: Messages.PunishmentExpired.EncodeHeader(), ct: ct); + if (unmuteResult.IsSuccess) + { + data.MutedUntil = null; + } + + return unmuteResult; + } + + return Result.FromSuccess(); + } + private async Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, CancellationToken ct) { From 81099fad4c1c3653e2c84af6df9684480038aa88 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 22 Sep 2023 15:33:14 +0300 Subject: [PATCH 151/329] Move all GuildData to one folder (#110) Signed-off-by: Macintosh II Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> Co-authored-by: Octol1ttle --- src/Services/GuildDataService.cs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 296377a..e3c0b96 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -73,11 +73,14 @@ public sealed class GuildDataService : IHostedService private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) { - var idString = $"{guildId}"; - var memberDataPath = $"{guildId}/MemberData"; - var settingsPath = $"{guildId}/Settings.json"; - var scheduledEventsPath = $"{guildId}/ScheduledEvents.json"; - Directory.CreateDirectory(idString); + var path = $"GuildData/{guildId}"; + var memberDataPath = $"{path}/MemberData"; + var settingsPath = $"{path}/Settings.json"; + var scheduledEventsPath = $"{path}/ScheduledEvents.json"; + + MigrateGuildData(guildId, path); + + Directory.CreateDirectory(path); if (!File.Exists(settingsPath)) { @@ -127,6 +130,19 @@ public sealed class GuildDataService : IHostedService return finalData; } + private void MigrateGuildData(Snowflake guildId, string newPath) + { + var oldPath = $"{guildId}"; + + if (Directory.Exists(oldPath)) + { + Directory.CreateDirectory($"{newPath}/.."); + Directory.Move(oldPath, newPath); + + _logger.LogInformation("Moved guild data to separate folder: \"{OldPath}\" -> \"{NewPath}\"", oldPath, newPath); + } + } + public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) { return (await GetData(guildId, ct)).Settings; From d6e1468f3e02c31c2d8c48412b2993858c5eef07 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 22 Sep 2023 20:09:22 +0300 Subject: [PATCH 152/329] Disable the logging of `SaveAsync` due to spam in the logs (#113) Closes #112 Signed-off-by: Macintosh II --- src/Services/GuildDataService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index e3c0b96..b07755d 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -44,7 +44,6 @@ public sealed class GuildDataService : IHostedService public async Task SaveAsync(CancellationToken ct) { - _logger.LogInformation("Saving guild data..."); var tasks = new List(); var datas = _datas.Values.ToArray(); foreach (var data in datas) From 3a3865ba3dad413f0b08d9a6fb1d68d651ff8d30 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 22 Sep 2023 20:23:08 +0300 Subject: [PATCH 153/329] Add /resetsettings (#111) Signed-off-by: Macintosh II Signed-off-by: Octol1ttle Co-authored-by: Octol1ttle --- locale/Messages.resx | 6 +++ locale/Messages.ru.resx | 6 +++ locale/Messages.tt-ru.resx | 6 +++ src/Commands/SettingsCommandGroup.cs | 75 ++++++++++++++++++++++++++++ src/Data/Options/IOption.cs | 1 + src/Data/Options/Option.cs | 6 +++ src/Messages.Designer.cs | 12 +++++ 7 files changed, 112 insertions(+) diff --git a/locale/Messages.resx b/locale/Messages.resx index 82392a8..84d7fa7 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -594,4 +594,10 @@ You don't have any reminders created! + + Setting {0} reset + + + All settings have been reset + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 7b5b3d4..abeaf88 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -594,4 +594,10 @@ У вас нет созданных напоминаний! + + Настройка {0} сброшена + + + Все настройки были сброшены + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index bb8c204..fb01e72 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -594,4 +594,10 @@ ты еще не крафтил напоминалки + + {0} откачен к заводским + + + откатываемся к заводским... + diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index dcb0a5f..50ea955 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -229,4 +229,79 @@ public class SettingsCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct); } + + /// + /// A slash command that resets per-guild GuildSettings. + /// + /// The setting to reset. + /// A feedback sending result which may have succeeded. + [Command("resetsettings")] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.ManageGuild)] + [Description("Reset settings for this server")] + [UsedImplicitly] + public async Task ExecuteResetSettingsAsync( + [Description("Setting to reset")] AllOptionsEnum? setting = null) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } + + var cfg = await _guildData.GetSettings(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(cfg); + + if (setting is not null) + { + return await ResetSingleSettingAsync(cfg, currentUser, AllOptions[(int)setting], CancellationToken); + } + + return await ResetAllSettingsAsync(cfg, currentUser, CancellationToken); + } + + private async Task ResetSingleSettingAsync(JsonNode cfg, IUser currentUser, + IOption option, CancellationToken ct = default) + { + var resetResult = option.Reset(cfg); + if (!resetResult.IsSuccess) + { + return Result.FromError(resetResult.Error); + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.SingleSettingReset, option.Name), currentUser) + .WithColour(ColorsList.Green) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } + + private async Task ResetAllSettingsAsync(JsonNode cfg, IUser currentUser, + CancellationToken ct = default) + { + var failedResults = new List(); + foreach (var resetResult in AllOptions.Select(option => option.Reset(cfg))) + { + failedResults.AddIfFailed(resetResult); + } + + if (failedResults.Count is not 0) + { + return failedResults.AggregateErrors(); + } + + var embed = new EmbedBuilder().WithSmallTitle(Messages.AllSettingsReset, currentUser) + .WithColour(ColorsList.Green) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } } diff --git a/src/Data/Options/IOption.cs b/src/Data/Options/IOption.cs index 6f435e5..42138dc 100644 --- a/src/Data/Options/IOption.cs +++ b/src/Data/Options/IOption.cs @@ -8,4 +8,5 @@ public interface IOption string Name { get; } string Display(JsonNode settings); Result Set(JsonNode settings, string from); + Result Reset(JsonNode settings); } diff --git a/src/Data/Options/Option.cs b/src/Data/Options/Option.cs index c96b6ac..a62eb04 100644 --- a/src/Data/Options/Option.cs +++ b/src/Data/Options/Option.cs @@ -48,4 +48,10 @@ public class Option : IOption var property = settings[Name]; return property != null ? property.GetValue() : DefaultValue; } + + public Result Reset(JsonNode settings) + { + settings[Name] = null; + return Result.FromSuccess(); + } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 0fee435..b45ae70 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1001,5 +1001,17 @@ namespace Boyfriend { return ResourceManager.GetString("NoRemindersFound", resourceCulture); } } + + internal static string SingleSettingReset { + get { + return ResourceManager.GetString("SingleSettingReset", resourceCulture); + } + } + + internal static string AllSettingsReset { + get { + return ResourceManager.GetString("AllSettingsReset", resourceCulture); + } + } } } From 1ab5a640a9d272058f33fa33a6be2e8bbb70d78e Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 27 Sep 2023 12:47:43 +0300 Subject: [PATCH 154/329] Set MemberData.BannedUntil to null in /unban (#114) Please keep `MemberData` clean of unnecessary values. --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- src/Commands/BanCommandGroup.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 6d662c6..25cd7e8 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -264,6 +264,8 @@ public class BanCommandGroup : CommandGroup return Result.FromError(unbanResult.Error); } + data.GetOrCreateMemberData(target.ID).BannedUntil = null; + var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnbanned, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); From f2db7f016c9a495957cd997b7cb604acd9deeaec Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 27 Sep 2023 21:25:49 +0300 Subject: [PATCH 155/329] Fix UnknownMember warning flood (#115) If a user was muted using the `MuteRole` method and then banned, the UnknownMember warning will flood your logs when `DateTimeOffset.UtcNow > data.MutedUntil` becomes true, because there is no user in the server to unmute. --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- src/Services/Update/MemberUpdateService.cs | 67 ++++++++++++---------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index d4424ec..3e87b79 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -80,8 +80,17 @@ public sealed partial class MemberUpdateService : BackgroundService var failedResults = new List(); var id = data.Id.ToSnowflake(); - var punishmentsResult = await CheckMemberPunishmentsAsync(guildId, id, data, ct); - failedResults.AddIfFailed(punishmentsResult); + var autoUnbanResult = await TryAutoUnbanAsync(guildId, id, data, ct); + failedResults.AddIfFailed(autoUnbanResult); + + var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, id, ct); + if (!guildMemberResult.IsDefined(out var guildMember)) + { + return failedResults.AggregateErrors(); + } + + var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct); + failedResults.AddIfFailed(autoUnmuteResult); if (defaultRole.Value is not 0 && !data.Roles.Contains(defaultRole.Value)) { @@ -90,12 +99,6 @@ public sealed partial class MemberUpdateService : BackgroundService failedResults.AddIfFailed(addResult); } - var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, id, ct); - if (!guildMemberResult.IsDefined(out var guildMember)) - { - return failedResults.AggregateErrors(); - } - if (!guildMember.User.IsDefined(out var user)) { failedResults.AddIfFailed(new ArgumentNullError(nameof(guildMember.User))); @@ -117,35 +120,41 @@ public sealed partial class MemberUpdateService : BackgroundService return failedResults.AggregateErrors(); } - private async Task CheckMemberPunishmentsAsync( + private async Task TryAutoUnbanAsync( Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) { - if (DateTimeOffset.UtcNow > data.BannedUntil) + if (DateTimeOffset.UtcNow <= data.BannedUntil) { - var unbanResult = await _guildApi.RemoveGuildBanAsync( - guildId, id, Messages.PunishmentExpired.EncodeHeader(), ct); - if (unbanResult.IsSuccess) - { - data.BannedUntil = null; - } - - return unbanResult; + return Result.FromSuccess(); } - if (DateTimeOffset.UtcNow > data.MutedUntil) + var unbanResult = await _guildApi.RemoveGuildBanAsync( + guildId, id, Messages.PunishmentExpired.EncodeHeader(), ct); + if (unbanResult.IsSuccess) { - var unmuteResult = await _guildApi.ModifyGuildMemberAsync( - guildId, id, roles: data.Roles.ConvertAll(r => r.ToSnowflake()), - reason: Messages.PunishmentExpired.EncodeHeader(), ct: ct); - if (unmuteResult.IsSuccess) - { - data.MutedUntil = null; - } - - return unmuteResult; + data.BannedUntil = null; } - return Result.FromSuccess(); + return unbanResult; + } + + private async Task TryAutoUnmuteAsync( + Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) + { + if (DateTimeOffset.UtcNow <= data.MutedUntil) + { + return Result.FromSuccess(); + } + + var unmuteResult = await _guildApi.ModifyGuildMemberAsync( + guildId, id, roles: data.Roles.ConvertAll(r => r.ToSnowflake()), + reason: Messages.PunishmentExpired.EncodeHeader(), ct: ct); + if (unmuteResult.IsSuccess) + { + data.MutedUntil = null; + } + + return unmuteResult; } private async Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, From 906bfd07e8ee798526d7ea590500f46b4cacd2d8 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 27 Sep 2023 21:27:28 +0300 Subject: [PATCH 156/329] Skip refreshing roles if the member is role-muted (#118) Closes #116 --- src/Services/GuildDataService.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index b07755d..0833adf 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -110,10 +110,13 @@ public sealed class GuildDataService : IHostedService continue; } - var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct); - if (memberResult.IsSuccess) + if (data.MutedUntil is null) { - data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value); + var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct); + if (memberResult.IsSuccess) + { + data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value); + } } memberData.Add(data.Id, data); From e907930623020ff90c860152794641ac22a12c44 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 28 Sep 2023 00:07:46 +0500 Subject: [PATCH 157/329] Fix auto-unban and auto-unmute always triggering (#119) Flipping `>` to `<=` changed null handling semantics within the operator, causing the unban/unmute code to always run Signed-off-by: Octol1ttle --- src/Services/Update/MemberUpdateService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 3e87b79..3db307d 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -123,7 +123,7 @@ public sealed partial class MemberUpdateService : BackgroundService private async Task TryAutoUnbanAsync( Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) { - if (DateTimeOffset.UtcNow <= data.BannedUntil) + if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil) { return Result.FromSuccess(); } @@ -141,7 +141,7 @@ public sealed partial class MemberUpdateService : BackgroundService private async Task TryAutoUnmuteAsync( Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) { - if (DateTimeOffset.UtcNow <= data.MutedUntil) + if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil) { return Result.FromSuccess(); } From 4e4e60f845b79f6cfa66a4e8727a2856cfa7594b Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Thu, 28 Sep 2023 14:26:58 +0300 Subject: [PATCH 158/329] Fix mute role returning when rejoining server (#121) Closes #117 --------- Signed-off-by: Macintosh II Co-authored-by: Octol1ttle --- src/Responders/GuildMemberJoinedResponder.cs | 37 +++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 5699008..849ff5e 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using Boyfriend.Data; using Boyfriend.Services; using JetBrains.Annotations; @@ -5,6 +6,7 @@ using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; +using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Responders; @@ -40,15 +42,10 @@ public class GuildMemberJoinedResponder : IResponder var cfg = data.Settings; var memberData = data.GetOrCreateMemberData(user.ID); - if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) + var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct); + if (!returnRolesResult.IsSuccess) { - var result = await _guildApi.ModifyGuildMemberAsync( - gatewayEvent.GuildID, user.ID, - roles: memberData.Roles.ConvertAll(r => r.ToSnowflake()), ct: ct); - if (!result.IsSuccess) - { - return Result.FromError(result.Error); - } + return Result.FromError(returnRolesResult.Error); } if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() @@ -83,4 +80,28 @@ public class GuildMemberJoinedResponder : IResponder GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct); } + + private async Task TryReturnRolesAsync( + JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct) + { + if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg)) + { + return Result.FromSuccess(); + } + + var assignRoles = new List(); + + if (memberData.MutedUntil is null || !GuildSettings.RemoveRolesOnMute.Get(cfg)) + { + assignRoles.AddRange(memberData.Roles.ConvertAll(r => r.ToSnowflake())); + } + + if (memberData.MutedUntil is not null) + { + assignRoles.Add(GuildSettings.MuteRole.Get(cfg)); + } + + return await _guildApi.ModifyGuildMemberAsync( + guildId, userId, roles: assignRoles, ct: ct); + } } From 0f916d46dedd1e9de5378575b777300721632469 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:31:45 +0300 Subject: [PATCH 159/329] Add /showinfo (#122) Signed-off-by: Macintosh II Signed-off-by: Octol1ttle Co-authored-by: Octol1ttle --- locale/Messages.resx | 42 ++++++ locale/Messages.ru.resx | 42 ++++++ locale/Messages.tt-ru.resx | 42 ++++++ src/Boyfriend.cs | 3 +- src/Commands/ToolsCommandGroup.cs | 231 ++++++++++++++++++++++++++++++ src/Extensions.cs | 17 +++ src/Messages.Designer.cs | 84 +++++++++++ 7 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 src/Commands/ToolsCommandGroup.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index 84d7fa7..3d05722 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -600,4 +600,46 @@ All settings have been reset + + Display name + + + Information about {0} + + + Muted + + + Discord user since + + + Banned + + + Punishments + + + Banned permanently + + + Not in the guild + + + Muted by timeout + + + Muted by mute role + + + Guild member since + + + Nickname + + + Roles + + + Nitro booster since + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index abeaf88..3f20dd0 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -600,4 +600,46 @@ Все настройки были сброшены + + Отображаемое имя + + + Информация о {0} + + + Заглушен + + + Вступил в Discord + + + Забанен + + + Наказания + + + Забанен навсегда + + + Не на сервере + + + Заглушен с помощью тайм-аута + + + Заглушен с помощью роли мута + + + Вступил на сервер + + + Никнейм + + + Роли + + + Начал бустить сервер + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index fb01e72..a564e1c 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -600,4 +600,46 @@ откатываемся к заводским... + + дисплейнейм + + + деанон {0} + + + замучен + + + юзер Discord со времен + + + забанен + + + приколы полученные по заслугам + + + забанен + + + вышел из сервера + + + замучен таймаутом + + + замучен ролькой + + + участник сервера со времен + + + сервернейм + + + рольки + + + бустит сервер со времен + diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index 246869e..4ba43f8 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -102,7 +102,8 @@ public sealed class Boyfriend .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() - .WithCommandGroup(); + .WithCommandGroup() + .WithCommandGroup(); var responderTypes = typeof(Boyfriend).Assembly .GetExportedTypes() .Where(t => t.IsResponder()); diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs new file mode 100644 index 0000000..3ac8b70 --- /dev/null +++ b/src/Commands/ToolsCommandGroup.cs @@ -0,0 +1,231 @@ +using System.ComponentModel; +using System.Drawing; +using System.Text; +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Commands; + +/// +/// Handles commands related to tools: /showinfo. +/// +[UsedImplicitly] +public class ToolsCommandGroup : CommandGroup +{ + private readonly ICommandContext _context; + private readonly FeedbackService _feedback; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; + + public ToolsCommandGroup( + ICommandContext context, FeedbackService feedback, + GuildDataService guildData, IDiscordRestGuildAPI guildApi, + IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi) + { + _context = context; + _guildData = guildData; + _feedback = feedback; + _guildApi = guildApi; + _userApi = userApi; + } + + /// + /// A slash command that shows information about user. + /// + /// + /// Information in the output: + /// + /// Display name + /// Discord user since + /// Guild nickname + /// Guild member since + /// Nitro booster since + /// Guild roles + /// Active mute information + /// Active ban information + /// Is on guild status + /// + /// + /// The user to show info about. + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("showinfo")] + [DiscordDefaultDMPermission(false)] + [Description("Shows info about user")] + [UsedImplicitly] + public async Task ExecuteShowInfoAsync( + [Description("User to show info about")] + IUser? target = null) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); + if (!userResult.IsDefined(out var user)) + { + return Result.FromError(userResult); + } + + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await ShowUserInfoAsync(target ?? user, currentUser, data, guildId, CancellationToken); + } + + private async Task ShowUserInfoAsync( + IUser user, IUser currentUser, GuildData data, Snowflake guildId, CancellationToken ct = default) + { + var builder = new StringBuilder().AppendLine($"### <@{user.ID}>"); + + if (user.GlobalName is not null) + { + builder.Append("- ").AppendLine(Messages.ShowInfoDisplayName) + .AppendLine(Markdown.InlineCode(user.GlobalName)); + } + + builder.Append("- ").AppendLine(Messages.ShowInfoDiscordUserSince) + .AppendLine(Markdown.Timestamp(user.ID.Timestamp)); + + var memberData = data.GetOrCreateMemberData(user.ID); + + var embedColor = ColorsList.Cyan; + + var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, user.ID, ct); + DateTimeOffset? communicationDisabledUntil = null; + if (guildMemberResult.IsDefined(out var guildMember)) + { + communicationDisabledUntil = guildMember.CommunicationDisabledUntil.OrDefault(null); + + embedColor = AppendGuildInformation(embedColor, guildMember, builder); + } + + var isMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || + communicationDisabledUntil is not null; + + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, user.ID, ct); + + if (isMuted || existingBanResult.IsDefined()) + { + builder.Append("### ") + .AppendLine(Markdown.Bold(Messages.ShowInfoPunishments)); + } + + if (isMuted) + { + AppendMuteInformation(memberData, communicationDisabledUntil, builder); + + embedColor = ColorsList.Red; + } + + if (existingBanResult.IsDefined()) + { + AppendBanInformation(memberData, builder); + + embedColor = ColorsList.Black; + } + + if (!guildMemberResult.IsSuccess && !existingBanResult.IsDefined()) + { + builder.Append("### ") + .AppendLine(Markdown.Bold(Messages.ShowInfoNotOnGuild)); + + embedColor = ColorsList.Default; + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.ShowInfoTitle, user.GetTag()), currentUser) + .WithDescription(builder.ToString()) + .WithColour(embedColor) + .WithLargeAvatar(user) + .WithFooter($"ID: {user.ID.ToString()}") + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } + + private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder) + { + if (guildMember.Nickname.IsDefined(out var nickname)) + { + builder.Append("- ").AppendLine(Messages.ShowInfoGuildNickname) + .AppendLine(Markdown.InlineCode(nickname)); + } + + builder.Append("- ").AppendLine(Messages.ShowInfoGuildMemberSince) + .AppendLine(Markdown.Timestamp(guildMember.JoinedAt)); + + if (guildMember.PremiumSince.IsDefined(out var premiumSince)) + { + builder.Append("- ").AppendLine(Messages.ShowInfoGuildMemberPremiumSince) + .AppendLine(Markdown.Timestamp(premiumSince.Value)); + color = ColorsList.Magenta; + } + + if (guildMember.Roles.Count > 0) + { + builder.Append("- ").AppendLine(Messages.ShowInfoGuildRoles); + for (var i = 0; i < guildMember.Roles.Count - 1; i++) + { + builder.Append($"<@&{guildMember.Roles[i]}>, "); + } + + builder.AppendLine($"<@&{guildMember.Roles[^1]}>"); + } + + return color; + } + + private static void AppendBanInformation(MemberData memberData, StringBuilder builder) + { + if (memberData.BannedUntil < DateTimeOffset.MaxValue) + { + builder.Append("- ").AppendLine(Messages.ShowInfoBanned) + .Append(" - ").AppendLine(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.BannedUntil.Value))); + return; + } + + builder.Append("- ").AppendLine(Messages.ShowInfoBannedPermanently); + } + + private static void AppendMuteInformation( + MemberData memberData, DateTimeOffset? communicationDisabledUntil, StringBuilder builder) + { + builder.Append("- ").AppendLine(Messages.ShowInfoMuted); + if (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) + { + builder.Append(" - ").AppendLine(Messages.ShowInfoMutedByMuteRole) + .Append(" - ").AppendLine(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.MutedUntil.Value))); + } + + if (communicationDisabledUntil is not null) + { + builder.Append(" - ").AppendLine(Messages.ShowInfoMutedByTimeout) + .Append(" - ").AppendLine(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(communicationDisabledUntil.Value))); + } + } +} diff --git a/src/Extensions.cs b/src/Extensions.cs index 9a05e80..1e30cf4 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -74,6 +74,23 @@ public static class Extensions return builder; } + /// + /// Adds a user avatar in the thumbnail field. + /// + /// The builder to add the thumbnail to. + /// The user whose avatar to use in the thumbnail field. + /// The builder with the added avatar in the thumbnail field. + public static EmbedBuilder WithLargeAvatar( + this EmbedBuilder builder, IUser avatarSource) + { + var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); + var avatarUrl = avatarUrlResult.IsSuccess + ? avatarUrlResult.Entity + : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; + + return builder.WithThumbnailUrl(avatarUrl.AbsoluteUri); + } + /// /// Adds a footer representing that the action was performed in the . /// diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index b45ae70..884cfd4 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1013,5 +1013,89 @@ namespace Boyfriend { return ResourceManager.GetString("AllSettingsReset", resourceCulture); } } + + internal static string ShowInfoTitle { + get { + return ResourceManager.GetString("ShowInfoTitle", resourceCulture); + } + } + + internal static string ShowInfoDisplayName { + get { + return ResourceManager.GetString("ShowInfoDisplayName", resourceCulture); + } + } + + internal static string ShowInfoDiscordUserSince { + get { + return ResourceManager.GetString("ShowInfoDiscordUserSince", resourceCulture); + } + } + + internal static string ShowInfoMuted { + get { + return ResourceManager.GetString("ShowInfoMuted", resourceCulture); + } + } + + internal static string ShowInfoBanned { + get { + return ResourceManager.GetString("ShowInfoBanned", resourceCulture); + } + } + + internal static string ShowInfoPunishments { + get { + return ResourceManager.GetString("ShowInfoPunishments", resourceCulture); + } + } + + internal static string ShowInfoBannedPermanently { + get { + return ResourceManager.GetString("ShowInfoBannedPermanently", resourceCulture); + } + } + + internal static string ShowInfoNotOnGuild { + get { + return ResourceManager.GetString("ShowInfoNotOnGuild", resourceCulture); + } + } + + internal static string ShowInfoMutedByTimeout { + get { + return ResourceManager.GetString("ShowInfoMutedByTimeout", resourceCulture); + } + } + + internal static string ShowInfoMutedByMuteRole { + get { + return ResourceManager.GetString("ShowInfoMutedByMuteRole", resourceCulture); + } + } + + internal static string ShowInfoGuildMemberSince { + get { + return ResourceManager.GetString("ShowInfoGuildMemberSince", resourceCulture); + } + } + + internal static string ShowInfoGuildNickname { + get { + return ResourceManager.GetString("ShowInfoGuildNickname", resourceCulture); + } + } + + internal static string ShowInfoGuildRoles { + get { + return ResourceManager.GetString("ShowInfoGuildRoles", resourceCulture); + } + } + + internal static string ShowInfoGuildMemberPremiumSince { + get { + return ResourceManager.GetString("ShowInfoGuildMemberPremiumSince", resourceCulture); + } + } } } From d5c43402105b8f56b4e9cc64666fe8969c38d6bb Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:50:15 +0300 Subject: [PATCH 160/329] =?UTF-8?q?Add=20some=20Splatoon=E2=84=A2=20songs?= =?UTF-8?q?=20that=20I=20liked=20(#125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Deep Cut - Big Betrayal — [source](https://www.youtube.com/watch?v=BMy7gvl3bXE) - Squid Sisters - Tommorrow's Nostalgia Today — [source](https://www.youtube.com/watch?v=S8VYi2ODeF4) - Deep Cut - Anarchy Rainbow — [source](https://www.youtube.com/watch?v=DtMOAvOWTvY) - Squid Sisters - Liquid Sunshine — [source](https://www.youtube.com/watch?v=EZx61kSObTIg) - Damp Socks feat. Off the Hook - Candy-Coated Rocks — [source](https://youtube.com/watch?v=_-nyDSANZt4) - H2Whoa - Aquasonic — [source](https://www.youtube.com/watch?v=t2sNgpFgFC0) - Yoko & the Gold Bazookas - Ska-Blam! — [source](https://www.youtube.com/watch?v=9pHIwuTHcbc) --------- Signed-off-by: Macintosh II --- src/Services/Update/SongUpdateService.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index 61bda52..70a2d70 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -12,7 +12,7 @@ public sealed class SongUpdateService : BackgroundService { ("UNDEAD CORPORATION - The Empress", new TimeSpan(0, 4, 34)), ("UNDEAD CORPORATION - Everything will freeze", new TimeSpan(0, 3, 17)), - ("Splatoon 3 - Rockagilly Blues (Yoko & the Gold Bazookas)", new TimeSpan(0, 3, 37)), + ("Yoko & the Gold Bazookas - Rockagilly Blues ", new TimeSpan(0, 3, 37)), ("Splatoon 3 - Seep and Destroy", new TimeSpan(0, 2, 42)), ("IA - A Tale of Six Trillion Years and a Night", new TimeSpan(0, 3, 40)), ("Manuel - Gas Gas Gas", new TimeSpan(0, 3, 17)), @@ -26,7 +26,14 @@ public sealed class SongUpdateService : BackgroundService ("dontleaveme - afterward", new TimeSpan(0, 2, 29)), ("Ferdous - Gravity", new TimeSpan(0, 2, 38)), ("The Drums - Money", new TimeSpan(0, 3, 53)), - ("Derek Pope - War Machine", new TimeSpan(0, 3, 39)) + ("Derek Pope - War Machine", new TimeSpan(0, 3, 39)), + ("Deep Cut - Big Betrayal", new TimeSpan(0, 1, 42)), + ("Squid Sisters - Tomorrow's Nostalgia Today", new TimeSpan(0, 2, 8)), + ("Deep Cut - Anarchy Rainbow", new TimeSpan(0, 1, 51)), + ("Squid Sisters feat. Ian BGM - Liquid Sunshine", new TimeSpan(0, 1, 32)), + ("Damp Socks feat. Off the Hook - Candy-Coated Rocks", new TimeSpan(0, 1, 11)), + ("H2Whoa - Aquasonic", new TimeSpan(0, 1, 1)), // Add some Splatoon™ songs that *I* liked #125 + ("Yoko & the Gold Bazookas - Ska-Blam!", new TimeSpan(0, 4, 4)) }; private readonly List _activityList = new(1) From 04897cab2074df50d6e4ea7f96db4a882144b1e7 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 29 Sep 2023 18:36:16 +0300 Subject: [PATCH 161/329] Redesign embeds (#123) TODO before merging: - [x] /about - [x] /ban - [x] /unban - [x] /kick - [x] /mute - [x] /unmute - [x] /remind - [x] /listremind - [x] MessageEditedResponder - [x] MessageDeletedResponder --------- Signed-off-by: Macintosh II --- locale/Messages.resx | 624 ++++++++++----------- locale/Messages.ru.resx | 636 ++++++++++----------- locale/Messages.tt-ru.resx | 640 +++++++++++----------- src/Commands/AboutCommandGroup.cs | 31 +- src/Commands/BanCommandGroup.cs | 10 +- src/Commands/KickCommandGroup.cs | 4 +- src/Commands/MuteCommandGroup.cs | 18 +- src/Commands/RemindCommandGroup.cs | 16 +- src/Extensions.cs | 16 - src/Messages.Designer.cs | 40 +- src/Responders/MessageDeletedResponder.cs | 9 +- src/Responders/MessageEditedResponder.cs | 20 +- 12 files changed, 1054 insertions(+), 1010 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 3d05722..282e678 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -1,6 +1,6 @@  - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + I'm ready! - + Deleted message by {0}: - + Cleared message from {0} in channel {1}: {2} - + Edited message by {0}: - + {0}, welcome to {1} - + Bah! - + Bop! - + Beep! - + I do not have permission to execute this command! - + You do not have permission to execute this command! - + You were banned - + Punishment expired - + You specified less than {0} messages! - + You specified more than {0} messages! - + Command help: - + You were kicked - + ms - + Member is already muted! - + Not specified - + Not specified - + Current settings: - + Language - + Prefix - + Remove roles on mute - + Send welcome messages - + Mute role - - Language not supported! - - + + Language not supported! + + Yes - + No - + This user is not banned! - + Member not muted! - + Welcome message - + You need to specify an integer from {0} to {1} instead of {2}! - + {0} was banned - + That setting doesn't exist! - + Receive startup messages - + Invalid setting value specified! - + This role does not exist! - + This channel does not exist! - + 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 - + I cannot use time-outs on other bots! Try to set a mute role in settings - + {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4} - + Role for event creation notifications - + Channel for event notifications - + Event start notifications receivers - + Event "{0}" started - + :( - + Event "{0}" is cancelled! - + Event "{0}" has completed! - + ever - + Cleared {0} messages - + Kicked {0}: {1} - + Muted {0} for{1}: {2} - + Unbanned {0}: {1} - + Unmuted {0}: {1} - + Nothing changed! `{0}` is already set to {1} - + Not specified - + Value of setting `{0}` is now set to {1} - + Bans a user - + Deletes a specified amount of messages in this channel - + Shows this message - + Kicks a member - + Mutes a member - + Shows (inaccurate) latency - + Allows you to change certain preferences for this guild - + Unbans a user - + Unmutes a member - + 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 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! - + You cannot manage this guild! - + I cannot ban users from this guild! - + I cannot manage messages in this guild! - + I cannot kick members from this guild! - + I cannot moderate members in this guild! - + I cannot manage this guild! - + You need to specify a reason to ban this user! - + You need to specify a reason to kick this member! - + You need to specify a reason to mute this member! - + You need to specify a reason to unban this user! - + You need to specify a reason for unmute this member! - + You cannot ban the owner of this guild! - + You cannot ban yourself! - + You cannot ban me! - + I cannot ban this user! - + You cannot ban this user! - + You cannot kick the owner of this guild! - + You cannot kick yourself! - + You cannot kick me! - + I cannot kick this member! - + You cannot kick this member! - + You cannot mute the owner of this guild! - + You cannot mute yourself! - + You cannot mute me! - + I cannot mute this member! - + You cannot mute this member! - + You don't need to unmute the owner of this guild! - + You are muted! - + ... - + I cannot unmute this member! - + You cannot unmute this user! - + Event "{0}" will start {1}! - + Early event start notification offset - + 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 - + Default 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! - - OK, I'll mention you on {0} - - + You need to specify when I should send you the reminder! - + Issued by - + {0} has created a new event: - + The event will start at {0} in {1} - + The event will start at {0} until {1} in {2} - + Event details - + The event has lasted for `{0}` - + The event is happening at {0} - + The event is happening at {0} until {1} - + This user is already banned! - + {0} was unbanned - + {0} was muted - + {0} was unmuted - + This member is not muted! - + I could not find this user! - + {0} was kicked - + Reason: {0} - + Expires at: {0} - + This user is already muted! - + From {0}: - + Developers: - - Boyfriend's Wiki Page: + + Boyfriend's source code - + About Boyfriend - - logo and embed designer, Boyfriend's Wiki creator + + developer & designer, Boyfriend's Wiki creator - + main developer - + developer - - Reminder for {0} created - - - Reminder for {0} - - - You asked me to remind you {0} - - - Boyfriend's Settings - - - Setting successfully changed - - - Setting not changed - - - is now - - - Rename members who attempt to hoist themselves - - - Page - - - Page not found! - - - There are {0} total pages - - - Next - - - Previous - + + Reminder for {0} created + + + Reminder for {0} + + + You asked me to remind you {0} + + + Boyfriend's Settings + + + Setting successfully changed + + + Setting not changed + + + is now + + + Rename members who attempt to hoist themselves + + + Page + + + Page not found! + + + There are {0} total pages + + + Next + + + Previous + - {0}'s reminders - + {0}'s reminders + - There's no reminder with that index! - + There's no reminder with that index! + - Reminder deleted - - - You don't have any reminders created! - - - Setting {0} reset - - - All settings have been reset - - - Display name - - - Information about {0} - - - Muted - - - Discord user since - - - Banned - - - Punishments - - - Banned permanently - - - Not in the guild - - - Muted by timeout - - - Muted by mute role - - - Guild member since - - - Nickname - - - Roles - - - Nitro booster since - + Reminder deleted + + + You don't have any reminders created! + + + Setting {0} reset + + + All settings have been reset + + + Jump to message: {0} + + + Jump to channel: {0} + + + Index: {0} + + + The reminder will be sent on: {0} + + + Reminder text: {0} + + + Display name + + + Information about {0} + + + Muted + + + Discord user since + + + Banned + + + Punishments + + + Banned permanently + + + Not in the guild + + + Muted by timeout + + + Muted by mute role + + + Guild member since + + + Nickname + + + Roles + + + Nitro booster since + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 3f20dd0..77e3838 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -1,6 +1,6 @@  - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Я запустился! - + Сообщение {0} удалено: - + Очищено сообщение от {0} в канале {1}: {2} - + Сообщение {0} отредактировано: - + {0}, добро пожаловать на сервер {1} - + Бап! - + Боп! - + Бип! - + У меня недостаточно прав для выполнения этой команды! - + У тебя недостаточно прав для выполнения этой команды! - + Время наказания истекло - + Указано менее {0} сообщений! - + Указано более {0} сообщений! - + Справка по командам: - + Вы были выгнаны - + мс - + Участник уже заглушен! - + Не указан - + Не указана - + Текущие настройки: - + Язык - + Префикс - + Удалять роли при муте - + Отправлять приветствия - + Роль мута - - Язык не поддерживается! - - + + Язык не поддерживается! + + Да - + Нет - + Этот пользователь не забанен! - + Участник не заглушен! - + Приветствие - + Надо указать целое число от {0} до {1} вместо {2}! - + {0} был(-а) забанен(-а) - + Такая настройка не существует! - + Получать сообщения о запуске - + Указано недействительное значение для настройки! - + Эта роль не существует! - + Этот канал не существует! - + Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках - + Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках - + {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4} - + Роль для уведомлений о создании событий - + Канал для уведомлений о событиях - + Получатели уведомлений о начале событий - + Событие "{0}" началось - + :( - + Событие "{0}" отменено! - + Событие "{0}" завершено! - + всегда - + Очищено {0} сообщений - + Выгнан {0}: {1} - + Заглушен {0} на{1}: {2} - + Возвращён из бана {0}: {1} - + Разглушен {0}: {1} - + Ничего не изменилось! Значение настройки `{0}` уже {1} - + Не указано - + Значение настройки `{0}` теперь установлено на {1} - + Банит пользователя - + Удаляет указанное количество сообщений в этом канале - + Показывает эту справку - + Выгоняет участника - + Глушит участника - + Показывает (неточную) задержку - + Позволяет менять некоторые настройки под этот сервер - + Возвращает пользователя из бана - + Разглушает участника - + Надо указать целое число от {0} до {1}! - + Надо указать пользователя! - + Надо указать пользователя вместо {0}! - + Надо указать участника сервера! - + Надо указать участника этого сервера! - + Ты не можешь банить пользователей на этом сервере! - + Ты не можешь управлять сообщениями этого сервера! - + Ты не можешь выгонять участников с этого сервера! - + Ты не можешь модерировать участников этого сервера! - + Ты не можешь настраивать этот сервер! - + Я не могу банить пользователей на этом сервере! - + Я не могу управлять сообщениями этого сервера! - + Я не могу выгонять участников с этого сервера! - + Я не могу модерировать участников этого сервера! - + Я не могу настраивать этот сервер! - + Надо указать причину для бана этого участника! - + Надо указать причину для кика этого участника! - + Надо указать причину для мута этого участника! - + Надо указать причину для разбана этого пользователя! - + Надо указать причину для размута этого участника! - + Ты не можешь меня забанить! - + Ты не можешь забанить владельца этого сервера! - + Ты не можешь забанить этого участника! - + Ты не можешь себя забанить! - + Я не могу забанить этого пользователя! - + Ты не можешь выгнать владельца этого сервера! - + Ты не можешь себя выгнать! - + Ты не можешь меня выгнать! - + Я не могу выгнать этого участника - + Ты не можешь выгнать этого участника! - + Ты не можешь заглушить владельца этого сервера! - + Ты не можешь себя заглушить! - + Ты не можешь заглушить меня! - + Я не могу заглушить этого пользователя! - + Ты не можешь заглушить этого участника! - + Тебе не надо возвращать из мута владельца этого сервера! - + Ты заглушен! - + ... - + Ты не можешь вернуть из мута этого пользователя! - + Я не могу вернуть из мута этого пользователя! - + Событие "{0}" начнется {1}! - + Офсет отправки преждевременного уведомления о начале события - + Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад - - Роль по умолчанию - - + + Роль по умолчанию + + Добавляет напоминание - + Канал для публичных уведомлений - + Канал для приватных уведомлений - + Возвращать роли при перезаходе - + Автоматически начинать события - + Тебе нужно указать текст напоминания! - - Хорошо, я упомяну тебя {0} - - + Нужно указать время, через которое придёт напоминание! - + Ответственный - + {0} создаёт новое событие: - + Событие пройдёт {0} в канале {1} - + Событие пройдёт с {0} до {1} в {2} - + Подробнее о событии - + Событие длилось `{0}` - + Событие происходит в {0} - + Событие происходит в {0} до {1} - + Этот пользователь уже забанен! - + {0} был(-а) разбанен(-а) - + {0} был(-а) заглушен(-а) - + Этот участник не заглушен! - + {0} был(-а) разглушен(-а) - + Я не смог найти этого пользователя! - + {0} был(-а) выгнан(-а) - + Причина: {0} - + Закончится: {0} - + Этот пользователь уже в муте! - + Вы были забанены - + От {0}: - + Разработчики: - - Страница Boyfriend's Wiki: + + Исходный код Boyfriend - + О Boyfriend - - разрабочик + + разработчик - + основной разработчик - - дизайнер лого и эмбедов, создатель Boyfriend's Wiki + + разработчик и дизайнер, создатель Boyfriend's Wiki + + + Напоминание для {0} создано + + + Напоминание для {0} + + + Вы просили напомнить вам {0} + + + Настройки Boyfriend + + + Настройка успешно изменена + + + Настройка не редактирована + + + теперь имеет значение + + + Переименовывать участников, которые пытаются поднять себя + + + Страница + + + Страница не найдена! + + + Всего есть {0} страниц(-ы) + + + Далее + + + Назад + + + Напоминания {0} + + + У тебя нет напоминания с указанным индексом! + + + Напоминание удалено + + + У вас нет созданных напоминаний! + + + Настройка {0} сброшена + + + Все настройки были сброшены + + + Перейти к сообщению: {0} + + + Перейти к каналу: {0} + + + Индекс: {0} + + + Напоминание будет отправлено: {0} + + + Текст напоминалки: {0} + + + Отображаемое имя + + + Информация о {0} + + + Заглушен + + + Вступил в Discord + + + Забанен + + + Наказания + + + Забанен навсегда + + + Не на сервере + + + Заглушен с помощью тайм-аута + + + Заглушен с помощью роли мута + + + Вступил на сервер + + + Никнейм + + + Роли + + + Начал бустить сервер - - Напоминание для {0} создано - - - Напоминание для {0} - - - Вы просили напомнить вам {0} - - - Настройки Boyfriend - - - Настройка успешно изменена - - - Настройка не редактирована - - - теперь имеет значение - - - Переименовывать участников, которые пытаются поднять себя - - - Страница - - - Страница не найдена! - - - Всего есть {0} страниц(-ы) - - - Далее - - - Назад - - - Напоминания {0} - - - У тебя нет напоминания с указанным индексом! - - - Напоминание удалено - - - У вас нет созданных напоминаний! - - - Настройка {0} сброшена - - - Все настройки были сброшены - - - Отображаемое имя - - - Информация о {0} - - - Заглушен - - - Вступил в Discord - - - Забанен - - - Наказания - - - Забанен навсегда - - - Не на сервере - - - Заглушен с помощью тайм-аута - - - Заглушен с помощью роли мута - - - Вступил на сервер - - - Никнейм - - - Роли - - - Начал бустить сервер - diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index a564e1c..b0335a8 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -1,6 +1,6 @@ - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + я родился! - + сообщение {0} вырезано: - + вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2} - + сообщение {0} переделано: - + {0}, добро пожаловать на сервер {1} - + брах! - + брох! - + брух! - + у меня прав нету, сделай что нибудь. - + у тебя прав нету, твои проблемы. - + вы были забанены - + время бана закончиловсь - + ты выбрал менее {0} сообщений - + ты выбрал более {0} сообщений - + туториал по приколам: - + вы были кикнуты - + мс - + шизоид уже замучен! - + *тут ничего нет* - + нъет - + настройки: - + язык - + префикс - + удалять звание при муте - + разглашать о том что пришел новый шизоид - + звание замученного - - такого языка нету... - - + + такого языка нету... + + да - + нъет - + шизик не забанен - + шизоид не замучен! - + здравствуйте (типо настройка) - + выбери число от {0} до {1} вместо {2}! - + {0} забанен - + такой прикол не существует - + получать инфу о старте бота - + криво настроил прикол, давай по новой - + этого звания нету, ты шо - + этого канала нету, ты шо - + ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим - + я не могу замутить ботов, сделай что нибудь - + {0} приготовил новую движуху {1}! она пройдёт в {2} и начнётся <t:{3}:R>!{4} - + роль для уведомлений о создании движухи - + канал для уведомлений о движухах - + получатели уведомлений о начале движух - + движуха "{0}" начинается - + оъмъомоъемъъео(((( - - движуха "{0}" отменена! - - - движуха "{0}" завершена! - - + + движуха "{0}" отменена! + + + движуха "{0}" завершена! + + всегда - + вырезано {0} забавных сообщений - + выгнан {0}: {1} - + замучен {0} на{1}: {2} - + раззабанен {0}: {1} - + раззамучен {0}: {1} - + ты все сломал! значение прикола `{0}` и так {1} - + нъет - + прикол для `{0}` теперь установлен на {1} - + возводит великий банхаммер над шизоидом - + удаляет сообщения. сколько хош, столько и удалит - + показывает то, что ты сейчас видишь прямо сейчас - + выпинывает шизоида - + мутит шизоида - + показывает пинг (сверхмегаточный (нет)) - + настройки бота под этот сервер - + отводит великий банхаммер от шизоида - + раззамучивает шизоида - + укажи целое число от {0} до {1} - + укажи самого шизика - + надо указать юзверя вместо {0}! - + укажи самого шизика - + укажи шизоида сервера! - + бан - + тебе нельзя иметь власть над сообщениями шизоидов - + кик шизиков нельзя - + тебе нельзя управлять шизоидами - + тебе нельзя редактировать дурку - + я не могу ваще никого банить чел. - + я не могу исправлять орфографический кринж участников, сделай что нибудь. - + я не могу ваще никого кикать чел. - + я не могу контроллировать за всеми ними, сделай что нибудь. - + я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь. - + укажи зачем банить шизика - + укажи зачем кикать шизика - + укажи зачем мутить шизика - + укажи зачем раззабанивать шизика - + укажи зачам размучивать шизика - + ээбля френдли фаер огонь по своим - + бан админу нельзя - + бан этому шизику нельзя - + самобан нельзя - + я не могу его забанить... - + кик админу нельзя - + самокик нельзя - + ээбля френдли фаер огонь по своим - + я не могу его кикнуть... - + кик этому шизику нельзя - + мут админу нельзя - + самомут нельзя - + ээбля френдли фаер огонь по своим - + я не могу его замутить... - + мут этому шизику нельзя - + сильно - + ты замучен. - + ... - + тебе нельзя раззамучивать - + я не могу его раззамутить... - + движуха "{0}" начнется {1}! - + заранее пнуть в минутах до начала движухи - + у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад) - + дефолтное звание - + крафтит напоминалку - + канал для секретных уведомлений - + канал для не секретных уведомлений - + вернуть звания при переподключении в дурку - + автоматом стартить движухи - + для крафта напоминалки нужен текст - - вас понял, упоминание будет {0} - - + шизоид у меня на часах такого нету - + ответственный - + {0} создает новое событие: - + движуха произойдет {0} в канале {1} - + движуха будет происходить с {0} до {1} в {2} - + побольше о движухе - + все это длилось `{0}` - + движуха происходит в {0} - + движуха происходит в {0} до {1} - + этот шизоид уже лежит в бане - + {0} раззабанен - + {0} в муте - + {0} в размуте - + этого шизоида никто не мутил. - + у нас такого шизоида нету... - + {0} вышел с посторонней помощью - + причина: {0} - + до: {0} - + этот шизоид УЖЕ замучился - + от {0} - + девелоперы: - - страничка Boyfriend's Wiki: + + репа Boyfriend (тык) - + немного о Boyfriend - - скучный лого/эмбед дизайнер создавший Boyfriend's Wiki + + скучный девелопер + дизайнер создавший Boyfriend's Wiki - + ВАЖНЫЙ соучастник кодинг-стримов @Octol1ttle - + САМЫЙ ВАЖНЫЙ чел написавший кода больше всех (99.99%) - - напоминалка для {0} скрафченА - - - напоминалка для {0} - - - ты хотел чтоб я напомнил тебе {0} - - - приколы Boyfriend - - - прикол редактирован - - - прикол сдох - - - стало - - - переобувать шизоидов пытающихся поднять себя в табе - - - это страница - - - если я был бы html, я бы сказал 404 - - - ну а если быть точнее, тут всего {0} страниц(-ы) - - - следующее - - - предыдущее - - - напоминалки {0} - - - у тебя нет напоминалки с этим индексом! - - - напоминалка уничтожена - - - ты еще не крафтил напоминалки - - - {0} откачен к заводским - - - откатываемся к заводским... - - - дисплейнейм - - - деанон {0} - - - замучен - - - юзер Discord со времен - - - забанен - - - приколы полученные по заслугам - - - забанен - - - вышел из сервера - - - замучен таймаутом - - - замучен ролькой - - - участник сервера со времен - - - сервернейм - - - рольки - - - бустит сервер со времен - - + + напоминалка для {0} скрафченА + + + напоминалка для {0} + + + ты хотел чтоб я напомнил тебе {0} + + + приколы Boyfriend + + + прикол редактирован + + + прикол сдох + + + стало + + + переобувать шизоидов пытающихся поднять себя в табе + + + это страница + + + если я был бы html, я бы сказал 404 + + + ну а если быть точнее, тут всего {0} страниц(-ы) + + + следующее + + + предыдущее + + + напоминалки {0} + + + у тебя нет напоминалки с этим индексом! + + + напоминалка уничтожена + + + ты еще не крафтил напоминалки + + + {0} откачен к заводским + + + откатываемся к заводским... + + + чекнуть сообщение: {0} + + + чекнуть канал: {0} + + + индекс: {0} + + + я пну тебе это: {0} + + + че там в напоминалке: {0} + + + дисплейнейм + + + деанон {0} + + + замучен + + + юзер Discord со времен + + + забанен + + + приколы полученные по заслугам + + + забанен + + + вышел из сервера + + + замучен таймаутом + + + замучен ролькой + + + участник сервера со времен + + + сервернейм + + + рольки + + + бустит сервер со времен + + \ No newline at end of file diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 4ad428a..e9d9874 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -12,7 +12,7 @@ using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; -using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; using Remora.Results; namespace Boyfriend.Commands; @@ -23,20 +23,29 @@ namespace Boyfriend.Commands; [UsedImplicitly] public class AboutCommandGroup : CommandGroup { - private static readonly string[] Developers = { "Octol1ttle", "mctaylors", "neroduckale" }; + private static readonly (string Username, Snowflake Id)[] Developers = + { + ("Octol1ttle", new Snowflake(504343489664909322)), + ("mctaylors", new Snowflake(326642240229474304)), + ("neroduckale", new Snowflake(474943797063843851)) + }; + private readonly ICommandContext _context; private readonly FeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; + private readonly IDiscordRestGuildAPI _guildApi; public AboutCommandGroup( ICommandContext context, GuildDataService guildData, - FeedbackService feedback, IDiscordRestUserAPI userApi) + FeedbackService feedback, IDiscordRestUserAPI userApi, + IDiscordRestGuildAPI guildApi) { _context = context; _guildData = guildData; _feedback = feedback; _userApi = userApi; + _guildApi = guildApi; } /// @@ -66,20 +75,22 @@ public class AboutCommandGroup : CommandGroup var cfg = await _guildData.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); - return await SendAboutBotAsync(currentUser, CancellationToken); + return await SendAboutBotAsync(currentUser, guildId, CancellationToken); } - private async Task SendAboutBotAsync(IUser currentUser, CancellationToken ct = default) + private async Task SendAboutBotAsync(IUser currentUser, Snowflake guildId, CancellationToken ct = default) { - var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers)); + var builder = new StringBuilder().Append("### ").AppendLine(Messages.AboutTitleDevelopers); foreach (var dev in Developers) { - builder.AppendLine($"@{dev} — {$"AboutDeveloper@{dev}".Localized()}"); + var guildMemberResult = await _guildApi.GetGuildMemberAsync( + guildId, dev.Id, ct); + var tag = guildMemberResult.IsSuccess ? $"<@{dev.Id}>" : $"@{dev.Username}"; + + builder.AppendLine($"- {tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); } - builder.AppendLine() - .AppendLine(Markdown.Bold(Messages.AboutTitleWiki)) - .AppendLine("https://github.com/LabsDevelopment/Boyfriend/wiki"); + builder.Append($"### [{Messages.AboutTitleRepository}](https://github.com/LabsDevelopment/Boyfriend)"); var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser) .WithDescription(builder.ToString()) diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 25cd7e8..0ac5a36 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -133,10 +133,11 @@ public class BanCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); } - var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)); + var builder = new StringBuilder().Append("- ") + .AppendLine(string.Format(Messages.DescriptionActionReason, reason)); if (duration is not null) { - builder.Append( + builder.Append("- ").Append( string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(DateTimeOffset.UtcNow.Add(duration.Value)))); @@ -271,9 +272,10 @@ public class BanCommandGroup : CommandGroup .WithColour(ColorsList.Green).Build(); var title = string.Format(Messages.UserUnbanned, target.GetTag()); - var description = string.Format(Messages.DescriptionActionReason, reason); + var description = new StringBuilder().Append("- ") + .Append(string.Format(Messages.DescriptionActionReason, reason)); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); + data.Settings, channelId, user, title, description.ToString(), target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) { return Result.FromError(logResult.Error); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 88d746f..c3a4179 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -131,7 +131,7 @@ public class KickCommandGroup : CommandGroup { var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereKicked) - .WithDescription(string.Format(Messages.DescriptionActionReason, reason)) + .WithDescription($"- {string.Format(Messages.DescriptionActionReason, reason)}") .WithActionFooter(user) .WithCurrentTimestamp() .WithColour(ColorsList.Red) @@ -156,7 +156,7 @@ public class KickCommandGroup : CommandGroup data.GetOrCreateMemberData(target.ID).Roles.Clear(); var title = string.Format(Messages.UserKicked, target.GetTag()); - var description = string.Format(Messages.DescriptionActionReason, reason); + var description = $"- {string.Format(Messages.DescriptionActionReason, reason)}"; var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index bc64a64..5b3b68d 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -152,10 +152,9 @@ public class MuteCommandGroup : CommandGroup } var title = string.Format(Messages.UserMuted, target.GetTag()); - var description = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)) - .Append( - string.Format( - Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); + var description = new StringBuilder().Append("- ").AppendLine(string.Format(Messages.DescriptionActionReason, reason)) + .Append("- ").Append(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); @@ -211,10 +210,9 @@ public class MuteCommandGroup : CommandGroup } var title = string.Format(Messages.UserMuted, target.GetTag()); - var description = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)) - .Append( - string.Format( - Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); + var description = new StringBuilder().Append("- ").AppendLine(string.Format(Messages.DescriptionActionReason, reason)) + .Append("- ").Append(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); @@ -328,7 +326,7 @@ public class MuteCommandGroup : CommandGroup } var title = string.Format(Messages.UserUnmuted, target.GetTag()); - var description = string.Format(Messages.DescriptionActionReason, reason); + var description = $"- {string.Format(Messages.DescriptionActionReason, reason)}"; var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) @@ -372,7 +370,7 @@ public class MuteCommandGroup : CommandGroup } var title = string.Format(Messages.UserUnmuted, target.GetTag()); - var description = string.Format(Messages.DescriptionActionReason, reason); + var description = $"- {string.Format(Messages.DescriptionActionReason, reason)}"; var logResult = _utility.LogActionAsync( data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index d437b5d..d150e9c 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -19,7 +19,7 @@ using Remora.Results; namespace Boyfriend.Commands; /// -/// Handles the command to manage reminders: /remind +/// Handles commands to manage reminders: /remind, /listremind, /delremind /// [UsedImplicitly] public class RemindCommandGroup : CommandGroup @@ -88,8 +88,9 @@ public class RemindCommandGroup : CommandGroup for (var i = data.Reminders.Count - 1; i >= 0; i--) { var reminder = data.Reminders[i]; - builder.AppendLine( - $"- {Markdown.InlineCode(i.ToString())} - {Markdown.InlineCode(reminder.Text)} - {Markdown.Timestamp(reminder.At)}"); + builder.Append("- ").AppendLine(string.Format(Messages.ReminderIndex, Markdown.InlineCode(i.ToString()))) + .Append(" - ").AppendLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) + .Append(" - ").AppendLine(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(reminder.At))); } var embed = new EmbedBuilder().WithSmallTitle( @@ -149,8 +150,13 @@ public class RemindCommandGroup : CommandGroup Text = message }); - var embed = new EmbedBuilder().WithSmallTitle(string.Format(Messages.ReminderCreated, user.GetTag()), user) - .WithDescription(string.Format(Messages.DescriptionReminderCreated, Markdown.Timestamp(remindAt))) + var builder = new StringBuilder().Append("- ").AppendLine(string.Format( + Messages.ReminderText, Markdown.InlineCode(message))) + .Append("- ").Append(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(remindAt))); + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.ReminderCreated, user.GetTag()), user) + .WithDescription(builder.ToString()) .WithColour(ColorsList.Green) .Build(); diff --git a/src/Extensions.cs b/src/Extensions.cs index 1e30cf4..c06c4ce 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -17,22 +17,6 @@ namespace Boyfriend; public static class Extensions { - /// - /// Adds a footer with the 's avatar and tag (@username or username#0000). - /// - /// The builder to add the footer to. - /// The user whose tag and avatar to add. - /// The builder with the added footer. - public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) - { - var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); - var avatarUrl = avatarUrlResult.IsSuccess - ? avatarUrlResult.Entity.AbsoluteUri - : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri; - - return builder.WithFooter(new EmbedFooter(user.GetTag(), avatarUrl)); - } - /// /// Adds a footer representing that an action was performed by a . /// diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 884cfd4..8784b90 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -744,12 +744,6 @@ namespace Boyfriend { } } - internal static string DescriptionReminderCreated { - get { - return ResourceManager.GetString("DescriptionReminderCreated", resourceCulture); - } - } - internal static string InvalidRemindIn { get { return ResourceManager.GetString("InvalidRemindIn", resourceCulture); @@ -876,9 +870,9 @@ namespace Boyfriend { } } - internal static string AboutTitleWiki { + internal static string AboutTitleRepository { get { - return ResourceManager.GetString("AboutTitleWiki", resourceCulture); + return ResourceManager.GetString("AboutTitleRepository", resourceCulture); } } @@ -1014,6 +1008,36 @@ namespace Boyfriend { } } + internal static string DescriptionActionJumpToMessage { + get { + return ResourceManager.GetString("DescriptionActionJumpToMessage", resourceCulture); + } + } + + internal static string DescriptionActionJumpToChannel { + get { + return ResourceManager.GetString("DescriptionActionJumpToChannel", resourceCulture); + } + } + + internal static string ReminderIndex { + get { + return ResourceManager.GetString("ReminderIndex", resourceCulture); + } + } + + internal static string ReminderWillBeSentOn { + get { + return ResourceManager.GetString("ReminderWillBeSentOn", resourceCulture); + } + } + + internal static string ReminderText { + get { + return ResourceManager.GetString("ReminderText", resourceCulture); + } + } + internal static string ShowInfoTitle { get { return ResourceManager.GetString("ShowInfoTitle", resourceCulture); diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 3611f80..8ae2418 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -1,3 +1,4 @@ +using System.Text; using Boyfriend.Data; using Boyfriend.Services; using JetBrains.Annotations; @@ -81,13 +82,17 @@ public class MessageDeletedResponder : IResponder Messages.Culture = GuildSettings.Language.Get(cfg); + var builder = new StringBuilder().AppendLine( + string.Format(Messages.DescriptionActionJumpToChannel, + Mention.Channel(gatewayEvent.ChannelID))) + .AppendLine(message.Content.InBlockCode()); + var embed = new EmbedBuilder() .WithSmallTitle( string.Format( Messages.CachedMessageDeleted, message.Author.GetTag()), message.Author) - .WithDescription( - $"{Mention.Channel(gatewayEvent.ChannelID)}\n{message.Content.InBlockCode()}") + .WithDescription(builder.ToString()) .WithActionFooter(user) .WithTimestamp(message.Timestamp) .WithColour(ColorsList.Red) diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index 7e02f9f..d7e2347 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -1,3 +1,4 @@ +using System.Text; using Boyfriend.Data; using Boyfriend.Services; using DiffPlex.DiffBuilder; @@ -23,16 +24,13 @@ public class MessageEditedResponder : IResponder private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; private readonly GuildDataService _guildData; - private readonly IDiscordRestUserAPI _userApi; public MessageEditedResponder( - CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService guildData, - IDiscordRestUserAPI userApi) + CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService guildData) { _cacheService = cacheService; _channelApi = channelApi; _guildData = guildData; - _userApi = userApi; } public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) @@ -92,20 +90,18 @@ public class MessageEditedResponder : IResponder // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) - { - return Result.FromError(currentUserResult); - } - var diff = InlineDiffBuilder.Diff(message.Content, newContent); Messages.Culture = GuildSettings.Language.Get(cfg); + var builder = new StringBuilder().AppendLine( + string.Format(Messages.DescriptionActionJumpToMessage, + $"https://discord.com/channels/{guildId}/{channelId}/{messageId}")) + .AppendLine(diff.AsMarkdown()); + var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) - .WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}") - .WithUserFooter(currentUser) + .WithDescription(builder.ToString()) .WithTimestamp(timestamp.Value) .WithColour(ColorsList.Yellow) .Build(); From 2e2f50908ee828712c8740a12189fc501172f64a Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 29 Sep 2023 19:22:44 +0300 Subject: [PATCH 162/329] Add /random (#127) It could have been a milestone PR, but of course I made a mistake _somewhere_. --------- Signed-off-by: Macintosh II --- locale/Messages.resx | 6 +++ locale/Messages.ru.resx | 6 +++ locale/Messages.tt-ru.resx | 8 +++- src/Commands/ToolsCommandGroup.cs | 63 ++++++++++++++++++++++++++++++- src/Messages.Designer.cs | 14 +++++++ 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 282e678..4d84198 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -648,4 +648,10 @@ Nitro booster since + + The minimum number is greater than the maximum! + + + Your random number is: + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 77e3838..4900e53 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -648,4 +648,10 @@ Начал бустить сервер + + Минимальное число больше максимального! + + + Ваше случайное число: + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index b0335a8..5a08755 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -648,4 +648,10 @@ бустит сервер со времен - \ No newline at end of file + + почему минимальное > максимальное + + + ваше рандомное число: + + diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 3ac8b70..fa0f015 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -19,7 +19,7 @@ using Remora.Results; namespace Boyfriend.Commands; /// -/// Handles commands related to tools: /showinfo. +/// Handles tool commands: /showinfo, /random. /// [UsedImplicitly] public class ToolsCommandGroup : CommandGroup @@ -228,4 +228,65 @@ public class ToolsCommandGroup : CommandGroup Messages.DescriptionActionExpiresAt, Markdown.Timestamp(communicationDisabledUntil.Value))); } } + + /// + /// A slash command that generates a random number using maximum and minimum numbers. + /// + /// The maximum number for randomization. + /// The minimum number for randomization. Default value: 1 + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("random")] + [DiscordDefaultDMPermission(false)] + [Description("Generates a random number")] + [UsedImplicitly] + public async Task ExecuteRandomAsync( + [Description("Maximum number")] int max, + [Description("Minumum number (Default: 1)")] + int min = 1) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + { + return Result.FromError(currentUserResult); + } + + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); + if (!userResult.IsDefined(out var user)) + { + return Result.FromError(userResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await SendRandomNumberAsync(max, min, user, currentUser, CancellationToken); + } + + private async Task SendRandomNumberAsync(int max, int min, IUser user, IUser currentUser, CancellationToken ct) + { + if (min > max) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle( + Messages.RandomMinGreaterThanMax, currentUser) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + } + + var i = Random.Shared.Next(min, max + 1); + + var embed = new EmbedBuilder().WithSmallTitle(Messages.RandomOutput, user) + .WithDescription($"# {i}\n({min}-{max})") + .WithColour(ColorsList.Blue) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 8784b90..ae712df 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1121,5 +1121,19 @@ namespace Boyfriend { return ResourceManager.GetString("ShowInfoGuildMemberPremiumSince", resourceCulture); } } + + internal static string RandomMinGreaterThanMax + { + get { + return ResourceManager.GetString("RandomMinGreaterThanMax", resourceCulture); + } + } + + internal static string RandomOutput + { + get { + return ResourceManager.GetString("RandomOutput", resourceCulture); + } + } } } From 804bcd6e68c56dda81df54773789a2876405a8df Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Sat, 30 Sep 2023 16:58:32 +0300 Subject: [PATCH 163/329] Rebrand to Octobot (#128) We're moving! --------- Signed-off-by: Macintosh II Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> Co-authored-by: Octol1ttle --- .github/CODEOWNERS | 4 +-- .github/workflows/build-pr.yml | 2 +- .github/workflows/build-push.yml | 2 +- Boyfriend.csproj => Octobot.csproj | 34 +++++++++--------- Boyfriend.sln => Octobot.sln | 2 +- docs/CONTRIBUTING.md | 6 ++-- docs/README.md | 21 ++++++----- docs/assets/boyfriend.png | Bin 229373 -> 0 bytes locale/Messages.resx | 20 +++++------ locale/Messages.ru.resx | 20 +++++------ locale/Messages.tt-ru.resx | 20 +++++------ src/ColorsList.cs | 2 +- src/Commands/AboutCommandGroup.cs | 12 +++---- src/Commands/BanCommandGroup.cs | 8 ++--- src/Commands/ClearCommandGroup.cs | 8 ++--- .../Events/ErrorLoggingPostExecutionEvent.cs | 2 +- .../Events/LoggingPreparationErrorEvent.cs | 2 +- src/Commands/KickCommandGroup.cs | 6 ++-- src/Commands/MuteCommandGroup.cs | 8 ++--- src/Commands/PingCommandGroup.cs | 10 +++--- src/Commands/RemindCommandGroup.cs | 6 ++-- src/Commands/SettingsCommandGroup.cs | 8 ++--- src/Commands/ToolsCommandGroup.cs | 6 ++-- src/Data/GuildData.cs | 2 +- src/Data/GuildSettings.cs | 6 ++-- src/Data/MemberData.cs | 2 +- src/Data/Options/AllOptionsEnum.cs | 4 +-- src/Data/Options/BoolOption.cs | 2 +- src/Data/Options/IOption.cs | 2 +- src/Data/Options/LanguageOption.cs | 2 +- src/Data/Options/Option.cs | 2 +- src/Data/Options/SnowflakeOption.cs | 2 +- src/Data/Options/TimeSpanOption.cs | 2 +- src/Data/Reminder.cs | 2 +- src/Data/ScheduledEventData.cs | 2 +- src/Extensions.cs | 2 +- src/InteractionResponders.cs | 2 +- src/Messages.Designer.cs | 16 ++++----- src/{Boyfriend.cs => Octobot.cs} | 16 ++++----- src/Responders/GuildLoadedResponder.cs | 8 ++--- src/Responders/GuildMemberJoinedResponder.cs | 8 ++--- .../GuildMemberRolesUpdatedResponder.cs | 6 ++-- src/Responders/MessageDeletedResponder.cs | 8 ++--- src/Responders/MessageEditedResponder.cs | 8 ++--- src/Responders/MessageReceivedResponder.cs | 2 +- .../ScheduledEventCreatedResponder.cs | 6 ++-- .../ScheduledEventUpdatedResponder.cs | 4 +-- .../BackgroundGuildDataSaverService.cs | 2 +- src/Services/GuildDataService.cs | 4 +-- src/Services/Update/MemberUpdateService.cs | 4 +-- .../Update/ScheduledEventUpdateService.cs | 4 +-- src/Services/Update/SongUpdateService.cs | 2 +- src/Services/UtilityService.cs | 4 +-- 53 files changed, 174 insertions(+), 171 deletions(-) rename Boyfriend.csproj => Octobot.csproj (67%) rename Boyfriend.sln => Octobot.sln (83%) delete mode 100644 docs/assets/boyfriend.png rename src/{Boyfriend.cs => Octobot.cs} (95%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7c34266..643492d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -* @LabsDevelopment/boyfriend -/docs/ @LabsDevelopment/boyfriend-docs +* @LabsDevelopment/octobot +/docs/ @LabsDevelopment/octobot-docs diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index c806834..512089c 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -25,7 +25,7 @@ jobs: - name: ReSharper CLI InspectCode uses: muno92/resharper_inspectcode@1.8.3 with: - solutionPath: ./Boyfriend.sln + solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement extensions: ReSharperPlugin.CognitiveComplexity solutionWideAnalysis: true diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 13597b2..a757eb2 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -9,7 +9,7 @@ on: jobs: upload-solution: - name: Upload Boyfriend to production + name: Upload Octobot to production runs-on: ubuntu-latest permissions: actions: read diff --git a/Boyfriend.csproj b/Octobot.csproj similarity index 67% rename from Boyfriend.csproj rename to Octobot.csproj index b99a311..2c5b16b 100644 --- a/Boyfriend.csproj +++ b/Octobot.csproj @@ -6,29 +6,29 @@ enable enable 2.0.0 - Boyfriend + Octobot Octol1ttle, mctaylors, neroduckale AGPLv3 - https://github.com/LabsDevelopment/Boyfriend - https://github.com/LabsDevelopment/Boyfriend/blob/master/LICENSE - https://github.com/LabsDevelopment/Boyfriend + https://github.com/LabsDevelopment/Octobot + https://github.com/LabsDevelopment/Octobot/blob/master/LICENSE + https://github.com/LabsDevelopment/Octobot github - TeamOctolings + LabsDevelopment en - A legacy-driven Discord bot written in C# + A general-purpose Discord bot for moderation written in C# - - - - - - - - - - + + + + + + + + + + @@ -37,6 +37,6 @@ - + diff --git a/Boyfriend.sln b/Octobot.sln similarity index 83% rename from Boyfriend.sln rename to Octobot.sln index b85c5f6..9dd2b89 100644 --- a/Boyfriend.sln +++ b/Octobot.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Boyfriend", "Boyfriend.csproj", "{9CA7A44F-167C-46D4-923D-88CE71044144}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Octobot", "Octobot.csproj", "{9CA7A44F-167C-46D4-923D-88CE71044144}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 602e8de..2a15ef2 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing Guidelines -Thank you for showing interest in the development of Boyfriend. We aim to provide a good collaborating environment for +Thank you for showing interest in the development of Octobot. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. Before starting, please read our [Code of Conduct](CODE_OF_CONDUCT.md) @@ -29,7 +29,7 @@ While pull requests from unaffiliated contributors are welcome, please note that internal issues that haven't been published to the issue tracker yet. Reviewing PRs is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change. -The [issue tracker](https://github.com/LabsDevelopment/Boyfriend/issues) should provide plenty of issues to start with. +The [issue tracker](https://github.com/LabsDevelopment/Octobot/issues) should provide plenty of issues to start with. Make sure to check that an issue you're planning to resolve does not already have people working on it and that there are no PRs associated with it @@ -62,7 +62,7 @@ After you're done with your changes and you wish to open the PR, please observe - Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge. -We are highly committed to quality when it comes to Boyfriend. This means that contributions from less experienced +We are highly committed to quality when it comes to Octobot. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience. diff --git a/docs/README.md b/docs/README.md index 381d541..e9ada97 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,12 +1,12 @@

- Boyfriend banner + Octobot banner

-![License](https://img.shields.io/github/license/LabsDevelopment/Boyfriend) -![Workflow Status](https://img.shields.io/github/actions/workflow/status/LabsDevelopment/Boyfriend/.github/workflows/build-push.yml?branch=master&logo=ReSharper) -![Last Commit](https://img.shields.io/github/last-commit/LabsDevelopment/Boyfriend) +![License](https://img.shields.io/github/license/LabsDevelopment/Octobot) +![Workflow Status](https://img.shields.io/github/actions/workflow/status/LabsDevelopment/Octobot/.github/workflows/build-push.yml?branch=master&logo=ReSharper) +![Last Commit](https://img.shields.io/github/last-commit/LabsDevelopment/Octobot) -Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and Remora.Discord +Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Labs Development Team](https://github.com/LabsDevelopment) in C# and Remora.Discord ## Features @@ -17,9 +17,9 @@ Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https:// *...and more!* -## Installing and running Boyfriend +## Installing and running Octobot -You can read our [wiki](https://github.com/LabsDevelopment/Boyfriend/wiki) in order to assemble your Boyfriend™ and +You can read our [wiki](https://github.com/LabsDevelopment/Octobot/wiki) in order to assemble your Octobot and moderate the server. ## Contributing @@ -33,8 +33,11 @@ the most effective way possible. ![JetBrains Logo (Main) logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg) [JetBrains](https://www.jetbrains.com/), creators of [ReSharper](https://www.jetbrains.com/resharper) -and [Rider](https://www.jetbrains.com/rider), supports Boyfriend with one of +and [Rider](https://www.jetbrains.com/rider), supports Octobot with one of their [Open Source Licenses](https://jb.gg/OpenSourceSupport). -Rider is the recommended IDE when working with Boyfriend, and everyone on the Boyfriend team uses it. +Rider is the recommended IDE when working with Octobot, and everyone on the Octobot team uses it. Additionally, ReSharper command-line tools made by JetBrains are used for status checks on pull requests to ensure code quality even when not using ReSharper or Rider. + +# +Not an official Splatoon™ product. We are in no way affiliated with or endorsed by Nintendo Company, or other rightsholders. diff --git a/docs/assets/boyfriend.png b/docs/assets/boyfriend.png deleted file mode 100644 index a8a5d16d00a9d1f89ed810d4df10114194e65cb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 229373 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

x*2w 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 z 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 z zgx1W+I4AHT)ARUB{;1_kmoDvmt!`D^{K9%yt&ghoqTH2opFZ~2*KB)cDZnzZ^+KA+e0n{QVe@X2Am#>Kh*T*PQEldIkGSx^>t5{^ zUblMp?%nTiWv~Bxa9i%}c?+IiezSbfo|=o>_y2u+dWVY@ztY9nXlm<`dFhQ}&rc8M*i>$c{Tx#?>Cc~<^-l~;_zf4ti7sdh%lFUPUi0&l|B@GzWKZYU z|Gw?M`275+FZM~y_s;|zm$4~V4R~1`)hTuH(v2G)C#A$*zIyeD*_B7~LW+ZI@d}@0 zA!~zwA}^{hK3Tk~si|p`N!(=RpNHlDDO4qkm@PPOk}~TL&r8=7c1Jr#pCsQPO>OPZ zqN1W_eGYH6n00ox`RPdvp`Uq!zc_NN@#nl`{o^m^?c!%=W-j~jo5j(hAwc7b?ER3J zn`Z3LOVgR$8>wY)R@Jw9{d)D;Czba9eVhMX``P-CGr2Qaj>+q8zPt2o-oBsDc52#u zpR-`+&dBe)R&F6>`#F5}^sPL$+yyjvr@G^|i@W>r`GWbi$4_xSVHe8npUm8OP-}M5 zoJWryP5Lr-nsH`c-mzQLjrOZvo9%xT#C+F||dd9I4b-d!-<9bV8bblbH){CP|Y=kHDTW$nKm)vx<_Q#`KX;eYP-!^x5bt7VU-PLKUI zGsADb(#pC12ZHl{buh6c%ROH+0x z^S(7{*3i&U_SSHzTQa*E;N?F6F+OwwDO_u%mBYXbFjT;9(pSQ0MG441o zuu*&2q)a#Mnx)L4lSG(*sb$6e{IcBM_WskNm%o%am_OQ8oN3DCzp2q-T+@;Gzb)sC&{fy_eokhSSr_HU*S%i5{mie; zl0|ukPlc;3`c!M!`R15cam zj9QaUG+50nT$tr7x2%k!K>AvPNF#L_mjFi+W-80`nL1P#*Re$#xpb3xP6jtIko!Ex2v7@^S{8s%U7=+ z-7gjRUweTR7XW(R-q@Fh>WJJZR3(SnR;iM!T27N1(=+P#85O6Avv1xM}FX9t&* zs!ingxZ<`=_V=0w+UAwF<9n>)}GP?Br*X#B31CB&5NcIUjE@Gr5)tz$u z=dO>*s;a7+&&Jvw{Cv*(eaF3wWs9df7d35~y7|e%{2U)YmEEiE&)5(j6JvAq*2-sQ z%F4>NcYd@@P0gq|+*ivoDaW%Q{o`z%puC>1ow*m)zs&9tR>35H`_{9qn?ivkqzG*CX ziHy7%Gs*CG#jBOe`3gJ@7qv*)U5wp2DP@`J4&%m&8pVe`oqW6KNoN1UTRkV=dV0S3 z|4IM?poPg3mKFOhQ%?M{s&HHWeYul|s#g{XeJ)LKuJ$>@@LR0hTJ@r<9Gi&}oIXU_7;>C-Z<@%4OKkc`kyJwHhKVHs&`lqGx6XSiC z$#>qHGARUfcVsbBql5C-N}IQD-`)nD?EKE;<2H}E*Z)aPyf;T@?)IJq%rhOTQw{!n zI<3F{_Ha8ZTz}$v&%UE)eynD2`5qlIf6LyKlaoH}*|R5U z665QZ!w)ZlMi1F@FC0{y8PJ!=x1)d4&hRU4PwvYsU$CGdsYr~2t@+Q7V>*5Le+qvn z+%*@uIpx7Q6+aem5pmyL9i~zV}yF2Djg@ z`Fyr~Uj4tH%?A@UsLyN3Vv)8mzdj1`~U26R1W=`5$cJKQ<_kGUV zwQFl@o}QXIe_O%c7RL40qrdKbU;Do6$84`3OTHbcZF#rw#XonRQXhrY6L=3YM21*O z9e7aoW%HITQ{J#iTh{#ekl6W8xu|dE)tQ?@{U`M<-}LQ5ME0HXi(K!%UAliiKjxt9 z+b2^OT>8IpKjY8zkoxJ2f3=Ma3@!x5#Q12}c^d3dWSf;P5zs$#_Yan`z`HG*%HG~8 z)!Y@bT&VKPMR#+iT{~Va44V2+ch=<@3;R+(?pexga(v|%!wq*u!(%SaX!~4dvM}Jq zk%P_b)dE)HVdlZL_c>R5(Z6VOKw#55P8O!(;F@=R%U7=C{OrbbHR9|Jt7it1=gLZI z0(>rhSDVt+#Z?u(6g08tT00^0+t!kAwmoMyELx-#b#SgiQNy%pVi~OE89c^G$17UD z=4#j8>U7Fi-{{x3|D#vh;}wr|18#F3pQalf_V8=f=}RTHeJ+m` z_3WvSyR_FWxOzKx(e~}%Pu#e1V*~G}hPl7h1ZoY`OT(udr4+A!VR1}O_P>sf&Jxi( zx(O8xxB5Qvu05T)Qpca`=ClWT%1JNwbmsObxI8+jsH}IC>z$xpVO#vs+M4R0pPr^K zeGtXjbRyC2+s6LC#Wp`|vpjOz)dJ+Kc_L?}a4K(l!uZ%s?c(J8*HOF6bS;nOs7Ua9 zTD|>rm(E37CY3uUM9SXWSU9^x^H<-Qhl<6gq)I(jNQCE42kk$$|9^;E-{i~;!^cU& z*IhVvd^p6tKE3C!SVU@kI&1mAea3!yE~Q2xeQYZapMCvR>_^SMtS9%C1aj`}`591i zdCg+Q=sEv)bvy~Y(pLD_<%{%$&u6F3pC6xe`KM6N#(n$t$+f)coRj@$V+bSb1%tkq zocDQKou)ik*AV?MZmP!N>(0ORoSk-ed*_uqo_re~TY9x8^YSv??H@nM_(wh9o_hXS?ll5d4=a~!nnrb$_be>|e_*0@&o$tf)^Y;J$WE6$E&RTbT z?mwTV@4r%O&H4D2)OXjEU4EQDXZ2RA3Hef);0fI6#PR})%E22W4jC&$o<}K!1{}?zHU?T zqthDOZBJdzoPYc6_WS?h7Y4j2VY^f39uN{@@;W>I_pP5@qG^VmvYZBeta8SEY=OT& zo?pFgSJte~&k78nal`j@-**dh$a*~KIPl8H?O1>0kcy+9C zOP<=K*F9Iy`Y24Q^qA*b>iKk0x1Lhvwi6E?Ja~C`cX@fyr;gem9^2M`IeF<-TxR2Z z?*(Sdem%Mx9`9RwLCM+K`Jk+I8B6NM$R~dqf9kb(`6+LrrrHHi`TBz)tDH$X0wIpi$B{=Z503g=GCi3Eaj|u+pYZtpIpAQy>Vyo zj-=fi=hS|?`P4Z5+@2btf2@(GA0BS6-<)}QnOSe>tgXP?6v{_2o2+(f`tGFM7q~VxGU=p!#j@t5>fo z&EmcM1Zz)}zrVLPmLt|X_4?{R5j;EI^GK}z@xH;xAoY|;)SPQ-W$*9lMjf0xb3!zlrJ}KDHSzuxF>&N5%?FvE}8#>>G30>N# zy?M6hq42q@{XXr|m?-bn!2bW6f8D3a-}&Y3{)Dl0G%BxHxiWHN$;(SzCr@p<@uVzr zFUL*8Wa$ift`>LsXDhG#5EtP3_3M}45p%;vrMV7`Z>n#0_-$y&*Z&pw2GrG8Id29! zweH-HWoHDJY4@i+2|vDW0;}bMIlzCkKzPeog_-(j#AAUpJqyCR+K|wPS;zvZq0f$JM8zuEWS%`CoA2KR7)4W6&@BARHWhfgyEz4^_!Mqj->(-_sx$j zm(#u;SFQVUx|n6v7md9iU)5jiG=F{L`@2t{in`3B7i`|V`R0-uql4S{R!Ov;(ND2C z=jG*g)fP(mSF)4vqp+?iC9_wOuZZZ;5v{d6zQk@|glOnuf>3+xL zSidG;m4;VK56;W@`NG{kYWDQ$;Xa>I`P0wMu{1F=Gt;>c8#FN``4~f~;_a3D_^&!P z)+KiynY?WSpXB5BJ(Ay!^L)Mdf8uND`#+9723`0azT@t@n#tGQ|FE8)TYj(d=Jb7E zmZn#zr|xOt{hfF+BH1NHUFDU5AIH?s^Yg7AMnpzNn)a4{yLj>9MXeW`Bc8K7e)S{y zL2uRjz2B82Yve;uO0U{>;l|96@X*ju*H1b6`FlRPwI3-|n-o3wmx{n99VzKlKHC-l zRlcr$e7t|Y+Rwn?n3y-2`Te@T*j`#4%boeYXTJMS|3pp}rhly5Vrv#wx2B(;XRH6_ z&Q9YMA2+Pdn2AZ?0cd zwbI`;x3Oe%6Ke$dZVfu&TEwm%iEh-MVjSd&9C3T{coCtMM@fkJf5sG?a=$( zpfjx+j_GHEE|UtAyO)&qd>hZs{D3=bU7Is*ez{n5*7Ukh`R==QlY&1Vk^huB%ZMwt zyYJ-!UE}BT>+4!?zx_6mCn7x~W5w>hd-vM!>z>M}ylJD4*RtD3K$qm5vP)>V^Xcn_ zJ33PQH+v+FUwLdhTm0t6M$gIz)&EnXxV@?YLG7R>u*51DB)0*bI z{f#vm!hNQurgOLbw2?je<>lqtw%E`Ql0F+IUHMr2riJA@XzoTH)Hj^h;(gxB&CRV! zzT!dSa}znas}~o~Try9xFWvUeo@KKphZIY=+4oHS^{Xl>^SV;k^|#$j_N%InUA5+% ztFZTl^`(vfr$y(@d|tFO#Y$38m{m1V3j}i%B0_^g6mJrPFt&(bAa5?}b<AjNK;nC67w!GILMSxPS{k~4w(?y~4)J4@*b{Mxf9kXUPC7bsO2dGuH75X%(b2_#F?{X=g!LgN{(*R0uHNRPY}ABdi;&npP$h`e=kxg z4HUB3@@QF+>bdvz|9|VdHb0b9;EOyzMKf3|rtqlfWuD}=yh*z=a)fGMG5lS&Y3=>@ z>y18s=V-Wg?b>Cg-9Aqf8n!L`cC%xhG4HQ+XMT#konQKGe$_ir|4#6J$WN(~S7%Qc zefwT2Q|oy=uKMlPGU4abilZDS*7vh&y6OA9-|t;k_n-IX z>IK=}T|0B4pYgLE?|JdcVCCLNJCX)26xr32KmJaL+*4tAaPX^dX&T^6!~QB_Ip{APUt$HN!`Q{6qa;Yn)A$+ z&AWD4dA-@-k)FOh=jjU;Bgth4(%2eaH2*R5nZy`kzwhpa3l97Ix>=eJ{#AP>aCMH- zs_mD;LyZ)_IRC7%gY@f}+4<&7u-TF0rk~c4Y_ni~)b;%ZcT2BdyzQy8=z@NzU0$H= z<5#UdZS@I4kI$yCM=sdEfB)5I>^vKPNjSf_$+P4AnIDm{v9ZaIE^#u-%F1r4U=@CI zIwwu-hpghvcbBi6nBJ5kp0)hb-wl^8U7C6AoLzaRU5c5NRafj02IGB(XAAos9&KSM zZwq;|k%^tP&0xRq$KQWl!5agZm{!Xq?&~jnAl$+eS-wYGI+V$=b%ToJ)AN%*8 zXP8*PQvCefUIC8`an)VMoyHA?#~0R021JL}zS+EC!vrDL+$UeJ$D6NX=-v3*CBstM z=Bfy4H!2EyzLl_z5pzj0 z`|>e|LHb2YpLg?v*Vot2_unU&4Vv{_>;Z)%N*?$f7F zr^IAWWM=0}5SkE@Y^r8wx=@e({yxK-4xc-gK3VnY)2GTztNPrI@Y$ zPR{*FstaeV`d+m+*k>NMqd?@5?Dc!Et?JnL;>E^)f>&T`0gmq4CU4g0XS%*uK6-1G zs48Ey%D-=^+TR2ozsmhwxqN2GvDvRL7yRp6x_NW(*M@~F_>y10IJNlHf8{Lc-#YsG z^OybH&e3prxxf0O{IqB4cdO^EE!%J^?7}wHg&XsZc8OlCtgJk$xliu65D#1FeLuy? zg>PbfW`LS`ilQnktkMk~61PstC`Pm=zuCNK(IPt`#hI!chvGm>u8;9bo4qj z_D|wj`6M=TsrN-(p^Ijp;9R(P&Sud4W(S`ZJ)IbH(>XmoeY48P+5RgU=d2De+M%@| zMg7?Q;NED#b-e4}@8p@^`D?==#X~Gc?;hnDGeth-PmvZEW->fV1 zR_uO0ulk&}uI^mtzcTLe@%K0We!u^JDK~#fS4m!4*!R9~>FxKeZ`Le-rG2RAT4efJ z^~&vTHKtBy4xjQVmJ{4M|Axlycd}315B7ZRxh+;#esfdmR905jrz)4P-PluUJf(r< zZK~^>XnIWc z`o)ts)^5KS<^NGUTgIxy<7f-K zk{MHPgudOp@I=_YBPze6V{_v|?28^9>5P?B5R2@;{r22{(Ben2W1TAt@;)Zq{&0Ij za?}6c!uyK9-Aunc`Ky_ciOH4LE$3anN=TpMESu-|fy=u7c%OdUo*DP{B^e*@m$z4) zm^qU_c~gz>u3Ej9yMF!toyl6xx^(H%I!9Bt=WSsb?S1|lppiZGOEoJa&%R#2e_!DD zT>BI30jcLo-G8a>beJgnO?}nAZQG{33F@9>ReI`7*7XTFi@$EmSfEiFKGn#e&a$Uh zStmTcwlulx{QZwTy}hk*RWFw&&-$c&`LWBdNh{`g@bA$*(qO0dOYXke%6G=|f1Zh% ze){Q`8982@?T4NH?f-sxX!6BTXWP#{Ni#V&3HcUm|Cs;Gr25+%&mZp$A5X~oSG4Qz>g43CqmP8ZbK;Zdc^9hP zQDWms^j~$%d_~^|k98Ko3J(uz{om!}b;wJ=<+DW%SZ#7=$2OmU<+t1$BW_MS z;gsW{^5FKOQ)?nOpLm#{?Cb0M+hk5snDFh$Sv)~^KOc(G|6)Jk?dMW2_OJK$R%;%- zt9H)%{hp&A3lHng{H?&wR3B3=ci2KL&cqMt5;{=cjmXr3{2%$gK<)6wX+ z`!%(dXXe>f$91h!ROe_vd>Ay?yzs}ma}`H-yi2JcG4r6i=uzHxw*AX{^=Wr$Cm4S&USh{`>t8qnk!eXobi3Q)ne8QB{u#2e~MPK*7}(4n<@F7 zqah|rmtpmd#2)4on)VMPDl2~~NuQgoz>&oC^XJch>+8O*ey-eOxzdQAAn8fmnWN+O>5h~@YnA%k19_H-8R(D>FPUv3e<|X_ivK0 zn3Q>W*<98h{Y^Xhi`tel-TWi9c-5+|rWs~NHa0bC`#3EnW+coBNWA7b{p5>Rbt{(i zc6D|A`V!mpxzj}{S%1$5r`ay^Hu4!;%Vt{r?QTO+krQY<@%{xk%6#jin-E63{=I@jvDJtDrw;UH$O^s7qwYXxl zYgJVhpS#jT!IpnVMQ+;|#c%ZqD>-iP?IC~t4lUo04#$nHtZp5*EjXo;Jh4w+J^O9) z6Qu>aHkz-Pr!QAExvMfK;)sJu-_uF;c55%Z{_s!yD#K}p#G`-s90gk5u8G)~B)Qy|9cm6urL+-s#Khq6CbeP z_L|S4zvg~CziO3M)WN%MZ0zjSs(h@0bNw!8NE=EoN$N1GImyk!H042_^54IIk1J0+ zu_DDUS@Ma;<&QHRzHU6fZJQY=ZW9w1>hD`={Ql$WP2I0rwRgrB9Av$^b?esYKA%$4 zd6)amt@^(I-|PMNo(qZn2)*O_OZm%#B|h6zSJl0HaP_v~u378kQ})Eve!be8xr5E! z*ubEm>ij(0Sg|+8FLq?y)?D#MB=-O5iVMbh)`DUT_xg^Dd}F!NmVey1?##pdhbPn& zZp($9I(<6xYe2V&xp}wTtV6fH?5YiD^Lc-uM*XCDNL<`I;hJ^@j&{&ukHeG9k956p zSk{qhu&$R!R$lSc_PTqO&o3U17CZUTJnrN(x4^yIV&yqlYFu4ijV~4G_Nhwnym}Qc zann?}d)e1DZEb8(dk(0zwYLlB2OR71t+9Q!&-&>3=3e*3$BoZThiv1ty#4s&kIOZ= zeFr~GkaX@Tj<~NDd{X~s#hHoi8Iw;t@OfVR*RpMTY*}Qeij$G2fJ@ej_3Qs%*m^zg z_bYGxy;l}VZg~3L^{wRfzLy#kO};&4(W*@BdK`50&$HS2^ZbiasuN zamly(`P}kLRm%E19x%;pK3t)vcX*$-(J@KcGkWi$4zuknm40<+#{c5*@aq>(o$~UU zwb3IkF7D;2soJZz=iDrcF*;^&aK@2_Z-U0f@(lKBTu(PIS+b<%n9c7OZ{NN(eY5PX z{A|td0ut=0pEs|WyGCDCcK_39(XV3lrcZxz(~f8Ty0nglWjY%YTq>JQ-X$J+azm51 zspi8$_T`>^PbzHW_!pg5D-l^N*PMC2e%Je*&*!~fw0hIUT1Ua3NA>GAUVL)uZA5JB zt@cTNCnCR}J!8YRb@DGS(AsRhtsd#=%QKe7uF*ZU=0#Ct+53<>OAr3Xw;MovLN>?f zg)5%iVmhz>U*%LEW7oMHcGsrdX?PdCYT{SjjOb_i9<{YTg|g8->OXDep~+Rki30ApS@si)0-6hS;nHkA^g9QlGOYZbLXYy zj>*!KPYA7EyH;0Sazp#Ug9qhK)*fq^+S(a^>cVgJj^mC-Yd*PtIvxPJDT>>3+u6%^ z?yT9}(ReQAjB#DhyP1|nrL5(*VWpT4o)eMi#X1-pMM%Fx7dYM{AQP~UArc7|A>n43$~2PW3MmGy)*B}>|;uq zS!xVw*6U`^5CLt%n)j&Q{anFG)#+!#&LmqkX2?C=)4rnp)=|5^b@iN|s#%&AE?mgC z&g)6egx=&^KF2EBU7kI-dX;_ZuP3eIaTQ^+)}Q})BtCZS;{;=+;w_zZ%4|vhjwFQD zchv6t`E0iAiT#r|i-?Jd9W^)nwe5_W$l7~$iK^dM1}~p>X14kJo!*ih~-O52k*VNQBWqQ<-ty@jga{^}{{1th@XkXG% zu1V?LF84h)l)sPLeb0lTb~@uH-z9Q&dX3u*%|b72+?stoNPY`wvC{WvXJ`8>J{L-J zmiGyd?>XsHSZKI*kB0!Cx(pvb=wcXkpM3|t&90pk__^)p<3HOp#B}C-K6UEUv)Mnw z1qySn>4)CF+0ZB3a_O*`ew>fh4A*-#pM7sCW%(q(nUt8LJeis8PJZO?7tJk;jwm-);D-IR8l)vH%ylbdD`0(Mwg+G>^*xAOIHT~n`d2R1f-8U$#ymI~efWo~lVgz3|#tIv2J^7qEs^=I@pC{8{F zT4>r|X(KmlwLr_^ha2VpJYesi<&kEtW;`SI;#}XP`}2Gh_OmYGPkOdnV;*crJ?NCg z&D%6s4xLZ3S@!At$K$T0oeQ4Z%+^y(fBEW_*HdXujcF#9FK+m6`q`rSOe8!xS^1}x z?xm|&TWd42HvL*48Xr>fk@?bg&O;AdoEEO=Pd{_$)Y}RD?A<5ik}lj~m66sjetvH5 zqt8Dx4!UzMiP(QeBd5#U`St?2u<&qm^V0CAHE!G~{a_L{Z z$iv6SR{rjeWXPHAn>O-W>D^MxX%M>ezsYNb3$u}lp?kczuM?>KZTWmo@w#8n%=cGH zI$eAyED^#r@sdW5vZeRpoSJf6tCiUBXoAKslRnDh|Q<@Vm zY6#s`d>oy+p`(udbw{6-zGmThwXCeHS1Nw~J<1_l_!nj|pPl@m&e73P@zHxB52u9z z9szT&GBR|(u)3Mz`b!|y)%B=l=03CIpY8vDwr77dJM7tqkj1m&4Zr>=b#YoKV9TMl zaKVBD()X{t++Pzfw(dsWo${SAtli@J`|^bJO;=}RW>$V(yZxTf+7s=*ImcDZJ~Y(c zV8e)?48 zwQeiN-iZe!GxmtGd}^MpWol}=<h{kMD^TZXzkL0A zdBB7R=We%8@Xa{VBgfP0d1Gf^zVz0a8F`yMva_>`YbJm3mYDcv(N~kI(+#G(cJJm_ z5Kl^&w9zX>|CZRoOUn(u6@T%yobn(KwBzprQ?SjS+G{)C`#GOh@9aGbI^rw+>C>kZ zc@rm{J9lnQ<%u(z0ZqFlHv4`1xNG_Rx?c?stXUidwte2%U;AeL|F`%5y^V4>`EaW2 z#je_#L(P7PP3KjFI^RusGw*B1J5v?$KZa~M{u-;p)~?dj&{z=OdebF7{{F>%`}Q&V zvdTW5^5FGmr%rd3oyo7(T1XwQU%Y+$_V5q4TR6MAx`ecko_MA_Q@Gwp;{Wc-%F213 zR_yLVT&--rEUQY7xAi!0ied>qbNo+XYYJb1&`Ww$5JV;=Fl>bcU=g*(5H~wiBx2z7!|Nm|K{@R__UoYL) ztMF^r&-|Ll(*Moneh91TCQQ0__Qi}d1vcRmU)C*qwxe5bSI0fCq>20X?NerEW^U%X zc}_Aqr0IlN;}@&o#tV-gCB+vOeq3V`+n;=MYxZ@!$yJhS$6Qj_HrbuH9Z*xf($B5C zG9vx|zrR+T9A}c`k9_gf{tyXVZsj7RMAu&=7*tt>zmkL2NhLBoyu|96p!N zEvB=8T|;-$<6ld4@_2HyPrhtocjtU( z20AN5W5u+)(ds|*j|Hx|$9MI$Sm3(rKQ@YLhw1npUTQV%@$vr4N^FsCacpy(mkB;( zlf8K4GUhCzpr??box2ag?RtoJ~=trwvfF; zBT+HuhmTCZ#4)?=iP0$*$9_T#tB z2NUE^?y{}edynhLk3%ZSV*TB-Z?94M`}<*mMe=WNGb1Z2tF%w67yaw~om6($KJtUt z^_$Pk&fNF*=zF_#_wLrybAO0{5<4YjW?~s@3aO^&aDV85gDW>4>oZ2kjln&m_`TBzHZ& zA>Hy-w{xHRq^%OIt*v#F1e*%Xbaiz%v6P=_KEG;Xue*i5=Nm=c`wu`@*IF!E*sqn?s(th;BXqtUcVIPb_e+ zk9td;blL>?OmS- z1q4i(VO=u!zQk|+uy?!P@B7@{-JS0DX=xfOXfg8lyXE&c$0RkK(|RY-ZfUkr@p=I3 z5yid!v-UWyH{5>i;of@rCsI7jI7s2OP~vDcifj z#LR3`Pt=pC;c~sqfvR-hZuHp7BT5>ClN2Ckl4`PZRr5S(1GD zkDYw6@VXm$YyMwow=>zgWy=;j-|NpU7Og&H^Yh8%O)^qBb9|p&SNY#}HlXRZO9*2` z&IHY{K6(3n+P=?y1y}@?f2~@ZU;8@xzfJC~Ekb9Pl-X@$oPFnr%I!t9ic_AEmy8+ zXN(E^Ee{P5sZ$M?PpG6+N!QCntG#H=+rE3jrcFf`RbuBScdmc4RQk%J=lN5c&%Tja zD%^2kPWGir(81IhpY9a3*0jc*yu040& zr`S|raQN_H^;xrKozuv1y?OY&kKwySftQnN_O6Hx+WF~}_VS~(pISVvtgRLQPO|jf zKS#w{Ln>d*`UDeb5!_tsayH*r@mlf)TAr8AXYSe5zj5QnE$Tbcg=VeZ{`!ad&lsJT zO-s!0mRxqt+!1s9(*67OTXye`{yIU@`B%Nf_VfJ{TYR6S90g^w=jwX&kmrsqYmyTZ7W~cs|1CfMV#M0H{!s_^w8_cxoDrNo%l||I)6eUiv-~wc zb6)QJ3Q@^9IXNF%#p5izwyh8|n%VqBGL)&2L02VV;%hi=tH9WGVTAQOR&Qsxav(>k*yFL?+|M~OhkICP=Ytm9uRJNp_pJ(s- ze)8$3T~jmmCR%TMU$*=BYvIz)ZO3wx=YFhaNlb`Q>-i*jf1X0{#iR`5ee!?bym(=e zQgK=Q%eM6$tuEz~X(gVSx4rfEPI+e5eL;HtvA~;-UX?9>l2cPtKL=IZ>s!8i_ioL7 znQs4+IyQQ?X{>8M>*X72NIKWLkBOy#qo ziM#V->6=2DD$9lLEasYhpmVB#!SqD=EN0Cqhb~>YQj+lU(o)S`QcVgRGhSX^9)D8e zceqQ_ZuW-~g_1(|ggf6JQ~BPJ!1MI=j0Ag5ucGrA2Iq8-)h=SP?JPRXYksC^_uYG- zV;?MwpPg|$b|a%E{Ao$UoJB^*B^REr_^+v}`_=#5N{jBcwl*c_rs}YX^XJD;VO;Kb zV8^DMGtHAC7Fy0+f7t5svHtjqhpng8A6m)!mbA_}JKKD_woK-#%DJA;$`#eV7R>y& z{+C?EJ?rv!Ho1>tirV(@@~ z`R<0xcb14)Tk%FF4#cJmo0Rrr=Ous$g*wfL~xd%E7* z4^?|bGk3`y-&FZIEs|-HSA25Aol6q)A`^ttx4d2b;`X{1S3h36xv$pR?BLv`JNdhg zB?ed6%wv*1cl={XY3bZwf1ca_-|}qs-qFx&B65-W@oS93F)~kWR!Ghclmp}dCOjd7AZaQj{kM((GQFF zh8lKqjYdxxU&T(!joTT-+SU8P?HG%|DaDDBFI0p&UQIgHC8`ZNq0Dctm1@wrnaeKs zaEcVK{502jwOiDo_unrcmN5&8jh*Wubev`BPbv_CUZMF9$ z(bunD>G?cNeWo&Z?%euob5=_lod0~><;A(%H@>Ac7i>KL_V)I2HzmQF(Tj_uw%?wc ze#lU-ctZblZ@rxM z*veC`rS?ND=c@K+3Qy;NT4!xL?zzoSk>TsN`S;_o?#I~J`D%;pUg)&7wN)|N&h}S$ zc3NY*u9kAsc|p%=t#f+HOfwIN#hp90W9n^J&v$~IZ%>HWiE%{cge`Gj_3F5HXsGDg zp4`R)3!5K*KA(U7OL~3D8-)}5*l)+y{d}5oJv!z6y}efX7ZPXvi$7ND-r~c4Tp-)L z`ddy^j!x+lpF1KQM^`>NzGu&?A0J$gCqLbGJMVV^m!9GB8{2ZD_3TroZGAE&SyjmK zbg+{_>>WGi7OR|yvlbg=EB}7I?qBmd`o7oMqXI0B8XWD1H(kALd*Wo(A%7-qrcWxz z{Qph`uXXp#BnBJjq8iPDYZ8TRL(cezE;z_b5?*tWvf{j z=W|)^O|wD6YzsH=t84`wE41)`=&8V7@0D!%H4}7hwB3JizWLS6GluK-SWYhdzhkEr zqkPGvtE(p&2JoByyBh!Z>dmd}#pXk+vUJ3#c4xjRSpFK;PsC!Ceo5udPDbXU@`;F?kwSH!t zn`8MTeWOQu`tv1wCpNr`y>;+Lgq7LU?30sJudwSDJv%dVnum(fqKO#{xzdhb50*G@ zYHGT^y7WuI;yn&lJG|T#i}-K;U^+SF6kkh9xZ2_g``f37-i$f+zWbR;+l>34yuG}n zGR_$JhlgKp+`(fo`IoBj31_nx)6?E61X{10bM5-|$$V+OCno#bb$S-0b5ye%*Ufab ze0KHPwO^iMZv5msshc0jIX#~EVn#-w=D*+_Rn;Zc|Ni`(KGA13Xe`e5 z^BH4zfyc8?`Azs)-SSxOu6g!_Q;fn7MeC;?ZZS8xGe_~!tM~8iLHh>EC#`E&=#Y7~ z^Z7jM9FG)pwXM?jl7}3Vj?8#6PvUoq#PO>~wtdWR=uEzNMAxh6e1rlsi)g~rSI4%W zK7IOSXO;BNyYK7RE#-Swh?L14xA@-pBtUtQ3e$>t<=Z-C3WWECM@4z9ci$p7|9p5u z$%Ay6!-o&oN)|}<9en-u8TYTuuv2Ynn^Nrl?a&fXh}KPNO3wZI>gx0*4{sIqot$+sg5g9*1rC>ZTn{{`zvl+&R*}W=i}p>w~VFSP;Yv7{K31qJNRqr`z5wXUYWmm z>Arn-7cN|ISbad@#hW*Ge(!x>`#$&V)!U_U*8HsOtxr1M>Rditnj7%2e7a6#kmKXN z(zx2MS8JsXDuyP^k?iZ-wwa0d_hinLsf?TSv^T!La^=c|`A3xw-2PbN#dOh*7pm6Yn_z+{Y?M#HzkAZhI!IUlkZi%UVCx-V9( zn|z;Lc5M+kUM*R7ysCnYo~$>Y_M$1+Zhn3gm;UvSb1aLWCH&#F_BmXBlIORN(Y}d) zCpc*ww4K?JxS*u)>#ELK{zc_qzkM?cZnAb1c$rpGvgQ7UChIA8Blg)~TvXNJ?6Mz80N7_ex1e=aiBY znXf`z=T48z*;^?8>n!JS=0=C9waa~v2;UJ-n5&$2TYBRq_KZ4jM!A0XCD(5_x>;PF zVP&8oHNB+k)WWo!+2+Z|dZuRXcrqsmw8~@p{y(YRypDQ;FBXXVhp4-=ELCdSx2TB2 ziP6f;z~F))2TO^car%kVr$1}`+W4`ayO(p1@ZH+~f4|qCd0SSk{_)bLw&nBds+!Y+ zdUYN;y4$Dk`g=VwfhlRz&I$MD2dvxo>lG_#H+w=xh6d==F@;0(TK>FQWVQS|^UV3I zlhXg~D6r#TVVZIyapJON%WAo9io7(Ou*bOd&7|HLl@6czPQBC8)}B56SaGRJJHPy~ zeRlKSXYAe*NmH-{RJP-f0p7tYpO~|fk z>)se0F_aD2RoQdtQqX?i`u>ECuHix-&G-H1N`AO${-bTmZRdL&C*H}s(|%0Z?%lGk zlP7Pv{+%Z8dpOlv>Vcy1vc&9Jn- zi;Ii92s-_&8g%;Ka`pvoOoi4KP^iw6v z4V~-Ot$X%EEzHndsd$#r)TVj796UjhMLZ$>rRzBR#l^*gn|Ak>l$9;37A?73x8PNH zi|>!W|Bi48rOD>pDi&0FdELw;th?ch!OY7(U$R`bC)kJW`*n5wyy^4j`{ys-Ff+km zNA$)dac8xH7c62yjj!IXoaC2ndwN0pL^#xk#tYgM0{3Ob}E7N? zYO(3t=F0nF6UWtNv!$zI%I}sM+sg5mm6|d=I4T}5qI%t^B~OjJY*F(9ms0{imu{?+ zTe)97aGC%7xJTcAXJ0PR?R(Z=|3`VsgS;c>cDjkbI-j{@!-fmm7dIZ~nZL+L>&KJR z9ZB=KVix?b-X?mn^kfR#FZ+&v>U(NV$+caRz24)IE9WfxMK*J$X|~u+AF-cr^Xs|y ze(ajr@!arT^`Ek36Zxz&+HSuE6%p=zG8>bwC;z$pa>B(KcU;78cptuhN^(!!`j7uL zyF(NEm+Nj@*8NOcqo;IJ)5+~Zzdk-cH#a&gI(oO3>-5>iGFBxVpBYRi-R}PHzIZk=W#SC+_W|r3+voDlAJk+{n)r?*l`#PE3y96(B%(J?VT>jh5htvRvn%kIO64=Zx-_bu2Fqjxp>)RbNB(-zO3l323lqn}xZ zeVE?fFPHj#E}2@*diLyDtV6)%H`%Ks)qbQF@r7G{N~sf7yJ(y=KQ=aY$^*U!WxH)1 z{!hJ`dMV{QXtzpHRfemticsgDFU#$phUbT?ZtMQjzN_(22^*ujZ1;LYclGoS#i_o& zyldSuCT`odZP^dCn~SPm{D^cZ^-R6GDs)Os%EZRT#>=2%m4jYHOWZt~BmB~RN&w?o z%|*2@=M+8eH3vd2ri4Ybz_Ol*`7w&(b;ZM*F#9$#blGC}O-&SG`5gL7wYynop+d_MEKi_kNpom3@V4 z$FsykX+>T?lm(~AxbiUjvE1;NkkUK6d}EK~cYax`FIVPR7Te9QBe_PBz^JxCxZ`yr(e}Dbw65GiZ2WKqGOKRrX-#({CT|n}*(PNpn9?f2* zrKJ z+uGw;!l_fIx_0f_RmJ-EM#l8cV{G?!%2luUd5BwoPusuySy9o^_5UAr>&rQ=yT_-h zf1;#F@ImiQQ0w}Y;DwsA=b!r&`<1v)HeDef@%Zoe`_GwO&$Q&8ntXMe26!6ol6Pv6 zo#VB|tFtHBL@(aGTY7f|!;<8g`xr~!U+Oz^Eos(g&oyV}+s}U{(p^$sZXW%jc~za| zP4o0a_vNLfr4=g!g*#nh=J>7kDhjvl$#T;#l!}m_arge&l9W0gS=sO{R=Zybf4Qsi z(;E%(bCddwfN&Hn}Z1gza2J6{tc>1R{Obc%kF$1H8JpN#j@_cSlf>+9&$N{-+xyZbDwy0X~~e-bA*nYy{Todc~Xp0ccelKku6g52`*hxqJ8SVX=LAq4Pp(J$~JN`0$})cl~^WfS4E_%Ns7W zL2Vx9Gfiq;4litz+^od1tbD{-Q;TY#N(muGY8azTf}C)nv^!@!ScPV#TVu-A|`Q zi}@_8zrrbFUpHs_^clfly;8E+N!43x(dx+MFRNty zO)THNxVYGsE1fx5eCJuOn}@5=>GEkyFBPnXonL!U(J}f zW}7RcOJktNbCcA1F;UT$)vHQ*4 z-PhmTo<4V3{IVl_pDJwbffi$C?h|vrc=KjwWiy}i2@R7vL)W=Hfp@}g-FyD|r|Ew- z#e=WE`gP`a{W7>Fuby@Of0)qA2!UW_vE`fCQwz=CGzE9QuMS%mvs3Tr^m)Qtwr)*) zp0F(Ne8^*I-J}HmDG&aMm#C%1KDM0p1a!R6zc0b_e_e5(SN(41?(c7JZ(q20adL5a z`S*3})~&m{c=6)DlPlh`I9kkEVYofWGHgO|wZM?yGnB zYe^&vCQsowJBQ>wf1p?q`(#(sXQF$eGtxoH8}lH_O$x+}~ZEAO2Y;O^hQ)Z-syJ*VotA+pk`;rl;u&mz%r0Gbj@UI~Xxf*`VN6a=mnx@!^ki7&czg ztM@T$?`F!Vsa<1YVlp8%YvQ?c=l(Xt7<qy<8gn`SWLQ$;8g*^Q!aglm1=! zT5S|+YsBcd{lC|3et!PwO5?YuYIAdSZ~9#8u=#o=*eZX|q@S#t0{0)e@!Y3i zPGV}+-(O#Ozg!E<-m+zj1HXjPlHS8Cn@%~G1x$PN`KOlPXSVttclBx0w(XeRS;KVf z?dnHG%TDe6esA}h9Xn=-8EjyD!hB^{owwBaOD>=}=crCAH6G?*6}uTO4UK}cRveD8 zp1zPzYDw~zEnDXNd{@5zw_I`#`vi+u`~LlU{X8TjWC{yYGeB5pR@k|W%K;MIpsBb43%b8xH_grF0jn0D9|!g z+r^r^tniHQpIC8oHRHV59`-X_nA8Fv&2+dZE@e~v{G96Y8ND*LRaee%C7($;V5U6Z z&Q)UZqS~1sS6Zo6?Py8veI(xmh6ej?tLdFna*%}XPE2q zcJ2lF5+REPMho9QI@&FLx#C~nlJ)D~U-sBGr}uQ9RgWB7Tt9vO z?ER;%K2bwNO7x5GMYG^9>1j9m3+&!5nGs_WH;qev!qFNgwjR$#S8r^6T(~-X{l1g$ zUY`w}SNH2>Ox+2c*C$u9eD`2c^*-jNw@xp~ z(p#cFZ9Z@Jd;8X{Tc5xFE6&k$A~kRS-*4;xz6#&}%gI3E%~QUQ%uBT2y)l0G`FQW` z&LkQB-sELEC-3e!uf0yVLu-2U_B_?23a&9F7hScQ`GamrF4(b{X~Wg4S3gfEHfc<_ zzAo1HqpvLM-A$hrC%;?hG;N`R+O@!E&$j)%-r?DM?R@RK;`H5j^W;zNsqN|M`QVqR zHZfBBe?{H9O;5{r@7ndtQ|`^-*I&c9ei=_b>+E1#@3U(029Xz<2O^e99S-3$?fJ}; zQ+!%?`x$+0ZR^=7*XnNFzP%l^06S&ERYVIN0|y6o*}^D7cwA-FN-Um6rX#w#=D0apF35xrztjvsQn0v;Wo-e^o9A{+#SmI-zdvjc5P)J0Az^ zotX1m-szh}+PmW-bB~lwou@PVwnWhMl%`7k%)IDrIgz1`j*gPOq0cVPxhfLCBKcWl$H(F+zn{EzxxMIb#LqYB_P-|w zXk0Pm{IA}muxSdYT{ZvjoAmETOVdEyDv4ZZ1c^(b@QIrrahZ`^JaP4y-&HH`SRJmt98A; zu+X`^;-?sk?DO;U^|NC{C95h+=Y6;4h?Ac9^L$%sVPRqGqyI`WF~%MxudZnBDx7?E z9%r(AZ`YrPqLHo-mm1If*Dbo^+^h2~Y+GmN?Rr_2AEWPd;@;ls?S*M|VWnyUopyne zo`((3?>u;_{mJCE)X-4TTQ&@D%67*lxQH*ixj8+)*=qWXxz^=%AwfYwD<5zETvA=e z@O8eyEv*}E&0m)0Uz?#@)Fgba;-*Ueu^!1xv2ND4PuL19WK2Q#0B@Tv`><|p_bZ36 z@~h80m+siHV_uw2c|gIN8;19m|GfL*6KBBj$ck?dkH|esN-*M|x6k#txwW35q2T^S z2@{0X{Z`c2%`@M&;)e6tjOI=Imw%Pp!s}_2a$-V7=dJ^j&$}A$-hAzKwBw@n?dRJ2 zgkPFWs9XH~bm+f%3bK7$TjEv-T=l+L*Yj@smcQLA)<66iv$RCTs#n^aZ@*vCgvZDG zPw#Q?w68bUwcPeoXyO`IiQl%Gc6M@K6_}??nUdo9?Qu(>f9fke##gJaZ@B$dao?Z) z2@jgM^s!sej~*pmoqmQR zT{e^PLc#z4iM(4T7&&TN*r?9%Th#E)O5x(~SKeM;r^468?A*foPP9p3?TjT$mURFB z&~E=Fon`B7)#lrw&39Z6%@v!MEcMZL&+{Y2zb+haczG*({oAa~HxJcT_%ixdR{k_> zKU^5+)F&vZx@AkpMMu%;LRtYXp;H2L_p`D#UAnzTA@Kail`B^st|_o?>N+NQC(?n< z^hLzAAl7+{xUJOsD`28sF;tUg)L3%)w@7c0fvf>X(43 zJMtC<3T)XDOSWwJ;<5PbtQRFM!ft=_MR%=Gn&5MCvb(EM*5oVu>;G>{Oibjo+cmEt z_4T#2?G07hlMXIn>J`}EusrnDH)7ulm5{p!gMEn1UFE*Gb-~jb3xj zdw<>ETjuMWN*h%BKQrBZsZe&!>iqln=N;d!QTSGM&^Ga|ape7`Rr*Q+!v5(`?waq4 z6FzIT`8~rg(}{ad-eQ-1?0I*0x%=Fc3if}$OwMVr{`h&LFTYV)*~B%hve!4wBX(EG%T<^E9s7J%@LZX3&6D=Yn(9y5AOE}; z@iFx5C%LK@3qc3wu5?>3{L{Xus3__+_Z1Iii3j)gY`WO2D;Sayw!nI=i<9HKJrBxO z*6Q#5a;fjop+kS|_QZ2Eu`p~-I~%n?-~=D*w#}bh4=ylb(tWgl1&h#zPqW@iTa{>> zyO*cb+S-~{>lA(R$Aw9Ub}DR`tD!OHR-N}0e$C|*Hr<+#xAW;V#)S(Peze=6>v;6% z=jYvxR%=?*W11(J$~Zq4nz}S`XYTE7PMw<+xW2r)x_a}I9WE9e@$WZvptLcO;_*UVRygg z+LBFdG7A?jT-fk2X4T&NrPpI0uL)aymtXJb`kQ+yKUe!L|GdYgVRse-vr50PTQ!H2 z&yOCwigZ zQLDIqbaHs=`^j(qZ~kQ_d2@!`{w+WE82YQ3{!ueD6s)ZT6@pvkYd##jc+59cwv(Z> zFW~pzy>CChT3jZ5OMlYA4SiBiPE1tRkv9%}vv|qZ%GTr%N88igC9kfW{A;xSRfSUW zb%iT-Z_9*xSawQt6YznMMcHL-T&*m9N5lt8(+M?zF;m z)=BirvD8I;N(gvg_3j{XC*L(WF68EB>0{<6;%& z+lsDJHoQ{4kUn#w)Vb=XQ^Vt)9*q{-l9BfPspia+|{gb%fia)iU>z5vzo144x z!JiJM&d$!cBFzFE8`fEwnJowp3p81E@oCD}M6p%!liqHgb+XK&dUyHHpU>xS*I+xk z%Hq(D$Gbcm8&{p<3jF^%^4as}$qm*H85tQIi|1~>RkeA~g$oxtxb=2CD6-$dv;WxF z(k1Qkbrp*n8_H%X2mCUN6}D_YwoO6X`_r6V6V}{X zVz+$h(ywMa-#E7S+nw$I!9JmOmccRZu(-`7ACHPBOPhE^M&8``>(?(S;Sxn%uQGqu zo!-afj%+qI)#>i;UbtRZNkrDFHDO=>LGqo^JIlLm7eO$-*)514U>cam1RzyJ{^1T z?N*scp-K9o$G2T;TEpX(-@G#a{=OC0rJu~s-zRvsz(=K2N!Lc2cbfZ7*jj*w7Z#xb z*FQ0C+_>>>Xinawf0j&71TSsT&uChu@AoMC!=C>sKOelfp+3=`k0Mj8rT~Dp|;9S~#z4GAOFI9&> z=I{Ue?cSa}d*<5hs^w^6;b>;(pJ!C@;Q?RMfwH&)>uk63zy-TKG+qCQpRhP)Y;ob| z*`VEJZ$I=_3E$jR`r6T}Z=zw(N1l{`xb6pGHl-}vPD>q%(0_KJYgJLg?rd9S<;Atf z`(#0l99C6TRn zRp>;e_daK_ICg1u`1&0iHf(T_kMVZ+e7F3*V_(6ci5Fe@6zV0fmnbUoccu8m8s#J< zDY2DFsMy=bKWpWk`D=~J{D<8>&lXwCY=8CR;HhknlP6E!VB-ynh?t>z^PGznYyMpx zg`K_A&rkX7H{UKct>`Axv?WWH=!J--{C}ou%yc)}XyX^*7g2MBykE!(9Sh=KKR?&k z&OYpBsG*jzKvwj#f87%{mU^9>Ia$s3Rodp8hf1a}WL{>otW zFXqwecj!B zOzYtco!8yLf23?e#OCSysZKk<_VdxN@9*#DJvh)9oU)E{;$hRy-ophg}#p4fNt7i48?mE4ol7su}E-yF36v1%^+va_2Fug%s8Umv$O>)aem@zrq( zO$rCkD)-xLdT=XyeQnRi)0Z+-k{RDl4qPGIu(-kQ-XUSO0P&BUHjeYAuAg&jOQx`t zq~y$|^Ot2}=3j{0oaVc3(Z`Tu1#S|@%?>UV$-BMn>=ky)oErwA&PL}2bd&d7l|G_3 z_1~Z8_VLf7l4{N82QT+~YP#;z6i-R#S#J&(X1x;3fAe_mde6w!t5$I>UbJXeai6rSNboeK9Lms`X!dPZR^hB=imH2Ja$O@)_)H= zqiwCIsAy>KS9Y$f>2n>Ycv$S3l_OoT&To~TmFZiCNuN_sKZ|^KXXj^~Yqn-)v(LJ1 zEuA*!xY3E*di#DnTK#d~CrvG_Ti+jb>%Y4sS)g>gY-@phN45>;qx54^{E@FO{5TY?tTSaPNemsOZ(Ho256UpH`VB<1cakw7i^1Zu6qIp1Y0*x|i=UE#;}Hw{tt6>(5IH?)`G1b0wp>O{O_) z3HvFuYIo4Knk&scfiW?2uI|vX&b#Ds)l;^1)tkzhzqPfswKvAiIA3}_w*26`ySvXD z)CAsoa^Z=!%55Xv_8kxVPQQClI?dis>7K)rcHy6QQvT+DNeMoq@Eug>&%2t%d$=lF zPWS7i$O*SU`>L;Cx}%z?S!HC}C%R?j-@U=YZ}gM}J}}*#tqVG(*w*|`!Qp+|a&N~a zeERDkpwf_eeO>H)(9INo@6~?48*h43%&)+DVT+B}mxS}hTEeULU!B+7(Gq&(^##As zLzR7B48LqV`zh_rjEQL*BXZ(&>H`-3Y%zN%anyxr$Gfs+$_jTijE+U0__*S3+S!^_ zB7aWa%mZcm!j~^!hF5-!V=;}4j4UzWy%_jD^%jF|+{qsQMV}_PDNCPwkf)TGnE1B( z%L~EkCnqL4u_{|+$XrjDSn=jXTc@WT}wqt-6mr=9g$K}ScYsYqzY!rUm| zUwxp-r|B*(E?aUNm9}ezt%>-!?UDNbh1;~U7B4&Xgt28!o@4)Yo!F`7$D%J?zI@qN zr#v7iD5z*fVd$}EXII@f`c$HzFv)P?E^m3}Wu3z6Wm|9NJha=htRdyJ#4_ zS4>ym()WYU<9L$nVOi@1BJ%R`f}*0X=kIQc2_L*LVP+FDwxR;^#Z{+Q&X zqb=?2+x`9g`p%s@r^m*Zc$Hbwc9#0-%g>W@e=~*MX}ad;`nqY)=ZyNtZU5Hx+5SSfqD_3J8(*5RVSJ0+7`_W449vlSoQtg-T4BaY(<~&{#?C9 zm^J(J+ynU>={?u?}_4D#reXc4%7$k@lrGU}z3LBatBldhQ_b#--Oi~r8d{Iclws`aa~vhA8tFCd3o8_NgI+CWn$u2#Oy9xdrDlZ@*juCJE5C3 z5f{D%+%w$t|87cTaBy%(Rlf|VV*Po^TmS6@o0x6?r>%_t^3&yiI?KeowOh}g{$f2L zcfMVldb;p}{V6Z!U#R~fyr!Dz_5aPe{mvzG%3AJLq zXN6+jtY7u?Crp`AvhWE@5ntoqS)woeEQ%(yt}?Hhr|*5gdD?{iz2#?jr4{h=^78Kg z_v>~3#X{SThO@KHKmRgU%6z==;J%~BS#le8pJq5$IOl@w;uXzT%J${Ga41*5nz))} zr?qO+os-S{c0cYvJlyWj&CUHf;oV&)0hI}z9UU3}4>0rh+}T_G{i9{^vpY97CLix! z;ry%o)s>ZrsTUSFa;Kf0_4OO*L=W?o$qV+K_R>{!mRNFNLBa#(6(0lEH;YEG2VY+o zd+|YnQaiuA-HMwnJm0k(*kY^Z>+@bUYKx9^ow9VssdE`RTmoO01TJJ16?9 zTsl8{`+CNcH+2SrZL2m8xbCuj4>o30x-)Ow@)~AKPPerFs4Ss50 z+xuDSMnnMr38au)tl39}P9a_*PQjI!tz6Ydor zm)*QJZ1veE$9qkJ0s{+u5>g&ZR=K*FzlaFj_olx3(!IUa{zdWLm6PqMq*Kwh zn0dLaQ_q`9g@uHONa&O~Tw3Nk+rZ&kBhTAw(s?@)=g5oPU4QcA$;ka>Z*R#o>iUcP z&@<||-0)T_A}yc&LGNar>ZVT?bx)^;mueu~=GoatL((&H?*?pdqD>a^%zr4hHdx7Jp&!0n^8C-u& zD3d>?cEnBy8-<4OZUE;;FqpSNva^G*+ zck-&Es#^cY?MHUWMar|Bp5}htT`=t=^8}rfTEgFVo}Xc;d^Txgg3m-ozqwYb*4qS6 zZtOLGR*}K^GiW)Fn?$A8uiB&}rC+mo142VfUwwIbS+wDup0VAfV0q{|jF67oR>nxjf@$yQn+EF_AMfjh{E&+glxf z-KpPbfu^pm?UU#F0m_qDuJXL|TOM+^$f!lPeBN&svDU6twcICf-f-MFZPQ6N(`jm( zcj(9L_;C5_>+Akp+}z$%_8D?CvDlXFwsqRnpR>TZo$s-@e%zeW*K4=0TkuO`h5u!) zx`6pyU)P@f6wCGL+S=&Agb#8$&T}&6rs^N~BYnElZcJ2BwJ-*KH z5lbVF@2#4(JuD}^wVs==?%7j1aT1s2jS>UvwNo~2o2?646mwcgNQf=j_PbpAxj7fv zb_jF2248!jc5B&jy@|}7t991L?fvy+@ArGJw?ycqZ4qq|5R#RhduzG>{2$Ehd?F0T zG;-&C``mFW^6uOV7T125-+J!*d}CjtqkVCPj%v?7Zlg^fdos5;_Wraya&ofz@~^~;}|QdsV7T(d{*dO%oMSVQSX%U=Bp zJAWIPi@lPpm^MAWu5vn`tksu;)trwDEY8_~zf=6)``oQ_x5GAm&dmH)@S)PQaXJ5o zIbm!5@@5|C5PVtD-y0YiS^4SAOk>?g9Qmo|`7VV=9zS7KID5&22@@`yIB`N`&E;c1 zembS18(fnY zRV|Qfz4Y|K&Elq(%XaLrxc^r*w#7duCnsum*;^%UZtl}8WzrdEo-$rl&sh0!+T2)TQ6C*EY6Li(*JRMvg%vA6@43P3ZrIk z554ewA75NG z-utqiJF4kdu6O4HkFcjxRz6r|yyNxElXK&$uG#}#P;vvjtkQU1L> z_K)BA+)Mra?d@vS^rmT?Me1|*ox6SFkNy?@OV1OYiHnIn;(5n&=+L1*bm!K5y4d4^FwgT5Q8#mdoCE54ZEDTTYK~@YDDuF>mj| z$w5IuAB>-|Y3k`cJL~qMZCYcbQK7+sO_h#cr`omIZHwIX^V#geGT&y4x#xbRo}PB` zpe)OZ+cvVnDxVehwO>7!^2KUeZO_l!-1>Vq)MjL4M9ateAJ~+3Rw_(DL9j>lOP$hA zz3#x@6Mf;0U;oUDEN?J!?#Ov# zQ*Cv=jgPg$(d_Fqj|sW*o0fk(b?S4VfZN{Yd5tqq^duYo^zA#iezHXR*H>5DE#5{P zy|z3j`~&lXUwX6kU)|SDKHm2>WBYC8+5%lhZ$H1jdDCk;{pOzZsj!MEYg&I|S)?_O|la_@-`2Uq{mNf$HOJo%nNxz&xmCx2zH-}~+I#l`OS zpCA5p5>VmTcrzyuw1P*&;lh$$n@w#Zhb-C*zA4{h+;x0HW8+rcR0(fT3yn$GvhdN7 z#LmMN=Y8BxHuG%1=b@8qs4f4h-o+0&7{7Bm^8P99bu>KNQHodRnNpX3@#-YKuN^{^h8#-}C0@ zPoK`s3O;h+P~P_2tVXlbZEbD$)_#9?SN~IU(r?|;($cA*Q-0mK9N+G7D4%NPZSq@C zLiDfjsr+d|LP7=+hZSz#yt!%q?dMmkBi(nX>odeLB%bWhvZ~sgY;0sy^?Sn~M$gE| zn;&o8x+QhgN_W-nH8pQON3%&<8~f`gKdrp<P@)B=seZ@ByW8uI5zw8q8*!SS( z^prO~RXNWZC-mGBuKao?r{a0oT5;c?m>8K~pLjrB!}1pw77DRSZc;sN7QbM-UDNSC z8B3wpD;&SQyIZ}XFf{Cb;HCSb-!uCYUxsZe-TQFIrAwE*PCgctjVU}T8m`UK+}(7u z^5+GK)1R(77J6OyxahM35*=h+6L?65_+rVC?#G#Sx|KH%nLo?7jf!bC7^w9+N!Fm)q4s~3$Qrae0h7j z+(see6vu?9J()KgEdq?6ZcMN&;86BIb@Q>Gr>AE|fx*Pu-`_48@5mOkmDY$~^2CuX zX5PV=^^P-^9#|#uAoAecHM#RH##x>C8~^{;_1?F)x68k1c9*|mA=CFtfBzq&aQ6nr zhZ?JOT-q#N?60VD+FZ-Z%6gn5NeiU%!oJ$y9-Vca*SCNCWpneNfBt>$ukA+1OvCDr z8SgRG0gbL*>FMd29rfcPt4(BNWHzVbR<6xXr>*ve&gHzuZ%|NB(vzi{mz;8Y&F?wBKC|cR>+6q?uyd)Xs`74&(8*wwAamW}sj z1x!2`aoqd0v!3|bb@DV*^0rNiy*OZFW9)$6l|F}7@)&4NElcV`$T?}{nC8p{3W zfgzjDI`I|!ayAh&`K+(0 zscEfl!F>T1PNAgaWOlnxCzO>%do3meHw3))IIWx=k?^!QI438^Uq06Pz~d6Dx!PfC zESyrE-*#tj2nqOC;rPOabxL`hI zw`hINfyPyrJM8*TZ8YjTqxO5#=FOW$^yDWV$l7}A|EKBud5mw=Z|F&%5FBuH!Lp&WI&$zIjMYC!%2a>wP~DKIV>);d6i6t-o(alv2htQ*V>)@)!T`USE)w^k7ZS ze9hNel&|b#+m>^)=-8Y`Y+_}*ZA0ADy1Tl%PMkO)@s8c~TXal(XvB_!hvJhqZ$G^y zw!MXC0c-DrvlA~`awgj^{MMLxtVhze;EZwG;fEIcYkq!O+&kN5XS7M&NvlK5vMcPo zRp-QBwm9A|-)?tyz3Y|fr>!h)e}7o2Rvb9vOtVLisnN=Z=ANFO5;3V0Cae4JdaJbQ zai@P2sV?O`wpd|)NJg>S91tNZn~pzd*O<>mur*P>|$8! zT-tW;{t}z(vE{OTxonn&k62XVKOHm3*t_(YnI_MPgn;_fr@t?0X<-4)Tr67jsG_!Z zul~LySLJB;(0_|(&f8Y~{aw(>|BfXI8zXYI-z~enbxvX6wf6N=$_FMDFDMkB#2xxb zAmZ`+u>^5^E`5*wyWnR24<;Don=xu$h3SNTs}c*MbC6X~ov zB}3ih{pMp|uS^nu!FTr2J(UNwmzH>T_sQG;TRA;Xwn>3w=bulf-|v6arER4t!OfYl z;^AB88uk>Q$!Eo-nveI%-quu6Q8~qaT}Wh(b-CX5HEY&%6?eRl+;~f8$%T*KBzYA~ zKhG&~uohv-%9>?SnDkvUc-fuAjS(BL6^n=gXJ%7jgsx{sC<}-g_VBpER4>y=*-MMpz zcjnBQ7uE>+wXJnHc7yF}^_PFs*tEXD!=wX2)g!f8M_T zuPidzz=ml<@$+;1S=rgERn?XX?5$wh8p(8}@KDx#i5DkaQk*AT^|n3EBVoWW_W_%x zzP|mnAFe_x_I0NeACi=2S}FUXseM6dhW%>^@A-DM1!ca%H@4;8c65xLw(_~jjpe_^ zC;Xk6`aMK4Jjc+`Fw<^VTSMymdwXXJ<@#RH%RH0Uys2th*W6Q+;{yX5H}B9kNI4;J z;@RS)>}ek}J8NQWB16UIy>Dgkj(c=-#)8g-1qaIF9_;es%6!OBZu?)~^kr|$+kW-i zC$I5eeeCjBYQ3E4o7XottM7BjoM2o1O(rL1s_c2D(0xx%CY0>!;z{f8>wEXE`4hWN zYiny{SLwuX_p=+^SO4)->7MxO*RRiCzkZ!+b-Kyp!s_t#+Mtp6^ApV@g!MP32u(b* z$o!ncmj3ViC!9I5OYh~EgY5Em6`$6%*Z>vZhHb7hdNay)CD( zPr{HXh;Po6fTwm7StFj$ZT`p4?ygy8R@ULN$nd6#_oB124U>;CsQ zHa-r$WnXUD$<57OZ(I5*WZUJJTi)E-s$Kl!!$a|}udnCJJdXm^sS#_ht+0N-=X3u3 zy5Db`j@fm;yPh&NXldiT>&umwF-fMU_Ut*6tiSusrjnYvXNAwsNNz9CGum;PEl+*x z+M^3C-NFjYxz2lVTkpZ-}ACDCV9ot~iRb6p z?$?rP2CdWD(4TRur!00^t;f{D!)?5kTeof%E!!;{&tNoh$BrF#;c=Cz*GpetOJ#P` zTE1+5@Z0YdY<%3$%cD|$8Q$VJZnUejCo91I+JyS!vgJIU-rlnt{!afMxiP8rjLt9R_q$zrVXIAf<}aK;Uj ziHXVecX#h@h*Vf2`S#XUX^WH-6OPDQm(5{ontW%Q+}gF#ulHZN)u`}$hw~zhHCB!+ z-}2;?ew#D?Qa(|4WOHkG)7}N2Pn~$S=JiMRicK18pJ%@Az4rg*f_336wRO*5X!=p3CTw_)MAXz35{xMo=HOg!AS zoKx|9(LJ-Q2))={CI5GozGiE0Z-2Z-pQFh`P*~XbMXPw+j%Vkr-^ZvX-cVvvZ8i86 zG;^P*A=~?zA9z;Xs?mJx?B&H(ckP19rp=rCIe2Do33_(rgi_Q`r`^6ROtywAeNTO1 zy|AsTP(H|KYg^TewS|cjejjLLwzm28VsU>bZ}%8eJboL zmpwO<$4ypLa^=pSPoF+*fBg6{`>Q>=90z(Njn_qP&%2w{w|H`D%4EZo6MJ}$ToTVb zW3qy?`R2>73!U55d}oFOB_wQ%7{yqyWjs>MfO}AZ*cWwT)Rc*%`Ik)xB z3D$+oQ(LcZi+Ig@de*F2n*##_@Agcyovfz?)UquJt86^dft0quez}z@o;so zaX>fI9oM&E{d*d%))^{DH-|X5*VNdYdyqE)bnIN8lH|=hy%{nKGJD^;9zDmD+x_KE zTdi&Nw>P;(H=Fw|UAp8vNnb)}LHzW!&pl2nFP^N)8WCIda%skElM_3OpBJ5*YaPDS zVqM3Vva1{2Shw%Q&i?wF8yg?{9$dm! z5+J&oKb?_v=jT76C36>i&pgi`#b@GfrV?fZQHg?v^V$O zu@cFg4-XDTxbuEG%x^CfvZjBL71vWo2P4rEoxOTl%I9XynziY}hYx;SsZJsKgN}lell)?tpD#M$@TwEUa=IN>E)IeH1FVFP;O2EZN1{XljS0G z-8)Ub?#DyHxyiZM=AP1C!P$90MU-88%6%PG6_p3+yuK;V&&_R@ z4o|%;zjjWfNY5Ru`TA39qa#I@aQ@vc_w=Hl?#r5cg=hRH?P|IWS~S1k-Ni*^TR|v? zmY27;@#|NwvKW5e>PU>>uM&NjrfF|)-~BQAh{DR1D;H0mJo&N8FOjObSMNJKfADqr zmtCt8d4Bna{a=1bJu24N!T!gC=I@WEGuIprkFWi@@X(tJRZJOMS4foa?d>_t7?mF} zZR%9f9}DKJ?V2!Qf|<pa$TcMmbEpjk2cJ0EqJxrU}xI( zFPBtVc`GX`jp9`%tUSFYSK!U>v#yU;E8MD8iZGui$U^#qEDHrkQO6bq>26Z`fsch7-Za?6OYzU#*faI` zFZr2IEWdSWuX~ZQ{q{%iDmD!bjT1p%LO&cc%lvtX|HC$(XPzfdp3IEXsJ{>pA0NNB z>gy}vE~|c@yQ)n!k||%bCjPm;!KVIS&BVU{?jjqa)~;L6a$fkyhlLUhlJav-_%GA? z?YO`FPtF7x)k%Ujna|G5tX1=!RkHmxtFL`U3gh{m#m_fI-{ij1!7%6G+{Pn0DYv+? z5}!n?Xl)b@uzBGhvh#fE)jxb*X2-1b`?u!&=ue+l`RsR3Pfwtsq2WoP?L3YGEk}+Y zk5^kcL+Dn(qR8dH!$mlv_~s=x8qAYT(OCT1Y0=`vyMHhEd|zpq_b z$D94irQa#NIsX{$nhW_^E|n2nx^!v!S?#!lgv~c^6{M&ZeLidcy|=gbsQ3qqHF0}? z<@yy^>tC3AJ&x1;+Jf(j1`}i(mwvx+`%pvg>-GEPm<4%*qNAg?B_3|O++}33+V;Z} zb*uGT7ppfk-#b4=`0=AhAG%rCPM$h-isjYn75(aSayev}qxep+9nH#LQ}b&^+ms0t z8uVvM95F54`DlmZikhnZA<`igCs{5PDnvJA%&q*hDs*+xzhBq)UyE3KE#PDkD`*v92~x7b8&WQvH67mUDNfMQ}mT=A{(Nf-<+_d`S1LiPbW7f zCMF8s2q@P5%$1^wZFeHi|Iz0y!v=t{{De3$CMv;?|gV7%f40Q<%>O^o&PU<)P4EsWNGWN zob8sy&vZ6LtwTeOYg-$$J@4+mGbc}KE|QshOZD{4 zlZO@+W<@nrd9Zx#cz1h&-5ITmZ@D!4`nqJFT}L|_zP`S$pMFJnW zW9b|Dl@|{l-PHY|@2REBjO*bsF+IjRf+N<3XY*4>?|IYMzS7$;pXHN_GmnN~Zk& z_I6WzpaR!7ulOz2>Wlq8zHwR<5+4^=XJTfyt)KstoWYmZ*YlI6>&0Gs^6Z%yx0>mN zUq?b;wMTzjp8H1ZnAO5RzqC5uJP4SyQMR+`_H342=jYj4*FHHhv0rl-9XJ?eKE_*ZQyxs2{&}7YdFShN%oray$ zcBZ^$d|&@!;{^{H#`0<39h=!ocl`hNTb_@Tv-4V^x8U3-&z}AI&3&aq$x$@nS;?xa zi_gwBH~;fs(`miQrI&VXH2756#d(^+iG6kE`PX3`3yLrH`iflf#a+@ zrwj2ExeK_cPf+e>sJvi!_Lu)vJK2k@?{{82#j3qNQpzB~!OG71vqkBvD}o3Awm03m zYq9eFuc~ilx&iYQjNSuc%;p8w!NO59WlSwKQ#2}OVjLY zDcouHVv_eO^mQBiq_gc5}Vf35m0utKYG|-n=MhUD}KJ0dISRs0Y`Icto5mF>41$(Pg4&-=UU$n`UsxBGomAJ|%RCti0w^f^DX;f;&SHhHEu zZeneBJ=AvWO{{%%q|h_+-@YD?}gO`6R(A_xi)$BcW@Q6*8b(E zFRkI7YgcQ<|GG1O!|k_${$S>uwnaWqn;XA-ozMfl zSp}yin-@Gdz<1u}^O?sB7cS(CV6Q#BE%)|<^(#7l*#z!C&N_ed_X9H-RSP7~-SzyW zej(wUu*X);S)yHjU)h3W)8i)>gO)L_e|vv_{o0e;MI9#`U^Tko;H3Qed&hJ`#R&~| zVOphcZ;9I9%vJ&|x6v}lie^~6ZEDc2ihP;vDVvyIUod=^YyEipnj==z%i<@eR#a46 zxwO>#zfIDSj>>fp%IsG#Ii5T@S-qU$lW}&eo2KZ~&jOO)rd)n`d3pWC!e{}`OP4OK zld>+$*>rC~!GiZ9k{cDCD~7Oz$?IPBKlNogZ|1c%k+sMB<^PM@SEx81KGrL3f5Fmv zOV7vN4|Ae)8&*B^KP5PE>eN=_9mX8|{M$FC9Gq#K-uJEf(NEpV%F1Za$?nb#bA7k< ze%g0$p-QmE4e4gydASZxG}^!1XcLi^?l#^btzv8(d@Rs~#n0XL)AsvHiRlZ!pS%^h z;7{9>IdgjSk4r6Cvc#qInUhk%BISSG?-aB$d`_M?@#5{_PwXnGsj2PKPYo6c)Hs;j zZZTdqcjZ@pq~F!qntNM}`Ht_C-|zP?XTDR} zRy@z}p0wApHA<(r&88deDrUOjdP+^?!@W1YS$T?|k$B_4@tqOdpyl^2F#b`}6aB{XF(B?W^4MjeF8JF@9V2 zb^T;@|96(v-`=#Z3Cr$~x0$ryz}Hal>14CdB}B&A1s0Z78a9L-o9Sp#t6b8j_S-D) zPQ^9QczN#aZM;jLzO}3UwZ-4td-nnUuIJM&=K39fbhLZARLw(|&~FM>seK77Tt6PL zHh*9;owYP@v72gZOUscTKXx(oowxh_Mqzn=`z=PsZILWV(T#7CZLGQH-q!*3VFG_L zPZL)6dolASchvN$g&$beCuc5P-!$ReTr)dfA80@URjsYBE~p@-5K@Xs*$%R^~}9H**yQ=o}cS}zhv*< zdUZqE*;%ra9Cs?E{9U=`v*RX}5}t^AFE1}I-yXHLtn-r+LuF-UWY@1rfqz#vPMe_b zJL%wp(kTb`xa8*MzWny~_UF zC0m?cSgNbsuih44|M%-&zV^p^4&^$E6r7d&cq6$#met~)0{g<8M_)JYv5P1b;|#Os zko;})Q&`>a&Ax<#P3~{Xs&~lSbS-%Jdj0-5tqVKnUI|K7`|i+ioxS?o)$7;ims`d~ zEvTvU&B@7;j{3Im?Y7%`&U-nZ{rLF!e$Ds0BRnHp{X%650pma@?BLBX`hgWv7tMvyIcw zG0fe(!g(W;(SFgNS1Sy8_xuRUmhw)S zK7IO+U-p75f$G}YuciB(XEJoHDvLfj!FjJ>-wbAb1&7_dZ=XvY&D-X6}ERFf9p z)z|T?rM30u)=yN@cg^QzzNe7Z^4mZ7L^Vp*qKp2WO5eQ6b) zsX@WPh6nB{96fq8(xG_!_UJk@@S(%S^HEqtA|hVz=>IzWE>M z)W}V+QPqUl6Cf0fA8bx=Jx(@@hgkY{hH6d%a+__JC*#f zx6s}7{KT!UhNTDg?Aa6Vqg8yN@S>}@@9vMsq^r4_4t`a+sPkb_sqN-Xr~RfJ*ljkE z_vGdh-;RREz2j5t+Z-%UvmXAu(W7A9=lR-u-IcY`=2vF&e7gVtU-*|+ zg-7biv&{uwelRkU_`c)a-QCCAT3a8UIl*R8`)kX*^%0U+XI7lJmO1l(PMJ??YHHt; zpMsMMY6B~;t%>~ZJKJonxx(rbKejD6`TonViy=t@5&uu}7S(?^$nJiAw)y*u6DCaX zu&-coJbiJo``>S@SuXYxD;}rbaeUX~;IrIKp?Ljn?R~A6Ib>!nF#W!5YoTWID&-Ta ze)^xjwA8!)?X9h^Uq3$9yW92Cf1Ztrhue7jZNJ_4=5P1&$@kpb+guez9sf-E_3Kv` zXzOIQ@3bDHQ_a`;mG(JaJLr|)4C;nuh!{_tGiOfnoQ=`PB$oZ{4HJ7P$-d{JUTDw0 zfcb}RZDYC}cC2c$Nx_2yE!FS$et!m9%>S(QwJXnulj`%=T(|fW!MITCV7kj@-i3d8 zL&76}E3is<`JS3RbLP+eg34|@7Yn-uICIRRZ!q)Q6g&sr)Mmc?a^&TT`;m=~I)|%m z7ijOCKY!}yKb6m)zDy37?`mxu$rpZmOXlTYpu4XadGfy7&aeOX(|Xdu%_aMfRVTKC zb|$^~e(a3kx?-d;)iuTNp&EKORwVJ)Ko8Nj$cxvy|>psV9Dn2~8 zw`I$g*FJj10S{&^N%{BZXL;VOEidOaH8pKImUH6W<2H5wOJ*gv_*i;5HzhZmU|Scv zTzz}u%{{J--yam6oTOTO=&ZN76R8{`=TpU!$R-@}zUA_w;wy-(322>&4R6O=q)e3;hnT zvtJdQmpk#>;m#*o^J+vtf!5DI+gJPB@6)5VE-IO>`T@#ZEK@I^ewnk=YFp-IHBkQ} zQ_;`bA)7r{D$MTg#O|g$kxVLr938$*x9>6^752B;sHvo+^lH;-z1b@(q?cG0J@I&! zy?*bu#OLScR<3*7Z}%(X@SmTb>tC<^?<{ac;{V_G_5TY$Jvr%hAmFg`*B@$IQug1Q zweXm-D7&_{zt#TY=jUGjICJLl^!PeUxBiPVt$j-mSg!myuas}%!vj1E>Ku;l(Dzz1 z^KB;U)k$;a$ZXHJxJVN;<(j@V%6Y@SU*e#K_O-Rqn@q8}zBT9O zrniUN`TtM8SSZxk+1dH>n+bDO4~H}RW3ZQ8ut`}_7z z@4mU~#qI6+#>aWKm70~7mWuM*|M_sd$?WBlX`6DVcx*YH#?LliR6Ocm`qS4JdEbC0 zVm{uJPCh@+wp}VbG^Jo-jY^Hizdv_;Hf?rzo$%_?Qtn%kJVoE$MDA^roxpQAD!u9a zx$F;LGBge}-V@t;xUDubGZVD2Gb&2UdYMBN|9i#?Z~r2ueX-N{XK2LCK5qBrob~%R zlID3ad$XT;?``EtJ9PN)mt5U7q?)-Dim#S{Kne%XS`uREyEv;8Sug6vArYD=-y>|DX z`OMCylqdSSMt{4#8mfhFuFAi^j}ugS#^2hOdt3hZ(rL@LZQFJ={P~ZFa05gBh-n6r`RMYPF)c*eVcAjbWHG9zU7d0>V^2$Lg z)ObvO?-yV(Y(L!2zrNKd@-{t6e^UrpRIcl#7yoz6V-*JB06w7m)YyNyZe%yDy-CswLp?BUbIP|dK3aC+# zuXRpH_wsbXzJ=3v-hXQH;oViE(-+pr%(1!mepXCyWMpLHMvK+nqaoTKl?X zvGrZGh2K1xw?)fuKYNWS*SX=7;_jvZ^}R2b&E6Hg_L|wgopTNxd022oKYH7njwN%> zN^>vxb~rn@vE$yaI}-}Zd>^Nzr#sJocKy(4b)m2G?BtF!PM)|y(AhD>W$D?AnwpxN zw--42&9!pf?x-XYp7M$Fg4o`xY@w$VQ?-jut_3aM^VLbdaB*Gi?#BGRU$4nCM)%46 z?w{v!`$^{Sy)Vr7Ja*YubI;Vw6SQ1>yLA4Zk8&p&a$KC_;_jvO`Cr!1`YZCJKK1SM zzFhM=1*s z-yL*_eR}i$i~qSn=gyotlQZ{!=ZuEaV&4 zd0)O<_Rl|e@}%Y*PNzTBG*>Zj!2-uAZr>gw?HcQ-aF_lwo#?|eF~(YET# zi?!eXa5Po)KYH}2c60jqI3|mQTR+UobDw@vC&3!(T>Gs{nn?(iIZ9VV&yH)I7gujKi@`-PvU(D9; z6I}2vvhgjO`=LKSKePA9T9?%%B|SPS*3BCK=SyOsu{Bo1{fAH1elzmnQe_ts%F-g_C?s-+>aqjIr6Ayf4da=xRwx97UFWaln z7K+EEq$xj?d>F+P7JtGz?1@dLTC<3V$egc1HzzVI2W^FzSN$$hHDmH7#`h(^RVE!P zRJryvZYu*=uM7___=oP+!L?Xy9CVVEa=I;cId~3 z4IB7CBfzP*x8<^{zBbC1XMCmg?ejm@)9*Yb-u9K+=tm|eB^AUNf-c`q-M5HgtMeoG z+zCtBSKeAX>%`1xKxOX{Yse=e)bL)Ehj4`F_`~ zU0J>7d4Dx+3f{c#yZ0BHcp2?FhZoa}YXw1Vq5sZBMV~a}?^wBj>Ot1C-f0Kf*mnGu zP&g55osrF+UFFG;*uT_Zw{_K`pSIdN?jC%xW9$Csyh7*r?S3eHKCQoh&-01yayzw5 zO}CaiIW=8a5FQg#vunqW9TvvMpKrABO84d8-)AeoYukhg6E)9>n*-@H9UUrf@RB|{o&T% z^WdLS@3#7dyxCtv>R&O;Tki8;Gdo~%Sp?`_A)Aj!gv*Z}JNAhCkS=I|zlrW&aG-|KcVX?X;@vrg zJ~N*7f`(IHU*r861WNEHSd@egXdXGG9#W^EcqyARbUSCj{h4nQ54CVgL}X9cwQJX@ z{Cz*w!X4X{Cw-m6@k#K#^LAB@`2O6Lim|m{ug1P9+x@O{{sgu;r>E;r=VVfuW9G*4 zn|HhYsr9c@u4rGY-*ieZTRSV_|BJ=_+v2NUF5NDvTfbrQ#EFJakBZ0JfR;|xEIPT( zpy~GOl}lR}YM)@)DeNGgaMkt1J71Psf%ASWH+yHX?%bYzUGG<~`MnAgC8b51Ik#KA zSg~e}&A)5W`LdTXcbhuLi@fyN^wT%!`MJ4)$4uH*{|~!3bHPSmmH1!UI(uWc*$K`3 zWtDPb0-vIy;zrP>?#D+ug*WL-2z8cT`?37!)zreI>$Mu)0f+jMO2c1+m*4@`xzi82-Pw#fWFFV`v zRrIlNc9`4q_mUc*10n2+pPjjPxQ+Lqd<2(Q1Sn-BUeUGJ%1_cLS)JRjhdGqFR&86L} zYiDfa`MBkCiwDQ2s@v0h7EXU!vE=jAo>;w`Dc9CSzC7H%!D^{$tI^DlT zz;F5GkH@6*YgQ(^g-S+F*N|}RNaMLAceuFd)x?w1>t46Z*PYPT)%|N~X7-IqI8XV| z!K|&fK+6KceSFTm?zj1LqN*)ec$VWKErWG}j`LR@U%+Oi^M2Rsb>5)qQE91bQPbYu z-oF3suQ_`kLy%|FJdcsEIh)0he1?Zce`1ttvLI;;a7d1JP?dKjpw0XVgR@qQ%?(FC# zkImj3zG>PU92QpQ5!A0TsY-?MmF1;RD{h!@Zn&3H{o=wxi{cfI=jK|MZ(6csNl^Rz zpetsa6VlbbSStQ)PygMhCXqeCv;6I?(rtV8{3$3YDVZgHNc8JDm+$QCJh;zC-MwG#?=0ot8|PW8 zD6crdr&G`QT4LJ#R_*9r*OQZz1JjKTfEF(KO5NV_BP++4dslyJpWw-)HMP^U*X>|Z zFEc!`ulBc*bhvBPgdEw{Paz9aHN*Z2zD!N9*4ELn>0o^K&oLw<HUA6>d(J&?V6MD9LH^uadS)7Z~mnecHjAQxQ}J_yE{9X*X{rJYj$!{(gwbt`s=2j z`SXu8%SGN%^ufE5U+%WIcN8uzzOo|F$T7}D=W&Ph+K)$0T1>lM@bdoyUXDHYJs)!= zfu{YArKP0Ac(8BzeY)65pv5;d^yiXS4Y_ygdX?Dv1@;?R9915mi#|Y&bs{FnTb3(+XHgjMDOTjFZpnc`HAHAiq{J+ z8`{{|)V#W~()QQ)_w(mXnsiBDPjB7&yLtTO^BGTV9iEZ*R}nFZp-h_2tIp3XZ3~->-kqm1Md8b2rx}K9OCu zO<$#cX9##ayZ)@RBjnBQCY>r3X;nR^!$HWVCVRMV?gwX^=b;FiLA+pc3$Og-8qbX8SGwT$8-bRp8FLTC**=cmM8Lu>PlKKX;d%#8ghf8IJFo+4=oyzFc${cXf9! zKV0V{zpnAm+U@s#U2Dj_$)OZEX)C|gHcu(5l8i}r^ZJigEu7!f)Z`#_J0xJ@!IZ!C zXLik=H0hG?>lJk|!d|tY@hz5bx{=kor{j2UZg4FP(YDX5%~y(+$$yhODaQD^y1IJ* z^Lq+Mrr-GW^>z8-RiUfha_;Tns@(q;(zeP!H>Ih&11#n z)n&3-TUss#R6b8!YrOvTzTfYp?dtyg=xll{!1B@g)TvWNX=i49tcZ+^T*{X!S|oAd z_=aCLZ}S(_ZIsMoXJ^~xeYp7Dg`b<+48^~#Sg}GbPOtjH10%aNk(-|_aXZ+QuvJrC zG(_vikqJzW?9W${!ydZjaKLW)`QRe`rDc{<_I_ zf4^M5vj6Yf{Pi+66&ofzyfB5gK)vagTmQmar7s!2AD63svnug$+t~w=x1;5*|M&yi z0&*#%Woh%R)U=}KUqzMf&b@GZtnL>(PY^7q;n!_$`_GaMsN4Gt=kY zTqSWPFUop}t6L}6SzjykrS{b~2nf5C+j@H-Nb+50jwa#3y)?t^r;FIlX6=h;8 z&RzaJ)9?C=7a4P6FDpbwM$Qzfl1`Lywzw@|!L~)lrdlm=i~Gv5851TnIGB$*?QyKAs5lfF9$WhR z!a`^D+?$)8ZU$XC6#bYb#K>s6$Gf1;+>a-s#JW|#fc9vV{P^(j`mrNN9-Lmbwq?;G zrHOT(7Rg48zr>IDA7Ci|-((&j{nu6{MZ!@f2zP@NJbyprq^6ch|SLC3P?3-}K>^GEd2KrQ|zGSAwoRyfpz-s;8{JD)s7c zHdn^0SFg_f|2+Tyo^weXO|CpYwlsM6+jGUa(QB`%9nZYRnSYnRrK0<89{EZ-4S*X5s{q#KOXmQ<~g%w)$xFwD<7UMIcE6c z|BEYcq|VK;3@&_jX6AoYHMO|31wI{0Z*OluU-9*7`27FhZs*^x`}K0UIb)mNyyVFo zucqcaU@_62zVu^p$Nv7lKEIC_a~)Y^EQ?aUrq8b}b1_^Io7(o`8e7WzhIOn(ekPIE zu3QmGXs}i~4eAW{Ez{j}Ye}&CZ50`1i@p38`B(o>=$-w=Y{suIFPr@enx{;kEB9>bmsTiz=pR^Xqx_j|kyOzi273O3M-0YB9r!O^a%9J zH^1iMdi415@4f$iy*|J9_q*NtI?>y5_AkrboR*uedQ*RiP^Tf|tMw7JQ+yhBxAJ9N zSNVO}-@f+j(&=$kn~x?P{2|bsb8}Ow>QvR_c(I#HzB*i2oW|qTV0Bez>5?T&q;>o+ ze16<-FE{1Q^UnFbxoL+dAAC`@YSI1OabJ!m1T%|>i8*}|pSY!V<&84i3x-nbau0e7 z<<0b3_SR!*=<2Xk{g@pa&g5-B&XF(js5mBmnxuW*pF@Vp$IjRkJUH;H{9fhr-=I=iqODmn&#Zh0R<67=GeOb$f<~GW=%`0#c0QS>?5>Ui>pwqim)A2^+;KO@+3@tl zmu;y9k1e&=@~}rv-ctAX*ZO7t^ZOXz*&Q=g1}*FSZ85iSL{LE>$ThCEb{K`I9XU&SX)ydE#MjBcpcOM4r-lwJMwSis=#?3*NoRIKI~9n zYF_A5H#s#lG}O_@hlg#Yl2cb#S6$Sml#_Btr~AAxTlJjPN^M2+zxow*hk5=6YwZtS z?subH;>nknm-pP{2$gYPlRJSS!@laZ<=*?7ZEJoMtU1rNa@{(+;ujYdCbjP3I+R%xBT;;7SUQ~@rHMYH}SvR$bE5B zhOlJG`+IxugZ8^J-|Lj z(3Dli($ccsvR3rr6Tt`Odwyv|htKV)Vt#u1Mf!p_p%D=~*wv?e%0G1I&@Fx`lZ;mj z9GmNcgMx}aJUH0=KYDxK-Se-ntvxN+vsPWZUsuTseZ%jWw@BRDT z@ArjHojO%?R!(63p;MQo?rmvmGV{M8zOkX_M2mV3OZI}@E((oh^It@6;1NCZ{N7S; zag#1Zjzv93-n{iw(C|HaQbmT{f^p7-!m}ai&-VZS``&i4n(wK;o*o~w=jW8Pwq##l zcmHnr{l8znyuAGX`$c#8wSWG6KEGL_)c?teC9fs@!?U05>FMn3-0^SfW#`B>u6&9)5Dw;mVI zIZ14iukD#4Yj0_}bGzqsfh|*}OnEW$ruVjgO8;0*>IAM;C>k&O^8UWO`ey@=%F3PE zJNEi5|LYdI{)6h4{Z1x%m$oTJnV0PO@u+*Ydyhn+be-bW7#*>0Q)T7kQ?IVB){om& z^76HO?Uz8ctJf;^56=>P!1wb}Ra#IxxwAl_3H!;W~K#_w>=tqbE__DH(&g1dwY6J zk*B}>6zc^yw`PZD|2fsrbJkHM+jNb*ZB>ar+m7v0I)#OWd-Zod5eolxcklOm(NQ5G zB`?0byzIVt^XAR^&Oi*;b_4WPz{mEWlUe`el ztlaDCV$ZKwvEtmmy(&kdE#{uP)o=I9<2?73mZ;_ZicK{~+!GqAxfKi#OEU7@yj5%S zw541|d;Y)piHi=~{C{$Ca;%$e_vM#ctYrGwT6Zm2upsU9G~MN~zm1KI*t}!Rn-uCC z($b#wJ)d7+cXpw3JD17kZO)BnzHF;@;|`qXn!IDH**y2%>*k)FcFc2XyP1seu`=VB zzlP_QTxYp=!-3_ztck;x{QGjuoB}L?M>S{co%=K<$n=!pS+nfB!YW#p-W$KkeA&3D zsBrE~PzT1evuZ}4m2=MFL`&TlH;7u+jt%nQKn!+U~Ss@vExy6z|Vp z@U$qb{zUY1s~=w8-rQ&Pjf65lN-u+a7IFNv+M)%bhYSmv7o5{=jus_o%|ooja?TtQVA?aP)d% zBO`p~=#uqK8@?5T&dma?1Niy#=OV+_rQbh%;+%T^hO;F&oReKR@1sc9uM4=abP$u;JEr_J43wnbSZoPMaG`!1%OJRxLLD>O+y<+s*E9Zz}Y zWzu;&6j|e!7`oNf*?I3%vC6IeXSVI*gP*<%vK@AAUh1!IZPk9|$W~w>V;W!c(REv6 zUH9_upFgE8tWRHXl$?r#{udlt8vnp9}?vbLdr-z3}PEyjNqw7ox9yl0fURu)j z=hv@auWsGC_375FTVCO&>({Sezv*U<;JPYdAt50_At9mU%{L2bxC^A>jaE;UDDF&% z*tQWTY1??|;s^bMpe; z<1HKSvTRpQIo!s3@P^|_DY1@2H!i3gJ#wU_y}iBs@IQG+0a3qsHb38jsw)pg24B^H zDXhlxFPCpTCT%X3u;;O*!OQcq=AQNcf8W2pK4#}9{<7V+LDzX*eUz=Nc3ppQadCC8 zlxbF8VBp0@W%s@}_ckUUZv;)Ib)RLcDEp=a?r{aD|5HkMBpb?d#F4(i?8+3oAU zwzB2%ih9tdhfmk{|NCm+bf9eS=53$nFebDXoZO-t5kF^^T|mlj0ZDaMkM7=Vh!) zRzw(tNfs)wdPy4{K6l7h?|4p1N{ZIC%w*_#Ca-$^OS-N6tn+8zSNODA&~2Xj(W6Jd z9!byaNj~0pa$m}!OWg|$ISy?;f8=@E6}=;yg)i<}^mz69eNm?6?_zigzROL!a^=eD z_}Z^mAI_XPGkbpZyPZ>y_sMEA#x;ai2F}gcw)6k_h)*?n7cN;PAM4o{yv%2&oV@(~ z=?``!Teb7c#|1}3L}Uh(1+10ViLeq5sRy;3T*YIraKG+0?31-#_Ni4ts$Syul8GIa zNhgC@R!k14+x%o!0PpLSb*E09ih85XY&Bn6H~(V}pV6ji#eaW&Re!(p`8?x=3m3|M z5#jh~za}g@A|qpkcH{Ac{enN9Jbiky=OD`q=ZK#zr_wJS`nO{NQ+mO7`HExt3mtux zA{%Y2KKA8Lnx-GW4>Y12w0V=dn%X1jM;w>93btOY|NA1%F4>tep>KX*8gQ;!1Ln&_hRMV=IhQ@~_+f`|&vc^_;!3Tpr3Yho=k9>9Gs4W^-&a;Wj9{ zbp0A=y<&Lfn;RSRtG>OdTzv48l3L=%h@7{#w(dSKwT(y8NlYgqp#I&Ros*wFeX8Ew z)pc%1+DtudZSD1-{r z`Y}A#eRRC$@7L@4PoF)TX2!NTJ3VXq%&oB^H}oDeWb1BP-4iIVYnoc^k4|CrbF<=Y zR3=!Iy}5CHwpp&#%Fn8nGtEvi<#ao)NC+A<969P_GdqPoIbzyTXJJ=)X{ayx2_#pt!*PYSvx!{ zc)DI}L0V38&b>X7Rh8b|@x8fmP2uvK0ql`KeLuz2#)gE5d@E$LD1O#+CY&whn5ks+ zdUoNQ158)s>J$85wPQyCL!Lv9+lUT(w(aS?i=V&IS|a&i(uK^LhLK&5zc+)jIgr zB_SufZT`uPHZx<%5R{y}+B9ey%878M! zPQK(BtEs7}d6#>-aLAKq&*ZXVrj}NHz7|=#N6bHt;YP-p4Vz6?-j|e=%t$x%aCJTU z5p;@=>+|I+-mS875qz)sx;;6U`}p+i^?SvRi!@)kdUa~<%}uKD8~0w^B<`>G_+~>5 zt2h6ojl7M=-`v=k{Pf1gWWxh@*(26on=sF|I&N>q#YL;dbs{$We?Gtd-iJ@8_4Bp0 zw5*l~PZz)HYHPH<_THXKaUNN#5|{0HciSF?%kMq^@$vEfb~Qf=G%wVBW!d#P$6n{f z#_p~2HyqRRll{DAciORQh0zn-Vv=t!^PT;PsV3*K<8^Bd`AhvRnYXrN-u>trpDbfv zXXCwmowLPPA*YCN&H1y1SIEDsSy33n4eco1i+}!#v8(UlT@KEdgz+>ux6Lw!qc$)G=xbb0xepIux?X`#%Hj>HG<=fWD zT9-X}{^ZFTZ$Ce|e5ad&EQSuC9WuA`_t$b71Y4U_TR-{H@=Ir$aCboEKb9vg39{}I z*WI5Ret&y=`}z~6ueGeU2#B%s$wcg||6ezA`jZ6=EHe{Cr8ek3dh_;_17CjFzBO^L z4vERi&VBZD6T8j+KcBcwF0b=ZYJ8z#bn+b2+UAws)Ab}`vL|d#KR@g1%dC4=kK`R3 z!`9@kESoWJUSIA71yxnmk^<4d2?t%wWaGCv-_l#+n$5qcdDESu<=4(X5;?gJ)XgsT}q@+g;7cN||(2p@Xbl_#lwY^_1dCSkvn5`CKC-~&z zi6c`yet%9%N;;&hq_j)(o|ua>LvL|KIP+RgWiB$ust; zAFx}}dVrx^eu451S)pflZhzrE{=Ai|fnH znmTps+dk`e5n+LWfd>utZMya{(tS&#hLXGb!oH_xlMY$Uu}TLz}o}8y93d#vX9h+5JGPL!?z+p{r>ZQ;>^-$V(k*}@=wp!8gD;mJOBC2=jZkqo6ns2dt33j@^cSrmpa`z&Yf1|`VMpk z-K)sWX@6N;<}y667IDpt*je=Sx2ULSSkr>JYI)C73&lmR2K`>kAbe>P(+-pN#o6=E zrfFy1-SzcpoK?7j!-m^$r%qP$jnbdEFg8uEagW7%4zq&&>T6@B3q-`q)qFT8r>LkX z7`I35@a+Jr?yJ|IQvPt#p*dBeyYPx&H1qMgnCLD#pgfksIREzkXj-=X>3* z;={?kFKSM(2%cJUOw8k1;+#K!YA&`fo8V%(Xp3w97ROof9V$8Mx)pbtEvAQxEY(w0 zO+5`-J36O*9dp8heSsUD+xf1qJiT`Ny{?emTb3+Qu{1X~w^dbD{rlixv!$k{rmBeb zwBOG+e!Va|f8Wl-U$4jC-|9c#&USa%+bYnyjP*TTU9a9WdNZ?l`1{|l`t;T+bw4>hU~xD$jtt!^jffshVonIDOTFeWpPjcL@%)v{bwIptesoGR--E@ zJ|v`MPw;ZT?b0^k3=Dr7y1KY78qRq7`(R=Ce6}5&0y}>5MHX7;CZ@(L*ImfORQ|~D zdTjaKryoClG_U#@$H?$N{maYC&--Va+tH{r z`DE;CAGelL7c2Rz4(62)I-mFm@V-;Xnfv4J>h=3})w;O2RO`p`GcYjvg@s)!2W{sy zYzmk%l~a;mWP{NYg+mf9CIOj$?Z2IJk$GF5v8e3UB?YCT@Xb4(KYdzze{1&j>LvLG z3=DY>ugBN_%{tm8nr?CPmMiPFMW0h9UDg#)U{&!rKH=I&qwCAhrfIV@S~nDbdUA5B zs;X+(n!O+IF*#SyW?I=hX@S))$0IAGB?H-3t~pwC+1LE*=SM$hPg=EV)tO(vs%EEb z=rC1xpYq8e-(01hGk>niw*}Xp$L+1!+S}8^QaIbO?$5{LCQg4-uExgIE}P1@&)Fr% zRqOFDx8&sH-xVMBG&fzj5@K^&H~-CopG5-8&%7=8<8J+)p>zia=TB0j$ zmQ$gkqqC;he4Y4$#c7xN`}_BY#-#$A{(YftQt06N3gW#1l?*IRN-!JaftzueijHukmv$Rx{CxGkS;Z^=&wlj%g43+}%S^lWJdj_tZk=9VZ*S@QRtCo2vuWB=_H}>E*`D#+ z6k6+FxXu&5&9k2EwYE!3yUIiTmlC(WOHF@wclY}|U;kJ8Mt=-rt+6Z7DCj@_9$FT*p_-3j(sz8OMv0i?-jdtFFn& z$k6#S?bwYinZa61bvtjktjW!-JneM;PhM2LK;HL=4Mw*X^_H6iY?Gee)z!7?@^b(6 z?Kx+pGR@ar6A|87aI} z4(2Fjzpr0>?~2{ezu)hJSE&@M+L&-KH2Qmab(zZ>6;C}p<4LT8$U{e#rae(9JiPNj z<-NGrrpeP(y{GNDveaAr#63O+2jwi#P6YWg74P?cf4BIH?G`(>_stQve>7YExc`Vv zj^mig*&Tdh6;4cVt#W6Z#67${?cUz%@1>u2GBYFu?fd<1cjeEYKc}`hG_DAkGA%Ro zNaUR}Or@WjeQlPT`Da{zRJH&Azu$AX#r1N^A6qgpC^Vc+)0VTZ`}6yTv|7s1w-vY3 zEgwxxyX~`KUAw42VePc()8juKsAOb3v(ULcOrtC5vSQZp+lp&rl`dAYByh|#m?8eh zZOPYfZ*MopW=&YLW=+Vsh*piaY=Zlot=dZV?0)`qo{P(szLFo2QCHsYtNra%Yvz%f znmX&q;fqzTEFaiUxV5ZxALByB1u<*|J)Eey&>`NFP6fX`?^b(E?sK)N@4XprBh}_?sHRa z{N3~UQ7m`vy{kJkx`O6Uo?Lw4#ZP922i5z2K9j!WoYrwsT;b5hj*H71*B|F+Xx!bN z&~ZD{-{4fo?MkKcX*1?d-?{hbrFF5pXQ+Bl`?ENFecXpGF$RWNf?{G~pd+B|_LjcB z7Ww^ddA$C?e;&*9l{%kh9;#fdRGze7_veO5@k#}}N{7m3F8x?N*Q&JX`cX!P1FGuk z>E}V0kjzk$O67F>n6MyY!hs2jrvz_Fa3(!)^m%1e{OpYAtJj6SG3*Qsk2Jb2fwuZZ zzI^#IwdqkA`@;8!T)DUWJN-3ZlkZR3kA*+{mz)>n>ytue{KKnWQ z6&kv_w%Ql>c|7hoe|yrBcn-sL{VDuG!g3bkrrp(1QMbbQ^ORB z;R|Vx$U6CJe`Y*Uzv#57RUq-sq3@IXHpbL`z4}&JNvUUVk-`bX!#AF#m2lt8EZtTA z|6c;Tw|;GSGTcX7{p{k5zxkV!`DH9N9PfS3!_dILV(r?!GbT<9 z)O)esrKeWubpt*%>n4j$bXcAdpW3D{{X}K=m(#j36OJA|+L~u!e?SG)ftLz1Mtla65LsN&fuj z%??V8%}bUon`Bho$zm^J2ldoUDetmlNhgl2?FF;48TP7dt`788o*}Xd7W%Y}W8b>;n z)$G1$lyX8qfKOV*-28e^vb10hpIY+j$){Z2Dtul0DZKLAsWZO0xw#9af|IVes_*%w zvFV+r%d-%!h5pOF-SwWX_t&>}_XPIyk_-&|!C_%+{4-fO6$-vHh&-3_y|Yd?x%2i% z|C?{#z7kRkGAnp+;2gJ@PDS0@TU-DCd@TR}$C_CSZyw&x!@w}P;O*AycDoV}w`CR< z78>@{bzTua$#p-I`(MC>XqR_sCm!`3MCFxxr~k-=Zy?)_1miPr0+BaCTO9w*JnCHH-`i z6DCjI{JYou-VK+RvnwKRXDYGA88CK!iD#U~8KUKJ{DpI%n;Tn70lU=76)QY`K4|9W z^GvxIZ1Fb07@?_*wn2$ky)s2eaH;U%p*3oiQ!G z{;%oU%+R!$i5r#w2wtpIw+ml-F1x#{YmIxqTue0mTmnUYgT|CrIv6oWMXUa- zajm(g^mk^*2HC)4<~6r3tq#}UFq5a~)s>Z3mR(Jdx}CD{=5HrZQM+)S)Pgl@e7@<1 zooF}XJH6at^L3BUno;(5-t2htrarAXJU1^&+QE8VX0v8*@`72upC24-?tZ`T_q**W@2nXZ z9<5lk543K=PBnUW&VJXTO{ZCeW-a{Y%xG{u>1*$;WRFJfw`H5pPgeIAJ7gmqaC1xM zWut?Z%(?##Wc8m2{@jtkGTVIT;eSh}np>;c*~z^LYJT$k`SQS9Ej|e{hPQ+!GDtp; zc(L~RlE1yD<^TUUzAs>Y@4|h%tZZ%f`Yc-ainBv0=x^KobB`b7c7^$MUoAX7CGl|E z%17VAK19EA7wu6~Q(Gpzp*5n|By8vH+X@HuU99|d#ZGNC%lKw_-Pz=|?jOfudYn+}*zyy*|pw&@gH8LlYrp1(>lHPU*FkTEH82U z%VLc#WkbV{HqXt=j^A+mb^qONfxjExNZzu_wSOt${r}(J$5C6euFAbyS@-GFr*md& zyUrbqc)h6f){5Bm38ja3`f4pdyYa)1sP5Erb1d)Ie!UugzO|)g%cbf?3=BW`I@`~D zxiL2+!RM3RgoAf`4dzJt6{t&{-;j`+liqpNb<_6U+d#LjSm)o{vpntmyuBKViVxTO zfy(NJxGQx}9=6N>`~G}>{XeB^?yZH_B5&TUym>qG=AFoM(nmN8SZtV_m}=y8Uo>lc z=hx4VS-0a6SH&zj1_muwR#tX3pBWpbDhs8u@`^Y;Xiy6%I3#*;$4{e7H=}nc%B;2? z*Y7;4^OW4Czf?*fqTDPz<$Z zpwX;O!Rfcs)U6SDJ07;##o5#cCvA-~7I9rEyrDCNg+nK?$^7A5_V-bqoiZ8!6rU&E z-;{cqNk3Mefra<#)vJ-o$NOTBe*A5*VVhUh{!QD|R{mdbRy6bX#3w(*E-mwwmblNe zpf8uN{ji$l@hisLetx!@k|uodmFl7^xk-EXe!o{OY*TX2@$k#b%L{kQNv>JE#VFR} z$CZ@xbFbK&M}PYKS-ZHn`1{V+Q495{h26 z$4*D<$Xb`N*mpY?6&F{2of+2N%i{G=QN#Rk+xDgh$GLQ4cbB!c`|t#Xgs6P_RdGZ> z>cTOJ`kZ8?$ExqdgEs8cKCgdeL&n8LlUrL`+iPmpG9*ZaEWC1RsV0^s%8~&NB zyb1;7!Xgiqy-iLOE!k+aeR9h?oy2O1%-zoId>iBIem=E-zvFS=b%kVW28MY{f|vW5 z>ql?P`E{U?`LX0*QQghES8SN8R+KJz)Gy=E*94(0m)~_3OmN=~IziOM)%EL_OJ5ln z1RQR>{dOvHbDHnkRjXDhrMOHJ?Cg?Q8a3nCa=C+y53NlWhadUr#+7E&JmcKc7Z(?c z`_HraIrYs?W(Ehh(qH+vx8**&9$!EAnBcZmm5md}bzV0Cn1*6f{cHhb-f=I5M0 zEG;c9fA9PGZ1(3#Oyv{XpH7%}K!!uS^sumoQWNia;r+X(RlnaW-t>TN)tBru(W&O&^$VkTY7?Yr1Y}?eA|N zCUiO)85w2p&r~?IW1(wU#)DhWSMHp(Al_qlzp2@&A8oU4&Mkg+=4b!mhbulFF=cYt zFmcYDJ!*!AhN0~n=Bh7pnX`|#eYy3)(-Fm0ByOa)&)FA%kl2=za?-?b4xwWI! z?Ok8^7}Vim<>KaksJK~yiRE3MgVP;7qlvZhx=XaqIAhx~_>==YA{T_kD6scvm-z;#rROj@m9e`IXKY zofh`DDV%yViSz4&pUeyb4H{iZ{ysi)z8&Vbe`08)B)oY_u~EcHtA~41XH1%tzv!IV z#D-#))jSrJg#}zb(QMff@~+=0>R_>w zB}ZkWZI*moWMpK{n>CFNu7M)Q)92T2b84Rlo5S}($L9aKj*hOfn8#HyF)<|}AtB-X@$D}yYkm|+E&i+6oO@Y*VQjPBN71wT zUp}9=7niXv%URjKUX+1p-l|orK7$M058=|4k2SL86fgH*Jf`zUV&2~?J3v<`Bp6FR zIWAxC^YqWLH0j$JA98BLY*Y0%ZrrF)V9xMu8sm`vd7X65Ap=McEoa@Uf9-sYwK;LM`eN& zcYDi#j>EiRntjb@-}`;P@8#XyRqD=vPmF<~L2N~S_m0==b{_+Yy<6fr`Jc{&gRw4e z`o8z>I3ZhK;NkK@F6RZ`hD+}rn_iD$_A_K)h$?t+ps|0NUTjs@Tdd=58=qJY;43mW+$-U5l>l{usre@WR8}`}eQ4+wa{vbMj8#_s}i>x~?6|JZBV? zuuzimjm2ck{CNx3e7u~sRczP8Jq-*`{`~xWn_u2;PQJqpJteCH3$G;>WPG0ZVXjhB z?Yh%yq4w70?|5!<@J^aOefqU6T1M~1blvCdyT|j^tNai{)ZBl6-`Dehy{Qnk+Vt?l zf&;5ppXK(pSbBHa+w^UR-Q{bytCyq4`*WO+Hu}1-yWV!U-ga}J_4_@W!7cdd>yA%(^5n^!sau|I?T~S{x+h#{ z{-?fso2lZ&-;-3m{X#=R<`h1*WM+7f$R}@irs~9*4fR4F^qaI^w=deUUG84(_q!(T zVkavcY?PanPNk&gE$6%wJ>~p7+v*pf!TBj#2BqI?85kHQC~9e~x<3Ekm*wH0E7{d2 zO`5cA&;NhFcXxMpYn#;0Z~oA!(YA?$cgp)ma~HY>=N+_@4rKmZ{p}6s3_lU+&UeeczOMIe+dpsGZf(51ombXs%lS55>3jP>9+P%o^{7n$J~smc zgVw9rDxf8w;^Awfw*GkBZ(p|yR4+3{-p%A*d0*x0)-?=lAL2duIHw(0a8pQJoPB*a zGsA(SKR-V=|NlDvf7IcVH}tgqgiB?Vr)@s#%&&WqML@@0@m-Fx)eVluscDX4rk_hd zO=A8SVTKvAK>hbGcZ$!S4HxDvVtM*@sHnZlq3W=)0CTZyX2i?+jt}|*_xP`T#5OY+UX$-|laOwdhLjjSUZ*ih~dBo4#>!P3rB-x8&yObZ_yEd3JvO|KiHZ%1Qj|SQ!{R zwY0TU+?$)`_{~*2LK&<){pI)WDtZOR0Di@}RZa3GPYhB(4+IeSxtVi;5pvdSWn^Hu(E97cSx~#U=IN=a-r{;OKQ1kBY<{GA4GR@**)KGe#6be?_ef%z-euU|jIqVQ3b?Aw4PzDkUOOcvX0+pSr;&Q7!c z^N?S`#l*wc*LT|0thEz4gf4!|RGi9Qb-QT${G~?=K^uBOi^29xUw3@VpD&mFGkbP* zTYQxYe5KXH`>%Dy4XfFf>#259JuW>!1 z?y}i4_S>r;A0L}LJYr@LIG(k2-qj!LJQtPC+i}-p6Z6b{r|y~w?|L>X8#Lkb`T6{hHe2 z{(uepB8Xoua0q9Jhr=OmlK7P0A_1Ya5 z=1$)mt1@|a$HyL%ryT)uFD|TWe^%qr^nHC{?THDB4@8(57EIi;XHP9?$6Sn?TS}N% z3j67kM+{shW-K^x*lmS}!Q_xOcB8GSOL$^+?Sg}Yy|?AwHap&YosD7H!aaNbT$$`| zXBiqOGTR~E_4JE#uNF&b{1utRnZ-8avX99N#m6o=f&w-rF9cEw_>KJLSZq9&wKa^J z{4?;iGQJq zp836s$4?(VeE4=xh3~SI-{0OY;dysr!2^egjt9vR-xSWPt4w<15dCb1LE@n~yLRmo zl8;emU=hi(o@rD0DWv^ypt3^VbJf&`Ki5>&ie&F!SlF}ez-`Z-&9%S3g}Bw9n`J*M zG3WB3O%Fd7U9?&)Bql3+_x9oEd0AZC+^P3Z9n*G?uoN`^btd8;>%^Zbne&du2nh=Z z>&5OWx#0VppP`}s#EBCE8}$X3%L~7~e&G6J_J#woLTx+dei2zzCZEfB@S11jZP%8! zt2j(@UR+qnsqEhOM?p)AORbuLfkh{FciG?1psO~1g7VawUv6BTIqy^ot$jcAX&fwc z(d15XXvzJ)zj%IjNw%tkiz-2iP@lD>=Wp6lU zmhn!SJh?cmfJb?&WYmP2ZQ1b*TlGI|W)ZN8i(TuwWFGUrRbG4uLXm%Lab*%f%y`=aNr@@xu@TjuDS@9>fuDkczcedHy^%`7X z{23e^1E=f)T^o28G-_s4b6x2qZ$u|w#a{`fHOieomUR4WnZ9#c+1p!=o3xm3C*m)0Lujc6P_xr3}0xtLo zZU6i$!9|lhqhwER%kvM5rkY1Tdi+@N0~doMpIYAXRF`RjyD&yA=l9kAezrDh>nk2*P40bq zubG{i_GL%icbsi}=~`sdBeBz>;W2{Jpskj^+l@6eb#+CzPXBQKe&@|+okqpFL6O~0 zk4fjZRBU$Sle6(yz1;85x_3g)LTu!jSKn9t$ujd%+1Jm%-|s)~>FfLV*5vD)Eb^kF zqSGf$o?Kt3mwtYp?Nj#Uq7!c9Y@4)X$&w__|Gg({dQI$^=l97AK4X9Wz_iD1P2G9h z?w8(K_iC;duty&ON;%AY$fZ<5_KRTTx++Log%xwp2wWPU8bz@mTi z=FQ!1JrW-URf~7`WQdfTJoM9;X3YJu$It&u@b2>WVJvT0uWJe%lYeV!wb3N^aamjQ zhIbca76<)*wR-)(_f4$acA(CdxOt2c!;Gd)n>NMIuX?pobH;ooyaJK_q*^ufnK+IZ`_O5AL6x zTsMtn#rb0%o}QjduXx{*X%UrhU9!vj7W@9$)e}3;E1sI6bm>`?%=L}Q$M=1RmQpr1 zH(#3>dcNY2u_I{SnNY43D zYVkaPch)c0sO!J}#NJ-N|KG1wywYZWR+hfL_Q60PVaBdqyIyTfKCXB6mM_c0sokZymXa{1k1^`b2~hin#Fo6fF2-Y1(3 z+5+)$!cJKR24i7K$;zi25)VJiyR+ltWBb1^{bM<1-pP!-8(FJ=_F!|yYoSN3_0B3Q zPH8>AXZ^LDlbgHy}+)AcTOFzz-;fHQ(Ypc6l?U#$5lOB~#+Yq~K zzEe-b)b+o$4s*=Ye>p3YF)mF0{*I8aFekR}3JKTO#hz9*dj64xV~N>`S>6rDZ+$+& zakF(ABQx8V&!0?Z^qo3&s-!o;FD=Kp>F@5y4?E^R?X!N@!OzW|{TMW)WnK10fz^KE zf?Um{>_Zg~t0R=O_QWiawff$^E%Wj-E&lntUs~4wGI{^?qf#<&-~FxDeZDSI;-0k= zSIiLG9_X3>qj=A^TiKhlva>(iRNQ0`2mty2^Xv8d=Vd4)?-radaOr0G#b;BN(ew#`K^8K#utF5l+?qk%_);{lfeZ{P@^^QB%FD+pCm4mTiP)&8uD;wS+Mk_IhNH8(WYYBM<=Zkt zx6})3x=-16HdHLb(0Vf?Q@Nvtu=%Z&KJlE3TzZi+Z+m)o|9!vr`#s}}7ccI$tpRnW z{xq58-irDD?RNh9*j*(rSIZlHm`<(rsh8{qoJ=)y76drr3Bu0`jwE|*|nQ9sow ztiDgevgk>=RN?NP4YBW6$GOfaIQwDCM@_*qS6){rTixbplnB4ME_SzG&Yc~FZHqMc zPE|5G1WcGd{rle!4-elD6*;<0a9elBZBPCQ9UU2k8Gol<|E(pjSnRv!_jL}AJ7@hX zD}UyLmW>H|Ha4F5(`a3Ofypn&(jb0S&jeY{HNkJM{`m2O<$P>tLBY#Qs*#2Uxp8`` zFAN=}neDsfgQBCW4M7u1NjwQxYwuY-h)t1GbDZ?p^YNzx%=}+CI5|5%JTjfh=Nc## zwl=Et9B&g-*zK>+za6)Zzqdo{wQ$xm=^$bGGjpxWyJj7|dipvqL!EnE+`WGjmEG&~ zUN9%ArYGK<+i@x0^X5FM45q|AK3P4lFD>Q1;R7CIj@|uj&AAz@kFJQiXnqd&C@k9( zQw7Q#9bH|o9zM-B6lG;)RVocR60a7#{BF{9;eTvz>pwpW*>Xf&iJ#Z(%zXR*1&)pf z85dt?W-)i~lexJ)>*^}wiob7eZcZ<^x7gdDGDFUY`5!mOox98@9osB~HwzrH((kV3 zp7>jL`yHYCyUX9V*Oc64U|_IYT>brBKj^^6$=d67D1`-FI487AS-AUFsy^f>wUe}3oHE0o-Aiu;la=>e&mYql@i_g z_5Xf;l&}9&xNrJ8afUyAU%q_NJ#X{*%_s9pY07tY`e+bj2+5ZBNLxq@5p+Nk3ex0NNgV zZp-brW+%%|Naz3AwpPlc=0J4CKE*7vSGLvPQr5-q|7YtO_$F=#E5n27BI4rmwcl>0 zKlk|2Tv4WRT6{_CmFXLQan7_<&+a=Z^F1j!`CyunLvC*Fw5wTb1!eX#)#WHVR6S@4 zSy!F+?a5^SxYUgi4yV%^zI^!-a&ni)=YzkL*OawQ?>u(B!|6Mxuv$%2czF1u!`ZA% z5~i1b9O)F^uex=A-rZe}Y~`{m_-C$--hOVyeIda<*9gl8=?9iL*l$@bAegJ;(b#p9=pPl(T=}}pfS&EH@f>w@r(Z4EfhJGr@04&ldItsyuH{u~{K#r}tj8fMdC_OTN%9_P3vX@d7Spu?9Wuzj zw_q*9gYp%RzNycvaQf|LKXJ|5O*3!1*4$XM;h4t4kKJ4El-v z7)&*uck!6Dac^!EXobz!>H`go&=odM_s--s{G}cB|Ec%xc~6b6t(z<+Ik9@Pg+p3e z+RHv!YrAicN}1FaE?c(j$ z@9VH{58<W#db91fvKYe)0%)s!&^zQER z{r$=r>8W|?tm0?6w0AUXJQf#{xPK@)DXFUZ;U7kZ56oHF*~Ruh9yAx8o~HXd)Ii$E z|BfHG$OA@Z^+kT$-2OPk959+z>1OT?nmgS7gBP^qa_aN*^Y6cTn$6BIf5E=Qr0eTq zHy`Vl-+#}S|KSFs(vNZ{9U@+S;2RA0OvtZFcF{JpX2Y^~X&% zy({LoiW_HyrEQEjaQ+e7q)C&4W{0I7V+ngOIgIUx*ew0%%Tg6ii$cDWL{M8J>Ak*PoxCUk^InE!sa;JHhUX zd4%s=tI|SOS69)@a|RV35>DJ(C*0V6#v@hfuh@^uKXaf4O!^P~HjmnWoa@iTU*FSy ze|vvF{=>7``Bl%j80Jr!J9lsT^K)}&S{a9T*%dT7=QgNi&)?VZ%>3y0i!E4t5KH2SWt4qtA3 zL~75+Mlk``2+NoCyWcx82${&My_C>&oLb!3)zt-B(9Bfloh)Np6>>gW#O1b{sQsV2 z9(D>U7rvV${MMN@gI(!LEPGkb=C>aoAFsEq|M%xb-QQol<`uRK559|tipqk{9jJU% zCV0_WuT7;>)$>D6K%mQUn}tiOzrO>`LEYGxY_RR4;??LSE4zP`ZC)C?yR3G>yjRB$ zm)Q#Q|IgErH(vKzA$EC~`K5+htLMJA{ICDgUccwl&sCwTe}Q(Ne|W;m)Y$*ZC2n8M z&riw6`<`B`S;Wl7^MbE-jvb4fQS#S(rQ;L&+J7|2Gf3X)Jvg_|M@Kc{Q2|u zpC5i@ZaBVT-MW1SrLV4p8y4<%{=ALzp0v8hUy-%Cj~IO?Df0^6vbi8LYq4PB_rTr$ z8*jfYE!%zfRqJQwJspcQN+jYTYa8%^{(osTMMPv!Bd3uzI(_{?GCsJ~9X1l6qPF zW25f0YF+l`#HEYRJnH0c?detb`oKA*(D%xG)mQRKKigLP zTeLCxc-_p26B+ZrGyC}Z`JKD>@Nm2T;UjOA{wWB~-^xrj_Lk|k(%08M#_bThJZb*?er|_Agif&~{V>baJ32es^z^H^(9qDopc~3w7g%K7sqAg|d1Gt#_3NMv>^sAF$F=G&FD~BY z_cK+nEPUkB)%P)chdlEiae*Du?avgFeeZZU`P3+O-j=ri{pDrx(&_6385j=S=Um8f zJJPAVyzBm7HXn}#>Ib^ywYQYKyu>;ui6J2gG)j1FZS?ZJ-)?1V3pjmcIjG_N#IJg< zymq#=ZomTSFgJ6RP;5-Pnb7v-`^V>lUbQ^ zCw>*^>bT&*y_J{E^Y<4srEltQE3OHFR>o}b=AATa)~sd0YZ7lcOqBkkb7)bS&y9=c z4^L04;^O9ZePt}P=KLK?*$2U(O7!iP`v3nXoH?uzw)*Ru7J(^d|I>d4GD+Urx~az5 z=Ht`?BbKjk>mnl~Z`W4*Wq4qC>GEasUJ1h_KBpCZkM|#IeY?r2eBw^lMLClnaJGLe zvi*Ga!;Bd-?oHY*av@=B)aBW)gfCfcQ*`^wF=b5{`>ttf7xEgpr9a^ z`$hsM%51amotWj^d2V%7P2K;$-y^HPzx#W^{W>=bgSy|G8-M@(elLF@$zAuE0O+tE z(Yk_NjP0`|(jOn|6)*Xg-*NG|qMzjh&?u+ev<0y#Np0!5(}lKg&%eLVcfMUMTjkGL zj0_BaWG5Yn^%pA-?zmIxB(+~)Ti^R~U0JIVhIVF#8BVx{5vg)sAW*T4j>bRA9gsmg6&*V4b`2#}hC%-Al zG<-C-{N7Df9UYxp-PcpxrhI*U{WM2YWUj{&e{*Y-#`X#GL6vOno{DwLRJ66br`_b{ zWHS?dYONCU`@utv?~j(X`?x$VUzhw~t->))4GoXQTTCS#nHJ>)f1Gai;@}Z>6ZwfN z&fd<@-v8kc_h-e))(K{QbFH?Pe0gzkzPW0~#LDjZD>lqcEw_63vo7+)4IhuBm~5N8 z7Z(=p)e-a7-npAA<6=*)(m%F|mKujnb^iGHC#WQ^EkAgAOAE`4W=ZGB-DPX7-&6(0 zuXR_vQadqsVx_3|HaBm%s|MBI-Y`EuH#hqI!_#a`psj^+b&q?^%>-YCZ??LzzyAM@ zj*boudyA(HhqJcMYi~c(BrdSyb-QZa65mTMN8W~PtOni5tP{B@CGGaM-1XlsnKCgj zuy9qxYfsuK`+r`f>syob>Dzjq$1IS&z5{f0zzonPny(*985ka66Y;fmvF3vgw&G{2OqSkI)9&x@Z?DQ~c=hVl9Z;j#{KnU)KWEp8&vc%6Dxz3x z;>U#>-z=RT_iNFL6)z_6uao`|ylM01`oG8R|9uni2{T~AhX@&zSH*&Te64egdb5TX&sPVlHj@w^ToUct5 zotY8xYw||T85_N${fc*dY?@is`tB=eKJoeTB}>+P{IHgRVf*5JpZ?7)zbEu7tCDaTU%TE@^=3I zy@3YRyFJ2eZ#io$X)sEy%!EwcTwvD&nhE{)ncUDwDp_9$d^D*05KUN#=?}~zs2SpdjB>%kozV5o6 zlG38Rpb5q|x3+eF6Jxz5!&`W3+J27JcUWJ~iu~U9RD99TM;+fj+}M~reXd2}BX0Q^ zYla=IyLRoGbhKN%f4*h$vp7z90sW7Rr`ylYl2?}fD{?+<(M^{<$x)!Goz{qXj^Ezh z^%mZd{jj&z0H@Te-G*@aGW@C z;z!z-7Z@^yH`L9ti0dRkgqO_eJw zE$aUKm<1ZP<5aZvys<8Ky@!H_JMTT#3pIz{|2Q50?@;!8li71pnm?V2^={{r6?$N9 z?B?g!=V9hFt%JGnSMH^cX&L#8>cnbh@A9wJwZD>{91;?;WaFk~o*k1u6uGRt{NI+N z&N|;gIy<5+e%gh-H-9}i*u0Qe%0+aJnyRYkYmxAar##BSy5&#Zud^g^mumNtJkkjgCd%#bLY~V$IKR1wXHpE zGbeH9yIrsK__?`v^SoMFS5#b_%Cf+y%$9TgjjwZdPWd*urS$@vw8?L&4gSv`U65(c zUp#BptXJFbRb`)=V_9sMb#+y!`}cTL0S*x{v1L6HhL7xhAC;+#t;vhpa&r0Z(%09R ze_7PNxxat@HwM#3J)76;>oMUow`Zu+-_KyQD?8_I6xb9SJ_Tc!0cb?tVw?LcKbl4cw4y0|4joO}f_XubY|KVpoExl4>4Ju?utF39I|v*^_;J z-OE?mwYD|h3=ZaHyZ6?-zP2{ltyk*n%0(Jmr)>8;k&-`Mbi1|e108lLr8T<+UI?Gz z(`|6St@bwi&z_2pNeh4b^M5cioBj6D63@x|riyHve9SHIBI9OWC+V*mI=?4}`|(a_ zZ)s_9cXw}oe?>vve_l#zm9S{`1D2BNLrpIhIh9@fTvt>^EUm|)YZNE`NEVH=a1{W?Md_Qx}!f$zC-nm zs&4PM&tG0%E?*nJ|DSC8;lQVhe={-!t?-PJt9&xC9+bQcUTy2xG56{IX>a%6xoCg# zhL2Lyqhnvo-rjO`cXKN%J^YhZ*Ox!wR9|k?8G~&XlU`Q6Stj=T-OlH78=84Q$HI4= zJ2;{9ZPl$io+);&v*HDhPna@g$|B3+XL~}!!^7XOU+1Zq3A%1+e&w^7{h`;*j!$2^ zHd`_G_O{lI5$t8V_ddF?&{dH-EOPObU$J7P`Ib_TNY!2TSu=AAPK+J)N+a)zp=Fb}!>u z%i?E$J{%Q~?+M<#w(><7LqndIw|772q|BPke}8_qZ>;OsqNklDUh1TD&>`)>*I+rf z{Dl6OIX>Rr%?wSPKRzCp-ybAg>$SyO@**SC@~!7DDrcp4wg~R~50Vku)GOs2IAzz@ z*VpGiXQ_8}wO#U4$NNR1TLu%~toUU&t8Q=072Z?Ow@l5{)O4!B^vROi8DD3x^r+rD zIG2CsgQsWz`8_{3_u%!+3EkrQr@9zYT{tb)8P#vyt|Ba-aoVHX;}P>T{S$s3Sr>Qc z#O(NR!(G1ii@zyn8+$%GgM^dsnZgeb4(`mFB-Va7@bE|Wk2Tk;CU5?b;~hDFf=>>g z;Hik)y|2N?i?7(|U$1Cyf4`&PMoIGQb2}w3`@OokS^dmGo&_3Re_yZP?>BwJ>%av! zzRvj}b-qpc>{1ukw`U}7@2ma2$^Q47&7G@$Tsyp-;e&EfadCa+yPeOUnV)rbk)Id* zZr|^BdRdeB?02*@EZ(z6M$U-4$l<{{`DurH%pxZXZ3kU*^>SV8?teGkud_2S2pI54 z7%VV8>fkoZ=*Y|6XZub#M=3o$`|iE3`Q0m;Ha35Z9xrESXwcsfwKfwptoGbnfA5!> zXVX?cOUV%z+}hpY9(m66NWlulL%X^e#CkWm2<=HwJI|@ERFp5s@axy>^}+Y6Uat*b z8}>R9v>Z)g5u1L$to61JACJq=pE7Aukk8A07v`Be7S9qra^$v563fCfcl=*AUfq6i zTP5Q@vEOySUaAYku~=Nadi8nwxj8%UH!PT|Zk2F#rTCL$tY4f}a(jT;-2+23Bhx+%GJ z-UE#XYAll~KU|o6<}j#BClD^FqN?hu%`~O*K#OS^OUZ8I2|pTC&m5|CR!Qv5Q2Tx? z_4&EElF7$`g-Q7yc}*I{J2t-Ra48 zig8N%S<H{rv?ReA}|c+&Tj+?H$AKdN;#C#O9Nf%GLBR_A#g2MZ;<#$=vcQZFIEZMhjpU>G6 zT5pv&7A-*MIX#o4tv@dGqFO zo0@2b22WSlqccJGu+B6{JjCYqf64OmN?${F6#GQ?Nu1Na{3Y0~Ma;(WhHd^f|Fb?z z`ZDL)+a3r8oq#j*z~RC*hW$^jxb>8Ezh~alzdNV+`oDFtyPIOOCNwoMm9umStUj+M z*zkCV|5Y4fcF}N;*crJXmOHmc^=Df~ajN9R-KV#mJ9SE{ z_RYrQ_x!xQqhBwrW?+cfv}w~mS-YAaH`YdPUl!c*cG14xu_!R7Mjf{zr=`iJ)G;`)mt&7Vl z9`UMN+|I7F=}_d#``(dN-JsZD>oTvQ;&t{ugPkzp>bT@q73EA(w6OH0l-!9eP|L4;r(B?kd z_QQdXTW>QnxV$N={%L-{#`x_~@%TR{vbIJ^?o*i7&ymlp^r-Ao^A(S4+YfFu{%W&E zwWy~1{kGiOVo8;30r%x5%%2~xX}Q0|X?F6jw>O@(x#V9tl6hnK>Ob?U-@O!*l9I~& z@Sn}}=FOX$lS)k_Ii9IErW2Rr-a(OxKcgl|Jc<%*ScIw?Nzgg@BY;p#}soV z{hTNBF8nL<(|*g>fJ@(=oSeL!SJLRonH7PH?{)R`w1mH4lv}=J$&(*@DnHxVmb?g% zTb%!>cGEAtJ@X%Z4ez)d|M0b*&o%$T!1d)H9voEq>~qOikaN+NAG~YAvI9#>zJyx& ziGMK6zP>J2&GNbeXO#<6u~K8I@r{aqZMW|&I%c&mdV5}O)YdFf{{8a}>;8On_h;$a z@pwsRt+r9p(zM&4!6*A}$4{R>2eZ6dcjCsuRhK#Rxwlo$=Pz_O;$M}&qx$>13v9eW z5fL*E&D`B-w?HFF-b7iT&YE#H=XcJHdS1umTmJK3+Esk}rF_t{Gc!LQ*5ChU66g}3 zKh{P@pS(`~XJ%;A(9*K9sr>Y$6133mnau`AZQ-w(pMPnpJP^-f%*<1GV61r5qSbio z$8jDh6Ai{!D-8K$t&VJZ z?fOcM>w?v)ghG=rAKt|4UbCDx{)%CL?NavkR_Vd4t^cwQwQ&Al{cF_`Klu;opmEY! zrrCBe`)VqKf(2K`J=_)GI{D3xi7#HfSg~S-#)ZaL3$|@Dvo|;29;0Nopf$9w;;-zV zxPQzk1?GJnj*2=J^X=>Z6&Ds3Zm)WIhmnEdOxudd?o00U?D#Ba;&*28qD2lqEDUw$ zmU>U;tN(J*T^}?=dF|id-A$;5Eb;_HeZ-xwTyKqCP2Y^&4wGnaQzUsh+<=%eT2Zr zy)CZp?#&e!6g)jWH6!k(*c!B2JZ3-C$M{FQ{71!9K3OY=w&><3PoF-_51deWgT<3; zb*v~5f6K0z#QuTp>({TT)!*OE z{dy(X-`BSMU5wySdzHuHNk6*^ZU^=6*z3Cc!*aWq^Qzx5&OQm6Dp=WKH+^gFwvE1O z@AQ7v>URB_u<(f`qm&Gv``34OcW;&~HVC@^ti|;Gf$g<%>Y8txN`2St6nA-o0#@&VyH^W?v+)Se5s$MRAEi5E7@9PIkW(I~CeV{`n_p5)| zy-D&RgD2>c9q<`bbp=`s3<3eh3i>fSK1>Cjy#K0C*4l{UQNXklbH7=IM?Taz|GE0jTwNhjsE%j z&CSiE}`~G}79ls`a_qVgVcI|TFzptdwaq`@`d$w6uRs?QN zJA3Q2;HJ%goVNVZpZ$a3X!nW1lSRQV0^qX(jYrjB#P4euEZ{CJjmAtrc{KWTW z+0|EPxqVpqa^u^x3D-COIlHM?^Wx6n%{!v1s;WLN?zj8({?DI3?`sSGHb|d1d)D^H z5n=y5pu;y~4&7-MuRrwK$NB%izX#7<@vN(>Th+La*RyZ`x>|w9pY&6H zzOL-)d7~3(t)Fe}+2Y2{)xpIzp`$TVtvSeGfu~lwgxX4@!yqF=+MyCw*Cp~li5^#dExKv<)vl8|M0@A71nVW6crVfy}!HLJlY}UKy^dk)wIGDUpa5(9B|=H+2!!MO{(70=ExW( z9r50|bLVc`n(hXL#oM--Ro~rJDjZ+)@u=m_ojaHE*l(NY!oJ0>^NPJd=}VE>o=4VaOxx&d(!JY5Hn(i8ShuQ3VZTXTEb+<8kfM zbG9kB4K*Eja<7!X5MSswZ}o2|{i zBgNTr{e7pM;=Hqtl=;<9xMtq+Uw77gzVFOiHMF&~Rt31)gEo@C`~7}@ea`D^Yj?le zQNxgNByW4Tx~l5hiqB`ww`cs~pJd+=e(aOz$C_JJH8_J=w>Da~UjBoSsj2BqZZVx73CH_n z#ZBI_1^&v{70U@U`TXhF`?41o808*aZ2UFh=BcUL=U;BnkK8YN@BGKQ_yhb6o=wjc zxNdH&)%*4BZT0+ee|`pr12$Xo?podA_fvk+0lII4eK{k8WACh+YL=Fj$N&8N{Qb$Z zXVZ+2B)EzHdH>LDu^hXkuf~IQ4;&UOPH-xVdD5rRXSsIA-Ji8l4#I(T@{3l?{qpmq z`g|ELA0MAthRMgcZL7XWByEgndYY@*;0J1DuMA#(&1Z(eL+Q{LUYvGS&NuqQpZ@xgxv#_$*?ZKVDcIS` z-CE$N7rX0-bg)MnhP zjA!aT9b}h(13GN{<+9IQUR_;Xmr8DUYjhkpn|JpzQ^ko37Q4&dDxE#pbHhGu!)sBm z7~bWK-ZD>fuJ76J8|6Oh_fwI*+Mp$c7nR-ndMbav-L8J*$dNzi4wy2^IRuJ4{qys4 zG-#vP(nT6(Zq|-B-uqr^ZA-Y%yYt_#*ZTbY{Otdi@~v9E+L(iX=F7K_GaefXFf=UW zWME)mv3YxY`}?y6Mx8868vZ-&PCu*GTv%ARR#{1@>cLE(&_UnQlH%g! zi`n0?CKz>fb@8o_*!bwQtaVvR!5=#}VFisI|G0RUUq`OTN_^pt(?Y+vancp>y)puAB?tc-MVvi`1*OX zXUypME#&emZ_LKZN>Bfrr{{=&s%3VL+{}mn; zRb^&n^{dL>LX?4lVe^B)=F{_!i^yJ>Q*z}KXh~r$@1CP6-x(SHNUgqF^$m10n3#Ut zpW5Is=}FCAZV&vd+rE{1_C?ty1wK5`aBBPgy45Vs@r&c0@BM!7b>76?twI+;ciZi}YDF8HJ&R}O3kx)un3!BywtRU#=*F75 zxf3S_9(yr4qtUZGC1<)|kisGLMLFg|I)A6kclgyS^Y!c3rJ&towH0v<4Ez6nyS?kr zpFdm&&3v=AE9FY6zj!WD*I?~(NF_S-<+ZiZ_CI)g1A~GlX(a@A^vv}LZi~o0U%KFD zz4gSplcoOce6n0$Zz`1SzPqdX`@5X3wc18~8;*(2n*ZiM*Mse!%{~6Hh5cXpy`9vHW~-&&6({K11Ii2akyQjE(FBkmJ+x>o1F+=|+pKIW&;Gm#upaU1y9)7r? z@D}%?E$lH5)`@Q0owQ+Z_4j>3zbtB>Jb9uL{H((5kIfeO!=Zfpr~hY^yz{r^^Yiob zUtC*zI~m+9U})sY4mvb<1&fgg`wnXbzkfnPLcgkC{AXlXa4>E2(`Cz-zrVjJ_4K{k z-`}1F$$U0qDxcQe{q)E6WDTXZgC}@-7N{MclKyT@eNw;^zr~Ejn-rW3CV##C@b-=! zdyoDC^$yO?wf_F0N77jC-TnRll|c(vOYR9NGz1!(u0He)bRRvl`;B$s7vA+9P+i&o z=xgLzj%{_P*g5XrzjfSTSOlbe`Z)Q@{|A4Ax_{kS?k|60UFL*Ut5%7eiV<3WUL{cI z^mLBo4*r74>nDHTy=xk1W8q%Zw#W^*=2xtgGN08*PGo2Fh9&G3Y8ChH zcq?{n_OM%c>@6#|m`v=3go7V7gO}a0E_iTY`Qbx{ZteVN#>nX9>U#8L?CvsEy{IiO z#2xJ?9O>uPkW}B%ukuRGC#TP<5>p2_QSifudmBFxsliKO|)*B^zDf2 z4qxw=yt!d$q@?tSOMmWSO;Ede-b0Buzy3Qfw7JoKeyzZ^dktsjdgLEgT&w)%U#2`y zPT020%gZ*;HqV#)ebHUscIJc$A66VqVtl(yTvJ!qc3;B5rvGnlZ`W^sXnf$ej?j(+ zvDW6-zZHL&#x)moJI_hBU*8 zfyHj)#*Nnt=CDOoOrDj{`YcCrTKTIhoO6;G9w=sIWf|$k>?rurE?;M{C-HEbN!R8n zYOk(r_Kixn%*bR@p2Wj7VOuQA>WnMSjJ_?FPd_B2aXhT}^PP7gi^<2@UdOYbdpejWJo7406ReVfZ>S(C#|Kb$4 zR_B-Sj?3YW_D`cf{F|UrvTDs5ot(?de4EPsI4>Oj`1pAE&9~p892=Q3?fBtU|ePK?#%^SST=cbsP+`jZ%TQdI_pEbR1!{zk-!IB+2emu|L_cN_D z_*hxk{R3WQ3bWS6UR&O)@_eiAhler_8GVw{($a#0f){VIKTnsE;1B5P$*rh$X0+J$ zF{k{_zUM!~qOV*&E?2E{&^N!1JL=Nq%brXDd+rNwY@Lucd&|4+9A_40d{%qAXN9A& zc~*jr!ivAQe^}Y&XDQ~D*J*WK`tWM?dcEJ5=l|Q1eq}|V{?_d4@7&Dqa5XfZO*?&a zXYunxpz>wKgFB7a%sf1g&iSgv@;lDM$LGw{xII3X9~4-u+Ma*E&T&J%@W+oVCHCTP z8VHXo;ACo+*Lmj`d3O|NV4Yf4ammNo|Mo(c*$Fr#M_P+Z5jm z2X2dH+3j$Eo6&btvfmf;W6ZObELfJUsjaBI(_#r zzi)Jaf5pm`Cv6HJ9a&a>zqWk+A`P>q%bvHtpGe6({@XAtUhP)8qSyYTQTc8gz6YCd z)K2e_Z~Zon^WDa&Q>VHL{b&DRWHwtj|I`%C`<~m%TQ97W)_5CnYFhKR*WnlHtl!N^ zInp5*9;Gua%+4x57&Q5}di_2xV~uMCe=ThvX9^r#dpR&L@Pivub4p6eha<&5YQ=fu zCO1^cT1==B)P*++e1p2+H7ci;>6 z%9H%}s$Q@C`taey+aU{ogH939v9Yl^$Ge^LL-G~Nw79r?jb~?@=ik_r>b-adwMbjO%aydOidQR_3+ae)zjhRBa1IOD2H%(5-&|S;5J9nm%BT5#A@f1_kv38x1vHqLJ#L^n~Gn(di7*e{(C{m>2q0J>e;riu)KTM zn3{5-FaP11o4d>3_odIT6=QkVa6ftb?d)@NEQ^<GR?dg_iS(5u!J$tXDg2iwBIF5`-Mm!n+*|QHa&){;rb1&!q z(Qa|=D>)*Cg@v#0)qFnt``7pP{~anSEP7NK`(~vrS+U~94SxGS8!8_jYF%>O=X1*| zCGL|KPrN$(;UBl*FTG3Zi*j~1?DLZ1PK{05-}|8;)&@PUtQ zz4eR0*0;<3=0=&Po|=;X`t@tid@J_!yg}KGzLk-^gJ}!FPYyR!)Bh}emhYmSi zx~=);hu%b={cIoY|Ff{X`xVII@^(w7^hO;+!;fz!``c9-+Su&bobE5n@MOd7w^zmW z<9tq^KY!k`>`lb2XJ=<$Rez_OeSKZ+X?{O`hB+tvq9TNYHakllj!2$T9H`!<*v!t~ z_U0{vLZek*SXkJy*j*)>x~i&AzkGUnx_9V!a=bH8^*Sh*;8)W0Xyu7sTRXpz9%CWFFf8Wn%Z_V$PTt0m9;>B8< zg5M1MLGkhNRiKXb{$H3=Fj|o zT1Zxwmp@5j!J0KXtpWC5Z+&IV|1VKtZPeek%>Uv}p?&sKHuzNu_gRJC5|WeqcXvzX z<$qlH`!BYiO*?H_`RPfUX7I8%K_acwOI#P8(H21UvaDGmW$|ryG$l=&}d!$`dzP9J-<`B zh(V$GY?}6&6DKU@&7C`U>dcvuHP6n>T=eA0lS#`OZ#30|ns%$DZ;LWWxE{+q@M>}7 z)Bm;#2CwbTN*>!Ce)ibvyt})28@BN?q{#5KZ(X%YYx9mB7RuJv)?ZIdRL%sgr<^E~ zb8%Z_U-eD@iT|}<&2Vh-ubI2$ZPM{RS*|bIDOakEQ%(q!vzg4zV4i(~*Qxxi)7e`~ zITx9onPXYZ@ATsL{=aYY_wRn(XWdrv^3v1)pc6l@Rjy{>U%W4A*Mg* z_U!5?B?dp~a>3{3SEiXJJ=(e7sZ&(p)ZXH|w}1Tjao@4l%q=rhQ-AaHjup*j@hMFE z=lU!58vdw@iZ1EZ1uC@Ba+kLjD!2L0{<-b>qh-RmoFx_4Jm1!= zZgn`mAb*A9Zcv+fF=$ZkXP@=E7vF9q_uF1r?A~wf?CgAd$H#Asa!b~&dv|GP@$<5^ zaeJ$-pSS-XbKt}06Tey_ev2McUtjMyr_Sw$`ICco?IzQg{0wV(Hp9@+&|shBfxPYU zHkF@JCN8jV4*fpC^SIe9o1S#`ozD`?@0JAbzHs5fxd8vK#V;={y)5+ASmRCA$r)1F z@0MgFrFaCFOV8ToD)cBz+{RMGG&1D!?fiY)B6OnF{N_|#=QY3c!6PLlg@5jL6^#Jx zl)}P~=iU3|?%LJ=`=jjp)+ue>!Ogw}1&zB~1JW;~PLI7N4_aAxYisxQ{hhaFtf`Fa zmS?Zi|IcEyV>kb#iHb=vCbN(HegFU8`&+Ht;(XkoZoolP2FDhywDl`j?wm4f*018M ztgN)8q$HzMfn(}ry`^6dvdgzzcV(<=*3u9yS(UZ$>%}zp+w(I0*lT*SuBjh6di3Ds zFAf2VrA~hR`t|76ty@{2K7G2gwY7Do7CWEJjtf%^`<_WCZPkyn%%2;o_Nm;Z*Zz;y zgahh}a?B5FO0mA!p}62Acb=I-t>alXMnmh&jTzg+*F;R*l617|eo}Js|Ifc(ufJb- z*7Q10UteF_8n(Kot69GP-fqADO)p|YLVaCb-O>ddbHz6tIbBiTf5rdgm*|d8{u7eiyg%$>xDhykg6jeUK)@=U$nY_s-wFPsVaX%E?Lbtbd&r)hPwW3*Kw* z;FuL*r*y5#*v97B`{eM2qPMr58{?)5jT<7Q8@6SHkC3;`ZzV44udY)p| z>!IM?5T>ig*#W!~b!@|i9hpZi#G#<&ON&I78eC~{B+fH$Tx!0p!{gAl+qNo4j z&-**;_pW%%5hwbgbZ?JBISY&9`o&2Gj3x&UGPWBiHcW6}dcyj|w>)lciTd1-x&K$0 zU7dID@9paU-z=A}dj0Y5n$Xv8Hoq=imGyqJ{PXL(-~PQZTfgCQ>HOD*vl1>`aIL-1 z5MVX^)^nfdmQx%WEE+j2g)KFw*Tw9*A1?T-ed}>A$9Ip_a@H93eOvVG#J_pA)oknb z{d!fsTK@a~KhyskeZSE~CKzP@JrsagKdgNKJF zH+{1_E;?lvhp=y@URc`BjF;B_JAXCIS#89{vi*7N%DZ*zK5xCYE_V0sU-4{SONBni z_&%w=|NCC~^oo^@RQ*RFHRw{H13<@~k)hY8hH`zO8H ze&t*2>YeSU>Xp7s+|_Djw2c32Z+^{V`+XmK^L4CWui2co59G?rk?C_wUp_fG*?-mH z@atbrJ^i#V_x3j3>OUWk%kTMkOnSQYslBW97tK{RnDW{)ZXQM z{Fk!vTPL=Cc8XCl;{Uo5&8uG4yv^OhxaaG&=={Goj@x~!3M*`1dn6Ay+@jltxX1TYvtPRmp)ec*8qt1fk=yB1E1omy=LA)PlG=9`vKIc;Ob$)Ia z2^pC?k2tz6l%&KoZ%vzdW)=I&yG4EmOVqc=f8TZ8_wDP~zXKigFAQoncpTa$o>CBN9?w$62K=aaRn`0#-7^Xg4ul2TGqGBPr2wykvD zUH)F~*3RPR0rmC&AH2G{n!T;9?VI&E;dQxZQkE{WmzXU+>&o}J;<1cc`#jfPxMuO~ zxI%)7^!b)Kg6Ce&;+~Z$y4pS?=={q~+d99_`Zi /// Contains all colors used in embeds. diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index e9d9874..c6a6ed3 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -1,8 +1,8 @@ using System.ComponentModel; using System.Text; -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -15,7 +15,7 @@ using Remora.Discord.Extensions.Embeds; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles the command to show information about this bot: /about. @@ -57,7 +57,7 @@ public class AboutCommandGroup : CommandGroup [Command("about")] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [Description("Shows Boyfriend's developers")] + [Description("Shows Octobot's developers")] [UsedImplicitly] public async Task ExecuteAboutAsync() { @@ -90,12 +90,12 @@ public class AboutCommandGroup : CommandGroup builder.AppendLine($"- {tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); } - builder.Append($"### [{Messages.AboutTitleRepository}](https://github.com/LabsDevelopment/Boyfriend)"); + builder.Append($"### [{Messages.AboutTitleRepository}](https://github.com/LabsDevelopment/Octobot)"); var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl("https://mctaylors.ddns.net/cdn/boyfriend-banner-light.png") + .WithImageUrl("https://mctaylors.ddns.net/cdn/octobot-banner.png") .Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct); diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 0ac5a36..a76e79c 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -1,9 +1,9 @@ using System.ComponentModel; using System.Text; -using Boyfriend.Data; -using Boyfriend.Services; -using Boyfriend.Services.Update; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; +using Octobot.Services.Update; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -17,7 +17,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles commands related to ban management: /ban and /unban. diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index f9ac630..e6c08f3 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -1,8 +1,8 @@ using System.ComponentModel; using System.Text; -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -16,7 +16,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles the command to clear messages in a channel: /clear. @@ -100,7 +100,7 @@ public class ClearCommandGroup : CommandGroup { var idList = new List(messages.Count); var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); - for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Boyfriend is thinking...') + for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...') { var message = messages[i]; idList.Add(message.ID); diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index b0fbccf..9000267 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -4,7 +4,7 @@ using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Services; using Remora.Results; -namespace Boyfriend.Commands.Events; +namespace Octobot.Commands.Events; /// /// Handles error logging for slash command groups. diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs index 6d80513..b3f9425 100644 --- a/src/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs @@ -4,7 +4,7 @@ using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Services; using Remora.Results; -namespace Boyfriend.Commands.Events; +namespace Octobot.Commands.Events; /// /// Handles error logging for slash commands that couldn't be successfully prepared. diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index c3a4179..f69103d 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -1,7 +1,7 @@ using System.ComponentModel; -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -14,7 +14,7 @@ using Remora.Discord.Extensions.Embeds; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles the command to kick members of a guild: /kick. diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 5b3b68d..33e8a9f 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -1,9 +1,9 @@ using System.ComponentModel; using System.Text; -using Boyfriend.Data; -using Boyfriend.Services; -using Boyfriend.Services.Update; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; +using Octobot.Services.Update; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -17,7 +17,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles commands related to mute management: /mute and /unmute. diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index e52f22c..07851a7 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -1,7 +1,7 @@ using System.ComponentModel; -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -15,7 +15,7 @@ using Remora.Discord.Gateway; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping @@ -78,7 +78,7 @@ public class PingCommandGroup : CommandGroup var latency = _client.Latency.TotalMilliseconds; if (latency is 0) { - // No heartbeat has occurred, estimate latency from local time and "Boyfriend is thinking..." message + // No heartbeat has occurred, estimate latency from local time and "Octobot is thinking..." message var lastMessageResult = await _channelApi.GetChannelMessagesAsync( channelId, limit: 1, ct: ct); if (!lastMessageResult.IsDefined(out var lastMessage)) @@ -90,7 +90,7 @@ public class PingCommandGroup : CommandGroup } var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) - .WithTitle($"Beep{Random.Shared.Next(1, 4)}".Localized()) + .WithTitle($"Sound{Random.Shared.Next(1, 4)}".Localized()) .WithDescription($"{latency:F0}{Messages.Milliseconds}") .WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red) .WithCurrentTimestamp() diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index d150e9c..cead86e 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -1,8 +1,8 @@ using System.ComponentModel; using System.Text; -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -16,7 +16,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles commands to manage reminders: /remind, /listremind, /delremind diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 50ea955..9f7c3df 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -1,10 +1,10 @@ using System.ComponentModel; using System.Text; using System.Text.Json.Nodes; -using Boyfriend.Data; -using Boyfriend.Data.Options; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Data.Options; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -18,7 +18,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles the commands to list and modify per-guild settings: /settings and /settings list. diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index fa0f015..64e6729 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -1,9 +1,9 @@ using System.ComponentModel; using System.Drawing; using System.Text; -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -16,7 +16,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Commands; +namespace Octobot.Commands; /// /// Handles tool commands: /showinfo, /random. diff --git a/src/Data/GuildData.cs b/src/Data/GuildData.cs index 2e14e17..a779b56 100644 --- a/src/Data/GuildData.cs +++ b/src/Data/GuildData.cs @@ -1,7 +1,7 @@ using System.Text.Json.Nodes; using Remora.Rest.Core; -namespace Boyfriend.Data; +namespace Octobot.Data; /// /// Stores information about a guild. This information is not accessible via the Discord API. diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index 1c94835..cdaede6 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -1,8 +1,8 @@ -using Boyfriend.Data.Options; -using Boyfriend.Responders; +using Octobot.Data.Options; +using Octobot.Responders; using Remora.Discord.API.Abstractions.Objects; -namespace Boyfriend.Data; +namespace Octobot.Data; /// /// Contains all per-guild settings that can be set by a member diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 72a9ee1..c7ddc27 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -1,4 +1,4 @@ -namespace Boyfriend.Data; +namespace Octobot.Data; /// /// Stores information about a member diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index d0b1ae7..a96a9ac 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -1,7 +1,7 @@ -using Boyfriend.Commands; using JetBrains.Annotations; +using Octobot.Commands; -namespace Boyfriend.Data.Options; +namespace Octobot.Data.Options; /// /// Represents all options as enums. diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs index 51cd3a1..130687e 100644 --- a/src/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -1,7 +1,7 @@ using System.Text.Json.Nodes; using Remora.Results; -namespace Boyfriend.Data.Options; +namespace Octobot.Data.Options; public sealed class BoolOption : Option { diff --git a/src/Data/Options/IOption.cs b/src/Data/Options/IOption.cs index 42138dc..b8ed03c 100644 --- a/src/Data/Options/IOption.cs +++ b/src/Data/Options/IOption.cs @@ -1,7 +1,7 @@ using System.Text.Json.Nodes; using Remora.Results; -namespace Boyfriend.Data.Options; +namespace Octobot.Data.Options; public interface IOption { diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs index a82d7f6..464c61b 100644 --- a/src/Data/Options/LanguageOption.cs +++ b/src/Data/Options/LanguageOption.cs @@ -3,7 +3,7 @@ using System.Text.Json.Nodes; using Remora.Discord.Extensions.Formatting; using Remora.Results; -namespace Boyfriend.Data.Options; +namespace Octobot.Data.Options; /// public sealed class LanguageOption : Option diff --git a/src/Data/Options/Option.cs b/src/Data/Options/Option.cs index a62eb04..0ba8ce1 100644 --- a/src/Data/Options/Option.cs +++ b/src/Data/Options/Option.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using Remora.Discord.Extensions.Formatting; using Remora.Results; -namespace Boyfriend.Data.Options; +namespace Octobot.Data.Options; /// /// Represents an per-guild option. diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs index 7391b00..2150725 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -4,7 +4,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Data.Options; +namespace Octobot.Data.Options; public sealed partial class SnowflakeOption : Option { diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index 5e39cf0..7f60ebb 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using Remora.Commands.Parsers; using Remora.Results; -namespace Boyfriend.Data.Options; +namespace Octobot.Data.Options; public sealed class TimeSpanOption : Option { diff --git a/src/Data/Reminder.cs b/src/Data/Reminder.cs index a332003..828fadb 100644 --- a/src/Data/Reminder.cs +++ b/src/Data/Reminder.cs @@ -1,4 +1,4 @@ -namespace Boyfriend.Data; +namespace Octobot.Data; public struct Reminder { diff --git a/src/Data/ScheduledEventData.cs b/src/Data/ScheduledEventData.cs index f03daee..59efc63 100644 --- a/src/Data/ScheduledEventData.cs +++ b/src/Data/ScheduledEventData.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using Remora.Discord.API.Abstractions.Objects; -namespace Boyfriend.Data; +namespace Octobot.Data; /// /// Stores information about scheduled events. This information is not provided by the Discord API. diff --git a/src/Extensions.cs b/src/Extensions.cs index c06c4ce..511ab85 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -13,7 +13,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend; +namespace Octobot; public static class Extensions { diff --git a/src/InteractionResponders.cs b/src/InteractionResponders.cs index d3916b0..8e42110 100644 --- a/src/InteractionResponders.cs +++ b/src/InteractionResponders.cs @@ -5,7 +5,7 @@ using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Interactivity; using Remora.Results; -namespace Boyfriend; +namespace Octobot; /// /// Handles responding to various interactions. diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index ae712df..3c2c747 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -7,7 +7,7 @@ // //------------------------------------------------------------------------------ -namespace Boyfriend { +namespace Octobot { [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] @@ -25,7 +25,7 @@ namespace Boyfriend { internal static System.Resources.ResourceManager ResourceManager { get { if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Boyfriend.locale.Messages", typeof(Messages).Assembly); + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Octobot.locale.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; @@ -72,21 +72,21 @@ namespace Boyfriend { } } - internal static string Beep1 { + internal static string Sound1 { get { - return ResourceManager.GetString("Beep1", resourceCulture); + return ResourceManager.GetString("Sound1", resourceCulture); } } - internal static string Beep2 { + internal static string Sound2 { get { - return ResourceManager.GetString("Beep2", resourceCulture); + return ResourceManager.GetString("Sound2", resourceCulture); } } - internal static string Beep3 { + internal static string Sound3 { get { - return ResourceManager.GetString("Beep3", resourceCulture); + return ResourceManager.GetString("Sound3", resourceCulture); } } diff --git a/src/Boyfriend.cs b/src/Octobot.cs similarity index 95% rename from src/Boyfriend.cs rename to src/Octobot.cs index 4ba43f8..662d1bf 100644 --- a/src/Boyfriend.cs +++ b/src/Octobot.cs @@ -1,11 +1,11 @@ -using Boyfriend.Commands; -using Boyfriend.Commands.Events; -using Boyfriend.Services; -using Boyfriend.Services.Update; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Commands; +using Octobot.Commands.Events; +using Octobot.Services; +using Octobot.Services.Update; using Remora.Commands.Extensions; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; @@ -21,9 +21,9 @@ using Remora.Discord.Interactivity.Extensions; using Remora.Rest.Core; using Serilog.Extensions.Logging; -namespace Boyfriend; +namespace Octobot; -public sealed class Boyfriend +public sealed class Octobot { public static readonly AllowedMentions NoMentions = new( Array.Empty(), Array.Empty(), Array.Empty()); @@ -104,7 +104,7 @@ public sealed class Boyfriend .WithCommandGroup() .WithCommandGroup() .WithCommandGroup(); - var responderTypes = typeof(Boyfriend).Assembly + var responderTypes = typeof(Octobot).Assembly .GetExportedTypes() .Where(t => t.IsResponder()); foreach (var responderType in responderTypes) @@ -114,7 +114,7 @@ public sealed class Boyfriend } ).ConfigureLogging( c => c.AddConsole() - .AddFile("Logs/Boyfriend-{Date}.log", + .AddFile("Logs/Octobot-{Date}.log", outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index b3288c0..a6b44dd 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -1,7 +1,7 @@ -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Octobot.Data; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Gateway.Events; @@ -9,7 +9,7 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; -namespace Boyfriend.Responders; +namespace Octobot.Responders; /// /// Handles sending a message to a guild that has just initialized if that guild @@ -85,7 +85,7 @@ public class GuildLoadedResponder : IResponder var i = Random.Shared.Next(1, 4); var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) - .WithTitle($"Beep{i}".Localized()) + .WithTitle($"Sound{i}".Localized()) .WithDescription(Messages.Ready) .WithCurrentTimestamp() .WithColour(ColorsList.Blue) diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 849ff5e..57bd01f 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -1,7 +1,7 @@ using System.Text.Json.Nodes; -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; @@ -9,7 +9,7 @@ using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Responders; +namespace Octobot.Responders; /// /// Handles sending a guild's if one is set. @@ -78,7 +78,7 @@ public class GuildMemberJoinedResponder : IResponder return (Result)await _channelApi.CreateMessageAsync( GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); + allowedMentions: Octobot.NoMentions, ct: ct); } private async Task TryReturnRolesAsync( diff --git a/src/Responders/GuildMemberRolesUpdatedResponder.cs b/src/Responders/GuildMemberRolesUpdatedResponder.cs index eade781..b883c89 100644 --- a/src/Responders/GuildMemberRolesUpdatedResponder.cs +++ b/src/Responders/GuildMemberRolesUpdatedResponder.cs @@ -1,11 +1,11 @@ -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Gateway.Responders; using Remora.Results; -namespace Boyfriend.Responders; +namespace Octobot.Responders; /// /// Handles updating when a guild member is updated. diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 8ae2418..ae92770 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -1,7 +1,7 @@ using System.Text; -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; @@ -10,7 +10,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Discord.Gateway.Responders; using Remora.Results; -namespace Boyfriend.Responders; +namespace Octobot.Responders; /// /// Handles logging the contents of a deleted message and the user who deleted the message @@ -104,6 +104,6 @@ public class MessageDeletedResponder : IResponder return (Result)await _channelApi.CreateMessageAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); + allowedMentions: Octobot.NoMentions, ct: ct); } } diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index d7e2347..5e2084c 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -1,8 +1,8 @@ using System.Text; -using Boyfriend.Data; -using Boyfriend.Services; using DiffPlex.DiffBuilder; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; @@ -12,7 +12,7 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; -namespace Boyfriend.Responders; +namespace Octobot.Responders; /// /// Handles logging the difference between an edited message's old and new content @@ -112,6 +112,6 @@ public class MessageEditedResponder : IResponder return (Result)await _channelApi.CreateMessageAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); + allowedMentions: Octobot.NoMentions, ct: ct); } } diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs index cadef5f..f5c65f4 100644 --- a/src/Responders/MessageReceivedResponder.cs +++ b/src/Responders/MessageReceivedResponder.cs @@ -5,7 +5,7 @@ using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Responders; +namespace Octobot.Responders; /// /// Handles sending replies to easter egg messages. diff --git a/src/Responders/ScheduledEventCreatedResponder.cs b/src/Responders/ScheduledEventCreatedResponder.cs index 3541237..4d87b6c 100644 --- a/src/Responders/ScheduledEventCreatedResponder.cs +++ b/src/Responders/ScheduledEventCreatedResponder.cs @@ -1,11 +1,11 @@ -using Boyfriend.Data; -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Gateway.Responders; using Remora.Results; -namespace Boyfriend.Responders; +namespace Octobot.Responders; /// /// Handles adding a scheduled event to a guild's ScheduledEventData. diff --git a/src/Responders/ScheduledEventUpdatedResponder.cs b/src/Responders/ScheduledEventUpdatedResponder.cs index 7db7edd..85162a4 100644 --- a/src/Responders/ScheduledEventUpdatedResponder.cs +++ b/src/Responders/ScheduledEventUpdatedResponder.cs @@ -1,10 +1,10 @@ -using Boyfriend.Services; using JetBrains.Annotations; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Gateway.Responders; using Remora.Results; -namespace Boyfriend.Responders; +namespace Octobot.Responders; [UsedImplicitly] public class ScheduledEventUpdatedResponder : IResponder diff --git a/src/Services/BackgroundGuildDataSaverService.cs b/src/Services/BackgroundGuildDataSaverService.cs index 76a7ddb..766ffe0 100644 --- a/src/Services/BackgroundGuildDataSaverService.cs +++ b/src/Services/BackgroundGuildDataSaverService.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Hosting; -namespace Boyfriend.Services; +namespace Octobot.Services; public sealed class BackgroundGuildDataSaverService : BackgroundService { diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 0833adf..5d2b1b1 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -1,13 +1,13 @@ using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Nodes; -using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Data; using Remora.Discord.API.Abstractions.Rest; using Remora.Rest.Core; -namespace Boyfriend.Services; +namespace Octobot.Services; /// /// Handles saving, loading, initializing and providing . diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 3db307d..7cd13fe 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; -using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Data; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; @@ -9,7 +9,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Services.Update; +namespace Octobot.Services.Update; public sealed partial class MemberUpdateService : BackgroundService { diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index de03fc5..85d532a 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -1,7 +1,7 @@ using System.Text.Json.Nodes; -using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Data; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; @@ -11,7 +11,7 @@ using Remora.Discord.Interactivity; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Services.Update; +namespace Octobot.Services.Update; public sealed class ScheduledEventUpdateService : BackgroundService { diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index 70a2d70..5102fbf 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -4,7 +4,7 @@ using Remora.Discord.API.Gateway.Commands; using Remora.Discord.API.Objects; using Remora.Discord.Gateway; -namespace Boyfriend.Services.Update; +namespace Octobot.Services.Update; public sealed class SongUpdateService : BackgroundService { diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 35a11dd..bc4ce35 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -1,8 +1,8 @@ using System.Drawing; using System.Text; using System.Text.Json.Nodes; -using Boyfriend.Data; using Microsoft.Extensions.Hosting; +using Octobot.Data; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; @@ -10,7 +10,7 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -namespace Boyfriend.Services; +namespace Octobot.Services; /// /// Provides utility methods that cannot be transformed to extension methods because they require usage From 5d278883d55e70f18219a3f77a1df6db1bd234ef Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 30 Sep 2023 20:25:22 +0500 Subject: [PATCH 164/329] Synchronize scheduled events on sch. events tick (#129) Because the Discord API sucks at providing data when needed, this PR makes it so that events are synchronized every tick instead of on gateway events. This fixes an issue where the bot won't know about a scheduled event if it was created before the bot was started --- src/Responders/GuildLoadedResponder.cs | 2 -- .../ScheduledEventCreatedResponder.cs | 32 ------------------- .../Update/ScheduledEventUpdateService.cs | 17 +++++++++- 3 files changed, 16 insertions(+), 35 deletions(-) delete mode 100644 src/Responders/ScheduledEventCreatedResponder.cs diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index a6b44dd..cfc33fa 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -54,8 +54,6 @@ public class GuildLoadedResponder : IResponder { if (!data.ScheduledEvents.TryGetValue(schEvent.ID.Value, out var eventData)) { - data.ScheduledEvents.Add(schEvent.ID.Value, new ScheduledEventData(schEvent.ID.Value, - schEvent.Name, schEvent.ScheduledStartTime, schEvent.Status)); continue; } diff --git a/src/Responders/ScheduledEventCreatedResponder.cs b/src/Responders/ScheduledEventCreatedResponder.cs deleted file mode 100644 index 4d87b6c..0000000 --- a/src/Responders/ScheduledEventCreatedResponder.cs +++ /dev/null @@ -1,32 +0,0 @@ -using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Services; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.Gateway.Responders; -using Remora.Results; - -namespace Octobot.Responders; - -/// -/// Handles adding a scheduled event to a guild's ScheduledEventData. -/// -[UsedImplicitly] -public class ScheduledEventCreatedResponder : IResponder -{ - private readonly GuildDataService _guildData; - - public ScheduledEventCreatedResponder(GuildDataService guildData) - { - _guildData = guildData; - } - - public async Task RespondAsync(IGuildScheduledEventCreate gatewayEvent, CancellationToken ct = default) - { - var data = await _guildData.GetData(gatewayEvent.GuildID, ct); - data.ScheduledEvents.Add(gatewayEvent.ID.Value, - new ScheduledEventData(gatewayEvent.ID.Value, - gatewayEvent.Name, gatewayEvent.ScheduledStartTime, gatewayEvent.Status)); - - return Result.FromSuccess(); - } -} diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 85d532a..2a8beca 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -74,7 +74,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (!storedEvent.ScheduleOnStatusUpdated) { - var tickResult = await TickScheduledEventAsync(guildId, data, scheduledEvent.Entity, storedEvent, ct); + var tickResult = + await TickScheduledEventAsync(guildId, data, scheduledEvent.Entity, storedEvent, ct); failedResults.AddIfFailed(tickResult); continue; } @@ -99,9 +100,23 @@ public sealed class ScheduledEventUpdateService : BackgroundService failedResults.AddIfFailed(statusUpdatedResponseResult); } + SyncScheduledEvents(data, events); + return failedResults.AggregateErrors(); } + private static void SyncScheduledEvents(GuildData data, IReadOnlyCollection events) + { + if (data.ScheduledEvents.Count < events.Count) + { + foreach (var @event in events.Where(@event => !data.ScheduledEvents.ContainsKey(@event.ID.Value))) + { + data.ScheduledEvents.Add(@event.ID.Value, new ScheduledEventData(@event.ID.Value, + @event.Name, @event.ScheduledStartTime, @event.Status)); + } + } + } + private static Result TryGetScheduledEvent(IEnumerable from, ulong id) { var filtered = from.Where(schEvent => schEvent.ID == id); From d713b977f04751d3a6982cebfafc517463ae2b2d Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 30 Sep 2023 20:36:55 +0500 Subject: [PATCH 165/329] Synchronize roles only on member data updates (#130) This PR makes it so that roles in MemberData are updated only in MemberUpdateService. This reduces possible points of failures, maintenance burden and reliance on gateway events --- .../GuildMemberRolesUpdatedResponder.cs | 33 ------------------- src/Services/GuildDataService.cs | 19 +---------- src/Services/Update/MemberUpdateService.cs | 5 +++ 3 files changed, 6 insertions(+), 51 deletions(-) delete mode 100644 src/Responders/GuildMemberRolesUpdatedResponder.cs diff --git a/src/Responders/GuildMemberRolesUpdatedResponder.cs b/src/Responders/GuildMemberRolesUpdatedResponder.cs deleted file mode 100644 index b883c89..0000000 --- a/src/Responders/GuildMemberRolesUpdatedResponder.cs +++ /dev/null @@ -1,33 +0,0 @@ -using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Services; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.Gateway.Responders; -using Remora.Results; - -namespace Octobot.Responders; - -/// -/// Handles updating when a guild member is updated. -/// -[UsedImplicitly] -public class GuildMemberUpdateResponder : IResponder -{ - private readonly GuildDataService _guildData; - - public GuildMemberUpdateResponder(GuildDataService guildData) - { - _guildData = guildData; - } - - public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) - { - var memberData = await _guildData.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); - if (memberData.MutedUntil is null) - { - memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value); - } - - return Result.FromSuccess(); - } -} diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 5d2b1b1..f5c7faa 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -4,7 +4,6 @@ using System.Text.Json.Nodes; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Octobot.Data; -using Remora.Discord.API.Abstractions.Rest; using Remora.Rest.Core; namespace Octobot.Services; @@ -15,14 +14,12 @@ namespace Octobot.Services; public sealed class GuildDataService : IHostedService { private readonly ConcurrentDictionary _datas = new(); - private readonly IDiscordRestGuildAPI _guildApi; private readonly ILogger _logger; // https://github.com/dotnet/aspnetcore/issues/39139 public GuildDataService( - IHostApplicationLifetime lifetime, IDiscordRestGuildAPI guildApi, ILogger logger) + IHostApplicationLifetime lifetime, ILogger logger) { - _guildApi = guildApi; _logger = logger; lifetime.ApplicationStopping.Register(ApplicationStopping); } @@ -110,15 +107,6 @@ public sealed class GuildDataService : IHostedService continue; } - if (data.MutedUntil is null) - { - var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct); - if (memberResult.IsSuccess) - { - data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value); - } - } - memberData.Add(data.Id, data); } @@ -150,11 +138,6 @@ public sealed class GuildDataService : IHostedService return (await GetData(guildId, ct)).Settings; } - public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) - { - return (await GetData(guildId, ct)).GetOrCreateMemberData(userId); - } - public ICollection GetGuildIds() { return _datas.Keys; diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 7cd13fe..1d2b7f6 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -89,6 +89,11 @@ public sealed partial class MemberUpdateService : BackgroundService return failedResults.AggregateErrors(); } + if (data.MutedUntil is null) + { + data.Roles = guildMember.Roles.ToList().ConvertAll(r => r.Value); + } + var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct); failedResults.AddIfFailed(autoUnmuteResult); From e073c5a5720955fe2aa2a93ae2051f878be9a874 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 30 Sep 2023 20:38:52 +0500 Subject: [PATCH 166/329] Synchronize events only on sch. event updates (#131) This PR moves all code related to synchronization of scheduled events to ScheduledEventUpdateService. Just like #130, this reduces possible points of failures, maintenance burden and reliance on gateway events --- src/Responders/GuildLoadedResponder.cs | 13 -------- .../ScheduledEventUpdatedResponder.cs | 30 ------------------- .../Update/ScheduledEventUpdateService.cs | 21 ++++++++----- 3 files changed, 14 insertions(+), 50 deletions(-) delete mode 100644 src/Responders/ScheduledEventUpdatedResponder.cs diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index cfc33fa..92fc009 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -50,19 +50,6 @@ public class GuildLoadedResponder : IResponder data.GetOrCreateMemberData(member.User.Value.ID); } - foreach (var schEvent in guild.GuildScheduledEvents) - { - if (!data.ScheduledEvents.TryGetValue(schEvent.ID.Value, out var eventData)) - { - continue; - } - - eventData.Name = schEvent.Name; - eventData.ScheduledStartTime = schEvent.ScheduledStartTime; - eventData.ScheduleOnStatusUpdated = eventData.Status != schEvent.Status; - eventData.Status = schEvent.Status; - } - if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) { return Result.FromSuccess(); diff --git a/src/Responders/ScheduledEventUpdatedResponder.cs b/src/Responders/ScheduledEventUpdatedResponder.cs deleted file mode 100644 index 85162a4..0000000 --- a/src/Responders/ScheduledEventUpdatedResponder.cs +++ /dev/null @@ -1,30 +0,0 @@ -using JetBrains.Annotations; -using Octobot.Services; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.Gateway.Responders; -using Remora.Results; - -namespace Octobot.Responders; - -[UsedImplicitly] -public class ScheduledEventUpdatedResponder : IResponder -{ - private readonly GuildDataService _guildData; - - public ScheduledEventUpdatedResponder(GuildDataService guildData) - { - _guildData = guildData; - } - - public async Task RespondAsync(IGuildScheduledEventUpdate gatewayEvent, CancellationToken ct = default) - { - var data = await _guildData.GetData(gatewayEvent.GuildID, ct); - var eventData = data.ScheduledEvents[gatewayEvent.ID.Value]; - eventData.Name = gatewayEvent.Name; - eventData.ScheduledStartTime = gatewayEvent.ScheduledStartTime; - eventData.ScheduleOnStatusUpdated = eventData.Status != gatewayEvent.Status; - eventData.Status = gatewayEvent.Status; - - return Result.FromSuccess(); - } -} diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 2a8beca..f83c1de 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -61,6 +61,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromError(eventsResult); } + SyncScheduledEvents(data, events); + foreach (var storedEvent in data.ScheduledEvents.Values) { var scheduledEvent = TryGetScheduledEvent(events, storedEvent.Id); @@ -100,20 +102,25 @@ public sealed class ScheduledEventUpdateService : BackgroundService failedResults.AddIfFailed(statusUpdatedResponseResult); } - SyncScheduledEvents(data, events); - return failedResults.AggregateErrors(); } - private static void SyncScheduledEvents(GuildData data, IReadOnlyCollection events) + private static void SyncScheduledEvents(GuildData data, IEnumerable events) { - if (data.ScheduledEvents.Count < events.Count) + foreach (var @event in events) { - foreach (var @event in events.Where(@event => !data.ScheduledEvents.ContainsKey(@event.ID.Value))) + if (!data.ScheduledEvents.ContainsKey(@event.ID.Value)) { - data.ScheduledEvents.Add(@event.ID.Value, new ScheduledEventData(@event.ID.Value, - @event.Name, @event.ScheduledStartTime, @event.Status)); + data.ScheduledEvents.Add(@event.ID.Value, + new ScheduledEventData(@event.ID.Value, @event.Name, @event.ScheduledStartTime, @event.Status)); + continue; } + + var eventData = data.ScheduledEvents[@event.ID.Value]; + eventData.Name = @event.Name; + eventData.ScheduledStartTime = @event.ScheduledStartTime; + eventData.ScheduleOnStatusUpdated = eventData.Status != @event.Status; + eventData.Status = @event.Status; } } From f3da876d6d8317537c55ab4a8819ca514710546a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 30 Sep 2023 20:41:46 +0500 Subject: [PATCH 167/329] Run scheduled event updates synchronously to avoid a rate limit error (#132) This PR attempts to fix this error: https://paste.gg/p/anonymous/358d5b1b80b44011b7afe5007270b175 based on the assumption that the error is caused by a race condition affecting the rate limit tracking code in Remora.Discord --- src/Services/Update/ScheduledEventUpdateService.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index f83c1de..f9aee51 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -34,20 +34,15 @@ public sealed class ScheduledEventUpdateService : BackgroundService protected override async Task ExecuteAsync(CancellationToken ct) { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); - var tasks = new List(); while (await timer.WaitForNextTickAsync(ct)) { var guildIds = _guildData.GetGuildIds(); - - tasks.AddRange(guildIds.Select(async id => + foreach (var id in guildIds) { var tickResult = await TickScheduledEventsAsync(id, ct); _logger.LogResult(tickResult, $"Error in scheduled events update for guild {id}."); - })); - - await Task.WhenAll(tasks); - tasks.Clear(); + } } } From e283ba5a6d2c226acabd6d7f45a165b2008945cf Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 30 Sep 2023 20:50:58 +0500 Subject: [PATCH 168/329] Don't forget to remove scheduled event data when notification channel is not set (#133) This PR fixes an issue that caused data of completed/cancelled scheduled events to never be collected if EventNotificationChannel wasn't set --- src/Services/Update/ScheduledEventUpdateService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index f9aee51..97f9329 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -317,6 +317,12 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data, CancellationToken ct) { + if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + { + data.ScheduledEvents.Remove(eventData.Id); + return Result.FromSuccess(); + } + var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, eventData.Name)) .WithDescription( string.Format( @@ -349,6 +355,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { + data.ScheduledEvents.Remove(eventData.Id); return Result.FromSuccess(); } From 6247a55a35f5ec1d971e40ea10b7019a3ab02d96 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 30 Sep 2023 20:53:05 +0500 Subject: [PATCH 169/329] Use Snowflake#Empty extension method instead of '== 0' checks (#134) --- src/Commands/MuteCommandGroup.cs | 2 +- src/Services/Update/MemberUpdateService.cs | 2 +- src/Services/UtilityService.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 33e8a9f..d558ba2 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -101,7 +101,7 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } - if (GuildSettings.MuteRole.Get(data.Settings) != 0) + if (!GuildSettings.MuteRole.Get(data.Settings).Empty()) { return await RoleMuteUserAsync( target, reason, duration, guildId, data, channelId, user, currentUser, CancellationToken); diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 1d2b7f6..22b2849 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -97,7 +97,7 @@ public sealed partial class MemberUpdateService : BackgroundService var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct); failedResults.AddIfFailed(autoUnmuteResult); - if (defaultRole.Value is not 0 && !data.Roles.Contains(defaultRole.Value)) + if (!defaultRole.Empty() && !data.Roles.Contains(defaultRole.Value)) { var addResult = await _guildApi.AddGuildMemberRoleAsync( guildId, id, defaultRole, ct: ct); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index bc4ce35..4bd9add 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -174,7 +174,7 @@ public sealed class UtilityService : IHostedService return Result.FromError(usersResult); } - if (role.Value is not 0) + if (!role.Empty()) { builder.Append($"{Mention.Role(role)} "); } From 9323e891a12e84e756d3ebd7d107fb885767c303 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 1 Oct 2023 12:04:00 +0500 Subject: [PATCH 170/329] Remove all non-Splatoon songs (#137) --- src/Services/Update/SongUpdateService.cs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index 5102fbf..27cfa3c 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -10,29 +10,14 @@ public sealed class SongUpdateService : BackgroundService { private static readonly (string Name, TimeSpan Duration)[] SongList = { - ("UNDEAD CORPORATION - The Empress", new TimeSpan(0, 4, 34)), - ("UNDEAD CORPORATION - Everything will freeze", new TimeSpan(0, 3, 17)), - ("Yoko & the Gold Bazookas - Rockagilly Blues ", new TimeSpan(0, 3, 37)), + ("Yoko & the Gold Bazookas - Rockagilly Blues", new TimeSpan(0, 3, 37)), ("Splatoon 3 - Seep and Destroy", new TimeSpan(0, 2, 42)), - ("IA - A Tale of Six Trillion Years and a Night", new TimeSpan(0, 3, 40)), - ("Manuel - Gas Gas Gas", new TimeSpan(0, 3, 17)), - ("Camellia - Flamewall", new TimeSpan(0, 6, 50)), - ("Jukio Kallio, Daniel Hagström - Fall 'n' Roll", new TimeSpan(0, 3, 14)), - ("SCATTLE - Hypertension", new TimeSpan(0, 3, 18)), - ("KEYGEN CHURCH - Tenebre Rosso Sangue", new TimeSpan(0, 3, 53)), - ("Chipzel - Swing Me Another 6", new TimeSpan(0, 5, 32)), - ("Noisecream - Mist of Rage", new TimeSpan(0, 2, 25)), - ("EDWXRDX - CONSCIENCE", new TimeSpan(0, 2, 16)), - ("dontleaveme - afterward", new TimeSpan(0, 2, 29)), - ("Ferdous - Gravity", new TimeSpan(0, 2, 38)), - ("The Drums - Money", new TimeSpan(0, 3, 53)), - ("Derek Pope - War Machine", new TimeSpan(0, 3, 39)), ("Deep Cut - Big Betrayal", new TimeSpan(0, 1, 42)), ("Squid Sisters - Tomorrow's Nostalgia Today", new TimeSpan(0, 2, 8)), ("Deep Cut - Anarchy Rainbow", new TimeSpan(0, 1, 51)), ("Squid Sisters feat. Ian BGM - Liquid Sunshine", new TimeSpan(0, 1, 32)), ("Damp Socks feat. Off the Hook - Candy-Coated Rocks", new TimeSpan(0, 1, 11)), - ("H2Whoa - Aquasonic", new TimeSpan(0, 1, 1)), // Add some Splatoon™ songs that *I* liked #125 + ("H2Whoa - Aquasonic", new TimeSpan(0, 1, 1)), ("Yoko & the Gold Bazookas - Ska-Blam!", new TimeSpan(0, 4, 4)) }; From 67ef6b7209e0941abaf14885abb23567effd0c97 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Sun, 1 Oct 2023 10:06:50 +0300 Subject: [PATCH 171/329] Make README slightly better (#136) Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- docs/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/README.md b/docs/README.md index e9ada97..7d899f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,9 +2,10 @@ Octobot banner

-![License](https://img.shields.io/github/license/LabsDevelopment/Octobot) -![Workflow Status](https://img.shields.io/github/actions/workflow/status/LabsDevelopment/Octobot/.github/workflows/build-push.yml?branch=master&logo=ReSharper) -![Last Commit](https://img.shields.io/github/last-commit/LabsDevelopment/Octobot) + + + +
Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Labs Development Team](https://github.com/LabsDevelopment) in C# and Remora.Discord @@ -13,14 +14,13 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr * Banning, muting, kicking, etc. * Reminding you about something if you wish * Reminding everyone about that new event you made +* Renaming those annoying self-hoisting members * Log everything from joining the server to deleting messages +* Listen to music! -*...and more!* +*...a-a-and more!* -## Installing and running Octobot - -You can read our [wiki](https://github.com/LabsDevelopment/Octobot/wiki) in order to assemble your Octobot and -moderate the server. +[//]: # (if you are reading this, message @mctaylors and ask him to bring back the wiki) ## Contributing From 186eb65eb1ac5d14c71aa38d558793e1fbf1f2c8 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Sun, 1 Oct 2023 10:57:25 +0300 Subject: [PATCH 172/329] =?UTF-8?q?Add=20MORE=20Splatoon=E2=84=A2=20songs?= =?UTF-8?q?=20that=20I=20liked=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Song list: - Off the Hook - Muck Warfare — [source](https://youtu.be/Se3AuyS-gTo) - Off the Hook - Acid Hues — [source](https://youtu.be/RoBhJth2VZE) - Off the Hook - Shark Bytes — [source](https://youtu.be/YrGSrxDRAXU) - DJ Octavio feat. Squid Sisters & Deep Cut - Calamari Inkantation 3MIX — [source](https://youtu.be/_TE22RCxCi4) - Splatoon - Ink Me Up — [source](https://www.youtube.com/watch?v=LrsAKUnb3Qg) --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- src/Services/Update/SongUpdateService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index 27cfa3c..c47055e 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -18,7 +18,12 @@ public sealed class SongUpdateService : BackgroundService ("Squid Sisters feat. Ian BGM - Liquid Sunshine", new TimeSpan(0, 1, 32)), ("Damp Socks feat. Off the Hook - Candy-Coated Rocks", new TimeSpan(0, 1, 11)), ("H2Whoa - Aquasonic", new TimeSpan(0, 1, 1)), - ("Yoko & the Gold Bazookas - Ska-Blam!", new TimeSpan(0, 4, 4)) + ("Yoko & the Gold Bazookas - Ska-Blam!", new TimeSpan(0, 4, 4)), + ("Off the Hook - Muck Warfare", new TimeSpan(0, 3, 39)), + ("Off the Hook - Acid Hues", new TimeSpan(0, 3, 39)), + ("Off the Hook - Shark Bytes", new TimeSpan(0, 3, 48)), + ("DJ Octavio feat. Squid Sisters & Deep Cut - Calamari Inkantation", new TimeSpan(0, 7, 9)), + ("Splatoon - Ink Me Up", new TimeSpan(0, 2, 13)) }; private readonly List _activityList = new(1) From d837745b11a3d87edaad78a6bcaf29341668bd66 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:41:43 +0300 Subject: [PATCH 173/329] Add string interpolation for SongUpdateService (#139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I added a string interpolation for SongUpdateService for better activity status configuration. So now you can add some characters if you want, or even swap the song title and author. Also songs are now displaying in Splatoon™ style. --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- src/Services/Update/SongUpdateService.cs | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index c47055e..f1ef296 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -8,22 +8,22 @@ namespace Octobot.Services.Update; public sealed class SongUpdateService : BackgroundService { - private static readonly (string Name, TimeSpan Duration)[] SongList = + private static readonly (string Author, string Name, TimeSpan Duration)[] SongList = { - ("Yoko & the Gold Bazookas - Rockagilly Blues", new TimeSpan(0, 3, 37)), - ("Splatoon 3 - Seep and Destroy", new TimeSpan(0, 2, 42)), - ("Deep Cut - Big Betrayal", new TimeSpan(0, 1, 42)), - ("Squid Sisters - Tomorrow's Nostalgia Today", new TimeSpan(0, 2, 8)), - ("Deep Cut - Anarchy Rainbow", new TimeSpan(0, 1, 51)), - ("Squid Sisters feat. Ian BGM - Liquid Sunshine", new TimeSpan(0, 1, 32)), - ("Damp Socks feat. Off the Hook - Candy-Coated Rocks", new TimeSpan(0, 1, 11)), - ("H2Whoa - Aquasonic", new TimeSpan(0, 1, 1)), - ("Yoko & the Gold Bazookas - Ska-Blam!", new TimeSpan(0, 4, 4)), - ("Off the Hook - Muck Warfare", new TimeSpan(0, 3, 39)), - ("Off the Hook - Acid Hues", new TimeSpan(0, 3, 39)), - ("Off the Hook - Shark Bytes", new TimeSpan(0, 3, 48)), - ("DJ Octavio feat. Squid Sisters & Deep Cut - Calamari Inkantation", new TimeSpan(0, 7, 9)), - ("Splatoon - Ink Me Up", new TimeSpan(0, 2, 13)) + ("Yoko & the Gold Bazookas", "Rockagilly Blues", new TimeSpan(0, 3, 37)), + ("Splatoon 3", "Seep and Destroy", new TimeSpan(0, 2, 42)), + ("Deep Cut", "Big Betrayal", new TimeSpan(0, 1, 42)), + ("Squid Sisters", "Tomorrow's Nostalgia Today", new TimeSpan(0, 2, 8)), + ("Deep Cut", "Anarchy Rainbow", new TimeSpan(0, 1, 51)), + ("Squid Sisters feat. Ian BGM", "Liquid Sunshine", new TimeSpan(0, 1, 32)), + ("Damp Socks feat. Off the Hook", "Candy-Coated Rocks", new TimeSpan(0, 1, 11)), + ("H2Whoa", "Aquasonic", new TimeSpan(0, 1, 1)), + ("Yoko & the Gold Bazookas", "Ska-BLAM", new TimeSpan(0, 4, 4)), + ("Off the Hook", "Muck Warfare", new TimeSpan(0, 3, 39)), + ("Off the Hook", "Acid Hues", new TimeSpan(0, 3, 39)), + ("Off the Hook", "Shark Bytes", new TimeSpan(0, 3, 48)), + ("DJ Octavio feat. Squid Sisters & Deep Cut", "Calamari Inkantation 3MIX", new TimeSpan(0, 7, 9)), + ("Squid Sisters", "Ink Me Up", new TimeSpan(0, 2, 13)) }; private readonly List _activityList = new(1) @@ -52,7 +52,8 @@ public sealed class SongUpdateService : BackgroundService while (!ct.IsCancellationRequested) { var nextSong = SongList[_nextSongIndex]; - _activityList[0] = new Activity(nextSong.Name, ActivityType.Listening); + _activityList[0] = new Activity($"{nextSong.Name} / {nextSong.Author}", + ActivityType.Listening); _client.SubmitCommand( new UpdatePresence( UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList)); From 74a287ba72a34a04c966ceba01119fda1bebb3e6 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 3 Oct 2023 16:55:58 +0500 Subject: [PATCH 174/329] fix(i18n): [RU] do not use colloquialisms in ReminderText (#141) --- locale/Messages.ru.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 3e31f8f..8489a45 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -604,7 +604,7 @@ Напоминание будет отправлено: {0} - Текст напоминалки: {0} + Текст напоминания: {0} Отображаемое имя From 413b8a4781724dc008e1ee6db691106c19712dac Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Tue, 3 Oct 2023 15:07:41 +0300 Subject: [PATCH 175/329] Add /timestamp (#140) Original idea from @Octol1ttle --------- Signed-off-by: Macintosh II --- locale/Messages.resx | 6 +++ locale/Messages.ru.resx | 6 +++ locale/Messages.tt-ru.resx | 6 +++ src/Commands/ToolsCommandGroup.cs | 72 ++++++++++++++++++++++++++++++- src/Messages.Designer.cs | 16 +++++++ 5 files changed, 105 insertions(+), 1 deletion(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 7b9a471..cf6f89d 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -654,4 +654,10 @@ Your random number is: + + Timestamp for {0}: + + + Offset: {0} + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 8489a45..aff4dd3 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -654,4 +654,10 @@ Ваше случайное число: + + Временная метка для {0}: + + + Офсет: {0} + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index df98622..7c65929 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -654,4 +654,10 @@ ваше рандомное число: + + таймштамп для {0}: + + + офсет: {0} + diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 64e6729..6abb918 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -19,7 +19,7 @@ using Remora.Results; namespace Octobot.Commands; /// -/// Handles tool commands: /showinfo, /random. +/// Handles tool commands: /showinfo, /random, /timestamp. /// [UsedImplicitly] public class ToolsCommandGroup : CommandGroup @@ -289,4 +289,74 @@ public class ToolsCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct); } + + private static readonly TimestampStyle[] AllStyles = + { + TimestampStyle.ShortDate, + TimestampStyle.LongDate, + TimestampStyle.ShortTime, + TimestampStyle.LongTime, + TimestampStyle.ShortDateTime, + TimestampStyle.LongDateTime, + TimestampStyle.RelativeTime + }; + + /// + /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. + /// + /// The offset for the current timestamp. + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("timestamp")] + [DiscordDefaultDMPermission(false)] + [Description("Shows a timestamp in all styles")] + [UsedImplicitly] + public async Task ExecuteTimestampAsync( + [Description("Offset from current time")] + TimeSpan? offset = null) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var userResult = await _userApi.GetUserAsync(userId, CancellationToken); + if (!userResult.IsDefined(out var user)) + { + return Result.FromError(userResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await SendTimestampAsync(offset, user, CancellationToken); + } + + private async Task SendTimestampAsync(TimeSpan? offset, IUser user, CancellationToken ct) + { + var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds(); + + var description = new StringBuilder().Append("# ").AppendLine(timestamp.ToString()); + + if (offset is not null) + { + description.AppendLine(string.Format( + Messages.TimestampOffset, Markdown.InlineCode(offset.ToString() ?? string.Empty))).AppendLine(); + } + + foreach (var markdownTimestamp in AllStyles.Select(style => Markdown.Timestamp(timestamp, style))) + { + description.Append("- ").Append(Markdown.InlineCode(markdownTimestamp)) + .Append(" → ").AppendLine(markdownTimestamp); + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.TimestampTitle, user.GetTag()), user) + .WithDescription(description.ToString()) + .WithColour(ColorsList.Blue) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 3c2c747..549a681 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1135,5 +1135,21 @@ namespace Octobot { return ResourceManager.GetString("RandomOutput", resourceCulture); } } + + internal static string TimestampTitle + { + get + { + return ResourceManager.GetString("TimestampTitle", resourceCulture); + } + } + + internal static string TimestampOffset + { + get + { + return ResourceManager.GetString("TimestampOffset", resourceCulture); + } + } } } From bae92fc84ba83d4e95bcc8b713b5eb8d99022896 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 3 Oct 2023 18:27:27 +0500 Subject: [PATCH 176/329] Remove unused messages (#144) --- locale/Messages.resx | 123 ------------------- locale/Messages.ru.resx | 123 ------------------- locale/Messages.tt-ru.resx | 123 ------------------- src/Messages.Designer.cs | 240 ------------------------------------- 4 files changed, 609 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index cf6f89d..b03c9f1 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -111,9 +111,6 @@ Deleted message by {0}: - - Cleared message from {0} in channel {1}: {2} - Edited message by {0}: @@ -129,45 +126,24 @@ Ngyes! - - I do not have permission to execute this command! - - - You do not have permission to execute this command! - You were banned Punishment expired - - You specified less than {0} messages! - - - You specified more than {0} messages! - - - Command help: - You were kicked ms - - Member is already muted! - Not specified Not specified - - Current settings: - Language @@ -201,36 +177,21 @@ Welcome message - - You need to specify an integer from {0} to {1} instead of {2}! - {0} was banned - - That setting doesn't exist! - Receive startup messages Invalid setting value specified! - - This role does not exist! - - - This channel does not exist! - 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 I cannot use time-outs on other bots! Try to set a mute role in settings - - {0} has created event {1}! It will take place in {2} and will start <t:{3}:R>! \n {4} - Role for event creation notifications @@ -243,84 +204,24 @@ Event "{0}" started - - :( - Event "{0}" is cancelled! Event "{0}" has completed! - - ever - Cleared {0} messages - - Kicked {0}: {1} - - - Muted {0} for{1}: {2} - - - Unbanned {0}: {1} - - - Unmuted {0}: {1} - Nothing changed! `{0}` is already set to {1} Not specified - - Value of setting `{0}` is now set to {1} - - - Bans a user - - - Deletes a specified amount of messages in this channel - - - Shows this message - - - Kicks a member - - - Mutes a member - - - Shows (inaccurate) latency - - - Allows you to change certain preferences for this guild - - - Unbans a user - - - Unmutes a member - - - 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 member of this guild! - You cannot ban users from this guild! @@ -351,21 +252,6 @@ I cannot manage this guild! - - You need to specify a reason to ban this user! - - - You need to specify a reason to kick this member! - - - You need to specify a reason to mute this member! - - - You need to specify a reason to unban this user! - - - You need to specify a reason for unmute this member! - You cannot ban the owner of this guild! @@ -438,9 +324,6 @@ Default role - - Adds a reminder - Channel for public notifications @@ -453,12 +336,6 @@ Automatically start scheduled events - - You need to specify reminder text! - - - You need to specify when I should send you the reminder! - Issued by diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index aff4dd3..7e0d6cd 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -111,9 +111,6 @@ Сообщение {0} удалено: - - Очищено сообщение от {0} в канале {1}: {2} - Сообщение {0} отредактировано: @@ -129,42 +126,21 @@ Нгьес! - - У меня недостаточно прав для выполнения этой команды! - - - У тебя недостаточно прав для выполнения этой команды! - Время наказания истекло - - Указано менее {0} сообщений! - - - Указано более {0} сообщений! - - - Справка по командам: - Вы были выгнаны мс - - Участник уже заглушен! - Не указан Не указана - - Текущие настройки: - Язык @@ -198,36 +174,21 @@ Приветствие - - Надо указать целое число от {0} до {1} вместо {2}! - {0} был(-а) забанен(-а) - - Такая настройка не существует! - Получать сообщения о запуске Указано недействительное значение для настройки! - - Эта роль не существует! - - - Этот канал не существует! - Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках - - {0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4} - Роль для уведомлений о создании событий @@ -240,84 +201,24 @@ Событие "{0}" началось - - :( - Событие "{0}" отменено! Событие "{0}" завершено! - - всегда - Очищено {0} сообщений - - Выгнан {0}: {1} - - - Заглушен {0} на{1}: {2} - - - Возвращён из бана {0}: {1} - - - Разглушен {0}: {1} - Ничего не изменилось! Значение настройки `{0}` уже {1} Не указано - - Значение настройки `{0}` теперь установлено на {1} - - - Банит пользователя - - - Удаляет указанное количество сообщений в этом канале - - - Показывает эту справку - - - Выгоняет участника - - - Глушит участника - - - Показывает (неточную) задержку - - - Позволяет менять некоторые настройки под этот сервер - - - Возвращает пользователя из бана - - - Разглушает участника - - - Надо указать целое число от {0} до {1}! - Надо указать пользователя! - - Надо указать пользователя вместо {0}! - - - Надо указать участника сервера! - - - Надо указать участника этого сервера! - Ты не можешь банить пользователей на этом сервере! @@ -348,21 +249,6 @@ Я не могу настраивать этот сервер! - - Надо указать причину для бана этого участника! - - - Надо указать причину для кика этого участника! - - - Надо указать причину для мута этого участника! - - - Надо указать причину для разбана этого пользователя! - - - Надо указать причину для размута этого участника! - Ты не можешь меня забанить! @@ -435,9 +321,6 @@ Роль по умолчанию - - Добавляет напоминание - Канал для публичных уведомлений @@ -450,12 +333,6 @@ Автоматически начинать события - - Тебе нужно указать текст напоминания! - - - Нужно указать время, через которое придёт напоминание! - Ответственный diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 7c65929..b27104b 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -111,9 +111,6 @@ сообщение {0} вырезано: - - вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2} - сообщение {0} переделано: @@ -129,45 +126,24 @@ нгьес! - - у меня прав нету, сделай что нибудь. - - - у тебя прав нету, твои проблемы. - вы были забанены время бана закончиловсь - - ты выбрал менее {0} сообщений - - - ты выбрал более {0} сообщений - - - туториал по приколам: - вы были кикнуты мс - - шизоид уже замучен! - *тут ничего нет* нъет - - настройки: - язык @@ -201,36 +177,21 @@ здравствуйте (типо настройка) - - выбери число от {0} до {1} вместо {2}! - {0} забанен - - такой прикол не существует - получать инфу о старте бота криво настроил прикол, давай по новой - - этого звания нету, ты шо - - - этого канала нету, ты шо - ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим я не могу замутить ботов, сделай что нибудь - - {0} приготовил новую движуху {1}! она пройдёт в {2} и начнётся <t:{3}:R>!{4} - роль для уведомлений о создании движухи @@ -243,84 +204,24 @@ движуха "{0}" начинается - - оъмъомоъемъъео(((( - движуха "{0}" отменена! движуха "{0}" завершена! - - всегда - вырезано {0} забавных сообщений - - выгнан {0}: {1} - - - замучен {0} на{1}: {2} - - - раззабанен {0}: {1} - - - раззамучен {0}: {1} - ты все сломал! значение прикола `{0}` и так {1} нъет - - прикол для `{0}` теперь установлен на {1} - - - возводит великий банхаммер над шизоидом - - - удаляет сообщения. сколько хош, столько и удалит - - - показывает то, что ты сейчас видишь прямо сейчас - - - выпинывает шизоида - - - мутит шизоида - - - показывает пинг (сверхмегаточный (нет)) - - - настройки бота под этот сервер - - - отводит великий банхаммер от шизоида - - - раззамучивает шизоида - - - укажи целое число от {0} до {1} - укажи самого шизика - - надо указать юзверя вместо {0}! - - - укажи самого шизика - - - укажи шизоида сервера! - бан @@ -351,21 +252,6 @@ я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь. - - укажи зачем банить шизика - - - укажи зачем кикать шизика - - - укажи зачем мутить шизика - - - укажи зачем раззабанивать шизика - - - укажи зачам размучивать шизика - ээбля френдли фаер огонь по своим @@ -438,9 +324,6 @@ дефолтное звание - - крафтит напоминалку - канал для секретных уведомлений @@ -453,12 +336,6 @@ автоматом стартить движухи - - для крафта напоминалки нужен текст - - - шизоид у меня на часах такого нету - ответственный diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 549a681..aef4b7a 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -54,12 +54,6 @@ namespace Octobot { } } - internal static string CachedMessageCleared { - get { - return ResourceManager.GetString("CachedMessageCleared", resourceCulture); - } - } - internal static string CachedMessageEdited { get { return ResourceManager.GetString("CachedMessageEdited", resourceCulture); @@ -90,18 +84,6 @@ namespace Octobot { } } - internal static string CommandNoPermissionBot { - get { - return ResourceManager.GetString("CommandNoPermissionBot", resourceCulture); - } - } - - internal static string CommandNoPermissionUser { - get { - return ResourceManager.GetString("CommandNoPermissionUser", resourceCulture); - } - } - internal static string YouWereBanned { get { return ResourceManager.GetString("YouWereBanned", resourceCulture); @@ -114,24 +96,6 @@ namespace Octobot { } } - internal static string ClearAmountTooSmall { - get { - return ResourceManager.GetString("ClearAmountTooSmall", resourceCulture); - } - } - - internal static string ClearAmountTooLarge { - get { - return ResourceManager.GetString("ClearAmountTooLarge", resourceCulture); - } - } - - internal static string CommandHelp { - get { - return ResourceManager.GetString("CommandHelp", resourceCulture); - } - } - internal static string YouWereKicked { get { return ResourceManager.GetString("YouWereKicked", resourceCulture); @@ -144,12 +108,6 @@ namespace Octobot { } } - internal static string MemberAlreadyMuted { - get { - return ResourceManager.GetString("MemberAlreadyMuted", resourceCulture); - } - } - internal static string ChannelNotSpecified { get { return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); @@ -162,12 +120,6 @@ namespace Octobot { } } - internal static string CurrentSettings { - get { - return ResourceManager.GetString("CurrentSettings", resourceCulture); - } - } - internal static string SettingsLang { get { return ResourceManager.GetString("SettingsLang", resourceCulture); @@ -234,24 +186,12 @@ namespace Octobot { } } - internal static string ClearAmountInvalid { - get { - return ResourceManager.GetString("ClearAmountInvalid", resourceCulture); - } - } - internal static string UserBanned { get { return ResourceManager.GetString("UserBanned", resourceCulture); } } - internal static string SettingDoesntExist { - get { - return ResourceManager.GetString("SettingDoesntExist", resourceCulture); - } - } - internal static string SettingsReceiveStartupMessages { get { return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); @@ -288,12 +228,6 @@ namespace Octobot { } } - internal static string EventCreated { - get { - return ResourceManager.GetString("EventCreated", resourceCulture); - } - } - internal static string SettingsEventNotificationRole { get { return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); @@ -318,12 +252,6 @@ namespace Octobot { } } - internal static string SettingsFrowningFace { - get { - return ResourceManager.GetString("SettingsFrowningFace", resourceCulture); - } - } - internal static string EventCancelled { get { return ResourceManager.GetString("EventCancelled", resourceCulture); @@ -336,42 +264,12 @@ namespace Octobot { } } - internal static string Ever { - get { - return ResourceManager.GetString("Ever", resourceCulture); - } - } - internal static string MessagesCleared { get { return ResourceManager.GetString("MessagesCleared", resourceCulture); } } - internal static string FeedbackMemberKicked { - get { - return ResourceManager.GetString("FeedbackMemberKicked", resourceCulture); - } - } - - internal static string FeedbackMemberMuted { - get { - return ResourceManager.GetString("FeedbackMemberMuted", resourceCulture); - } - } - - internal static string FeedbackUserUnbanned { - get { - return ResourceManager.GetString("FeedbackUserUnbanned", resourceCulture); - } - } - - internal static string FeedbackMemberUnmuted { - get { - return ResourceManager.GetString("FeedbackMemberUnmuted", resourceCulture); - } - } - internal static string SettingsNothingChanged { get { return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); @@ -384,96 +282,6 @@ namespace Octobot { } } - internal static string FeedbackSettingsUpdated { - get { - return ResourceManager.GetString("FeedbackSettingsUpdated", resourceCulture); - } - } - - internal static string CommandDescriptionBan { - get { - return ResourceManager.GetString("CommandDescriptionBan", resourceCulture); - } - } - - internal static string CommandDescriptionClear { - get { - return ResourceManager.GetString("CommandDescriptionClear", resourceCulture); - } - } - - internal static string CommandDescriptionHelp { - get { - return ResourceManager.GetString("CommandDescriptionHelp", resourceCulture); - } - } - - internal static string CommandDescriptionKick { - get { - return ResourceManager.GetString("CommandDescriptionKick", resourceCulture); - } - } - - internal static string CommandDescriptionMute { - get { - return ResourceManager.GetString("CommandDescriptionMute", resourceCulture); - } - } - - internal static string CommandDescriptionPing { - get { - return ResourceManager.GetString("CommandDescriptionPing", resourceCulture); - } - } - - internal static string CommandDescriptionSettings { - get { - return ResourceManager.GetString("CommandDescriptionSettings", resourceCulture); - } - } - - internal static string CommandDescriptionUnban { - get { - return ResourceManager.GetString("CommandDescriptionUnban", resourceCulture); - } - } - - internal static string CommandDescriptionUnmute { - get { - return ResourceManager.GetString("CommandDescriptionUnmute", resourceCulture); - } - } - - internal static string MissingNumber { - get { - return ResourceManager.GetString("MissingNumber", resourceCulture); - } - } - - internal static string MissingUser { - get { - return ResourceManager.GetString("MissingUser", resourceCulture); - } - } - - internal static string InvalidUser { - get { - return ResourceManager.GetString("InvalidUser", resourceCulture); - } - } - - internal static string MissingMember { - get { - return ResourceManager.GetString("MissingMember", resourceCulture); - } - } - - internal static string InvalidMember { - get { - return ResourceManager.GetString("InvalidMember", resourceCulture); - } - } - internal static string UserCannotBanMembers { get { return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); @@ -534,36 +342,6 @@ namespace Octobot { } } - internal static string MissingBanReason { - get { - return ResourceManager.GetString("MissingBanReason", resourceCulture); - } - } - - internal static string MissingKickReason { - get { - return ResourceManager.GetString("MissingKickReason", resourceCulture); - } - } - - internal static string MissingMuteReason { - get { - return ResourceManager.GetString("MissingMuteReason", resourceCulture); - } - } - - internal static string MissingUnbanReason { - get { - return ResourceManager.GetString("MissingUnbanReason", resourceCulture); - } - } - - internal static string MissingUnmuteReason { - get { - return ResourceManager.GetString("MissingUnmuteReason", resourceCulture); - } - } - internal static string UserCannotBanOwner { get { return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); @@ -708,12 +486,6 @@ namespace Octobot { } } - internal static string CommandDescriptionRemind { - get { - return ResourceManager.GetString("CommandDescriptionRemind", resourceCulture); - } - } - internal static string SettingsPublicFeedbackChannel { get { return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture); @@ -738,18 +510,6 @@ namespace Octobot { } } - internal static string MissingReminderText { - get { - return ResourceManager.GetString("MissingReminderText", resourceCulture); - } - } - - internal static string InvalidRemindIn { - get { - return ResourceManager.GetString("InvalidRemindIn", resourceCulture); - } - } - internal static string IssuedBy { get { return ResourceManager.GetString("IssuedBy", resourceCulture); From 7cf200d8de607b4dbf1932f22a6f613e5f8eec95 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Tue, 3 Oct 2023 17:25:28 +0300 Subject: [PATCH 177/329] Fix /unmute not checking if the target is muted (#143) Closes #142 --------- Signed-off-by: Macintosh II --- src/Commands/MuteCommandGroup.cs | 98 +++++++++++++++++--------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index d558ba2..fac9c84 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -285,17 +285,11 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } - if (data.GetOrCreateMemberData(target.ID).MutedUntil is not null) - { - return await RemoveMuteRoleUserAsync( - target, reason, guildId, data, channelId, user, currentUser, CancellationToken); - } - - return await RemoveTimeoutUserAsync( + return await RemoveMuteAsync( target, reason, guildId, data, channelId, user, currentUser, CancellationToken); } - private async Task RemoveMuteRoleUserAsync( + private async Task RemoveMuteAsync( IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, IUser currentUser, CancellationToken ct = default) { @@ -315,14 +309,36 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } - var memberData = data.GetOrCreateMemberData(target.ID); - var unmuteResult = await _guildApi.ModifyGuildMemberAsync( - guildId, target.ID, roles: memberData.Roles.ConvertAll(r => r.ToSnowflake()), - reason: $"({user.GetTag()}) {reason}".EncodeHeader(), ct: ct); - memberData.MutedUntil = null; - if (!unmuteResult.IsSuccess) + var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, ct); + DateTimeOffset? communicationDisabledUntil = null; + if (guildMemberResult.IsDefined(out var guildMember)) { - return Result.FromError(unmuteResult.Error); + communicationDisabledUntil = guildMember.CommunicationDisabledUntil.OrDefault(null); + } + + var memberData = data.GetOrCreateMemberData(target.ID); + var isMuted = memberData.MutedUntil is not null || communicationDisabledUntil is not null; + + if (!isMuted) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, currentUser) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + } + + var removeMuteRoleAsync = + await RemoveMuteRoleAsync(target, reason, guildId, memberData, user, CancellationToken); + if (!removeMuteRoleAsync.IsSuccess) + { + return Result.FromError(removeMuteRoleAsync.Error); + } + + var removeTimeoutResult = + await RemoveTimeoutAsync(target, reason, guildId, communicationDisabledUntil, user, CancellationToken); + if (!removeTimeoutResult.IsSuccess) + { + return Result.FromError(removeTimeoutResult.Error); } var title = string.Format(Messages.UserUnmuted, target.GetTag()); @@ -341,47 +357,37 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct); } - private async Task RemoveTimeoutUserAsync( - IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, - IUser currentUser, CancellationToken ct = default) + private async Task RemoveMuteRoleAsync( + IUser target, string reason, Snowflake guildId, MemberData memberData, IUser user, CancellationToken ct = default) { - var interactionResult - = await _utility.CheckInteractionsAsync( - guildId, user.ID, target.ID, "Unmute", ct); - if (!interactionResult.IsSuccess) + if (memberData.MutedUntil is null) { - return Result.FromError(interactionResult); + return Result.FromSuccess(); } - if (interactionResult.Entity is not null) + var unmuteResult = await _guildApi.ModifyGuildMemberAsync( + guildId, target.ID, roles: memberData.Roles.ConvertAll(r => r.ToSnowflake()), + reason: $"({user.GetTag()}) {reason}".EncodeHeader(), ct: ct); + if (unmuteResult.IsSuccess) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) - .WithColour(ColorsList.Red).Build(); + memberData.MutedUntil = null; + } - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return unmuteResult; + } + + private async Task RemoveTimeoutAsync( + IUser target, string reason, Snowflake guildId, DateTimeOffset? communicationDisabledUntil, + IUser user, CancellationToken ct = default) + { + if (communicationDisabledUntil is null) + { + return Result.FromSuccess(); } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), communicationDisabledUntil: null, ct: ct); - if (!unmuteResult.IsSuccess) - { - return Result.FromError(unmuteResult.Error); - } - - var title = string.Format(Messages.UserUnmuted, target.GetTag()); - var description = $"- {string.Format(Messages.DescriptionActionReason, reason)}"; - var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserUnmuted, target.GetTag()), target) - .WithColour(ColorsList.Green).Build(); - - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return unmuteResult; } } From 777dbc6eec9662df05eb5e9b8019a7ce213cbf5c Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:32:54 +0300 Subject: [PATCH 178/329] Update /random (#138) Updates in /random: - Set default minimum number to 0. - Show maximum & minimum numbers. - Recolor & display a message when user tries to use exact same number in first and second fields for some reason. - Mention user in small title. - Automatically detect max & min numbers. - Add `long` support. - Show what default number is. --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> Signed-off-by: Apceniy <53149450+Apceniy@users.noreply.github.com> Signed-off-by: Macintosh II Co-authored-by: Apceniy <53149450+Apceniy@users.noreply.github.com> --- locale/Messages.resx | 17 +++++++-- locale/Messages.ru.resx | 17 +++++++-- locale/Messages.tt-ru.resx | 17 +++++++-- src/Commands/ToolsCommandGroup.cs | 62 +++++++++++++++++++------------ src/Messages.Designer.cs | 29 +++++++++++++-- 5 files changed, 103 insertions(+), 39 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index b03c9f1..7f7e209 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -525,11 +525,20 @@ Nitro booster since - - The minimum number is greater than the maximum! + + Random number for {0} is: - - Your random number is: + + Isn't it obvious? + + + Minimum number: {0} + + + Maximum number: {0} + + + (default) Timestamp for {0}: diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 7e0d6cd..5f3dcf5 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -525,11 +525,20 @@ Начал бустить сервер - - Минимальное число больше максимального! + + Случайное число для {0}: - - Ваше случайное число: + + Разве это не очевидно? + + + Максимальное число: {0} + + + Минимальное число: {0} + + + (по умолчанию) Временная метка для {0}: diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index b27104b..ce0ef61 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -525,11 +525,20 @@ бустит сервер со времен - - почему минимальное > максимальное + + рандомное число {0}: - - ваше рандомное число: + + ну чувак... + + + наибольшее: {0} + + + наименьшее: {0} + + + (дефолт) таймштамп для {0}: diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 6abb918..445140d 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -232,8 +232,8 @@ public class ToolsCommandGroup : CommandGroup /// /// A slash command that generates a random number using maximum and minimum numbers. /// - /// The maximum number for randomization. - /// The minimum number for randomization. Default value: 1 + /// The first number used for randomization. + /// The second number used for randomization. Default value: 0 /// /// A feedback sending result which may or may not have succeeded. /// @@ -242,21 +242,15 @@ public class ToolsCommandGroup : CommandGroup [Description("Generates a random number")] [UsedImplicitly] public async Task ExecuteRandomAsync( - [Description("Maximum number")] int max, - [Description("Minumum number (Default: 1)")] - int min = 1) + [Description("First number")] long first, + [Description("Second number (Default: 0)")] + long? second = null) { if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) - { - return Result.FromError(currentUserResult); - } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); if (!userResult.IsDefined(out var user)) { @@ -266,25 +260,47 @@ public class ToolsCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await SendRandomNumberAsync(max, min, user, currentUser, CancellationToken); + return await SendRandomNumberAsync(first, second, user, CancellationToken); } - private async Task SendRandomNumberAsync(int max, int min, IUser user, IUser currentUser, CancellationToken ct) + private async Task SendRandomNumberAsync(long first, long? secondNullable, + IUser user, CancellationToken ct) { - if (min > max) - { - var failedEmbed = new EmbedBuilder().WithSmallTitle( - Messages.RandomMinGreaterThanMax, currentUser) - .WithColour(ColorsList.Red).Build(); + const long secondDefault = 0; + var second = secondNullable ?? secondDefault; - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + var min = Math.Min(first, second); + var max = Math.Max(first, second); + + var i = Random.Shared.NextInt64(min, max + 1); + + var description = new StringBuilder().Append("# ").Append(i); + + description.AppendLine().Append("- ").Append(string.Format( + Messages.RandomMin, Markdown.InlineCode(min.ToString()))); + if (secondNullable is null && first >= secondDefault) + { + description.Append(' ').Append(Messages.Default); } - var i = Random.Shared.Next(min, max + 1); + description.AppendLine().Append("- ").Append(string.Format( + Messages.RandomMax, Markdown.InlineCode(max.ToString()))); + if (secondNullable is null && first < secondDefault) + { + description.Append(' ').Append(Messages.Default); + } - var embed = new EmbedBuilder().WithSmallTitle(Messages.RandomOutput, user) - .WithDescription($"# {i}\n({min}-{max})") - .WithColour(ColorsList.Blue) + var embedColor = ColorsList.Blue; + if (secondNullable is not null && min == max) + { + description.AppendLine().Append(Markdown.Italicise(Messages.RandomMinMaxSame)); + embedColor = ColorsList.Red; + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.RandomTitle, user.GetTag()), user) + .WithDescription(description.ToString()) + .WithColour(embedColor) .Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct); diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index aef4b7a..bcdc9cd 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -882,17 +882,38 @@ namespace Octobot { } } - internal static string RandomMinGreaterThanMax + internal static string RandomTitle { get { - return ResourceManager.GetString("RandomMinGreaterThanMax", resourceCulture); + return ResourceManager.GetString("RandomTitle", resourceCulture); } } - internal static string RandomOutput + internal static string RandomMinMaxSame { get { - return ResourceManager.GetString("RandomOutput", resourceCulture); + return ResourceManager.GetString("RandomMinMaxSame", resourceCulture); + } + } + + internal static string RandomMax + { + get { + return ResourceManager.GetString("RandomMax", resourceCulture); + } + } + + internal static string RandomMin + { + get { + return ResourceManager.GetString("RandomMin", resourceCulture); + } + } + + internal static string Default + { + get { + return ResourceManager.GetString("Default", resourceCulture); } } From 68d1534b265d1208c12c6bc591f123fd58018ead Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:58:56 +0300 Subject: [PATCH 179/329] Unload guild datas when they become unavailable (#146) Closes #120 --------- Signed-off-by: Macintosh II Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- src/Responders/GuildUnloadedResponder.cs | 38 ++++++++++++++++++++++++ src/Services/GuildDataService.cs | 5 ++++ 2 files changed, 43 insertions(+) create mode 100644 src/Responders/GuildUnloadedResponder.cs diff --git a/src/Responders/GuildUnloadedResponder.cs b/src/Responders/GuildUnloadedResponder.cs new file mode 100644 index 0000000..0cffe25 --- /dev/null +++ b/src/Responders/GuildUnloadedResponder.cs @@ -0,0 +1,38 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Octobot.Data; +using Octobot.Services; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Octobot.Responders; + +/// +/// Handles removing guild ID from if the guild becomes unavailable. +/// +[UsedImplicitly] +public class GuildUnloadedResponder : IResponder +{ + private readonly GuildDataService _guildData; + private readonly ILogger _logger; + + public GuildUnloadedResponder( + GuildDataService guildData, ILogger logger) + { + _guildData = guildData; + _logger = logger; + } + + public Task RespondAsync(IGuildDelete gatewayEvent, CancellationToken ct = default) + { + var guildId = gatewayEvent.ID; + var isDataRemoved = _guildData.UnloadGuildData(guildId); + if (isDataRemoved) + { + _logger.LogInformation("Left guild {GuildId}", guildId); + } + + return Task.FromResult(Result.FromSuccess()); + } +} diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index f5c7faa..73d4a25 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -142,4 +142,9 @@ public sealed class GuildDataService : IHostedService { return _datas.Keys; } + + public bool UnloadGuildData(Snowflake id) + { + return _datas.TryRemove(id, out _); + } } From 395a656fc2492d38c2521f87bc8683c132b24b04 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:01:26 +0300 Subject: [PATCH 180/329] Edit log output in GuildLoadedResponder (#147) Depends on #146 Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- src/Responders/GuildLoadedResponder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 92fc009..b91b209 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -41,7 +41,7 @@ public class GuildLoadedResponder : IResponder } var guild = gatewayEvent.Guild.AsT0; - _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); + _logger.LogInformation("Joined guild {ID} (\"{Name}\")", guild.ID, guild.Name); var data = await _guildData.GetData(guild.ID, ct); var cfg = data.Settings; From f7b59c173ff86b25704371f687f6f632555b391d Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:27:05 +0300 Subject: [PATCH 181/329] Use one common method in /mute (#145) Signed-off-by: Macintosh II Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- src/Commands/MuteCommandGroup.cs | 132 ++++++++++++++----------------- 1 file changed, 58 insertions(+), 74 deletions(-) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index fac9c84..5146fc3 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -101,17 +101,11 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } - if (!GuildSettings.MuteRole.Get(data.Settings).Empty()) - { - return await RoleMuteUserAsync( - target, reason, duration, guildId, data, channelId, user, currentUser, CancellationToken); - } - - return await TimeoutUserAsync( + return await MuteUserAsync( target, reason, duration, guildId, data, channelId, user, currentUser, CancellationToken); } - private async Task RoleMuteUserAsync( + private async Task MuteUserAsync( IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, IUser currentUser, CancellationToken ct = default) { @@ -132,12 +126,57 @@ public class MuteCommandGroup : CommandGroup } var until = DateTimeOffset.UtcNow.Add(duration); // >:) - var memberData = data.GetOrCreateMemberData(target.ID); - memberData.MutedUntil = until; - var assignRoles = new List + + var muteMethodResult = await SelectMuteMethodAsync( + target, reason, duration, guildId, data, user, currentUser, until, ct); + if (!muteMethodResult.IsSuccess) { - GuildSettings.MuteRole.Get(data.Settings) - }; + return muteMethodResult; + } + + var title = string.Format(Messages.UserMuted, target.GetTag()); + var description = new StringBuilder().Append("- ").AppendLine(string.Format(Messages.DescriptionActionReason, reason)) + .Append("- ").Append(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); + + var logResult = _utility.LogActionAsync( + data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); + if (!logResult.IsSuccess) + { + return Result.FromError(logResult.Error); + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserMuted, target.GetTag()), target) + .WithColour(ColorsList.Green).Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } + + private async Task SelectMuteMethodAsync( + IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, + IUser user, IUser currentUser, DateTimeOffset until, CancellationToken ct) + { + var muteRole = GuildSettings.MuteRole.Get(data.Settings); + + if (muteRole.Empty()) + { + var timeoutResult = await TimeoutUserAsync( + target, reason, duration, guildId, user, currentUser, until, ct); + return timeoutResult; + } + + var muteRoleResult = await RoleMuteUserAsync( + target, reason, guildId, data, user, until, muteRole, ct); + return muteRoleResult; + } + + private async Task RoleMuteUserAsync( + IUser target, string reason, Snowflake guildId, GuildData data, + IUser user, DateTimeOffset until, Snowflake muteRole, CancellationToken ct) + { + var assignRoles = new List { muteRole }; + var memberData = data.GetOrCreateMemberData(target.ID); if (!GuildSettings.RemoveRolesOnMute.Get(data.Settings)) { assignRoles.AddRange(memberData.Roles.ConvertAll(r => r.ToSnowflake())); @@ -146,33 +185,17 @@ public class MuteCommandGroup : CommandGroup var muteResult = await _guildApi.ModifyGuildMemberAsync( guildId, target.ID, roles: assignRoles, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), ct: ct); - if (!muteResult.IsSuccess) + if (muteResult.IsSuccess) { - return Result.FromError(muteResult.Error); + memberData.MutedUntil = until; } - var title = string.Format(Messages.UserMuted, target.GetTag()); - var description = new StringBuilder().Append("- ").AppendLine(string.Format(Messages.DescriptionActionReason, reason)) - .Append("- ").Append(string.Format( - Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); - - var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserMuted, target.GetTag()), target) - .WithColour(ColorsList.Green).Build(); - - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return muteResult; } private async Task TimeoutUserAsync( - IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, - IUser user, IUser currentUser, CancellationToken ct = default) + IUser target, string reason, TimeSpan duration, Snowflake guildId, + IUser user, IUser currentUser, DateTimeOffset until, CancellationToken ct) { if (duration.TotalDays >= 28) { @@ -180,52 +203,13 @@ public class MuteCommandGroup : CommandGroup .WithDescription(Messages.DurationRequiredForTimeOuts) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, CancellationToken); - } - - var interactionResult - = await _utility.CheckInteractionsAsync( - guildId, user.ID, target.ID, "Mute", ct); - if (!interactionResult.IsSuccess) - { - return Result.FromError(interactionResult); - } - - if (interactionResult.Entity is not null) - { - var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) - .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } - var until = DateTimeOffset.UtcNow.Add(duration); // >:) var muteResult = await _guildApi.ModifyGuildMemberAsync( guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), communicationDisabledUntil: until, ct: ct); - - if (!muteResult.IsSuccess) - { - return Result.FromError(muteResult.Error); - } - - var title = string.Format(Messages.UserMuted, target.GetTag()); - var description = new StringBuilder().Append("- ").AppendLine(string.Format(Messages.DescriptionActionReason, reason)) - .Append("- ").Append(string.Format( - Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); - - var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.UserMuted, target.GetTag()), target) - .WithColour(ColorsList.Green).Build(); - - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return muteResult; } /// From d27168a89f050a372845a2582172dc006da3cdf8 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:18:59 +0300 Subject: [PATCH 182/329] Add building & running instructions (#148) Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- docs/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/README.md b/docs/README.md index 7d899f9..be73f87 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,6 +22,20 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr [//]: # (if you are reading this, message @mctaylors and ask him to bring back the wiki) +## Running Octobot + +1. Install [.NET 7 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) +2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents! +3. Clone this repository and open `Octobot` folder. +``` +git clone https://github.com/LabsDevelopment/Octobot +cd Octobot +``` +4. Run Octobot using `dotnet` with `BOT_TOKEN` variable. +``` +dotnet run BOT_TOKEN='ENTER_TOKEN_HERE' +``` + ## Contributing When it comes to contributing to the project, the two main things you can do to help out are reporting issues and From 2ab020a2b49eb49b766f1a41abf14579bd9705fb Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 4 Oct 2023 20:21:10 +0500 Subject: [PATCH 183/329] Change IUser variable and parameter names to be less confusing (#149) note: there are still instances of `IUser user` because I could not find a better name for them --------- Signed-off-by: Octol1ttle --- src/Commands/AboutCommandGroup.cs | 12 +-- src/Commands/BanCommandGroup.cs | 64 +++++++------ src/Commands/ClearCommandGroup.cs | 40 ++++----- src/Commands/KickCommandGroup.cs | 32 +++---- src/Commands/MuteCommandGroup.cs | 105 +++++++++++----------- src/Commands/PingCommandGroup.cs | 12 +-- src/Commands/RemindCommandGroup.cs | 57 ++++++------ src/Commands/SettingsCommandGroup.cs | 56 ++++++------ src/Commands/ToolsCommandGroup.cs | 66 +++++++------- src/Data/GuildData.cs | 8 +- src/Extensions.cs | 6 +- src/Responders/GuildLoadedResponder.cs | 8 +- src/Responders/MessageDeletedResponder.cs | 10 +-- src/Services/UtilityService.cs | 20 ++--- 14 files changed, 245 insertions(+), 251 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index c6a6ed3..c034907 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -66,19 +66,19 @@ public class AboutCommandGroup : CommandGroup return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); - return await SendAboutBotAsync(currentUser, guildId, CancellationToken); + return await SendAboutBotAsync(bot, guildId, CancellationToken); } - private async Task SendAboutBotAsync(IUser currentUser, Snowflake guildId, CancellationToken ct = default) + private async Task SendAboutBotAsync(IUser bot, Snowflake guildId, CancellationToken ct = default) { var builder = new StringBuilder().Append("### ").AppendLine(Messages.AboutTitleDevelopers); foreach (var dev in Developers) @@ -92,7 +92,7 @@ public class AboutCommandGroup : CommandGroup builder.Append($"### [{Messages.AboutTitleRepository}](https://github.com/LabsDevelopment/Octobot)"); - var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) .WithImageUrl("https://mctaylors.ddns.net/cdn/octobot-banner.png") diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index a76e79c..010a9da 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -74,22 +74,22 @@ public class BanCommandGroup : CommandGroup [Description("Ban reason")] string reason, [Description("Ban duration")] TimeSpan? duration = null) { - if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - // The current user's avatar is used when sending error messages - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + // The bot's avatar is used when sending error messages + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); @@ -101,25 +101,24 @@ public class BanCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await BanUserAsync( - target, reason, duration, guild, data, channelId, user, currentUser, CancellationToken); + return await BanUserAsync(executor, target, reason, duration, guild, data, channelId, bot, CancellationToken); } private async Task BanUserAsync( - IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, - IUser user, IUser currentUser, CancellationToken ct = default) + IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, + IUser bot, CancellationToken ct = default) { var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct); if (existingBanResult.IsDefined()) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Ban", ct); + = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct); if (!interactionResult.IsSuccess) { return Result.FromError(interactionResult); @@ -127,7 +126,7 @@ public class BanCommandGroup : CommandGroup if (interactionResult.Entity is not null) { - var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); @@ -152,7 +151,7 @@ public class BanCommandGroup : CommandGroup var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereBanned) .WithDescription(description) - .WithActionFooter(user) + .WithActionFooter(executor) .WithCurrentTimestamp() .WithColour(ColorsList.Red) .Build(); @@ -166,7 +165,7 @@ public class BanCommandGroup : CommandGroup } var banResult = await _guildApi.CreateGuildBanAsync( - guild.ID, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), + guild.ID, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), ct: ct); if (!banResult.IsSuccess) { @@ -183,7 +182,7 @@ public class BanCommandGroup : CommandGroup .WithColour(ColorsList.Green).Build(); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); + data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) { return Result.FromError(logResult.Error); @@ -218,47 +217,46 @@ public class BanCommandGroup : CommandGroup [Description("User to unban")] IUser target, [Description("Unban reason")] string reason) { - if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - // The current user's avatar is used when sending error messages - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + // The bot's avatar is used when sending error messages + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } // Needed to get the tag and avatar - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await UnbanUserAsync( - target, reason, guildId, data, channelId, user, currentUser, CancellationToken); + return await UnbanUserAsync(executor, target, reason, guildId, data, channelId, bot, CancellationToken); } private async Task UnbanUserAsync( - IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, - IUser currentUser, CancellationToken ct = default) + IUser executor, IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, + IUser bot, CancellationToken ct = default) { var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct); if (!existingBanResult.IsDefined()) { - var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser) + var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); } var unbanResult = await _guildApi.RemoveGuildBanAsync( - guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + guildId, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(), ct); if (!unbanResult.IsSuccess) { @@ -275,7 +273,7 @@ public class BanCommandGroup : CommandGroup var description = new StringBuilder().Append("- ") .Append(string.Format(Messages.DescriptionActionReason, reason)); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description.ToString(), target, ColorsList.Green, ct: ct); + data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) { return Result.FromError(logResult.Error); diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index e6c08f3..c963fdf 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -63,11 +63,24 @@ public class ClearCommandGroup : CommandGroup [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] int amount) { - if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } + // The bot's avatar is used when sending messages + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) + { + return Result.FromError(executorResult); + } + var messagesResult = await _channelApi.GetChannelMessagesAsync( channelId, limit: amount + 1, ct: CancellationToken); if (!messagesResult.IsDefined(out var messages)) @@ -75,28 +88,15 @@ public class ClearCommandGroup : CommandGroup return Result.FromError(messagesResult); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) - { - return Result.FromError(userResult); - } - - // The current user's avatar is used when sending messages - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) - { - return Result.FromError(currentUserResult); - } - var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ClearMessagesAsync(amount, data, channelId, messages, user, currentUser, CancellationToken); + return await ClearMessagesAsync(executor, amount, data, channelId, messages, bot, CancellationToken); } private async Task ClearMessagesAsync( - int amount, GuildData data, Snowflake channelId, IReadOnlyList messages, - IUser user, IUser currentUser, CancellationToken ct = default) + IUser executor, int amount, GuildData data, Snowflake channelId, IReadOnlyList messages, IUser bot, + CancellationToken ct = default) { var idList = new List(messages.Count); var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); @@ -112,20 +112,20 @@ public class ClearCommandGroup : CommandGroup var description = builder.ToString(); var deleteResult = await _channelApi.BulkDeleteMessagesAsync( - channelId, idList, user.GetTag().EncodeHeader(), ct); + channelId, idList, executor.GetTag().EncodeHeader(), ct); if (!deleteResult.IsSuccess) { return Result.FromError(deleteResult.Error); } var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, currentUser, ColorsList.Red, false, ct); + data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct); if (!logResult.IsSuccess) { return Result.FromError(logResult.Error); } - var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithColour(ColorsList.Green).Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index f69103d..2ee99a9 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -68,22 +68,22 @@ public class KickCommandGroup : CommandGroup [Description("Member to kick")] IUser target, [Description("Kick reason")] string reason) { - if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - // The current user's avatar is used when sending error messages - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + // The bot's avatar is used when sending error messages + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); @@ -98,21 +98,21 @@ public class KickCommandGroup : CommandGroup var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); if (!memberResult.IsSuccess) { - var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } - return await KickUserAsync(target, reason, guild, channelId, data, user, currentUser, CancellationToken); + return await KickUserAsync(executor, target, reason, guild, channelId, data, bot, CancellationToken); } private async Task KickUserAsync( - IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser user, IUser currentUser, + IUser executor, IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser bot, CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Kick", ct); + = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct); if (!interactionResult.IsSuccess) { return Result.FromError(interactionResult); @@ -120,7 +120,7 @@ public class KickCommandGroup : CommandGroup if (interactionResult.Entity is not null) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); @@ -132,7 +132,7 @@ public class KickCommandGroup : CommandGroup var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereKicked) .WithDescription($"- {string.Format(Messages.DescriptionActionReason, reason)}") - .WithActionFooter(user) + .WithActionFooter(executor) .WithCurrentTimestamp() .WithColour(ColorsList.Red) .Build(); @@ -146,7 +146,7 @@ public class KickCommandGroup : CommandGroup } var kickResult = await _guildApi.RemoveGuildMemberAsync( - guild.ID, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), + guild.ID, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(), ct); if (!kickResult.IsSuccess) { @@ -158,7 +158,7 @@ public class KickCommandGroup : CommandGroup var title = string.Format(Messages.UserKicked, target.GetTag()); var description = $"- {string.Format(Messages.DescriptionActionReason, reason)}"; var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); + data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) { return Result.FromError(logResult.Error); diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 5146fc3..4ec4c6c 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -71,22 +71,22 @@ public class MuteCommandGroup : CommandGroup [Description("Mute reason")] string reason, [Description("Mute duration")] TimeSpan duration) { - if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - // The current user's avatar is used when sending error messages - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + // The bot's avatar is used when sending error messages + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -95,23 +95,22 @@ public class MuteCommandGroup : CommandGroup var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); if (!memberResult.IsSuccess) { - var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } - return await MuteUserAsync( - target, reason, duration, guildId, data, channelId, user, currentUser, CancellationToken); + return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); } private async Task MuteUserAsync( - IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, - IUser user, IUser currentUser, CancellationToken ct = default) + IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, + Snowflake channelId, IUser bot, CancellationToken ct = default) { var interactionResult = await _utility.CheckInteractionsAsync( - guildId, user.ID, target.ID, "Mute", ct); + guildId, executor.ID, target.ID, "Mute", ct); if (!interactionResult.IsSuccess) { return Result.FromError(interactionResult); @@ -119,7 +118,7 @@ public class MuteCommandGroup : CommandGroup if (interactionResult.Entity is not null) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); @@ -127,8 +126,7 @@ public class MuteCommandGroup : CommandGroup var until = DateTimeOffset.UtcNow.Add(duration); // >:) - var muteMethodResult = await SelectMuteMethodAsync( - target, reason, duration, guildId, data, user, currentUser, until, ct); + var muteMethodResult = await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct); if (!muteMethodResult.IsSuccess) { return muteMethodResult; @@ -140,7 +138,7 @@ public class MuteCommandGroup : CommandGroup Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ColorsList.Red, ct: ct); + data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) { return Result.FromError(logResult.Error); @@ -154,26 +152,24 @@ public class MuteCommandGroup : CommandGroup } private async Task SelectMuteMethodAsync( - IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, - IUser user, IUser currentUser, DateTimeOffset until, CancellationToken ct) + IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, + IUser bot, DateTimeOffset until, CancellationToken ct) { var muteRole = GuildSettings.MuteRole.Get(data.Settings); if (muteRole.Empty()) { - var timeoutResult = await TimeoutUserAsync( - target, reason, duration, guildId, user, currentUser, until, ct); + var timeoutResult = await TimeoutUserAsync(executor, target, reason, duration, guildId, bot, until, ct); return timeoutResult; } - var muteRoleResult = await RoleMuteUserAsync( - target, reason, guildId, data, user, until, muteRole, ct); + var muteRoleResult = await RoleMuteUserAsync(executor, target, reason, guildId, data, until, muteRole, ct); return muteRoleResult; } private async Task RoleMuteUserAsync( - IUser target, string reason, Snowflake guildId, GuildData data, - IUser user, DateTimeOffset until, Snowflake muteRole, CancellationToken ct) + IUser executor, IUser target, string reason, Snowflake guildId, GuildData data, + DateTimeOffset until, Snowflake muteRole, CancellationToken ct) { var assignRoles = new List { muteRole }; var memberData = data.GetOrCreateMemberData(target.ID); @@ -184,7 +180,7 @@ public class MuteCommandGroup : CommandGroup var muteResult = await _guildApi.ModifyGuildMemberAsync( guildId, target.ID, roles: assignRoles, - reason: $"({user.GetTag()}) {reason}".EncodeHeader(), ct: ct); + reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), ct: ct); if (muteResult.IsSuccess) { memberData.MutedUntil = until; @@ -194,12 +190,12 @@ public class MuteCommandGroup : CommandGroup } private async Task TimeoutUserAsync( - IUser target, string reason, TimeSpan duration, Snowflake guildId, - IUser user, IUser currentUser, DateTimeOffset until, CancellationToken ct) + IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, + IUser bot, DateTimeOffset until, CancellationToken ct) { if (duration.TotalDays >= 28) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.BotCannotMuteTarget, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.BotCannotMuteTarget, bot) .WithDescription(Messages.DurationRequiredForTimeOuts) .WithColour(ColorsList.Red).Build(); @@ -207,7 +203,7 @@ public class MuteCommandGroup : CommandGroup } var muteResult = await _guildApi.ModifyGuildMemberAsync( - guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), + guildId, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), communicationDisabledUntil: until, ct: ct); return muteResult; } @@ -238,23 +234,23 @@ public class MuteCommandGroup : CommandGroup [Description("Member to unmute")] IUser target, [Description("Unmute reason")] string reason) { - if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - // The current user's avatar is used when sending error messages - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + // The bot's avatar is used when sending error messages + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } // Needed to get the tag and avatar - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -263,23 +259,22 @@ public class MuteCommandGroup : CommandGroup var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken); if (!memberResult.IsSuccess) { - var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); } - return await RemoveMuteAsync( - target, reason, guildId, data, channelId, user, currentUser, CancellationToken); + return await RemoveMuteAsync(executor, target, reason, guildId, data, channelId, bot, CancellationToken); } private async Task RemoveMuteAsync( - IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user, - IUser currentUser, CancellationToken ct = default) + IUser executor, IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, + IUser bot, CancellationToken ct = default) { var interactionResult = await _utility.CheckInteractionsAsync( - guildId, user.ID, target.ID, "Unmute", ct); + guildId, executor.ID, target.ID, "Unmute", ct); if (!interactionResult.IsSuccess) { return Result.FromError(interactionResult); @@ -287,7 +282,7 @@ public class MuteCommandGroup : CommandGroup if (interactionResult.Entity is not null) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); @@ -305,21 +300,21 @@ public class MuteCommandGroup : CommandGroup if (!isMuted) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, bot) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var removeMuteRoleAsync = - await RemoveMuteRoleAsync(target, reason, guildId, memberData, user, CancellationToken); + await RemoveMuteRoleAsync(executor, target, reason, guildId, memberData, CancellationToken); if (!removeMuteRoleAsync.IsSuccess) { return Result.FromError(removeMuteRoleAsync.Error); } var removeTimeoutResult = - await RemoveTimeoutAsync(target, reason, guildId, communicationDisabledUntil, user, CancellationToken); + await RemoveTimeoutAsync(executor, target, reason, guildId, communicationDisabledUntil, CancellationToken); if (!removeTimeoutResult.IsSuccess) { return Result.FromError(removeTimeoutResult.Error); @@ -328,7 +323,7 @@ public class MuteCommandGroup : CommandGroup var title = string.Format(Messages.UserUnmuted, target.GetTag()); var description = $"- {string.Format(Messages.DescriptionActionReason, reason)}"; var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, target, ColorsList.Green, ct: ct); + data.Settings, channelId, executor, title, description, target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) { return Result.FromError(logResult.Error); @@ -342,7 +337,7 @@ public class MuteCommandGroup : CommandGroup } private async Task RemoveMuteRoleAsync( - IUser target, string reason, Snowflake guildId, MemberData memberData, IUser user, CancellationToken ct = default) + IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData, CancellationToken ct = default) { if (memberData.MutedUntil is null) { @@ -351,7 +346,7 @@ public class MuteCommandGroup : CommandGroup var unmuteResult = await _guildApi.ModifyGuildMemberAsync( guildId, target.ID, roles: memberData.Roles.ConvertAll(r => r.ToSnowflake()), - reason: $"({user.GetTag()}) {reason}".EncodeHeader(), ct: ct); + reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), ct: ct); if (unmuteResult.IsSuccess) { memberData.MutedUntil = null; @@ -361,8 +356,8 @@ public class MuteCommandGroup : CommandGroup } private async Task RemoveTimeoutAsync( - IUser target, string reason, Snowflake guildId, DateTimeOffset? communicationDisabledUntil, - IUser user, CancellationToken ct = default) + IUser executor, IUser target, string reason, Snowflake guildId, DateTimeOffset? communicationDisabledUntil, + CancellationToken ct = default) { if (communicationDisabledUntil is null) { @@ -370,7 +365,7 @@ public class MuteCommandGroup : CommandGroup } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( - guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), + guildId, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), communicationDisabledUntil: null, ct: ct); return unmuteResult; } diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 07851a7..a1b14bd 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -60,20 +60,20 @@ public class PingCommandGroup : CommandGroup return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); - return await SendLatencyAsync(channelId, currentUser, CancellationToken); + return await SendLatencyAsync(channelId, bot, CancellationToken); } private async Task SendLatencyAsync( - Snowflake channelId, IUser currentUser, CancellationToken ct = default) + Snowflake channelId, IUser bot, CancellationToken ct = default) { var latency = _client.Latency.TotalMilliseconds; if (latency is 0) @@ -89,7 +89,7 @@ public class PingCommandGroup : CommandGroup latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds; } - var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) + var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) .WithTitle($"Sound{Random.Shared.Next(1, 4)}".Localized()) .WithDescription($"{latency:F0}{Messages.Milliseconds}") .WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index cead86e..b6bb235 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -50,34 +50,34 @@ public class RemindCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteListReminderAsync() { - if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(userResult); + return Result.FromError(botResult); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(currentUserResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ListRemindersAsync(data.GetOrCreateMemberData(userId), user, currentUser, CancellationToken); + return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), executor, bot, CancellationToken); } - private async Task ListRemindersAsync(MemberData data, IUser user, IUser currentUser, CancellationToken ct) + private async Task ListRemindersAsync(MemberData data, IUser executor, IUser bot, CancellationToken ct) { if (data.Reminders.Count == 0) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.NoRemindersFound, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.NoRemindersFound, bot) .WithColour(ColorsList.Red) .Build(); @@ -90,11 +90,12 @@ public class RemindCommandGroup : CommandGroup var reminder = data.Reminders[i]; builder.Append("- ").AppendLine(string.Format(Messages.ReminderIndex, Markdown.InlineCode(i.ToString()))) .Append(" - ").AppendLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) - .Append(" - ").AppendLine(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(reminder.At))); + .Append(" - ") + .AppendLine(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(reminder.At))); } var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.ReminderList, user.GetTag()), user) + string.Format(Messages.ReminderList, executor.GetTag()), executor) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) .Build(); @@ -119,30 +120,30 @@ public class RemindCommandGroup : CommandGroup TimeSpan @in, [Description("Reminder message")] string message) { - if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await AddReminderAsync(@in, message, data, channelId, user, CancellationToken); + return await AddReminderAsync(@in, message, data, channelId, executor, CancellationToken); } private async Task AddReminderAsync( TimeSpan @in, string message, GuildData data, - Snowflake channelId, IUser user, CancellationToken ct = default) + Snowflake channelId, IUser executor, CancellationToken ct = default) { var remindAt = DateTimeOffset.UtcNow.Add(@in); - data.GetOrCreateMemberData(user.ID).Reminders.Add( + data.GetOrCreateMemberData(executor.ID).Reminders.Add( new Reminder { At = remindAt, @@ -155,7 +156,7 @@ public class RemindCommandGroup : CommandGroup .Append("- ").Append(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.ReminderCreated, user.GetTag()), user) + string.Format(Messages.ReminderCreated, executor.GetTag()), executor) .WithDescription(builder.ToString()) .WithColour(ColorsList.Green) .Build(); @@ -177,29 +178,29 @@ public class RemindCommandGroup : CommandGroup [Description("Index of reminder to delete")] [MinValue(0)] int index) { - if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await DeleteReminderAsync(data.GetOrCreateMemberData(userId), index, currentUser, CancellationToken); + return await DeleteReminderAsync(data.GetOrCreateMemberData(executorId), index, bot, CancellationToken); } - private async Task DeleteReminderAsync(MemberData data, int index, IUser currentUser, + private async Task DeleteReminderAsync(MemberData data, int index, IUser bot, CancellationToken ct) { if (index >= data.Reminders.Count) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderIndex, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderIndex, bot) .WithColour(ColorsList.Red) .Build(); @@ -208,7 +209,7 @@ public class RemindCommandGroup : CommandGroup data.Reminders.RemoveAt(index); - var embed = new EmbedBuilder().WithSmallTitle(Messages.ReminderDeleted, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(Messages.ReminderDeleted, bot) .WithColour(ColorsList.Green) .Build(); diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 9f7c3df..fc4fbe7 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -91,19 +91,19 @@ public class SettingsCommandGroup : CommandGroup return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); - return await SendSettingsListAsync(cfg, currentUser, page, CancellationToken); + return await SendSettingsListAsync(cfg, bot, page, CancellationToken); } - private async Task SendSettingsListAsync(JsonNode cfg, IUser currentUser, int page, + private async Task SendSettingsListAsync(JsonNode cfg, IUser bot, int page, CancellationToken ct = default) { var description = new StringBuilder(); @@ -117,7 +117,7 @@ public class SettingsCommandGroup : CommandGroup if (firstOptionOnPage >= AllOptions.Length) { - var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.PageNotFound, currentUser) + var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.PageNotFound, bot) .WithDescription(string.Format(Messages.PagesAllowed, Markdown.Bold(totalPages.ToString()))) .WithColour(ColorsList.Red) .Build(); @@ -141,7 +141,7 @@ public class SettingsCommandGroup : CommandGroup .AppendLine(optionValue); } - var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, bot) .WithDescription(description.ToString()) .WithColour(ColorsList.Default) .WithFooter(footer.ToString()) @@ -168,38 +168,38 @@ public class SettingsCommandGroup : CommandGroup AllOptionsEnum setting, [Description("Setting value")] string value) { - if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await EditSettingAsync(AllOptions[(int)setting], value, data, channelId, user, currentUser, + return await EditSettingAsync(AllOptions[(int)setting], value, data, channelId, executor, bot, CancellationToken); } private async Task EditSettingAsync( - IOption option, string value, GuildData data, Snowflake channelId, IUser user, IUser currentUser, + IOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot, CancellationToken ct = default) { var setResult = option.Set(data.Settings, value); if (!setResult.IsSuccess) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser) + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot) .WithDescription(setResult.Error.Message) .WithColour(ColorsList.Red) .Build(); @@ -216,13 +216,13 @@ public class SettingsCommandGroup : CommandGroup var description = builder.ToString(); var logResult = _utility.LogActionAsync( - data.Settings, channelId, user, title, description, currentUser, ColorsList.Magenta, false, ct); + data.Settings, channelId, executor, title, description, bot, ColorsList.Magenta, false, ct); if (!logResult.IsSuccess) { return Result.FromError(logResult.Error); } - var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithDescription(description) .WithColour(ColorsList.Green) .Build(); @@ -250,10 +250,10 @@ public class SettingsCommandGroup : CommandGroup return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); @@ -261,13 +261,13 @@ public class SettingsCommandGroup : CommandGroup if (setting is not null) { - return await ResetSingleSettingAsync(cfg, currentUser, AllOptions[(int)setting], CancellationToken); + return await ResetSingleSettingAsync(cfg, bot, AllOptions[(int)setting], CancellationToken); } - return await ResetAllSettingsAsync(cfg, currentUser, CancellationToken); + return await ResetAllSettingsAsync(cfg, bot, CancellationToken); } - private async Task ResetSingleSettingAsync(JsonNode cfg, IUser currentUser, + private async Task ResetSingleSettingAsync(JsonNode cfg, IUser bot, IOption option, CancellationToken ct = default) { var resetResult = option.Reset(cfg); @@ -277,14 +277,14 @@ public class SettingsCommandGroup : CommandGroup } var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.SingleSettingReset, option.Name), currentUser) + string.Format(Messages.SingleSettingReset, option.Name), bot) .WithColour(ColorsList.Green) .Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct); } - private async Task ResetAllSettingsAsync(JsonNode cfg, IUser currentUser, + private async Task ResetAllSettingsAsync(JsonNode cfg, IUser bot, CancellationToken ct = default) { var failedResults = new List(); @@ -298,7 +298,7 @@ public class SettingsCommandGroup : CommandGroup return failedResults.AggregateErrors(); } - var embed = new EmbedBuilder().WithSmallTitle(Messages.AllSettingsReset, currentUser) + var embed = new EmbedBuilder().WithSmallTitle(Messages.AllSettingsReset, bot) .WithColour(ColorsList.Green) .Build(); diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 445140d..f478907 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -71,48 +71,48 @@ public class ToolsCommandGroup : CommandGroup [Description("User to show info about")] IUser? target = null) { - if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(userResult); + return Result.FromError(botResult); } - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(currentUserResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ShowUserInfoAsync(target ?? user, currentUser, data, guildId, CancellationToken); + return await ShowUserInfoAsync(target ?? executor, bot, data, guildId, CancellationToken); } private async Task ShowUserInfoAsync( - IUser user, IUser currentUser, GuildData data, Snowflake guildId, CancellationToken ct = default) + IUser target, IUser bot, GuildData data, Snowflake guildId, CancellationToken ct = default) { - var builder = new StringBuilder().AppendLine($"### <@{user.ID}>"); + var builder = new StringBuilder().AppendLine($"### <@{target.ID}>"); - if (user.GlobalName is not null) + if (target.GlobalName is not null) { builder.Append("- ").AppendLine(Messages.ShowInfoDisplayName) - .AppendLine(Markdown.InlineCode(user.GlobalName)); + .AppendLine(Markdown.InlineCode(target.GlobalName)); } builder.Append("- ").AppendLine(Messages.ShowInfoDiscordUserSince) - .AppendLine(Markdown.Timestamp(user.ID.Timestamp)); + .AppendLine(Markdown.Timestamp(target.ID.Timestamp)); - var memberData = data.GetOrCreateMemberData(user.ID); + var memberData = data.GetOrCreateMemberData(target.ID); var embedColor = ColorsList.Cyan; - var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, user.ID, ct); + var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, ct); DateTimeOffset? communicationDisabledUntil = null; if (guildMemberResult.IsDefined(out var guildMember)) { @@ -124,7 +124,7 @@ public class ToolsCommandGroup : CommandGroup var isMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || communicationDisabledUntil is not null; - var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, user.ID, ct); + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct); if (isMuted || existingBanResult.IsDefined()) { @@ -155,11 +155,11 @@ public class ToolsCommandGroup : CommandGroup } var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.ShowInfoTitle, user.GetTag()), currentUser) + string.Format(Messages.ShowInfoTitle, target.GetTag()), bot) .WithDescription(builder.ToString()) .WithColour(embedColor) - .WithLargeAvatar(user) - .WithFooter($"ID: {user.ID.ToString()}") + .WithLargeAvatar(target) + .WithFooter($"ID: {target.ID.ToString()}") .Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct); @@ -246,25 +246,25 @@ public class ToolsCommandGroup : CommandGroup [Description("Second number (Default: 0)")] long? second = null) { - if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await SendRandomNumberAsync(first, second, user, CancellationToken); + return await SendRandomNumberAsync(first, second, executor, CancellationToken); } private async Task SendRandomNumberAsync(long first, long? secondNullable, - IUser user, CancellationToken ct) + IUser executor, CancellationToken ct) { const long secondDefault = 0; var second = secondNullable ?? secondDefault; @@ -298,7 +298,7 @@ public class ToolsCommandGroup : CommandGroup } var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.RandomTitle, user.GetTag()), user) + string.Format(Messages.RandomTitle, executor.GetTag()), executor) .WithDescription(description.ToString()) .WithColour(embedColor) .Build(); @@ -332,24 +332,24 @@ public class ToolsCommandGroup : CommandGroup [Description("Offset from current time")] TimeSpan? offset = null) { - if (!_context.TryGetContextIDs(out var guildId, out _, out var userId)) + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } - var userResult = await _userApi.GetUserAsync(userId, CancellationToken); - if (!userResult.IsDefined(out var user)) + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(userResult); + return Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await SendTimestampAsync(offset, user, CancellationToken); + return await SendTimestampAsync(offset, executor, CancellationToken); } - private async Task SendTimestampAsync(TimeSpan? offset, IUser user, CancellationToken ct) + private async Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct) { var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds(); @@ -368,7 +368,7 @@ public class ToolsCommandGroup : CommandGroup } var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.TimestampTitle, user.GetTag()), user) + string.Format(Messages.TimestampTitle, executor.GetTag()), executor) .WithDescription(description.ToString()) .WithColour(ColorsList.Blue) .Build(); diff --git a/src/Data/GuildData.cs b/src/Data/GuildData.cs index a779b56..a675037 100644 --- a/src/Data/GuildData.cs +++ b/src/Data/GuildData.cs @@ -30,15 +30,15 @@ public sealed class GuildData MemberDataPath = memberDataPath; } - public MemberData GetOrCreateMemberData(Snowflake userId) + public MemberData GetOrCreateMemberData(Snowflake memberId) { - if (MemberData.TryGetValue(userId.Value, out var existing)) + if (MemberData.TryGetValue(memberId.Value, out var existing)) { return existing; } - var newData = new MemberData(userId.Value); - MemberData.Add(userId.Value, newData); + var newData = new MemberData(memberId.Value); + MemberData.Add(memberId.Value, newData); return newData; } } diff --git a/src/Extensions.cs b/src/Extensions.cs index 511ab85..20b15e6 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -215,13 +215,13 @@ public static class Extensions public static bool TryGetContextIDs( this ICommandContext context, out Snowflake guildId, - out Snowflake channelId, out Snowflake userId) + out Snowflake channelId, out Snowflake executorId) { channelId = default; - userId = default; + executorId = default; return context.TryGetGuildID(out guildId) && context.TryGetChannelID(out channelId) - && context.TryGetUserID(out userId); + && context.TryGetUserID(out executorId); } /// diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index b91b209..d78b69a 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -60,16 +60,16 @@ public class GuildLoadedResponder : IResponder return Result.FromSuccess(); } - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) + var botResult = await _userApi.GetCurrentUserAsync(ct); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } Messages.Culture = GuildSettings.Language.Get(cfg); var i = Random.Shared.Next(1, 4); - var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) + var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) .WithTitle($"Sound{i}".Localized()) .WithDescription(Messages.Ready) .WithCurrentTimestamp() diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index ae92770..9233820 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -67,17 +67,17 @@ public class MessageDeletedResponder : IResponder var auditLog = auditLogPage.AuditLogEntries.Single(); - var userResult = Result.FromSuccess(message.Author); + var deleterResult = Result.FromSuccess(message.Author); if (auditLog.UserID is not null && auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { - userResult = await _userApi.GetUserAsync(auditLog.UserID.Value, ct); + deleterResult = await _userApi.GetUserAsync(auditLog.UserID.Value, ct); } - if (!userResult.IsDefined(out var user)) + if (!deleterResult.IsDefined(out var deleter)) { - return Result.FromError(userResult); + return Result.FromError(deleterResult); } Messages.Culture = GuildSettings.Language.Get(cfg); @@ -93,7 +93,7 @@ public class MessageDeletedResponder : IResponder Messages.CachedMessageDeleted, message.Author.GetTag()), message.Author) .WithDescription(builder.ToString()) - .WithActionFooter(user) + .WithActionFooter(deleter) .WithTimestamp(message.Timestamp) .WithColour(ColorsList.Red) .Build(); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 4bd9add..22e38cb 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -69,10 +69,10 @@ public sealed class UtilityService : IHostedService return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); } - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) + var botResult = await _userApi.GetCurrentUserAsync(ct); + if (!botResult.IsDefined(out var bot)) { - return Result.FromError(currentUserResult); + return Result.FromError(botResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); @@ -87,7 +87,7 @@ public sealed class UtilityService : IHostedService return Result.FromSuccess(null); } - var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, currentUser.ID, ct); + var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); if (!currentMemberResult.IsDefined(out var currentMember)) { return Result.FromError(currentMemberResult); @@ -167,11 +167,11 @@ public sealed class UtilityService : IHostedService { var builder = new StringBuilder(); var role = GuildSettings.EventNotificationRole.Get(settings); - var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync( + var subscribersResult = await _eventApi.GetGuildScheduledEventUsersAsync( scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct); - if (!usersResult.IsDefined(out var users)) + if (!subscribersResult.IsDefined(out var subscribers)) { - return Result.FromError(usersResult); + return Result.FromError(subscribersResult); } if (!role.Empty()) @@ -179,9 +179,9 @@ public sealed class UtilityService : IHostedService builder.Append($"{Mention.Role(role)} "); } - builder = users.Where( - user => user.GuildMember.IsDefined(out var member) && !member.Roles.Contains(role)) - .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} ")); + builder = subscribers.Where( + subscriber => subscriber.GuildMember.IsDefined(out var member) && !member.Roles.Contains(role)) + .Aggregate(builder, (current, subscriber) => current.Append($"{Mention.User(subscriber.User)} ")); return builder.ToString(); } From 8a27aae2b7b223a397099b976fdf2e4a181a3bb5 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 4 Oct 2023 19:49:21 +0300 Subject: [PATCH 184/329] Update .gitignore (#150) Signed-off-by: Macintosh II --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4529511..f97f6b8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ obj/ riderModule.iml /_ReSharper.Caches/ /.vs/ +GuildData/ +Logs/ From 6f1e543edb8ce45b37857be521c68b6a8272fade Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 4 Oct 2023 21:55:39 +0500 Subject: [PATCH 185/329] Correct illegal nickname characters regex (#151) Apparently there are non-letter characters in between A-Z and a-z --- src/Services/Update/MemberUpdateService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 22b2849..6614273 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -197,7 +197,7 @@ public sealed partial class MemberUpdateService : BackgroundService ct: ct); } - [GeneratedRegex("[^0-9A-zЁА-яё]")] + [GeneratedRegex("[^0-9A-Za-zА-Яа-яЁё]")] private static partial Regex IllegalChars(); private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData data, CancellationToken ct) From 72f728323e58959652734a7227e6936250257a6e Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:23:45 +0300 Subject: [PATCH 186/329] Add /guildinfo (#152) Signed-off-by: Macintosh II --- locale/Messages.resx | 20 ++++++- locale/Messages.ru.resx | 20 ++++++- locale/Messages.tt-ru.resx | 20 ++++++- src/Commands/ToolsCommandGroup.cs | 91 ++++++++++++++++++++++++++++++- src/Extensions.cs | 31 ++++++++++- src/Messages.Designer.cs | 52 +++++++++++++++++- 6 files changed, 225 insertions(+), 9 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 7f7e209..da1496e 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -486,7 +486,7 @@ Display name - + Information about {0} @@ -546,4 +546,22 @@ Offset: {0} + + Guild description + + + Creation date + + + Guild owner + + + Server Boost + + + Boost level + + + Boost count + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 5f3dcf5..e3f507e 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -486,7 +486,7 @@ Отображаемое имя - + Информация о {0} @@ -546,4 +546,22 @@ Офсет: {0} + + Описание сервера + + + Дата создания + + + Владелец сервера + + + Буст сервера + + + Уровень буста + + + Количество бустов + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index ce0ef61..1e784d9 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -486,7 +486,7 @@ дисплейнейм - + деанон {0} @@ -546,4 +546,22 @@ офсет: {0} + + дескрипшон гильдии + + + создался + + + админ гильдии + + + буст гильдии + + + уровень + + + кол-во бустов + diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index f478907..d8f0723 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -19,7 +19,7 @@ using Remora.Results; namespace Octobot.Commands; /// -/// Handles tool commands: /showinfo, /random, /timestamp. +/// Handles tool commands: /showinfo, /guildinfo, /random, /timestamp. /// [UsedImplicitly] public class ToolsCommandGroup : CommandGroup @@ -155,10 +155,10 @@ public class ToolsCommandGroup : CommandGroup } var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.ShowInfoTitle, target.GetTag()), bot) + string.Format(Messages.InformationAbout, target.GetTag()), bot) .WithDescription(builder.ToString()) .WithColour(embedColor) - .WithLargeAvatar(target) + .WithLargeUserAvatar(target) .WithFooter($"ID: {target.ID.ToString()}") .Build(); @@ -229,6 +229,91 @@ public class ToolsCommandGroup : CommandGroup } } + /// + /// A slash command that shows guild information. + /// + /// + /// Information in the output: + /// + /// Guild description + /// Creation date + /// Guild's language + /// Guild's owner + /// Boost level + /// Boost count + /// + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("guildinfo")] + [DiscordDefaultDMPermission(false)] + [Description("Shows info current guild")] + [UsedImplicitly] + public async Task ExecuteGuildInfoAsync() + { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await ShowGuildInfoAsync(bot, guild, CancellationToken); + } + + private async Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct) + { + var description = new StringBuilder().AppendLine($"## {guild.Name}"); + + if (guild.Description is not null) + { + description.Append("- ").AppendLine(Messages.GuildInfoDescription) + .AppendLine(Markdown.InlineCode(guild.Description)); + } + + description.Append("- ").AppendLine(Messages.GuildInfoCreatedAt) + .AppendLine(Markdown.Timestamp(guild.ID.Timestamp)) + .Append("- ").AppendLine(Messages.GuildInfoOwner) + .AppendLine(Mention.User(guild.OwnerID)); + + var embedColor = ColorsList.Cyan; + + if (guild.PremiumTier > PremiumTier.None) + { + description.Append("### ").AppendLine(Messages.GuildInfoServerBoost) + .Append("- ").Append(Messages.GuildInfoBoostTier) + .Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumTier.ToString())) + .Append("- ").Append(Messages.GuildInfoBoostCount) + .Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumSubscriptionCount.ToString())); + embedColor = ColorsList.Magenta; + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.InformationAbout, guild.Name), bot) + .WithDescription(description.ToString()) + .WithColour(embedColor) + .WithLargeGuildIcon(guild) + .WithGuildBanner(guild) + .WithFooter($"ID: {guild.ID.ToString()}") + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); + } + /// /// A slash command that generates a random number using maximum and minimum numbers. /// diff --git a/src/Extensions.cs b/src/Extensions.cs index 20b15e6..00d3d36 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -64,7 +64,7 @@ public static class Extensions /// The builder to add the thumbnail to. /// The user whose avatar to use in the thumbnail field. /// The builder with the added avatar in the thumbnail field. - public static EmbedBuilder WithLargeAvatar( + public static EmbedBuilder WithLargeUserAvatar( this EmbedBuilder builder, IUser avatarSource) { var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); @@ -75,6 +75,35 @@ public static class Extensions return builder.WithThumbnailUrl(avatarUrl.AbsoluteUri); } + /// + /// Adds a guild icon in the thumbnail field. + /// + /// The builder to add the thumbnail to. + /// The guild whose icon to use in the thumbnail field. + /// The builder with the added icon in the thumbnail field. + public static EmbedBuilder WithLargeGuildIcon( + this EmbedBuilder builder, IGuild iconSource) + { + var iconUrlResult = CDN.GetGuildIconUrl(iconSource, imageSize: 256); + return iconUrlResult.IsSuccess + ? builder.WithThumbnailUrl(iconUrlResult.Entity.AbsoluteUri) + : builder; + } + + /// + /// Adds a guild banner in the image field. + /// + /// The builder to add the image to. + /// The guild whose banner to use in the image field. + /// The builder with the added banner in the image field. + public static EmbedBuilder WithGuildBanner( + this EmbedBuilder builder, IGuild bannerSource) + { + return bannerSource.Banner is not null + ? builder.WithImageUrl(CDN.GetGuildBannerUrl(bannerSource).Entity.AbsoluteUri) + : builder; + } + /// /// Adds a footer representing that the action was performed in the . /// diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index bcdc9cd..83d596b 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -798,9 +798,9 @@ namespace Octobot { } } - internal static string ShowInfoTitle { + internal static string InformationAbout { get { - return ResourceManager.GetString("ShowInfoTitle", resourceCulture); + return ResourceManager.GetString("InformationAbout", resourceCulture); } } @@ -932,5 +932,53 @@ namespace Octobot { return ResourceManager.GetString("TimestampOffset", resourceCulture); } } + + internal static string GuildInfoDescription + { + get + { + return ResourceManager.GetString("GuildInfoDescription", resourceCulture); + } + } + + internal static string GuildInfoCreatedAt + { + get + { + return ResourceManager.GetString("GuildInfoCreatedAt", resourceCulture); + } + } + + internal static string GuildInfoOwner + { + get + { + return ResourceManager.GetString("GuildInfoOwner", resourceCulture); + } + } + + internal static string GuildInfoServerBoost + { + get + { + return ResourceManager.GetString("GuildInfoServerBoost", resourceCulture); + } + } + + internal static string GuildInfoBoostTier + { + get + { + return ResourceManager.GetString("GuildInfoBoostTier", resourceCulture); + } + } + + internal static string GuildInfoBoostCount + { + get + { + return ResourceManager.GetString("GuildInfoBoostCount", resourceCulture); + } + } } } From 4748c5de2c3e11b0a8b353a88cb50a36adfd5f5b Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:35:08 +0300 Subject: [PATCH 187/329] Rename /showinfo to avoid confusion with /guildinfo (#153) Depends on #152 Signed-off-by: Macintosh II Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- locale/Messages.resx | 26 ++++++++-------- locale/Messages.ru.resx | 26 ++++++++-------- locale/Messages.tt-ru.resx | 26 ++++++++-------- src/Commands/ToolsCommandGroup.cs | 32 +++++++++---------- src/Messages.Designer.cs | 52 +++++++++++++++---------------- 5 files changed, 81 insertions(+), 81 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index da1496e..51cc2a0 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -483,46 +483,46 @@ Reminder text: {0} - + Display name Information about {0} - + Muted - + Discord user since - + Banned - + Punishments - + Banned permanently - + Not in the guild - + Muted by timeout - + Muted by mute role - + Guild member since - + Nickname - + Roles - + Nitro booster since diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index e3f507e..c18c49f 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -483,46 +483,46 @@ Текст напоминания: {0} - + Отображаемое имя Информация о {0} - + Заглушен - + Вступил в Discord - + Забанен - + Наказания - + Забанен навсегда - + Не на сервере - + Заглушен с помощью тайм-аута - + Заглушен с помощью роли мута - + Вступил на сервер - + Никнейм - + Роли - + Начал бустить сервер diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 1e784d9..a1ffbac 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -483,46 +483,46 @@ че там в напоминалке: {0} - + дисплейнейм деанон {0} - + замучен - + юзер Discord со времен - + забанен - + приколы полученные по заслугам - + забанен - + вышел из сервера - + замучен таймаутом - + замучен ролькой - + участник сервера со времен - + сервернейм - + рольки - + бустит сервер со времен diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index d8f0723..92f1785 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -19,7 +19,7 @@ using Remora.Results; namespace Octobot.Commands; /// -/// Handles tool commands: /showinfo, /guildinfo, /random, /timestamp. +/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp. /// [UsedImplicitly] public class ToolsCommandGroup : CommandGroup @@ -63,11 +63,11 @@ public class ToolsCommandGroup : CommandGroup /// /// A feedback sending result which may or may not have succeeded. /// - [Command("showinfo")] + [Command("userinfo")] [DiscordDefaultDMPermission(false)] [Description("Shows info about user")] [UsedImplicitly] - public async Task ExecuteShowInfoAsync( + public async Task ExecuteUserInfoAsync( [Description("User to show info about")] IUser? target = null) { @@ -101,11 +101,11 @@ public class ToolsCommandGroup : CommandGroup if (target.GlobalName is not null) { - builder.Append("- ").AppendLine(Messages.ShowInfoDisplayName) + builder.Append("- ").AppendLine(Messages.UserInfoDisplayName) .AppendLine(Markdown.InlineCode(target.GlobalName)); } - builder.Append("- ").AppendLine(Messages.ShowInfoDiscordUserSince) + builder.Append("- ").AppendLine(Messages.UserInfoDiscordUserSince) .AppendLine(Markdown.Timestamp(target.ID.Timestamp)); var memberData = data.GetOrCreateMemberData(target.ID); @@ -129,7 +129,7 @@ public class ToolsCommandGroup : CommandGroup if (isMuted || existingBanResult.IsDefined()) { builder.Append("### ") - .AppendLine(Markdown.Bold(Messages.ShowInfoPunishments)); + .AppendLine(Markdown.Bold(Messages.UserInfoPunishments)); } if (isMuted) @@ -149,7 +149,7 @@ public class ToolsCommandGroup : CommandGroup if (!guildMemberResult.IsSuccess && !existingBanResult.IsDefined()) { builder.Append("### ") - .AppendLine(Markdown.Bold(Messages.ShowInfoNotOnGuild)); + .AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild)); embedColor = ColorsList.Default; } @@ -169,23 +169,23 @@ public class ToolsCommandGroup : CommandGroup { if (guildMember.Nickname.IsDefined(out var nickname)) { - builder.Append("- ").AppendLine(Messages.ShowInfoGuildNickname) + builder.Append("- ").AppendLine(Messages.UserInfoGuildNickname) .AppendLine(Markdown.InlineCode(nickname)); } - builder.Append("- ").AppendLine(Messages.ShowInfoGuildMemberSince) + builder.Append("- ").AppendLine(Messages.UserInfoGuildMemberSince) .AppendLine(Markdown.Timestamp(guildMember.JoinedAt)); if (guildMember.PremiumSince.IsDefined(out var premiumSince)) { - builder.Append("- ").AppendLine(Messages.ShowInfoGuildMemberPremiumSince) + builder.Append("- ").AppendLine(Messages.UserInfoGuildMemberPremiumSince) .AppendLine(Markdown.Timestamp(premiumSince.Value)); color = ColorsList.Magenta; } if (guildMember.Roles.Count > 0) { - builder.Append("- ").AppendLine(Messages.ShowInfoGuildRoles); + builder.Append("- ").AppendLine(Messages.UserInfoGuildRoles); for (var i = 0; i < guildMember.Roles.Count - 1; i++) { builder.Append($"<@&{guildMember.Roles[i]}>, "); @@ -201,29 +201,29 @@ public class ToolsCommandGroup : CommandGroup { if (memberData.BannedUntil < DateTimeOffset.MaxValue) { - builder.Append("- ").AppendLine(Messages.ShowInfoBanned) + builder.Append("- ").AppendLine(Messages.UserInfoBanned) .Append(" - ").AppendLine(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.BannedUntil.Value))); return; } - builder.Append("- ").AppendLine(Messages.ShowInfoBannedPermanently); + builder.Append("- ").AppendLine(Messages.UserInfoBannedPermanently); } private static void AppendMuteInformation( MemberData memberData, DateTimeOffset? communicationDisabledUntil, StringBuilder builder) { - builder.Append("- ").AppendLine(Messages.ShowInfoMuted); + builder.Append("- ").AppendLine(Messages.UserInfoMuted); if (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) { - builder.Append(" - ").AppendLine(Messages.ShowInfoMutedByMuteRole) + builder.Append(" - ").AppendLine(Messages.UserInfoMutedByMuteRole) .Append(" - ").AppendLine(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.MutedUntil.Value))); } if (communicationDisabledUntil is not null) { - builder.Append(" - ").AppendLine(Messages.ShowInfoMutedByTimeout) + builder.Append(" - ").AppendLine(Messages.UserInfoMutedByTimeout) .Append(" - ").AppendLine(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(communicationDisabledUntil.Value))); } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 83d596b..ca63b96 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -804,81 +804,81 @@ namespace Octobot { } } - internal static string ShowInfoDisplayName { + internal static string UserInfoDisplayName { get { - return ResourceManager.GetString("ShowInfoDisplayName", resourceCulture); + return ResourceManager.GetString("UserInfoDisplayName", resourceCulture); } } - internal static string ShowInfoDiscordUserSince { + internal static string UserInfoDiscordUserSince { get { - return ResourceManager.GetString("ShowInfoDiscordUserSince", resourceCulture); + return ResourceManager.GetString("UserInfoDiscordUserSince", resourceCulture); } } - internal static string ShowInfoMuted { + internal static string UserInfoMuted { get { - return ResourceManager.GetString("ShowInfoMuted", resourceCulture); + return ResourceManager.GetString("UserInfoMuted", resourceCulture); } } - internal static string ShowInfoBanned { + internal static string UserInfoBanned { get { - return ResourceManager.GetString("ShowInfoBanned", resourceCulture); + return ResourceManager.GetString("UserInfoBanned", resourceCulture); } } - internal static string ShowInfoPunishments { + internal static string UserInfoPunishments { get { - return ResourceManager.GetString("ShowInfoPunishments", resourceCulture); + return ResourceManager.GetString("UserInfoPunishments", resourceCulture); } } - internal static string ShowInfoBannedPermanently { + internal static string UserInfoBannedPermanently { get { - return ResourceManager.GetString("ShowInfoBannedPermanently", resourceCulture); + return ResourceManager.GetString("UserInfoBannedPermanently", resourceCulture); } } - internal static string ShowInfoNotOnGuild { + internal static string UserInfoNotOnGuild { get { - return ResourceManager.GetString("ShowInfoNotOnGuild", resourceCulture); + return ResourceManager.GetString("UserInfoNotOnGuild", resourceCulture); } } - internal static string ShowInfoMutedByTimeout { + internal static string UserInfoMutedByTimeout { get { - return ResourceManager.GetString("ShowInfoMutedByTimeout", resourceCulture); + return ResourceManager.GetString("UserInfoMutedByTimeout", resourceCulture); } } - internal static string ShowInfoMutedByMuteRole { + internal static string UserInfoMutedByMuteRole { get { - return ResourceManager.GetString("ShowInfoMutedByMuteRole", resourceCulture); + return ResourceManager.GetString("UserInfoMutedByMuteRole", resourceCulture); } } - internal static string ShowInfoGuildMemberSince { + internal static string UserInfoGuildMemberSince { get { - return ResourceManager.GetString("ShowInfoGuildMemberSince", resourceCulture); + return ResourceManager.GetString("UserInfoGuildMemberSince", resourceCulture); } } - internal static string ShowInfoGuildNickname { + internal static string UserInfoGuildNickname { get { - return ResourceManager.GetString("ShowInfoGuildNickname", resourceCulture); + return ResourceManager.GetString("UserInfoGuildNickname", resourceCulture); } } - internal static string ShowInfoGuildRoles { + internal static string UserInfoGuildRoles { get { - return ResourceManager.GetString("ShowInfoGuildRoles", resourceCulture); + return ResourceManager.GetString("UserInfoGuildRoles", resourceCulture); } } - internal static string ShowInfoGuildMemberPremiumSince { + internal static string UserInfoGuildMemberPremiumSince { get { - return ResourceManager.GetString("ShowInfoGuildMemberPremiumSince", resourceCulture); + return ResourceManager.GetString("UserInfoGuildMemberPremiumSince", resourceCulture); } } From a70c228bc4f42ce17640e2e1fa7d3a8e22b76f10 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:12:36 +0300 Subject: [PATCH 188/329] Change reminders sorting in /listremind (#156) Signed-off-by: Macintosh II --- src/Commands/RemindCommandGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index b6bb235..ab576f0 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -85,7 +85,7 @@ public class RemindCommandGroup : CommandGroup } var builder = new StringBuilder(); - for (var i = data.Reminders.Count - 1; i >= 0; i--) + for (var i = 0; i < data.Reminders.Count; i++) { var reminder = data.Reminders[i]; builder.Append("- ").AppendLine(string.Format(Messages.ReminderIndex, Markdown.InlineCode(i.ToString()))) From d1e3558bc6bf65904a32cabdaa3ed1242ba50831 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 6 Oct 2023 17:17:48 +0300 Subject: [PATCH 189/329] /remind: Switch away from zero-based numbering and rename `index` (#155) Signed-off-by: Macintosh II --- locale/Messages.resx | 8 ++++---- locale/Messages.ru.resx | 8 ++++---- locale/Messages.tt-ru.resx | 8 ++++---- src/Commands/RemindCommandGroup.cs | 18 ++++++++++-------- src/Messages.Designer.cs | 8 ++++---- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 51cc2a0..ab821ac 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -453,8 +453,8 @@ {0}'s reminders - - There's no reminder with that index! + + There's no reminder in this position! Reminder deleted @@ -474,8 +474,8 @@ Jump to channel: {0} - - Index: {0} + + Position in list: {0} The reminder will be sent on: {0} diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index c18c49f..f38032b 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -453,8 +453,8 @@ Напоминания {0} - - У тебя нет напоминания с указанным индексом! + + У тебя нет напоминания на этой позиции! Напоминание удалено @@ -474,8 +474,8 @@ Перейти к каналу: {0} - - Индекс: {0} + + Позиция в списке: {0} Напоминание будет отправлено: {0} diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index a1ffbac..52d6c7e 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -453,8 +453,8 @@ напоминалки {0} - - у тебя нет напоминалки с этим индексом! + + у тебя нет напоминалки на этом номере! напоминалка уничтожена @@ -474,8 +474,8 @@ чекнуть канал: {0} - - индекс: {0} + + номер в списке: {0} я пну тебе это: {0} diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index ab576f0..89e9a7e 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -88,7 +88,7 @@ public class RemindCommandGroup : CommandGroup for (var i = 0; i < data.Reminders.Count; i++) { var reminder = data.Reminders[i]; - builder.Append("- ").AppendLine(string.Format(Messages.ReminderIndex, Markdown.InlineCode(i.ToString()))) + builder.Append("- ").AppendLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString()))) .Append(" - ").AppendLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) .Append(" - ") .AppendLine(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(reminder.At))); @@ -142,8 +142,9 @@ public class RemindCommandGroup : CommandGroup Snowflake channelId, IUser executor, CancellationToken ct = default) { var remindAt = DateTimeOffset.UtcNow.Add(@in); + var memberData = data.GetOrCreateMemberData(executor.ID); - data.GetOrCreateMemberData(executor.ID).Reminders.Add( + memberData.Reminders.Add( new Reminder { At = remindAt, @@ -159,15 +160,16 @@ public class RemindCommandGroup : CommandGroup string.Format(Messages.ReminderCreated, executor.GetTag()), executor) .WithDescription(builder.ToString()) .WithColour(ColorsList.Green) + .WithFooter(string.Format(Messages.ReminderPosition, memberData.Reminders.Count)) .Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct); } /// - /// A slash command that deletes a reminder using its index. + /// A slash command that deletes a reminder using its list position. /// - /// The index of the reminder to delete. + /// The list position of the reminder to delete. /// A feedback sending result which may or may not have succeeded. [Command("delremind")] [Description("Delete one of your reminders")] @@ -175,8 +177,8 @@ public class RemindCommandGroup : CommandGroup [RequireContext(ChannelContext.Guild)] [UsedImplicitly] public async Task ExecuteDeleteReminderAsync( - [Description("Index of reminder to delete")] [MinValue(0)] - int index) + [Description("Position in list")] [MinValue(1)] + int position) { if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { @@ -192,7 +194,7 @@ public class RemindCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await DeleteReminderAsync(data.GetOrCreateMemberData(executorId), index, bot, CancellationToken); + return await DeleteReminderAsync(data.GetOrCreateMemberData(executorId), position - 1, bot, CancellationToken); } private async Task DeleteReminderAsync(MemberData data, int index, IUser bot, @@ -200,7 +202,7 @@ public class RemindCommandGroup : CommandGroup { if (index >= data.Reminders.Count) { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderIndex, bot) + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot) .WithColour(ColorsList.Red) .Build(); diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index ca63b96..ebf7fec 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -738,9 +738,9 @@ namespace Octobot { } } - internal static string InvalidReminderIndex { + internal static string InvalidReminderPosition { get { - return ResourceManager.GetString("InvalidReminderIndex", resourceCulture); + return ResourceManager.GetString("InvalidReminderPosition", resourceCulture); } } @@ -780,9 +780,9 @@ namespace Octobot { } } - internal static string ReminderIndex { + internal static string ReminderPosition { get { - return ResourceManager.GetString("ReminderIndex", resourceCulture); + return ResourceManager.GetString("ReminderPosition", resourceCulture); } } From 8b659a658239354f6187f6df6b9195b25faa2a09 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 6 Oct 2023 17:20:18 +0300 Subject: [PATCH 190/329] Rename Reminder message to Reminder text (#157) Signed-off-by: Macintosh II --- src/Commands/RemindCommandGroup.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 89e9a7e..966b3ec 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -108,7 +108,7 @@ public class RemindCommandGroup : CommandGroup /// A slash command that schedules a reminder with the specified text. /// /// The period of time which must pass before the reminder will be sent. - /// The text of the reminder. + /// The text of the reminder. /// A feedback sending result which may or may not have succeeded. [Command("remind")] [Description("Create a reminder")] @@ -118,7 +118,7 @@ public class RemindCommandGroup : CommandGroup public async Task ExecuteReminderAsync( [Description("After what period of time mention the reminder")] TimeSpan @in, - [Description("Reminder message")] string message) + [Description("Reminder text")] string text) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -134,11 +134,11 @@ public class RemindCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await AddReminderAsync(@in, message, data, channelId, executor, CancellationToken); + return await AddReminderAsync(@in, text, data, channelId, executor, CancellationToken); } private async Task AddReminderAsync( - TimeSpan @in, string message, GuildData data, + TimeSpan @in, string text, GuildData data, Snowflake channelId, IUser executor, CancellationToken ct = default) { var remindAt = DateTimeOffset.UtcNow.Add(@in); @@ -149,11 +149,11 @@ public class RemindCommandGroup : CommandGroup { At = remindAt, Channel = channelId.Value, - Text = message + Text = text }); var builder = new StringBuilder().Append("- ").AppendLine(string.Format( - Messages.ReminderText, Markdown.InlineCode(message))) + Messages.ReminderText, Markdown.InlineCode(text))) .Append("- ").Append(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( From e21cefd79d3c09215efea26cec5949b97bbbf268 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 11 Oct 2023 22:16:52 +0300 Subject: [PATCH 191/329] Use link button for created event message (#158) Signed-off-by: Macintosh II --- src/InteractionResponders.cs | 42 ------------------- src/Octobot.cs | 4 -- .../Update/ScheduledEventUpdateService.cs | 6 +-- 3 files changed, 2 insertions(+), 50 deletions(-) delete mode 100644 src/InteractionResponders.cs diff --git a/src/InteractionResponders.cs b/src/InteractionResponders.cs deleted file mode 100644 index 8e42110..0000000 --- a/src/InteractionResponders.cs +++ /dev/null @@ -1,42 +0,0 @@ -using JetBrains.Annotations; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.Commands.Feedback.Messages; -using Remora.Discord.Commands.Feedback.Services; -using Remora.Discord.Interactivity; -using Remora.Results; - -namespace Octobot; - -/// -/// Handles responding to various interactions. -/// -[UsedImplicitly] -public class InteractionResponders : InteractionGroup -{ - private readonly FeedbackService _feedback; - - public InteractionResponders(FeedbackService feedback) - { - _feedback = feedback; - } - - /// - /// A button that will output an ephemeral embed containing the information about a scheduled event. - /// - /// The ID of the guild and scheduled event, encoded as "guildId:eventId". - /// An ephemeral feedback sending result which may or may not have succeeded. - [Button("scheduled-event-details")] - [UsedImplicitly] - public async Task OnStatefulButtonClicked(string? state = null) - { - if (state is null) - { - return new ArgumentNullError(nameof(state)); - } - - var idArray = state.Split(':'); - return (Result)await _feedback.SendContextualAsync( - $"https://discord.com/events/{idArray[0]}/{idArray[1]}", - options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral), ct: CancellationToken); - } -} diff --git a/src/Octobot.cs b/src/Octobot.cs index 662d1bf..234f3a8 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -17,7 +17,6 @@ using Remora.Discord.Commands.Services; using Remora.Discord.Gateway; using Remora.Discord.Gateway.Extensions; using Remora.Discord.Hosting.Extensions; -using Remora.Discord.Interactivity.Extensions; using Remora.Rest.Core; using Serilog.Extensions.Logging; @@ -80,9 +79,6 @@ public sealed class Octobot // Init .AddDiscordCaching() .AddDiscordCommands(true) - // Interactions - .AddInteractivity() - .AddInteractionGroup() // Slash command event handlers .AddPreparationErrorEvent() .AddPostExecutionEvent() diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 97f9329..ee2c982 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -7,7 +7,6 @@ using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; -using Remora.Discord.Interactivity; using Remora.Rest.Core; using Remora.Results; @@ -217,11 +216,10 @@ public sealed class ScheduledEventUpdateService : BackgroundService : string.Empty; var button = new ButtonComponent( - ButtonComponentStyle.Primary, + ButtonComponentStyle.Link, Messages.EventDetailsButton, new PartialEmoji(Name: "📋"), - CustomIDHelpers.CreateButtonIDWithState( - "scheduled-event-details", $"{scheduledEvent.GuildID}:{scheduledEvent.ID}") + URL: $"https://discord.com/events/{scheduledEvent.GuildID}/{scheduledEvent.ID}" ); return (Result)await _channelApi.CreateMessageAsync( From 4d526ad32ff50c01ae9741c72f39a5cf74a58512 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:56:24 +0300 Subject: [PATCH 192/329] Sanitize scheduled event names (#159) In this PR, I added `Markdown.Sanitize` for every `scheduledEvent.Name` and `eventData.Name`. That means, every title in scheduled event embeds will be sanitized. I did that because in scheduled event list event title displays only as sanitized string. Signed-off-by: Macintosh II --- src/Services/Update/ScheduledEventUpdateService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index ee2c982..7653d2b 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -200,7 +200,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) - .WithTitle(scheduledEvent.Name) + .WithTitle(Markdown.Sanitize(scheduledEvent.Name)) .WithDescription(embedDescription) .WithEventCover(scheduledEvent.ID, scheduledEvent.Image) .WithCurrentTimestamp() @@ -296,7 +296,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromError(embedDescriptionResult); } - var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) + var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, Markdown.Sanitize(scheduledEvent.Name))) .WithDescription(embedDescription) .WithColour(ColorsList.Green) .WithCurrentTimestamp() @@ -321,7 +321,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromSuccess(); } - var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, eventData.Name)) + var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, Markdown.Sanitize(eventData.Name))) .WithDescription( string.Format( Messages.EventDuration, @@ -358,7 +358,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventCancelled, eventData.Name)) + .WithSmallTitle(string.Format(Messages.EventCancelled, Markdown.Sanitize(eventData.Name))) .WithDescription(":(") .WithColour(ColorsList.Red) .WithCurrentTimestamp() @@ -419,7 +419,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService var earlyResult = new EmbedBuilder() .WithDescription( - string.Format(Messages.EventEarlyNotification, scheduledEvent.Name, + string.Format(Messages.EventEarlyNotification, Markdown.Sanitize(scheduledEvent.Name), Markdown.Timestamp(scheduledEvent.ScheduledStartTime, TimestampStyle.RelativeTime))) .WithColour(ColorsList.Default) .Build(); From 20eac79380315fcdb17ca6c782a481294fda8c60 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 12 Oct 2023 19:44:13 +0500 Subject: [PATCH 193/329] Correct issue label names per recent changes (#160) Issue labels have been reworked recently, causing some files in `.github` to reference non-existing issue labels. This PR updates the names of labels in these files. --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- .github/ISSUE_TEMPLATE/feature-request.yml | 2 +- .github/dependabot.yml | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 3f1083a..691e635 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,6 +1,6 @@ name: Bug Report description: Create a report to help us improve -labels: [ "bug" ] +labels: [ "type: bug" ] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 6dac200..e840cf6 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,6 +1,6 @@ name: Feature Request description: Create a request for a feature you would like -labels: [ "type: enhancement" ] +labels: [ "type: feature" ] body: - type: textarea id: background diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fd32dff..77a6b9e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,7 +13,8 @@ updates: # Allow both direct and indirect updates for all packages - dependency-type: "all" labels: - - "type: dependencies" + - "type: change" + - "area: build/ci" - package-ecosystem: "nuget" # See documentation for possible values directory: "/" # Location of package manifests @@ -23,4 +24,5 @@ updates: # Allow both direct and indirect updates for all packages - dependency-type: "all" labels: - - "type: dependencies" + - "type: change" + - "area: build/ci" From e6f53b13f0880a70852235d5669b5c69e524e6f9 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 12 Oct 2023 20:37:25 +0500 Subject: [PATCH 194/329] Split extension methods into separate classes (#161) This PR splits the extension methods contained in `Extensions.cs` into separate classes in the `Octobot.Extensions` namespace. This was done for multiple reasons: 1) The `Extensions.cs` violates SRP (Single Responsibility Principle) - it takes upon itself every extension method for many types 2) Having a separate class for each extended type is a standard practice - take a look at [Remora.Discord](https://github.com/Remora/Remora.Discord/tree/main/Backend/Remora.Discord.Rest/Extensions) or [osu!](https://github.com/ppy/osu/tree/master/osu.Game/Extensions) 3) Having all extension methods in one file makes it hard to find the method you want --- src/Commands/AboutCommandGroup.cs | 1 + src/Commands/BanCommandGroup.cs | 5 +- src/Commands/ClearCommandGroup.cs | 1 + .../Events/ErrorLoggingPostExecutionEvent.cs | 1 + .../Events/LoggingPreparationErrorEvent.cs | 1 + src/Commands/KickCommandGroup.cs | 3 +- src/Commands/MuteCommandGroup.cs | 5 +- src/Commands/PingCommandGroup.cs | 1 + src/Commands/RemindCommandGroup.cs | 1 + src/Commands/SettingsCommandGroup.cs | 1 + src/Commands/ToolsCommandGroup.cs | 1 + src/Data/Options/SnowflakeOption.cs | 1 + src/Extensions.cs | 366 ------------------ src/Extensions/CollectionExtensions.cs | 40 ++ src/Extensions/CommandContextExtensions.cs | 19 + src/Extensions/DiffPaneModelExtensions.cs | 31 ++ src/Extensions/EmbedBuilderExtensions.cs | 149 +++++++ src/Extensions/FeedbackServiceExtensions.cs | 19 + .../GuildScheduledEventExtensions.cs | 28 ++ src/Extensions/LoggerExtensions.cs | 35 ++ src/Extensions/SnowflakeExtensions.cs | 32 ++ src/Extensions/StringExtensions.cs | 52 +++ src/Extensions/UInt64Extensions.cs | 12 + src/Extensions/UserExtensions.cs | 11 + src/Responders/GuildLoadedResponder.cs | 1 + src/Responders/GuildMemberJoinedResponder.cs | 1 + src/Responders/MessageDeletedResponder.cs | 1 + src/Responders/MessageEditedResponder.cs | 1 + src/Services/Update/MemberUpdateService.cs | 1 + .../Update/ScheduledEventUpdateService.cs | 1 + src/Services/UtilityService.cs | 1 + 31 files changed, 452 insertions(+), 371 deletions(-) delete mode 100644 src/Extensions.cs create mode 100644 src/Extensions/CollectionExtensions.cs create mode 100644 src/Extensions/CommandContextExtensions.cs create mode 100644 src/Extensions/DiffPaneModelExtensions.cs create mode 100644 src/Extensions/EmbedBuilderExtensions.cs create mode 100644 src/Extensions/FeedbackServiceExtensions.cs create mode 100644 src/Extensions/GuildScheduledEventExtensions.cs create mode 100644 src/Extensions/LoggerExtensions.cs create mode 100644 src/Extensions/SnowflakeExtensions.cs create mode 100644 src/Extensions/StringExtensions.cs create mode 100644 src/Extensions/UInt64Extensions.cs create mode 100644 src/Extensions/UserExtensions.cs diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index c034907..8529d48 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -2,6 +2,7 @@ using System.Text; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 010a9da..8b62858 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Text; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Octobot.Services.Update; using Remora.Commands.Attributes; @@ -53,7 +54,7 @@ public class BanCommandGroup : CommandGroup /// The user to ban. /// The duration for this ban. The user will be automatically unbanned after this duration. /// - /// The reason for this ban. Must be encoded with when passed to + /// The reason for this ban. Must be encoded with when passed to /// . /// /// @@ -196,7 +197,7 @@ public class BanCommandGroup : CommandGroup /// /// The user to unban. /// - /// The reason for this unban. Must be encoded with when passed to + /// The reason for this unban. Must be encoded with when passed to /// . /// /// diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index c963fdf..a6ac188 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Text; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 9000267..d6a66cc 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Octobot.Extensions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Services; using Remora.Results; diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs index b3f9425..be48e74 100644 --- a/src/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Octobot.Extensions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Services; using Remora.Results; diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 2ee99a9..05552a2 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -49,7 +50,7 @@ public class KickCommandGroup : CommandGroup ///
/// The member to kick. /// - /// The reason for this kick. Must be encoded with when passed to + /// The reason for this kick. Must be encoded with when passed to /// . /// /// diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 4ec4c6c..50fe7a3 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Text; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Octobot.Services.Update; using Remora.Commands.Attributes; @@ -50,7 +51,7 @@ public class MuteCommandGroup : CommandGroup /// The member to mute. /// The duration for this mute. The member will be automatically unmuted after this duration. /// - /// The reason for this mute. Must be encoded with when passed to + /// The reason for this mute. Must be encoded with when passed to /// . /// /// @@ -213,7 +214,7 @@ public class MuteCommandGroup : CommandGroup ///
/// The member to unmute. /// - /// The reason for this unmute. Must be encoded with when passed to + /// The reason for this unmute. Must be encoded with when passed to /// . /// /// diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index a1b14bd..293fbff 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 966b3ec..4a4f6a1 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Text; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index fc4fbe7..317b5c8 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -4,6 +4,7 @@ using System.Text.Json.Nodes; using JetBrains.Annotations; using Octobot.Data; using Octobot.Data.Options; +using Octobot.Extensions; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 92f1785..6c70c01 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -3,6 +3,7 @@ using System.Drawing; using System.Text; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs index 2150725..66ada96 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -1,5 +1,6 @@ using System.Text.Json.Nodes; using System.Text.RegularExpressions; +using Octobot.Extensions; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; diff --git a/src/Extensions.cs b/src/Extensions.cs deleted file mode 100644 index 00d3d36..0000000 --- a/src/Extensions.cs +++ /dev/null @@ -1,366 +0,0 @@ -using System.Net; -using System.Text; -using DiffPlex.DiffBuilder.Model; -using Microsoft.Extensions.Logging; -using Remora.Discord.API; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Objects; -using Remora.Discord.Commands.Contexts; -using Remora.Discord.Commands.Extensions; -using Remora.Discord.Commands.Feedback.Services; -using Remora.Discord.Extensions.Embeds; -using Remora.Discord.Extensions.Formatting; -using Remora.Rest.Core; -using Remora.Results; - -namespace Octobot; - -public static class Extensions -{ - /// - /// Adds a footer representing that an action was performed by a . - /// - /// The builder to add the footer to. - /// The user that performed the action whose tag and avatar to use. - /// The builder with the added footer. - public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) - { - var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); - var avatarUrl = avatarUrlResult.IsSuccess - ? avatarUrlResult.Entity.AbsoluteUri - : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri; - - return builder.WithFooter( - new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl)); - } - - /// - /// Adds a title using the author field, making it smaller than using the title field. - /// - /// The builder to add the small title to. - /// The text of the small title. - /// The user whose avatar to use in the small title. - /// The builder with the added small title in the author field. - public static EmbedBuilder WithSmallTitle( - this EmbedBuilder builder, string text, IUser? avatarSource = null) - { - Uri? avatarUrl = null; - if (avatarSource is not null) - { - var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); - - avatarUrl = avatarUrlResult.IsSuccess - ? avatarUrlResult.Entity - : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; - } - - builder.Author = new EmbedAuthorBuilder(text, iconUrl: avatarUrl?.AbsoluteUri); - return builder; - } - - /// - /// Adds a user avatar in the thumbnail field. - /// - /// The builder to add the thumbnail to. - /// The user whose avatar to use in the thumbnail field. - /// The builder with the added avatar in the thumbnail field. - public static EmbedBuilder WithLargeUserAvatar( - this EmbedBuilder builder, IUser avatarSource) - { - var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); - var avatarUrl = avatarUrlResult.IsSuccess - ? avatarUrlResult.Entity - : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; - - return builder.WithThumbnailUrl(avatarUrl.AbsoluteUri); - } - - /// - /// Adds a guild icon in the thumbnail field. - /// - /// The builder to add the thumbnail to. - /// The guild whose icon to use in the thumbnail field. - /// The builder with the added icon in the thumbnail field. - public static EmbedBuilder WithLargeGuildIcon( - this EmbedBuilder builder, IGuild iconSource) - { - var iconUrlResult = CDN.GetGuildIconUrl(iconSource, imageSize: 256); - return iconUrlResult.IsSuccess - ? builder.WithThumbnailUrl(iconUrlResult.Entity.AbsoluteUri) - : builder; - } - - /// - /// Adds a guild banner in the image field. - /// - /// The builder to add the image to. - /// The guild whose banner to use in the image field. - /// The builder with the added banner in the image field. - public static EmbedBuilder WithGuildBanner( - this EmbedBuilder builder, IGuild bannerSource) - { - return bannerSource.Banner is not null - ? builder.WithImageUrl(CDN.GetGuildBannerUrl(bannerSource).Entity.AbsoluteUri) - : builder; - } - - /// - /// Adds a footer representing that the action was performed in the . - /// - /// The builder to add the footer to. - /// The guild whose name and icon to use. - /// The builder with the added footer. - public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) - { - var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); - var iconUrl = iconUrlResult.IsSuccess - ? iconUrlResult.Entity.AbsoluteUri - : default(Optional); - - return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl)); - } - - /// - /// Adds a title representing that the action happened in the . - /// - /// The builder to add the title to. - /// The guild whose name and icon to use. - /// The builder with the added title. - public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild) - { - var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); - var iconUrl = iconUrlResult.IsSuccess - ? iconUrlResult.Entity.AbsoluteUri - : null; - - builder.Author = new EmbedAuthorBuilder(guild.Name, iconUrl: iconUrl); - return builder; - } - - /// - /// Adds a scheduled event's cover image. - /// - /// The builder to add the image to. - /// The ID of the scheduled event whose image to use. - /// The Optional containing the image hash. - /// The builder with the added cover image. - public static EmbedBuilder WithEventCover( - this EmbedBuilder builder, Snowflake eventId, Optional imageHashOptional) - { - if (!imageHashOptional.IsDefined(out var imageHash)) - { - return builder; - } - - var iconUrlResult = CDN.GetGuildScheduledEventCoverUrl(eventId, imageHash, imageSize: 1024); - return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder; - } - - /// - /// Sanitizes a string for use in by inserting zero-width spaces in between - /// symbols used to format the string with block code. - /// - /// The string to sanitize. - /// The sanitized string that can be safely used in . - private static string SanitizeForBlockCode(this string s) - { - return s.Replace("```", "​`​`​`​"); - } - - /// - /// Sanitizes a string (see ) and formats the string to use Markdown Block Code - /// formatting with a specified - /// language for syntax highlighting. - /// - /// The string to sanitize and format. - /// - /// - /// The sanitized string formatted to use Markdown Block Code with a specified - /// language for syntax highlighting. - /// - public static string InBlockCode(this string s, string language = "") - { - s = s.SanitizeForBlockCode(); - return - $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith("`", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; - } - - public static string Localized(this string key) - { - return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key; - } - - /// - /// Encodes a string to allow its transmission in request headers. - /// - /// Used when encountering "Request headers must contain only ASCII characters". - /// The string to encode. - /// An encoded string with spaces kept intact. - public static string EncodeHeader(this string s) - { - return WebUtility.UrlEncode(s).Replace('+', ' '); - } - - public static string AsMarkdown(this DiffPaneModel model) - { - var builder = new StringBuilder(); - foreach (var line in model.Lines) - { - if (line.Type is ChangeType.Deleted) - { - builder.Append("-- "); - } - - if (line.Type is ChangeType.Inserted) - { - builder.Append("++ "); - } - - if (line.Type is not ChangeType.Imaginary) - { - builder.AppendLine(line.Text); - } - } - - return InBlockCode(builder.ToString(), "diff"); - } - - public static string GetTag(this IUser user) - { - return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}"; - } - - public static Snowflake ToSnowflake(this ulong id) - { - return DiscordSnowflake.New(id); - } - - public static TResult? MaxOrDefault( - this IEnumerable source, Func selector) - { - var list = source.ToList(); - return list.Any() ? list.Max(selector) : default; - } - - public static bool TryGetContextIDs( - this ICommandContext context, out Snowflake guildId, - out Snowflake channelId, out Snowflake executorId) - { - channelId = default; - executorId = default; - return context.TryGetGuildID(out guildId) - && context.TryGetChannelID(out channelId) - && context.TryGetUserID(out executorId); - } - - /// - /// Checks whether this Snowflake has any value set. - /// - /// The Snowflake to check. - /// true if the Snowflake has no value set or it's set to 0, false otherwise. - public static bool Empty(this Snowflake snowflake) - { - return snowflake.Value is 0; - } - - /// - /// Checks whether this snowflake is empty (see ) or it's equal to - /// - /// - /// The Snowflake to check for emptiness - /// The Snowflake to check for equality with . - /// - /// true if is empty or is equal to , false - /// otherwise. - /// - /// - public static bool EmptyOrEqualTo(this Snowflake snowflake, Snowflake anotherSnowflake) - { - return snowflake.Empty() || snowflake == anotherSnowflake; - } - - public static async Task SendContextualEmbedResultAsync( - this FeedbackService feedback, Result embedResult, CancellationToken ct = default) - { - if (!embedResult.IsDefined(out var embed)) - { - return Result.FromError(embedResult); - } - - return (Result)await feedback.SendContextualEmbedAsync(embed, ct: ct); - } - - /// - /// Checks if the has failed due to an error that has resulted from neither invalid user - /// input nor the execution environment and logs the error using the provided . - /// - /// - /// This has special behavior for - its exception will be passed to the - /// - /// - /// The logger to use. - /// The Result whose error check. - /// The message to use if this result has failed. - public static void LogResult(this ILogger logger, IResult result, string? message = "") - { - if (result.IsSuccess || result.Error.IsUserOrEnvironmentError()) - { - return; - } - - if (result.Error is ExceptionError exe) - { - logger.LogError(exe.Exception, "{ErrorMessage}", message); - return; - } - - logger.LogWarning("{UserMessage}\n{ResultErrorMessage}", message, result.Error.Message); - } - - public static void AddIfFailed(this List list, Result result) - { - if (!result.IsSuccess) - { - list.Add(result); - } - } - - /// - /// Return an appropriate result for a list of failed results. The list must only contain failed results. - /// - /// The list of failed results. - /// - /// A successful result if the list is empty, the only Result in the list, or - /// containing all results from the list. - /// - /// - public static Result AggregateErrors(this List list) - { - return list.Count switch - { - 0 => Result.FromSuccess(), - 1 => list[0], - _ => new AggregateError(list.Cast().ToArray()) - }; - } - - public static Result TryGetExternalEventData(this IGuildScheduledEvent scheduledEvent, out DateTimeOffset endTime, - out string? location) - { - endTime = default; - location = default; - if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - { - return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); - } - - if (!metadata.Location.IsDefined(out location)) - { - return new ArgumentNullError(nameof(metadata.Location)); - } - - return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime) - ? Result.FromSuccess() - : new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); - } -} diff --git a/src/Extensions/CollectionExtensions.cs b/src/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000..5322d09 --- /dev/null +++ b/src/Extensions/CollectionExtensions.cs @@ -0,0 +1,40 @@ +using Remora.Results; + +namespace Octobot.Extensions; + +public static class CollectionExtensions +{ + public static TResult? MaxOrDefault( + this IEnumerable source, Func selector) + { + var list = source.ToList(); + return list.Any() ? list.Max(selector) : default; + } + + public static void AddIfFailed(this List list, Result result) + { + if (!result.IsSuccess) + { + list.Add(result); + } + } + + /// + /// Return an appropriate result for a list of failed results. The list must only contain failed results. + /// + /// The list of failed results. + /// + /// A successful result if the list is empty, the only Result in the list, or + /// containing all results from the list. + /// + /// + public static Result AggregateErrors(this List list) + { + return list.Count switch + { + 0 => Result.FromSuccess(), + 1 => list[0], + _ => new AggregateError(list.Cast().ToArray()) + }; + } +} diff --git a/src/Extensions/CommandContextExtensions.cs b/src/Extensions/CommandContextExtensions.cs new file mode 100644 index 0000000..a0c02f2 --- /dev/null +++ b/src/Extensions/CommandContextExtensions.cs @@ -0,0 +1,19 @@ +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Rest.Core; + +namespace Octobot.Extensions; + +public static class CommandContextExtensions +{ + public static bool TryGetContextIDs( + this ICommandContext context, out Snowflake guildId, + out Snowflake channelId, out Snowflake executorId) + { + channelId = default; + executorId = default; + return context.TryGetGuildID(out guildId) + && context.TryGetChannelID(out channelId) + && context.TryGetUserID(out executorId); + } +} diff --git a/src/Extensions/DiffPaneModelExtensions.cs b/src/Extensions/DiffPaneModelExtensions.cs new file mode 100644 index 0000000..ec7b8ee --- /dev/null +++ b/src/Extensions/DiffPaneModelExtensions.cs @@ -0,0 +1,31 @@ +using System.Text; +using DiffPlex.DiffBuilder.Model; + +namespace Octobot.Extensions; + +public static class DiffPaneModelExtensions +{ + public static string AsMarkdown(this DiffPaneModel model) + { + var builder = new StringBuilder(); + foreach (var line in model.Lines) + { + if (line.Type is ChangeType.Deleted) + { + builder.Append("- "); + } + + if (line.Type is ChangeType.Inserted) + { + builder.Append("+ "); + } + + if (line.Type is not ChangeType.Imaginary) + { + builder.AppendLine(line.Text); + } + } + + return builder.ToString().InBlockCode("diff"); + } +} diff --git a/src/Extensions/EmbedBuilderExtensions.cs b/src/Extensions/EmbedBuilderExtensions.cs new file mode 100644 index 0000000..2d61403 --- /dev/null +++ b/src/Extensions/EmbedBuilderExtensions.cs @@ -0,0 +1,149 @@ +using Remora.Discord.API; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Discord.Extensions.Embeds; +using Remora.Rest.Core; + +namespace Octobot.Extensions; + +public static class EmbedBuilderExtensions +{ + /// + /// Adds a footer representing that an action was performed by a . + /// + /// The builder to add the footer to. + /// The user that performed the action whose tag and avatar to use. + /// The builder with the added footer. + public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) + { + var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); + var avatarUrl = avatarUrlResult.IsSuccess + ? avatarUrlResult.Entity.AbsoluteUri + : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri; + + return builder.WithFooter( + new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl)); + } + + /// + /// Adds a title using the author field, making it smaller than using the title field. + /// + /// The builder to add the small title to. + /// The text of the small title. + /// The user whose avatar to use in the small title. + /// The builder with the added small title in the author field. + public static EmbedBuilder WithSmallTitle( + this EmbedBuilder builder, string text, IUser? avatarSource = null) + { + Uri? avatarUrl = null; + if (avatarSource is not null) + { + var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); + + avatarUrl = avatarUrlResult.IsSuccess + ? avatarUrlResult.Entity + : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; + } + + builder.Author = new EmbedAuthorBuilder(text, iconUrl: avatarUrl?.AbsoluteUri); + return builder; + } + + /// + /// Adds a user avatar in the thumbnail field. + /// + /// The builder to add the thumbnail to. + /// The user whose avatar to use in the thumbnail field. + /// The builder with the added avatar in the thumbnail field. + public static EmbedBuilder WithLargeUserAvatar( + this EmbedBuilder builder, IUser avatarSource) + { + var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); + var avatarUrl = avatarUrlResult.IsSuccess + ? avatarUrlResult.Entity + : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; + + return builder.WithThumbnailUrl(avatarUrl.AbsoluteUri); + } + + /// + /// Adds a guild icon in the thumbnail field. + /// + /// The builder to add the thumbnail to. + /// The guild whose icon to use in the thumbnail field. + /// The builder with the added icon in the thumbnail field. + public static EmbedBuilder WithLargeGuildIcon( + this EmbedBuilder builder, IGuild iconSource) + { + var iconUrlResult = CDN.GetGuildIconUrl(iconSource, imageSize: 256); + return iconUrlResult.IsSuccess + ? builder.WithThumbnailUrl(iconUrlResult.Entity.AbsoluteUri) + : builder; + } + + /// + /// Adds a guild banner in the image field. + /// + /// The builder to add the image to. + /// The guild whose banner to use in the image field. + /// The builder with the added banner in the image field. + public static EmbedBuilder WithGuildBanner( + this EmbedBuilder builder, IGuild bannerSource) + { + return bannerSource.Banner is not null + ? builder.WithImageUrl(CDN.GetGuildBannerUrl(bannerSource).Entity.AbsoluteUri) + : builder; + } + + /// + /// Adds a footer representing that the action was performed in the . + /// + /// The builder to add the footer to. + /// The guild whose name and icon to use. + /// The builder with the added footer. + public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) + { + var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); + var iconUrl = iconUrlResult.IsSuccess + ? iconUrlResult.Entity.AbsoluteUri + : default(Optional); + + return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl)); + } + + /// + /// Adds a title representing that the action happened in the . + /// + /// The builder to add the title to. + /// The guild whose name and icon to use. + /// The builder with the added title. + public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild) + { + var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); + var iconUrl = iconUrlResult.IsSuccess + ? iconUrlResult.Entity.AbsoluteUri + : null; + + builder.Author = new EmbedAuthorBuilder(guild.Name, iconUrl: iconUrl); + return builder; + } + + /// + /// Adds a scheduled event's cover image. + /// + /// The builder to add the image to. + /// The ID of the scheduled event whose image to use. + /// The Optional containing the image hash. + /// The builder with the added cover image. + public static EmbedBuilder WithEventCover( + this EmbedBuilder builder, Snowflake eventId, Optional imageHashOptional) + { + if (!imageHashOptional.IsDefined(out var imageHash)) + { + return builder; + } + + var iconUrlResult = CDN.GetGuildScheduledEventCoverUrl(eventId, imageHash, imageSize: 1024); + return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder; + } +} diff --git a/src/Extensions/FeedbackServiceExtensions.cs b/src/Extensions/FeedbackServiceExtensions.cs new file mode 100644 index 0000000..401a865 --- /dev/null +++ b/src/Extensions/FeedbackServiceExtensions.cs @@ -0,0 +1,19 @@ +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Results; + +namespace Octobot.Extensions; + +public static class FeedbackServiceExtensions +{ + public static async Task SendContextualEmbedResultAsync( + this FeedbackService feedback, Result embedResult, CancellationToken ct = default) + { + if (!embedResult.IsDefined(out var embed)) + { + return Result.FromError(embedResult); + } + + return (Result)await feedback.SendContextualEmbedAsync(embed, ct: ct); + } +} diff --git a/src/Extensions/GuildScheduledEventExtensions.cs b/src/Extensions/GuildScheduledEventExtensions.cs new file mode 100644 index 0000000..e3217e3 --- /dev/null +++ b/src/Extensions/GuildScheduledEventExtensions.cs @@ -0,0 +1,28 @@ +using Remora.Discord.API.Abstractions.Objects; +using Remora.Rest.Core; +using Remora.Results; + +namespace Octobot.Extensions; + +public static class GuildScheduledEventExtensions +{ + public static Result TryGetExternalEventData(this IGuildScheduledEvent scheduledEvent, out DateTimeOffset endTime, + out string? location) + { + endTime = default; + location = default; + if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + { + return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); + } + + if (!metadata.Location.IsDefined(out location)) + { + return new ArgumentNullError(nameof(metadata.Location)); + } + + return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime) + ? Result.FromSuccess() + : new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); + } +} diff --git a/src/Extensions/LoggerExtensions.cs b/src/Extensions/LoggerExtensions.cs new file mode 100644 index 0000000..fd4aeb7 --- /dev/null +++ b/src/Extensions/LoggerExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging; +using Remora.Discord.Commands.Extensions; +using Remora.Results; + +namespace Octobot.Extensions; + +public static class LoggerExtensions +{ + /// + /// Checks if the has failed due to an error that has resulted from neither invalid user + /// input nor the execution environment and logs the error using the provided . + /// + /// + /// This has special behavior for - its exception will be passed to the + /// + /// + /// The logger to use. + /// The Result whose error check. + /// The message to use if this result has failed. + public static void LogResult(this ILogger logger, IResult result, string? message = "") + { + if (result.IsSuccess || result.Error.IsUserOrEnvironmentError()) + { + return; + } + + if (result.Error is ExceptionError exe) + { + logger.LogError(exe.Exception, "{ErrorMessage}", message); + return; + } + + logger.LogWarning("{UserMessage}\n{ResultErrorMessage}", message, result.Error.Message); + } +} diff --git a/src/Extensions/SnowflakeExtensions.cs b/src/Extensions/SnowflakeExtensions.cs new file mode 100644 index 0000000..e60bc44 --- /dev/null +++ b/src/Extensions/SnowflakeExtensions.cs @@ -0,0 +1,32 @@ +using Remora.Rest.Core; + +namespace Octobot.Extensions; + +public static class SnowflakeExtensions +{ + /// + /// Checks whether this Snowflake has any value set. + /// + /// The Snowflake to check. + /// true if the Snowflake has no value set or it's set to 0, false otherwise. + public static bool Empty(this Snowflake snowflake) + { + return snowflake.Value is 0; + } + + /// + /// Checks whether this snowflake is empty (see ) or it's equal to + /// + /// + /// The Snowflake to check for emptiness + /// The Snowflake to check for equality with . + /// + /// true if is empty or is equal to , false + /// otherwise. + /// + /// + public static bool EmptyOrEqualTo(this Snowflake snowflake, Snowflake anotherSnowflake) + { + return snowflake.Empty() || snowflake == anotherSnowflake; + } +} diff --git a/src/Extensions/StringExtensions.cs b/src/Extensions/StringExtensions.cs new file mode 100644 index 0000000..3de05d3 --- /dev/null +++ b/src/Extensions/StringExtensions.cs @@ -0,0 +1,52 @@ +using System.Net; +using Remora.Discord.Extensions.Formatting; + +namespace Octobot.Extensions; + +public static class StringExtensions +{ + /// + /// Sanitizes a string for use in by inserting zero-width spaces in between + /// symbols used to format the string with block code. + /// + /// The string to sanitize. + /// The sanitized string that can be safely used in . + private static string SanitizeForBlockCode(this string s) + { + return s.Replace("```", "​`​`​`​"); + } + + /// + /// Sanitizes a string (see ) and formats the string to use Markdown Block Code + /// formatting with a specified + /// language for syntax highlighting. + /// + /// The string to sanitize and format. + /// + /// + /// The sanitized string formatted to use Markdown Block Code with a specified + /// language for syntax highlighting. + /// + public static string InBlockCode(this string s, string language = "") + { + s = s.SanitizeForBlockCode(); + return + $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith("`", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; + } + + public static string Localized(this string key) + { + return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key; + } + + /// + /// Encodes a string to allow its transmission in request headers. + /// + /// Used when encountering "Request headers must contain only ASCII characters". + /// The string to encode. + /// An encoded string with spaces kept intact. + public static string EncodeHeader(this string s) + { + return WebUtility.UrlEncode(s).Replace('+', ' '); + } +} diff --git a/src/Extensions/UInt64Extensions.cs b/src/Extensions/UInt64Extensions.cs new file mode 100644 index 0000000..5d1db00 --- /dev/null +++ b/src/Extensions/UInt64Extensions.cs @@ -0,0 +1,12 @@ +using Remora.Discord.API; +using Remora.Rest.Core; + +namespace Octobot.Extensions; + +public static class UInt64Extensions +{ + public static Snowflake ToSnowflake(this ulong id) + { + return DiscordSnowflake.New(id); + } +} diff --git a/src/Extensions/UserExtensions.cs b/src/Extensions/UserExtensions.cs new file mode 100644 index 0000000..38fe985 --- /dev/null +++ b/src/Extensions/UserExtensions.cs @@ -0,0 +1,11 @@ +using Remora.Discord.API.Abstractions.Objects; + +namespace Octobot.Extensions; + +public static class UserExtensions +{ + public static string GetTag(this IUser user) + { + return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}"; + } +} diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index d78b69a..78dcc43 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -1,6 +1,7 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 57bd01f..09075bf 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -1,6 +1,7 @@ using System.Text.Json.Nodes; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 9233820..5e4870b 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -1,6 +1,7 @@ using System.Text; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index 5e2084c..7841b14 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -2,6 +2,7 @@ using System.Text; using DiffPlex.DiffBuilder; using JetBrains.Annotations; using Octobot.Data; +using Octobot.Extensions; using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 6614273..ae099f6 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Octobot.Data; +using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 7653d2b..20d23fa 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -2,6 +2,7 @@ using System.Text.Json.Nodes; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Octobot.Data; +using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 22e38cb..b144ca7 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json.Nodes; using Microsoft.Extensions.Hosting; using Octobot.Data; +using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; From 67d44ff835d856b7f0497f2ba04e6068fe79adb9 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 12 Oct 2023 21:51:51 +0500 Subject: [PATCH 195/329] Sanitize input text in message edit log (#162) This PR fixes an issue that caused syntax highlighting to be applied to incorrect lines in the message edit log. The fix is to prepend a zero-width space before every line of input text. This prevents Discord from highlighting the line when it wasn't edited --- src/Extensions/DiffPaneModelExtensions.cs | 6 +++--- src/Extensions/StringExtensions.cs | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Extensions/DiffPaneModelExtensions.cs b/src/Extensions/DiffPaneModelExtensions.cs index ec7b8ee..1c3a098 100644 --- a/src/Extensions/DiffPaneModelExtensions.cs +++ b/src/Extensions/DiffPaneModelExtensions.cs @@ -12,17 +12,17 @@ public static class DiffPaneModelExtensions { if (line.Type is ChangeType.Deleted) { - builder.Append("- "); + builder.Append("-- "); } if (line.Type is ChangeType.Inserted) { - builder.Append("+ "); + builder.Append("++ "); } if (line.Type is not ChangeType.Imaginary) { - builder.AppendLine(line.Text); + builder.AppendLine(line.Text.SanitizeForDiffBlock()); } } diff --git a/src/Extensions/StringExtensions.cs b/src/Extensions/StringExtensions.cs index 3de05d3..13cd88a 100644 --- a/src/Extensions/StringExtensions.cs +++ b/src/Extensions/StringExtensions.cs @@ -5,6 +5,8 @@ namespace Octobot.Extensions; public static class StringExtensions { + private const string ZeroWidthSpace = "​"; + /// /// Sanitizes a string for use in by inserting zero-width spaces in between /// symbols used to format the string with block code. @@ -13,7 +15,19 @@ public static class StringExtensions /// The sanitized string that can be safely used in . private static string SanitizeForBlockCode(this string s) { - return s.Replace("```", "​`​`​`​"); + return s.Replace("```", $"{ZeroWidthSpace}`{ZeroWidthSpace}`{ZeroWidthSpace}`{ZeroWidthSpace}"); + } + + /// + /// Sanitizes a string for use in when "language" is "diff" by + /// prepending a zero-width space before the input string to prevent Discord from applying syntax highlighting. + /// + /// This does not call , you have to do so yourself if needed. + /// The string to sanitize. + /// The sanitized string that can be safely used in with "diff" as the language. + public static string SanitizeForDiffBlock(this string s) + { + return $"{ZeroWidthSpace}{s}"; } /// From a326adb6804645f68f06f56d5f9b5324495fc30f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:01:24 +0500 Subject: [PATCH 196/329] Fix Reminder serialization/deserialization (#166) Closes #124 Before we dive into this PR, let's explain a few rules of serialization and deserialization in System.Text.Json (often referred as STJ). The important rule of serialization is that fields are ***not*** serialized unless the serializer gets passed an instance of `JsonSerializerOptions` that explicitly tells it to include fields. However, properties are serialized by default. The important rule of ***de***serialization is that class members are only deserialized if: 1) In case of properties, they must have a setter 2) If they do not have a setter, the constructor must have an argument, required or optional, that will act as the setter for that property. Unfortunately, both of these rules were ignored in some commit that refactored reminders. This PR is here to fix that issue by: 1) Converting fields in `Reminder.cs` to properties 2) Adding an optional argument to the `MemberData` constructor --- src/Data/MemberData.cs | 6 +++++- src/Data/Reminder.cs | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index c7ddc27..b63f8ad 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -5,10 +5,14 @@ namespace Octobot.Data; /// public sealed class MemberData { - public MemberData(ulong id, DateTimeOffset? bannedUntil = null) + public MemberData(ulong id, DateTimeOffset? bannedUntil = null, List? reminders = null) { Id = id; BannedUntil = bannedUntil; + if (reminders is not null) + { + Reminders = reminders; + } } public ulong Id { get; } diff --git a/src/Data/Reminder.cs b/src/Data/Reminder.cs index 828fadb..42144f9 100644 --- a/src/Data/Reminder.cs +++ b/src/Data/Reminder.cs @@ -2,7 +2,7 @@ namespace Octobot.Data; public struct Reminder { - public DateTimeOffset At; - public string Text; - public ulong Channel; + public DateTimeOffset At { get; init; } + public string Text { get; init; } + public ulong Channel { get; init; } } From 687883bbf8294e595210f4e81003c14d23e713f8 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:07:01 +0500 Subject: [PATCH 197/329] Use MemberData to determine a subscriber's role list (#165) Closes #163 Discord's API sucks a lot. You ask it for a member, but it won't give you a member. This is why this PR updates the `GetEventNotificationMentions` method used to determine what roles and users should get pinged for a scheduled event. Previously, the bot asked Discord to provide the member for each subscriber to determine whether or not they have the event notification role (to avoid pinging people personally when the role would already ping them). With this pull request, the bot uses MemberData, it's own member storage, for that purpose (if you're wondering why, refer to the first two sentences) --- src/Services/Update/ScheduledEventUpdateService.cs | 4 ++-- src/Services/UtilityService.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 20d23fa..792eef9 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -286,7 +286,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService }; var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); + scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) { return Result.FromError(contentResult); @@ -412,7 +412,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) { var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); + scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) { return Result.FromError(contentResult); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index b144ca7..5bc06ea 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -160,16 +160,16 @@ public sealed class UtilityService : IHostedService /// /// The scheduled event whose subscribers will be mentioned. /// - /// The settings of the guild containing the scheduled event + /// The data of the guild containing the scheduled event. /// The cancellation token for this operation. /// A result containing the string which may or may not have succeeded. public async Task> GetEventNotificationMentions( - IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) { var builder = new StringBuilder(); - var role = GuildSettings.EventNotificationRole.Get(settings); + var role = GuildSettings.EventNotificationRole.Get(data.Settings); var subscribersResult = await _eventApi.GetGuildScheduledEventUsersAsync( - scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct); + scheduledEvent.GuildID, scheduledEvent.ID, ct: ct); if (!subscribersResult.IsDefined(out var subscribers)) { return Result.FromError(subscribersResult); @@ -181,7 +181,7 @@ public sealed class UtilityService : IHostedService } builder = subscribers.Where( - subscriber => subscriber.GuildMember.IsDefined(out var member) && !member.Roles.Contains(role)) + subscriber => !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value)) .Aggregate(builder, (current, subscriber) => current.Append($"{Mention.User(subscriber.User)} ")); return builder.ToString(); } From b30d690113f484d9cedc6ad18af20f83fac642cc Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:20:58 +0500 Subject: [PATCH 198/329] Add filtering by message author to /clear (#169) Closes #164 This PR adds an optional argument to `/clear` - `author` of type User. If the user is specified, only messages sent by that user will be cleared. Simple as that. --- locale/Messages.resx | 6 ++++++ locale/Messages.ru.resx | 6 ++++++ locale/Messages.tt-ru.resx | 6 ++++++ src/Commands/ClearCommandGroup.cs | 27 ++++++++++++++++++++++----- src/Messages.Designer.cs | 16 ++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index ab821ac..31ed7b3 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -564,4 +564,10 @@ Boost count + + There are no messages matching your filter! + + + Cleared {0} messages from {1} + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index f38032b..cb65749 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -564,4 +564,10 @@ Количество бустов + + Нет сообщений, которые подходят под твой фильтр! + + + Очищено {0} сообщений от {1} + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 52d6c7e..b5f6ad1 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -564,4 +564,10 @@ кол-во бустов + + алло а чё мне удалять-то + + + вырезано {0} забавных сообщений от {1} + diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index a6ac188..714c9de 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -45,9 +45,10 @@ public class ClearCommandGroup : CommandGroup } /// - /// A slash command that clears messages in the channel it was executed. + /// A slash command that clears messages in the channel it was executed, optionally filtering by message author. /// /// The amount of messages to clear. + /// The user whose messages will be cleared. /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages /// were cleared and vice-versa. @@ -62,7 +63,8 @@ public class ClearCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteClear( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] - int amount) + int amount, + IUser? author = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -92,11 +94,11 @@ public class ClearCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ClearMessagesAsync(executor, amount, data, channelId, messages, bot, CancellationToken); + return await ClearMessagesAsync(executor, author, data, channelId, messages, bot, CancellationToken); } private async Task ClearMessagesAsync( - IUser executor, int amount, GuildData data, Snowflake channelId, IReadOnlyList messages, IUser bot, + IUser executor, IUser? author, GuildData data, Snowflake channelId, IReadOnlyList messages, IUser bot, CancellationToken ct = default) { var idList = new List(messages.Count); @@ -104,12 +106,27 @@ public class ClearCommandGroup : CommandGroup for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...') { var message = messages[i]; + if (author is not null && message.Author.ID != author.ID) + { + continue; + } + idList.Add(message.ID); builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author))); builder.Append(message.Content.InBlockCode()); } - var title = string.Format(Messages.MessagesCleared, amount.ToString()); + if (idList.Count == 0) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.NoMessagesToClear, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + } + + var title = author is not null + ? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag()) + : string.Format(Messages.MessagesCleared, idList.Count.ToString()); var description = builder.ToString(); var deleteResult = await _channelApi.BulkDeleteMessagesAsync( diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index ebf7fec..4a771d0 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -980,5 +980,21 @@ namespace Octobot { return ResourceManager.GetString("GuildInfoBoostCount", resourceCulture); } } + + internal static string NoMessagesToClear + { + get + { + return ResourceManager.GetString("NoMessagesToClear", resourceCulture); + } + } + + internal static string MessagesClearedFiltered + { + get + { + return ResourceManager.GetString("MessagesClearedFiltered", resourceCulture); + } + } } } From 580cd24810c1a96230ed1ad1e2c01ac6e026fb66 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:23:14 +0500 Subject: [PATCH 199/329] Do not try to send messages in empty EventNotificationChannels (#167) This PR fixes an error that would occur if an event was created, was about to start or started and the EventNotificationChannel was empty. --- .../Update/ScheduledEventUpdateService.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 792eef9..1672e00 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -177,6 +177,11 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task SendScheduledEventCreatedMessage( IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { + if (GuildSettings.EventNotificationChannel.Get(settings).Empty()) + { + return Result.FromSuccess(); + } + if (!scheduledEvent.Creator.IsDefined(out var creator)) { return new ArgumentNullError(nameof(scheduledEvent.Creator)); @@ -277,6 +282,11 @@ public sealed class ScheduledEventUpdateService : BackgroundService { data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + { + return Result.FromSuccess(); + } + var embedDescriptionResult = scheduledEvent.EntityType switch { GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => @@ -297,7 +307,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromError(embedDescriptionResult); } - var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, Markdown.Sanitize(scheduledEvent.Name))) + var startedEmbed = new EmbedBuilder() + .WithTitle(string.Format(Messages.EventStarted, Markdown.Sanitize(scheduledEvent.Name))) .WithDescription(embedDescription) .WithColour(ColorsList.Green) .WithCurrentTimestamp() @@ -322,7 +333,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromSuccess(); } - var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, Markdown.Sanitize(eventData.Name))) + var completedEmbed = new EmbedBuilder() + .WithTitle(string.Format(Messages.EventCompleted, Markdown.Sanitize(eventData.Name))) .WithDescription( string.Format( Messages.EventDuration, @@ -411,6 +423,11 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task SendEarlyEventNotificationAsync( IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) { + if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + { + return Result.FromSuccess(); + } + var contentResult = await _utility.GetEventNotificationMentions( scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) From fead92129dcf0c4439afc7829b38ccea39104efa Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:25:15 +0500 Subject: [PATCH 200/329] Unschedule scheduled event status update only if last update was successful (#168) This PR fixes an issue that prevented scheduled event status updates from running again if they failed. This happened because, before each update, the events would be synchronized. The `ScheduleOnStatusUpdated` field would be set even if it's already `true`, which will cause it to be set to `false` for unsuccessful updates. The issue is fixed by surrounding the field set call with a condition that will prevent setting the field from `true` to `false` --- src/Services/Update/ScheduledEventUpdateService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 1672e00..f1d3524 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -114,7 +114,11 @@ public sealed class ScheduledEventUpdateService : BackgroundService var eventData = data.ScheduledEvents[@event.ID.Value]; eventData.Name = @event.Name; eventData.ScheduledStartTime = @event.ScheduledStartTime; - eventData.ScheduleOnStatusUpdated = eventData.Status != @event.Status; + if (!eventData.ScheduleOnStatusUpdated) + { + eventData.ScheduleOnStatusUpdated = eventData.Status != @event.Status; + } + eventData.Status = @event.Status; } } From 02707312f597507c810e716eb67304cc4dc7dbd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:13:11 +0300 Subject: [PATCH 201/329] Bump muno92/resharper_inspectcode from 1.8.3 to 1.8.4 (#170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.8.3 to 1.8.4.
Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.8.4

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.8.3...1.8.4

Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.8.4 - 2023-10-18

Commits
  • 8658d1b Merge pull request #417 from muno92/tagpr-from-1.8.3
  • c4974f7 Compile
  • c4dc792 [tagpr] update CHANGELOG.md
  • d12022e [tagpr] prepare for the next release
  • dce5dca Merge pull request #416 from muno92/dependabot/npm_and_yarn/babel/traverse-7....
  • fd69cf5 Bump @​babel/traverse from 7.19.0 to 7.23.2
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.8.3&new-version=1.8.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 512089c..51ef712 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.8.3 + uses: muno92/resharper_inspectcode@1.8.4 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement From 5b84c8d8d0e15cba5c3ff871310e0ff9a7b798b7 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Thu, 26 Oct 2023 17:41:47 +0300 Subject: [PATCH 202/329] Add more info output to /delremind (#173) _There are times when you want to be sure of what you've destroyed._ Therefore, in this PR I added the output of the text of the deleted reminder along with its position in the list, because you can make a mistake with deleting a reminder and forget about what you needed to be reminded about. --------- Signed-off-by: mctaylors --- locale/Messages.resx | 4 ++-- locale/Messages.ru.resx | 4 ++-- locale/Messages.tt-ru.resx | 4 ++-- src/Commands/RemindCommandGroup.cs | 11 +++++++++-- src/Messages.Designer.cs | 4 ++-- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 31ed7b3..0a072c3 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -477,8 +477,8 @@ Position in list: {0} - - The reminder will be sent on: {0} + + Reminder send time: {0} Reminder text: {0} diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index cb65749..92d4c60 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -477,8 +477,8 @@ Позиция в списке: {0} - - Напоминание будет отправлено: {0} + + Время отправки напоминания: {0} Текст напоминания: {0} diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index b5f6ad1..9c5d487 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -477,8 +477,8 @@ номер в списке: {0} - - я пну тебе это: {0} + + время отправки: {0} че там в напоминалке: {0} diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 4a4f6a1..eb46d7c 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -92,7 +92,7 @@ public class RemindCommandGroup : CommandGroup builder.Append("- ").AppendLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString()))) .Append(" - ").AppendLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) .Append(" - ") - .AppendLine(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(reminder.At))); + .AppendLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); } var embed = new EmbedBuilder().WithSmallTitle( @@ -155,7 +155,7 @@ public class RemindCommandGroup : CommandGroup var builder = new StringBuilder().Append("- ").AppendLine(string.Format( Messages.ReminderText, Markdown.InlineCode(text))) - .Append("- ").Append(string.Format(Messages.ReminderWillBeSentOn, Markdown.Timestamp(remindAt))); + .Append("- ").Append(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderCreated, executor.GetTag()), executor) @@ -210,9 +210,16 @@ public class RemindCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } + var reminder = data.Reminders[index]; + + var description = new StringBuilder() + .Append("- ").AppendLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) + .Append("- ").AppendLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); + data.Reminders.RemoveAt(index); var embed = new EmbedBuilder().WithSmallTitle(Messages.ReminderDeleted, bot) + .WithDescription(description.ToString()) .WithColour(ColorsList.Green) .Build(); diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 4a771d0..898528c 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -786,9 +786,9 @@ namespace Octobot { } } - internal static string ReminderWillBeSentOn { + internal static string ReminderTime { get { - return ResourceManager.GetString("ReminderWillBeSentOn", resourceCulture); + return ResourceManager.GetString("ReminderTime", resourceCulture); } } From fb3aebb1e0a4b856e5f19b01e0039e3383b21c56 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 26 Oct 2023 19:54:15 +0500 Subject: [PATCH 203/329] Notify user when a command execution error occurs (#171) With this PR, whenever command execution fails, the user will get an error message with details of the error that can be passed on to developers An unrelated minor change: errors caused by task cancellations will no longer be logged --------- Signed-off-by: Octol1ttle Signed-off-by: mctaylors --- locale/Messages.resx | 6 +++ locale/Messages.ru.resx | 6 +++ locale/Messages.tt-ru.resx | 6 +++ .../Events/ErrorLoggingPostExecutionEvent.cs | 38 +++++++++++++++++-- src/Extensions/LoggerExtensions.cs | 5 +++ src/Messages.Designer.cs | 16 ++++++++ 6 files changed, 74 insertions(+), 3 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 0a072c3..5e24811 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -570,4 +570,10 @@ Cleared {0} messages from {1} + + An error occurred during command execution, try again later. + + + Contact the developers if the problem occurs again. + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 92d4c60..1cc1f9e 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -570,4 +570,10 @@ Очищено {0} сообщений от {1} + + Произошла ошибка при выполнении команды, повтори попытку позже. + + + Обратись к разработчикам, если проблема возникнет снова. + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 9c5d487..48ad8e7 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -570,4 +570,10 @@ вырезано {0} забавных сообщений от {1} + + произошёл тотальный разнос в команде, удачи. + + + если ты это читаешь второй раз за сегодня, пиши разрабам + diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index d6a66cc..a6daaf0 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -1,8 +1,12 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Octobot.Extensions; +using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Commands.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; using Remora.Results; namespace Octobot.Commands.Events; @@ -14,10 +18,15 @@ namespace Octobot.Commands.Events; public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { private readonly ILogger _logger; + private readonly FeedbackService _feedback; + private readonly IDiscordRestUserAPI _userApi; - public ErrorLoggingPostExecutionEvent(ILogger logger) + public ErrorLoggingPostExecutionEvent(ILogger logger, FeedbackService feedback, + IDiscordRestUserAPI userApi) { _logger = logger; + _feedback = feedback; + _userApi = userApi; } /// @@ -28,11 +37,34 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent /// The result whose success is checked. /// The cancellation token for this operation. Unused. /// A result which has succeeded. - public Task AfterExecutionAsync( + public async Task AfterExecutionAsync( ICommandContext context, IResult commandResult, CancellationToken ct = default) { _logger.LogResult(commandResult, $"Error in slash command execution for /{context.Command.Command.Node.Key}."); - return Task.FromResult(Result.FromSuccess()); + var result = commandResult; + while (result.Inner is not null) + { + result = result.Inner; + } + + if (result.IsSuccess) + { + return Result.FromSuccess(); + } + + var botResult = await _userApi.GetCurrentUserAsync(ct); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + + var embed = new EmbedBuilder().WithSmallTitle(Messages.CommandExecutionFailed, bot) + .WithDescription(Markdown.InlineCode(result.Error.Message)) + .WithFooter(Messages.ContactDevelopers) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Extensions/LoggerExtensions.cs b/src/Extensions/LoggerExtensions.cs index fd4aeb7..3805cea 100644 --- a/src/Extensions/LoggerExtensions.cs +++ b/src/Extensions/LoggerExtensions.cs @@ -26,6 +26,11 @@ public static class LoggerExtensions if (result.Error is ExceptionError exe) { + if (exe.Exception is TaskCanceledException) + { + return; + } + logger.LogError(exe.Exception, "{ErrorMessage}", message); return; } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 898528c..5f38061 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -996,5 +996,21 @@ namespace Octobot { return ResourceManager.GetString("MessagesClearedFiltered", resourceCulture); } } + + internal static string CommandExecutionFailed + { + get + { + return ResourceManager.GetString("CommandExecutionFailed", resourceCulture); + } + } + + internal static string ContactDevelopers + { + get + { + return ResourceManager.GetString("ContactDevelopers", resourceCulture); + } + } } } From cf7007f2691080029995332418c0f764f06cc88e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 26 Oct 2023 20:14:27 +0500 Subject: [PATCH 204/329] Handle guild data load errors better (#172) Previously, any errors in guild data load will cause the bot to be unusable in that guild. It didn't help that the end users had no information that something was wrong! Now, any errors will be logged better (with the full path to the file that couldn't be loaded), and the users will receive a message saying that functionality is degraded The old way to save objects was to serialize them directly into streams opened by `File#Create`. This can cause problems if the serialization isn't completed, because `File#Create` overwrites the file with an empty one on the spot. Now, objects are first deserialized into a temporary file, then the original is replaced by the temporary, then the temporary is deleted. Errors during guild data load would sometimes cause the bot to replace the corrupted file with a default one whenever a save is triggered. Now, guilds with load errors won't have their data saved to aid in debugging --------- Signed-off-by: Octol1ttle Signed-off-by: mctaylors --- locale/Messages.resx | 6 ++ locale/Messages.ru.resx | 6 ++ locale/Messages.tt-ru.resx | 6 ++ src/Data/GuildData.cs | 5 +- src/Messages.Designer.cs | 18 +++++- src/Responders/GuildLoadedResponder.cs | 53 ++++++++++++++--- src/Responders/GuildUnloadedResponder.cs | 2 +- src/Services/GuildDataService.cs | 74 ++++++++++++++++++------ 8 files changed, 142 insertions(+), 28 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 5e24811..a9367f7 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -570,6 +570,12 @@ Cleared {0} messages from {1} + + An error occurred during guild data load. + + + This will lead to unexpected behavior. Data will no longer be saved + An error occurred during command execution, try again later. diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 1cc1f9e..d0cbd79 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -570,6 +570,12 @@ Очищено {0} сообщений от {1} + + Произошла ошибка при загрузке данных сервера. + + + Это может привести к неожиданному поведению. Данные больше не будут сохраняться. + Произошла ошибка при выполнении команды, повтори попытку позже. diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 48ad8e7..3bed232 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -570,6 +570,12 @@ вырезано {0} забавных сообщений от {1} + + произошёл тотальный разнос в гилддате. + + + возможно всё съедет с крыши, но знай, что я больше ничё не сохраню. + произошёл тотальный разнос в команде, удачи. diff --git a/src/Data/GuildData.cs b/src/Data/GuildData.cs index a675037..5a903d6 100644 --- a/src/Data/GuildData.cs +++ b/src/Data/GuildData.cs @@ -17,10 +17,12 @@ public sealed class GuildData public readonly JsonNode Settings; public readonly string SettingsPath; + public readonly bool DataLoadFailed; + public GuildData( JsonNode settings, string settingsPath, Dictionary scheduledEvents, string scheduledEventsPath, - Dictionary memberData, string memberDataPath) + Dictionary memberData, string memberDataPath, bool dataLoadFailed) { Settings = settings; SettingsPath = settingsPath; @@ -28,6 +30,7 @@ public sealed class GuildData ScheduledEventsPath = scheduledEventsPath; MemberData = memberData; MemberDataPath = memberDataPath; + DataLoadFailed = dataLoadFailed; } public MemberData GetOrCreateMemberData(Snowflake memberId) diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 5f38061..e2184d7 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -997,6 +997,22 @@ namespace Octobot { } } + internal static string DataLoadFailedTitle + { + get + { + return ResourceManager.GetString("DataLoadFailedTitle", resourceCulture); + } + } + + internal static string DataLoadFailedDescription + { + get + { + return ResourceManager.GetString("DataLoadFailedDescription", resourceCulture); + } + } + internal static string CommandExecutionFailed { get @@ -1004,7 +1020,7 @@ namespace Octobot { return ResourceManager.GetString("CommandExecutionFailed", resourceCulture); } } - + internal static string ContactDevelopers { get diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 78dcc43..3e08060 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -8,6 +8,7 @@ using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Gateway.Events; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; +using Remora.Rest.Core; using Remora.Results; namespace Octobot.Responders; @@ -42,7 +43,6 @@ public class GuildLoadedResponder : IResponder } var guild = gatewayEvent.Guild.AsT0; - _logger.LogInformation("Joined guild {ID} (\"{Name}\")", guild.ID, guild.Name); var data = await _guildData.GetData(guild.ID, ct); var cfg = data.Settings; @@ -51,6 +51,32 @@ public class GuildLoadedResponder : IResponder data.GetOrCreateMemberData(member.User.Value.ID); } + var botResult = await _userApi.GetCurrentUserAsync(ct); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + + if (data.DataLoadFailed) + { + var errorEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.DataLoadFailedTitle, bot) + .WithDescription(Messages.DataLoadFailedDescription) + .WithFooter(Messages.ContactDevelopers) + .WithColour(ColorsList.Red) + .Build(); + + if (!errorEmbed.IsDefined(out var errorBuilt)) + { + return Result.FromError(errorEmbed); + } + + return (Result)await _channelApi.CreateMessageAsync( + GetEmergencyFeedbackChannel(guild, data), embeds: new[] { errorBuilt }, ct: ct); + } + + _logger.LogInformation("Loaded guild {ID} (\"{Name}\")", guild.ID, guild.Name); + if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) { return Result.FromSuccess(); @@ -61,12 +87,6 @@ public class GuildLoadedResponder : IResponder return Result.FromSuccess(); } - var botResult = await _userApi.GetCurrentUserAsync(ct); - if (!botResult.IsDefined(out var bot)) - { - return Result.FromError(botResult); - } - Messages.Culture = GuildSettings.Language.Get(cfg); var i = Random.Shared.Next(1, 4); @@ -84,4 +104,23 @@ public class GuildLoadedResponder : IResponder return (Result)await _channelApi.CreateMessageAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct); } + + private static Snowflake GetEmergencyFeedbackChannel(IGuildCreate.IAvailableGuild guild, GuildData data) + { + var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings); + if (!privateFeedback.Empty()) + { + return privateFeedback; + } + + var publicFeedback = GuildSettings.PublicFeedbackChannel.Get(data.Settings); + if (!publicFeedback.Empty()) + { + return publicFeedback; + } + + return guild.SystemChannelID.AsOptional().IsDefined(out var systemChannel) + ? systemChannel + : guild.Channels[0].ID; + } } diff --git a/src/Responders/GuildUnloadedResponder.cs b/src/Responders/GuildUnloadedResponder.cs index 0cffe25..47bde75 100644 --- a/src/Responders/GuildUnloadedResponder.cs +++ b/src/Responders/GuildUnloadedResponder.cs @@ -30,7 +30,7 @@ public class GuildUnloadedResponder : IResponder var isDataRemoved = _guildData.UnloadGuildData(guildId); if (isDataRemoved) { - _logger.LogInformation("Left guild {GuildId}", guildId); + _logger.LogInformation("Unloaded guild {GuildId}", guildId); } return Task.FromResult(Result.FromSuccess()); diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 73d4a25..78203b5 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -43,25 +43,31 @@ public sealed class GuildDataService : IHostedService { var tasks = new List(); var datas = _datas.Values.ToArray(); - foreach (var data in datas) + foreach (var data in datas.Where(data => !data.DataLoadFailed)) { - await using var settingsStream = File.Create(data.SettingsPath); - tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct)); - - await using var eventsStream = File.Create(data.ScheduledEventsPath); - tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct)); + tasks.Add(SerializeObjectSafelyAsync(data.Settings, data.SettingsPath, ct)); + tasks.Add(SerializeObjectSafelyAsync(data.ScheduledEvents, data.ScheduledEventsPath, ct)); var memberDatas = data.MemberData.Values.ToArray(); - foreach (var memberData in memberDatas) - { - await using var memberDataStream = File.Create($"{data.MemberDataPath}/{memberData.Id}.json"); - tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct)); - } + tasks.AddRange(memberDatas.Select(memberData => + SerializeObjectSafelyAsync(memberData, $"{data.MemberDataPath}/{memberData.Id}.json", ct))); } await Task.WhenAll(tasks); } + private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct) + { + var tempFilePath = path + ".tmp"; + await using (var tempFileStream = File.Create(tempFilePath)) + { + await JsonSerializer.SerializeAsync(tempFileStream, obj, cancellationToken: ct); + } + + File.Copy(tempFilePath, path, true); + File.Delete(tempFilePath); + } + public async Task GetData(Snowflake guildId, CancellationToken ct = default) { return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); @@ -88,20 +94,50 @@ public sealed class GuildDataService : IHostedService await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); } + var dataLoadFailed = false; + await using var settingsStream = File.OpenRead(settingsPath); - var jsonSettings - = JsonNode.Parse(settingsStream); + JsonNode? jsonSettings = null; + try + { + jsonSettings = JsonNode.Parse(settingsStream); + } + catch (Exception e) + { + _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath); + dataLoadFailed = true; + } await using var eventsStream = File.OpenRead(scheduledEventsPath); - var events - = await JsonSerializer.DeserializeAsync>( + Dictionary? events = null; + try + { + events = await JsonSerializer.DeserializeAsync>( eventsStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath); + dataLoadFailed = true; + } var memberData = new Dictionary(); foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles()) { await using var dataStream = dataFileInfo.OpenRead(); - var data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); + MemberData? data; + try + { + data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath, + dataFileInfo.Name); + dataLoadFailed = true; + continue; + } + if (data is null) { continue; @@ -113,7 +149,8 @@ public sealed class GuildDataService : IHostedService var finalData = new GuildData( jsonSettings ?? new JsonObject(), settingsPath, events ?? new Dictionary(), scheduledEventsPath, - memberData, memberDataPath); + memberData, memberDataPath, + dataLoadFailed); _datas.TryAdd(guildId, finalData); @@ -129,7 +166,8 @@ public sealed class GuildDataService : IHostedService Directory.CreateDirectory($"{newPath}/.."); Directory.Move(oldPath, newPath); - _logger.LogInformation("Moved guild data to separate folder: \"{OldPath}\" -> \"{NewPath}\"", oldPath, newPath); + _logger.LogInformation("Moved guild data to separate folder: \"{OldPath}\" -> \"{NewPath}\"", oldPath, + newPath); } } From 0ba357e4c75a62cd0dea713b98fc055e8741109e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 28 Oct 2023 23:10:16 +0500 Subject: [PATCH 205/329] Limit string argument length to avoid "embed description too long" errors (#174) This PR fixes an error that would appear if a string that's way too long was passed as a command argument by limiting the string's length ![image](https://github.com/LabsDevelopment/Octobot/assets/61277953/8f8267fd-d382-4a24-b92d-5f9966d7563b) --- src/Commands/BanCommandGroup.cs | 7 +++++-- src/Commands/KickCommandGroup.cs | 4 +++- src/Commands/MuteCommandGroup.cs | 7 +++++-- src/Commands/RemindCommandGroup.cs | 4 +++- src/Commands/SettingsCommandGroup.cs | 4 +++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 8b62858..91668ec 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; using Octobot.Data; @@ -72,7 +73,8 @@ public class BanCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteBanAsync( [Description("User to ban")] IUser target, - [Description("Ban reason")] string reason, + [Description("Ban reason")] [MaxLength(256)] + string reason, [Description("Ban duration")] TimeSpan? duration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) @@ -216,7 +218,8 @@ public class BanCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteUnban( [Description("User to unban")] IUser target, - [Description("Unban reason")] string reason) + [Description("Unban reason")] [MaxLength(256)] + string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 05552a2..9d9f745 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; @@ -67,7 +68,8 @@ public class KickCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteKick( [Description("Member to kick")] IUser target, - [Description("Kick reason")] string reason) + [Description("Kick reason")] [MaxLength(256)] + string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 50fe7a3..533edef 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; using Octobot.Data; @@ -69,7 +70,8 @@ public class MuteCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteMute( [Description("Member to mute")] IUser target, - [Description("Mute reason")] string reason, + [Description("Mute reason")] [MaxLength(256)] + string reason, [Description("Mute duration")] TimeSpan duration) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) @@ -233,7 +235,8 @@ public class MuteCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteUnmute( [Description("Member to unmute")] IUser target, - [Description("Unmute reason")] string reason) + [Description("Unmute reason")] [MaxLength(256)] + string reason) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index eb46d7c..db80eab 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; using Octobot.Data; @@ -119,7 +120,8 @@ public class RemindCommandGroup : CommandGroup public async Task ExecuteReminderAsync( [Description("After what period of time mention the reminder")] TimeSpan @in, - [Description("Reminder text")] string text) + [Description("Reminder text")] [MaxLength(512)] + string text) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 317b5c8..b1f9b95 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Text; using System.Text.Json.Nodes; using JetBrains.Annotations; @@ -167,7 +168,8 @@ public class SettingsCommandGroup : CommandGroup public async Task ExecuteEditSettingsAsync( [Description("The setting whose value you want to change")] AllOptionsEnum setting, - [Description("Setting value")] string value) + [Description("Setting value")] [MaxLength(512)] + string value) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { From a4b009a26f695a1b792f5b9a99797c470d2a2902 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 30 Oct 2023 09:37:16 +0500 Subject: [PATCH 206/329] Disable text commands (#175) This PR disables text commands as their presence was not intentional. --- src/Octobot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Octobot.cs b/src/Octobot.cs index 234f3a8..07bc058 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -78,7 +78,7 @@ public sealed class Octobot services.AddTransient() // Init .AddDiscordCaching() - .AddDiscordCommands(true) + .AddDiscordCommands(true, false) // Slash command event handlers .AddPreparationErrorEvent() .AddPostExecutionEvent() From b2879ad35ad6fab421e62843fe0859e2f9817275 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:51:54 +0500 Subject: [PATCH 207/329] Bump muno92/resharper_inspectcode from 1.8.4 to 1.9.0 (#176) Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.8.4 to 1.9.0. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 51ef712..77ae131 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.8.4 + uses: muno92/resharper_inspectcode@1.9.0 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement From 5f0d8062137afc3d3fef1a8fd04ab270181b4fed Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 4 Nov 2023 23:28:22 +0500 Subject: [PATCH 208/329] Use IFeedbackService interface instead of implementation (#178) This PR replaces usages of the `FeedbackService` implementation with the `IFeedbackService` interface. Using concrete implementations breaks the whole point of dependency injection, so it doesn't make sense to use them --- src/Commands/AboutCommandGroup.cs | 4 ++-- src/Commands/BanCommandGroup.cs | 4 ++-- src/Commands/ClearCommandGroup.cs | 4 ++-- src/Commands/Events/ErrorLoggingPostExecutionEvent.cs | 4 ++-- src/Commands/KickCommandGroup.cs | 4 ++-- src/Commands/MuteCommandGroup.cs | 4 ++-- src/Commands/PingCommandGroup.cs | 4 ++-- src/Commands/RemindCommandGroup.cs | 4 ++-- src/Commands/SettingsCommandGroup.cs | 4 ++-- src/Commands/ToolsCommandGroup.cs | 4 ++-- src/Extensions/FeedbackServiceExtensions.cs | 2 +- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 8529d48..8bfc881 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -32,14 +32,14 @@ public class AboutCommandGroup : CommandGroup }; private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestGuildAPI _guildApi; public AboutCommandGroup( ICommandContext context, GuildDataService guildData, - FeedbackService feedback, IDiscordRestUserAPI userApi, + IFeedbackService feedback, IDiscordRestUserAPI userApi, IDiscordRestGuildAPI guildApi) { _context = context; diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 91668ec..575e709 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -29,7 +29,7 @@ public class BanCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; @@ -37,7 +37,7 @@ public class BanCommandGroup : CommandGroup public BanCommandGroup( ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, - FeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) { _context = context; diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 714c9de..8e1f90d 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -27,14 +27,14 @@ public class ClearCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; private readonly UtilityService _utility; public ClearCommandGroup( IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService guildData, - FeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) + IFeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) { _channelApi = channelApi; _context = context; diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index a6daaf0..426fd35 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -18,10 +18,10 @@ namespace Octobot.Commands.Events; public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { private readonly ILogger _logger; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly IDiscordRestUserAPI _userApi; - public ErrorLoggingPostExecutionEvent(ILogger logger, FeedbackService feedback, + public ErrorLoggingPostExecutionEvent(ILogger logger, IFeedbackService feedback, IDiscordRestUserAPI userApi) { _logger = logger; diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 9d9f745..25fe1b5 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -26,7 +26,7 @@ public class KickCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; @@ -34,7 +34,7 @@ public class KickCommandGroup : CommandGroup public KickCommandGroup( ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, - FeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) { _context = context; diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 533edef..e431a53 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -28,14 +28,14 @@ namespace Octobot.Commands; public class MuteCommandGroup : CommandGroup { private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; private readonly UtilityService _utility; public MuteCommandGroup( - ICommandContext context, GuildDataService guildData, FeedbackService feedback, + ICommandContext context, GuildDataService guildData, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) { _context = context; diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 293fbff..84b15a0 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -27,13 +27,13 @@ public class PingCommandGroup : CommandGroup private readonly IDiscordRestChannelAPI _channelApi; private readonly DiscordGatewayClient _client; private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; public PingCommandGroup( IDiscordRestChannelAPI channelApi, ICommandContext context, DiscordGatewayClient client, - GuildDataService guildData, FeedbackService feedback, IDiscordRestUserAPI userApi) + GuildDataService guildData, IFeedbackService feedback, IDiscordRestUserAPI userApi) { _channelApi = channelApi; _context = context; diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index db80eab..6e4d31b 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -27,12 +27,12 @@ namespace Octobot.Commands; public class RemindCommandGroup : CommandGroup { private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; public RemindCommandGroup( - ICommandContext context, GuildDataService guildData, FeedbackService feedback, + ICommandContext context, GuildDataService guildData, IFeedbackService feedback, IDiscordRestUserAPI userApi) { _context = context; diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index b1f9b95..2c114c1 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -54,14 +54,14 @@ public class SettingsCommandGroup : CommandGroup }; private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; private readonly UtilityService _utility; public SettingsCommandGroup( ICommandContext context, GuildDataService guildData, - FeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) + IFeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) { _context = context; _guildData = guildData; diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 6c70c01..c0b99f5 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -26,13 +26,13 @@ namespace Octobot.Commands; public class ToolsCommandGroup : CommandGroup { private readonly ICommandContext _context; - private readonly FeedbackService _feedback; + private readonly IFeedbackService _feedback; private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; public ToolsCommandGroup( - ICommandContext context, FeedbackService feedback, + ICommandContext context, IFeedbackService feedback, GuildDataService guildData, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi) { diff --git a/src/Extensions/FeedbackServiceExtensions.cs b/src/Extensions/FeedbackServiceExtensions.cs index 401a865..739aa34 100644 --- a/src/Extensions/FeedbackServiceExtensions.cs +++ b/src/Extensions/FeedbackServiceExtensions.cs @@ -7,7 +7,7 @@ namespace Octobot.Extensions; public static class FeedbackServiceExtensions { public static async Task SendContextualEmbedResultAsync( - this FeedbackService feedback, Result embedResult, CancellationToken ct = default) + this IFeedbackService feedback, Result embedResult, CancellationToken ct = default) { if (!embedResult.IsDefined(out var embed)) { From f12d6d13c5ebca88ffa745b1d4eae4120787550a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 4 Nov 2023 23:33:37 +0500 Subject: [PATCH 209/329] Check interactions in MemberUpdateService before operating on members (#177) This PR fixes an issue that caused REST errors to occur in MemberUpdateService if the bot tries to interact with a member it can't interact with. The issue is fixed by returning from TickMemberDataAsync early if the member cannot be interacted with. An error message was planned, but it requires adding a lot of services and severely increasing the complexity. Contributors may feel free to add one if they deem so necessary. --- src/Responders/GuildLoadedResponder.cs | 46 ++++++++++------------ src/Services/Update/MemberUpdateService.cs | 38 ++++++++++++------ src/Services/UtilityService.cs | 35 +++++++++++++++- 3 files changed, 80 insertions(+), 39 deletions(-) diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 3e08060..bbf4a0a 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -4,11 +4,11 @@ using Octobot.Data; using Octobot.Extensions; using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Gateway.Events; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; -using Remora.Rest.Core; using Remora.Results; namespace Octobot.Responders; @@ -24,15 +24,17 @@ public class GuildLoadedResponder : IResponder private readonly GuildDataService _guildData; private readonly ILogger _logger; private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public GuildLoadedResponder( IDiscordRestChannelAPI channelApi, GuildDataService guildData, ILogger logger, - IDiscordRestUserAPI userApi) + IDiscordRestUserAPI userApi, UtilityService utility) { _channelApi = channelApi; _guildData = guildData; _logger = logger; _userApi = userApi; + _utility = utility; } public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) @@ -59,20 +61,7 @@ public class GuildLoadedResponder : IResponder if (data.DataLoadFailed) { - var errorEmbed = new EmbedBuilder() - .WithSmallTitle(Messages.DataLoadFailedTitle, bot) - .WithDescription(Messages.DataLoadFailedDescription) - .WithFooter(Messages.ContactDevelopers) - .WithColour(ColorsList.Red) - .Build(); - - if (!errorEmbed.IsDefined(out var errorBuilt)) - { - return Result.FromError(errorEmbed); - } - - return (Result)await _channelApi.CreateMessageAsync( - GetEmergencyFeedbackChannel(guild, data), embeds: new[] { errorBuilt }, ct: ct); + return await SendDataLoadFailed(guild, data, bot, ct); } _logger.LogInformation("Loaded guild {ID} (\"{Name}\")", guild.ID, guild.Name); @@ -105,22 +94,27 @@ public class GuildLoadedResponder : IResponder GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct); } - private static Snowflake GetEmergencyFeedbackChannel(IGuildCreate.IAvailableGuild guild, GuildData data) + private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct) { - var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings); - if (!privateFeedback.Empty()) + var errorEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.DataLoadFailedTitle, bot) + .WithDescription(Messages.DataLoadFailedDescription) + .WithFooter(Messages.ContactDevelopers) + .WithColour(ColorsList.Red) + .Build(); + + if (!errorEmbed.IsDefined(out var errorBuilt)) { - return privateFeedback; + return Result.FromError(errorEmbed); } - var publicFeedback = GuildSettings.PublicFeedbackChannel.Get(data.Settings); - if (!publicFeedback.Empty()) + var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); + if (!channelResult.IsDefined(out var channel)) { - return publicFeedback; + return Result.FromError(channelResult); } - return guild.SystemChannelID.AsOptional().IsDefined(out var systemChannel) - ? systemChannel - : guild.Channels[0].ID; + return (Result)await _channelApi.CreateMessageAsync( + channel, embeds: new[] { errorBuilt }, ct: ct); } } diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index ae099f6..b4289ce 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -29,14 +29,16 @@ public sealed partial class MemberUpdateService : BackgroundService private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly ILogger _logger; + private readonly UtilityService _utility; public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, - GuildDataService guildData, ILogger logger) + GuildDataService guildData, ILogger logger, UtilityService utility) { _channelApi = channelApi; _guildApi = guildApi; _guildData = guildData; _logger = logger; + _utility = utility; } protected override async Task ExecuteAsync(CancellationToken ct) @@ -90,21 +92,20 @@ public sealed partial class MemberUpdateService : BackgroundService return failedResults.AggregateErrors(); } + var interactionResult + = await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct); + if (!interactionResult.IsSuccess) + { + return Result.FromError(interactionResult); + } + + var canInteract = interactionResult.Entity is null; + if (data.MutedUntil is null) { data.Roles = guildMember.Roles.ToList().ConvertAll(r => r.Value); } - var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct); - failedResults.AddIfFailed(autoUnmuteResult); - - if (!defaultRole.Empty() && !data.Roles.Contains(defaultRole.Value)) - { - var addResult = await _guildApi.AddGuildMemberRoleAsync( - guildId, id, defaultRole, ct: ct); - failedResults.AddIfFailed(addResult); - } - if (!guildMember.User.IsDefined(out var user)) { failedResults.AddIfFailed(new ArgumentNullError(nameof(guildMember.User))); @@ -117,6 +118,21 @@ public sealed partial class MemberUpdateService : BackgroundService failedResults.AddIfFailed(reminderTickResult); } + if (!canInteract) + { + return Result.FromSuccess(); + } + + var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct); + failedResults.AddIfFailed(autoUnmuteResult); + + if (!defaultRole.Empty() && !data.Roles.Contains(defaultRole.Value)) + { + var addResult = await _guildApi.AddGuildMemberRoleAsync( + guildId, id, defaultRole, ct: ct); + failedResults.AddIfFailed(addResult); + } + if (GuildSettings.RenameHoistedUsers.Get(guildData.Settings)) { var filterResult = await FilterNicknameAsync(guildId, user, guildMember, ct); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index 5bc06ea..d40570a 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -63,7 +63,7 @@ public sealed class UtilityService : IHostedService /// /// public async Task> CheckInteractionsAsync( - Snowflake guildId, Snowflake interacterId, Snowflake targetId, string action, CancellationToken ct = default) + Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default) { if (interacterId == targetId) { @@ -100,7 +100,12 @@ public sealed class UtilityService : IHostedService return Result.FromError(rolesResult); } - var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId, ct); + if (interacterId is null) + { + return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember); + } + + var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); return interacterResult.IsDefined(out var interacter) ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter) : Result.FromError(interacterResult); @@ -246,4 +251,30 @@ public sealed class UtilityService : IHostedService return Result.FromSuccess(); } + + public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct) + { + var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings); + if (!privateFeedback.Empty()) + { + return privateFeedback; + } + + var publicFeedback = GuildSettings.PublicFeedbackChannel.Get(data.Settings); + if (!publicFeedback.Empty()) + { + return publicFeedback; + } + + if (guild.SystemChannelID.AsOptional().IsDefined(out var systemChannel)) + { + return systemChannel; + } + + var channelsResult = await _guildApi.GetGuildChannelsAsync(guild.ID, ct); + + return channelsResult.IsDefined(out var channels) + ? channels[0].ID + : Result.FromError(channelsResult); + } } From f785efcbc0d0e29cede1be68f5cbb35240e181af Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 6 Nov 2023 23:39:26 +0500 Subject: [PATCH 210/329] Add more information to guild loaded log message (#179) This PR adds additional information in the log about guilds as they are being loaded. The new information is the owner tag & ID and member count --- src/Responders/GuildLoadedResponder.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index bbf4a0a..5d5d68a 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -64,7 +64,14 @@ public class GuildLoadedResponder : IResponder return await SendDataLoadFailed(guild, data, bot, ct); } - _logger.LogInformation("Loaded guild {ID} (\"{Name}\")", guild.ID, guild.Name); + var ownerResult = await _userApi.GetUserAsync(guild.OwnerID, ct); + if (!ownerResult.IsDefined(out var owner)) + { + return Result.FromError(ownerResult); + } + + _logger.LogInformation("Loaded guild \"{Name}\" ({ID}) owned by {Owner} ({OwnerID}) with {MemberCount} members", + guild.Name, guild.ID, owner.GetTag(), owner.ID, guild.MemberCount); if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) { From bca1b1074989031c21ba3f7a44acd365f937fcf4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:04:45 +0300 Subject: [PATCH 211/329] Bump JetBrains.Annotations from 2023.2.0 to 2023.3.0 (#181) Bumps [JetBrains.Annotations](https://github.com/JetBrains/JetBrains.Annotations) from 2023.2.0 to 2023.3.0.
Commits
  • e00a7e4 2023.3
  • c2392bc 2023.3-eap4
  • a373835 Merge pull request #24 from estrizhok/main
  • 9380318 Allow NoReorderAttribute to be used for partial classes to indicate which par...
  • 0dea5ba Merge pull request #23 from mr146/main
  • 6565d31 fixes
  • a328420 Added IgnoreSpellingAndGrammarErrorsAttribute and AspMinimalApiImplicitEndpoi...
  • 53cd720 2023.3.0-eap2
  • 0a43548 Merge pull request #22 from tutushkin/main
  • 802aa5f Fixed after review,
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=JetBrains.Annotations&package-manager=nuget&previous-version=2023.2.0&new-version=2023.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Octobot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Octobot.csproj b/Octobot.csproj index 2c5b16b..3abe2d6 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -21,7 +21,7 @@ - + From c031b66eb4054dc3fef927abb690bdb93fad20d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 06:06:31 +0000 Subject: [PATCH 212/329] Bump muno92/resharper_inspectcode from 1.9.0 to 1.9.1 (#182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.9.0 to 1.9.1.
Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.9.1

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.9.0...1.9.1

Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.9.1 - 2023-11-08

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.9.0&new-version=1.9.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 77ae131..db4f4e9 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.9.0 + uses: muno92/resharper_inspectcode@1.9.1 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement From e65c7a469dcc7dbbb3215555ad65b6debee8e7c4 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 22 Nov 2023 13:19:45 +0500 Subject: [PATCH 213/329] Use TryGetValue instead of ContainsKey + index access to avoid double lookup (#193) This PR fixes an issue that is currently causing CI to fail in all pull requests: `Notice: "[CA1854] Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup" on /home/runner/work/Octobot/Octobot/src/Services/Update/ScheduledEventUpdateService.cs(107,4168)` The issue is resolved by following the advice mentioned in the notice. --- src/Services/Update/ScheduledEventUpdateService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index f1d3524..8872ddc 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -104,14 +104,13 @@ public sealed class ScheduledEventUpdateService : BackgroundService { foreach (var @event in events) { - if (!data.ScheduledEvents.ContainsKey(@event.ID.Value)) + if (!data.ScheduledEvents.TryGetValue(@event.ID.Value, out var eventData)) { data.ScheduledEvents.Add(@event.ID.Value, new ScheduledEventData(@event.ID.Value, @event.Name, @event.ScheduledStartTime, @event.Status)); continue; } - var eventData = data.ScheduledEvents[@event.ID.Value]; eventData.Name = @event.Name; eventData.ScheduledStartTime = @event.ScheduledStartTime; if (!eventData.ScheduleOnStatusUpdated) From b446c9731121cacea4f9c2f59b9de694a82c79c9 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 22 Nov 2023 13:26:10 +0500 Subject: [PATCH 214/329] Make issue templates less annoying (#194) Closes #184 This PR makes issue templates less annoying by removing some unnecessary text boxes and allowing issues to be created without the use of a template. In the Bug Report template, `Expected Behavior` and `Actual Behavior` were combined into one, and the `Known Workarounds` section was removed In the Feature Request template, `Considered Alternatives` was also removed. --- .github/ISSUE_TEMPLATE/bug-report.yml | 55 +++++----------------- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature-request.yml | 9 ---- 3 files changed, 12 insertions(+), 54 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 691e635..9c0524f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -14,6 +14,15 @@ body: placeholder: Description validations: required: true + - type: textarea + id: expected-vs-actual-behavior + attributes: + label: Expected vs. Actual Behavior + description: | + Provide a description of the expected behavior compared to the actual behavior. + placeholder: Expected vs. Actual Behavior + validations: + required: true - type: textarea id: repro-steps attributes: @@ -23,54 +32,12 @@ body: placeholder: Minimal Reproduction validations: required: true - - type: textarea - id: expected-behavior - attributes: - label: Expected behavior - description: | - Provide a description of the expected behavior. - placeholder: Expected behavior - validations: - required: true - - type: textarea - id: actual-behavior - attributes: - label: Actual behavior - description: | - Provide a description of the actual behavior observed. If applicable please include any error messages, exception stacktraces or memory dumps. - placeholder: Actual behavior - validations: - required: true - - type: textarea - id: known-workarounds - attributes: - label: Known Workarounds - description: | - Please provide a description of any known workarounds. - placeholder: Known Workarounds - validations: - required: false - - type: textarea - id: configuration - attributes: - label: Configuration - description: | - Please provide more information on your configuration: - * Which version of .NET is the bot running on? - * What OS and version, and what distro if applicable? - * What is the architecture (x64, x86, ARM, ARM64)? - * Do you know whether it is specific to that configuration? - * If possible, please provide the Configuration.json for the affected guild - * If applicable, provide the member data JSON for the affected members - placeholder: Configuration - validations: - required: false - type: textarea id: other-info attributes: - label: Other information + label: Other Information description: | If you have an idea where the problem might lie, let us know that here. Please include any pointers to code, relevant changes, or related issues you know of. - placeholder: Other information + placeholder: Other Information validations: required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3ba13e0..0086358 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1 @@ -blank_issues_enabled: false +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index e840cf6..2fd43c5 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -18,15 +18,6 @@ body: placeholder: Proposed Solution validations: required: true - - type: textarea - id: alternatives - attributes: - label: Considered Alternatives - description: | - Please provide a description of any alternative solutions or features you've considered. - placeholder: Considered Alternatives - validations: - required: false - type: textarea id: other-info attributes: From f908919ac95f261b9e8270f35c5500af88cf402e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 22 Nov 2023 13:29:27 +0500 Subject: [PATCH 215/329] Put edited message in cache if it's not in cache already (#196) Closes #183 In addition to the fix, now no error will be returned if the message doesn't exist in the cache as that is a (relatively) normal occurrence and isn't an indicator of an issue with the bot or its configuration --- src/Responders/MessageEditedResponder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index 7841b14..3b0a6aa 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -72,7 +72,8 @@ public class MessageEditedResponder : IResponder cacheKey, ct); if (!messageResult.IsDefined(out var message)) { - return Result.FromError(messageResult); + _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + return Result.FromSuccess(); } if (message.Content == newContent) From 71299c708606475d3ee12faa4c28dacbf18886bf Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 22 Nov 2023 13:31:58 +0500 Subject: [PATCH 216/329] Combine Dependabot PRs updating Remora.Discord packages into one (#195) Closes #191 --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 77a6b9e..4545f2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,3 +26,7 @@ updates: labels: - "type: change" - "area: build/ci" + groups: + remora: + patterns: + - "Remora.Discord.*" From b166300642b4e2b0f94634841e4df1a4357ddec8 Mon Sep 17 00:00:00 2001 From: Macintosh II <95250141+mctaylors@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:34:40 +0300 Subject: [PATCH 217/329] Update CDN links for Octobot banner (#192) Old CDN links are no longer working and they must be changed to new links to display Octobot banner properly. --- docs/README.md | 2 +- src/Commands/AboutCommandGroup.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index be73f87..cdd9ced 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@

- Octobot banner + Octobot banner

diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 8bfc881..ff58b66 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -96,7 +96,7 @@ public class AboutCommandGroup : CommandGroup var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl("https://mctaylors.ddns.net/cdn/octobot-banner.png") + .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png") .Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct); From c09d26fd6f10c658f8e29fbe0d045ddf9684b6b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:43:21 +0300 Subject: [PATCH 218/329] Bump muno92/resharper_inspectcode from 1.9.1 to 1.10.3 (#197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.9.1 to 1.10.3.
Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.10.3

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.10.2...1.10.3

1.10.2

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.10.1...1.10.2

1.10.1

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.10.0...1.10.1

1.10.0

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.9.3...1.10.0

1.9.3

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.9.2...1.9.3

1.9.2

What's Changed

... (truncated)

Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.10.3 - 2023-11-21

1.10.2 - 2023-11-21

1.10.1 - 2023-11-20

1.10.0 - 2023-11-20

1.9.3 - 2023-11-19

1.9.2 - 2023-11-13

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.9.1&new-version=1.10.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index db4f4e9..a10bdca 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.9.1 + uses: muno92/resharper_inspectcode@1.10.3 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement From 7e47c56015fef894e8e70bd65479f2fe17614fac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:27:55 +0500 Subject: [PATCH 219/329] Bump Microsoft.Extensions.Hosting from 7.0.1 to 8.0.0 (#187) Bumps [Microsoft.Extensions.Hosting](https://github.com/dotnet/runtime) from 7.0.1 to 8.0.0. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Octol1ttle --- Octobot.csproj | 2 +- src/Services/GuildDataService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Octobot.csproj b/Octobot.csproj index 3abe2d6..92f4755 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 78203b5..d76fffc 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -100,7 +100,7 @@ public sealed class GuildDataService : IHostedService JsonNode? jsonSettings = null; try { - jsonSettings = JsonNode.Parse(settingsStream); + jsonSettings = await JsonNode.ParseAsync(settingsStream, cancellationToken: ct); } catch (Exception e) { From 5fce01c15cb9a99bd8dc4264ce2cb6ec61e92978 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 06:48:33 +0000 Subject: [PATCH 220/329] Bump muno92/resharper_inspectcode from 1.10.3 to 1.11.0 (#199) --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index a10bdca..c08fa4d 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.10.3 + uses: muno92/resharper_inspectcode@1.11.0 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement From 18cdc63883097bc2eccfc5e88b9493bccf249556 Mon Sep 17 00:00:00 2001 From: Macintxsh II <95250141+mctaylors@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:09:47 +0300 Subject: [PATCH 221/329] Add StringBuilder & Markdown extensions (#206) In this PR, I have added StringBuilder extensions to avoid `.Append` reuse such as `.Append("- ").AppendLine()` Closes #205 --------- Signed-off-by: mctaylors --- src/Commands/AboutCommandGroup.cs | 2 +- src/Commands/BanCommandGroup.cs | 8 ++- src/Commands/KickCommandGroup.cs | 4 +- src/Commands/MuteCommandGroup.cs | 6 +-- src/Commands/RemindCommandGroup.cs | 15 +++--- src/Commands/SettingsCommandGroup.cs | 6 +-- src/Commands/ToolsCommandGroup.cs | 44 ++++++++-------- src/Extensions/MarkdownExtensions.cs | 16 ++++++ src/Extensions/StringBuilderExtensions.cs | 62 +++++++++++++++++++++++ 9 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 src/Extensions/MarkdownExtensions.cs create mode 100644 src/Extensions/StringBuilderExtensions.cs diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index ff58b66..e491470 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -88,7 +88,7 @@ public class AboutCommandGroup : CommandGroup guildId, dev.Id, ct); var tag = guildMemberResult.IsSuccess ? $"<@{dev.Id}>" : $"@{dev.Username}"; - builder.AppendLine($"- {tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); + builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); } builder.Append($"### [{Messages.AboutTitleRepository}](https://github.com/LabsDevelopment/Octobot)"); diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 575e709..3f89819 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -135,11 +135,10 @@ public class BanCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); } - var builder = new StringBuilder().Append("- ") - .AppendLine(string.Format(Messages.DescriptionActionReason, reason)); + var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); if (duration is not null) { - builder.Append("- ").Append( + builder.AppendBulletPoint( string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(DateTimeOffset.UtcNow.Add(duration.Value)))); @@ -274,8 +273,7 @@ public class BanCommandGroup : CommandGroup .WithColour(ColorsList.Green).Build(); var title = string.Format(Messages.UserUnbanned, target.GetTag()); - var description = new StringBuilder().Append("- ") - .Append(string.Format(Messages.DescriptionActionReason, reason)); + var description = new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason)); var logResult = _utility.LogActionAsync( data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 25fe1b5..f2a840d 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -134,7 +134,7 @@ public class KickCommandGroup : CommandGroup { var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereKicked) - .WithDescription($"- {string.Format(Messages.DescriptionActionReason, reason)}") + .WithDescription(MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason))) .WithActionFooter(executor) .WithCurrentTimestamp() .WithColour(ColorsList.Red) @@ -159,7 +159,7 @@ public class KickCommandGroup : CommandGroup data.GetOrCreateMemberData(target.ID).Roles.Clear(); var title = string.Format(Messages.UserKicked, target.GetTag()); - var description = $"- {string.Format(Messages.DescriptionActionReason, reason)}"; + var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); var logResult = _utility.LogActionAsync( data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); if (!logResult.IsSuccess) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index e431a53..2ce06ae 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -136,8 +136,8 @@ public class MuteCommandGroup : CommandGroup } var title = string.Format(Messages.UserMuted, target.GetTag()); - var description = new StringBuilder().Append("- ").AppendLine(string.Format(Messages.DescriptionActionReason, reason)) - .Append("- ").Append(string.Format( + var description = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)) + .AppendBulletPoint(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); var logResult = _utility.LogActionAsync( @@ -325,7 +325,7 @@ public class MuteCommandGroup : CommandGroup } var title = string.Format(Messages.UserUnmuted, target.GetTag()); - var description = $"- {string.Format(Messages.DescriptionActionReason, reason)}"; + var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); var logResult = _utility.LogActionAsync( data.Settings, channelId, executor, title, description, target, ColorsList.Green, ct: ct); if (!logResult.IsSuccess) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 6e4d31b..7c087e1 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -90,10 +90,9 @@ public class RemindCommandGroup : CommandGroup for (var i = 0; i < data.Reminders.Count; i++) { var reminder = data.Reminders[i]; - builder.Append("- ").AppendLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString()))) - .Append(" - ").AppendLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) - .Append(" - ") - .AppendLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); + builder.AppendBulletPointLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString()))) + .AppendSubBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) + .AppendSubBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); } var embed = new EmbedBuilder().WithSmallTitle( @@ -155,9 +154,9 @@ public class RemindCommandGroup : CommandGroup Text = text }); - var builder = new StringBuilder().Append("- ").AppendLine(string.Format( + var builder = new StringBuilder().AppendBulletPointLine(string.Format( Messages.ReminderText, Markdown.InlineCode(text))) - .Append("- ").Append(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); + .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderCreated, executor.GetTag()), executor) @@ -215,8 +214,8 @@ public class RemindCommandGroup : CommandGroup var reminder = data.Reminders[index]; var description = new StringBuilder() - .Append("- ").AppendLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) - .Append("- ").AppendLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) + .AppendBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); data.Reminders.RemoveAt(index); diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 2c114c1..dd0e9e3 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -138,9 +138,9 @@ public class SettingsCommandGroup : CommandGroup var optionName = AllOptions[i].Name; var optionValue = AllOptions[i].Display(cfg); - description.AppendLine($"- {$"Settings{optionName}".Localized()}") - .Append($" - {Markdown.InlineCode(optionName)}: ") - .AppendLine(optionValue); + description.AppendBulletPointLine($"Settings{optionName}".Localized()) + .AppendSubBulletPoint(Markdown.InlineCode(optionName)) + .Append(": ").AppendLine(optionValue); } var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, bot) diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index c0b99f5..0cb237a 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -102,11 +102,11 @@ public class ToolsCommandGroup : CommandGroup if (target.GlobalName is not null) { - builder.Append("- ").AppendLine(Messages.UserInfoDisplayName) + builder.AppendBulletPointLine(Messages.UserInfoDisplayName) .AppendLine(Markdown.InlineCode(target.GlobalName)); } - builder.Append("- ").AppendLine(Messages.UserInfoDiscordUserSince) + builder.AppendBulletPointLine(Messages.UserInfoDiscordUserSince) .AppendLine(Markdown.Timestamp(target.ID.Timestamp)); var memberData = data.GetOrCreateMemberData(target.ID); @@ -170,23 +170,23 @@ public class ToolsCommandGroup : CommandGroup { if (guildMember.Nickname.IsDefined(out var nickname)) { - builder.Append("- ").AppendLine(Messages.UserInfoGuildNickname) + builder.AppendBulletPointLine(Messages.UserInfoGuildNickname) .AppendLine(Markdown.InlineCode(nickname)); } - builder.Append("- ").AppendLine(Messages.UserInfoGuildMemberSince) + builder.AppendBulletPointLine(Messages.UserInfoGuildMemberSince) .AppendLine(Markdown.Timestamp(guildMember.JoinedAt)); if (guildMember.PremiumSince.IsDefined(out var premiumSince)) { - builder.Append("- ").AppendLine(Messages.UserInfoGuildMemberPremiumSince) + builder.AppendBulletPointLine(Messages.UserInfoGuildMemberPremiumSince) .AppendLine(Markdown.Timestamp(premiumSince.Value)); color = ColorsList.Magenta; } if (guildMember.Roles.Count > 0) { - builder.Append("- ").AppendLine(Messages.UserInfoGuildRoles); + builder.AppendBulletPointLine(Messages.UserInfoGuildRoles); for (var i = 0; i < guildMember.Roles.Count - 1; i++) { builder.Append($"<@&{guildMember.Roles[i]}>, "); @@ -202,30 +202,30 @@ public class ToolsCommandGroup : CommandGroup { if (memberData.BannedUntil < DateTimeOffset.MaxValue) { - builder.Append("- ").AppendLine(Messages.UserInfoBanned) - .Append(" - ").AppendLine(string.Format( + builder.AppendBulletPointLine(Messages.UserInfoBanned) + .AppendSubBulletPointLine(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.BannedUntil.Value))); return; } - builder.Append("- ").AppendLine(Messages.UserInfoBannedPermanently); + builder.AppendBulletPointLine(Messages.UserInfoBannedPermanently); } private static void AppendMuteInformation( MemberData memberData, DateTimeOffset? communicationDisabledUntil, StringBuilder builder) { - builder.Append("- ").AppendLine(Messages.UserInfoMuted); + builder.AppendBulletPointLine(Messages.UserInfoMuted); if (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) { - builder.Append(" - ").AppendLine(Messages.UserInfoMutedByMuteRole) - .Append(" - ").AppendLine(string.Format( + builder.AppendSubBulletPointLine(Messages.UserInfoMutedByMuteRole) + .AppendSubBulletPointLine(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.MutedUntil.Value))); } if (communicationDisabledUntil is not null) { - builder.Append(" - ").AppendLine(Messages.UserInfoMutedByTimeout) - .Append(" - ").AppendLine(string.Format( + builder.AppendSubBulletPointLine(Messages.UserInfoMutedByTimeout) + .AppendSubBulletPointLine(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(communicationDisabledUntil.Value))); } } @@ -282,13 +282,13 @@ public class ToolsCommandGroup : CommandGroup if (guild.Description is not null) { - description.Append("- ").AppendLine(Messages.GuildInfoDescription) + description.AppendBulletPointLine(Messages.GuildInfoDescription) .AppendLine(Markdown.InlineCode(guild.Description)); } - description.Append("- ").AppendLine(Messages.GuildInfoCreatedAt) + description.AppendBulletPointLine(Messages.GuildInfoCreatedAt) .AppendLine(Markdown.Timestamp(guild.ID.Timestamp)) - .Append("- ").AppendLine(Messages.GuildInfoOwner) + .AppendBulletPointLine(Messages.GuildInfoOwner) .AppendLine(Mention.User(guild.OwnerID)); var embedColor = ColorsList.Cyan; @@ -296,9 +296,9 @@ public class ToolsCommandGroup : CommandGroup if (guild.PremiumTier > PremiumTier.None) { description.Append("### ").AppendLine(Messages.GuildInfoServerBoost) - .Append("- ").Append(Messages.GuildInfoBoostTier) + .AppendBulletPoint(Messages.GuildInfoBoostTier) .Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumTier.ToString())) - .Append("- ").Append(Messages.GuildInfoBoostCount) + .AppendBulletPoint(Messages.GuildInfoBoostCount) .Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumSubscriptionCount.ToString())); embedColor = ColorsList.Magenta; } @@ -362,14 +362,14 @@ public class ToolsCommandGroup : CommandGroup var description = new StringBuilder().Append("# ").Append(i); - description.AppendLine().Append("- ").Append(string.Format( + description.AppendLine().AppendBulletPoint(string.Format( Messages.RandomMin, Markdown.InlineCode(min.ToString()))); if (secondNullable is null && first >= secondDefault) { description.Append(' ').Append(Messages.Default); } - description.AppendLine().Append("- ").Append(string.Format( + description.AppendLine().AppendBulletPoint(string.Format( Messages.RandomMax, Markdown.InlineCode(max.ToString()))); if (secondNullable is null && first < secondDefault) { @@ -449,7 +449,7 @@ public class ToolsCommandGroup : CommandGroup foreach (var markdownTimestamp in AllStyles.Select(style => Markdown.Timestamp(timestamp, style))) { - description.Append("- ").Append(Markdown.InlineCode(markdownTimestamp)) + description.AppendBulletPoint(Markdown.InlineCode(markdownTimestamp)) .Append(" → ").AppendLine(markdownTimestamp); } diff --git a/src/Extensions/MarkdownExtensions.cs b/src/Extensions/MarkdownExtensions.cs new file mode 100644 index 0000000..7b7f780 --- /dev/null +++ b/src/Extensions/MarkdownExtensions.cs @@ -0,0 +1,16 @@ +namespace Octobot.Extensions; + +public static class MarkdownExtensions +{ + /// + /// Formats a string to use Markdown Bullet formatting. + /// + /// The input text to format. + /// + /// A markdown-formatted bullet string. + /// + public static string BulletPoint(string text) + { + return $"- {text}"; + } +} diff --git a/src/Extensions/StringBuilderExtensions.cs b/src/Extensions/StringBuilderExtensions.cs new file mode 100644 index 0000000..ddd24a3 --- /dev/null +++ b/src/Extensions/StringBuilderExtensions.cs @@ -0,0 +1,62 @@ +using System.Text; + +namespace Octobot.Extensions; + +public static class StringBuilderExtensions +{ + /// + /// Appends the input string with Markdown Bullet formatting to the specified object. + /// + /// The object. + /// The string to append with bullet point. + /// + /// The builder with the appended string with Markdown Bullet formatting. + /// + public static StringBuilder AppendBulletPoint(this StringBuilder builder, string? value) + { + return builder.Append("- ").Append(value); + } + + /// + /// Appends the input string with Markdown Sub-Bullet formatting to the specified object. + /// + /// The object. + /// The string to append with sub-bullet point. + /// + /// The builder with the appended string with Markdown Sub-Bullet formatting. + /// + public static StringBuilder AppendSubBulletPoint(this StringBuilder builder, string? value) + { + return builder.Append(" - ").Append(value); + } + + /// + /// Appends the input string with Markdown Bullet formatting followed by + /// the default line terminator to the end of specified object. + /// + /// The object. + /// The string to append with bullet point. + /// + /// The builder with the appended string with Markdown Bullet formatting + /// and default line terminator at the end. + /// + public static StringBuilder AppendBulletPointLine(this StringBuilder builder, string? value) + { + return builder.Append("- ").AppendLine(value); + } + + /// + /// Appends the input string with Markdown Sub-Bullet formatting followed by + /// the default line terminator to the end of specified object. + /// + /// The object. + /// The string to append with sub-bullet point. + /// + /// The builder with the appended string with Markdown Sub-Bullet formatting + /// and default line terminator at the end. + /// + public static StringBuilder AppendSubBulletPointLine(this StringBuilder builder, string? value) + { + return builder.Append(" - ").AppendLine(value); + } +} From 4c50bdaff752c4341b9b58c7ece3f4f48bd9843a Mon Sep 17 00:00:00 2001 From: Macintxsh II <95250141+mctaylors@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:18:56 +0300 Subject: [PATCH 222/329] Bump .NET version to 8.0 (#207) Signed-off-by: mctaylors --- .github/workflows/build-pr.yml | 2 +- Octobot.csproj | 2 +- src/Extensions/CollectionExtensions.cs | 2 +- src/Extensions/StringExtensions.cs | 2 +- src/Services/Update/ScheduledEventUpdateService.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index c08fa4d..e71a955 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -26,6 +26,6 @@ jobs: uses: muno92/resharper_inspectcode@1.11.0 with: solutionPath: ./Octobot.sln - ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement + ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor extensions: ReSharperPlugin.CognitiveComplexity solutionWideAnalysis: true diff --git a/Octobot.csproj b/Octobot.csproj index 92f4755..41ac76b 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable enable 2.0.0 diff --git a/src/Extensions/CollectionExtensions.cs b/src/Extensions/CollectionExtensions.cs index 5322d09..9c873f2 100644 --- a/src/Extensions/CollectionExtensions.cs +++ b/src/Extensions/CollectionExtensions.cs @@ -8,7 +8,7 @@ public static class CollectionExtensions this IEnumerable source, Func selector) { var list = source.ToList(); - return list.Any() ? list.Max(selector) : default; + return list.Count > 0 ? list.Max(selector) : default; } public static void AddIfFailed(this List list, Result result) diff --git a/src/Extensions/StringExtensions.cs b/src/Extensions/StringExtensions.cs index 13cd88a..cb8d606 100644 --- a/src/Extensions/StringExtensions.cs +++ b/src/Extensions/StringExtensions.cs @@ -45,7 +45,7 @@ public static class StringExtensions { s = s.SanitizeForBlockCode(); return - $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith("`", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; + $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith('`') || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; } public static string Localized(this string key) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 8872ddc..af940c8 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -126,7 +126,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService { var filtered = from.Where(schEvent => schEvent.ID == id); var filteredArray = filtered.ToArray(); - return filteredArray.Any() + return filteredArray.Length > 0 ? Result.FromSuccess(filteredArray.Single()) : new NotFoundError(); } From bcd1db8c8ec36692b140f482f603cc4d38770c85 Mon Sep 17 00:00:00 2001 From: neroduckale <100025711+neroduckale@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:24:55 +0500 Subject: [PATCH 223/329] Fix inspection warnings (#208) Signed-off-by: neroduckale <100025711+neroduckale@users.noreply.github.com> --- src/Commands/RemindCommandGroup.cs | 16 ++++++++-------- src/Commands/SettingsCommandGroup.cs | 6 +++--- src/Commands/ToolsCommandGroup.cs | 12 ++++++------ src/Data/MemberData.cs | 4 ++-- src/Services/GuildDataService.cs | 4 ++-- src/Services/Update/SongUpdateService.cs | 5 +---- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 7c087e1..79a583b 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -75,7 +75,7 @@ public class RemindCommandGroup : CommandGroup return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), executor, bot, CancellationToken); } - private async Task ListRemindersAsync(MemberData data, IUser executor, IUser bot, CancellationToken ct) + private Task ListRemindersAsync(MemberData data, IUser executor, IUser bot, CancellationToken ct) { if (data.Reminders.Count == 0) { @@ -83,7 +83,7 @@ public class RemindCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var builder = new StringBuilder(); @@ -101,7 +101,7 @@ public class RemindCommandGroup : CommandGroup .WithColour(ColorsList.Cyan) .Build(); - return await _feedback.SendContextualEmbedResultAsync( + return _feedback.SendContextualEmbedResultAsync( embed, ct); } @@ -139,7 +139,7 @@ public class RemindCommandGroup : CommandGroup return await AddReminderAsync(@in, text, data, channelId, executor, CancellationToken); } - private async Task AddReminderAsync( + private Task AddReminderAsync( TimeSpan @in, string text, GuildData data, Snowflake channelId, IUser executor, CancellationToken ct = default) { @@ -165,7 +165,7 @@ public class RemindCommandGroup : CommandGroup .WithFooter(string.Format(Messages.ReminderPosition, memberData.Reminders.Count)) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct); } /// @@ -199,7 +199,7 @@ public class RemindCommandGroup : CommandGroup return await DeleteReminderAsync(data.GetOrCreateMemberData(executorId), position - 1, bot, CancellationToken); } - private async Task DeleteReminderAsync(MemberData data, int index, IUser bot, + private Task DeleteReminderAsync(MemberData data, int index, IUser bot, CancellationToken ct) { if (index >= data.Reminders.Count) @@ -208,7 +208,7 @@ public class RemindCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); } var reminder = data.Reminders[index]; @@ -224,7 +224,7 @@ public class RemindCommandGroup : CommandGroup .WithColour(ColorsList.Green) .Build(); - return await _feedback.SendContextualEmbedResultAsync( + return _feedback.SendContextualEmbedResultAsync( embed, ct); } } diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index dd0e9e3..60323d7 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -105,7 +105,7 @@ public class SettingsCommandGroup : CommandGroup return await SendSettingsListAsync(cfg, bot, page, CancellationToken); } - private async Task SendSettingsListAsync(JsonNode cfg, IUser bot, int page, + private Task SendSettingsListAsync(JsonNode cfg, IUser bot, int page, CancellationToken ct = default) { var description = new StringBuilder(); @@ -124,7 +124,7 @@ public class SettingsCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); + return _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); } footer.Append($"{Messages.Page} {page}/{totalPages} "); @@ -149,7 +149,7 @@ public class SettingsCommandGroup : CommandGroup .WithFooter(footer.ToString()) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct); } /// diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 0cb237a..be4c2c1 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -276,7 +276,7 @@ public class ToolsCommandGroup : CommandGroup return await ShowGuildInfoAsync(bot, guild, CancellationToken); } - private async Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct) + private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct) { var description = new StringBuilder().AppendLine($"## {guild.Name}"); @@ -312,7 +312,7 @@ public class ToolsCommandGroup : CommandGroup .WithFooter($"ID: {guild.ID.ToString()}") .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct); } /// @@ -349,7 +349,7 @@ public class ToolsCommandGroup : CommandGroup return await SendRandomNumberAsync(first, second, executor, CancellationToken); } - private async Task SendRandomNumberAsync(long first, long? secondNullable, + private Task SendRandomNumberAsync(long first, long? secondNullable, IUser executor, CancellationToken ct) { const long secondDefault = 0; @@ -389,7 +389,7 @@ public class ToolsCommandGroup : CommandGroup .WithColour(embedColor) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct); } private static readonly TimestampStyle[] AllStyles = @@ -435,7 +435,7 @@ public class ToolsCommandGroup : CommandGroup return await SendTimestampAsync(offset, executor, CancellationToken); } - private async Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct) + private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct) { var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds(); @@ -459,6 +459,6 @@ public class ToolsCommandGroup : CommandGroup .WithColour(ColorsList.Blue) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct); } } diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index b63f8ad..0b0cfb2 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -18,6 +18,6 @@ public sealed class MemberData public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } public DateTimeOffset? MutedUntil { get; set; } - public List Roles { get; set; } = new(); - public List Reminders { get; } = new(); + public List Roles { get; set; } = []; + public List Reminders { get; } = []; } diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index d76fffc..3cc8cea 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -39,7 +39,7 @@ public sealed class GuildDataService : IHostedService SaveAsync(CancellationToken.None).GetAwaiter().GetResult(); } - public async Task SaveAsync(CancellationToken ct) + public Task SaveAsync(CancellationToken ct) { var tasks = new List(); var datas = _datas.Values.ToArray(); @@ -53,7 +53,7 @@ public sealed class GuildDataService : IHostedService SerializeObjectSafelyAsync(memberData, $"{data.MemberDataPath}/{memberData.Id}.json", ct))); } - await Task.WhenAll(tasks); + return Task.WhenAll(tasks); } private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct) diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index f1ef296..c8221fc 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -26,10 +26,7 @@ public sealed class SongUpdateService : BackgroundService ("Squid Sisters", "Ink Me Up", new TimeSpan(0, 2, 13)) }; - private readonly List _activityList = new(1) - { - new Activity("with Remora.Discord", ActivityType.Game) - }; + private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)]; private readonly DiscordGatewayClient _client; private readonly GuildDataService _guildData; From 42ab11d2533f60d816d0e6dee332b45786febf4b Mon Sep 17 00:00:00 2001 From: Macintxsh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:06:50 +0300 Subject: [PATCH 224/329] Rename "Event details" button to "Open Event Info" (#209) The correct phrasing used by Discord is "Event Info" ![image](https://github.com/LabsDevelopment/Octobot/assets/95250141/8165b70d-4c81-4b85-8251-db2de6a7f4ca) Signed-off-by: mctaylors --- locale/Messages.resx | 4 ++-- locale/Messages.ru.resx | 4 ++-- locale/Messages.tt-ru.resx | 4 ++-- src/Messages.Designer.cs | 8 ++++---- src/Services/Update/ScheduledEventUpdateService.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index a9367f7..f145ab2 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -348,8 +348,8 @@ The event will start at {0} until {1} in {2} - - Event details + + Open Event Info The event has lasted for `{0}` diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index d0cbd79..5b97eda 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -345,8 +345,8 @@ Событие пройдёт с {0} до {1} в {2} - - Подробнее о событии + + Открыть сведения о событии Событие длилось `{0}` diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 3bed232..2761827 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -348,8 +348,8 @@ движуха будет происходить с {0} до {1} в {2} - - побольше о движухе + + открыть ивент все это длилось `{0}` diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index e2184d7..33ba7b3 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -534,9 +534,9 @@ namespace Octobot { } } - internal static string EventDetailsButton { + internal static string OpenEventInfoButton { get { - return ResourceManager.GetString("EventDetailsButton", resourceCulture); + return ResourceManager.GetString("OpenEventInfoButton", resourceCulture); } } @@ -1012,7 +1012,7 @@ namespace Octobot { return ResourceManager.GetString("DataLoadFailedDescription", resourceCulture); } } - + internal static string CommandExecutionFailed { get @@ -1020,7 +1020,7 @@ namespace Octobot { return ResourceManager.GetString("CommandExecutionFailed", resourceCulture); } } - + internal static string ContactDevelopers { get diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index af940c8..9ec9fcf 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -226,7 +226,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService var button = new ButtonComponent( ButtonComponentStyle.Link, - Messages.EventDetailsButton, + Messages.OpenEventInfoButton, new PartialEmoji(Name: "📋"), URL: $"https://discord.com/events/{scheduledEvent.GuildID}/{scheduledEvent.ID}" ); From eeaff23fb537a8d1ba7b68f55721a5db8adf1db7 Mon Sep 17 00:00:00 2001 From: Macintxsh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:15:44 +0300 Subject: [PATCH 225/329] Rewrite music list (#210) Changes: - Use timestamps from [Splatunes](https://splatoonwiki.org/wiki/Splatune_(Splatoon_Original_Soundtrack)) - Increase track count to 20 - Replace Calamari Inkantation 3MIX with OG Calamari Inkantation due to long duration Signed-off-by: mctaylors --- src/Services/Update/SongUpdateService.cs | 34 ++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index c8221fc..391c416 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -10,20 +10,26 @@ public sealed class SongUpdateService : BackgroundService { private static readonly (string Author, string Name, TimeSpan Duration)[] SongList = { - ("Yoko & the Gold Bazookas", "Rockagilly Blues", new TimeSpan(0, 3, 37)), - ("Splatoon 3", "Seep and Destroy", new TimeSpan(0, 2, 42)), - ("Deep Cut", "Big Betrayal", new TimeSpan(0, 1, 42)), - ("Squid Sisters", "Tomorrow's Nostalgia Today", new TimeSpan(0, 2, 8)), - ("Deep Cut", "Anarchy Rainbow", new TimeSpan(0, 1, 51)), - ("Squid Sisters feat. Ian BGM", "Liquid Sunshine", new TimeSpan(0, 1, 32)), - ("Damp Socks feat. Off the Hook", "Candy-Coated Rocks", new TimeSpan(0, 1, 11)), - ("H2Whoa", "Aquasonic", new TimeSpan(0, 1, 1)), - ("Yoko & the Gold Bazookas", "Ska-BLAM", new TimeSpan(0, 4, 4)), - ("Off the Hook", "Muck Warfare", new TimeSpan(0, 3, 39)), - ("Off the Hook", "Acid Hues", new TimeSpan(0, 3, 39)), - ("Off the Hook", "Shark Bytes", new TimeSpan(0, 3, 48)), - ("DJ Octavio feat. Squid Sisters & Deep Cut", "Calamari Inkantation 3MIX", new TimeSpan(0, 7, 9)), - ("Squid Sisters", "Ink Me Up", new TimeSpan(0, 2, 13)) + ("Yoko & the Gold Bazookas", "Rockagilly Blues", new TimeSpan(0, 2, 52)), + ("Deep Cut", "Big Betrayal", new TimeSpan(0, 5, 55)), + ("Squid Sisters", "Tomorrow's Nostalgia Today", new TimeSpan(0, 3, 7)), + ("Deep Cut", "Anarchy Rainbow", new TimeSpan(0, 3, 20)), + ("Squid Sisters feat. Ian BGM", "Liquid Sunshine", new TimeSpan(0, 2, 37)), + ("Damp Socks feat. Off the Hook", "Candy-Coated Rocks", new TimeSpan(0, 2, 58)), + ("H2Whoa", "Aquasonic", new TimeSpan(0, 2, 51)), + ("Yoko & the Gold Bazookas", "Ska-BLAM", new TimeSpan(0, 2, 57)), + ("Off the Hook", "Muck Warfare", new TimeSpan(0, 3, 20)), + ("Off the Hook", "Acid Hues", new TimeSpan(0, 3, 15)), + ("Off the Hook", "Shark Bytes", new TimeSpan(0, 3, 34)), + ("Squid Sisters", "Calamari Inkantation", new TimeSpan(0, 2, 14)), + ("Squid Sisters", "Ink Me Up", new TimeSpan(0, 2, 13)), + ("Chirpy Chips", "No Quarters", new TimeSpan(0, 2, 36)), + ("Chirpy Chips", "Shellfie", new TimeSpan(0, 2, 1)), + ("Dedf1sh", "#11 above", new TimeSpan(0, 2, 10)), + ("Callie", "Bomb Rush Blush", new TimeSpan(0, 2, 18)), + ("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)), + ("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)), + ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5)) }; private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)]; From a9f2f8eeefc3b550de6d15fd5305e3e1c33f3a97 Mon Sep 17 00:00:00 2001 From: Macintxsh II <95250141+mctaylors@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:26:04 +0300 Subject: [PATCH 226/329] Add application icon (#211) Signed-off-by: mctaylors --- Octobot.csproj | 1 + docs/octobot.ico | Bin 0 -> 120000 bytes 2 files changed, 1 insertion(+) create mode 100644 docs/octobot.ico diff --git a/Octobot.csproj b/Octobot.csproj index 41ac76b..7d3500b 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -16,6 +16,7 @@ LabsDevelopment en A general-purpose Discord bot for moderation written in C# + docs/octobot.ico diff --git a/docs/octobot.ico b/docs/octobot.ico new file mode 100644 index 0000000000000000000000000000000000000000..147b71659fea572ce955ba6efb8dc7314f893caf GIT binary patch literal 120000 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 RC7>g9JYD@<);T3K0RUua%(Vaj literal 0 HcmV?d00001 From e21e19321821716b74fe21455430ca172b08a13c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 06:02:34 +0000 Subject: [PATCH 227/329] Bump muno92/resharper_inspectcode from 1.11.0 to 1.11.1 (#212) --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index e71a955..b697dac 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.0 + uses: muno92/resharper_inspectcode@1.11.1 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 59c2c5eada6da66f5472bef7859c3fe1975d6f91 Mon Sep 17 00:00:00 2001 From: neroduckale <100025711+neroduckale@users.noreply.github.com> Date: Tue, 12 Dec 2023 01:38:26 +0500 Subject: [PATCH 228/329] Add link to original message for activated reminders and /listremind (#203) --- src/Commands/RemindCommandGroup.cs | 37 +++++++++++++--------- src/Data/Reminder.cs | 3 +- src/Services/Update/MemberUpdateService.cs | 15 ++++++--- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 79a583b..1966b9b 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -26,19 +26,21 @@ namespace Octobot.Commands; [UsedImplicitly] public class RemindCommandGroup : CommandGroup { - private readonly ICommandContext _context; + private readonly IInteractionCommandContext _context; private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; + private readonly IDiscordRestInteractionAPI _interactionApi; public RemindCommandGroup( - ICommandContext context, GuildDataService guildData, IFeedbackService feedback, - IDiscordRestUserAPI userApi) + IInteractionCommandContext context, GuildDataService guildData, IFeedbackService feedback, + IDiscordRestUserAPI userApi, IDiscordRestInteractionAPI interactionApi) { _context = context; _guildData = guildData; _feedback = feedback; _userApi = userApi; + _interactionApi = interactionApi; } /// @@ -72,10 +74,10 @@ public class RemindCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), executor, bot, CancellationToken); + return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), guildId, executor, bot, CancellationToken); } - private Task ListRemindersAsync(MemberData data, IUser executor, IUser bot, CancellationToken ct) + private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct) { if (data.Reminders.Count == 0) { @@ -92,7 +94,8 @@ public class RemindCommandGroup : CommandGroup var reminder = data.Reminders[i]; builder.AppendBulletPointLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString()))) .AppendSubBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) - .AppendSubBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); + .AppendSubBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))) + .AppendSubBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); } var embed = new EmbedBuilder().WithSmallTitle( @@ -139,25 +142,29 @@ public class RemindCommandGroup : CommandGroup return await AddReminderAsync(@in, text, data, channelId, executor, CancellationToken); } - private Task AddReminderAsync( - TimeSpan @in, string text, GuildData data, + private async Task AddReminderAsync(TimeSpan @in, string text, GuildData data, Snowflake channelId, IUser executor, CancellationToken ct = default) { - var remindAt = DateTimeOffset.UtcNow.Add(@in); var memberData = data.GetOrCreateMemberData(executor.ID); + var remindAt = DateTimeOffset.UtcNow.Add(@in); + var responseResult = await _interactionApi.GetOriginalInteractionResponseAsync(_context.Interaction.ApplicationID, _context.Interaction.Token, ct); + if (!responseResult.IsDefined(out var response)) + { + return (Result)responseResult; + } memberData.Reminders.Add( new Reminder { At = remindAt, - Channel = channelId.Value, - Text = text + ChannelId = channelId.Value, + Text = text, + MessageId = response.ID.Value }); - var builder = new StringBuilder().AppendBulletPointLine(string.Format( - Messages.ReminderText, Markdown.InlineCode(text))) + var builder = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(text))) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); - var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderCreated, executor.GetTag()), executor) .WithDescription(builder.ToString()) @@ -165,7 +172,7 @@ public class RemindCommandGroup : CommandGroup .WithFooter(string.Format(Messages.ReminderPosition, memberData.Reminders.Count)) .Build(); - return _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct); } /// diff --git a/src/Data/Reminder.cs b/src/Data/Reminder.cs index 42144f9..f21b222 100644 --- a/src/Data/Reminder.cs +++ b/src/Data/Reminder.cs @@ -4,5 +4,6 @@ public struct Reminder { public DateTimeOffset At { get; init; } public string Text { get; init; } - public ulong Channel { get; init; } + public ulong ChannelId { get; init; } + public ulong MessageId { get; init; } } diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index b4289ce..c3139c3 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -1,3 +1,4 @@ +using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -114,7 +115,7 @@ public sealed partial class MemberUpdateService : BackgroundService for (var i = data.Reminders.Count - 1; i >= 0; i--) { - var reminderTickResult = await TickReminderAsync(data.Reminders[i], user, data, ct); + var reminderTickResult = await TickReminderAsync(data.Reminders[i], user, data, guildId, ct); failedResults.AddIfFailed(reminderTickResult); } @@ -217,17 +218,21 @@ public sealed partial class MemberUpdateService : BackgroundService [GeneratedRegex("[^0-9A-Za-zА-Яа-яЁё]")] private static partial Regex IllegalChars(); - private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData data, CancellationToken ct) + private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData data, Snowflake guildId, + CancellationToken ct) { if (DateTimeOffset.UtcNow < reminder.At) { return Result.FromSuccess(); } + var builder = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) + .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); + var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.Reminder, user.GetTag()), user) - .WithDescription( - string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) + .WithDescription(builder.ToString()) .WithColour(ColorsList.Magenta) .Build(); @@ -237,7 +242,7 @@ public sealed partial class MemberUpdateService : BackgroundService } var messageResult = await _channelApi.CreateMessageAsync( - reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); + reminder.ChannelId.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); if (!messageResult.IsSuccess) { return Result.FromError(messageResult); From eed08b237b0d08faf578a9cd1a084b4227666e32 Mon Sep 17 00:00:00 2001 From: Macintxsh II <95250141+mctaylors@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:01:10 +0300 Subject: [PATCH 229/329] Recommend to install .NET 8 in README.md (#213) completely forgot about this while making #207 Signed-off-by: Macintxsh II <95250141+mctaylors@users.noreply.github.com> --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index cdd9ced..10ee63c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,7 +24,7 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr ## Running Octobot -1. Install [.NET 7 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) +1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents! 3. Clone this repository and open `Octobot` folder. ``` From 8594dfdb9bbcd77e9388b7bb02a19ca02a1cb0fd Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 13 Dec 2023 22:44:55 +0500 Subject: [PATCH 230/329] Bump Remora.Commands to fix issue with optional slash command args (#214) Fixes an exception that would occur when a slash command that has optional arguments would be called without them. See https://github.com/Remora/Remora.Discord/issues/319 for details ![image](https://github.com/LabsDevelopment/Octobot/assets/61277953/6589dee9-4bba-484e-9f77-b23dae514f45) --- Octobot.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Octobot.csproj b/Octobot.csproj index 7d3500b..5258c89 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -25,6 +25,7 @@ + From b284ac28d506e2199d19e10e0d85d84bddf21f94 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 17 Dec 2023 19:08:24 +0500 Subject: [PATCH 231/329] Disable "Convert to Primary Constructor" warning (#216) Me and other .NET developers hold the stance that primary constructors are not ready for production use, particularly with dependency injection. There are debates regarding the styling of primary constructors, but the bigger issue is that a primary constructor parameter cannot be made `readonly` (which is crucial with dependency injection). The inspection "Convert to Primary Constructor" was disabled in ReSharper CLI when we updated to .NET 8, but the warning is still present for ReSharper and Rider users. This PR disables this warning to avoid developers accidentally using a primary constructor. --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 4982036..ff9c068 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1022,7 +1022,7 @@ resharper_convert_to_constant_local_highlighting = warning resharper_convert_to_lambda_expression_highlighting = warning resharper_convert_to_local_function_highlighting = warning resharper_convert_to_null_coalescing_compound_assignment_highlighting = warning -resharper_convert_to_primary_constructor_highlighting = warning +resharper_convert_to_primary_constructor_highlighting = none resharper_convert_to_static_class_highlighting = warning resharper_convert_to_using_declaration_highlighting = warning resharper_convert_type_check_pattern_to_null_check_highlighting = warning From 9a23e1d5337e0232686feaef34cbb789b06cfe74 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 17 Dec 2023 18:44:18 +0300 Subject: [PATCH 232/329] FeedbackServiceExtensions: Add FeedbackMessageOptions support (#219) Required for #218 --- src/Commands/AboutCommandGroup.cs | 2 +- src/Commands/BanCommandGroup.cs | 10 +++++----- src/Commands/ClearCommandGroup.cs | 4 ++-- .../Events/ErrorLoggingPostExecutionEvent.cs | 2 +- src/Commands/KickCommandGroup.cs | 6 +++--- src/Commands/MuteCommandGroup.cs | 16 ++++++++-------- src/Commands/PingCommandGroup.cs | 2 +- src/Commands/RemindCommandGroup.cs | 12 +++++------- src/Commands/SettingsCommandGroup.cs | 12 ++++++------ src/Commands/ToolsCommandGroup.cs | 8 ++++---- src/Extensions/FeedbackServiceExtensions.cs | 6 ++++-- 11 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index e491470..45077e6 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -99,6 +99,6 @@ public class AboutCommandGroup : CommandGroup .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png") .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 3f89819..7493505 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -117,7 +117,7 @@ public class BanCommandGroup : CommandGroup var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var interactionResult @@ -132,7 +132,7 @@ public class BanCommandGroup : CommandGroup var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); } var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); @@ -190,7 +190,7 @@ public class BanCommandGroup : CommandGroup return Result.FromError(logResult.Error); } - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } /// @@ -255,7 +255,7 @@ public class BanCommandGroup : CommandGroup var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); } var unbanResult = await _guildApi.RemoveGuildBanAsync( @@ -281,6 +281,6 @@ public class BanCommandGroup : CommandGroup return Result.FromError(logResult.Error); } - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 8e1f90d..7ebd4ea 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -121,7 +121,7 @@ public class ClearCommandGroup : CommandGroup var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.NoMessagesToClear, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var title = author is not null @@ -146,6 +146,6 @@ public class ClearCommandGroup : CommandGroup var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithColour(ColorsList.Green).Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 426fd35..2d5f606 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -65,6 +65,6 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent .WithColour(ColorsList.Red) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index f2a840d..1ef6057 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -104,7 +104,7 @@ public class KickCommandGroup : CommandGroup var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken); } return await KickUserAsync(executor, target, reason, guild, channelId, data, bot, CancellationToken); @@ -126,7 +126,7 @@ public class KickCommandGroup : CommandGroup var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct); @@ -171,6 +171,6 @@ public class KickCommandGroup : CommandGroup string.Format(Messages.UserKicked, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 2ce06ae..6a28f38 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -101,7 +101,7 @@ public class MuteCommandGroup : CommandGroup var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken); } return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); @@ -124,7 +124,7 @@ public class MuteCommandGroup : CommandGroup var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var until = DateTimeOffset.UtcNow.Add(duration); // >:) @@ -151,7 +151,7 @@ public class MuteCommandGroup : CommandGroup string.Format(Messages.UserMuted, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } private async Task SelectMuteMethodAsync( @@ -202,7 +202,7 @@ public class MuteCommandGroup : CommandGroup .WithDescription(Messages.DurationRequiredForTimeOuts) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var muteResult = await _guildApi.ModifyGuildMemberAsync( @@ -266,7 +266,7 @@ public class MuteCommandGroup : CommandGroup var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, CancellationToken); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken); } return await RemoveMuteAsync(executor, target, reason, guildId, data, channelId, bot, CancellationToken); @@ -289,7 +289,7 @@ public class MuteCommandGroup : CommandGroup var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, ct); @@ -307,7 +307,7 @@ public class MuteCommandGroup : CommandGroup var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var removeMuteRoleAsync = @@ -337,7 +337,7 @@ public class MuteCommandGroup : CommandGroup string.Format(Messages.UserUnmuted, target.GetTag()), target) .WithColour(ColorsList.Green).Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } private async Task RemoveMuteRoleAsync( diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 84b15a0..31fa6dc 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -97,6 +97,6 @@ public class PingCommandGroup : CommandGroup .WithCurrentTimestamp() .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 1966b9b..67e7910 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -85,7 +85,7 @@ public class RemindCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - return _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var builder = new StringBuilder(); @@ -104,8 +104,7 @@ public class RemindCommandGroup : CommandGroup .WithColour(ColorsList.Cyan) .Build(); - return _feedback.SendContextualEmbedResultAsync( - embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } /// @@ -172,7 +171,7 @@ public class RemindCommandGroup : CommandGroup .WithFooter(string.Format(Messages.ReminderPosition, memberData.Reminders.Count)) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } /// @@ -215,7 +214,7 @@ public class RemindCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - return _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var reminder = data.Reminders[index]; @@ -231,7 +230,6 @@ public class RemindCommandGroup : CommandGroup .WithColour(ColorsList.Green) .Build(); - return _feedback.SendContextualEmbedResultAsync( - embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 60323d7..a8891bd 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -124,7 +124,7 @@ public class SettingsCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - return _feedback.SendContextualEmbedResultAsync(errorEmbed, ct); + return _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); } footer.Append($"{Messages.Page} {page}/{totalPages} "); @@ -149,7 +149,7 @@ public class SettingsCommandGroup : CommandGroup .WithFooter(footer.ToString()) .Build(); - return _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } /// @@ -207,7 +207,7 @@ public class SettingsCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } var builder = new StringBuilder(); @@ -230,7 +230,7 @@ public class SettingsCommandGroup : CommandGroup .WithColour(ColorsList.Green) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } /// @@ -284,7 +284,7 @@ public class SettingsCommandGroup : CommandGroup .WithColour(ColorsList.Green) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } private async Task ResetAllSettingsAsync(JsonNode cfg, IUser bot, @@ -305,6 +305,6 @@ public class SettingsCommandGroup : CommandGroup .WithColour(ColorsList.Green) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index be4c2c1..78058cb 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -163,7 +163,7 @@ public class ToolsCommandGroup : CommandGroup .WithFooter($"ID: {target.ID.ToString()}") .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct); + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder) @@ -312,7 +312,7 @@ public class ToolsCommandGroup : CommandGroup .WithFooter($"ID: {guild.ID.ToString()}") .Build(); - return _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } /// @@ -389,7 +389,7 @@ public class ToolsCommandGroup : CommandGroup .WithColour(embedColor) .Build(); - return _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } private static readonly TimestampStyle[] AllStyles = @@ -459,6 +459,6 @@ public class ToolsCommandGroup : CommandGroup .WithColour(ColorsList.Blue) .Build(); - return _feedback.SendContextualEmbedResultAsync(embed, ct); + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } } diff --git a/src/Extensions/FeedbackServiceExtensions.cs b/src/Extensions/FeedbackServiceExtensions.cs index 739aa34..40e0d53 100644 --- a/src/Extensions/FeedbackServiceExtensions.cs +++ b/src/Extensions/FeedbackServiceExtensions.cs @@ -1,4 +1,5 @@ using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Services; using Remora.Results; @@ -7,13 +8,14 @@ namespace Octobot.Extensions; public static class FeedbackServiceExtensions { public static async Task SendContextualEmbedResultAsync( - this IFeedbackService feedback, Result embedResult, CancellationToken ct = default) + this IFeedbackService feedback, Result embedResult, + FeedbackMessageOptions? options = null, CancellationToken ct = default) { if (!embedResult.IsDefined(out var embed)) { return Result.FromError(embedResult); } - return (Result)await feedback.SendContextualEmbedAsync(embed, ct: ct); + return (Result)await feedback.SendContextualEmbedAsync(embed, options, ct); } } From 541e18fff002598827e94f39c93e0142835ab617 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 17 Dec 2023 19:47:52 +0300 Subject: [PATCH 233/329] Add ChannelApiExtensions (#217) This PR adds an extension method to make it easier to pass Result to CreateMessageAsync --------- Co-authored-by: Octol1ttle --- src/Commands/BanCommandGroup.cs | 7 +-- src/Commands/KickCommandGroup.cs | 7 +-- src/Extensions/ChannelApiExtensions.cs | 29 ++++++++++++ src/Responders/GuildLoadedResponder.cs | 29 +++++------- src/Responders/GuildMemberJoinedResponder.cs | 8 +--- src/Responders/MessageDeletedResponder.cs | 8 +--- src/Responders/MessageEditedResponder.cs | 8 +--- src/Services/Update/MemberUpdateService.cs | 11 ++--- .../Update/ScheduledEventUpdateService.cs | 44 +++++-------------- src/Services/UtilityService.cs | 15 ++----- 10 files changed, 64 insertions(+), 102 deletions(-) create mode 100644 src/Extensions/ChannelApiExtensions.cs diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 7493505..f0da978 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -158,12 +158,7 @@ public class BanCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - if (!dmEmbed.IsDefined(out var dmBuilt)) - { - return Result.FromError(dmEmbed); - } - - await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct); + await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); } var banResult = await _guildApi.CreateGuildBanAsync( diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 1ef6057..cad8ea9 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -140,12 +140,7 @@ public class KickCommandGroup : CommandGroup .WithColour(ColorsList.Red) .Build(); - if (!dmEmbed.IsDefined(out var dmBuilt)) - { - return Result.FromError(dmEmbed); - } - - await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct); + await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); } var kickResult = await _guildApi.RemoveGuildMemberAsync( diff --git a/src/Extensions/ChannelApiExtensions.cs b/src/Extensions/ChannelApiExtensions.cs new file mode 100644 index 0000000..12ccf35 --- /dev/null +++ b/src/Extensions/ChannelApiExtensions.cs @@ -0,0 +1,29 @@ +using OneOf; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; +using Remora.Results; + +namespace Octobot.Extensions; + +public static class ChannelApiExtensions +{ + public static async Task CreateMessageWithEmbedResultAsync(this IDiscordRestChannelAPI channelApi, + Snowflake channelId, Optional message = default, Optional nonce = default, + Optional isTextToSpeech = default, Optional> embedResult = default, + Optional allowedMentions = default, Optional messageRefenence = default, + Optional> components = default, + Optional> stickerIds = default, + Optional>> attachments = default, + Optional flags = default, CancellationToken ct = default) + { + if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed)) + { + return Result.FromError(embedResult.Value); + } + + return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed }, + allowedMentions, messageRefenence, components, stickerIds, attachments, flags, ct); + } +} diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 5d5d68a..cc720c8 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -92,17 +92,19 @@ public class GuildLoadedResponder : IResponder .WithCurrentTimestamp() .WithColour(ColorsList.Blue) .Build(); - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct); + return await _channelApi.CreateMessageWithEmbedResultAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, ct: ct); } private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct) { + var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); + if (!channelResult.IsDefined(out var channel)) + { + return Result.FromError(channelResult); + } + var errorEmbed = new EmbedBuilder() .WithSmallTitle(Messages.DataLoadFailedTitle, bot) .WithDescription(Messages.DataLoadFailedDescription) @@ -110,18 +112,7 @@ public class GuildLoadedResponder : IResponder .WithColour(ColorsList.Red) .Build(); - if (!errorEmbed.IsDefined(out var errorBuilt)) - { - return Result.FromError(errorEmbed); - } - - var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); - if (!channelResult.IsDefined(out var channel)) - { - return Result.FromError(channelResult); - } - - return (Result)await _channelApi.CreateMessageAsync( - channel, embeds: new[] { errorBuilt }, ct: ct); + return await _channelApi.CreateMessageWithEmbedResultAsync( + channel, embedResult: errorEmbed, ct: ct); } } diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 09075bf..66faa28 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -72,13 +72,9 @@ public class GuildMemberJoinedResponder : IResponder .WithTimestamp(gatewayEvent.JoinedAt) .WithColour(ColorsList.Green) .Build(); - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built }, + return await _channelApi.CreateMessageWithEmbedResultAsync( + GuildSettings.PublicFeedbackChannel.Get(cfg), embedResult: embed, allowedMentions: Octobot.NoMentions, ct: ct); } diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 5e4870b..bfedb22 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -98,13 +98,9 @@ public class MessageDeletedResponder : IResponder .WithTimestamp(message.Timestamp) .WithColour(ColorsList.Red) .Build(); - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, + return await _channelApi.CreateMessageWithEmbedResultAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, allowedMentions: Octobot.NoMentions, ct: ct); } } diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index 3b0a6aa..16f7af4 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -107,13 +107,9 @@ public class MessageEditedResponder : IResponder .WithTimestamp(timestamp.Value) .WithColour(ColorsList.Yellow) .Build(); - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, + return await _channelApi.CreateMessageWithEmbedResultAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, allowedMentions: Octobot.NoMentions, ct: ct); } } diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index c3139c3..e7860ae 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -236,16 +236,11 @@ public sealed partial class MemberUpdateService : BackgroundService .WithColour(ColorsList.Magenta) .Build(); - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - - var messageResult = await _channelApi.CreateMessageAsync( - reminder.ChannelId.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct); + var messageResult = await _channelApi.CreateMessageWithEmbedResultAsync( + reminder.ChannelId.ToSnowflake(), Mention.User(user), embedResult: embed, ct: ct); if (!messageResult.IsSuccess) { - return Result.FromError(messageResult); + return messageResult; } data.Reminders.Remove(reminder); diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 9ec9fcf..38fe4a7 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -215,10 +215,6 @@ public sealed class ScheduledEventUpdateService : BackgroundService .WithCurrentTimestamp() .WithColour(ColorsList.White) .Build(); - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } var roleMention = !GuildSettings.EventNotificationRole.Get(settings).Empty() ? Mention.Role(GuildSettings.EventNotificationRole.Get(settings)) @@ -231,8 +227,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService URL: $"https://discord.com/events/{scheduledEvent.GuildID}/{scheduledEvent.ID}" ); - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(settings), roleMention, embeds: new[] { built }, + return await _channelApi.CreateMessageWithEmbedResultAsync( + GuildSettings.EventNotificationChannel.Get(settings), roleMention, embedResult: embed, components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); } @@ -317,14 +313,9 @@ public sealed class ScheduledEventUpdateService : BackgroundService .WithCurrentTimestamp() .Build(); - if (!startedEmbed.IsDefined(out var startedBuilt)) - { - return Result.FromError(startedEmbed); - } - - return (Result)await _channelApi.CreateMessageAsync( + return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), - content, embeds: new[] { startedBuilt }, ct: ct); + content, embedResult: startedEmbed, ct: ct); } private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data, @@ -348,14 +339,9 @@ public sealed class ScheduledEventUpdateService : BackgroundService .WithCurrentTimestamp() .Build(); - if (!completedEmbed.IsDefined(out var completedBuilt)) - { - return Result.FromError(completedEmbed); - } - - var createResult = (Result)await _channelApi.CreateMessageAsync( + var createResult = await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), - embeds: new[] { completedBuilt }, ct: ct); + embedResult: completedEmbed, ct: ct); if (createResult.IsSuccess) { data.ScheduledEvents.Remove(eventData.Id); @@ -380,13 +366,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService .WithCurrentTimestamp() .Build(); - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - - var createResult = (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(data.Settings), embeds: new[] { built }, ct: ct); + var createResult = await _channelApi.CreateMessageWithEmbedResultAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), embedResult: embed, ct: ct); if (createResult.IsSuccess) { data.ScheduledEvents.Remove(eventData.Id); @@ -445,14 +426,9 @@ public sealed class ScheduledEventUpdateService : BackgroundService .WithColour(ColorsList.Default) .Build(); - if (!earlyResult.IsDefined(out var earlyBuilt)) - { - return Result.FromError(earlyResult); - } - - return (Result)await _channelApi.CreateMessageAsync( + return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), content, - embeds: new[] { earlyBuilt }, ct: ct); + embedResult: earlyResult, ct: ct); } } diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index d40570a..9ac481b 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -226,26 +226,19 @@ public sealed class UtilityService : IHostedService .WithColour(color) .Build(); - if (!logEmbed.IsDefined(out var logBuilt)) - { - return Result.FromError(logEmbed); - } - - var builtArray = new[] { logBuilt }; - // Not awaiting to reduce response time if (isPublic && publicChannel != channelId) { - _ = _channelApi.CreateMessageAsync( - publicChannel, embeds: builtArray, + _ = _channelApi.CreateMessageWithEmbedResultAsync( + publicChannel, embedResult: logEmbed, ct: ct); } if (privateChannel != publicChannel && privateChannel != channelId) { - _ = _channelApi.CreateMessageAsync( - privateChannel, embeds: builtArray, + _ = _channelApi.CreateMessageWithEmbedResultAsync( + privateChannel, embedResult: logEmbed, ct: ct); } From 2dc5220f462cbf47b6fa543f55cc5d86bf09e60d Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 17 Dec 2023 19:49:44 +0300 Subject: [PATCH 234/329] /about: Add repository link button (#218) In this PR, I moved the repository link from the embed to a button in /about command for better UI/UX --------- Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- src/Commands/AboutCommandGroup.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 45077e6..4b20a63 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -8,9 +8,11 @@ using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Rest.Core; @@ -31,6 +33,8 @@ public class AboutCommandGroup : CommandGroup ("neroduckale", new Snowflake(474943797063843851)) }; + private const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot"; + private readonly ICommandContext _context; private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; @@ -91,14 +95,22 @@ public class AboutCommandGroup : CommandGroup builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); } - builder.Append($"### [{Messages.AboutTitleRepository}](https://github.com/LabsDevelopment/Octobot)"); - var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png") .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + var button = new ButtonComponent( + ButtonComponentStyle.Link, + Messages.AboutTitleRepository, + URL: RepositoryUrl + ); + + return await _feedback.SendContextualEmbedResultAsync(embed, + new FeedbackMessageOptions(MessageComponents: new[] + { + new ActionRowComponent(new[] { button }) + }), ct); } } From 4581b402aa5d9ec6d27d6cf579d57436df939a23 Mon Sep 17 00:00:00 2001 From: neroduckale <100025711+neroduckale@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:02:50 +0500 Subject: [PATCH 235/329] Do not log messages edited by bots (#202) If the author of the edited message is a bot, then Octobot won't log the edit --- src/Responders/MessageEditedResponder.cs | 27 ++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index 16f7af4..c7426d2 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -36,18 +36,22 @@ public class MessageEditedResponder : IResponder public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.ID.IsDefined(out var messageId)) + { + return new ArgumentNullError(nameof(gatewayEvent.ID)); + } + + if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) + { + return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); + } + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) { return Result.FromSuccess(); } - var cfg = await _guildData.GetSettings(guildId, ct); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) - { - return Result.FromSuccess(); - } - - if (!gatewayEvent.Content.IsDefined(out var newContent)) + if (gatewayEvent.Author.IsDefined(out var author) && author.IsBot.OrDefault(false)) { return Result.FromSuccess(); } @@ -57,14 +61,15 @@ public class MessageEditedResponder : IResponder return Result.FromSuccess(); // The message wasn't actually edited } - if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) + if (!gatewayEvent.Content.IsDefined(out var newContent)) { - return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); + return Result.FromSuccess(); } - if (!gatewayEvent.ID.IsDefined(out var messageId)) + var cfg = await _guildData.GetSettings(guildId, ct); + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { - return new ArgumentNullError(nameof(gatewayEvent.ID)); + return Result.FromSuccess(); } var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); From 4dc460a2692b1d26ae5ae6231de301910203ae3b Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:35:09 +0300 Subject: [PATCH 236/329] Add issue button w/ some button updates (#221) In this PR, I've added a "Report an issue" button and a few more button-related changes: - Add "Report an issue" button - Add icon for "Octobot's source code" - Rename `AboutTitleRepository` to `ButtonOpenRepository` - Rename `OpenEventInfoButton` to `ButtonOpenEventInfo` to be consistent with other language string names - Rename `ColorsList.cs` to `Miscellaneous.cs` - Add public const strings in `Octobot.cs` to get repository & issues links --------- Signed-off-by: mctaylors --- locale/Messages.resx | 7 +++++-- locale/Messages.ru.resx | 7 +++++-- locale/Messages.tt-ru.resx | 7 +++++-- src/Commands/AboutCommandGroup.cs | 18 ++++++++++++------ .../Events/ErrorLoggingPostExecutionEvent.cs | 16 +++++++++++++++- src/Messages.Designer.cs | 16 ++++++++++++---- src/Octobot.cs | 3 +++ src/Responders/GuildLoadedResponder.cs | 12 ++++++++++-- .../Update/ScheduledEventUpdateService.cs | 2 +- 9 files changed, 68 insertions(+), 20 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index f145ab2..743dd93 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -348,7 +348,7 @@ The event will start at {0} until {1} in {2} - + Open Event Info @@ -396,7 +396,7 @@ Developers: - + Octobot's source code @@ -582,4 +582,7 @@ Contact the developers if the problem occurs again. + + Report an issue + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 5b97eda..67a1d29 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -345,7 +345,7 @@ Событие пройдёт с {0} до {1} в {2} - + Открыть сведения о событии @@ -396,7 +396,7 @@ Разработчики: - + Исходный код Octobot @@ -582,4 +582,7 @@ Обратись к разработчикам, если проблема возникнет снова. + + Сообщить о проблеме + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 2761827..4050d43 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -348,7 +348,7 @@ движуха будет происходить с {0} до {1} в {2} - + открыть ивент @@ -396,7 +396,7 @@ девелоперы: - + репа Octobot (тык) @@ -582,4 +582,7 @@ если ты это читаешь второй раз за сегодня, пиши разрабам + + зарепортить баг + diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 4b20a63..2c1e770 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -33,8 +33,6 @@ public class AboutCommandGroup : CommandGroup ("neroduckale", new Snowflake(474943797063843851)) }; - private const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot"; - private readonly ICommandContext _context; private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; @@ -101,16 +99,24 @@ public class AboutCommandGroup : CommandGroup .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png") .Build(); - var button = new ButtonComponent( + var repositoryButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.AboutTitleRepository, - URL: RepositoryUrl + Messages.ButtonOpenRepository, + new PartialEmoji(Name: "🌐"), + URL: Octobot.RepositoryUrl + ); + + var issuesButton = new ButtonComponent( + ButtonComponentStyle.Link, + Messages.ButtonReportIssue, + new PartialEmoji(Name: "⚠️"), + URL: Octobot.IssuesUrl ); return await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent(new[] { button }) + new ActionRowComponent(new[] { repositoryButton, issuesButton }) }), ct); } } diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 2d5f606..87cfc84 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -1,8 +1,11 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Octobot.Extensions; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Commands.Services; using Remora.Discord.Extensions.Embeds; @@ -65,6 +68,17 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent .WithColour(ColorsList.Red) .Build(); - return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + var issuesButton = new ButtonComponent( + ButtonComponentStyle.Link, + Messages.ButtonReportIssue, + new PartialEmoji(Name: "⚠️"), + URL: Octobot.IssuesUrl + ); + + return await _feedback.SendContextualEmbedResultAsync(embed, + new FeedbackMessageOptions(MessageComponents: new[] + { + new ActionRowComponent(new[] { issuesButton }) + }), ct); } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 33ba7b3..767bd5b 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -534,9 +534,9 @@ namespace Octobot { } } - internal static string OpenEventInfoButton { + internal static string ButtonOpenEventInfo { get { - return ResourceManager.GetString("OpenEventInfoButton", resourceCulture); + return ResourceManager.GetString("ButtonOpenEventInfo", resourceCulture); } } @@ -630,9 +630,9 @@ namespace Octobot { } } - internal static string AboutTitleRepository { + internal static string ButtonOpenRepository { get { - return ResourceManager.GetString("AboutTitleRepository", resourceCulture); + return ResourceManager.GetString("ButtonOpenRepository", resourceCulture); } } @@ -1028,5 +1028,13 @@ namespace Octobot { return ResourceManager.GetString("ContactDevelopers", resourceCulture); } } + + internal static string ButtonReportIssue + { + get + { + return ResourceManager.GetString("ButtonReportIssue", resourceCulture); + } + } } } diff --git a/src/Octobot.cs b/src/Octobot.cs index 07bc058..1806330 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -27,6 +27,9 @@ public sealed class Octobot public static readonly AllowedMentions NoMentions = new( Array.Empty(), Array.Empty(), Array.Empty()); + public const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot"; + public const string IssuesUrl = $"{RepositoryUrl}/issues"; + public static async Task Main(string[] args) { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index cc720c8..2d66a3b 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -7,6 +7,7 @@ using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Gateway.Events; +using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; @@ -112,7 +113,14 @@ public class GuildLoadedResponder : IResponder .WithColour(ColorsList.Red) .Build(); - return await _channelApi.CreateMessageWithEmbedResultAsync( - channel, embedResult: errorEmbed, ct: ct); + var issuesButton = new ButtonComponent( + ButtonComponentStyle.Link, + Messages.ButtonReportIssue, + new PartialEmoji(Name: "⚠️"), + URL: Octobot.IssuesUrl + ); + + return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, + components: new[] { new ActionRowComponent(new[] { issuesButton }) }, ct: ct); } } diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 38fe4a7..dd9be0d 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -222,7 +222,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService var button = new ButtonComponent( ButtonComponentStyle.Link, - Messages.OpenEventInfoButton, + Messages.ButtonOpenEventInfo, new PartialEmoji(Name: "📋"), URL: $"https://discord.com/events/{scheduledEvent.GuildID}/{scheduledEvent.ID}" ); From d4ea62d4199f2b81d4122834f5c9dc98343dd77f Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:06:33 +0300 Subject: [PATCH 237/329] README: Add invite button (#222) Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- docs/README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index 10ee63c..5be0bd8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,6 @@ - Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Labs Development Team](https://github.com/LabsDevelopment) in C# and Remora.Discord @@ -22,7 +21,17 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr [//]: # (if you are reading this, message @mctaylors and ask him to bring back the wiki) -## Running Octobot +## Invite Octobot + +Did you know that Octobot is a public bot? You can invite it to your server and use it without building it! +

+ +

+ +> [!IMPORTANT] +> The bot will not be able to respond in private channels unless you have configured permissions for the bot in those channels. + +## Building Octobot 1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents! From 96eed1a820c353e8e80f2cfd1726f40c0034d32a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 08:46:40 +0300 Subject: [PATCH 238/329] Bump DiffPlex from 1.7.1 to 1.7.2 (#227) Bumps [DiffPlex](https://github.com/mmanela/diffplex) from 1.7.1 to 1.7.2.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=DiffPlex&package-manager=nuget&previous-version=1.7.1&new-version=1.7.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Octobot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Octobot.csproj b/Octobot.csproj index 5258c89..e8f0dfa 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -20,7 +20,7 @@ - + From c0b43c6a184dcb118fccb2b5101afd3b63cc96a9 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:26:08 +0300 Subject: [PATCH 239/329] /about: Show link to GitHub profile if Discord member wasn't found (#226) In this PR, the behavior of the developer list display in /about has been changed. Now, if a developer is not on the same server where the /about command was executed, their username will have a link to their GitHub profile. --------- Signed-off-by: mctaylors --- src/Commands/AboutCommandGroup.cs | 4 +++- src/Extensions/MarkdownExtensions.cs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 2c1e770..a12c070 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -88,7 +88,9 @@ public class AboutCommandGroup : CommandGroup { var guildMemberResult = await _guildApi.GetGuildMemberAsync( guildId, dev.Id, ct); - var tag = guildMemberResult.IsSuccess ? $"<@{dev.Id}>" : $"@{dev.Username}"; + var tag = guildMemberResult.IsSuccess + ? $"<@{dev.Id}>" + : MarkdownExtensions.Hyperlink($"@{dev.Username}", $"https://github.com/{dev.Username}"); builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); } diff --git a/src/Extensions/MarkdownExtensions.cs b/src/Extensions/MarkdownExtensions.cs index 7b7f780..95cd344 100644 --- a/src/Extensions/MarkdownExtensions.cs +++ b/src/Extensions/MarkdownExtensions.cs @@ -13,4 +13,17 @@ public static class MarkdownExtensions { return $"- {text}"; } + + /// + /// Formats a string to use Markdown Hyperlink formatting. + /// + /// The input text to format. + /// The URL to use in formatting. + /// + /// A markdown-formatted Hyperlink string. + /// + public static string Hyperlink(string text, string url) + { + return $"[{text}]({url})"; + } } From f79968fdc2dcc1317be87a394a5ef5311450f82a Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Tue, 19 Dec 2023 13:32:11 +0300 Subject: [PATCH 240/329] /about: Use Markdown.Hyperlink instead of custom extension (#229) Signed-off-by: mctaylors --- src/Commands/AboutCommandGroup.cs | 3 ++- src/Extensions/MarkdownExtensions.cs | 13 ------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index a12c070..eec1f99 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -15,6 +15,7 @@ using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; @@ -90,7 +91,7 @@ public class AboutCommandGroup : CommandGroup guildId, dev.Id, ct); var tag = guildMemberResult.IsSuccess ? $"<@{dev.Id}>" - : MarkdownExtensions.Hyperlink($"@{dev.Username}", $"https://github.com/{dev.Username}"); + : Markdown.Hyperlink($"@{dev.Username}", $"https://github.com/{dev.Username}"); builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); } diff --git a/src/Extensions/MarkdownExtensions.cs b/src/Extensions/MarkdownExtensions.cs index 95cd344..7b7f780 100644 --- a/src/Extensions/MarkdownExtensions.cs +++ b/src/Extensions/MarkdownExtensions.cs @@ -13,17 +13,4 @@ public static class MarkdownExtensions { return $"- {text}"; } - - /// - /// Formats a string to use Markdown Hyperlink formatting. - /// - /// The input text to format. - /// The URL to use in formatting. - /// - /// A markdown-formatted Hyperlink string. - /// - public static string Hyperlink(string text, string url) - { - return $"[{text}]({url})"; - } } From 74e32dee9b245d1afe6d32b642b149fbfabf3e2c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Dec 2023 21:23:37 +0500 Subject: [PATCH 241/329] Use collection expressions in more places (#238) ReSharper inspections have been updated, causing new warnings to appear in the codebase. This time, the "Use collection expressions" inspection has been enabled for usecases where the collection is not empty. This PR fixes the check failures caused by this inspection. --- src/Commands/AboutCommandGroup.cs | 4 ++-- src/Commands/SettingsCommandGroup.cs | 4 ++-- src/Commands/ToolsCommandGroup.cs | 4 ++-- src/Services/Update/MemberUpdateService.cs | 4 ++-- src/Services/Update/SongUpdateService.cs | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index eec1f99..1c2656b 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -28,11 +28,11 @@ namespace Octobot.Commands; public class AboutCommandGroup : CommandGroup { private static readonly (string Username, Snowflake Id)[] Developers = - { + [ ("Octol1ttle", new Snowflake(504343489664909322)), ("mctaylors", new Snowflake(326642240229474304)), ("neroduckale", new Snowflake(474943797063843851)) - }; + ]; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index a8891bd..15fd514 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -36,7 +36,7 @@ public class SettingsCommandGroup : CommandGroup /// that the orders match. /// private static readonly IOption[] AllOptions = - { + [ GuildSettings.Language, GuildSettings.WelcomeMessage, GuildSettings.ReceiveStartupMessages, @@ -51,7 +51,7 @@ public class SettingsCommandGroup : CommandGroup GuildSettings.MuteRole, GuildSettings.EventNotificationRole, GuildSettings.EventEarlyNotificationOffset - }; + ]; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 78058cb..f04ddf6 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -393,7 +393,7 @@ public class ToolsCommandGroup : CommandGroup } private static readonly TimestampStyle[] AllStyles = - { + [ TimestampStyle.ShortDate, TimestampStyle.LongDate, TimestampStyle.ShortTime, @@ -401,7 +401,7 @@ public class ToolsCommandGroup : CommandGroup TimestampStyle.ShortDateTime, TimestampStyle.LongDateTime, TimestampStyle.RelativeTime - }; + ]; /// /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index e7860ae..8937833 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -16,7 +16,7 @@ namespace Octobot.Services.Update; public sealed partial class MemberUpdateService : BackgroundService { private static readonly string[] GenericNicknames = - { + [ "Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary", "Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck", "Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane", @@ -24,7 +24,7 @@ public sealed partial class MemberUpdateService : BackgroundService "Nautilus", "Ostrich", "Octopus", "Pelican", "Puffin", "Pyramid", "Rattle", "Robin", "Rose", "Salmon", "Seal", "Shark", "Sheep", "Snake", "Sonar", "Stump", "Sparrow", "Toaster", "Toucan", "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag" - }; + ]; private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildAPI _guildApi; diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index 391c416..53cc59b 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -9,7 +9,7 @@ namespace Octobot.Services.Update; public sealed class SongUpdateService : BackgroundService { private static readonly (string Author, string Name, TimeSpan Duration)[] SongList = - { + [ ("Yoko & the Gold Bazookas", "Rockagilly Blues", new TimeSpan(0, 2, 52)), ("Deep Cut", "Big Betrayal", new TimeSpan(0, 5, 55)), ("Squid Sisters", "Tomorrow's Nostalgia Today", new TimeSpan(0, 3, 7)), @@ -30,7 +30,7 @@ public sealed class SongUpdateService : BackgroundService ("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)), ("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)), ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5)) - }; + ]; private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)]; From 6688481093ab217a0e8b036dc00122b82acc71e0 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 20 Dec 2023 19:25:13 +0300 Subject: [PATCH 242/329] /about: Show Discord bot username instead of hardcoded one (#230) In this PR, I made it so that in the Author field instead of the hardcoded name was the name of the Discord bot. This was done to match the icon next to it in the same field. Replaces #224 --------- Signed-off-by: mctaylors --- locale/Messages.resx | 2 +- locale/Messages.ru.resx | 2 +- locale/Messages.tt-ru.resx | 2 +- src/Commands/AboutCommandGroup.cs | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 743dd93..60e6e07 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -400,7 +400,7 @@ Octobot's source code - About Octobot + About {0} developer & designer, Octobot's Wiki creator diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 67a1d29..4b9492c 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -400,7 +400,7 @@ Исходный код Octobot - Об Octobot + О боте {0} разработчик diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 4050d43..de1f39f 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -400,7 +400,7 @@ репа Octobot (тык) - немного об Octobot + немного об {0} скучный девелопер + дизайнер создавший Octobot's Wiki diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 1c2656b..4c396d9 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -96,7 +96,8 @@ public class AboutCommandGroup : CommandGroup builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); } - var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, bot) + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png") From bd4c5b26da95d7a1fd3e00ce5ce0e90757018f8c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Dec 2023 21:33:52 +0500 Subject: [PATCH 243/329] Remove "extends IHostedService" from classes where it's not required (#236) Originally, these classes were services because I thought that all DI-resolvable classes need to be services. However, this is not true, so we can make these classes (notably Utility and GuildDataService) not extend anything. `UtilityService` was renamed to `Utility` for simplicity --------- Signed-off-by: Octol1ttle --- src/Commands/BanCommandGroup.cs | 4 ++-- src/Commands/ClearCommandGroup.cs | 4 ++-- src/Commands/KickCommandGroup.cs | 4 ++-- src/Commands/MuteCommandGroup.cs | 4 ++-- src/Commands/SettingsCommandGroup.cs | 4 ++-- src/Octobot.cs | 2 +- src/Responders/GuildLoadedResponder.cs | 4 ++-- src/Services/GuildDataService.cs | 12 +----------- src/Services/Update/MemberUpdateService.cs | 4 ++-- .../Update/ScheduledEventUpdateService.cs | 4 ++-- src/Services/{UtilityService.cs => Utility.cs} | 15 ++------------- 11 files changed, 20 insertions(+), 41 deletions(-) rename src/Services/{UtilityService.cs => Utility.cs} (97%) diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index f0da978..bbcf459 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -33,12 +33,12 @@ public class BanCommandGroup : CommandGroup private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly Utility _utility; public BanCommandGroup( ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - UtilityService utility) + Utility utility) { _context = context; _channelApi = channelApi; diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 7ebd4ea..1d0ad64 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -30,11 +30,11 @@ public class ClearCommandGroup : CommandGroup private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly Utility _utility; public ClearCommandGroup( IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService guildData, - IFeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) + IFeedbackService feedback, IDiscordRestUserAPI userApi, Utility utility) { _channelApi = channelApi; _context = context; diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index cad8ea9..ee94b93 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -30,12 +30,12 @@ public class KickCommandGroup : CommandGroup private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly Utility _utility; public KickCommandGroup( ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - UtilityService utility) + Utility utility) { _context = context; _channelApi = channelApi; diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 6a28f38..522c7f7 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -32,11 +32,11 @@ public class MuteCommandGroup : CommandGroup private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly Utility _utility; public MuteCommandGroup( ICommandContext context, GuildDataService guildData, IFeedbackService feedback, - IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) + IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, Utility utility) { _context = context; _guildData = guildData; diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 15fd514..ce7472f 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -57,11 +57,11 @@ public class SettingsCommandGroup : CommandGroup private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly Utility _utility; public SettingsCommandGroup( ICommandContext context, GuildDataService guildData, - IFeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) + IFeedbackService feedback, IDiscordRestUserAPI userApi, Utility utility) { _context = context; _guildData = guildData; diff --git a/src/Octobot.cs b/src/Octobot.cs index 1806330..5cffd70 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -87,7 +87,7 @@ public sealed class Octobot .AddPostExecutionEvent() // Services .AddSingleton() - .AddSingleton() + .AddSingleton() .AddHostedService() .AddHostedService() .AddHostedService() diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 2d66a3b..a1e7d16 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -25,11 +25,11 @@ public class GuildLoadedResponder : IResponder private readonly GuildDataService _guildData; private readonly ILogger _logger; private readonly IDiscordRestUserAPI _userApi; - private readonly UtilityService _utility; + private readonly Utility _utility; public GuildLoadedResponder( IDiscordRestChannelAPI channelApi, GuildDataService guildData, ILogger logger, - IDiscordRestUserAPI userApi, UtilityService utility) + IDiscordRestUserAPI userApi, Utility utility) { _channelApi = channelApi; _guildData = guildData; diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 3cc8cea..961c8f9 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -11,7 +11,7 @@ namespace Octobot.Services; /// /// Handles saving, loading, initializing and providing . /// -public sealed class GuildDataService : IHostedService +public sealed class GuildDataService { private readonly ConcurrentDictionary _datas = new(); private readonly ILogger _logger; @@ -24,16 +24,6 @@ public sealed class GuildDataService : IHostedService lifetime.ApplicationStopping.Register(ApplicationStopping); } - public Task StartAsync(CancellationToken ct) - { - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken ct) - { - return Task.CompletedTask; - } - private void ApplicationStopping() { SaveAsync(CancellationToken.None).GetAwaiter().GetResult(); diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 8937833..06e531f 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -30,10 +30,10 @@ public sealed partial class MemberUpdateService : BackgroundService private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly ILogger _logger; - private readonly UtilityService _utility; + private readonly Utility _utility; public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, - GuildDataService guildData, ILogger logger, UtilityService utility) + GuildDataService guildData, ILogger logger, Utility utility) { _channelApi = channelApi; _guildApi = guildApi; diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index dd9be0d..ac5c109 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -19,10 +19,10 @@ public sealed class ScheduledEventUpdateService : BackgroundService private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly GuildDataService _guildData; private readonly ILogger _logger; - private readonly UtilityService _utility; + private readonly Utility _utility; public ScheduledEventUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, - GuildDataService guildData, ILogger logger, UtilityService utility) + GuildDataService guildData, ILogger logger, Utility utility) { _channelApi = channelApi; _eventApi = eventApi; diff --git a/src/Services/UtilityService.cs b/src/Services/Utility.cs similarity index 97% rename from src/Services/UtilityService.cs rename to src/Services/Utility.cs index 9ac481b..401b067 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/Utility.cs @@ -1,7 +1,6 @@ using System.Drawing; using System.Text; using System.Text.Json.Nodes; -using Microsoft.Extensions.Hosting; using Octobot.Data; using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; @@ -17,14 +16,14 @@ namespace Octobot.Services; /// Provides utility methods that cannot be transformed to extension methods because they require usage /// of some Discord APIs. /// -public sealed class UtilityService : IHostedService +public sealed class Utility { private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestUserAPI _userApi; - public UtilityService( + public Utility( IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi) { @@ -34,16 +33,6 @@ public sealed class UtilityService : IHostedService _userApi = userApi; } - public Task StartAsync(CancellationToken ct) - { - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken ct) - { - return Task.CompletedTask; - } - /// /// Checks whether or not a member can interact with another member /// From 21f200c9884ca0fc9d4129fdc5c364bc0874547c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Dec 2023 22:08:56 +0500 Subject: [PATCH 244/329] Merge BackgroundGuildDataSaverService into GuildDataService (#239) Title. idk why I didn't think of this before. Also, GuildDataService is now properly registered as an IHostedService, so it receives start & shutdown events. So this PR gets rid of the workaround that was needed for save-on-shutdown to function Signed-off-by: Octol1ttle --- src/Octobot.cs | 10 ++++---- .../BackgroundGuildDataSaverService.cs | 23 ------------------ src/Services/GuildDataService.cs | 24 ++++++++++++------- 3 files changed, 21 insertions(+), 36 deletions(-) delete mode 100644 src/Services/BackgroundGuildDataSaverService.cs diff --git a/src/Octobot.cs b/src/Octobot.cs index 5cffd70..2648338 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -24,12 +24,12 @@ namespace Octobot; public sealed class Octobot { - public static readonly AllowedMentions NoMentions = new( - Array.Empty(), Array.Empty(), Array.Empty()); - public const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot"; public const string IssuesUrl = $"{RepositoryUrl}/issues"; + public static readonly AllowedMentions NoMentions = new( + Array.Empty(), Array.Empty(), Array.Empty()); + public static async Task Main(string[] args) { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); @@ -86,12 +86,12 @@ public sealed class Octobot .AddPreparationErrorEvent() .AddPostExecutionEvent() // Services - .AddSingleton() .AddSingleton() + .AddSingleton() + .AddHostedService(provider => provider.GetRequiredService()) .AddHostedService() .AddHostedService() .AddHostedService() - .AddHostedService() // Slash commands .AddCommandTree() .WithCommandGroup() diff --git a/src/Services/BackgroundGuildDataSaverService.cs b/src/Services/BackgroundGuildDataSaverService.cs deleted file mode 100644 index 766ffe0..0000000 --- a/src/Services/BackgroundGuildDataSaverService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.Extensions.Hosting; - -namespace Octobot.Services; - -public sealed class BackgroundGuildDataSaverService : BackgroundService -{ - private readonly GuildDataService _guildData; - - public BackgroundGuildDataSaverService(GuildDataService guildData) - { - _guildData = guildData; - } - - protected override async Task ExecuteAsync(CancellationToken ct) - { - using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); - - while (await timer.WaitForNextTickAsync(ct)) - { - await _guildData.SaveAsync(ct); - } - } -} diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index 961c8f9..c9458a0 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -11,25 +11,23 @@ namespace Octobot.Services; /// /// Handles saving, loading, initializing and providing . /// -public sealed class GuildDataService +public sealed class GuildDataService : BackgroundService { private readonly ConcurrentDictionary _datas = new(); private readonly ILogger _logger; - // https://github.com/dotnet/aspnetcore/issues/39139 - public GuildDataService( - IHostApplicationLifetime lifetime, ILogger logger) + public GuildDataService(ILogger logger) { _logger = logger; - lifetime.ApplicationStopping.Register(ApplicationStopping); } - private void ApplicationStopping() + public override Task StopAsync(CancellationToken ct) { - SaveAsync(CancellationToken.None).GetAwaiter().GetResult(); + base.StopAsync(ct); + return SaveAsync(ct); } - public Task SaveAsync(CancellationToken ct) + private Task SaveAsync(CancellationToken ct) { var tasks = new List(); var datas = _datas.Values.ToArray(); @@ -58,6 +56,16 @@ public sealed class GuildDataService File.Delete(tempFilePath); } + protected override async Task ExecuteAsync(CancellationToken ct) + { + using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); + + while (await timer.WaitForNextTickAsync(ct)) + { + await SaveAsync(ct); + } + } + public async Task GetData(Snowflake guildId, CancellationToken ct = default) { return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); From d4871bb23da59cd0018a93744cacb68cf5e1a08c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Dec 2023 22:48:32 +0500 Subject: [PATCH 245/329] Use AddFromAssembly for responders and command groups (#240) now we don't have to explicitly type out command groups woooo Signed-off-by: Octol1ttle --- src/Octobot.cs | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/src/Octobot.cs b/src/Octobot.cs index 2648338..2e810ed 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -2,11 +2,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Octobot.Commands; using Octobot.Commands.Events; using Octobot.Services; using Octobot.Services.Update; -using Remora.Commands.Extensions; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Objects; @@ -14,8 +12,8 @@ using Remora.Discord.Caching.Extensions; using Remora.Discord.Caching.Services; using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Services; +using Remora.Discord.Extensions.Extensions; using Remora.Discord.Gateway; -using Remora.Discord.Gateway.Extensions; using Remora.Discord.Hosting.Extensions; using Remora.Rest.Core; using Serilog.Extensions.Logging; @@ -82,6 +80,8 @@ public sealed class Octobot // Init .AddDiscordCaching() .AddDiscordCommands(true, false) + .AddRespondersFromAssembly(typeof(Octobot).Assembly) + .AddCommandGroupsFromAssembly(typeof(Octobot).Assembly) // Slash command event handlers .AddPreparationErrorEvent() .AddPostExecutionEvent() @@ -91,25 +91,7 @@ public sealed class Octobot .AddHostedService(provider => provider.GetRequiredService()) .AddHostedService() .AddHostedService() - .AddHostedService() - // Slash commands - .AddCommandTree() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup(); - var responderTypes = typeof(Octobot).Assembly - .GetExportedTypes() - .Where(t => t.IsResponder()); - foreach (var responderType in responderTypes) - { - services.AddResponder(responderType); - } + .AddHostedService(); } ).ConfigureLogging( c => c.AddConsole() From 7d9a85d8156a15953f71fbe11bc13e6fa81f1bed Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Dec 2023 22:59:17 +0500 Subject: [PATCH 246/329] Add profiler base (#235) This PR adds the base classes required for profiling code inside of Octobot. The implementation of the profiler is similar to Minecraft, however it is more detailed and provides per-event logs for each event. This PR does not change any code to be profiled and this is intentional. Changes required for profiling will come as separate PRs, one for commands, one for responders, and one for background services. Signed-off-by: Octol1ttle --- src/Octobot.cs | 3 + src/Services/Profiler/Profiler.cs | 114 +++++++++++++++++++++++ src/Services/Profiler/ProfilerEvent.cs | 9 ++ src/Services/Profiler/ProfilerFactory.cs | 27 ++++++ 4 files changed, 153 insertions(+) create mode 100644 src/Services/Profiler/Profiler.cs create mode 100644 src/Services/Profiler/ProfilerEvent.cs create mode 100644 src/Services/Profiler/ProfilerFactory.cs diff --git a/src/Octobot.cs b/src/Octobot.cs index 2e810ed..063bd14 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Octobot.Commands.Events; using Octobot.Services; +using Octobot.Services.Profiler; using Octobot.Services.Update; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; @@ -86,6 +87,8 @@ public sealed class Octobot .AddPreparationErrorEvent() .AddPostExecutionEvent() // Services + .AddTransient() + .AddSingleton() .AddSingleton() .AddSingleton() .AddHostedService(provider => provider.GetRequiredService()) diff --git a/src/Services/Profiler/Profiler.cs b/src/Services/Profiler/Profiler.cs new file mode 100644 index 0000000..8d4ca98 --- /dev/null +++ b/src/Services/Profiler/Profiler.cs @@ -0,0 +1,114 @@ +using System.Diagnostics; +using System.Text; +using Microsoft.Extensions.Logging; +using Remora.Results; + +// TODO: remove in future profiler PRs +// ReSharper disable All + +namespace Octobot.Services.Profiler; + +/// +/// Provides the ability to profile how long certain parts of code take to complete using es. +/// +/// Resolve instead in singletons. +public sealed class Profiler +{ + private const int MaxProfilerTime = 1000; // milliseconds + private readonly List _events = []; + private readonly ILogger _logger; + + public Profiler(ILogger logger) + { + _logger = logger; + } + + /// + /// Pushes an event to the profiler. + /// + /// The ID of the event. + public void Push(string id) + { + _events.Add(new ProfilerEvent + { + Id = id, + Stopwatch = Stopwatch.StartNew() + }); + } + + /// + /// Pops the last pushed event from the profiler. + /// + /// Thrown if the profiler contains no events. + public void Pop() + { + if (_events.Count is 0) + { + throw new InvalidOperationException("Nothing to pop"); + } + + _events.Last().Stopwatch.Stop(); + } + + /// + /// If the profiler took too long to execute, this will log a warning with per-event time usage + /// + /// + private void Report() + { + var main = _events[0]; + if (main.Stopwatch.ElapsedMilliseconds < MaxProfilerTime) + { + return; + } + + var unprofiled = main.Stopwatch.ElapsedMilliseconds; + var builder = new StringBuilder().AppendLine(); + for (var i = 1; i < _events.Count; i++) + { + var profilerEvent = _events[i]; + if (profilerEvent.Stopwatch.IsRunning) + { + throw new InvalidOperationException( + $"Tried to report on a profiler with running stopwatches: {profilerEvent.Id}"); + } + + builder.AppendLine($"{profilerEvent.Id}: {profilerEvent.Stopwatch.ElapsedMilliseconds}ms"); + unprofiled -= profilerEvent.Stopwatch.ElapsedMilliseconds; + } + + builder.AppendLine($": {unprofiled}ms"); + + _logger.LogWarning("Profiler {ID} took {Elapsed} milliseconds to execute (max: {Max}ms):{Events}", main.Id, + main.Stopwatch.ElapsedMilliseconds, MaxProfilerTime, builder.ToString()); + } + + /// + /// the profiler and on it afterwards. + /// + public void PopAndReport() + { + Pop(); + Report(); + } + + /// + /// on the profiler and return a . + /// + /// + /// + public Result ReportWithResult(Result result) + { + PopAndReport(); + return result; + } + + /// + /// Calls with + /// + /// A successful result. + public Result ReportWithSuccess() + { + return ReportWithResult(Result.FromSuccess()); + } +} diff --git a/src/Services/Profiler/ProfilerEvent.cs b/src/Services/Profiler/ProfilerEvent.cs new file mode 100644 index 0000000..f655fc4 --- /dev/null +++ b/src/Services/Profiler/ProfilerEvent.cs @@ -0,0 +1,9 @@ +using System.Diagnostics; + +namespace Octobot.Services.Profiler; + +public struct ProfilerEvent +{ + public string Id { get; init; } + public Stopwatch Stopwatch { get; init; } +} diff --git a/src/Services/Profiler/ProfilerFactory.cs b/src/Services/Profiler/ProfilerFactory.cs new file mode 100644 index 0000000..0135771 --- /dev/null +++ b/src/Services/Profiler/ProfilerFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Octobot.Services.Profiler; + +/// +/// Provides a method to create a . Useful in singletons. +/// +public sealed class ProfilerFactory +{ + private readonly IServiceScopeFactory _scopeFactory; + + public ProfilerFactory(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + /// + /// Creates a new . + /// + /// A new . + // TODO: remove in future profiler PRs + // ReSharper disable once UnusedMember.Global + public Profiler Create() + { + return _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + } +} From 285763d50dd02f1f6e0d8a82e7aabfe74a2fabd2 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:35:10 +0300 Subject: [PATCH 247/329] /userinfo: Show if the user was kicked (#242) Closes #241 Updates: - Show if the user was kicked by [adding "Kicked" parameter to MemberData](https://github.com/LabsDevelopment/Octobot/issues/241) - Change `mctaylors-ru`'s `UserInfoBannedPermanently` string to be different from `UserInfoBanned` - Finally add `AppendPunishmentsInformation` method to avoid Cognitive Complexity - Use MemberData to check if the user was banned - Rename variable `isMuted` to `wasMuted` to be consistent with other variable names --------- Signed-off-by: mctaylors --- locale/Messages.resx | 3 ++ locale/Messages.ru.resx | 3 ++ locale/Messages.tt-ru.resx | 5 +- src/Commands/KickCommandGroup.cs | 4 +- src/Commands/MuteCommandGroup.cs | 4 +- src/Commands/ToolsCommandGroup.cs | 52 ++++++++++++-------- src/Data/MemberData.cs | 1 + src/Messages.Designer.cs | 8 +++ src/Responders/GuildMemberJoinedResponder.cs | 2 + 9 files changed, 58 insertions(+), 24 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 60e6e07..1387edf 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -585,4 +585,7 @@ Report an issue + + Kicked + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 4b9492c..572c0b2 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -585,4 +585,7 @@ Сообщить о проблеме + + Выгнан + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index de1f39f..d20c358 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -502,7 +502,7 @@ приколы полученные по заслугам
- забанен + пермабан вышел из сервера @@ -585,4 +585,7 @@ зарепортить баг + + кикнут + diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index ee94b93..a278fb4 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -151,7 +151,9 @@ public class KickCommandGroup : CommandGroup return Result.FromError(kickResult.Error); } - data.GetOrCreateMemberData(target.ID).Roles.Clear(); + var memberData = data.GetOrCreateMemberData(target.ID); + memberData.Roles.Clear(); + memberData.Kicked = true; var title = string.Format(Messages.UserKicked, target.GetTag()); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 522c7f7..c7b21f6 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -300,9 +300,9 @@ public class MuteCommandGroup : CommandGroup } var memberData = data.GetOrCreateMemberData(target.ID); - var isMuted = memberData.MutedUntil is not null || communicationDisabledUntil is not null; + var wasMuted = memberData.MutedUntil is not null || communicationDisabledUntil is not null; - if (!isMuted) + if (!wasMuted) { var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, bot) .WithColour(ColorsList.Red).Build(); diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index f04ddf6..1dbf72d 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -122,32 +122,21 @@ public class ToolsCommandGroup : CommandGroup embedColor = AppendGuildInformation(embedColor, guildMember, builder); } - var isMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || - communicationDisabledUntil is not null; + var wasMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || + communicationDisabledUntil is not null; + var wasBanned = memberData.BannedUntil is not null; + var wasKicked = memberData.Kicked; - var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct); - - if (isMuted || existingBanResult.IsDefined()) + if (wasMuted || wasBanned || wasKicked) { builder.Append("### ") .AppendLine(Markdown.Bold(Messages.UserInfoPunishments)); + + embedColor = AppendPunishmentsInformation(wasMuted, wasKicked, wasBanned, memberData, + builder, embedColor, communicationDisabledUntil); } - if (isMuted) - { - AppendMuteInformation(memberData, communicationDisabledUntil, builder); - - embedColor = ColorsList.Red; - } - - if (existingBanResult.IsDefined()) - { - AppendBanInformation(memberData, builder); - - embedColor = ColorsList.Black; - } - - if (!guildMemberResult.IsSuccess && !existingBanResult.IsDefined()) + if (!guildMemberResult.IsSuccess && !wasBanned) { builder.Append("### ") .AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild)); @@ -166,6 +155,29 @@ public class ToolsCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + private static Color AppendPunishmentsInformation(bool wasMuted, bool wasKicked, bool wasBanned, + MemberData memberData, StringBuilder builder, Color embedColor, DateTimeOffset? communicationDisabledUntil) + { + if (wasMuted) + { + AppendMuteInformation(memberData, communicationDisabledUntil, builder); + embedColor = ColorsList.Red; + } + + if (wasKicked) + { + builder.AppendBulletPointLine(Messages.UserInfoKicked); + } + + if (wasBanned) + { + AppendBanInformation(memberData, builder); + embedColor = ColorsList.Black; + } + + return embedColor; + } + private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder) { if (guildMember.Nickname.IsDefined(out var nickname)) diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 0b0cfb2..8e23e54 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -18,6 +18,7 @@ public sealed class MemberData public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } public DateTimeOffset? MutedUntil { get; set; } + public bool Kicked { get; set; } public List Roles { get; set; } = []; public List Reminders { get; } = []; } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 767bd5b..8dad3dc 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1036,5 +1036,13 @@ namespace Octobot { return ResourceManager.GetString("ButtonReportIssue", resourceCulture); } } + + internal static string UserInfoKicked + { + get + { + return ResourceManager.GetString("UserInfoKicked", resourceCulture); + } + } } } diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 66faa28..eee93b6 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -43,6 +43,8 @@ public class GuildMemberJoinedResponder : IResponder var cfg = data.Settings; var memberData = data.GetOrCreateMemberData(user.ID); + memberData.Kicked = false; + var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct); if (!returnRolesResult.IsSuccess) { From 3134c3575110e6c693f83466eb82d8f627e0585a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 21 Dec 2023 22:21:20 +0500 Subject: [PATCH 248/329] Ban usages of Thread#Sleep (#243) Using Thread.Sleep blocks the _entire_ thread from doing *anything*, while Task.Delay allows the thread to execute other tasks while the delay is passing. The inability to cancel Thread.Sleep may also cause longer shutdowns tl;dr Thread.Sleep bad, Task.Delay good made because of https://github.com/LabsDevelopment/Octobot/pull/234/commits/578c03871de0dd042f2fe0918df296f0a8123e00 Signed-off-by: Octol1ttle --- CodeAnalysis/BannedSymbols.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 0a1ec81..bf444a9 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -18,3 +18,5 @@ P:System.DateTime.Now;Use System.DateTime.UtcNow instead. P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead. P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead. M:System.IO.File.OpenWrite(System.String);File.OpenWrite(string) does not clear the file before writing to it. Use File.Create(string) instead. +M:System.Threading.Thread.Sleep(System.Int32);Use Task.Delay(int, CancellationToken) instead. +M:System.Threading.Thread.Sleep(System.TimeSpan);Use Task.Delay(TimeSpan, CancellationToken) instead. From 894e8198658b14a9a5a5819329b454e8b4bf800d Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 31 Dec 2023 15:42:59 +0500 Subject: [PATCH 249/329] Fix newline in LogResult (#245) Fixes an issue on Windows that would cause the `ResultErrorMessage` to be unindented. Signed-off-by: Octol1ttle --- src/Extensions/LoggerExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Extensions/LoggerExtensions.cs b/src/Extensions/LoggerExtensions.cs index 3805cea..9df90b8 100644 --- a/src/Extensions/LoggerExtensions.cs +++ b/src/Extensions/LoggerExtensions.cs @@ -35,6 +35,7 @@ public static class LoggerExtensions return; } - logger.LogWarning("{UserMessage}\n{ResultErrorMessage}", message, result.Error.Message); + logger.LogWarning("{UserMessage}{NewLine}{ResultErrorMessage}", message, Environment.NewLine, + result.Error.Message); } } From e01fde83c63a0eab0ae972b689395cbca4982d50 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 31 Dec 2023 15:27:00 +0300 Subject: [PATCH 250/329] Use custom TimeSpanParser (#223) Closes #154 --------- Signed-off-by: mctaylors --- locale/Messages.resx | 3 ++ locale/Messages.ru.resx | 3 ++ locale/Messages.tt-ru.resx | 3 ++ src/Commands/BanCommandGroup.cs | 23 ++++++++- src/Commands/MuteCommandGroup.cs | 17 ++++++- src/Commands/RemindCommandGroup.cs | 29 +++++++++-- src/Commands/ToolsCommandGroup.cs | 29 +++++++++-- src/Data/Options/TimeSpanOption.cs | 6 +-- src/Messages.Designer.cs | 8 +++ src/Parsers/TimeSpanParser.cs | 78 ++++++++++++++++++++++++++++++ 10 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 src/Parsers/TimeSpanParser.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index 1387edf..adc9f6d 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -585,6 +585,9 @@ Report an issue + + Time specified incorrectly! + Kicked diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 572c0b2..de2158d 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -585,6 +585,9 @@ Сообщить о проблеме + + Неправильно указано время! + Выгнан diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index d20c358..ca3c19d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -585,6 +585,9 @@ зарепортить баг + + ты там правильно напиши таймспан + кикнут diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index bbcf459..e72a43c 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -4,6 +4,7 @@ using System.Text; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; +using Octobot.Parsers; using Octobot.Services; using Octobot.Services.Update; using Remora.Commands.Attributes; @@ -53,7 +54,7 @@ public class BanCommandGroup : CommandGroup /// A slash command that bans a Discord user with the specified reason. ///
/// The user to ban. - /// The duration for this ban. The user will be automatically unbanned after this duration. + /// The duration for this ban. The user will be automatically unbanned after this duration. /// /// The reason for this ban. Must be encoded with when passed to /// . @@ -75,7 +76,8 @@ public class BanCommandGroup : CommandGroup [Description("User to ban")] IUser target, [Description("Ban reason")] [MaxLength(256)] string reason, - [Description("Ban duration")] TimeSpan? duration = null) + [Description("Ban duration")] [Option("duration")] + string? stringDuration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -104,6 +106,23 @@ public class BanCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); + if (stringDuration is null) + { + return await BanUserAsync(executor, target, reason, null, guild, data, channelId, bot, + CancellationToken); + } + + var parseResult = TimeSpanParser.TryParse(stringDuration); + if (!parseResult.IsDefined(out var duration)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + return await BanUserAsync(executor, target, reason, duration, guild, data, channelId, bot, CancellationToken); } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index c7b21f6..0156f82 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -4,6 +4,7 @@ using System.Text; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; +using Octobot.Parsers; using Octobot.Services; using Octobot.Services.Update; using Remora.Commands.Attributes; @@ -50,7 +51,7 @@ public class MuteCommandGroup : CommandGroup /// A slash command that mutes a Discord member with the specified reason. ///
/// The member to mute. - /// The duration for this mute. The member will be automatically unmuted after this duration. + /// The duration for this mute. The member will be automatically unmuted after this duration. /// /// The reason for this mute. Must be encoded with when passed to /// . @@ -72,7 +73,8 @@ public class MuteCommandGroup : CommandGroup [Description("Member to mute")] IUser target, [Description("Mute reason")] [MaxLength(256)] string reason, - [Description("Mute duration")] TimeSpan duration) + [Description("Mute duration")] [Option("duration")] + string stringDuration) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -104,6 +106,17 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken); } + var parseResult = TimeSpanParser.TryParse(stringDuration); + if (!parseResult.IsDefined(out var duration)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); } diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 67e7910..5e8c9c5 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -17,6 +17,7 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using Octobot.Parsers; namespace Octobot.Commands; @@ -110,7 +111,7 @@ public class RemindCommandGroup : CommandGroup /// /// A slash command that schedules a reminder with the specified text. /// - /// The period of time which must pass before the reminder will be sent. + /// The period of time which must pass before the reminder will be sent. /// The text of the reminder. /// A feedback sending result which may or may not have succeeded. [Command("remind")] @@ -120,7 +121,8 @@ public class RemindCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteReminderAsync( [Description("After what period of time mention the reminder")] - TimeSpan @in, + [Option("in")] + string timeSpanString, [Description("Reminder text")] [MaxLength(512)] string text) { @@ -129,6 +131,12 @@ public class RemindCommandGroup : CommandGroup return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { @@ -138,14 +146,25 @@ public class RemindCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await AddReminderAsync(@in, text, data, channelId, executor, CancellationToken); + var parseResult = TimeSpanParser.TryParse(timeSpanString); + if (!parseResult.IsDefined(out var timeSpan)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + + return await AddReminderAsync(timeSpan, text, data, channelId, executor, CancellationToken); } - private async Task AddReminderAsync(TimeSpan @in, string text, GuildData data, + private async Task AddReminderAsync(TimeSpan timeSpan, string text, GuildData data, Snowflake channelId, IUser executor, CancellationToken ct = default) { var memberData = data.GetOrCreateMemberData(executor.ID); - var remindAt = DateTimeOffset.UtcNow.Add(@in); + var remindAt = DateTimeOffset.UtcNow.Add(timeSpan); var responseResult = await _interactionApi.GetOriginalInteractionResponseAsync(_context.Interaction.ApplicationID, _context.Interaction.Token, ct); if (!responseResult.IsDefined(out var response)) { diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 1dbf72d..b539355 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -4,6 +4,7 @@ using System.Text; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; +using Octobot.Parsers; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -418,7 +419,7 @@ public class ToolsCommandGroup : CommandGroup /// /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. /// - /// The offset for the current timestamp. + /// The offset for the current timestamp. /// /// A feedback sending result which may or may not have succeeded. /// @@ -427,14 +428,20 @@ public class ToolsCommandGroup : CommandGroup [Description("Shows a timestamp in all styles")] [UsedImplicitly] public async Task ExecuteTimestampAsync( - [Description("Offset from current time")] - TimeSpan? offset = null) + [Description("Offset from current time")] [Option("offset")] + string? stringOffset = null) { if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { @@ -444,6 +451,22 @@ public class ToolsCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); + if (stringOffset is null) + { + return await SendTimestampAsync(null, executor, CancellationToken); + } + + var parseResult = TimeSpanParser.TryParse(stringOffset); + if (!parseResult.IsDefined(out var offset)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + return await SendTimestampAsync(offset, executor, CancellationToken); } diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index 7f60ebb..b9b405f 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -1,13 +1,11 @@ using System.Text.Json.Nodes; -using Remora.Commands.Parsers; +using Octobot.Parsers; using Remora.Results; namespace Octobot.Data.Options; public sealed class TimeSpanOption : Option { - private static readonly TimeSpanParser Parser = new(); - public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { } public override TimeSpan Get(JsonNode settings) @@ -29,6 +27,6 @@ public sealed class TimeSpanOption : Option private static Result ParseTimeSpan(string from) { - return Parser.TryParseAsync(from).AsTask().GetAwaiter().GetResult(); + return TimeSpanParser.TryParse(from); } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 8dad3dc..f5a06c0 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1037,6 +1037,14 @@ namespace Octobot { } } + internal static string InvalidTimeSpan + { + get + { + return ResourceManager.GetString("InvalidTimeSpan", resourceCulture); + } + } + internal static string UserInfoKicked { get diff --git a/src/Parsers/TimeSpanParser.cs b/src/Parsers/TimeSpanParser.cs new file mode 100644 index 0000000..1f44d46 --- /dev/null +++ b/src/Parsers/TimeSpanParser.cs @@ -0,0 +1,78 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using JetBrains.Annotations; +using Remora.Commands.Parsers; +using Remora.Results; + +namespace Octobot.Parsers; + +/// +/// Parses s. +/// +[PublicAPI] +public partial class TimeSpanParser : AbstractTypeParser +{ + private static readonly Regex Pattern = ParseRegex(); + + /// + /// Parses a from the . + /// + /// + /// The parsed , or if parsing failed. + /// + public static Result TryParse(string timeSpanString) + { + if (timeSpanString.StartsWith('-')) + { + return new ArgumentInvalidError(nameof(timeSpanString), "TimeSpans cannot be negative."); + } + + if (TimeSpan.TryParse(timeSpanString, DateTimeFormatInfo.InvariantInfo, out var parsedTimeSpan)) + { + return parsedTimeSpan; + } + + var matches = ParseRegex().Matches(timeSpanString); + return matches.Count > 0 + ? ParseFromRegex(matches) + : new ArgumentInvalidError(nameof(timeSpanString), "The regex did not produce any matches."); + } + + private static Result ParseFromRegex(MatchCollection matches) + { + var timeSpan = TimeSpan.Zero; + + foreach (var groups in matches.Select(match => match.Groups + .Cast() + .Where(g => g.Success) + .Skip(1) + .Select(g => (g.Name, g.Value)))) + { + foreach ((var key, var groupValue) in groups) + { + if (!int.TryParse(groupValue, out var parsedIntegerValue)) + { + return new ArgumentInvalidError(nameof(groupValue), "The input value was not an integer."); + } + + var now = DateTimeOffset.UtcNow; + timeSpan += key switch + { + "Years" => now.AddYears(parsedIntegerValue) - now, + "Months" => now.AddMonths(parsedIntegerValue) - now, + "Weeks" => TimeSpan.FromDays(parsedIntegerValue * 7), + "Days" => TimeSpan.FromDays(parsedIntegerValue), + "Hours" => TimeSpan.FromHours(parsedIntegerValue), + "Minutes" => TimeSpan.FromMinutes(parsedIntegerValue), + "Seconds" => TimeSpan.FromSeconds(parsedIntegerValue), + _ => throw new ArgumentOutOfRangeException(key) + }; + } + } + + return timeSpan; + } + + [GeneratedRegex("(?\\d+(?=y|л|г))|(?\\d+(?=mo|мес))|(?\\d+(?=w|н|нед))|(?\\d+(?=d|д|дн))|(?\\d+(?=h|ч))|(?\\d+(?=m|min|мин|м))|(?\\d+(?=s|sec|с|сек))")] + private static partial Regex ParseRegex(); +} From 6a928482af7a49dcf45e14cfc58c94c8cc27cec3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 08:11:17 +0300 Subject: [PATCH 251/329] Bump muno92/resharper_inspectcode from 1.11.1 to 1.11.3 (#248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.11.1 to 1.11.3.
Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.11.3

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.11.2...1.11.3

1.11.2

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.11.1...1.11.2

Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.11.3 - 2024-01-14

1.11.2 - 2024-01-12

Commits
  • 2325281 Merge pull request #447 from muno92/tagpr-from-1.11.2
  • 647d06f Compile
  • cdd28cb [tagpr] update CHANGELOG.md
  • fa426c0 [tagpr] prepare for the next release
  • 17b3b07 Merge pull request #446 from muno92/renovate/all-minor-patch
  • 060783e Update dependency prettier to v3.2.2
  • 9251310 Merge pull request #445 from muno92/tagpr-from-1.11.1
  • da76157 Compile
  • adedbdf [tagpr] update CHANGELOG.md
  • 8d31ede [tagpr] prepare for the next release
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.11.1&new-version=1.11.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index b697dac..512ebc4 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.1 + uses: muno92/resharper_inspectcode@1.11.3 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 202daf5c1ff3c8cc1f7d953295db0dcc6bd719e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 14:28:24 +0300 Subject: [PATCH 252/329] Bump muno92/resharper_inspectcode from 1.11.3 to 1.11.5 (#249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.11.3 to 1.11.5.
Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.11.5

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.11.4...1.11.5

1.11.4

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.11.3...1.11.4

Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.11.5 - 2024-01-17

1.11.4 - 2024-01-17

Commits
  • fc10815 Merge pull request #451 from muno92/tagpr-from-1.11.4
  • bc33dd2 Compile
  • 028cfde [tagpr] update CHANGELOG.md
  • 6409723 [tagpr] prepare for the next release
  • c3894f5 Merge pull request #450 from muno92/renovate/all-minor-patch
  • 760594a Update dependency prettier to v3.2.4
  • 95aa802 Merge pull request #449 from muno92/tagpr-from-1.11.3
  • 48e3450 Compile
  • b5aefe4 [tagpr] update CHANGELOG.md
  • af674ca [tagpr] prepare for the next release
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.11.3&new-version=1.11.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 512ebc4..e93a460 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.3 + uses: muno92/resharper_inspectcode@1.11.5 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 83e2c5040ec819a704bfba782a4594ef883c57de Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 22 Jan 2024 18:47:53 +0300 Subject: [PATCH 253/329] Remove mctaylors.ru mentions (#251) Signed-off-by: mctaylors --- Octobot.csproj | 8 ++++---- docs/CONTRIBUTING.md | 2 +- docs/README.md | 22 +++++---------------- docs/octobot-banner.png | Bin 0 -> 122946 bytes src/Commands/AboutCommandGroup.cs | 2 +- src/Octobot.cs | 2 +- src/Responders/MessageReceivedResponder.cs | 2 +- 7 files changed, 13 insertions(+), 25 deletions(-) create mode 100644 docs/octobot-banner.png diff --git a/Octobot.csproj b/Octobot.csproj index e8f0dfa..1f050a6 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -9,11 +9,11 @@ Octobot Octol1ttle, mctaylors, neroduckale AGPLv3 - https://github.com/LabsDevelopment/Octobot - https://github.com/LabsDevelopment/Octobot/blob/master/LICENSE - https://github.com/LabsDevelopment/Octobot + https://github.com/TeamOctolings/Octobot + https://github.com/TeamOctolings/Octobot/blob/master/LICENSE + https://github.com/TeamOctolings/Octobot github - LabsDevelopment + TeamOctolings en A general-purpose Discord bot for moderation written in C# docs/octobot.ico diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 2a15ef2..dc5a793 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -29,7 +29,7 @@ While pull requests from unaffiliated contributors are welcome, please note that internal issues that haven't been published to the issue tracker yet. Reviewing PRs is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change. -The [issue tracker](https://github.com/LabsDevelopment/Octobot/issues) should provide plenty of issues to start with. +The [issue tracker](https://github.com/TeamOctolings/Octobot/issues) should provide plenty of issues to start with. Make sure to check that an issue you're planning to resolve does not already have people working on it and that there are no PRs associated with it diff --git a/docs/README.md b/docs/README.md index 5be0bd8..7056857 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,12 +1,12 @@

- Octobot banner + Octobot banner

- + - + -Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Labs Development Team](https://github.com/LabsDevelopment) in C# and Remora.Discord +Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Team Octolings](https://github.com/TeamOctolings) in C# and Remora.Discord ## Features @@ -19,25 +19,13 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr *...a-a-and more!* -[//]: # (if you are reading this, message @mctaylors and ask him to bring back the wiki) - -## Invite Octobot - -Did you know that Octobot is a public bot? You can invite it to your server and use it without building it! -

- -

- -> [!IMPORTANT] -> The bot will not be able to respond in private channels unless you have configured permissions for the bot in those channels. - ## Building Octobot 1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents! 3. Clone this repository and open `Octobot` folder. ``` -git clone https://github.com/LabsDevelopment/Octobot +git clone https://github.com/TeamOctolings/Octobot cd Octobot ``` 4. Run Octobot using `dotnet` with `BOT_TOKEN` variable. diff --git a/docs/octobot-banner.png b/docs/octobot-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..2ab5f5baed6cab8a85ada32ef0b5a73171c6cd72 GIT binary patch literal 122946 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 PFfe$!`njxgN@xNAOS>OW literal 0 HcmV?d00001 diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 4c396d9..e978ec9 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -100,7 +100,7 @@ public class AboutCommandGroup : CommandGroup .WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png") + .WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png") .Build(); var repositoryButton = new ButtonComponent( diff --git a/src/Octobot.cs b/src/Octobot.cs index 063bd14..a2b4773 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -23,7 +23,7 @@ namespace Octobot; public sealed class Octobot { - public const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot"; + public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; public const string IssuesUrl = $"{RepositoryUrl}/issues"; public static readonly AllowedMentions NoMentions = new( diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs index f5c65f4..4ac312b 100644 --- a/src/Responders/MessageReceivedResponder.cs +++ b/src/Responders/MessageReceivedResponder.cs @@ -28,7 +28,7 @@ public class MessageCreateResponder : IResponder "whoami" => "`nobody`", "сука !!" => "`root`", "воооо" => "`removing /...`", - "пон" => "https://cdn.upload.systems/uploads/2LNfUSwM.jpg", + "пон" => "https://i.ibb.co/Kw6QVcw/parry.jpg", "++++" => "#", "осу" => "https://github.com/ppy/osu", _ => default(Optional) From b4ae9cca2dbad498efe97871bc89effb66d8971a Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:59:16 +0300 Subject: [PATCH 254/329] Update CODEOWNERS (#253) Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 643492d..93a190c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -* @LabsDevelopment/octobot -/docs/ @LabsDevelopment/octobot-docs +* @TeamOctolings/octobot +/docs/ @TeamOctolings/octobot-docs From 7e9c08cab72d680741c4b17f4972812ebd716a48 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sat, 27 Jan 2024 15:06:10 +0300 Subject: [PATCH 255/329] Don't use Option attribute in ExecuteBanAsync method (#252) https://github.com/TeamOctolings/Octobot/issues/246#issuecomment-1912579699 > The `Option` attribute also somehow affects the command update behavior. I'll get rid of it then. Closes #246 Signed-off-by: mctaylors --- src/Commands/BanCommandGroup.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index e72a43c..926e615 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -54,7 +54,7 @@ public class BanCommandGroup : CommandGroup /// A slash command that bans a Discord user with the specified reason. ///
/// The user to ban. - /// The duration for this ban. The user will be automatically unbanned after this duration. + /// The duration for this ban. The user will be automatically unbanned after this duration. /// /// The reason for this ban. Must be encoded with when passed to /// . @@ -76,8 +76,7 @@ public class BanCommandGroup : CommandGroup [Description("User to ban")] IUser target, [Description("Ban reason")] [MaxLength(256)] string reason, - [Description("Ban duration")] [Option("duration")] - string? stringDuration = null) + [Description("Ban duration")] string? duration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -106,14 +105,14 @@ public class BanCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - if (stringDuration is null) + if (duration is null) { return await BanUserAsync(executor, target, reason, null, guild, data, channelId, bot, CancellationToken); } - var parseResult = TimeSpanParser.TryParse(stringDuration); - if (!parseResult.IsDefined(out var duration)) + var parseResult = TimeSpanParser.TryParse(duration); + if (!parseResult.IsDefined(out var timeSpan)) { var failedEmbed = new EmbedBuilder() .WithSmallTitle(Messages.InvalidTimeSpan, bot) @@ -123,7 +122,7 @@ public class BanCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); } - return await BanUserAsync(executor, target, reason, duration, guild, data, channelId, bot, CancellationToken); + return await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId, bot, CancellationToken); } private async Task BanUserAsync( From af84f8853ad4bffde0ede8b89256d508d958ecdb Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sat, 27 Jan 2024 15:50:27 +0300 Subject: [PATCH 256/329] Add /editremind (#247) In this PR, I'm adding a command to modify existing reminders. This can be useful if you, for example, got the name a bit wrong or set the wrong reminder time. Just use /editremind and recreating the reminder from scratch will no longer be necessary. --------- Signed-off-by: mctaylors --- locale/Messages.resx | 3 + locale/Messages.ru.resx | 3 + locale/Messages.tt-ru.resx | 3 + src/Commands/RemindCommandGroup.cs | 126 +++++++++++++++++++++++++++++ src/Messages.Designer.cs | 6 ++ 5 files changed, 141 insertions(+) diff --git a/locale/Messages.resx b/locale/Messages.resx index adc9f6d..218c414 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -591,4 +591,7 @@ Kicked + + Reminder edited + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index de2158d..3eb53f1 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -591,4 +591,7 @@ Выгнан + + Напоминание отредактировано + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index ca3c19d..f5d789b 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -591,4 +591,7 @@ кикнут + + напоминалка подправлена + diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 5e8c9c5..c270f30 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -193,6 +193,132 @@ public class RemindCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + public enum Parameters + { + [UsedImplicitly] Time, + [UsedImplicitly] Text + } + + /// + /// A slash command that edits a scheduled reminder using the specified text or time. + /// + /// The list position of the reminder to edit. + /// The reminder's parameter to edit. + /// The new value for the reminder as a text or time. + /// A feedback sending result which may or may not have succeeded. + [Command("editremind")] + [Description("Edit a reminder")] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [UsedImplicitly] + public async Task ExecuteEditReminderAsync( + [Description("Position in list")] [MinValue(1)] + int position, + [Description("Parameter to edit")] Parameters parameter, + [Description("Parameter's new value")] string value) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) + { + return Result.FromError(executorResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + var memberData = data.GetOrCreateMemberData(executor.ID); + + if (parameter is Parameters.Time) + { + return await EditReminderTimeAsync(position - 1, value, memberData, bot, executor, CancellationToken); + } + + return await EditReminderTextAsync(position - 1, value, memberData, bot, executor, CancellationToken); + } + + private async Task EditReminderTimeAsync(int index, string value, MemberData data, + IUser bot, IUser executor, CancellationToken ct = default) + { + if (index >= data.Reminders.Count) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var parseResult = TimeSpanParser.TryParse(value); + if (!parseResult.IsDefined(out var timeSpan)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var oldReminder = data.Reminders[index]; + var remindAt = DateTimeOffset.UtcNow.Add(timeSpan); + + data.Reminders.Add(oldReminder with { At = remindAt }); + data.Reminders.RemoveAt(index); + + var builder = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(oldReminder.Text))) + .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.ReminderEdited, executor.GetTag()), executor) + .WithDescription(builder.ToString()) + .WithColour(ColorsList.Cyan) + .WithFooter(string.Format(Messages.ReminderPosition, data.Reminders.Count)) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + + private async Task EditReminderTextAsync(int index, string value, MemberData data, + IUser bot, IUser executor, CancellationToken ct = default) + { + if (index >= data.Reminders.Count) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var oldReminder = data.Reminders[index]; + + data.Reminders.Add(oldReminder with { Text = value }); + data.Reminders.RemoveAt(index); + + var builder = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(value))) + .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(oldReminder.At))); + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.ReminderEdited, executor.GetTag()), executor) + .WithDescription(builder.ToString()) + .WithColour(ColorsList.Cyan) + .WithFooter(string.Format(Messages.ReminderPosition, data.Reminders.Count)) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + /// /// A slash command that deletes a reminder using its list position. /// diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index f5a06c0..a0c915a 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1052,5 +1052,11 @@ namespace Octobot { return ResourceManager.GetString("UserInfoKicked", resourceCulture); } } + + internal static string ReminderEdited { + get { + return ResourceManager.GetString("ReminderEdited", resourceCulture); + } + } } } From f034ede58dece5357d5ac6ee3b85a94682bba635 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 28 Jan 2024 15:27:21 +0300 Subject: [PATCH 257/329] =?UTF-8?q?Parry=20"=D0=BB=D0=B0=D0=BD"=20flooders?= =?UTF-8?q?=20(#256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- src/Responders/MessageReceivedResponder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs index 4ac312b..6ab7199 100644 --- a/src/Responders/MessageReceivedResponder.cs +++ b/src/Responders/MessageReceivedResponder.cs @@ -31,6 +31,7 @@ public class MessageCreateResponder : IResponder "пон" => "https://i.ibb.co/Kw6QVcw/parry.jpg", "++++" => "#", "осу" => "https://github.com/ppy/osu", + "лан" => "https://i.ibb.co/VYH2QLc/lan.jpg", _ => default(Optional) }); return Task.FromResult(Result.FromSuccess()); From 290449077a2541f87d836bb6fd9f553b44994134 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:36:29 +0300 Subject: [PATCH 258/329] Use TimeSpanParser.TryParse instead of ParseTimeSpan (#257) The ParseTimeSpan method is not needed because we no longer use the quirky (IMO) and long `Parser.TryParseAsync(from).AsTask().GetAwaiter().GetResult()` to parse TimeSpan Signed-off-by: mctaylors --- src/Data/Options/TimeSpanOption.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index b9b405f..c81a02d 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -11,12 +11,12 @@ public sealed class TimeSpanOption : Option public override TimeSpan Get(JsonNode settings) { var property = settings[Name]; - return property != null ? ParseTimeSpan(property.GetValue()).Entity : DefaultValue; + return property != null ? TimeSpanParser.TryParse(property.GetValue()).Entity : DefaultValue; } public override Result Set(JsonNode settings, string from) { - if (!ParseTimeSpan(from).IsDefined(out var span)) + if (!TimeSpanParser.TryParse(from).IsDefined(out var span)) { return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); } @@ -24,9 +24,4 @@ public sealed class TimeSpanOption : Option settings[Name] = span.ToString(); return Result.FromSuccess(); } - - private static Result ParseTimeSpan(string from) - { - return TimeSpanParser.TryParse(from); - } } From 5b4d581325b3db7a776c878811331fff49efdad9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:53:55 +0300 Subject: [PATCH 259/329] Bump muno92/resharper_inspectcode from 1.11.5 to 1.11.6 (#258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.11.5 to 1.11.6.
Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.11.6

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.11.5...1.11.6

Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.11.6 - 2024-02-04

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.11.5&new-version=1.11.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index e93a460..4867741 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.5 + uses: muno92/resharper_inspectcode@1.11.6 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 5483bbd2034c7e9e1029ab03576d8ab5ddcc11d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:09:26 +0000 Subject: [PATCH 260/329] Bump the remora group with 4 updates (#259) --- Octobot.csproj | 2 +- src/Commands/ToolsCommandGroup.cs | 4 ++-- src/Services/Update/MemberUpdateService.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Octobot.csproj b/Octobot.csproj index 1f050a6..1c2c08c 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index b539355..fb87117 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -101,10 +101,10 @@ public class ToolsCommandGroup : CommandGroup { var builder = new StringBuilder().AppendLine($"### <@{target.ID}>"); - if (target.GlobalName is not null) + if (target.GlobalName.IsDefined(out var globalName)) { builder.AppendBulletPointLine(Messages.UserInfoDisplayName) - .AppendLine(Markdown.InlineCode(target.GlobalName)); + .AppendLine(Markdown.InlineCode(globalName)); } builder.AppendBulletPointLine(Messages.UserInfoDiscordUserSince) diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 06e531f..7674bbe 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -185,7 +185,7 @@ public sealed partial class MemberUpdateService : BackgroundService { var currentNickname = member.Nickname.IsDefined(out var nickname) ? nickname - : user.GlobalName ?? user.Username; + : user.GlobalName.OrDefault(user.Username); var characterList = currentNickname.ToList(); var usernameChanged = false; foreach (var character in currentNickname) From 58bd439aa72473c6c69fd488b42837e18c9c4bc0 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 6 Feb 2024 23:35:33 +0500 Subject: [PATCH 261/329] Revert "Add profiler base" (#260) Reverts TeamOctolings/Octobot#235 See #244 --- src/Octobot.cs | 3 - src/Services/Profiler/Profiler.cs | 114 ----------------------- src/Services/Profiler/ProfilerEvent.cs | 9 -- src/Services/Profiler/ProfilerFactory.cs | 27 ------ 4 files changed, 153 deletions(-) delete mode 100644 src/Services/Profiler/Profiler.cs delete mode 100644 src/Services/Profiler/ProfilerEvent.cs delete mode 100644 src/Services/Profiler/ProfilerFactory.cs diff --git a/src/Octobot.cs b/src/Octobot.cs index a2b4773..1ebf7c3 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Octobot.Commands.Events; using Octobot.Services; -using Octobot.Services.Profiler; using Octobot.Services.Update; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; @@ -87,8 +86,6 @@ public sealed class Octobot .AddPreparationErrorEvent() .AddPostExecutionEvent() // Services - .AddTransient() - .AddSingleton() .AddSingleton() .AddSingleton() .AddHostedService(provider => provider.GetRequiredService()) diff --git a/src/Services/Profiler/Profiler.cs b/src/Services/Profiler/Profiler.cs deleted file mode 100644 index 8d4ca98..0000000 --- a/src/Services/Profiler/Profiler.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Diagnostics; -using System.Text; -using Microsoft.Extensions.Logging; -using Remora.Results; - -// TODO: remove in future profiler PRs -// ReSharper disable All - -namespace Octobot.Services.Profiler; - -/// -/// Provides the ability to profile how long certain parts of code take to complete using es. -/// -/// Resolve instead in singletons. -public sealed class Profiler -{ - private const int MaxProfilerTime = 1000; // milliseconds - private readonly List _events = []; - private readonly ILogger _logger; - - public Profiler(ILogger logger) - { - _logger = logger; - } - - /// - /// Pushes an event to the profiler. - /// - /// The ID of the event. - public void Push(string id) - { - _events.Add(new ProfilerEvent - { - Id = id, - Stopwatch = Stopwatch.StartNew() - }); - } - - /// - /// Pops the last pushed event from the profiler. - /// - /// Thrown if the profiler contains no events. - public void Pop() - { - if (_events.Count is 0) - { - throw new InvalidOperationException("Nothing to pop"); - } - - _events.Last().Stopwatch.Stop(); - } - - /// - /// If the profiler took too long to execute, this will log a warning with per-event time usage - /// - /// - private void Report() - { - var main = _events[0]; - if (main.Stopwatch.ElapsedMilliseconds < MaxProfilerTime) - { - return; - } - - var unprofiled = main.Stopwatch.ElapsedMilliseconds; - var builder = new StringBuilder().AppendLine(); - for (var i = 1; i < _events.Count; i++) - { - var profilerEvent = _events[i]; - if (profilerEvent.Stopwatch.IsRunning) - { - throw new InvalidOperationException( - $"Tried to report on a profiler with running stopwatches: {profilerEvent.Id}"); - } - - builder.AppendLine($"{profilerEvent.Id}: {profilerEvent.Stopwatch.ElapsedMilliseconds}ms"); - unprofiled -= profilerEvent.Stopwatch.ElapsedMilliseconds; - } - - builder.AppendLine($": {unprofiled}ms"); - - _logger.LogWarning("Profiler {ID} took {Elapsed} milliseconds to execute (max: {Max}ms):{Events}", main.Id, - main.Stopwatch.ElapsedMilliseconds, MaxProfilerTime, builder.ToString()); - } - - /// - /// the profiler and on it afterwards. - /// - public void PopAndReport() - { - Pop(); - Report(); - } - - /// - /// on the profiler and return a . - /// - /// - /// - public Result ReportWithResult(Result result) - { - PopAndReport(); - return result; - } - - /// - /// Calls with - /// - /// A successful result. - public Result ReportWithSuccess() - { - return ReportWithResult(Result.FromSuccess()); - } -} diff --git a/src/Services/Profiler/ProfilerEvent.cs b/src/Services/Profiler/ProfilerEvent.cs deleted file mode 100644 index f655fc4..0000000 --- a/src/Services/Profiler/ProfilerEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Diagnostics; - -namespace Octobot.Services.Profiler; - -public struct ProfilerEvent -{ - public string Id { get; init; } - public Stopwatch Stopwatch { get; init; } -} diff --git a/src/Services/Profiler/ProfilerFactory.cs b/src/Services/Profiler/ProfilerFactory.cs deleted file mode 100644 index 0135771..0000000 --- a/src/Services/Profiler/ProfilerFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Octobot.Services.Profiler; - -/// -/// Provides a method to create a . Useful in singletons. -/// -public sealed class ProfilerFactory -{ - private readonly IServiceScopeFactory _scopeFactory; - - public ProfilerFactory(IServiceScopeFactory scopeFactory) - { - _scopeFactory = scopeFactory; - } - - /// - /// Creates a new . - /// - /// A new . - // TODO: remove in future profiler PRs - // ReSharper disable once UnusedMember.Global - public Profiler Create() - { - return _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - } -} From d39303560d2c1863fa36c772e457c343bd3578af Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 6 Feb 2024 23:39:20 +0500 Subject: [PATCH 262/329] Make LogAction return void (#261) 1) the method isn't actually async lulw 2) it always returns success, so might as well just be void reduces complexity by a bit Signed-off-by: Octol1ttle --- src/Commands/BanCommandGroup.cs | 13 +++---------- src/Commands/ClearCommandGroup.cs | 6 +----- src/Commands/KickCommandGroup.cs | 7 ++----- src/Commands/MuteCommandGroup.cs | 13 +++---------- src/Commands/SettingsCommandGroup.cs | 6 +----- src/Services/Utility.cs | 6 ++---- 6 files changed, 12 insertions(+), 39 deletions(-) diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 926e615..6dbf9b9 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -196,12 +196,8 @@ public class BanCommandGroup : CommandGroup title, target) .WithColour(ColorsList.Green).Build(); - var logResult = _utility.LogActionAsync( + _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } @@ -287,12 +283,9 @@ public class BanCommandGroup : CommandGroup var title = string.Format(Messages.UserUnbanned, target.GetTag()); var description = new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason)); - var logResult = _utility.LogActionAsync( + + _utility.LogAction( data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 1d0ad64..395810f 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -136,12 +136,8 @@ public class ClearCommandGroup : CommandGroup return Result.FromError(deleteResult.Error); } - var logResult = _utility.LogActionAsync( + _utility.LogAction( data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithColour(ColorsList.Green).Build(); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index a278fb4..0faa1d3 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -157,12 +157,9 @@ public class KickCommandGroup : CommandGroup var title = string.Format(Messages.UserKicked, target.GetTag()); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); - var logResult = _utility.LogActionAsync( + + _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserKicked, target.GetTag()), target) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 0156f82..788eb2c 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -153,12 +153,8 @@ public class MuteCommandGroup : CommandGroup .AppendBulletPoint(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); - var logResult = _utility.LogActionAsync( + _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserMuted, target.GetTag()), target) @@ -339,12 +335,9 @@ public class MuteCommandGroup : CommandGroup var title = string.Format(Messages.UserUnmuted, target.GetTag()); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); - var logResult = _utility.LogActionAsync( + + _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Green, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnmuted, target.GetTag()), target) diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index ce7472f..acfb8ed 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -218,12 +218,8 @@ public class SettingsCommandGroup : CommandGroup var title = Messages.SettingSuccessfullyChanged; var description = builder.ToString(); - var logResult = _utility.LogActionAsync( + _utility.LogAction( data.Settings, channelId, executor, title, description, bot, ColorsList.Magenta, false, ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithDescription(description) diff --git a/src/Services/Utility.cs b/src/Services/Utility.cs index 401b067..ad06315 100644 --- a/src/Services/Utility.cs +++ b/src/Services/Utility.cs @@ -196,7 +196,7 @@ public sealed class Utility /// /// The cancellation token for this operation. /// A result which has succeeded. - public Result LogActionAsync( + public void LogAction( JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar, Color color, bool isPublic = true, CancellationToken ct = default) { @@ -205,7 +205,7 @@ public sealed class Utility if (GuildSettings.PublicFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId) && GuildSettings.PrivateFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId)) { - return Result.FromSuccess(); + return; } var logEmbed = new EmbedBuilder().WithSmallTitle(title, avatar) @@ -230,8 +230,6 @@ public sealed class Utility privateChannel, embedResult: logEmbed, ct: ct); } - - return Result.FromSuccess(); } public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct) From fe22ed3025daa94a73d261395fa7e86f76a14e48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 08:44:52 +0000 Subject: [PATCH 263/329] Bump muno92/resharper_inspectcode from 1.11.6 to 1.11.7 (#262) --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 4867741..8002f6f 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.6 + uses: muno92/resharper_inspectcode@1.11.7 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From bf8a89c4e967efc69e4dc8eadf04617ad6a2f79f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:56:14 +0500 Subject: [PATCH 264/329] Bump the remora group with 3 updates (#263) --- Octobot.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Octobot.csproj b/Octobot.csproj index 1c2c08c..ab76400 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -26,9 +26,9 @@ - - - + + + From 8eed295fcda981fdd506b20a187634d60434f239 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:40:29 +0300 Subject: [PATCH 265/329] Add /8ball command (#264) @neroduckale was bored so I made this feature. --------- Signed-off-by: mctaylors Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> Co-authored-by: Octol1ttle --- locale/Messages.resx | 60 +++++++++++++++ locale/Messages.ru.resx | 60 +++++++++++++++ locale/Messages.tt-ru.resx | 60 +++++++++++++++ src/Commands/ToolsCommandGroup.cs | 63 +++++++++++++++- src/Messages.Designer.cs | 120 ++++++++++++++++++++++++++++++ 5 files changed, 362 insertions(+), 1 deletion(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 218c414..b881996 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -594,4 +594,64 @@ Reminder edited + + It is certain + + + It is decidedly so + + + Without a doubt + + + Yes — definitely + + + You may rely on it + + + As I see it, yes + + + Most likely + + + Outlook good + + + Signs point to yes + + + Yes + + + Reply hazy, try again + + + Ask again later + + + Better not tell you now + + + Cannot predict now + + + Concentrate and ask again + + + Don’t count on it + + + My reply is no + + + My sources say no + + + Outlook not so good + + + Very doubtful + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 3eb53f1..cb318cd 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -594,4 +594,64 @@ Напоминание отредактировано + + Бесспорно + + + Предрешено + + + Никаких сомнений + + + Определённо да + + + Можешь быть уверен в этом + + + Мне кажется — «да» + + + Вероятнее всего + + + Хорошие перспективы + + + Знаки говорят — «да» + + + Да + + + Пока не ясно, попробуй снова + + + Спроси позже + + + Лучше не рассказывать + + + Сейчас нельзя предсказать + + + Сконцентрируйся и спроси снова + + + Даже не думай + + + Мой ответ — «нет» + + + По моим данным — «нет» + + + Перспективы не очень хорошие + + + Весьма сомнительно + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index f5d789b..f0b80a7 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -594,4 +594,64 @@ напоминалка подправлена + + абсолютли + + + заявлено + + + ваще не сомневайся + + + 100% да + + + будь в этом уверен + + + я считаю что да + + + ну вполне вероятно + + + ну выглядит нормально + + + мне сказали ок + + + мгм + + + ну-ка попробуй снова + + + давай позже + + + щас пока не скажу + + + я не могу сейчас предсказать + + + ну сконцентрируйся и давай еще раз + + + даже не думай + + + мое завление это нет + + + я тут посчитал, короче нет + + + выглядит такое себе + + + чот сомневаюсь + diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index fb87117..3c16232 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -21,7 +21,7 @@ using Remora.Results; namespace Octobot.Commands; /// -/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp. +/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp, /8ball. /// [UsedImplicitly] public class ToolsCommandGroup : CommandGroup @@ -496,4 +496,65 @@ public class ToolsCommandGroup : CommandGroup return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + + /// + /// A slash command that shows a random answer from the Magic 8-Ball. + /// + /// Unused input. + /// + /// The 8-Ball answers were taken from Wikipedia. + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("8ball")] + [DiscordDefaultDMPermission(false)] + [Description("Ask the Magic 8-Ball a question")] + [UsedImplicitly] + public async Task ExecuteEightBallAsync( + // let the user think he's actually asking the ball a question + string question) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await AnswerEightBallAsync(bot, CancellationToken); + } + + private static readonly string[] AnswerTypes = + [ + "Positive", "Questionable", "Neutral", "Negative" + ]; + + private Task AnswerEightBallAsync(IUser bot, CancellationToken ct) + { + var typeNumber = Random.Shared.Next(0, 4); + var embedColor = typeNumber switch + { + 0 => ColorsList.Blue, + 1 => ColorsList.Green, + 2 => ColorsList.Yellow, + 3 => ColorsList.Red, + _ => throw new ArgumentOutOfRangeException(null, nameof(typeNumber)) + }; + + var answer = $"EightBall{AnswerTypes[typeNumber]}{Random.Shared.Next(1, 6)}".Localized(); + + var embed = new EmbedBuilder().WithSmallTitle(answer, bot) + .WithColour(embedColor) + .Build(); + + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index a0c915a..9597bcd 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1058,5 +1058,125 @@ namespace Octobot { return ResourceManager.GetString("ReminderEdited", resourceCulture); } } + + internal static string EightBallPositive1 { + get { + return ResourceManager.GetString("EightBallPositive1", resourceCulture); + } + } + + internal static string EightBallPositive2 { + get { + return ResourceManager.GetString("EightBallPositive2", resourceCulture); + } + } + + internal static string EightBallPositive3 { + get { + return ResourceManager.GetString("EightBallPositive3", resourceCulture); + } + } + + internal static string EightBallPositive4 { + get { + return ResourceManager.GetString("EightBallPositive4", resourceCulture); + } + } + + internal static string EightBallPositive5 { + get { + return ResourceManager.GetString("EightBallPositive5", resourceCulture); + } + } + + internal static string EightBallQuestionable1 { + get { + return ResourceManager.GetString("EightBallQuestionable1", resourceCulture); + } + } + + internal static string EightBallQuestionable2 { + get { + return ResourceManager.GetString("EightBallQuestionable2", resourceCulture); + } + } + + internal static string EightBallQuestionable3 { + get { + return ResourceManager.GetString("EightBallQuestionable3", resourceCulture); + } + } + + internal static string EightBallQuestionable4 { + get { + return ResourceManager.GetString("EightBallQuestionable4", resourceCulture); + } + } + + internal static string EightBallQuestionable5 { + get { + return ResourceManager.GetString("EightBallQuestionable5", resourceCulture); + } + } + + internal static string EightBallNeutral1 { + get { + return ResourceManager.GetString("EightBallNeutral1", resourceCulture); + } + } + + internal static string EightBallNeutral2 { + get { + return ResourceManager.GetString("EightBallNeutral2", resourceCulture); + } + } + + internal static string EightBallNeutral3 { + get { + return ResourceManager.GetString("EightBallNeutral3", resourceCulture); + } + } + + internal static string EightBallNeutral4 { + get { + return ResourceManager.GetString("EightBallNeutral4", resourceCulture); + } + } + + internal static string EightBallNeutral5 { + get { + return ResourceManager.GetString("EightBallNeutral5", resourceCulture); + } + } + + internal static string EightBallNegative1 { + get { + return ResourceManager.GetString("EightBallNegative1", resourceCulture); + } + } + + internal static string EightBallNegative2 { + get { + return ResourceManager.GetString("EightBallNegative2", resourceCulture); + } + } + + internal static string EightBallNegative3 { + get { + return ResourceManager.GetString("EightBallNegative3", resourceCulture); + } + } + + internal static string EightBallNegative4 { + get { + return ResourceManager.GetString("EightBallNegative4", resourceCulture); + } + } + + internal static string EightBallNegative5 { + get { + return ResourceManager.GetString("EightBallNegative5", resourceCulture); + } + } } } From 62709d927be7dcbd19ffe32d03c2a6e77ccf88b9 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 17 Mar 2024 16:46:53 +0300 Subject: [PATCH 266/329] Add time format example to the description of commands that use TimeSpan. (#267) This PR was made to help users who are trying /remind for the first time by showing an example of the correct time format in the description. --------- Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> Signed-off-by: mctaylors --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ locale/Messages.tt-ru.resx | 3 +++ src/Commands/BanCommandGroup.cs | 4 +++- src/Commands/MuteCommandGroup.cs | 3 ++- src/Commands/RemindCommandGroup.cs | 4 +++- src/Commands/ToolsCommandGroup.cs | 1 + src/Messages.Designer.cs | 6 ++++++ 8 files changed, 24 insertions(+), 3 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index b881996..ca48fba 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -654,4 +654,7 @@ Very doubtful + + Example of a valid input: `1h30m` + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index cb318cd..7423347 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -654,4 +654,7 @@ Весьма сомнительно + + Пример правильного ввода: `1ч30м` + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index f0b80a7..dc3bb6f 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -654,4 +654,7 @@ чот сомневаюсь + + правильно пишут так: `1h30m` + diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 6dbf9b9..c350729 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -76,7 +76,8 @@ public class BanCommandGroup : CommandGroup [Description("User to ban")] IUser target, [Description("Ban reason")] [MaxLength(256)] string reason, - [Description("Ban duration")] string? duration = null) + [Description("Ban duration (e.g. 1h30m)")] + string? duration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -116,6 +117,7 @@ public class BanCommandGroup : CommandGroup { var failedEmbed = new EmbedBuilder() .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithDescription(Messages.TimeSpanExample) .WithColour(ColorsList.Red) .Build(); diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 788eb2c..c2542e8 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -73,7 +73,7 @@ public class MuteCommandGroup : CommandGroup [Description("Member to mute")] IUser target, [Description("Mute reason")] [MaxLength(256)] string reason, - [Description("Mute duration")] [Option("duration")] + [Description("Mute duration (e.g. 1h30m)")] [Option("duration")] string stringDuration) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) @@ -111,6 +111,7 @@ public class MuteCommandGroup : CommandGroup { var failedEmbed = new EmbedBuilder() .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithDescription(Messages.TimeSpanExample) .WithColour(ColorsList.Red) .Build(); diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index c270f30..f9c006e 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -120,7 +120,7 @@ public class RemindCommandGroup : CommandGroup [RequireContext(ChannelContext.Guild)] [UsedImplicitly] public async Task ExecuteReminderAsync( - [Description("After what period of time mention the reminder")] + [Description("After what period of time mention the reminder (e.g. 1h30m)")] [Option("in")] string timeSpanString, [Description("Reminder text")] [MaxLength(512)] @@ -151,6 +151,7 @@ public class RemindCommandGroup : CommandGroup { var failedEmbed = new EmbedBuilder() .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithDescription(Messages.TimeSpanExample) .WithColour(ColorsList.Red) .Build(); @@ -264,6 +265,7 @@ public class RemindCommandGroup : CommandGroup { var failedEmbed = new EmbedBuilder() .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithDescription(Messages.TimeSpanExample) .WithColour(ColorsList.Red) .Build(); diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 3c16232..ea91e1e 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -461,6 +461,7 @@ public class ToolsCommandGroup : CommandGroup { var failedEmbed = new EmbedBuilder() .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithDescription(Messages.TimeSpanExample) .WithColour(ColorsList.Red) .Build(); diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 9597bcd..ca460cf 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1178,5 +1178,11 @@ namespace Octobot { return ResourceManager.GetString("EightBallNegative5", resourceCulture); } } + + internal static string TimeSpanExample { + get { + return ResourceManager.GetString("TimeSpanExample", resourceCulture); + } + } } } From 5105b43eff654839edc1d126fa8f33972bfee75b Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:42:37 +0300 Subject: [PATCH 267/329] Add WelcomeMessagesChannel setting (#269) Closes #232 Signed-off-by: mctaylors --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ locale/Messages.tt-ru.resx | 3 +++ src/Commands/SettingsCommandGroup.cs | 1 + src/Data/GuildSettings.cs | 5 +++++ src/Data/Options/AllOptionsEnum.cs | 1 + src/Messages.Designer.cs | 7 +++++++ src/Responders/GuildMemberJoinedResponder.cs | 4 ++-- 8 files changed, 25 insertions(+), 2 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index ca48fba..c8ef510 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -657,4 +657,7 @@ Example of a valid input: `1h30m` + + Welcome messages channel + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 7423347..eb8e57b 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -657,4 +657,7 @@ Пример правильного ввода: `1ч30м` + + Канал для приветствий + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index dc3bb6f..df50d8d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -657,4 +657,7 @@ правильно пишут так: `1h30m` + + канал куда говорить здравствуйте + diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index acfb8ed..86f031f 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -46,6 +46,7 @@ public class SettingsCommandGroup : CommandGroup GuildSettings.RenameHoistedUsers, GuildSettings.PublicFeedbackChannel, GuildSettings.PrivateFeedbackChannel, + GuildSettings.WelcomeMessagesChannel, GuildSettings.EventNotificationChannel, GuildSettings.DefaultRole, GuildSettings.MuteRole, diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index cdaede6..5a99505 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -56,6 +56,11 @@ public static class GuildSettings ///
public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel"); + /// + /// Controls what channel should welcome messages be sent to. + /// + public static readonly SnowflakeOption WelcomeMessagesChannel = new("WelcomeMessagesChannel"); + public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel"); public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); public static readonly SnowflakeOption MuteRole = new("MuteRole"); diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index a96a9ac..e9637d6 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -21,6 +21,7 @@ public enum AllOptionsEnum [UsedImplicitly] RenameHoistedUsers, [UsedImplicitly] PublicFeedbackChannel, [UsedImplicitly] PrivateFeedbackChannel, + [UsedImplicitly] WelcomeMessagesChannel, [UsedImplicitly] EventNotificationChannel, [UsedImplicitly] DefaultRole, [UsedImplicitly] MuteRole, diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index ca460cf..5ad741e 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1184,5 +1184,12 @@ namespace Octobot { return ResourceManager.GetString("TimeSpanExample", resourceCulture); } } + + internal static string SettingsWelcomeMessagesChannel + { + get { + return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); + } + } } } diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index eee93b6..012bfad 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -51,7 +51,7 @@ public class GuildMemberJoinedResponder : IResponder return Result.FromError(returnRolesResult.Error); } - if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") { return Result.FromSuccess(); @@ -76,7 +76,7 @@ public class GuildMemberJoinedResponder : IResponder .Build(); return await _channelApi.CreateMessageWithEmbedResultAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embedResult: embed, + GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, allowedMentions: Octobot.NoMentions, ct: ct); } From 398abad27796eb1ee57ec382ad5fd46331fe930f Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:23:42 +0300 Subject: [PATCH 268/329] Remove unused IDiscordRestChannelAPI in ToolsCommandGroup (#273) Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- src/Commands/ToolsCommandGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index ea91e1e..9a7c9b4 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -35,7 +35,7 @@ public class ToolsCommandGroup : CommandGroup public ToolsCommandGroup( ICommandContext context, IFeedbackService feedback, GuildDataService guildData, IDiscordRestGuildAPI guildApi, - IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi) + IDiscordRestUserAPI userApi) { _context = context; _guildData = guildData; From 1894b063aeb2fc611202a32ad068a59679099a1b Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:26:04 +0300 Subject: [PATCH 269/329] Fix auto-unban log spam (#271) Closes #255 Signed-off-by: mctaylors --- src/Services/Update/MemberUpdateService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 7674bbe..dfe8219 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -151,6 +151,13 @@ public sealed partial class MemberUpdateService : BackgroundService return Result.FromSuccess(); } + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, id, ct); + if (!existingBanResult.IsDefined()) + { + data.BannedUntil = null; + return Result.FromSuccess(); + } + var unbanResult = await _guildApi.RemoveGuildBanAsync( guildId, id, Messages.PunishmentExpired.EncodeHeader(), ct); if (unbanResult.IsSuccess) From 771750c92282d61bcd1421ee406c92bd97331706 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:27:35 +0300 Subject: [PATCH 270/329] Rename locale Sound to Loaded for clarity (#270) If you go through the locales, sooner or later you will notice `Sound*`, which is used in `GuildLoadedResponder.cs`. A new contributor (most likely) will not understand what it is used for at once, because we use `$"Loaded{i}".Localized()` instead of `Messages.Sound*` directly. Also, if you change the locale's value, for example the same "Loaded!", `Sound` will not fit anymore, because "Loaded!" is not a sound, but a phrase. Other suggestions are welcome. Signed-off-by: mctaylors --- locale/Messages.resx | 6 +++--- locale/Messages.ru.resx | 6 +++--- locale/Messages.tt-ru.resx | 6 +++--- src/Messages.Designer.cs | 12 ++++++------ src/Responders/GuildLoadedResponder.cs | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index c8ef510..c2b9abb 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -117,13 +117,13 @@ {0}, welcome to {1} - + Veemo! - + Woomy! - + Ngyes! diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index eb8e57b..814106a 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -117,13 +117,13 @@ {0}, добро пожаловать на сервер {1} - + Виимо! - + Вууми! - + Нгьес! diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index df50d8d..71357ad 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -117,13 +117,13 @@ {0}, добро пожаловать на сервер {1} - + вииимо! - + вуууми! - + нгьес! diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 5ad741e..4694254 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -66,21 +66,21 @@ namespace Octobot { } } - internal static string Sound1 { + internal static string Loaded1 { get { - return ResourceManager.GetString("Sound1", resourceCulture); + return ResourceManager.GetString("Loaded1", resourceCulture); } } - internal static string Sound2 { + internal static string Loaded2 { get { - return ResourceManager.GetString("Sound2", resourceCulture); + return ResourceManager.GetString("Loaded2", resourceCulture); } } - internal static string Sound3 { + internal static string Loaded3 { get { - return ResourceManager.GetString("Sound3", resourceCulture); + return ResourceManager.GetString("Loaded3", resourceCulture); } } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index a1e7d16..fd289fc 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -88,7 +88,7 @@ public class GuildLoadedResponder : IResponder var i = Random.Shared.Next(1, 4); var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) - .WithTitle($"Sound{i}".Localized()) + .WithTitle($"Loaded{i}".Localized()) .WithDescription(Messages.Ready) .WithCurrentTimestamp() .WithColour(ColorsList.Blue) From 2342116e87af94406cfc2d745a55739f174b7676 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Tue, 19 Mar 2024 20:51:32 +0300 Subject: [PATCH 271/329] Add GitInfo NuGet package (#268) In this PR, I added a NuGet package called GitInfo. It can replace Octobot.RepositoryUrl and display the bot version as the current branch and commit. --------- Signed-off-by: mctaylors Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- Octobot.csproj | 2 + locale/Messages.resx | 3 ++ locale/Messages.ru.resx | 3 ++ locale/Messages.tt-ru.resx | 3 ++ src/BuildInfo.cs | 52 +++++++++++++++++++ src/Commands/AboutCommandGroup.cs | 5 +- .../Events/ErrorLoggingPostExecutionEvent.cs | 2 +- src/Messages.Designer.cs | 9 +++- src/Octobot.cs | 3 -- src/Responders/GuildLoadedResponder.cs | 2 +- 10 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 src/BuildInfo.cs diff --git a/Octobot.csproj b/Octobot.csproj index ab76400..bdfb46a 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -17,10 +17,12 @@ en A general-purpose Discord bot for moderation written in C# docs/octobot.ico + false + diff --git a/locale/Messages.resx b/locale/Messages.resx index c2b9abb..c2be4cd 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -657,6 +657,9 @@ Example of a valid input: `1h30m` + + Version: {0} + Welcome messages channel diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 814106a..d38509c 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -657,6 +657,9 @@ Пример правильного ввода: `1ч30м` + + Версия: {0} + Канал для приветствий diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 71357ad..dfb1ee6 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -657,6 +657,9 @@ правильно пишут так: `1h30m` + + {0} + канал куда говорить здравствуйте diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs new file mode 100644 index 0000000..50f86a2 --- /dev/null +++ b/src/BuildInfo.cs @@ -0,0 +1,52 @@ +namespace Octobot; + +public static class BuildInfo +{ + public static string RepositoryUrl + { + get + { + return ThisAssembly.Git.RepositoryUrl; + } + } + + public static string IssuesUrl + { + get + { + return $"{RepositoryUrl}/issues"; + } + } + + private static string Commit + { + get + { + return ThisAssembly.Git.Commit; + } + } + + private static string Branch + { + get + { + return ThisAssembly.Git.Branch; + } + } + + private static bool IsDirty + { + get + { + return ThisAssembly.Git.IsDirty; + } + } + + public static string Version + { + get + { + return IsDirty ? $"{Branch}-{Commit}-dirty" : $"{Branch}-{Commit}"; + } + } +} diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index e978ec9..05b1855 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -101,20 +101,21 @@ public class AboutCommandGroup : CommandGroup .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) .WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png") + .WithFooter(string.Format(Messages.Version, BuildInfo.Version)) .Build(); var repositoryButton = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonOpenRepository, new PartialEmoji(Name: "🌐"), - URL: Octobot.RepositoryUrl + URL: BuildInfo.RepositoryUrl ); var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: Octobot.IssuesUrl + URL: BuildInfo.IssuesUrl ); return await _feedback.SendContextualEmbedResultAsync(embed, diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 87cfc84..5d7830b 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -72,7 +72,7 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent ButtonComponentStyle.Link, Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: Octobot.IssuesUrl + URL: BuildInfo.IssuesUrl ); return await _feedback.SendContextualEmbedResultAsync(embed, diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 4694254..707c814 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1185,8 +1185,13 @@ namespace Octobot { } } - internal static string SettingsWelcomeMessagesChannel - { + internal static string Version { + get { + return ResourceManager.GetString("Version", resourceCulture); + } + } + + internal static string SettingsWelcomeMessagesChannel { get { return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); } diff --git a/src/Octobot.cs b/src/Octobot.cs index 1ebf7c3..e0d9b07 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -22,9 +22,6 @@ namespace Octobot; public sealed class Octobot { - public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; - public const string IssuesUrl = $"{RepositoryUrl}/issues"; - public static readonly AllowedMentions NoMentions = new( Array.Empty(), Array.Empty(), Array.Empty()); diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index fd289fc..c493910 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -117,7 +117,7 @@ public class GuildLoadedResponder : IResponder ButtonComponentStyle.Link, Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: Octobot.IssuesUrl + URL: BuildInfo.IssuesUrl ); return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, From cd75780582d82cb53c252054ddf287f9aa721f4d Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:18:40 +0300 Subject: [PATCH 272/329] Add "Member left" message (#234) Closes #231 --------- Signed-off-by: mctaylors Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- locale/Messages.resx | 6 ++ locale/Messages.ru.resx | 6 ++ locale/Messages.tt-ru.resx | 6 ++ src/Commands/BanCommandGroup.cs | 8 ++- src/Commands/KickCommandGroup.cs | 6 +- src/Commands/SettingsCommandGroup.cs | 1 + src/Data/GuildSettings.cs | 16 ++++- src/Data/Options/AllOptionsEnum.cs | 1 + src/Messages.Designer.cs | 24 ++++--- src/Responders/GuildMemberLeftResponder.cs | 73 ++++++++++++++++++++++ 10 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 src/Responders/GuildMemberLeftResponder.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index c2be4cd..dfaff85 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -585,6 +585,12 @@ Report an issue + + See you soon, {0}! + + + Leave message + Time specified incorrectly! diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index d38509c..6169a13 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -585,6 +585,12 @@ Сообщить о проблеме + + До скорой встречи, {0}! + + + Сообщение о выходе + Неправильно указано время! diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index dfb1ee6..3118e74 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -585,6 +585,12 @@ зарепортить баг + + ну, мы потеряли {0} + + + до свидания (типо настройка) + ты там правильно напиши таймспан diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index c350729..30cff14 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -181,17 +181,19 @@ public class BanCommandGroup : CommandGroup await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); } + var memberData = data.GetOrCreateMemberData(target.ID); + memberData.BannedUntil + = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; + var banResult = await _guildApi.CreateGuildBanAsync( guild.ID, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), ct: ct); if (!banResult.IsSuccess) { + memberData.BannedUntil = null; return Result.FromError(banResult.Error); } - var memberData = data.GetOrCreateMemberData(target.ID); - memberData.BannedUntil - = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; memberData.Roles.Clear(); var embed = new EmbedBuilder().WithSmallTitle( diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 0faa1d3..59c4045 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -143,17 +143,19 @@ public class KickCommandGroup : CommandGroup await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); } + var memberData = data.GetOrCreateMemberData(target.ID); + memberData.Kicked = true; + var kickResult = await _guildApi.RemoveGuildMemberAsync( guild.ID, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(), ct); if (!kickResult.IsSuccess) { + memberData.Kicked = false; return Result.FromError(kickResult.Error); } - var memberData = data.GetOrCreateMemberData(target.ID); memberData.Roles.Clear(); - memberData.Kicked = true; var title = string.Format(Messages.UserKicked, target.GetTag()); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 86f031f..97ebc32 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -39,6 +39,7 @@ public class SettingsCommandGroup : CommandGroup [ GuildSettings.Language, GuildSettings.WelcomeMessage, + GuildSettings.LeaveMessage, GuildSettings.ReceiveStartupMessages, GuildSettings.RemoveRolesOnMute, GuildSettings.ReturnRolesOnRejoin, diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index 5a99505..518465b 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -13,17 +13,29 @@ public static class GuildSettings public static readonly LanguageOption Language = new("Language", "en"); /// - /// Controls what message should be sent in when a new member joins the server. + /// Controls what message should be sent in when a new member joins the guild. /// /// /// /// No message will be sent if set to "off", "disable" or "disabled". - /// will be sent if set to "default" or "reset" + /// will be sent if set to "default" or "reset". /// /// /// public static readonly Option WelcomeMessage = new("WelcomeMessage", "default"); + /// + /// Controls what message should be sent in when a member leaves the guild. + /// + /// + /// + /// No message will be sent if set to "off", "disable" or "disabled". + /// will be sent if set to "default" or "reset". + /// + /// + /// + public static readonly Option LeaveMessage = new("LeaveMessage", "default"); + /// /// Controls whether or not the message should be sent /// in on startup. diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index e9637d6..6932822 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -14,6 +14,7 @@ public enum AllOptionsEnum { [UsedImplicitly] Language, [UsedImplicitly] WelcomeMessage, + [UsedImplicitly] LeaveMessage, [UsedImplicitly] ReceiveStartupMessages, [UsedImplicitly] RemoveRolesOnMute, [UsedImplicitly] ReturnRolesOnRejoin, diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 707c814..0fa4820 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1037,18 +1037,26 @@ namespace Octobot { } } - internal static string InvalidTimeSpan - { - get - { + internal static string DefaultLeaveMessage { + get { + return ResourceManager.GetString("DefaultLeaveMessage", resourceCulture); + } + } + + internal static string SettingsLeaveMessage { + get { + return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture); + } + } + + internal static string InvalidTimeSpan { + get { return ResourceManager.GetString("InvalidTimeSpan", resourceCulture); } } - internal static string UserInfoKicked - { - get - { + internal static string UserInfoKicked { + get { return ResourceManager.GetString("UserInfoKicked", resourceCulture); } } diff --git a/src/Responders/GuildMemberLeftResponder.cs b/src/Responders/GuildMemberLeftResponder.cs new file mode 100644 index 0000000..5c6db36 --- /dev/null +++ b/src/Responders/GuildMemberLeftResponder.cs @@ -0,0 +1,73 @@ +using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Octobot.Responders; + +/// +/// Handles sending a guild's if one is set. +/// +/// +[UsedImplicitly] +public class GuildMemberLeftResponder : IResponder +{ + private readonly IDiscordRestChannelAPI _channelApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + + public GuildMemberLeftResponder( + IDiscordRestChannelAPI channelApi, GuildDataService guildData, IDiscordRestGuildAPI guildApi) + { + _channelApi = channelApi; + _guildData = guildData; + _guildApi = guildApi; + } + + public async Task RespondAsync(IGuildMemberRemove gatewayEvent, CancellationToken ct = default) + { + var user = gatewayEvent.User; + var data = await _guildData.GetData(gatewayEvent.GuildID, ct); + var cfg = data.Settings; + + var memberData = data.GetOrCreateMemberData(user.ID); + if (memberData.BannedUntil is not null || memberData.Kicked) + { + return Result.FromSuccess(); + } + + if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() + || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") + { + return Result.FromSuccess(); + } + + Messages.Culture = GuildSettings.Language.Get(cfg); + var leaveMessage = GuildSettings.LeaveMessage.Get(cfg) is "default" or "reset" + ? Messages.DefaultLeaveMessage + : GuildSettings.LeaveMessage.Get(cfg); + + var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(leaveMessage, user.GetTag(), guild.Name), user) + .WithGuildFooter(guild) + .WithTimestamp(DateTimeOffset.UtcNow) + .WithColour(ColorsList.Black) + .Build(); + + return await _channelApi.CreateMessageWithEmbedResultAsync( + GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, + allowedMentions: Octobot.NoMentions, ct: ct); + } +} + From ecb92a318cf1fbe8a655f46e6ac53eaaa5bfa474 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:22:54 +0300 Subject: [PATCH 273/329] Fix /ping not showing correct locale (#276) yeah... --------- Signed-off-by: mctaylors --- locale/Messages.resx | 6 +++--- locale/Messages.ru.resx | 6 +++--- locale/Messages.tt-ru.resx | 6 +++--- src/Commands/PingCommandGroup.cs | 2 +- src/Messages.Designer.cs | 12 ++++++------ src/Responders/GuildLoadedResponder.cs | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index dfaff85..de99b4e 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -117,13 +117,13 @@ {0}, welcome to {1} - + Veemo! - + Woomy! - + Ngyes! diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 6169a13..991ffcc 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -117,13 +117,13 @@ {0}, добро пожаловать на сервер {1} - + Виимо! - + Вууми! - + Нгьес! diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 3118e74..a5ba94d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -117,13 +117,13 @@ {0}, добро пожаловать на сервер {1} - + вииимо! - + вуууми! - + нгьес! diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 31fa6dc..f74e5e2 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -91,7 +91,7 @@ public class PingCommandGroup : CommandGroup } var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) - .WithTitle($"Sound{Random.Shared.Next(1, 4)}".Localized()) + .WithTitle($"Generic{Random.Shared.Next(1, 4)}".Localized()) .WithDescription($"{latency:F0}{Messages.Milliseconds}") .WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red) .WithCurrentTimestamp() diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 0fa4820..7b44c44 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -66,21 +66,21 @@ namespace Octobot { } } - internal static string Loaded1 { + internal static string Generic1 { get { - return ResourceManager.GetString("Loaded1", resourceCulture); + return ResourceManager.GetString("Generic1", resourceCulture); } } - internal static string Loaded2 { + internal static string Generic2 { get { - return ResourceManager.GetString("Loaded2", resourceCulture); + return ResourceManager.GetString("Generic2", resourceCulture); } } - internal static string Loaded3 { + internal static string Generic3 { get { - return ResourceManager.GetString("Loaded3", resourceCulture); + return ResourceManager.GetString("Generic3", resourceCulture); } } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index c493910..a0636f6 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -88,7 +88,7 @@ public class GuildLoadedResponder : IResponder var i = Random.Shared.Next(1, 4); var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) - .WithTitle($"Loaded{i}".Localized()) + .WithTitle($"Generic{i}".Localized()) .WithDescription(Messages.Ready) .WithCurrentTimestamp() .WithColour(ColorsList.Blue) From ca49231c86f833abc13a9d15de44e68fad2aad63 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:59:25 +0300 Subject: [PATCH 274/329] Disable issue report button if dirty version detected (#275) In this PR, I'm disabling the Report Issue button if a "dirty" version is detected. This is done just in case so that "smart" developers don't accidentally report a bug that they themselves created. --------- Signed-off-by: mctaylors --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ locale/Messages.tt-ru.resx | 3 +++ src/BuildInfo.cs | 2 +- src/Commands/AboutCommandGroup.cs | 7 +++++-- src/Commands/Events/ErrorLoggingPostExecutionEvent.cs | 7 +++++-- src/Messages.Designer.cs | 6 ++++++ src/Responders/GuildLoadedResponder.cs | 7 +++++-- 8 files changed, 31 insertions(+), 7 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index de99b4e..f7500b6 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -669,4 +669,7 @@ Welcome messages channel + + Can't report an issue in the development version + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 991ffcc..e28b405 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -669,4 +669,7 @@ Канал для приветствий + + Нельзя сообщить о проблеме в версии под разработкой + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index a5ba94d..58e1178 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -669,4 +669,7 @@ канал куда говорить здравствуйте + + вот иди сам и почини что сломал + diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs index 50f86a2..c704328 100644 --- a/src/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -34,7 +34,7 @@ public static class BuildInfo } } - private static bool IsDirty + public static bool IsDirty { get { diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 05b1855..ebfe0f2 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -113,9 +113,12 @@ public class AboutCommandGroup : CommandGroup var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonReportIssue, + BuildInfo.IsDirty + ? Messages.ButtonDirty + : Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: BuildInfo.IssuesUrl + URL: BuildInfo.IssuesUrl, + IsDisabled: BuildInfo.IsDirty ); return await _feedback.SendContextualEmbedResultAsync(embed, diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 5d7830b..6f39690 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -70,9 +70,12 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonReportIssue, + BuildInfo.IsDirty + ? Messages.ButtonDirty + : Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: BuildInfo.IssuesUrl + URL: BuildInfo.IssuesUrl, + IsDisabled: BuildInfo.IsDirty ); return await _feedback.SendContextualEmbedResultAsync(embed, diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 7b44c44..3d452a6 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1204,5 +1204,11 @@ namespace Octobot { return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); } } + + internal static string ButtonDirty { + get { + return ResourceManager.GetString("ButtonDirty", resourceCulture); + } + } } } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index a0636f6..e60dd4f 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -115,9 +115,12 @@ public class GuildLoadedResponder : IResponder var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonReportIssue, + BuildInfo.IsDirty + ? Messages.ButtonDirty + : Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: BuildInfo.IssuesUrl + URL: BuildInfo.IssuesUrl, + IsDisabled: BuildInfo.IsDirty ); return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, From 0a930dcab16c990c5a25cd9df95430ff5c347a7f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Mar 2024 21:08:59 +0500 Subject: [PATCH 275/329] Allow using expression-bodied properties (#280) This change significantly reduces the code space used by properties while maintaining clarity on types. Since only properties are allowed to use expression bodies, it is clear to developers that what they are looking at is a property or not Signed-off-by: Octol1ttle --- .editorconfig | 2 +- src/BuildInfo.cs | 48 ++++++------------------------------------------ 2 files changed, 7 insertions(+), 43 deletions(-) diff --git a/.editorconfig b/.editorconfig index ff9c068..adbec5a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,7 +42,7 @@ csharp_space_between_square_brackets = false csharp_style_expression_bodied_accessors = false:warning csharp_style_expression_bodied_constructors = false:warning csharp_style_expression_bodied_methods = false:warning -csharp_style_expression_bodied_properties = false:warning +csharp_style_expression_bodied_properties = true:warning csharp_style_namespace_declarations = file_scoped:warning csharp_style_prefer_utf8_string_literals = true:warning csharp_style_var_elsewhere = true:warning diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs index c704328..369feff 100644 --- a/src/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -2,51 +2,15 @@ public static class BuildInfo { - public static string RepositoryUrl - { - get - { - return ThisAssembly.Git.RepositoryUrl; - } - } + public static string RepositoryUrl => ThisAssembly.Git.RepositoryUrl; - public static string IssuesUrl - { - get - { - return $"{RepositoryUrl}/issues"; - } - } + public static string IssuesUrl => $"{RepositoryUrl}/issues"; - private static string Commit - { - get - { - return ThisAssembly.Git.Commit; - } - } + private static string Commit => ThisAssembly.Git.Commit; - private static string Branch - { - get - { - return ThisAssembly.Git.Branch; - } - } + private static string Branch => ThisAssembly.Git.Branch; - public static bool IsDirty - { - get - { - return ThisAssembly.Git.IsDirty; - } - } + public static bool IsDirty => ThisAssembly.Git.IsDirty; - public static string Version - { - get - { - return IsDirty ? $"{Branch}-{Commit}-dirty" : $"{Branch}-{Commit}"; - } - } + public static string Version => IsDirty ? $"{Branch}-{Commit}-dirty" : $"{Branch}-{Commit}"; } From 5cc04e9cc32e23c2f2ba4d936c0f965545913d4e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Mar 2024 22:34:22 +0500 Subject: [PATCH 276/329] Change position of "Jump to ..." action for better consistency (#279) This will better align with how a normal moderator would respond to the log: Before: "see log" -> "jump to message without knowing what changed" -> "read message diff" After: "see log" -> "read message diff" -> "jump to message for context" In addition, the change improves consistency with how reminders are shown. --------- Signed-off-by: Octol1ttle --- src/Responders/MessageDeletedResponder.cs | 9 +++++---- src/Responders/MessageEditedResponder.cs | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index bfedb22..52be8c9 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -83,10 +83,11 @@ public class MessageDeletedResponder : IResponder Messages.Culture = GuildSettings.Language.Get(cfg); - var builder = new StringBuilder().AppendLine( - string.Format(Messages.DescriptionActionJumpToChannel, - Mention.Channel(gatewayEvent.ChannelID))) - .AppendLine(message.Content.InBlockCode()); + var builder = new StringBuilder() + .AppendLine(message.Content.InBlockCode()) + .AppendLine( + string.Format(Messages.DescriptionActionJumpToChannel, Mention.Channel(gatewayEvent.ChannelID)) + ); var embed = new EmbedBuilder() .WithSmallTitle( diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index c7426d2..dd3f51d 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -101,10 +101,11 @@ public class MessageEditedResponder : IResponder Messages.Culture = GuildSettings.Language.Get(cfg); - var builder = new StringBuilder().AppendLine( - string.Format(Messages.DescriptionActionJumpToMessage, - $"https://discord.com/channels/{guildId}/{channelId}/{messageId}")) - .AppendLine(diff.AsMarkdown()); + var builder = new StringBuilder() + .AppendLine(diff.AsMarkdown()) + .AppendLine(string.Format(Messages.DescriptionActionJumpToMessage, + $"https://discord.com/channels/{guildId}/{channelId}/{messageId}") + ); var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) From 309d9000673723d5dc2c799a5cd41bfac8e35b2c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Mar 2024 23:08:16 +0500 Subject: [PATCH 277/329] Log result failures with stack traces (#282) This feature will improve the debugging experience for developers by providing the information about *where exactly* a result has failed --------- Signed-off-by: Octol1ttle --- src/Attributes/StaticCallersOnlyAttribute.cs | 8 +++ src/Commands/AboutCommandGroup.cs | 2 +- src/Commands/BanCommandGroup.cs | 16 ++--- src/Commands/ClearCommandGroup.cs | 8 +-- .../Events/ErrorLoggingPostExecutionEvent.cs | 7 ++- src/Commands/KickCommandGroup.cs | 10 +-- src/Commands/MuteCommandGroup.cs | 18 +++--- src/Commands/PingCommandGroup.cs | 4 +- src/Commands/RemindCommandGroup.cs | 14 ++--- src/Commands/SettingsCommandGroup.cs | 10 +-- src/Commands/ToolsCommandGroup.cs | 16 ++--- src/Extensions/ChannelApiExtensions.cs | 2 +- src/Extensions/FeedbackServiceExtensions.cs | 2 +- src/Extensions/ResultExtensions.cs | 61 +++++++++++++++++++ src/Octobot.cs | 5 ++ src/Responders/GuildLoadedResponder.cs | 6 +- src/Responders/GuildMemberJoinedResponder.cs | 4 +- src/Responders/GuildMemberLeftResponder.cs | 3 +- src/Responders/MessageDeletedResponder.cs | 6 +- src/Services/Update/MemberUpdateService.cs | 4 +- .../Update/ScheduledEventUpdateService.cs | 10 +-- 21 files changed, 145 insertions(+), 71 deletions(-) create mode 100644 src/Attributes/StaticCallersOnlyAttribute.cs create mode 100644 src/Extensions/ResultExtensions.cs diff --git a/src/Attributes/StaticCallersOnlyAttribute.cs b/src/Attributes/StaticCallersOnlyAttribute.cs new file mode 100644 index 0000000..e8787bf --- /dev/null +++ b/src/Attributes/StaticCallersOnlyAttribute.cs @@ -0,0 +1,8 @@ +namespace Octobot.Attributes; + +/// +/// Any property marked with should only be accessed by static methods. +/// Such properties may be used to provide dependencies where it is not possible to acquire them through normal means. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class StaticCallersOnlyAttribute : Attribute; diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index ebfe0f2..b37b2f0 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -73,7 +73,7 @@ public class AboutCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 30cff14..ef5e9a4 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -88,19 +88,19 @@ public class BanCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var data = await _guildData.GetData(guild.ID, CancellationToken); @@ -144,7 +144,7 @@ public class BanCommandGroup : CommandGroup = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } if (interactionResult.Entity is not null) @@ -191,7 +191,7 @@ public class BanCommandGroup : CommandGroup if (!banResult.IsSuccess) { memberData.BannedUntil = null; - return Result.FromError(banResult.Error); + return ResultExtensions.FromError(banResult); } memberData.Roles.Clear(); @@ -242,14 +242,14 @@ public class BanCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } // Needed to get the tag and avatar var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -276,7 +276,7 @@ public class BanCommandGroup : CommandGroup ct); if (!unbanResult.IsSuccess) { - return Result.FromError(unbanResult.Error); + return ResultExtensions.FromError(unbanResult); } data.GetOrCreateMemberData(target.ID).BannedUntil = null; diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 395810f..70afede 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -75,20 +75,20 @@ public class ClearCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var messagesResult = await _channelApi.GetChannelMessagesAsync( channelId, limit: amount + 1, ct: CancellationToken); if (!messagesResult.IsDefined(out var messages)) { - return Result.FromError(messagesResult); + return ResultExtensions.FromError(messagesResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -133,7 +133,7 @@ public class ClearCommandGroup : CommandGroup channelId, idList, executor.GetTag().EncodeHeader(), ct); if (!deleteResult.IsSuccess) { - return Result.FromError(deleteResult.Error); + return ResultExtensions.FromError(deleteResult); } _utility.LogAction( diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 6f39690..7e97e5c 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -59,7 +59,7 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent var botResult = await _userApi.GetCurrentUserAsync(ct); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var embed = new EmbedBuilder().WithSmallTitle(Messages.CommandExecutionFailed, bot) @@ -78,10 +78,11 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent IsDisabled: BuildInfo.IsDirty ); - return await _feedback.SendContextualEmbedResultAsync(embed, + return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { new ActionRowComponent(new[] { issuesButton }) - }), ct); + }), ct) + ); } } diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 59c4045..5149ad4 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -80,19 +80,19 @@ public class KickCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -118,7 +118,7 @@ public class KickCommandGroup : CommandGroup = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } if (interactionResult.Entity is not null) @@ -152,7 +152,7 @@ public class KickCommandGroup : CommandGroup if (!kickResult.IsSuccess) { memberData.Kicked = false; - return Result.FromError(kickResult.Error); + return ResultExtensions.FromError(kickResult); } memberData.Roles.Clear(); diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index c2542e8..5ac6a04 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -85,13 +85,13 @@ public class MuteCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -130,7 +130,7 @@ public class MuteCommandGroup : CommandGroup guildId, executor.ID, target.ID, "Mute", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } if (interactionResult.Entity is not null) @@ -146,7 +146,7 @@ public class MuteCommandGroup : CommandGroup var muteMethodResult = await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct); if (!muteMethodResult.IsSuccess) { - return muteMethodResult; + return ResultExtensions.FromError(muteMethodResult); } var title = string.Format(Messages.UserMuted, target.GetTag()); @@ -257,14 +257,14 @@ public class MuteCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } // Needed to get the tag and avatar var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -291,7 +291,7 @@ public class MuteCommandGroup : CommandGroup guildId, executor.ID, target.ID, "Unmute", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } if (interactionResult.Entity is not null) @@ -324,14 +324,14 @@ public class MuteCommandGroup : CommandGroup await RemoveMuteRoleAsync(executor, target, reason, guildId, memberData, CancellationToken); if (!removeMuteRoleAsync.IsSuccess) { - return Result.FromError(removeMuteRoleAsync.Error); + return ResultExtensions.FromError(removeMuteRoleAsync); } var removeTimeoutResult = await RemoveTimeoutAsync(executor, target, reason, guildId, communicationDisabledUntil, CancellationToken); if (!removeTimeoutResult.IsSuccess) { - return Result.FromError(removeTimeoutResult.Error); + return ResultExtensions.FromError(removeTimeoutResult); } var title = string.Format(Messages.UserUnmuted, target.GetTag()); diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index f74e5e2..d64c6dd 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -64,7 +64,7 @@ public class PingCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); @@ -84,7 +84,7 @@ public class PingCommandGroup : CommandGroup channelId, limit: 1, ct: ct); if (!lastMessageResult.IsDefined(out var lastMessage)) { - return Result.FromError(lastMessageResult); + return ResultExtensions.FromError(lastMessageResult); } latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds; diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index f9c006e..aa1ef7e 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -63,13 +63,13 @@ public class RemindCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -134,13 +134,13 @@ public class RemindCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -226,13 +226,13 @@ public class RemindCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -343,7 +343,7 @@ public class RemindCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var data = await _guildData.GetData(guildId, CancellationToken); diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 97ebc32..30150ee 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -98,7 +98,7 @@ public class SettingsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); @@ -181,13 +181,13 @@ public class SettingsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -254,7 +254,7 @@ public class SettingsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); @@ -274,7 +274,7 @@ public class SettingsCommandGroup : CommandGroup var resetResult = option.Reset(cfg); if (!resetResult.IsSuccess) { - return Result.FromError(resetResult.Error); + return ResultExtensions.FromError(resetResult); } var embed = new EmbedBuilder().WithSmallTitle( diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 9a7c9b4..cc3c2cf 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -81,13 +81,13 @@ public class ToolsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -274,13 +274,13 @@ public class ToolsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -353,7 +353,7 @@ public class ToolsCommandGroup : CommandGroup var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -439,13 +439,13 @@ public class ToolsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -524,7 +524,7 @@ public class ToolsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var data = await _guildData.GetData(guildId, CancellationToken); diff --git a/src/Extensions/ChannelApiExtensions.cs b/src/Extensions/ChannelApiExtensions.cs index 12ccf35..99eff67 100644 --- a/src/Extensions/ChannelApiExtensions.cs +++ b/src/Extensions/ChannelApiExtensions.cs @@ -20,7 +20,7 @@ public static class ChannelApiExtensions { if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed)) { - return Result.FromError(embedResult.Value); + return ResultExtensions.FromError(embedResult.Value); } return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed }, diff --git a/src/Extensions/FeedbackServiceExtensions.cs b/src/Extensions/FeedbackServiceExtensions.cs index 40e0d53..e6ef376 100644 --- a/src/Extensions/FeedbackServiceExtensions.cs +++ b/src/Extensions/FeedbackServiceExtensions.cs @@ -13,7 +13,7 @@ public static class FeedbackServiceExtensions { if (!embedResult.IsDefined(out var embed)) { - return Result.FromError(embedResult); + return ResultExtensions.FromError(embedResult); } return (Result)await feedback.SendContextualEmbedAsync(embed, options, ct); diff --git a/src/Extensions/ResultExtensions.cs b/src/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..f456dac --- /dev/null +++ b/src/Extensions/ResultExtensions.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Remora.Results; + +namespace Octobot.Extensions; + +public static class ResultExtensions +{ + public static Result FromError(Result result) + { + LogResultStackTrace(result); + + return result; + } + + public static Result FromError(Result result) + { + var casted = (Result)result; + LogResultStackTrace(casted); + + return casted; + } + + [Conditional("DEBUG")] + private static void LogResultStackTrace(Result result) + { + if (Octobot.StaticLogger is null || result.IsSuccess) + { + return; + } + + Octobot.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}", + result.Error.GetType().FullName, result.Error.Message, Environment.NewLine, ConstructStackTrace()); + + var inner = result.Inner; + while (inner is { IsSuccess: false }) + { + Octobot.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}", + inner.Error.GetType().FullName, inner.Error.Message); + + inner = inner.Inner; + } + } + + private static string ConstructStackTrace() + { + var stackArray = new StackTrace(3, true).ToString().Split(Environment.NewLine).ToList(); + for (var i = stackArray.Count - 1; i >= 0; i--) + { + var frame = stackArray[i]; + var trimmed = frame.TrimStart(); + if (trimmed.StartsWith("at System.Threading", StringComparison.Ordinal) + || trimmed.StartsWith("at System.Runtime.CompilerServices", StringComparison.Ordinal)) + { + stackArray.RemoveAt(i); + } + } + + return string.Join(Environment.NewLine, stackArray); + } +} diff --git a/src/Octobot.cs b/src/Octobot.cs index e0d9b07..a4871f4 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Attributes; using Octobot.Commands.Events; using Octobot.Services; using Octobot.Services.Update; @@ -25,10 +26,14 @@ public sealed class Octobot public static readonly AllowedMentions NoMentions = new( Array.Empty(), Array.Empty(), Array.Empty()); + [StaticCallersOnly] + public static ILogger? StaticLogger { get; private set; } + public static async Task Main(string[] args) { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); var services = host.Services; + StaticLogger = services.GetRequiredService>(); var slashService = services.GetRequiredService(); // Providing a guild ID to this call will result in command duplicates! diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index e60dd4f..80e8735 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -57,7 +57,7 @@ public class GuildLoadedResponder : IResponder var botResult = await _userApi.GetCurrentUserAsync(ct); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } if (data.DataLoadFailed) @@ -68,7 +68,7 @@ public class GuildLoadedResponder : IResponder var ownerResult = await _userApi.GetUserAsync(guild.OwnerID, ct); if (!ownerResult.IsDefined(out var owner)) { - return Result.FromError(ownerResult); + return ResultExtensions.FromError(ownerResult); } _logger.LogInformation("Loaded guild \"{Name}\" ({ID}) owned by {Owner} ({OwnerID}) with {MemberCount} members", @@ -103,7 +103,7 @@ public class GuildLoadedResponder : IResponder var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); if (!channelResult.IsDefined(out var channel)) { - return Result.FromError(channelResult); + return ResultExtensions.FromError(channelResult); } var errorEmbed = new EmbedBuilder() diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 012bfad..05ceb2f 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -48,7 +48,7 @@ public class GuildMemberJoinedResponder : IResponder var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct); if (!returnRolesResult.IsSuccess) { - return Result.FromError(returnRolesResult.Error); + return ResultExtensions.FromError(returnRolesResult); } if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() @@ -65,7 +65,7 @@ public class GuildMemberJoinedResponder : IResponder var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var embed = new EmbedBuilder() diff --git a/src/Responders/GuildMemberLeftResponder.cs b/src/Responders/GuildMemberLeftResponder.cs index 5c6db36..b029c96 100644 --- a/src/Responders/GuildMemberLeftResponder.cs +++ b/src/Responders/GuildMemberLeftResponder.cs @@ -55,7 +55,7 @@ public class GuildMemberLeftResponder : IResponder var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var embed = new EmbedBuilder() @@ -70,4 +70,3 @@ public class GuildMemberLeftResponder : IResponder allowedMentions: Octobot.NoMentions, ct: ct); } } - diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 52be8c9..7d788a7 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -51,7 +51,7 @@ public class MessageDeletedResponder : IResponder var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); if (!messageResult.IsDefined(out var message)) { - return Result.FromError(messageResult); + return ResultExtensions.FromError(messageResult); } if (string.IsNullOrWhiteSpace(message.Content)) @@ -63,7 +63,7 @@ public class MessageDeletedResponder : IResponder guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); if (!auditLogResult.IsDefined(out var auditLogPage)) { - return Result.FromError(auditLogResult); + return ResultExtensions.FromError(auditLogResult); } var auditLog = auditLogPage.AuditLogEntries.Single(); @@ -78,7 +78,7 @@ public class MessageDeletedResponder : IResponder if (!deleterResult.IsDefined(out var deleter)) { - return Result.FromError(deleterResult); + return ResultExtensions.FromError(deleterResult); } Messages.Culture = GuildSettings.Language.Get(cfg); diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index dfe8219..a21cdd4 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -97,7 +97,7 @@ public sealed partial class MemberUpdateService : BackgroundService = await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } var canInteract = interactionResult.Entity is null; @@ -247,7 +247,7 @@ public sealed partial class MemberUpdateService : BackgroundService reminder.ChannelId.ToSnowflake(), Mention.User(user), embedResult: embed, ct: ct); if (!messageResult.IsSuccess) { - return messageResult; + return ResultExtensions.FromError(messageResult); } data.Reminders.Remove(reminder); diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index ac5c109..bce9996 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -53,7 +53,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); if (!eventsResult.IsDefined(out var events)) { - return Result.FromError(eventsResult); + return ResultExtensions.FromError(eventsResult); } SyncScheduledEvents(data, events); @@ -204,7 +204,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (!embedDescriptionResult.IsDefined(out var embedDescription)) { - return Result.FromError(embedDescriptionResult); + return ResultExtensions.FromError(embedDescriptionResult); } var embed = new EmbedBuilder() @@ -298,12 +298,12 @@ public sealed class ScheduledEventUpdateService : BackgroundService scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) { - return Result.FromError(contentResult); + return ResultExtensions.FromError(contentResult); } if (!embedDescriptionResult.IsDefined(out var embedDescription)) { - return Result.FromError(embedDescriptionResult); + return ResultExtensions.FromError(embedDescriptionResult); } var startedEmbed = new EmbedBuilder() @@ -416,7 +416,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) { - return Result.FromError(contentResult); + return ResultExtensions.FromError(contentResult); } var earlyResult = new EmbedBuilder() From a80debf1b17b3d03f27e548be85bf77261ad0f5e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 21 Mar 2024 20:55:34 +0500 Subject: [PATCH 278/329] Use Result.Success property instead of Result.FromSuccess() (#283) Signed-off-by: Octol1ttle --- .../Events/ErrorLoggingPostExecutionEvent.cs | 4 ++-- .../Events/LoggingPreparationErrorEvent.cs | 2 +- src/Commands/MuteCommandGroup.cs | 16 ++++++++++------ src/Data/Options/BoolOption.cs | 2 +- src/Data/Options/Option.cs | 14 +++++++------- src/Data/Options/SnowflakeOption.cs | 2 +- src/Data/Options/TimeSpanOption.cs | 2 +- src/Extensions/CollectionExtensions.cs | 2 +- src/Extensions/GuildScheduledEventExtensions.cs | 2 +- src/Responders/GuildLoadedResponder.cs | 6 +++--- src/Responders/GuildMemberJoinedResponder.cs | 4 ++-- src/Responders/GuildMemberLeftResponder.cs | 4 ++-- src/Responders/GuildUnloadedResponder.cs | 2 +- src/Responders/MessageDeletedResponder.cs | 6 +++--- src/Responders/MessageEditedResponder.cs | 14 +++++++------- src/Responders/MessageReceivedResponder.cs | 2 +- src/Services/Update/MemberUpdateService.cs | 17 +++++++++-------- .../Update/ScheduledEventUpdateService.cs | 12 ++++++------ 18 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 7e97e5c..5fa2ea8 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -20,8 +20,8 @@ namespace Octobot.Commands.Events; [UsedImplicitly] public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { - private readonly ILogger _logger; private readonly IFeedbackService _feedback; + private readonly ILogger _logger; private readonly IDiscordRestUserAPI _userApi; public ErrorLoggingPostExecutionEvent(ILogger logger, IFeedbackService feedback, @@ -53,7 +53,7 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent if (result.IsSuccess) { - return Result.FromSuccess(); + return Result.Success; } var botResult = await _userApi.GetCurrentUserAsync(ct); diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs index be48e74..87b4090 100644 --- a/src/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs @@ -33,6 +33,6 @@ public class LoggingPreparationErrorEvent : IPreparationErrorEvent { _logger.LogResult(preparationResult, "Error in slash command preparation."); - return Task.FromResult(Result.FromSuccess()); + return Task.FromResult(Result.Success); } } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 5ac6a04..8e79830 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -118,7 +118,8 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); } - return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); + return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, + CancellationToken); } private async Task MuteUserAsync( @@ -143,14 +144,16 @@ public class MuteCommandGroup : CommandGroup var until = DateTimeOffset.UtcNow.Add(duration); // >:) - var muteMethodResult = await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct); + var muteMethodResult = + await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct); if (!muteMethodResult.IsSuccess) { return ResultExtensions.FromError(muteMethodResult); } var title = string.Format(Messages.UserMuted, target.GetTag()); - var description = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)) + var description = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)) .AppendBulletPoint(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); @@ -348,11 +351,12 @@ public class MuteCommandGroup : CommandGroup } private async Task RemoveMuteRoleAsync( - IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData, CancellationToken ct = default) + IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData, + CancellationToken ct = default) { if (memberData.MutedUntil is null) { - return Result.FromSuccess(); + return Result.Success; } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( @@ -372,7 +376,7 @@ public class MuteCommandGroup : CommandGroup { if (communicationDisabledUntil is null) { - return Result.FromSuccess(); + return Result.Success; } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs index 130687e..6876164 100644 --- a/src/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -20,7 +20,7 @@ public sealed class BoolOption : Option } settings[Name] = value; - return Result.FromSuccess(); + return Result.Success; } private static bool TryParseBool(string from, out bool value) diff --git a/src/Data/Options/Option.cs b/src/Data/Options/Option.cs index 0ba8ce1..5d703a8 100644 --- a/src/Data/Options/Option.cs +++ b/src/Data/Options/Option.cs @@ -35,7 +35,13 @@ public class Option : IOption public virtual Result Set(JsonNode settings, string from) { settings[Name] = from; - return Result.FromSuccess(); + return Result.Success; + } + + public Result Reset(JsonNode settings) + { + settings[Name] = null; + return Result.Success; } /// @@ -48,10 +54,4 @@ public class Option : IOption var property = settings[Name]; return property != null ? property.GetValue() : DefaultValue; } - - public Result Reset(JsonNode settings) - { - settings[Name] = null; - return Result.FromSuccess(); - } } diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs index 66ada96..7118da8 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -32,7 +32,7 @@ public sealed partial class SnowflakeOption : Option } settings[Name] = parsed; - return Result.FromSuccess(); + return Result.Success; } [GeneratedRegex("[^0-9]")] diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index c81a02d..d237b6e 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -22,6 +22,6 @@ public sealed class TimeSpanOption : Option } settings[Name] = span.ToString(); - return Result.FromSuccess(); + return Result.Success; } } diff --git a/src/Extensions/CollectionExtensions.cs b/src/Extensions/CollectionExtensions.cs index 9c873f2..2369532 100644 --- a/src/Extensions/CollectionExtensions.cs +++ b/src/Extensions/CollectionExtensions.cs @@ -32,7 +32,7 @@ public static class CollectionExtensions { return list.Count switch { - 0 => Result.FromSuccess(), + 0 => Result.Success, 1 => list[0], _ => new AggregateError(list.Cast().ToArray()) }; diff --git a/src/Extensions/GuildScheduledEventExtensions.cs b/src/Extensions/GuildScheduledEventExtensions.cs index e3217e3..f1b6985 100644 --- a/src/Extensions/GuildScheduledEventExtensions.cs +++ b/src/Extensions/GuildScheduledEventExtensions.cs @@ -22,7 +22,7 @@ public static class GuildScheduledEventExtensions } return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime) - ? Result.FromSuccess() + ? Result.Success : new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); } } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 80e8735..cd40134 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -42,7 +42,7 @@ public class GuildLoadedResponder : IResponder { if (!gatewayEvent.Guild.IsT0) // Guild is not IAvailableGuild { - return Result.FromSuccess(); + return Result.Success; } var guild = gatewayEvent.Guild.AsT0; @@ -76,12 +76,12 @@ public class GuildLoadedResponder : IResponder if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) { - return Result.FromSuccess(); + return Result.Success; } if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { - return Result.FromSuccess(); + return Result.Success; } Messages.Culture = GuildSettings.Language.Get(cfg); diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 05ceb2f..61ef5cc 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -54,7 +54,7 @@ public class GuildMemberJoinedResponder : IResponder if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") { - return Result.FromSuccess(); + return Result.Success; } Messages.Culture = GuildSettings.Language.Get(cfg); @@ -85,7 +85,7 @@ public class GuildMemberJoinedResponder : IResponder { if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { - return Result.FromSuccess(); + return Result.Success; } var assignRoles = new List(); diff --git a/src/Responders/GuildMemberLeftResponder.cs b/src/Responders/GuildMemberLeftResponder.cs index b029c96..90cc64c 100644 --- a/src/Responders/GuildMemberLeftResponder.cs +++ b/src/Responders/GuildMemberLeftResponder.cs @@ -38,13 +38,13 @@ public class GuildMemberLeftResponder : IResponder var memberData = data.GetOrCreateMemberData(user.ID); if (memberData.BannedUntil is not null || memberData.Kicked) { - return Result.FromSuccess(); + return Result.Success; } if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") { - return Result.FromSuccess(); + return Result.Success; } Messages.Culture = GuildSettings.Language.Get(cfg); diff --git a/src/Responders/GuildUnloadedResponder.cs b/src/Responders/GuildUnloadedResponder.cs index 47bde75..b49d136 100644 --- a/src/Responders/GuildUnloadedResponder.cs +++ b/src/Responders/GuildUnloadedResponder.cs @@ -33,6 +33,6 @@ public class GuildUnloadedResponder : IResponder _logger.LogInformation("Unloaded guild {GuildId}", guildId); } - return Task.FromResult(Result.FromSuccess()); + return Task.FromResult(Result.Success); } } diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 7d788a7..5a69273 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -39,13 +39,13 @@ public class MessageDeletedResponder : IResponder { if (!gatewayEvent.GuildID.IsDefined(out var guildId)) { - return Result.FromSuccess(); + return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { - return Result.FromSuccess(); + return Result.Success; } var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); @@ -56,7 +56,7 @@ public class MessageDeletedResponder : IResponder if (string.IsNullOrWhiteSpace(message.Content)) { - return Result.FromSuccess(); + return Result.Success; } var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index dd3f51d..2f30732 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -48,28 +48,28 @@ public class MessageEditedResponder : IResponder if (!gatewayEvent.GuildID.IsDefined(out var guildId)) { - return Result.FromSuccess(); + return Result.Success; } if (gatewayEvent.Author.IsDefined(out var author) && author.IsBot.OrDefault(false)) { - return Result.FromSuccess(); + return Result.Success; } if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) { - return Result.FromSuccess(); // The message wasn't actually edited + return Result.Success; // The message wasn't actually edited } if (!gatewayEvent.Content.IsDefined(out var newContent)) { - return Result.FromSuccess(); + return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { - return Result.FromSuccess(); + return Result.Success; } var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); @@ -78,12 +78,12 @@ public class MessageEditedResponder : IResponder if (!messageResult.IsDefined(out var message)) { _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); - return Result.FromSuccess(); + return Result.Success; } if (message.Content == newContent) { - return Result.FromSuccess(); + return Result.Success; } // Custom event responders are called earlier than responders responsible for message caching diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs index 6ab7199..4c26d8d 100644 --- a/src/Responders/MessageReceivedResponder.cs +++ b/src/Responders/MessageReceivedResponder.cs @@ -34,6 +34,6 @@ public class MessageCreateResponder : IResponder "лан" => "https://i.ibb.co/VYH2QLc/lan.jpg", _ => default(Optional) }); - return Task.FromResult(Result.FromSuccess()); + return Task.FromResult(Result.Success); } } diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index a21cdd4..45d0476 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -121,7 +121,7 @@ public sealed partial class MemberUpdateService : BackgroundService if (!canInteract) { - return Result.FromSuccess(); + return Result.Success; } var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct); @@ -148,14 +148,14 @@ public sealed partial class MemberUpdateService : BackgroundService { if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil) { - return Result.FromSuccess(); + return Result.Success; } var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, id, ct); if (!existingBanResult.IsDefined()) { data.BannedUntil = null; - return Result.FromSuccess(); + return Result.Success; } var unbanResult = await _guildApi.RemoveGuildBanAsync( @@ -173,7 +173,7 @@ public sealed partial class MemberUpdateService : BackgroundService { if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil) { - return Result.FromSuccess(); + return Result.Success; } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( @@ -209,7 +209,7 @@ public sealed partial class MemberUpdateService : BackgroundService if (!usernameChanged) { - return Result.FromSuccess(); + return Result.Success; } var newNickname = string.Concat(characterList.ToArray()); @@ -230,12 +230,13 @@ public sealed partial class MemberUpdateService : BackgroundService { if (DateTimeOffset.UtcNow < reminder.At) { - return Result.FromSuccess(); + return Result.Success; } var builder = new StringBuilder() .AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) - .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); + .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, + $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.Reminder, user.GetTag()), user) @@ -251,6 +252,6 @@ public sealed partial class MemberUpdateService : BackgroundService } data.Reminders.Remove(reminder); - return Result.FromSuccess(); + return Result.Success; } } diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index bce9996..8168fc1 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -147,7 +147,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService || eventData.EarlyNotificationSent || DateTimeOffset.UtcNow < scheduledEvent.ScheduledStartTime - offset) { - return Result.FromSuccess(); + return Result.Success; } var sendResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct); @@ -182,7 +182,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService { if (GuildSettings.EventNotificationChannel.Get(settings).Empty()) { - return Result.FromSuccess(); + return Result.Success; } if (!scheduledEvent.Creator.IsDefined(out var creator)) @@ -283,7 +283,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { - return Result.FromSuccess(); + return Result.Success; } var embedDescriptionResult = scheduledEvent.EntityType switch @@ -324,7 +324,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { data.ScheduledEvents.Remove(eventData.Id); - return Result.FromSuccess(); + return Result.Success; } var completedEmbed = new EmbedBuilder() @@ -356,7 +356,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { data.ScheduledEvents.Remove(eventData.Id); - return Result.FromSuccess(); + return Result.Success; } var embed = new EmbedBuilder() @@ -409,7 +409,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { - return Result.FromSuccess(); + return Result.Success; } var contentResult = await _utility.GetEventNotificationMentions( From e0232f600810e0dd1c347bb77856292f5273f3ca Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 21 Mar 2024 21:31:17 +0500 Subject: [PATCH 279/329] Merge some sequential 'if' statements (#284) i thought there would be a lot of statements that could be merged, but these are only ones I could find, apparently Signed-off-by: Octol1ttle --- src/Responders/GuildLoadedResponder.cs | 8 ++------ src/Responders/MessageEditedResponder.cs | 22 +++++----------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index cd40134..b03fd3f 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -74,12 +74,8 @@ public class GuildLoadedResponder : IResponder _logger.LogInformation("Loaded guild \"{Name}\" ({ID}) owned by {Owner} ({OwnerID}) with {MemberCount} members", guild.Name, guild.ID, owner.GetTag(), owner.ID, guild.MemberCount); - if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) - { - return Result.Success; - } - - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() + || !GuildSettings.ReceiveStartupMessages.Get(cfg)) { return Result.Success; } diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index 2f30732..1143652 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -46,28 +46,16 @@ public class MessageEditedResponder : IResponder return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); } - if (!gatewayEvent.GuildID.IsDefined(out var guildId)) - { - return Result.Success; - } - - if (gatewayEvent.Author.IsDefined(out var author) && author.IsBot.OrDefault(false)) - { - return Result.Success; - } - - if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) - { - return Result.Success; // The message wasn't actually edited - } - - if (!gatewayEvent.Content.IsDefined(out var newContent)) + if (!gatewayEvent.GuildID.IsDefined(out var guildId) + || !gatewayEvent.Author.IsDefined(out var author) + || !gatewayEvent.EditedTimestamp.IsDefined(out var timestamp) + || !gatewayEvent.Content.IsDefined(out var newContent)) { return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + if (author.IsBot.OrDefault(false) || GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { return Result.Success; } From ac8621a2ec8e54c96fa04020756914c11132caa7 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sat, 23 Mar 2024 21:45:39 +0300 Subject: [PATCH 280/329] Pre-Wiki Update (#285) This PR has been opened to finally update Octobot's Wiki. Current changes summary: - correct minor spelling issues in some command descriptions - /about: add Octobot's Wiki button --------- Signed-off-by: mctaylors --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ locale/Messages.tt-ru.resx | 3 +++ src/BuildInfo.cs | 2 ++ src/Commands/AboutCommandGroup.cs | 9 ++++++++- src/Commands/SettingsCommandGroup.cs | 2 +- src/Commands/ToolsCommandGroup.cs | 4 ++-- src/Messages.Designer.cs | 6 ++++++ 8 files changed, 28 insertions(+), 4 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index f7500b6..41bb6ef 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -672,4 +672,7 @@ Can't report an issue in the development version + + Open Octobot's Wiki + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index e28b405..273338b 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -672,4 +672,7 @@ Нельзя сообщить о проблеме в версии под разработкой + + Открыть Octobot's Wiki + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 58e1178..af2c94d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -672,4 +672,7 @@ вот иди сам и почини что сломал + + вики Octobot (жмак) + diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs index 369feff..fc3a089 100644 --- a/src/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -6,6 +6,8 @@ public static class BuildInfo public static string IssuesUrl => $"{RepositoryUrl}/issues"; + public static string WikiUrl => $"{RepositoryUrl}/wiki"; + private static string Commit => ThisAssembly.Git.Commit; private static string Branch => ThisAssembly.Git.Branch; diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index b37b2f0..027e7f8 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -111,6 +111,13 @@ public class AboutCommandGroup : CommandGroup URL: BuildInfo.RepositoryUrl ); + var wikiButton = new ButtonComponent( + ButtonComponentStyle.Link, + Messages.ButtonOpenWiki, + new PartialEmoji(Name: "📖"), + URL: BuildInfo.WikiUrl + ); + var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, BuildInfo.IsDirty @@ -124,7 +131,7 @@ public class AboutCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent(new[] { repositoryButton, issuesButton }) + new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton }) }), ct); } } diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 30150ee..f756e93 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -241,7 +241,7 @@ public class SettingsCommandGroup : CommandGroup [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.ManageGuild)] - [Description("Reset settings for this server")] + [Description("Reset settings for this guild")] [UsedImplicitly] public async Task ExecuteResetSettingsAsync( [Description("Setting to reset")] AllOptionsEnum? setting = null) diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index cc3c2cf..d4f3f75 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -262,7 +262,7 @@ public class ToolsCommandGroup : CommandGroup /// [Command("guildinfo")] [DiscordDefaultDMPermission(false)] - [Description("Shows info current guild")] + [Description("Shows info about current guild")] [UsedImplicitly] public async Task ExecuteGuildInfoAsync() { @@ -514,7 +514,7 @@ public class ToolsCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteEightBallAsync( // let the user think he's actually asking the ball a question - string question) + [Description("Question to ask")] string question) { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) { diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 3d452a6..2910bae 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1210,5 +1210,11 @@ namespace Octobot { return ResourceManager.GetString("ButtonDirty", resourceCulture); } } + + internal static string ButtonOpenWiki { + get { + return ResourceManager.GetString("ButtonOpenWiki", resourceCulture); + } + } } } From a9509deb1cd3dc9b02127d8d27520010c0c97d2d Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 24 Mar 2024 13:11:00 +0300 Subject: [PATCH 281/329] Don't use BannedUntil in MemberData. (#286) Signed-off-by: mctaylors --- src/Data/MemberData.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 8e23e54..1b3d0d9 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -5,10 +5,9 @@ namespace Octobot.Data; /// public sealed class MemberData { - public MemberData(ulong id, DateTimeOffset? bannedUntil = null, List? reminders = null) + public MemberData(ulong id, List? reminders = null) { Id = id; - BannedUntil = bannedUntil; if (reminders is not null) { Reminders = reminders; From 844615e8bfac768e1b7bdf3165eb0db045ed5d8c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 17:39:26 +0500 Subject: [PATCH 282/329] fix: do not use RepositoryUrl from GitInfo (#287) GitInfo's `RepositoryUrl` string depends on origin URL, which is unvalidated user input that isn't even guaranteed to exist. This can cause issues that are almost impossible to debug Closes #281 --- src/BuildInfo.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs index fc3a089..2eb6059 100644 --- a/src/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -2,15 +2,15 @@ public static class BuildInfo { - public static string RepositoryUrl => ThisAssembly.Git.RepositoryUrl; + public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; - public static string IssuesUrl => $"{RepositoryUrl}/issues"; + public const string IssuesUrl = $"{RepositoryUrl}/issues"; - public static string WikiUrl => $"{RepositoryUrl}/wiki"; + public const string WikiUrl = $"{RepositoryUrl}/wiki"; - private static string Commit => ThisAssembly.Git.Commit; + private const string Commit = ThisAssembly.Git.Commit; - private static string Branch => ThisAssembly.Git.Branch; + private const string Branch = ThisAssembly.Git.Branch; public static bool IsDirty => ThisAssembly.Git.IsDirty; From c2f7aadaeacdd1f0e88b6bc441f7783cac00ab07 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 20:38:51 +0500 Subject: [PATCH 283/329] Do not use ResultError#IsUserOrEnvironmentError (#289) In LoggerExtensions#LogResult we skip logging the result if the error is "user or environment error". What matches that criteria is defined by Remora's implementation. However, none of errors defined by the implementation should *ever* happen or be ignored: * CommandNotFoundError: The client shouldn't send us non-existing commands. This *can* happen because the client's command list can get out of sync with the server's, but this happens rarely. * AmbiguousCommandInvocationError: We don't have commands that would trigger this error * RequiredParameterValueMissingError: The client shouldn't send us commands without required paremeters * ParameterParsingError: See #220 * ConditionNotSatisfiedError: The client shouldn't send us commands that don't satisfy our conditions Closes #220 --- src/Extensions/LoggerExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Extensions/LoggerExtensions.cs b/src/Extensions/LoggerExtensions.cs index 9df90b8..fca3702 100644 --- a/src/Extensions/LoggerExtensions.cs +++ b/src/Extensions/LoggerExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using Remora.Discord.Commands.Extensions; using Remora.Results; namespace Octobot.Extensions; @@ -19,7 +18,7 @@ public static class LoggerExtensions /// The message to use if this result has failed. public static void LogResult(this ILogger logger, IResult result, string? message = "") { - if (result.IsSuccess || result.Error.IsUserOrEnvironmentError()) + if (result.IsSuccess) { return; } From 5e4d0a528c5a604511a02a6f9a06d2d77378b0f4 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 20:48:32 +0500 Subject: [PATCH 284/329] Split message clear log when cleared messages are too long (#288) This change makes Octobot split the message clear log into multiple messages when the combined length of cleared messages exceeds the maximum length for an embed description. Closes #180 --- src/Commands/ClearCommandGroup.cs | 34 +++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 70afede..84b69db 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -102,7 +102,9 @@ public class ClearCommandGroup : CommandGroup CancellationToken ct = default) { var idList = new List(messages.Count); - var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); + + var logEntries = new List { new() }; + var currentLogEntry = 0; for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...') { var message = messages[i]; @@ -112,8 +114,17 @@ public class ClearCommandGroup : CommandGroup } idList.Add(message.ID); - builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author))); - builder.Append(message.Content.InBlockCode()); + + var entry = logEntries[currentLogEntry]; + var str = $"{string.Format(Messages.MessageFrom, Mention.User(message.Author))}\n{message.Content.InBlockCode()}"; + if (entry.Builder.Length + str.Length > EmbedConstants.MaxDescriptionLength) + { + logEntries.Add(entry = new ClearedMessageEntry()); + currentLogEntry++; + } + + entry.Builder.Append(str); + entry.DeletedCount++; } if (idList.Count == 0) @@ -127,7 +138,6 @@ public class ClearCommandGroup : CommandGroup var title = author is not null ? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag()) : string.Format(Messages.MessagesCleared, idList.Count.ToString()); - var description = builder.ToString(); var deleteResult = await _channelApi.BulkDeleteMessagesAsync( channelId, idList, executor.GetTag().EncodeHeader(), ct); @@ -136,12 +146,24 @@ public class ClearCommandGroup : CommandGroup return ResultExtensions.FromError(deleteResult); } - _utility.LogAction( - data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct); + foreach (var log in logEntries) + { + _utility.LogAction( + data.Settings, channelId, executor, author is not null + ? string.Format(Messages.MessagesClearedFiltered, log.DeletedCount.ToString(), author.GetTag()) + : string.Format(Messages.MessagesCleared, log.DeletedCount.ToString()), + log.Builder.ToString(), bot, ColorsList.Red, false, ct); + } var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithColour(ColorsList.Green).Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + + private sealed class ClearedMessageEntry + { + public StringBuilder Builder { get; } = new(); + public int DeletedCount { get; set; } + } } From 171cfaea1ac2789a189fc8a7da0fede6d3e727cc Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 23:29:10 +0500 Subject: [PATCH 285/329] Add 'ModeratorRole' guild setting (#290) Octobot has various moderation commands such as /ban, /mute, /kick. These commands add multiple features to Discord's built-in functions (such as durations and logging). Some admins may want to force their users to use Octobot's commands instead of Discord UI functions. However, due to the current design, they can't take away the permissions as that remove access to the respective command. This PR adds the `ModeratorRole` option which allows anyone who has `ManageMessages` permission and the role to perform any moderator action. If the role is not set, the Discord permissions are checked instead. If the user doesn't have the role, but has the permission, they can still run the command. --------- Signed-off-by: Octol1ttle --- locale/Messages.resx | 10 +- locale/Messages.ru.resx | 10 +- locale/Messages.tt-ru.resx | 10 +- src/Commands/BanCommandGroup.cs | 32 ++-- src/Commands/KickCommandGroup.cs | 22 +-- src/Commands/MuteCommandGroup.cs | 21 +-- src/Commands/SettingsCommandGroup.cs | 1 + src/Data/GuildSettings.cs | 1 + src/Data/Options/AllOptionsEnum.cs | 1 + src/Octobot.cs | 3 +- src/Services/AccessControlService.cs | 176 +++++++++++++++++++++ src/Services/Update/MemberUpdateService.cs | 10 +- src/Services/Utility.cs | 118 +------------- 13 files changed, 252 insertions(+), 163 deletions(-) create mode 100644 src/Services/AccessControlService.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index 41bb6ef..47e7d4f 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -231,8 +231,11 @@ You cannot kick members from this guild! - - You cannot moderate members in this guild! + + You cannot mute members in this guild! + + + You cannot unmute members in this guild! You cannot manage this guild! @@ -675,4 +678,7 @@ Open Octobot's Wiki + + Moderator role + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 273338b..2eef257 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -228,8 +228,11 @@ Ты не можешь выгонять участников с этого сервера! - - Ты не можешь модерировать участников этого сервера! + + Ты не можешь глушить участников этого сервера! + + + Ты не можешь разглушать участников этого сервера! Ты не можешь настраивать этот сервер! @@ -675,4 +678,7 @@ Открыть Octobot's Wiki + + Роль модератора + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index af2c94d..4e92a44 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -231,8 +231,11 @@ кик шизиков нельзя - - тебе нельзя управлять шизоидами + + тебе нельзя мутить шизоидов + + + тебе нельзя раззамучивать шизоидов тебе нельзя редактировать дурку @@ -675,4 +678,7 @@ вики Octobot (жмак) + + звание админа + diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index ef5e9a4..02a377a 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -28,6 +28,7 @@ namespace Octobot.Commands; [UsedImplicitly] public class BanCommandGroup : CommandGroup { + private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; @@ -36,16 +37,16 @@ public class BanCommandGroup : CommandGroup private readonly IDiscordRestUserAPI _userApi; private readonly Utility _utility; - public BanCommandGroup( - ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, - IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - Utility utility) + public BanCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context, + IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData, + IDiscordRestUserAPI userApi, Utility utility) { - _context = context; + _access = access; _channelApi = channelApi; - _guildData = guildData; + _context = context; _feedback = feedback; _guildApi = guildApi; + _guildData = guildData; _userApi = userApi; _utility = utility; } @@ -65,10 +66,10 @@ public class BanCommandGroup : CommandGroup /// /// [Command("ban", "бан")] - [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.BanMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Ban user")] [UsedImplicitly] @@ -128,7 +129,8 @@ public class BanCommandGroup : CommandGroup } private async Task BanUserAsync( - IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, + IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, + Snowflake channelId, IUser bot, CancellationToken ct = default) { var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct); @@ -141,7 +143,7 @@ public class BanCommandGroup : CommandGroup } var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct); + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); @@ -155,7 +157,8 @@ public class BanCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); } - var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); + var builder = + new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); if (duration is not null) { builder.AppendBulletPoint( @@ -221,10 +224,10 @@ public class BanCommandGroup : CommandGroup /// /// [Command("unban")] - [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.BanMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Unban user")] [UsedImplicitly] @@ -286,7 +289,8 @@ public class BanCommandGroup : CommandGroup .WithColour(ColorsList.Green).Build(); var title = string.Format(Messages.UserUnbanned, target.GetTag()); - var description = new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason)); + var description = + new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason)); _utility.LogAction( data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 5149ad4..87b915a 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -24,6 +24,7 @@ namespace Octobot.Commands; [UsedImplicitly] public class KickCommandGroup : CommandGroup { + private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; @@ -32,16 +33,16 @@ public class KickCommandGroup : CommandGroup private readonly IDiscordRestUserAPI _userApi; private readonly Utility _utility; - public KickCommandGroup( - ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, - IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - Utility utility) + public KickCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context, + IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData, + IDiscordRestUserAPI userApi, Utility utility) { - _context = context; + _access = access; _channelApi = channelApi; - _guildData = guildData; + _context = context; _feedback = feedback; _guildApi = guildApi; + _guildData = guildData; _userApi = userApi; _utility = utility; } @@ -59,10 +60,10 @@ public class KickCommandGroup : CommandGroup /// was kicked and vice-versa. /// [Command("kick", "кик")] - [DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.KickMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] [Description("Kick member")] [UsedImplicitly] @@ -115,7 +116,7 @@ public class KickCommandGroup : CommandGroup CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct); + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); @@ -134,7 +135,8 @@ public class KickCommandGroup : CommandGroup { var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereKicked) - .WithDescription(MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason))) + .WithDescription( + MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason))) .WithActionFooter(executor) .WithCurrentTimestamp() .WithColour(ColorsList.Red) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 8e79830..ce0a296 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -28,6 +28,7 @@ namespace Octobot.Commands; [UsedImplicitly] public class MuteCommandGroup : CommandGroup { + private readonly AccessControlService _access; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; private readonly IDiscordRestGuildAPI _guildApi; @@ -35,14 +36,14 @@ public class MuteCommandGroup : CommandGroup private readonly IDiscordRestUserAPI _userApi; private readonly Utility _utility; - public MuteCommandGroup( - ICommandContext context, GuildDataService guildData, IFeedbackService feedback, - IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, Utility utility) + public MuteCommandGroup(AccessControlService access, ICommandContext context, IFeedbackService feedback, + IDiscordRestGuildAPI guildApi, GuildDataService guildData, IDiscordRestUserAPI userApi, Utility utility) { + _access = access; _context = context; - _guildData = guildData; _feedback = feedback; _guildApi = guildApi; + _guildData = guildData; _userApi = userApi; _utility = utility; } @@ -62,10 +63,10 @@ public class MuteCommandGroup : CommandGroup /// /// [Command("mute", "мут")] - [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.ModerateMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Mute member")] [UsedImplicitly] @@ -127,7 +128,7 @@ public class MuteCommandGroup : CommandGroup Snowflake channelId, IUser bot, CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync( + = await _access.CheckInteractionsAsync( guildId, executor.ID, target.ID, "Mute", ct); if (!interactionResult.IsSuccess) { @@ -239,10 +240,10 @@ public class MuteCommandGroup : CommandGroup /// /// [Command("unmute", "размут")] - [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.ModerateMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Unmute member")] [UsedImplicitly] @@ -290,7 +291,7 @@ public class MuteCommandGroup : CommandGroup IUser bot, CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync( + = await _access.CheckInteractionsAsync( guildId, executor.ID, target.ID, "Unmute", ct); if (!interactionResult.IsSuccess) { diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index f756e93..a39e9c7 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -51,6 +51,7 @@ public class SettingsCommandGroup : CommandGroup GuildSettings.EventNotificationChannel, GuildSettings.DefaultRole, GuildSettings.MuteRole, + GuildSettings.ModeratorRole, GuildSettings.EventNotificationRole, GuildSettings.EventEarlyNotificationOffset ]; diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index 518465b..a1d8d74 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -76,6 +76,7 @@ public static class GuildSettings public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel"); public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); public static readonly SnowflakeOption MuteRole = new("MuteRole"); + public static readonly SnowflakeOption ModeratorRole = new("ModeratorRole"); public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole"); /// diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index 6932822..d9e0c13 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -26,6 +26,7 @@ public enum AllOptionsEnum [UsedImplicitly] EventNotificationChannel, [UsedImplicitly] DefaultRole, [UsedImplicitly] MuteRole, + [UsedImplicitly] ModeratorRole, [UsedImplicitly] EventNotificationRole, [UsedImplicitly] EventEarlyNotificationOffset } diff --git a/src/Octobot.cs b/src/Octobot.cs index a4871f4..065967e 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -88,8 +88,9 @@ public sealed class Octobot .AddPreparationErrorEvent() .AddPostExecutionEvent() // Services - .AddSingleton() + .AddSingleton() .AddSingleton() + .AddSingleton() .AddHostedService(provider => provider.GetRequiredService()) .AddHostedService() .AddHostedService() diff --git a/src/Services/AccessControlService.cs b/src/Services/AccessControlService.cs new file mode 100644 index 0000000..84667c3 --- /dev/null +++ b/src/Services/AccessControlService.cs @@ -0,0 +1,176 @@ +using Octobot.Data; +using Octobot.Extensions; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Conditions; +using Remora.Discord.Commands.Results; +using Remora.Rest.Core; +using Remora.Results; + +namespace Octobot.Services; + +public sealed class AccessControlService +{ + private readonly GuildDataService _data; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly RequireDiscordPermissionCondition _permission; + private readonly IDiscordRestUserAPI _userApi; + + public AccessControlService(GuildDataService data, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + RequireDiscordPermissionCondition permission) + { + _data = data; + _guildApi = guildApi; + _userApi = userApi; + _permission = permission; + } + + private async Task> CheckPermissionAsync(GuildData data, Snowflake memberId, IGuildMember member, + DiscordPermission permission, CancellationToken ct = default) + { + var moderatorRole = GuildSettings.ModeratorRole.Get(data.Settings); + var result = await _permission.CheckAsync(new RequireDiscordPermissionAttribute([permission]), member, ct); + + if (result.Error is not null and not PermissionDeniedError) + { + return Result.FromError(result); + } + + var hasPermission = result.IsSuccess; + return hasPermission || (!moderatorRole.Empty() && + data.GetOrCreateMemberData(memberId).Roles.Contains(moderatorRole.Value)); + } + + /// + /// Checks whether or not a member can interact with another member + /// + /// The ID of the guild in which an operation is being performed. + /// The executor of the operation. + /// The target of the operation. + /// The operation. + /// The cancellation token for this operation. + /// + /// + /// A result which has succeeded with a null string if the member can interact with the target. + /// + /// A result which has succeeded with a non-null string containing the error message if the member cannot + /// interact with the target. + /// + /// A result which has failed if an error occurred during the execution of this method. + /// + /// + public async Task> CheckInteractionsAsync( + Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default) + { + if (interacterId == targetId) + { + return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); + } + + var botResult = await _userApi.GetCurrentUserAsync(ct); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } + + var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); + if (!targetMemberResult.IsDefined(out var targetMember)) + { + return Result.FromSuccess(null); + } + + var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); + if (!currentMemberResult.IsDefined(out var currentMember)) + { + return Result.FromError(currentMemberResult); + } + + var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); + if (!rolesResult.IsDefined(out var roles)) + { + return Result.FromError(rolesResult); + } + + if (interacterId is null) + { + return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember); + } + + var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); + if (!interacterResult.IsDefined(out var interacter)) + { + return Result.FromError(interacterResult); + } + + var data = await _data.GetData(guildId, ct); + + var permissionResult = await CheckPermissionAsync(data, interacterId.Value, interacter, + action switch + { + "Ban" => DiscordPermission.BanMembers, + "Kick" => DiscordPermission.KickMembers, + "Mute" or "Unmute" => DiscordPermission.ModerateMembers, + _ => throw new Exception() + }, ct); + if (!permissionResult.IsDefined(out var hasPermission)) + { + return Result.FromError(permissionResult); + } + + return hasPermission + ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter) + : Result.FromSuccess($"UserCannot{action}Members".Localized()); + } + + private static Result CheckInteractions( + string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, + IGuildMember interacter) + { + if (!targetMember.User.IsDefined(out var targetUser)) + { + return new ArgumentNullError(nameof(targetMember.User)); + } + + if (!interacter.User.IsDefined(out var interacterUser)) + { + return new ArgumentNullError(nameof(interacter.User)); + } + + if (currentMember.User == targetMember.User) + { + return Result.FromSuccess($"UserCannot{action}Bot".Localized()); + } + + if (targetUser.ID == guild.OwnerID) + { + return Result.FromSuccess($"UserCannot{action}Owner".Localized()); + } + + var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); + var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); + + var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); + if (targetBotRoleDiff >= 0) + { + return Result.FromSuccess($"BotCannot{action}Target".Localized()); + } + + if (interacterUser.ID == guild.OwnerID) + { + return Result.FromSuccess(null); + } + + var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); + var targetInteracterRoleDiff + = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); + return targetInteracterRoleDiff < 0 + ? Result.FromSuccess(null) + : Result.FromSuccess($"UserCannot{action}Target".Localized()); + } +} diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 45d0476..e177fca 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -26,20 +26,20 @@ public sealed partial class MemberUpdateService : BackgroundService "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag" ]; + private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly ILogger _logger; - private readonly Utility _utility; - public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, - GuildDataService guildData, ILogger logger, Utility utility) + public MemberUpdateService(AccessControlService access, IDiscordRestChannelAPI channelApi, + IDiscordRestGuildAPI guildApi, GuildDataService guildData, ILogger logger) { + _access = access; _channelApi = channelApi; _guildApi = guildApi; _guildData = guildData; _logger = logger; - _utility = utility; } protected override async Task ExecuteAsync(CancellationToken ct) @@ -94,7 +94,7 @@ public sealed partial class MemberUpdateService : BackgroundService } var interactionResult - = await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct); + = await _access.CheckInteractionsAsync(guildId, null, id, "Update", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); diff --git a/src/Services/Utility.cs b/src/Services/Utility.cs index ad06315..3b9ab19 100644 --- a/src/Services/Utility.cs +++ b/src/Services/Utility.cs @@ -21,129 +21,13 @@ public sealed class Utility private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildAPI _guildApi; - private readonly IDiscordRestUserAPI _userApi; public Utility( - IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, - IDiscordRestUserAPI userApi) + IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi) { _channelApi = channelApi; _eventApi = eventApi; _guildApi = guildApi; - _userApi = userApi; - } - - /// - /// Checks whether or not a member can interact with another member - /// - /// The ID of the guild in which an operation is being performed. - /// The executor of the operation. - /// The target of the operation. - /// The operation. - /// The cancellation token for this operation. - /// - /// - /// A result which has succeeded with a null string if the member can interact with the target. - /// - /// A result which has succeeded with a non-null string containing the error message if the member cannot - /// interact with the target. - /// - /// A result which has failed if an error occurred during the execution of this method. - /// - /// - public async Task> CheckInteractionsAsync( - Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default) - { - if (interacterId == targetId) - { - return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); - } - - var botResult = await _userApi.GetCurrentUserAsync(ct); - if (!botResult.IsDefined(out var bot)) - { - return Result.FromError(botResult); - } - - var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); - if (!guildResult.IsDefined(out var guild)) - { - return Result.FromError(guildResult); - } - - var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); - if (!targetMemberResult.IsDefined(out var targetMember)) - { - return Result.FromSuccess(null); - } - - var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); - if (!currentMemberResult.IsDefined(out var currentMember)) - { - return Result.FromError(currentMemberResult); - } - - var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); - if (!rolesResult.IsDefined(out var roles)) - { - return Result.FromError(rolesResult); - } - - if (interacterId is null) - { - return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember); - } - - var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); - return interacterResult.IsDefined(out var interacter) - ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter) - : Result.FromError(interacterResult); - } - - private static Result CheckInteractions( - string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, - IGuildMember interacter) - { - if (!targetMember.User.IsDefined(out var targetUser)) - { - return new ArgumentNullError(nameof(targetMember.User)); - } - - if (!interacter.User.IsDefined(out var interacterUser)) - { - return new ArgumentNullError(nameof(interacter.User)); - } - - if (currentMember.User == targetMember.User) - { - return Result.FromSuccess($"UserCannot{action}Bot".Localized()); - } - - if (targetUser.ID == guild.OwnerID) - { - return Result.FromSuccess($"UserCannot{action}Owner".Localized()); - } - - var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); - var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); - - var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); - if (targetBotRoleDiff >= 0) - { - return Result.FromSuccess($"BotCannot{action}Target".Localized()); - } - - if (interacterUser.ID == guild.OwnerID) - { - return Result.FromSuccess(null); - } - - var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); - var targetInteracterRoleDiff - = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); - return targetInteracterRoleDiff < 0 - ? Result.FromSuccess(null) - : Result.FromSuccess($"UserCannot{action}Target".Localized()); } /// From e76fccd62228190ee528233cc6eb3ab34a2bf1ef Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:40:04 +0300 Subject: [PATCH 286/329] Rename currentMember to botMember (#291) Signed-off-by: mctaylors --- src/Services/AccessControlService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Services/AccessControlService.cs b/src/Services/AccessControlService.cs index 84667c3..aeb16e4 100644 --- a/src/Services/AccessControlService.cs +++ b/src/Services/AccessControlService.cs @@ -85,10 +85,10 @@ public sealed class AccessControlService return Result.FromSuccess(null); } - var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); - if (!currentMemberResult.IsDefined(out var currentMember)) + var botMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); + if (!botMemberResult.IsDefined(out var botMember)) { - return Result.FromError(currentMemberResult); + return Result.FromError(botMemberResult); } var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); @@ -99,7 +99,7 @@ public sealed class AccessControlService if (interacterId is null) { - return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember); + return CheckInteractions(action, guild, roles, targetMember, botMember, botMember); } var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); @@ -124,12 +124,12 @@ public sealed class AccessControlService } return hasPermission - ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter) + ? CheckInteractions(action, guild, roles, targetMember, botMember, interacter) : Result.FromSuccess($"UserCannot{action}Members".Localized()); } private static Result CheckInteractions( - string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, + string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember botMember, IGuildMember interacter) { if (!targetMember.User.IsDefined(out var targetUser)) @@ -142,7 +142,7 @@ public sealed class AccessControlService return new ArgumentNullError(nameof(interacter.User)); } - if (currentMember.User == targetMember.User) + if (botMember.User == targetMember.User) { return Result.FromSuccess($"UserCannot{action}Bot".Localized()); } @@ -153,7 +153,7 @@ public sealed class AccessControlService } var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); - var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); + var botRoles = roles.Where(r => botMember.Roles.Contains(r.ID)); var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); if (targetBotRoleDiff >= 0) From cccc4d6205259d2712567e2de91174112395cc2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:47:05 +0300 Subject: [PATCH 287/329] Bump muno92/resharper_inspectcode from 1.11.7 to 1.11.8 (#292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.11.7 to 1.11.8.
Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.11.8

What's Changed

New Contributors

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.11.7...1.11.8

Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.11.8 - 2024-03-23

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.11.7&new-version=1.11.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 8002f6f..859f8fa 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.7 + uses: muno92/resharper_inspectcode@1.11.8 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 9429dfe8d8c64d1c49ee6879fcd82e2b57f740ae Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 26 Mar 2024 15:35:31 +0500 Subject: [PATCH 288/329] Fix "No operation context has been set for this scope." crash on startup (#293) Signed-off-by: Octol1ttle --- src/Services/AccessControlService.cs | 65 +++++++++++----------------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/src/Services/AccessControlService.cs b/src/Services/AccessControlService.cs index aeb16e4..cb235f9 100644 --- a/src/Services/AccessControlService.cs +++ b/src/Services/AccessControlService.cs @@ -2,8 +2,6 @@ using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.Commands.Conditions; -using Remora.Discord.Commands.Results; using Remora.Rest.Core; using Remora.Results; @@ -13,32 +11,30 @@ public sealed class AccessControlService { private readonly GuildDataService _data; private readonly IDiscordRestGuildAPI _guildApi; - private readonly RequireDiscordPermissionCondition _permission; private readonly IDiscordRestUserAPI _userApi; - public AccessControlService(GuildDataService data, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - RequireDiscordPermissionCondition permission) + public AccessControlService(GuildDataService data, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi) { _data = data; _guildApi = guildApi; _userApi = userApi; - _permission = permission; } - private async Task> CheckPermissionAsync(GuildData data, Snowflake memberId, IGuildMember member, - DiscordPermission permission, CancellationToken ct = default) + private static bool CheckPermission(IEnumerable roles, GuildData data, Snowflake memberId, + IGuildMember member, + DiscordPermission permission) { var moderatorRole = GuildSettings.ModeratorRole.Get(data.Settings); - var result = await _permission.CheckAsync(new RequireDiscordPermissionAttribute([permission]), member, ct); - - if (result.Error is not null and not PermissionDeniedError) + if (!moderatorRole.Empty() && data.GetOrCreateMemberData(memberId).Roles.Contains(moderatorRole.Value)) { - return Result.FromError(result); + return true; } - var hasPermission = result.IsSuccess; - return hasPermission || (!moderatorRole.Empty() && - data.GetOrCreateMemberData(memberId).Roles.Contains(moderatorRole.Value)); + return roles + .Where(r => member.Roles.Contains(r.ID)) + .Any(r => + r.Permissions.HasPermission(permission) + ); } /// @@ -67,30 +63,35 @@ public sealed class AccessControlService return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); } - var botResult = await _userApi.GetCurrentUserAsync(ct); - if (!botResult.IsDefined(out var bot)) - { - return Result.FromError(botResult); - } - var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); if (!guildResult.IsDefined(out var guild)) { return Result.FromError(guildResult); } - var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); - if (!targetMemberResult.IsDefined(out var targetMember)) + if (interacterId == guild.OwnerID) { return Result.FromSuccess(null); } + var botResult = await _userApi.GetCurrentUserAsync(ct); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + var botMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); if (!botMemberResult.IsDefined(out var botMember)) { return Result.FromError(botMemberResult); } + var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); + if (!targetMemberResult.IsDefined(out var targetMember)) + { + return Result.FromSuccess(null); + } + var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); if (!rolesResult.IsDefined(out var roles)) { @@ -110,18 +111,14 @@ public sealed class AccessControlService var data = await _data.GetData(guildId, ct); - var permissionResult = await CheckPermissionAsync(data, interacterId.Value, interacter, + var hasPermission = CheckPermission(roles, data, interacterId.Value, interacter, action switch { "Ban" => DiscordPermission.BanMembers, "Kick" => DiscordPermission.KickMembers, "Mute" or "Unmute" => DiscordPermission.ModerateMembers, _ => throw new Exception() - }, ct); - if (!permissionResult.IsDefined(out var hasPermission)) - { - return Result.FromError(permissionResult); - } + }); return hasPermission ? CheckInteractions(action, guild, roles, targetMember, botMember, interacter) @@ -137,11 +134,6 @@ public sealed class AccessControlService return new ArgumentNullError(nameof(targetMember.User)); } - if (!interacter.User.IsDefined(out var interacterUser)) - { - return new ArgumentNullError(nameof(interacter.User)); - } - if (botMember.User == targetMember.User) { return Result.FromSuccess($"UserCannot{action}Bot".Localized()); @@ -161,11 +153,6 @@ public sealed class AccessControlService return Result.FromSuccess($"BotCannot{action}Target".Localized()); } - if (interacterUser.ID == guild.OwnerID) - { - return Result.FromSuccess(null); - } - var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); var targetInteracterRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); From 70fccf83352dcfdb0c22f046e8d08aae58eabb08 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:57:49 +0300 Subject: [PATCH 289/329] Use unicode codes instead of emojis (#295) This change was made to avoid using emoji in the code, which may not render correctly depending on the IDE and/or operating system. --- src/Commands/AboutCommandGroup.cs | 6 +++--- src/Commands/Events/ErrorLoggingPostExecutionEvent.cs | 2 +- src/Responders/GuildLoadedResponder.cs | 2 +- src/Services/Update/ScheduledEventUpdateService.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 027e7f8..f2fa11f 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -107,14 +107,14 @@ public class AboutCommandGroup : CommandGroup var repositoryButton = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonOpenRepository, - new PartialEmoji(Name: "🌐"), + new PartialEmoji(Name: "\ud83c\udf10"), // 'GLOBE WITH MERIDIANS' (U+1F310) URL: BuildInfo.RepositoryUrl ); var wikiButton = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonOpenWiki, - new PartialEmoji(Name: "📖"), + new PartialEmoji(Name: "\ud83d\udcd6"), // 'OPEN BOOK' (U+1F4D6) URL: BuildInfo.WikiUrl ); @@ -123,7 +123,7 @@ public class AboutCommandGroup : CommandGroup BuildInfo.IsDirty ? Messages.ButtonDirty : Messages.ButtonReportIssue, - new PartialEmoji(Name: "⚠️"), + new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0) URL: BuildInfo.IssuesUrl, IsDisabled: BuildInfo.IsDirty ); diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 5fa2ea8..551c2d0 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -73,7 +73,7 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent BuildInfo.IsDirty ? Messages.ButtonDirty : Messages.ButtonReportIssue, - new PartialEmoji(Name: "⚠️"), + new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0) URL: BuildInfo.IssuesUrl, IsDisabled: BuildInfo.IsDirty ); diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index b03fd3f..55e9673 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -114,7 +114,7 @@ public class GuildLoadedResponder : IResponder BuildInfo.IsDirty ? Messages.ButtonDirty : Messages.ButtonReportIssue, - new PartialEmoji(Name: "⚠️"), + new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0) URL: BuildInfo.IssuesUrl, IsDisabled: BuildInfo.IsDirty ); diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 8168fc1..cb87779 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -223,7 +223,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService var button = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonOpenEventInfo, - new PartialEmoji(Name: "📋"), + new PartialEmoji(Name: "\ud83d\udccb"), // 'CLIPBOARD' (U+1F4CB) URL: $"https://discord.com/events/{scheduledEvent.GuildID}/{scheduledEvent.ID}" ); From 96680d3beb4361d20f7efe1edd02c567519476a5 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:59:19 +0300 Subject: [PATCH 290/329] Make the logo in /about independent of image hosting (#296) PR's name speaks for itself. It might also be useful to update the logo more easily. --------- Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> Co-authored-by: Octol1ttle --- src/Commands/AboutCommandGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index f2fa11f..b8c6d0f 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -100,7 +100,7 @@ public class AboutCommandGroup : CommandGroup .WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png") + .WithImageUrl("https://raw.githubusercontent.com/TeamOctolings/Octobot/master/docs/octobot-banner.png") .WithFooter(string.Format(Messages.Version, BuildInfo.Version)) .Build(); From d3053d87e813bf698851d19b2d3337fe1ee1852e Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 1 Apr 2024 22:20:41 +0300 Subject: [PATCH 291/329] Remove mctaylors' version of the Russian language (#297) 295 PR/issues ~(not 300, however)~ or ~1.5 years ago, I made #2, the Russian language replacement aka mctaylors-ru. This was my first contribution to the Octobot project (formerly known as Boyfriend). This was to add some sort of unique, unusual feature to Octobot, which doesn't have any moderator bots. Everyone loved the language. But it just became difficult to maintain. I certainly don't want to get rid of it, but it leaves me no other choice. This isn't a joke or anything like that. I'm tired of maintaining it. And I'm sure the other contributors are too. This PR removes the mctaylors-ru language. --------- Signed-off-by: mctaylors Co-authored-by: Octol1ttle --- locale/Messages.tt-ru.resx | 684 ----------------------------- src/Data/Options/LanguageOption.cs | 3 +- src/Services/GuildDataService.cs | 18 +- 3 files changed, 17 insertions(+), 688 deletions(-) delete mode 100644 locale/Messages.tt-ru.resx diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx deleted file mode 100644 index 4e92a44..0000000 --- a/locale/Messages.tt-ru.resx +++ /dev/null @@ -1,684 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - я родился! - - - сообщение {0} вырезано: - - - сообщение {0} переделано: - - - {0}, добро пожаловать на сервер {1} - - - вииимо! - - - вуууми! - - - нгьес! - - - вы были забанены - - - время бана закончиловсь - - - вы были кикнуты - - - мс - - - *тут ничего нет* - - - нъет - - - язык - - - префикс - - - удалять звание при муте - - - разглашать о том что пришел новый шизоид - - - звание замученного - - - такого языка нету... - - - да - - - нъет - - - шизик не забанен - - - шизоид не замучен! - - - здравствуйте (типо настройка) - - - {0} забанен - - - получать инфу о старте бота - - - криво настроил прикол, давай по новой - - - ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим - - - я не могу замутить ботов, сделай что нибудь - - - роль для уведомлений о создании движухи - - - канал для уведомлений о движухах - - - получатели уведомлений о начале движух - - - движуха "{0}" начинается - - - движуха "{0}" отменена! - - - движуха "{0}" завершена! - - - вырезано {0} забавных сообщений - - - ты все сломал! значение прикола `{0}` и так {1} - - - нъет - - - укажи самого шизика - - - бан - - - тебе нельзя иметь власть над сообщениями шизоидов - - - кик шизиков нельзя - - - тебе нельзя мутить шизоидов - - - тебе нельзя раззамучивать шизоидов - - - тебе нельзя редактировать дурку - - - я не могу ваще никого банить чел. - - - я не могу исправлять орфографический кринж участников, сделай что нибудь. - - - я не могу ваще никого кикать чел. - - - я не могу контроллировать за всеми ними, сделай что нибудь. - - - я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь. - - - ээбля френдли фаер огонь по своим - - - бан админу нельзя - - - бан этому шизику нельзя - - - самобан нельзя - - - я не могу его забанить... - - - кик админу нельзя - - - самокик нельзя - - - ээбля френдли фаер огонь по своим - - - я не могу его кикнуть... - - - кик этому шизику нельзя - - - мут админу нельзя - - - самомут нельзя - - - ээбля френдли фаер огонь по своим - - - я не могу его замутить... - - - мут этому шизику нельзя - - - сильно - - - ты замучен. - - - ... - - - тебе нельзя раззамучивать - - - я не могу его раззамутить... - - - движуха "{0}" начнется {1}! - - - заранее пнуть в минутах до начала движухи - - - у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад) - - - дефолтное звание - - - канал для секретных уведомлений - - - канал для не секретных уведомлений - - - вернуть звания при переподключении в дурку - - - автоматом стартить движухи - - - ответственный - - - {0} создает новое событие: - - - движуха произойдет {0} в канале {1} - - - движуха будет происходить с {0} до {1} в {2} - - - открыть ивент - - - все это длилось `{0}` - - - движуха происходит в {0} - - - движуха происходит в {0} до {1} - - - этот шизоид уже лежит в бане - - - {0} раззабанен - - - {0} в муте - - - {0} в размуте - - - этого шизоида никто не мутил. - - - у нас такого шизоида нету... - - - {0} вышел с посторонней помощью - - - причина: {0} - - - до: {0} - - - этот шизоид УЖЕ замучился - - - от {0} - - - девелоперы: - - - репа Octobot (тык) - - - немного об {0} - - - скучный девелопер + дизайнер создавший Octobot's Wiki - - - ВАЖНЫЙ соучастник кодинг-стримов @Octol1ttle - - - САМЫЙ ВАЖНЫЙ чел написавший кода больше всех (99.99%) - - - напоминалка для {0} скрафченА - - - напоминалка для {0} - - - ты хотел чтоб я напомнил тебе {0} - - - приколы Octobot - - - прикол редактирован - - - прикол сдох - - - стало - - - переобувать шизоидов пытающихся поднять себя в табе - - - это страница - - - если я был бы html, я бы сказал 404 - - - ну а если быть точнее, тут всего {0} страниц(-ы) - - - следующее - - - предыдущее - - - напоминалки {0} - - - у тебя нет напоминалки на этом номере! - - - напоминалка уничтожена - - - ты еще не крафтил напоминалки - - - {0} откачен к заводским - - - откатываемся к заводским... - - - чекнуть сообщение: {0} - - - чекнуть канал: {0} - - - номер в списке: {0} - - - время отправки: {0} - - - че там в напоминалке: {0} - - - дисплейнейм - - - деанон {0} - - - замучен - - - юзер Discord со времен - - - забанен - - - приколы полученные по заслугам - - - пермабан - - - вышел из сервера - - - замучен таймаутом - - - замучен ролькой - - - участник сервера со времен - - - сервернейм - - - рольки - - - бустит сервер со времен - - - рандомное число {0}: - - - ну чувак... - - - наибольшее: {0} - - - наименьшее: {0} - - - (дефолт) - - - таймштамп для {0}: - - - офсет: {0} - - - дескрипшон гильдии - - - создался - - - админ гильдии - - - буст гильдии - - - уровень - - - кол-во бустов - - - алло а чё мне удалять-то - - - вырезано {0} забавных сообщений от {1} - - - произошёл тотальный разнос в гилддате. - - - возможно всё съедет с крыши, но знай, что я больше ничё не сохраню. - - - произошёл тотальный разнос в команде, удачи. - - - если ты это читаешь второй раз за сегодня, пиши разрабам - - - зарепортить баг - - - ну, мы потеряли {0} - - - до свидания (типо настройка) - - - ты там правильно напиши таймспан - - - кикнут - - - напоминалка подправлена - - - абсолютли - - - заявлено - - - ваще не сомневайся - - - 100% да - - - будь в этом уверен - - - я считаю что да - - - ну вполне вероятно - - - ну выглядит нормально - - - мне сказали ок - - - мгм - - - ну-ка попробуй снова - - - давай позже - - - щас пока не скажу - - - я не могу сейчас предсказать - - - ну сконцентрируйся и давай еще раз - - - даже не думай - - - мое завление это нет - - - я тут посчитал, короче нет - - - выглядит такое себе - - - чот сомневаюсь - - - правильно пишут так: `1h30m` - - - {0} - - - канал куда говорить здравствуйте - - - вот иди сам и почини что сломал - - - вики Octobot (жмак) - - - звание админа - - diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs index 464c61b..22f98df 100644 --- a/src/Data/Options/LanguageOption.cs +++ b/src/Data/Options/LanguageOption.cs @@ -11,8 +11,7 @@ public sealed class LanguageOption : Option private static readonly Dictionary CultureInfoCache = new() { { "en", new CultureInfo("en-US") }, - { "ru", new CultureInfo("ru-RU") }, - { "mctaylors-ru", new CultureInfo("tt-RU") } + { "ru", new CultureInfo("ru-RU") } }; public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { } diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs index c9458a0..e503d22 100644 --- a/src/Services/GuildDataService.cs +++ b/src/Services/GuildDataService.cs @@ -78,7 +78,7 @@ public sealed class GuildDataService : BackgroundService var settingsPath = $"{path}/Settings.json"; var scheduledEventsPath = $"{path}/ScheduledEvents.json"; - MigrateGuildData(guildId, path); + MigrateDataDirectory(guildId, path); Directory.CreateDirectory(path); @@ -106,6 +106,11 @@ public sealed class GuildDataService : BackgroundService dataLoadFailed = true; } + if (jsonSettings is not null) + { + FixJsonSettings(jsonSettings); + } + await using var eventsStream = File.OpenRead(scheduledEventsPath); Dictionary? events = null; try @@ -155,7 +160,7 @@ public sealed class GuildDataService : BackgroundService return finalData; } - private void MigrateGuildData(Snowflake guildId, string newPath) + private void MigrateDataDirectory(Snowflake guildId, string newPath) { var oldPath = $"{guildId}"; @@ -169,6 +174,15 @@ public sealed class GuildDataService : BackgroundService } } + private static void FixJsonSettings(JsonNode settings) + { + var language = settings[GuildSettings.Language.Name]?.GetValue(); + if (language is "mctaylors-ru") + { + settings[GuildSettings.Language.Name] = "ru"; + } + } + public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) { return (await GetData(guildId, ct)).Settings; From defa3c2e4acc5e30f6e533a2b3480b73e37a05b2 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:51:28 +0300 Subject: [PATCH 292/329] Listen to Maritime Memory on Wii U Discontinuation Day (#299) Due to the shutdown of Wii U online services on April 8 at 23:00 UTC (which affects Splatoon for Wii U), I'm opening a PR to memorialize Splatoon multiplayer on Wii U by replacing bot music with Maritime Memory on April 8-9. Signed-off-by: mctaylors --- src/Services/Update/SongUpdateService.cs | 31 +++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs index 53cc59b..41d5bf3 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -32,6 +32,11 @@ public sealed class SongUpdateService : BackgroundService ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5)) ]; + private static readonly (string Author, string Name, TimeSpan Duration)[] SpecialSongList = + [ + ("Squid Sisters", "Maritime Memory", new TimeSpan(0, 2, 47)) + ]; + private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)]; private readonly DiscordGatewayClient _client; @@ -54,19 +59,33 @@ public sealed class SongUpdateService : BackgroundService while (!ct.IsCancellationRequested) { - var nextSong = SongList[_nextSongIndex]; + var nextSong = NextSong(); _activityList[0] = new Activity($"{nextSong.Name} / {nextSong.Author}", ActivityType.Listening); _client.SubmitCommand( new UpdatePresence( UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList)); - _nextSongIndex++; - if (_nextSongIndex >= SongList.Length) - { - _nextSongIndex = 0; - } await Task.Delay(nextSong.Duration, ct); } } + + private (string Author, string Name, TimeSpan Duration) NextSong() + { + var today = DateTime.Today; + // Discontinuation of Online Services for Nintendo Wii U + if (today.Day is 8 or 9 && today.Month is 4) + { + return SpecialSongList[0]; // Maritime Memory / Squid Sisters + } + + var nextSong = SongList[_nextSongIndex]; + _nextSongIndex++; + if (_nextSongIndex >= SongList.Length) + { + _nextSongIndex = 0; + } + + return nextSong; + } } From 508edcbd683f5505a6cbe636aaed6b315a6ed378 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:02:31 +0300 Subject: [PATCH 293/329] Don't hardcode logo's link in /about (#301) In this PR, I made the logo link in /about use HEAD instead of hardcoded branch. --------- Signed-off-by: mctaylors Co-authored-by: Octol1ttle --- src/Commands/AboutCommandGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index b8c6d0f..02384f8 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -100,7 +100,7 @@ public class AboutCommandGroup : CommandGroup .WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl("https://raw.githubusercontent.com/TeamOctolings/Octobot/master/docs/octobot-banner.png") + .WithImageUrl("https://raw.githubusercontent.com/TeamOctolings/Octobot/HEAD/docs/octobot-banner.png") .WithFooter(string.Format(Messages.Version, BuildInfo.Version)) .Build(); From 3029089385e66e886c9a43f369889287d90cfa8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 12:41:55 +0000 Subject: [PATCH 294/329] Bump muno92/resharper_inspectcode from 1.11.8 to 1.11.10 (#302) --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 859f8fa..c92386e 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.8 + uses: muno92/resharper_inspectcode@1.11.10 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 2502beb5df94c0d62bb7e7ed4a0c5bb528f7ecda Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 13 May 2024 17:43:59 +0500 Subject: [PATCH 295/329] Dependabot: ignore patch updates (#304) This PR *should* disable creating Dependabot PRs for patch updates. These updates often don't contain significant changes and only clutter the PR feed in addition to taking the maintainers' time --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4545f2b..57eea90 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,10 @@ updates: labels: - "type: change" - "area: build/ci" + # For all packages, ignore all patch updates + ignore: + - dependency-name: "*" + update-types: [ "version-update:semver-patch" ] - package-ecosystem: "nuget" # See documentation for possible values directory: "/" # Location of package manifests @@ -30,3 +34,7 @@ updates: remora: patterns: - "Remora.Discord.*" + # For all packages, ignore all patch updates + ignore: + - dependency-name: "*" + update-types: [ "version-update:semver-patch" ] From 19fadead913edc8dea303c337e9f9fcf94d47d12 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 13 May 2024 18:10:41 +0500 Subject: [PATCH 296/329] Enable result stacktraces in release mode (#305) Originally I did not enable because "stack traces are expensive to retrieve", but let's be honest, who cares, this is a Discord bot, there's no such thing as "good performance" --- src/Extensions/ResultExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Extensions/ResultExtensions.cs b/src/Extensions/ResultExtensions.cs index f456dac..a95a1e2 100644 --- a/src/Extensions/ResultExtensions.cs +++ b/src/Extensions/ResultExtensions.cs @@ -21,7 +21,6 @@ public static class ResultExtensions return casted; } - [Conditional("DEBUG")] private static void LogResultStackTrace(Result result) { if (Octobot.StaticLogger is null || result.IsSuccess) From 793afd0e06fc346ba3161d368df7c87f2f068f38 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 16 May 2024 20:34:26 +0500 Subject: [PATCH 297/329] Apply official naming guidelines to Octobot (#306) 1. The root namespace was changed from `Octobot` to `TeamOctolings.Octobot`: > DO prefix namespace names with a company name to prevent namespaces from different companies from having the same name. 2. `Octobot.cs` was renamed to `Program.cs`: > DO NOT use the same name for a namespace and a type in that namespace. 3. `IOption`, `Option` were renamed to `IGuildOption` and `GuildOption` respectively: > DO NOT introduce generic type names such as Element, Node, Log, and Message. 4. `Utility` was moved out of the `Services` namespace. It didn't belong there anyway 5. `Program` static fields were moved to `Utility` 6. Localisation files were moved back to the project source files. Looks like this fixed `Message.Designer.cs` code generation --------- Signed-off-by: Octol1ttle --- Octobot.sln | 10 +- .../Attributes/StaticCallersOnlyAttribute.cs | 2 +- {src => TeamOctolings.Octobot}/BuildInfo.cs | 2 +- {src => TeamOctolings.Octobot}/ColorsList.cs | 2 +- .../Commands/AboutCommandGroup.cs | 8 +- .../Commands/BanCommandGroup.cs | 12 +- .../Commands/ClearCommandGroup.cs | 8 +- .../Events/ErrorLoggingPostExecutionEvent.cs | 4 +- .../Events/LoggingPreparationErrorEvent.cs | 4 +- .../Commands/KickCommandGroup.cs | 8 +- .../Commands/MuteCommandGroup.cs | 12 +- .../Commands/PingCommandGroup.cs | 8 +- .../Commands/RemindCommandGroup.cs | 10 +- .../Commands/SettingsCommandGroup.cs | 18 +- .../Commands/ToolsCommandGroup.cs | 10 +- .../Data/GuildData.cs | 2 +- .../Data/GuildSettings.cs | 10 +- .../Data/MemberData.cs | 2 +- .../Data/Options/AllOptionsEnum.cs | 4 +- .../Data/Options/BoolOption.cs | 4 +- .../Data/Options/GuildOption.cs | 8 +- .../Data/Options/IGuildOption.cs | 4 +- .../Data/Options/LanguageOption.cs | 4 +- .../Data/Options/SnowflakeOption.cs | 6 +- .../Data/Options/TimeSpanOption.cs | 6 +- .../Data/Reminder.cs | 2 +- .../Data/ScheduledEventData.cs | 2 +- .../Extensions/ChannelApiExtensions.cs | 2 +- .../Extensions/CollectionExtensions.cs | 2 +- .../Extensions/CommandContextExtensions.cs | 2 +- .../Extensions/DiffPaneModelExtensions.cs | 2 +- .../Extensions/EmbedBuilderExtensions.cs | 2 +- .../Extensions/FeedbackServiceExtensions.cs | 2 +- .../GuildScheduledEventExtensions.cs | 2 +- .../Extensions/LoggerExtensions.cs | 2 +- .../Extensions/MarkdownExtensions.cs | 2 +- .../Extensions/ResultExtensions.cs | 13 +- .../Extensions/SnowflakeExtensions.cs | 2 +- .../Extensions/StringBuilderExtensions.cs | 2 +- .../Extensions/StringExtensions.cs | 2 +- .../Extensions/UInt64Extensions.cs | 2 +- .../Extensions/UserExtensions.cs | 2 +- .../Messages.Designer.cs | 564 +++++++++--------- .../Messages.resx | 0 .../Messages.ru.resx | 0 .../Parsers/TimeSpanParser.cs | 2 +- .../Program.cs | 25 +- .../Responders/GuildLoadedResponder.cs | 8 +- .../Responders/GuildMemberJoinedResponder.cs | 10 +- .../Responders/GuildMemberLeftResponder.cs | 10 +- .../Responders/GuildUnloadedResponder.cs | 6 +- .../Responders/MessageDeletedResponder.cs | 10 +- .../Responders/MessageEditedResponder.cs | 10 +- .../Responders/MessageReceivedResponder.cs | 2 +- .../Services/AccessControlService.cs | 8 +- .../Services/GuildDataService.cs | 4 +- .../Services/Update/MemberUpdateService.cs | 6 +- .../Update/ScheduledEventUpdateService.cs | 6 +- .../Services/Update/SongUpdateService.cs | 2 +- .../TeamOctolings.Octobot.csproj | 8 +- .../Utility.cs | 15 +- 61 files changed, 447 insertions(+), 462 deletions(-) rename {src => TeamOctolings.Octobot}/Attributes/StaticCallersOnlyAttribute.cs (88%) rename {src => TeamOctolings.Octobot}/BuildInfo.cs (93%) rename {src => TeamOctolings.Octobot}/ColorsList.cs (95%) rename {src => TeamOctolings.Octobot}/Commands/AboutCommandGroup.cs (97%) rename {src => TeamOctolings.Octobot}/Commands/BanCommandGroup.cs (98%) rename {src => TeamOctolings.Octobot}/Commands/ClearCommandGroup.cs (97%) rename {src => TeamOctolings.Octobot}/Commands/Events/ErrorLoggingPostExecutionEvent.cs (97%) rename {src => TeamOctolings.Octobot}/Commands/Events/LoggingPreparationErrorEvent.cs (93%) rename {src => TeamOctolings.Octobot}/Commands/KickCommandGroup.cs (97%) rename {src => TeamOctolings.Octobot}/Commands/MuteCommandGroup.cs (98%) rename {src => TeamOctolings.Octobot}/Commands/PingCommandGroup.cs (96%) rename {src => TeamOctolings.Octobot}/Commands/RemindCommandGroup.cs (98%) rename {src => TeamOctolings.Octobot}/Commands/SettingsCommandGroup.cs (96%) rename {src => TeamOctolings.Octobot}/Commands/ToolsCommandGroup.cs (99%) rename {src => TeamOctolings.Octobot}/Data/GuildData.cs (97%) rename {src => TeamOctolings.Octobot}/Data/GuildSettings.cs (92%) rename {src => TeamOctolings.Octobot}/Data/MemberData.cs (93%) rename {src => TeamOctolings.Octobot}/Data/Options/AllOptionsEnum.cs (92%) rename {src => TeamOctolings.Octobot}/Data/Options/BoolOption.cs (91%) rename src/Data/Options/Option.cs => TeamOctolings.Octobot/Data/Options/GuildOption.cs (90%) rename src/Data/Options/IOption.cs => TeamOctolings.Octobot/Data/Options/IGuildOption.cs (73%) rename {src => TeamOctolings.Octobot}/Data/Options/LanguageOption.cs (91%) rename {src => TeamOctolings.Octobot}/Data/Options/SnowflakeOption.cs (87%) rename {src => TeamOctolings.Octobot}/Data/Options/TimeSpanOption.cs (82%) rename {src => TeamOctolings.Octobot}/Data/Reminder.cs (83%) rename {src => TeamOctolings.Octobot}/Data/ScheduledEventData.cs (97%) rename {src => TeamOctolings.Octobot}/Extensions/ChannelApiExtensions.cs (96%) rename {src => TeamOctolings.Octobot}/Extensions/CollectionExtensions.cs (96%) rename {src => TeamOctolings.Octobot}/Extensions/CommandContextExtensions.cs (92%) rename {src => TeamOctolings.Octobot}/Extensions/DiffPaneModelExtensions.cs (94%) rename {src => TeamOctolings.Octobot}/Extensions/EmbedBuilderExtensions.cs (99%) rename {src => TeamOctolings.Octobot}/Extensions/FeedbackServiceExtensions.cs (93%) rename {src => TeamOctolings.Octobot}/Extensions/GuildScheduledEventExtensions.cs (95%) rename {src => TeamOctolings.Octobot}/Extensions/LoggerExtensions.cs (96%) rename {src => TeamOctolings.Octobot}/Extensions/MarkdownExtensions.cs (88%) rename {src => TeamOctolings.Octobot}/Extensions/ResultExtensions.cs (82%) rename {src => TeamOctolings.Octobot}/Extensions/SnowflakeExtensions.cs (96%) rename {src => TeamOctolings.Octobot}/Extensions/StringBuilderExtensions.cs (98%) rename {src => TeamOctolings.Octobot}/Extensions/StringExtensions.cs (98%) rename {src => TeamOctolings.Octobot}/Extensions/UInt64Extensions.cs (82%) rename {src => TeamOctolings.Octobot}/Extensions/UserExtensions.cs (85%) rename {src => TeamOctolings.Octobot}/Messages.Designer.cs (89%) rename {locale => TeamOctolings.Octobot}/Messages.resx (100%) rename {locale => TeamOctolings.Octobot}/Messages.ru.resx (100%) rename {src => TeamOctolings.Octobot}/Parsers/TimeSpanParser.cs (98%) rename src/Octobot.cs => TeamOctolings.Octobot/Program.cs (86%) rename {src => TeamOctolings.Octobot}/Responders/GuildLoadedResponder.cs (96%) rename {src => TeamOctolings.Octobot}/Responders/GuildMemberJoinedResponder.cs (94%) rename {src => TeamOctolings.Octobot}/Responders/GuildMemberLeftResponder.cs (91%) rename {src => TeamOctolings.Octobot}/Responders/GuildUnloadedResponder.cs (90%) rename {src => TeamOctolings.Octobot}/Responders/MessageDeletedResponder.cs (94%) rename {src => TeamOctolings.Octobot}/Responders/MessageEditedResponder.cs (95%) rename {src => TeamOctolings.Octobot}/Responders/MessageReceivedResponder.cs (96%) rename {src => TeamOctolings.Octobot}/Services/AccessControlService.cs (97%) rename {src => TeamOctolings.Octobot}/Services/GuildDataService.cs (98%) rename {src => TeamOctolings.Octobot}/Services/Update/MemberUpdateService.cs (98%) rename {src => TeamOctolings.Octobot}/Services/Update/ScheduledEventUpdateService.cs (99%) rename {src => TeamOctolings.Octobot}/Services/Update/SongUpdateService.cs (98%) rename Octobot.csproj => TeamOctolings.Octobot/TeamOctolings.Octobot.csproj (90%) rename {src/Services => TeamOctolings.Octobot}/Utility.cs (92%) diff --git a/Octobot.sln b/Octobot.sln index 9dd2b89..b82f7a9 100644 --- a/Octobot.sln +++ b/Octobot.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Octobot", "Octobot.csproj", "{9CA7A44F-167C-46D4-923D-88CE71044144}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamOctolings.Octobot", "TeamOctolings.Octobot\TeamOctolings.Octobot.csproj", "{A1679BA2-3A36-4D98-80C0-EEE771398FBD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -8,9 +8,9 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9CA7A44F-167C-46D4-923D-88CE71044144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9CA7A44F-167C-46D4-923D-88CE71044144}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9CA7A44F-167C-46D4-923D-88CE71044144}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9CA7A44F-167C-46D4-923D-88CE71044144}.Release|Any CPU.Build.0 = Release|Any CPU + {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Attributes/StaticCallersOnlyAttribute.cs b/TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs similarity index 88% rename from src/Attributes/StaticCallersOnlyAttribute.cs rename to TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs index e8787bf..0256f62 100644 --- a/src/Attributes/StaticCallersOnlyAttribute.cs +++ b/TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs @@ -1,4 +1,4 @@ -namespace Octobot.Attributes; +namespace TeamOctolings.Octobot.Attributes; /// /// Any property marked with should only be accessed by static methods. diff --git a/src/BuildInfo.cs b/TeamOctolings.Octobot/BuildInfo.cs similarity index 93% rename from src/BuildInfo.cs rename to TeamOctolings.Octobot/BuildInfo.cs index 2eb6059..4b9a09f 100644 --- a/src/BuildInfo.cs +++ b/TeamOctolings.Octobot/BuildInfo.cs @@ -1,4 +1,4 @@ -namespace Octobot; +namespace TeamOctolings.Octobot; public static class BuildInfo { diff --git a/src/ColorsList.cs b/TeamOctolings.Octobot/ColorsList.cs similarity index 95% rename from src/ColorsList.cs rename to TeamOctolings.Octobot/ColorsList.cs index cd40313..3b66c0a 100644 --- a/src/ColorsList.cs +++ b/TeamOctolings.Octobot/ColorsList.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Octobot; +namespace TeamOctolings.Octobot; /// /// Contains all colors used in embeds. diff --git a/src/Commands/AboutCommandGroup.cs b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs similarity index 97% rename from src/Commands/AboutCommandGroup.cs rename to TeamOctolings.Octobot/Commands/AboutCommandGroup.cs index 02384f8..9f05af3 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs @@ -1,9 +1,6 @@ using System.ComponentModel; using System.Text; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -18,8 +15,11 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles the command to show information about this bot: /about. diff --git a/src/Commands/BanCommandGroup.cs b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs similarity index 98% rename from src/Commands/BanCommandGroup.cs rename to TeamOctolings.Octobot/Commands/BanCommandGroup.cs index 02a377a..8d90286 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs @@ -2,11 +2,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Parsers; -using Octobot.Services; -using Octobot.Services.Update; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -19,8 +14,13 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Parsers; +using TeamOctolings.Octobot.Services; +using TeamOctolings.Octobot.Services.Update; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles commands related to ban management: /ban and /unban. diff --git a/src/Commands/ClearCommandGroup.cs b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs similarity index 97% rename from src/Commands/ClearCommandGroup.cs rename to TeamOctolings.Octobot/Commands/ClearCommandGroup.cs index 84b69db..8a8cb2f 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs @@ -1,9 +1,6 @@ using System.ComponentModel; using System.Text; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -16,8 +13,11 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles the command to clear messages in a channel: /clear. diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs similarity index 97% rename from src/Commands/Events/ErrorLoggingPostExecutionEvent.cs rename to TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 551c2d0..7ffc4fe 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -1,6 +1,5 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; @@ -11,8 +10,9 @@ using Remora.Discord.Commands.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; +using TeamOctolings.Octobot.Extensions; -namespace Octobot.Commands.Events; +namespace TeamOctolings.Octobot.Commands.Events; /// /// Handles error logging for slash command groups. diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs similarity index 93% rename from src/Commands/Events/LoggingPreparationErrorEvent.cs rename to TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs index 87b4090..10a6a1f 100644 --- a/src/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs @@ -1,11 +1,11 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using Octobot.Extensions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Services; using Remora.Results; +using TeamOctolings.Octobot.Extensions; -namespace Octobot.Commands.Events; +namespace TeamOctolings.Octobot.Commands.Events; /// /// Handles error logging for slash commands that couldn't be successfully prepared. diff --git a/src/Commands/KickCommandGroup.cs b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs similarity index 97% rename from src/Commands/KickCommandGroup.cs rename to TeamOctolings.Octobot/Commands/KickCommandGroup.cs index 87b915a..4252232 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs @@ -1,9 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -15,8 +12,11 @@ using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles the command to kick members of a guild: /kick. diff --git a/src/Commands/MuteCommandGroup.cs b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs similarity index 98% rename from src/Commands/MuteCommandGroup.cs rename to TeamOctolings.Octobot/Commands/MuteCommandGroup.cs index ce0a296..8e449f7 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs @@ -2,11 +2,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Parsers; -using Octobot.Services; -using Octobot.Services.Update; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -19,8 +14,13 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Parsers; +using TeamOctolings.Octobot.Services; +using TeamOctolings.Octobot.Services.Update; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles commands related to mute management: /mute and /unmute. diff --git a/src/Commands/PingCommandGroup.cs b/TeamOctolings.Octobot/Commands/PingCommandGroup.cs similarity index 96% rename from src/Commands/PingCommandGroup.cs rename to TeamOctolings.Octobot/Commands/PingCommandGroup.cs index d64c6dd..70b9f23 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/PingCommandGroup.cs @@ -1,8 +1,5 @@ using System.ComponentModel; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -15,8 +12,11 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping diff --git a/src/Commands/RemindCommandGroup.cs b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs similarity index 98% rename from src/Commands/RemindCommandGroup.cs rename to TeamOctolings.Octobot/Commands/RemindCommandGroup.cs index aa1ef7e..f40ba6b 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs @@ -2,9 +2,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -17,9 +14,12 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using Octobot.Parsers; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Parsers; +using TeamOctolings.Octobot.Services; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles commands to manage reminders: /remind, /listremind, /delremind diff --git a/src/Commands/SettingsCommandGroup.cs b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs similarity index 96% rename from src/Commands/SettingsCommandGroup.cs rename to TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs index a39e9c7..56584bf 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs @@ -3,10 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Text; using System.Text.Json.Nodes; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Data.Options; -using Octobot.Extensions; -using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -19,8 +15,12 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Data.Options; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles the commands to list and modify per-guild settings: /settings and /settings list. @@ -29,13 +29,13 @@ namespace Octobot.Commands; public class SettingsCommandGroup : CommandGroup { /// - /// Represents all options as an array of objects implementing . + /// Represents all options as an array of objects implementing . /// /// /// WARNING: If you update this array in any way, you must also update and make sure /// that the orders match. /// - private static readonly IOption[] AllOptions = + private static readonly IGuildOption[] AllOptions = [ GuildSettings.Language, GuildSettings.WelcomeMessage, @@ -199,7 +199,7 @@ public class SettingsCommandGroup : CommandGroup } private async Task EditSettingAsync( - IOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot, + IGuildOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot, CancellationToken ct = default) { var setResult = option.Set(data.Settings, value); @@ -270,7 +270,7 @@ public class SettingsCommandGroup : CommandGroup } private async Task ResetSingleSettingAsync(JsonNode cfg, IUser bot, - IOption option, CancellationToken ct = default) + IGuildOption option, CancellationToken ct = default) { var resetResult = option.Reset(cfg); if (!resetResult.IsSuccess) diff --git a/src/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs similarity index 99% rename from src/Commands/ToolsCommandGroup.cs rename to TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs index d4f3f75..3e84527 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs @@ -2,10 +2,6 @@ using System.ComponentModel; using System.Drawing; using System.Text; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Parsers; -using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -17,8 +13,12 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Parsers; +using TeamOctolings.Octobot.Services; -namespace Octobot.Commands; +namespace TeamOctolings.Octobot.Commands; /// /// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp, /8ball. diff --git a/src/Data/GuildData.cs b/TeamOctolings.Octobot/Data/GuildData.cs similarity index 97% rename from src/Data/GuildData.cs rename to TeamOctolings.Octobot/Data/GuildData.cs index 5a903d6..f393323 100644 --- a/src/Data/GuildData.cs +++ b/TeamOctolings.Octobot/Data/GuildData.cs @@ -1,7 +1,7 @@ using System.Text.Json.Nodes; using Remora.Rest.Core; -namespace Octobot.Data; +namespace TeamOctolings.Octobot.Data; /// /// Stores information about a guild. This information is not accessible via the Discord API. diff --git a/src/Data/GuildSettings.cs b/TeamOctolings.Octobot/Data/GuildSettings.cs similarity index 92% rename from src/Data/GuildSettings.cs rename to TeamOctolings.Octobot/Data/GuildSettings.cs index a1d8d74..dc59d6f 100644 --- a/src/Data/GuildSettings.cs +++ b/TeamOctolings.Octobot/Data/GuildSettings.cs @@ -1,8 +1,8 @@ -using Octobot.Data.Options; -using Octobot.Responders; using Remora.Discord.API.Abstractions.Objects; +using TeamOctolings.Octobot.Data.Options; +using TeamOctolings.Octobot.Responders; -namespace Octobot.Data; +namespace TeamOctolings.Octobot.Data; /// /// Contains all per-guild settings that can be set by a member @@ -22,7 +22,7 @@ public static class GuildSettings /// /// /// - public static readonly Option WelcomeMessage = new("WelcomeMessage", "default"); + public static readonly GuildOption WelcomeMessage = new("WelcomeMessage", "default"); /// /// Controls what message should be sent in when a member leaves the guild. @@ -34,7 +34,7 @@ public static class GuildSettings /// /// /// - public static readonly Option LeaveMessage = new("LeaveMessage", "default"); + public static readonly GuildOption LeaveMessage = new("LeaveMessage", "default"); /// /// Controls whether or not the message should be sent diff --git a/src/Data/MemberData.cs b/TeamOctolings.Octobot/Data/MemberData.cs similarity index 93% rename from src/Data/MemberData.cs rename to TeamOctolings.Octobot/Data/MemberData.cs index 1b3d0d9..984d4af 100644 --- a/src/Data/MemberData.cs +++ b/TeamOctolings.Octobot/Data/MemberData.cs @@ -1,4 +1,4 @@ -namespace Octobot.Data; +namespace TeamOctolings.Octobot.Data; /// /// Stores information about a member diff --git a/src/Data/Options/AllOptionsEnum.cs b/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs similarity index 92% rename from src/Data/Options/AllOptionsEnum.cs rename to TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs index d9e0c13..6a4280e 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs @@ -1,7 +1,7 @@ using JetBrains.Annotations; -using Octobot.Commands; +using TeamOctolings.Octobot.Commands; -namespace Octobot.Data.Options; +namespace TeamOctolings.Octobot.Data.Options; /// /// Represents all options as enums. diff --git a/src/Data/Options/BoolOption.cs b/TeamOctolings.Octobot/Data/Options/BoolOption.cs similarity index 91% rename from src/Data/Options/BoolOption.cs rename to TeamOctolings.Octobot/Data/Options/BoolOption.cs index 6876164..6a3c899 100644 --- a/src/Data/Options/BoolOption.cs +++ b/TeamOctolings.Octobot/Data/Options/BoolOption.cs @@ -1,9 +1,9 @@ using System.Text.Json.Nodes; using Remora.Results; -namespace Octobot.Data.Options; +namespace TeamOctolings.Octobot.Data.Options; -public sealed class BoolOption : Option +public sealed class BoolOption : GuildOption { public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { } diff --git a/src/Data/Options/Option.cs b/TeamOctolings.Octobot/Data/Options/GuildOption.cs similarity index 90% rename from src/Data/Options/Option.cs rename to TeamOctolings.Octobot/Data/Options/GuildOption.cs index 5d703a8..5d9f1a2 100644 --- a/src/Data/Options/Option.cs +++ b/TeamOctolings.Octobot/Data/Options/GuildOption.cs @@ -2,18 +2,18 @@ using System.Text.Json.Nodes; using Remora.Discord.Extensions.Formatting; using Remora.Results; -namespace Octobot.Data.Options; +namespace TeamOctolings.Octobot.Data.Options; /// -/// Represents an per-guild option. +/// Represents a per-guild option. /// /// The type of the option. -public class Option : IOption +public class GuildOption : IGuildOption where T : notnull { protected readonly T DefaultValue; - public Option(string name, T defaultValue) + public GuildOption(string name, T defaultValue) { Name = name; DefaultValue = defaultValue; diff --git a/src/Data/Options/IOption.cs b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs similarity index 73% rename from src/Data/Options/IOption.cs rename to TeamOctolings.Octobot/Data/Options/IGuildOption.cs index b8ed03c..a8c3e6e 100644 --- a/src/Data/Options/IOption.cs +++ b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs @@ -1,9 +1,9 @@ using System.Text.Json.Nodes; using Remora.Results; -namespace Octobot.Data.Options; +namespace TeamOctolings.Octobot.Data.Options; -public interface IOption +public interface IGuildOption { string Name { get; } string Display(JsonNode settings); diff --git a/src/Data/Options/LanguageOption.cs b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs similarity index 91% rename from src/Data/Options/LanguageOption.cs rename to TeamOctolings.Octobot/Data/Options/LanguageOption.cs index 22f98df..15ab6ff 100644 --- a/src/Data/Options/LanguageOption.cs +++ b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs @@ -3,10 +3,10 @@ using System.Text.Json.Nodes; using Remora.Discord.Extensions.Formatting; using Remora.Results; -namespace Octobot.Data.Options; +namespace TeamOctolings.Octobot.Data.Options; /// -public sealed class LanguageOption : Option +public sealed class LanguageOption : GuildOption { private static readonly Dictionary CultureInfoCache = new() { diff --git a/src/Data/Options/SnowflakeOption.cs b/TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs similarity index 87% rename from src/Data/Options/SnowflakeOption.cs rename to TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs index 7118da8..b7405f2 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs @@ -1,13 +1,13 @@ using System.Text.Json.Nodes; using System.Text.RegularExpressions; -using Octobot.Extensions; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Extensions; -namespace Octobot.Data.Options; +namespace TeamOctolings.Octobot.Data.Options; -public sealed partial class SnowflakeOption : Option +public sealed partial class SnowflakeOption : GuildOption { public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { } diff --git a/src/Data/Options/TimeSpanOption.cs b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs similarity index 82% rename from src/Data/Options/TimeSpanOption.cs rename to TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs index d237b6e..3501f09 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs @@ -1,10 +1,10 @@ using System.Text.Json.Nodes; -using Octobot.Parsers; using Remora.Results; +using TeamOctolings.Octobot.Parsers; -namespace Octobot.Data.Options; +namespace TeamOctolings.Octobot.Data.Options; -public sealed class TimeSpanOption : Option +public sealed class TimeSpanOption : GuildOption { public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { } diff --git a/src/Data/Reminder.cs b/TeamOctolings.Octobot/Data/Reminder.cs similarity index 83% rename from src/Data/Reminder.cs rename to TeamOctolings.Octobot/Data/Reminder.cs index f21b222..c3936da 100644 --- a/src/Data/Reminder.cs +++ b/TeamOctolings.Octobot/Data/Reminder.cs @@ -1,4 +1,4 @@ -namespace Octobot.Data; +namespace TeamOctolings.Octobot.Data; public struct Reminder { diff --git a/src/Data/ScheduledEventData.cs b/TeamOctolings.Octobot/Data/ScheduledEventData.cs similarity index 97% rename from src/Data/ScheduledEventData.cs rename to TeamOctolings.Octobot/Data/ScheduledEventData.cs index 59efc63..7ba6e92 100644 --- a/src/Data/ScheduledEventData.cs +++ b/TeamOctolings.Octobot/Data/ScheduledEventData.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using Remora.Discord.API.Abstractions.Objects; -namespace Octobot.Data; +namespace TeamOctolings.Octobot.Data; /// /// Stores information about scheduled events. This information is not provided by the Discord API. diff --git a/src/Extensions/ChannelApiExtensions.cs b/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs similarity index 96% rename from src/Extensions/ChannelApiExtensions.cs rename to TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs index 99eff67..2767f96 100644 --- a/src/Extensions/ChannelApiExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs @@ -5,7 +5,7 @@ using Remora.Discord.API.Objects; using Remora.Rest.Core; using Remora.Results; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class ChannelApiExtensions { diff --git a/src/Extensions/CollectionExtensions.cs b/TeamOctolings.Octobot/Extensions/CollectionExtensions.cs similarity index 96% rename from src/Extensions/CollectionExtensions.cs rename to TeamOctolings.Octobot/Extensions/CollectionExtensions.cs index 2369532..3ea13a8 100644 --- a/src/Extensions/CollectionExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/CollectionExtensions.cs @@ -1,6 +1,6 @@ using Remora.Results; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class CollectionExtensions { diff --git a/src/Extensions/CommandContextExtensions.cs b/TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs similarity index 92% rename from src/Extensions/CommandContextExtensions.cs rename to TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs index a0c02f2..16b8b56 100644 --- a/src/Extensions/CommandContextExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs @@ -2,7 +2,7 @@ using Remora.Discord.Commands.Extensions; using Remora.Rest.Core; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class CommandContextExtensions { diff --git a/src/Extensions/DiffPaneModelExtensions.cs b/TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs similarity index 94% rename from src/Extensions/DiffPaneModelExtensions.cs rename to TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs index 1c3a098..3bb707b 100644 --- a/src/Extensions/DiffPaneModelExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs @@ -1,7 +1,7 @@ using System.Text; using DiffPlex.DiffBuilder.Model; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class DiffPaneModelExtensions { diff --git a/src/Extensions/EmbedBuilderExtensions.cs b/TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs similarity index 99% rename from src/Extensions/EmbedBuilderExtensions.cs rename to TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs index 2d61403..dab0265 100644 --- a/src/Extensions/EmbedBuilderExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs @@ -4,7 +4,7 @@ using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Rest.Core; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class EmbedBuilderExtensions { diff --git a/src/Extensions/FeedbackServiceExtensions.cs b/TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs similarity index 93% rename from src/Extensions/FeedbackServiceExtensions.cs rename to TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs index e6ef376..c66c946 100644 --- a/src/Extensions/FeedbackServiceExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs @@ -3,7 +3,7 @@ using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Services; using Remora.Results; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class FeedbackServiceExtensions { diff --git a/src/Extensions/GuildScheduledEventExtensions.cs b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs similarity index 95% rename from src/Extensions/GuildScheduledEventExtensions.cs rename to TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs index f1b6985..b8eb2d1 100644 --- a/src/Extensions/GuildScheduledEventExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs @@ -2,7 +2,7 @@ using Remora.Rest.Core; using Remora.Results; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class GuildScheduledEventExtensions { diff --git a/src/Extensions/LoggerExtensions.cs b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs similarity index 96% rename from src/Extensions/LoggerExtensions.cs rename to TeamOctolings.Octobot/Extensions/LoggerExtensions.cs index fca3702..76fa386 100644 --- a/src/Extensions/LoggerExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Logging; using Remora.Results; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class LoggerExtensions { diff --git a/src/Extensions/MarkdownExtensions.cs b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs similarity index 88% rename from src/Extensions/MarkdownExtensions.cs rename to TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs index 7b7f780..202cd37 100644 --- a/src/Extensions/MarkdownExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs @@ -1,4 +1,4 @@ -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class MarkdownExtensions { diff --git a/src/Extensions/ResultExtensions.cs b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs similarity index 82% rename from src/Extensions/ResultExtensions.cs rename to TeamOctolings.Octobot/Extensions/ResultExtensions.cs index a95a1e2..d7ef7d7 100644 --- a/src/Extensions/ResultExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using Remora.Results; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class ResultExtensions { @@ -23,18 +23,23 @@ public static class ResultExtensions private static void LogResultStackTrace(Result result) { - if (Octobot.StaticLogger is null || result.IsSuccess) + if (result.IsSuccess) { return; } - Octobot.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}", + if (Utility.StaticLogger is null) + { + throw new InvalidOperationException(); + } + + Utility.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}", result.Error.GetType().FullName, result.Error.Message, Environment.NewLine, ConstructStackTrace()); var inner = result.Inner; while (inner is { IsSuccess: false }) { - Octobot.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}", + Utility.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}", inner.Error.GetType().FullName, inner.Error.Message); inner = inner.Inner; diff --git a/src/Extensions/SnowflakeExtensions.cs b/TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs similarity index 96% rename from src/Extensions/SnowflakeExtensions.cs rename to TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs index e60bc44..70810ef 100644 --- a/src/Extensions/SnowflakeExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs @@ -1,6 +1,6 @@ using Remora.Rest.Core; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class SnowflakeExtensions { diff --git a/src/Extensions/StringBuilderExtensions.cs b/TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs similarity index 98% rename from src/Extensions/StringBuilderExtensions.cs rename to TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs index ddd24a3..25b7b5b 100644 --- a/src/Extensions/StringBuilderExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs @@ -1,6 +1,6 @@ using System.Text; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class StringBuilderExtensions { diff --git a/src/Extensions/StringExtensions.cs b/TeamOctolings.Octobot/Extensions/StringExtensions.cs similarity index 98% rename from src/Extensions/StringExtensions.cs rename to TeamOctolings.Octobot/Extensions/StringExtensions.cs index cb8d606..bf7f6c8 100644 --- a/src/Extensions/StringExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/StringExtensions.cs @@ -1,7 +1,7 @@ using System.Net; using Remora.Discord.Extensions.Formatting; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class StringExtensions { diff --git a/src/Extensions/UInt64Extensions.cs b/TeamOctolings.Octobot/Extensions/UInt64Extensions.cs similarity index 82% rename from src/Extensions/UInt64Extensions.cs rename to TeamOctolings.Octobot/Extensions/UInt64Extensions.cs index 5d1db00..2b9c0a2 100644 --- a/src/Extensions/UInt64Extensions.cs +++ b/TeamOctolings.Octobot/Extensions/UInt64Extensions.cs @@ -1,7 +1,7 @@ using Remora.Discord.API; using Remora.Rest.Core; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class UInt64Extensions { diff --git a/src/Extensions/UserExtensions.cs b/TeamOctolings.Octobot/Extensions/UserExtensions.cs similarity index 85% rename from src/Extensions/UserExtensions.cs rename to TeamOctolings.Octobot/Extensions/UserExtensions.cs index 38fe985..d9eff33 100644 --- a/src/Extensions/UserExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/UserExtensions.cs @@ -1,6 +1,6 @@ using Remora.Discord.API.Abstractions.Objects; -namespace Octobot.Extensions; +namespace TeamOctolings.Octobot.Extensions; public static class UserExtensions { diff --git a/src/Messages.Designer.cs b/TeamOctolings.Octobot/Messages.Designer.cs similarity index 89% rename from src/Messages.Designer.cs rename to TeamOctolings.Octobot/Messages.Designer.cs index 2910bae..bbc1366 100644 --- a/src/Messages.Designer.cs +++ b/TeamOctolings.Octobot/Messages.Designer.cs @@ -7,31 +7,34 @@ // //------------------------------------------------------------------------------ -namespace Octobot { +namespace TeamOctolings.Octobot { + using System; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - + private static System.Resources.ResourceManager resourceMan; - + private static System.Globalization.CultureInfo resourceCulture; - + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Resources.ResourceManager ResourceManager { get { if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Octobot.locale.Messages", typeof(Messages).Assembly); + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("TeamOctolings.Octobot.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Globalization.CultureInfo Culture { get { @@ -41,1180 +44,1157 @@ namespace Octobot { resourceCulture = value; } } - + internal static string Ready { get { return ResourceManager.GetString("Ready", resourceCulture); } } - + internal static string CachedMessageDeleted { get { return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - + internal static string CachedMessageEdited { get { return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - + internal static string DefaultWelcomeMessage { get { return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); } } - + internal static string Generic1 { get { return ResourceManager.GetString("Generic1", resourceCulture); } } - + internal static string Generic2 { get { return ResourceManager.GetString("Generic2", resourceCulture); } } - + internal static string Generic3 { get { return ResourceManager.GetString("Generic3", resourceCulture); } } - + internal static string YouWereBanned { get { return ResourceManager.GetString("YouWereBanned", resourceCulture); } } - + internal static string PunishmentExpired { get { return ResourceManager.GetString("PunishmentExpired", resourceCulture); } } - + internal static string YouWereKicked { get { return ResourceManager.GetString("YouWereKicked", resourceCulture); } } - + internal static string Milliseconds { get { return ResourceManager.GetString("Milliseconds", resourceCulture); } } - + internal static string ChannelNotSpecified { get { return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); } } - + internal static string RoleNotSpecified { get { return ResourceManager.GetString("RoleNotSpecified", resourceCulture); } } - - internal static string SettingsLang { + + internal static string SettingsLanguage { get { - return ResourceManager.GetString("SettingsLang", resourceCulture); + return ResourceManager.GetString("SettingsLanguage", resourceCulture); } } - + internal static string SettingsPrefix { get { return ResourceManager.GetString("SettingsPrefix", resourceCulture); } } - + internal static string SettingsRemoveRolesOnMute { get { return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); } } - + internal static string SettingsSendWelcomeMessages { get { return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); } } - + internal static string SettingsMuteRole { get { return ResourceManager.GetString("SettingsMuteRole", resourceCulture); } } - + internal static string LanguageNotSupported { get { return ResourceManager.GetString("LanguageNotSupported", resourceCulture); } } - + internal static string Yes { get { return ResourceManager.GetString("Yes", resourceCulture); } } - + internal static string No { get { return ResourceManager.GetString("No", resourceCulture); } } - + internal static string UserNotBanned { get { return ResourceManager.GetString("UserNotBanned", resourceCulture); } } - + internal static string MemberNotMuted { get { return ResourceManager.GetString("MemberNotMuted", resourceCulture); } } - + internal static string SettingsWelcomeMessage { get { return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); } } - + internal static string UserBanned { get { return ResourceManager.GetString("UserBanned", resourceCulture); } } - + internal static string SettingsReceiveStartupMessages { get { return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); } } - + internal static string InvalidSettingValue { get { return ResourceManager.GetString("InvalidSettingValue", resourceCulture); } } - - internal static string InvalidRole { - get { - return ResourceManager.GetString("InvalidRole", resourceCulture); - } - } - - internal static string InvalidChannel { - get { - return ResourceManager.GetString("InvalidChannel", resourceCulture); - } - } - + internal static string DurationRequiredForTimeOuts { get { return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); } } - + internal static string CannotTimeOutBot { get { return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); } } - + internal static string SettingsEventNotificationRole { get { return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); } } - + internal static string SettingsEventNotificationChannel { get { return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); } } - + internal static string SettingsEventStartedReceivers { get { return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); } } - + internal static string EventStarted { get { return ResourceManager.GetString("EventStarted", resourceCulture); } } - + internal static string EventCancelled { get { return ResourceManager.GetString("EventCancelled", resourceCulture); } } - + internal static string EventCompleted { get { return ResourceManager.GetString("EventCompleted", resourceCulture); } } - + internal static string MessagesCleared { get { return ResourceManager.GetString("MessagesCleared", resourceCulture); } } - + internal static string SettingsNothingChanged { get { return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); } } - + internal static string SettingNotDefined { get { return ResourceManager.GetString("SettingNotDefined", resourceCulture); } } - + + internal static string MissingUser { + get { + return ResourceManager.GetString("MissingUser", resourceCulture); + } + } + internal static string UserCannotBanMembers { get { return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); } } - + internal static string UserCannotManageMessages { get { return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); } } - + internal static string UserCannotKickMembers { get { return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); } } - - internal static string UserCannotModerateMembers { + + internal static string UserCannotMuteMembers { get { - return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture); + return ResourceManager.GetString("UserCannotMuteMembers", resourceCulture); } } - + + internal static string UserCannotUnmuteMembers { + get { + return ResourceManager.GetString("UserCannotUnmuteMembers", resourceCulture); + } + } + internal static string UserCannotManageGuild { get { return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); } } - + internal static string BotCannotBanMembers { get { return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); } } - + internal static string BotCannotManageMessages { get { return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); } } - + internal static string BotCannotKickMembers { get { return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); } } - + internal static string BotCannotModerateMembers { get { return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); } } - + internal static string BotCannotManageGuild { get { return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); } } - + internal static string UserCannotBanOwner { get { return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); } } - + internal static string UserCannotBanThemselves { get { return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); } } - + internal static string UserCannotBanBot { get { return ResourceManager.GetString("UserCannotBanBot", resourceCulture); } } - + internal static string BotCannotBanTarget { get { return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); } } - + internal static string UserCannotBanTarget { get { return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); } } - + internal static string UserCannotKickOwner { get { return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); } } - + internal static string UserCannotKickThemselves { get { return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); } } - + internal static string UserCannotKickBot { get { return ResourceManager.GetString("UserCannotKickBot", resourceCulture); } } - + internal static string BotCannotKickTarget { get { return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); } } - + internal static string UserCannotKickTarget { get { return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); } } - + internal static string UserCannotMuteOwner { get { return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); } } - + internal static string UserCannotMuteThemselves { get { return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); } } - + internal static string UserCannotMuteBot { get { return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); } } - + internal static string BotCannotMuteTarget { get { return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); } } - + internal static string UserCannotMuteTarget { get { return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); } } - + internal static string UserCannotUnmuteOwner { get { return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); } } - + internal static string UserCannotUnmuteThemselves { get { return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); } } - + internal static string UserCannotUnmuteBot { get { return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); } } - + internal static string BotCannotUnmuteTarget { get { return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); } } - + internal static string UserCannotUnmuteTarget { get { return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); } } - + internal static string EventEarlyNotification { get { return ResourceManager.GetString("EventEarlyNotification", resourceCulture); } } - + internal static string SettingsEventEarlyNotificationOffset { get { return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); } } - + internal static string UserNotFound { get { return ResourceManager.GetString("UserNotFound", resourceCulture); } } - + internal static string SettingsDefaultRole { get { return ResourceManager.GetString("SettingsDefaultRole", resourceCulture); } } - + internal static string SettingsPublicFeedbackChannel { get { return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture); } } - + internal static string SettingsPrivateFeedbackChannel { get { return ResourceManager.GetString("SettingsPrivateFeedbackChannel", resourceCulture); } } - + internal static string SettingsReturnRolesOnRejoin { get { return ResourceManager.GetString("SettingsReturnRolesOnRejoin", resourceCulture); } } - + internal static string SettingsAutoStartEvents { get { return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); } } - + internal static string IssuedBy { get { return ResourceManager.GetString("IssuedBy", resourceCulture); } } - + internal static string EventCreatedTitle { get { return ResourceManager.GetString("EventCreatedTitle", resourceCulture); } } - + internal static string DescriptionLocalEventCreated { get { return ResourceManager.GetString("DescriptionLocalEventCreated", resourceCulture); } } - + internal static string DescriptionExternalEventCreated { get { return ResourceManager.GetString("DescriptionExternalEventCreated", resourceCulture); } } - + internal static string ButtonOpenEventInfo { get { return ResourceManager.GetString("ButtonOpenEventInfo", resourceCulture); } } - + internal static string EventDuration { get { return ResourceManager.GetString("EventDuration", resourceCulture); } } - + internal static string DescriptionLocalEventStarted { get { return ResourceManager.GetString("DescriptionLocalEventStarted", resourceCulture); } } - + internal static string DescriptionExternalEventStarted { get { return ResourceManager.GetString("DescriptionExternalEventStarted", resourceCulture); } } - + internal static string UserAlreadyBanned { get { return ResourceManager.GetString("UserAlreadyBanned", resourceCulture); } } - + internal static string UserUnbanned { get { return ResourceManager.GetString("UserUnbanned", resourceCulture); } } - + internal static string UserMuted { get { return ResourceManager.GetString("UserMuted", resourceCulture); } } - + internal static string UserUnmuted { get { return ResourceManager.GetString("UserUnmuted", resourceCulture); } } - + internal static string UserNotMuted { get { return ResourceManager.GetString("UserNotMuted", resourceCulture); } } - + internal static string UserNotFoundShort { get { return ResourceManager.GetString("UserNotFoundShort", resourceCulture); } } - + internal static string UserKicked { get { return ResourceManager.GetString("UserKicked", resourceCulture); } } - + internal static string DescriptionActionReason { get { return ResourceManager.GetString("DescriptionActionReason", resourceCulture); } } - + internal static string DescriptionActionExpiresAt { get { return ResourceManager.GetString("DescriptionActionExpiresAt", resourceCulture); } } - + internal static string UserAlreadyMuted { get { return ResourceManager.GetString("UserAlreadyMuted", resourceCulture); } } - + internal static string MessageFrom { get { return ResourceManager.GetString("MessageFrom", resourceCulture); } } - + internal static string AboutTitleDevelopers { get { return ResourceManager.GetString("AboutTitleDevelopers", resourceCulture); } } - + internal static string ButtonOpenRepository { get { return ResourceManager.GetString("ButtonOpenRepository", resourceCulture); } } - + internal static string AboutBot { get { return ResourceManager.GetString("AboutBot", resourceCulture); } } - + internal static string AboutDeveloper_mctaylors { get { return ResourceManager.GetString("AboutDeveloper@mctaylors", resourceCulture); } } - + internal static string AboutDeveloper_Octol1ttle { get { return ResourceManager.GetString("AboutDeveloper@Octol1ttle", resourceCulture); } } - + internal static string AboutDeveloper_neroduckale { get { return ResourceManager.GetString("AboutDeveloper@neroduckale", resourceCulture); } } - + internal static string ReminderCreated { get { return ResourceManager.GetString("ReminderCreated", resourceCulture); } } - + internal static string Reminder { get { return ResourceManager.GetString("Reminder", resourceCulture); } } - + internal static string DescriptionReminder { get { return ResourceManager.GetString("DescriptionReminder", resourceCulture); } } - + internal static string SettingsListTitle { get { return ResourceManager.GetString("SettingsListTitle", resourceCulture); } } - + internal static string SettingSuccessfullyChanged { get { return ResourceManager.GetString("SettingSuccessfullyChanged", resourceCulture); } } - + internal static string SettingNotChanged { get { return ResourceManager.GetString("SettingNotChanged", resourceCulture); } } - + internal static string SettingIsNow { get { return ResourceManager.GetString("SettingIsNow", resourceCulture); } } - + + internal static string SettingsRenameHoistedUsers { + get { + return ResourceManager.GetString("SettingsRenameHoistedUsers", resourceCulture); + } + } + internal static string Page { get { return ResourceManager.GetString("Page", resourceCulture); } } - + internal static string PageNotFound { get { return ResourceManager.GetString("PageNotFound", resourceCulture); } } - + internal static string PagesAllowed { get { return ResourceManager.GetString("PagesAllowed", resourceCulture); } } - + internal static string Next { get { return ResourceManager.GetString("Next", resourceCulture); } } - + internal static string Previous { get { return ResourceManager.GetString("Previous", resourceCulture); } } - + internal static string ReminderList { get { return ResourceManager.GetString("ReminderList", resourceCulture); } } - + internal static string InvalidReminderPosition { get { return ResourceManager.GetString("InvalidReminderPosition", resourceCulture); } } - + internal static string ReminderDeleted { get { return ResourceManager.GetString("ReminderDeleted", resourceCulture); } } - + internal static string NoRemindersFound { get { return ResourceManager.GetString("NoRemindersFound", resourceCulture); } } - + internal static string SingleSettingReset { get { return ResourceManager.GetString("SingleSettingReset", resourceCulture); } } - + internal static string AllSettingsReset { get { return ResourceManager.GetString("AllSettingsReset", resourceCulture); } } - + internal static string DescriptionActionJumpToMessage { get { return ResourceManager.GetString("DescriptionActionJumpToMessage", resourceCulture); } } - + internal static string DescriptionActionJumpToChannel { get { return ResourceManager.GetString("DescriptionActionJumpToChannel", resourceCulture); } } - + internal static string ReminderPosition { get { return ResourceManager.GetString("ReminderPosition", resourceCulture); } } - + internal static string ReminderTime { get { return ResourceManager.GetString("ReminderTime", resourceCulture); } } - + internal static string ReminderText { get { return ResourceManager.GetString("ReminderText", resourceCulture); } } - - internal static string InformationAbout { - get { - return ResourceManager.GetString("InformationAbout", resourceCulture); - } - } - + internal static string UserInfoDisplayName { get { return ResourceManager.GetString("UserInfoDisplayName", resourceCulture); } } - - internal static string UserInfoDiscordUserSince { + + internal static string InformationAbout { get { - return ResourceManager.GetString("UserInfoDiscordUserSince", resourceCulture); + return ResourceManager.GetString("InformationAbout", resourceCulture); } } - + internal static string UserInfoMuted { get { return ResourceManager.GetString("UserInfoMuted", resourceCulture); } } - + + internal static string UserInfoDiscordUserSince { + get { + return ResourceManager.GetString("UserInfoDiscordUserSince", resourceCulture); + } + } + internal static string UserInfoBanned { get { return ResourceManager.GetString("UserInfoBanned", resourceCulture); } } - + internal static string UserInfoPunishments { get { return ResourceManager.GetString("UserInfoPunishments", resourceCulture); } } - + internal static string UserInfoBannedPermanently { get { return ResourceManager.GetString("UserInfoBannedPermanently", resourceCulture); } } - + internal static string UserInfoNotOnGuild { get { return ResourceManager.GetString("UserInfoNotOnGuild", resourceCulture); } } - + internal static string UserInfoMutedByTimeout { get { return ResourceManager.GetString("UserInfoMutedByTimeout", resourceCulture); } } - + internal static string UserInfoMutedByMuteRole { get { return ResourceManager.GetString("UserInfoMutedByMuteRole", resourceCulture); } } - + internal static string UserInfoGuildMemberSince { get { return ResourceManager.GetString("UserInfoGuildMemberSince", resourceCulture); } } - + internal static string UserInfoGuildNickname { get { return ResourceManager.GetString("UserInfoGuildNickname", resourceCulture); } } - + internal static string UserInfoGuildRoles { get { return ResourceManager.GetString("UserInfoGuildRoles", resourceCulture); } } - + internal static string UserInfoGuildMemberPremiumSince { get { return ResourceManager.GetString("UserInfoGuildMemberPremiumSince", resourceCulture); } } - - internal static string RandomTitle - { + + internal static string RandomTitle { get { return ResourceManager.GetString("RandomTitle", resourceCulture); } } - - internal static string RandomMinMaxSame - { + + internal static string RandomMinMaxSame { get { return ResourceManager.GetString("RandomMinMaxSame", resourceCulture); } } - - internal static string RandomMax - { - get { - return ResourceManager.GetString("RandomMax", resourceCulture); - } - } - - internal static string RandomMin - { + + internal static string RandomMin { get { return ResourceManager.GetString("RandomMin", resourceCulture); } } - - internal static string Default - { + + internal static string RandomMax { + get { + return ResourceManager.GetString("RandomMax", resourceCulture); + } + } + + internal static string Default { get { return ResourceManager.GetString("Default", resourceCulture); } } - - internal static string TimestampTitle - { - get - { + + internal static string TimestampTitle { + get { return ResourceManager.GetString("TimestampTitle", resourceCulture); } } - - internal static string TimestampOffset - { - get - { + + internal static string TimestampOffset { + get { return ResourceManager.GetString("TimestampOffset", resourceCulture); } } - - internal static string GuildInfoDescription - { - get - { + + internal static string GuildInfoDescription { + get { return ResourceManager.GetString("GuildInfoDescription", resourceCulture); } } - - internal static string GuildInfoCreatedAt - { - get - { + + internal static string GuildInfoCreatedAt { + get { return ResourceManager.GetString("GuildInfoCreatedAt", resourceCulture); } } - - internal static string GuildInfoOwner - { - get - { + + internal static string GuildInfoOwner { + get { return ResourceManager.GetString("GuildInfoOwner", resourceCulture); } } - - internal static string GuildInfoServerBoost - { - get - { + + internal static string GuildInfoServerBoost { + get { return ResourceManager.GetString("GuildInfoServerBoost", resourceCulture); } } - - internal static string GuildInfoBoostTier - { - get - { + + internal static string GuildInfoBoostTier { + get { return ResourceManager.GetString("GuildInfoBoostTier", resourceCulture); } } - - internal static string GuildInfoBoostCount - { - get - { + + internal static string GuildInfoBoostCount { + get { return ResourceManager.GetString("GuildInfoBoostCount", resourceCulture); } } - - internal static string NoMessagesToClear - { - get - { + + internal static string NoMessagesToClear { + get { return ResourceManager.GetString("NoMessagesToClear", resourceCulture); } } - - internal static string MessagesClearedFiltered - { - get - { + + internal static string MessagesClearedFiltered { + get { return ResourceManager.GetString("MessagesClearedFiltered", resourceCulture); } } - - internal static string DataLoadFailedTitle - { - get - { + + internal static string DataLoadFailedTitle { + get { return ResourceManager.GetString("DataLoadFailedTitle", resourceCulture); } } - - internal static string DataLoadFailedDescription - { - get - { + + internal static string DataLoadFailedDescription { + get { return ResourceManager.GetString("DataLoadFailedDescription", resourceCulture); } } - - internal static string CommandExecutionFailed - { - get - { + + internal static string CommandExecutionFailed { + get { return ResourceManager.GetString("CommandExecutionFailed", resourceCulture); } } - - internal static string ContactDevelopers - { - get - { + + internal static string ContactDevelopers { + get { return ResourceManager.GetString("ContactDevelopers", resourceCulture); } } - - internal static string ButtonReportIssue - { - get - { + + internal static string ButtonReportIssue { + get { return ResourceManager.GetString("ButtonReportIssue", resourceCulture); } } - + internal static string DefaultLeaveMessage { get { return ResourceManager.GetString("DefaultLeaveMessage", resourceCulture); } } - + internal static string SettingsLeaveMessage { get { return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture); } } - + internal static string InvalidTimeSpan { get { return ResourceManager.GetString("InvalidTimeSpan", resourceCulture); } } - + internal static string UserInfoKicked { get { return ResourceManager.GetString("UserInfoKicked", resourceCulture); } } - + internal static string ReminderEdited { get { return ResourceManager.GetString("ReminderEdited", resourceCulture); } } - + internal static string EightBallPositive1 { get { return ResourceManager.GetString("EightBallPositive1", resourceCulture); } } - + internal static string EightBallPositive2 { get { return ResourceManager.GetString("EightBallPositive2", resourceCulture); } } - + internal static string EightBallPositive3 { get { return ResourceManager.GetString("EightBallPositive3", resourceCulture); } } - + internal static string EightBallPositive4 { get { return ResourceManager.GetString("EightBallPositive4", resourceCulture); } } - + internal static string EightBallPositive5 { get { return ResourceManager.GetString("EightBallPositive5", resourceCulture); } } - + internal static string EightBallQuestionable1 { get { return ResourceManager.GetString("EightBallQuestionable1", resourceCulture); } } - + internal static string EightBallQuestionable2 { get { return ResourceManager.GetString("EightBallQuestionable2", resourceCulture); } } - + internal static string EightBallQuestionable3 { get { return ResourceManager.GetString("EightBallQuestionable3", resourceCulture); } } - + internal static string EightBallQuestionable4 { get { return ResourceManager.GetString("EightBallQuestionable4", resourceCulture); } } - + internal static string EightBallQuestionable5 { get { return ResourceManager.GetString("EightBallQuestionable5", resourceCulture); } } - + internal static string EightBallNeutral1 { get { return ResourceManager.GetString("EightBallNeutral1", resourceCulture); } } - + internal static string EightBallNeutral2 { get { return ResourceManager.GetString("EightBallNeutral2", resourceCulture); } } - + internal static string EightBallNeutral3 { get { return ResourceManager.GetString("EightBallNeutral3", resourceCulture); } } - + internal static string EightBallNeutral4 { get { return ResourceManager.GetString("EightBallNeutral4", resourceCulture); } } - + internal static string EightBallNeutral5 { get { return ResourceManager.GetString("EightBallNeutral5", resourceCulture); } } - + internal static string EightBallNegative1 { get { return ResourceManager.GetString("EightBallNegative1", resourceCulture); } } - + internal static string EightBallNegative2 { get { return ResourceManager.GetString("EightBallNegative2", resourceCulture); } } - + internal static string EightBallNegative3 { get { return ResourceManager.GetString("EightBallNegative3", resourceCulture); } } - + internal static string EightBallNegative4 { get { return ResourceManager.GetString("EightBallNegative4", resourceCulture); } } - + internal static string EightBallNegative5 { get { return ResourceManager.GetString("EightBallNegative5", resourceCulture); } } - + internal static string TimeSpanExample { get { return ResourceManager.GetString("TimeSpanExample", resourceCulture); } } - + internal static string Version { get { return ResourceManager.GetString("Version", resourceCulture); } } - + internal static string SettingsWelcomeMessagesChannel { get { return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); } } - + internal static string ButtonDirty { get { return ResourceManager.GetString("ButtonDirty", resourceCulture); } } - + internal static string ButtonOpenWiki { get { return ResourceManager.GetString("ButtonOpenWiki", resourceCulture); } } + + internal static string SettingsModeratorRole { + get { + return ResourceManager.GetString("SettingsModeratorRole", resourceCulture); + } + } } } diff --git a/locale/Messages.resx b/TeamOctolings.Octobot/Messages.resx similarity index 100% rename from locale/Messages.resx rename to TeamOctolings.Octobot/Messages.resx diff --git a/locale/Messages.ru.resx b/TeamOctolings.Octobot/Messages.ru.resx similarity index 100% rename from locale/Messages.ru.resx rename to TeamOctolings.Octobot/Messages.ru.resx diff --git a/src/Parsers/TimeSpanParser.cs b/TeamOctolings.Octobot/Parsers/TimeSpanParser.cs similarity index 98% rename from src/Parsers/TimeSpanParser.cs rename to TeamOctolings.Octobot/Parsers/TimeSpanParser.cs index 1f44d46..99a8b90 100644 --- a/src/Parsers/TimeSpanParser.cs +++ b/TeamOctolings.Octobot/Parsers/TimeSpanParser.cs @@ -4,7 +4,7 @@ using JetBrains.Annotations; using Remora.Commands.Parsers; using Remora.Results; -namespace Octobot.Parsers; +namespace TeamOctolings.Octobot.Parsers; /// /// Parses s. diff --git a/src/Octobot.cs b/TeamOctolings.Octobot/Program.cs similarity index 86% rename from src/Octobot.cs rename to TeamOctolings.Octobot/Program.cs index 065967e..d1d6220 100644 --- a/src/Octobot.cs +++ b/TeamOctolings.Octobot/Program.cs @@ -2,13 +2,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Octobot.Attributes; -using Octobot.Commands.Events; -using Octobot.Services; -using Octobot.Services.Update; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Objects; using Remora.Discord.Caching.Extensions; using Remora.Discord.Caching.Services; using Remora.Discord.Commands.Extensions; @@ -16,24 +11,20 @@ using Remora.Discord.Commands.Services; using Remora.Discord.Extensions.Extensions; using Remora.Discord.Gateway; using Remora.Discord.Hosting.Extensions; -using Remora.Rest.Core; using Serilog.Extensions.Logging; +using TeamOctolings.Octobot.Commands.Events; +using TeamOctolings.Octobot.Services; +using TeamOctolings.Octobot.Services.Update; -namespace Octobot; +namespace TeamOctolings.Octobot; -public sealed class Octobot +public sealed class Program { - public static readonly AllowedMentions NoMentions = new( - Array.Empty(), Array.Empty(), Array.Empty()); - - [StaticCallersOnly] - public static ILogger? StaticLogger { get; private set; } - public static async Task Main(string[] args) { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); var services = host.Services; - StaticLogger = services.GetRequiredService>(); + Utility.StaticLogger = services.GetRequiredService>(); var slashService = services.GetRequiredService(); // Providing a guild ID to this call will result in command duplicates! @@ -82,8 +73,8 @@ public sealed class Octobot // Init .AddDiscordCaching() .AddDiscordCommands(true, false) - .AddRespondersFromAssembly(typeof(Octobot).Assembly) - .AddCommandGroupsFromAssembly(typeof(Octobot).Assembly) + .AddRespondersFromAssembly(typeof(Program).Assembly) + .AddCommandGroupsFromAssembly(typeof(Program).Assembly) // Slash command event handlers .AddPreparationErrorEvent() .AddPostExecutionEvent() diff --git a/src/Responders/GuildLoadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs similarity index 96% rename from src/Responders/GuildLoadedResponder.cs rename to TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs index 55e9673..0c71a06 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs @@ -1,8 +1,5 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; @@ -11,8 +8,11 @@ using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Responders; +namespace TeamOctolings.Octobot.Responders; /// /// Handles sending a message to a guild that has just initialized if that guild diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs similarity index 94% rename from src/Responders/GuildMemberJoinedResponder.cs rename to TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs index 61ef5cc..6964fe7 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs @@ -1,16 +1,16 @@ using System.Text.Json.Nodes; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Responders; +namespace TeamOctolings.Octobot.Responders; /// /// Handles sending a guild's if one is set. @@ -77,7 +77,7 @@ public class GuildMemberJoinedResponder : IResponder return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, - allowedMentions: Octobot.NoMentions, ct: ct); + allowedMentions: Utility.NoMentions, ct: ct); } private async Task TryReturnRolesAsync( diff --git a/src/Responders/GuildMemberLeftResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs similarity index 91% rename from src/Responders/GuildMemberLeftResponder.cs rename to TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs index 90cc64c..4f6150c 100644 --- a/src/Responders/GuildMemberLeftResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs @@ -1,14 +1,14 @@ using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Responders; +namespace TeamOctolings.Octobot.Responders; /// /// Handles sending a guild's if one is set. @@ -67,6 +67,6 @@ public class GuildMemberLeftResponder : IResponder return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, - allowedMentions: Octobot.NoMentions, ct: ct); + allowedMentions: Utility.NoMentions, ct: ct); } } diff --git a/src/Responders/GuildUnloadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs similarity index 90% rename from src/Responders/GuildUnloadedResponder.cs rename to TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs index b49d136..a4680d4 100644 --- a/src/Responders/GuildUnloadedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs @@ -1,12 +1,12 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using Octobot.Data; -using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Gateway.Responders; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Services; -namespace Octobot.Responders; +namespace TeamOctolings.Octobot.Responders; /// /// Handles removing guild ID from if the guild becomes unavailable. diff --git a/src/Responders/MessageDeletedResponder.cs b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs similarity index 94% rename from src/Responders/MessageDeletedResponder.cs rename to TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs index 5a69273..6b09b8d 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs @@ -1,8 +1,5 @@ using System.Text; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; @@ -10,8 +7,11 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Discord.Gateway.Responders; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Responders; +namespace TeamOctolings.Octobot.Responders; /// /// Handles logging the contents of a deleted message and the user who deleted the message @@ -102,6 +102,6 @@ public class MessageDeletedResponder : IResponder return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, - allowedMentions: Octobot.NoMentions, ct: ct); + allowedMentions: Utility.NoMentions, ct: ct); } } diff --git a/src/Responders/MessageEditedResponder.cs b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs similarity index 95% rename from src/Responders/MessageEditedResponder.cs rename to TeamOctolings.Octobot/Responders/MessageEditedResponder.cs index 1143652..6134214 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs @@ -1,9 +1,6 @@ using System.Text; using DiffPlex.DiffBuilder; using JetBrains.Annotations; -using Octobot.Data; -using Octobot.Extensions; -using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; @@ -12,8 +9,11 @@ using Remora.Discord.Caching.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; -namespace Octobot.Responders; +namespace TeamOctolings.Octobot.Responders; /// /// Handles logging the difference between an edited message's old and new content @@ -104,6 +104,6 @@ public class MessageEditedResponder : IResponder return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, - allowedMentions: Octobot.NoMentions, ct: ct); + allowedMentions: Utility.NoMentions, ct: ct); } } diff --git a/src/Responders/MessageReceivedResponder.cs b/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs similarity index 96% rename from src/Responders/MessageReceivedResponder.cs rename to TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs index 4c26d8d..34a8f5c 100644 --- a/src/Responders/MessageReceivedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs @@ -5,7 +5,7 @@ using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; -namespace Octobot.Responders; +namespace TeamOctolings.Octobot.Responders; /// /// Handles sending replies to easter egg messages. diff --git a/src/Services/AccessControlService.cs b/TeamOctolings.Octobot/Services/AccessControlService.cs similarity index 97% rename from src/Services/AccessControlService.cs rename to TeamOctolings.Octobot/Services/AccessControlService.cs index cb235f9..b5b98ea 100644 --- a/src/Services/AccessControlService.cs +++ b/TeamOctolings.Octobot/Services/AccessControlService.cs @@ -1,11 +1,11 @@ -using Octobot.Data; -using Octobot.Extensions; -using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; -namespace Octobot.Services; +namespace TeamOctolings.Octobot.Services; public sealed class AccessControlService { diff --git a/src/Services/GuildDataService.cs b/TeamOctolings.Octobot/Services/GuildDataService.cs similarity index 98% rename from src/Services/GuildDataService.cs rename to TeamOctolings.Octobot/Services/GuildDataService.cs index e503d22..866ee08 100644 --- a/src/Services/GuildDataService.cs +++ b/TeamOctolings.Octobot/Services/GuildDataService.cs @@ -3,10 +3,10 @@ using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Octobot.Data; using Remora.Rest.Core; +using TeamOctolings.Octobot.Data; -namespace Octobot.Services; +namespace TeamOctolings.Octobot.Services; /// /// Handles saving, loading, initializing and providing . diff --git a/src/Services/Update/MemberUpdateService.cs b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs similarity index 98% rename from src/Services/Update/MemberUpdateService.cs rename to TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs index e177fca..51cf647 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs @@ -2,16 +2,16 @@ using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Octobot.Data; -using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; -namespace Octobot.Services.Update; +namespace TeamOctolings.Octobot.Services.Update; public sealed partial class MemberUpdateService : BackgroundService { diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs similarity index 99% rename from src/Services/Update/ScheduledEventUpdateService.cs rename to TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs index cb87779..ce9c212 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs @@ -1,8 +1,6 @@ using System.Text.Json.Nodes; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Octobot.Data; -using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; @@ -10,8 +8,10 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; -namespace Octobot.Services.Update; +namespace TeamOctolings.Octobot.Services.Update; public sealed class ScheduledEventUpdateService : BackgroundService { diff --git a/src/Services/Update/SongUpdateService.cs b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs similarity index 98% rename from src/Services/Update/SongUpdateService.cs rename to TeamOctolings.Octobot/Services/Update/SongUpdateService.cs index 41d5bf3..b07256f 100644 --- a/src/Services/Update/SongUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs @@ -4,7 +4,7 @@ using Remora.Discord.API.Gateway.Commands; using Remora.Discord.API.Objects; using Remora.Discord.Gateway; -namespace Octobot.Services.Update; +namespace TeamOctolings.Octobot.Services.Update; public sealed class SongUpdateService : BackgroundService { diff --git a/Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj similarity index 90% rename from Octobot.csproj rename to TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index bdfb46a..9c3d58b 100644 --- a/Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -1,4 +1,4 @@ - + Exe @@ -16,7 +16,7 @@ TeamOctolings en A general-purpose Discord bot for moderation written in C# - docs/octobot.ico + ../docs/octobot.ico false @@ -35,12 +35,12 @@ - + ResXFileCodeGenerator Messages.Designer.cs - + diff --git a/src/Services/Utility.cs b/TeamOctolings.Octobot/Utility.cs similarity index 92% rename from src/Services/Utility.cs rename to TeamOctolings.Octobot/Utility.cs index 3b9ab19..463212b 100644 --- a/src/Services/Utility.cs +++ b/TeamOctolings.Octobot/Utility.cs @@ -1,16 +1,19 @@ using System.Drawing; using System.Text; using System.Text.Json.Nodes; -using Octobot.Data; -using Octobot.Extensions; +using Microsoft.Extensions.Logging; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using TeamOctolings.Octobot.Attributes; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; -namespace Octobot.Services; +namespace TeamOctolings.Octobot; /// /// Provides utility methods that cannot be transformed to extension methods because they require usage @@ -18,6 +21,9 @@ namespace Octobot.Services; /// public sealed class Utility { + public static readonly AllowedMentions NoMentions = new( + Array.Empty(), Array.Empty(), Array.Empty()); + private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildAPI _guildApi; @@ -30,6 +36,9 @@ public sealed class Utility _guildApi = guildApi; } + [StaticCallersOnly] + public static ILogger? StaticLogger { get; set; } + /// /// Gets the string mentioning the and event subscribers related to /// a scheduled From ebcdcb35f7f4f34fd37209821c939336cf5cd878 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 18 May 2024 21:12:38 +0500 Subject: [PATCH 298/329] Separate /*info commands from ToolsCommandGroup (#308) who tf thought that putting 1234915912 methods responsible for 23981 commands in a single class was a good idea??????? Signed-off-by: Octol1ttle --- .../Commands/InfoCommandGroup.cs | 329 ++++++++++++++++++ .../Commands/ToolsCommandGroup.cs | 325 +---------------- 2 files changed, 347 insertions(+), 307 deletions(-) create mode 100644 TeamOctolings.Octobot/Commands/InfoCommandGroup.cs diff --git a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs new file mode 100644 index 0000000..65ddd53 --- /dev/null +++ b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs @@ -0,0 +1,329 @@ +using System.ComponentModel; +using System.Drawing; +using System.Text; +using JetBrains.Annotations; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Rest.Core; +using Remora.Results; +using TeamOctolings.Octobot.Data; +using TeamOctolings.Octobot.Extensions; +using TeamOctolings.Octobot.Services; + +namespace TeamOctolings.Octobot.Commands; + +/// +/// Handles info commands: /userinfo, /guildinfo. +/// +[UsedImplicitly] +public class InfoCommandGroup : CommandGroup +{ + private readonly ICommandContext _context; + private readonly IFeedbackService _feedback; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; + + public InfoCommandGroup( + ICommandContext context, IFeedbackService feedback, + GuildDataService guildData, IDiscordRestGuildAPI guildApi, + IDiscordRestUserAPI userApi) + { + _context = context; + _guildData = guildData; + _feedback = feedback; + _guildApi = guildApi; + _userApi = userApi; + } + + /// + /// A slash command that shows information about user. + /// + /// + /// Information in the output: + /// + /// Display name + /// Discord user since + /// Guild nickname + /// Guild member since + /// Nitro booster since + /// Guild roles + /// Active mute information + /// Active ban information + /// Is on guild status + /// + /// + /// The user to show info about. + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("userinfo")] + [DiscordDefaultDMPermission(false)] + [Description("Shows info about user")] + [UsedImplicitly] + public async Task ExecuteUserInfoAsync( + [Description("User to show info about")] + IUser? target = null) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return ResultExtensions.FromError(botResult); + } + + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) + { + return ResultExtensions.FromError(executorResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await ShowUserInfoAsync(target ?? executor, bot, data, guildId, CancellationToken); + } + + private async Task ShowUserInfoAsync( + IUser target, IUser bot, GuildData data, Snowflake guildId, CancellationToken ct = default) + { + var builder = new StringBuilder().AppendLine($"### <@{target.ID}>"); + + if (target.GlobalName.IsDefined(out var globalName)) + { + builder.AppendBulletPointLine(Messages.UserInfoDisplayName) + .AppendLine(Markdown.InlineCode(globalName)); + } + + builder.AppendBulletPointLine(Messages.UserInfoDiscordUserSince) + .AppendLine(Markdown.Timestamp(target.ID.Timestamp)); + + var memberData = data.GetOrCreateMemberData(target.ID); + + var embedColor = ColorsList.Cyan; + + var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, ct); + DateTimeOffset? communicationDisabledUntil = null; + if (guildMemberResult.IsDefined(out var guildMember)) + { + communicationDisabledUntil = guildMember.CommunicationDisabledUntil.OrDefault(null); + + embedColor = AppendGuildInformation(embedColor, guildMember, builder); + } + + var wasMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || + communicationDisabledUntil is not null; + var wasBanned = memberData.BannedUntil is not null; + var wasKicked = memberData.Kicked; + + if (wasMuted || wasBanned || wasKicked) + { + builder.Append("### ") + .AppendLine(Markdown.Bold(Messages.UserInfoPunishments)); + + embedColor = AppendPunishmentsInformation(wasMuted, wasKicked, wasBanned, memberData, + builder, embedColor, communicationDisabledUntil); + } + + if (!guildMemberResult.IsSuccess && !wasBanned) + { + builder.Append("### ") + .AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild)); + + embedColor = ColorsList.Default; + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.InformationAbout, target.GetTag()), bot) + .WithDescription(builder.ToString()) + .WithColour(embedColor) + .WithLargeUserAvatar(target) + .WithFooter($"ID: {target.ID.ToString()}") + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + + private static Color AppendPunishmentsInformation(bool wasMuted, bool wasKicked, bool wasBanned, + MemberData memberData, StringBuilder builder, Color embedColor, DateTimeOffset? communicationDisabledUntil) + { + if (wasMuted) + { + AppendMuteInformation(memberData, communicationDisabledUntil, builder); + embedColor = ColorsList.Red; + } + + if (wasKicked) + { + builder.AppendBulletPointLine(Messages.UserInfoKicked); + } + + if (wasBanned) + { + AppendBanInformation(memberData, builder); + embedColor = ColorsList.Black; + } + + return embedColor; + } + + private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder) + { + if (guildMember.Nickname.IsDefined(out var nickname)) + { + builder.AppendBulletPointLine(Messages.UserInfoGuildNickname) + .AppendLine(Markdown.InlineCode(nickname)); + } + + builder.AppendBulletPointLine(Messages.UserInfoGuildMemberSince) + .AppendLine(Markdown.Timestamp(guildMember.JoinedAt)); + + if (guildMember.PremiumSince.IsDefined(out var premiumSince)) + { + builder.AppendBulletPointLine(Messages.UserInfoGuildMemberPremiumSince) + .AppendLine(Markdown.Timestamp(premiumSince.Value)); + color = ColorsList.Magenta; + } + + if (guildMember.Roles.Count > 0) + { + builder.AppendBulletPointLine(Messages.UserInfoGuildRoles); + for (var i = 0; i < guildMember.Roles.Count - 1; i++) + { + builder.Append($"<@&{guildMember.Roles[i]}>, "); + } + + builder.AppendLine($"<@&{guildMember.Roles[^1]}>"); + } + + return color; + } + + private static void AppendBanInformation(MemberData memberData, StringBuilder builder) + { + if (memberData.BannedUntil < DateTimeOffset.MaxValue) + { + builder.AppendBulletPointLine(Messages.UserInfoBanned) + .AppendSubBulletPointLine(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.BannedUntil.Value))); + return; + } + + builder.AppendBulletPointLine(Messages.UserInfoBannedPermanently); + } + + private static void AppendMuteInformation( + MemberData memberData, DateTimeOffset? communicationDisabledUntil, StringBuilder builder) + { + builder.AppendBulletPointLine(Messages.UserInfoMuted); + if (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) + { + builder.AppendSubBulletPointLine(Messages.UserInfoMutedByMuteRole) + .AppendSubBulletPointLine(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.MutedUntil.Value))); + } + + if (communicationDisabledUntil is not null) + { + builder.AppendSubBulletPointLine(Messages.UserInfoMutedByTimeout) + .AppendSubBulletPointLine(string.Format( + Messages.DescriptionActionExpiresAt, Markdown.Timestamp(communicationDisabledUntil.Value))); + } + } + + /// + /// A slash command that shows guild information. + /// + /// + /// Information in the output: + /// + /// Guild description + /// Creation date + /// Guild's language + /// Guild's owner + /// Boost level + /// Boost count + /// + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("guildinfo")] + [DiscordDefaultDMPermission(false)] + [Description("Shows info about current guild")] + [UsedImplicitly] + public async Task ExecuteGuildInfoAsync() + { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return ResultExtensions.FromError(botResult); + } + + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + { + return ResultExtensions.FromError(guildResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await ShowGuildInfoAsync(bot, guild, CancellationToken); + } + + private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct) + { + var description = new StringBuilder().AppendLine($"## {guild.Name}"); + + if (guild.Description is not null) + { + description.AppendBulletPointLine(Messages.GuildInfoDescription) + .AppendLine(Markdown.InlineCode(guild.Description)); + } + + description.AppendBulletPointLine(Messages.GuildInfoCreatedAt) + .AppendLine(Markdown.Timestamp(guild.ID.Timestamp)) + .AppendBulletPointLine(Messages.GuildInfoOwner) + .AppendLine(Mention.User(guild.OwnerID)); + + var embedColor = ColorsList.Cyan; + + if (guild.PremiumTier > PremiumTier.None) + { + description.Append("### ").AppendLine(Messages.GuildInfoServerBoost) + .AppendBulletPoint(Messages.GuildInfoBoostTier) + .Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumTier.ToString())) + .AppendBulletPoint(Messages.GuildInfoBoostCount) + .Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumSubscriptionCount.ToString())); + embedColor = ColorsList.Magenta; + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.InformationAbout, guild.Name), bot) + .WithDescription(description.ToString()) + .WithColour(embedColor) + .WithLargeGuildIcon(guild) + .WithGuildBanner(guild) + .WithFooter($"ID: {guild.ID.ToString()}") + .Build(); + + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } +} diff --git a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs index 3e84527..6af3040 100644 --- a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using System.Drawing; using System.Text; using JetBrains.Annotations; using Remora.Commands.Attributes; @@ -11,7 +10,6 @@ using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; -using Remora.Rest.Core; using Remora.Results; using TeamOctolings.Octobot.Data; using TeamOctolings.Octobot.Extensions; @@ -21,313 +19,42 @@ using TeamOctolings.Octobot.Services; namespace TeamOctolings.Octobot.Commands; /// -/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp, /8ball. +/// Handles tool commands: /random, /timestamp, /8ball. /// [UsedImplicitly] public class ToolsCommandGroup : CommandGroup { + private static readonly TimestampStyle[] AllStyles = + [ + TimestampStyle.ShortDate, + TimestampStyle.LongDate, + TimestampStyle.ShortTime, + TimestampStyle.LongTime, + TimestampStyle.ShortDateTime, + TimestampStyle.LongDateTime, + TimestampStyle.RelativeTime + ]; + + private static readonly string[] AnswerTypes = + [ + "Positive", "Questionable", "Neutral", "Negative" + ]; + private readonly ICommandContext _context; private readonly IFeedbackService _feedback; - private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; public ToolsCommandGroup( ICommandContext context, IFeedbackService feedback, - GuildDataService guildData, IDiscordRestGuildAPI guildApi, - IDiscordRestUserAPI userApi) + GuildDataService guildData, IDiscordRestUserAPI userApi) { _context = context; _guildData = guildData; _feedback = feedback; - _guildApi = guildApi; _userApi = userApi; } - /// - /// A slash command that shows information about user. - /// - /// - /// Information in the output: - /// - /// Display name - /// Discord user since - /// Guild nickname - /// Guild member since - /// Nitro booster since - /// Guild roles - /// Active mute information - /// Active ban information - /// Is on guild status - /// - /// - /// The user to show info about. - /// - /// A feedback sending result which may or may not have succeeded. - /// - [Command("userinfo")] - [DiscordDefaultDMPermission(false)] - [Description("Shows info about user")] - [UsedImplicitly] - public async Task ExecuteUserInfoAsync( - [Description("User to show info about")] - IUser? target = null) - { - if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) - { - return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); - } - - var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!botResult.IsDefined(out var bot)) - { - return ResultExtensions.FromError(botResult); - } - - var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); - if (!executorResult.IsDefined(out var executor)) - { - return ResultExtensions.FromError(executorResult); - } - - var data = await _guildData.GetData(guildId, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(data.Settings); - - return await ShowUserInfoAsync(target ?? executor, bot, data, guildId, CancellationToken); - } - - private async Task ShowUserInfoAsync( - IUser target, IUser bot, GuildData data, Snowflake guildId, CancellationToken ct = default) - { - var builder = new StringBuilder().AppendLine($"### <@{target.ID}>"); - - if (target.GlobalName.IsDefined(out var globalName)) - { - builder.AppendBulletPointLine(Messages.UserInfoDisplayName) - .AppendLine(Markdown.InlineCode(globalName)); - } - - builder.AppendBulletPointLine(Messages.UserInfoDiscordUserSince) - .AppendLine(Markdown.Timestamp(target.ID.Timestamp)); - - var memberData = data.GetOrCreateMemberData(target.ID); - - var embedColor = ColorsList.Cyan; - - var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, ct); - DateTimeOffset? communicationDisabledUntil = null; - if (guildMemberResult.IsDefined(out var guildMember)) - { - communicationDisabledUntil = guildMember.CommunicationDisabledUntil.OrDefault(null); - - embedColor = AppendGuildInformation(embedColor, guildMember, builder); - } - - var wasMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || - communicationDisabledUntil is not null; - var wasBanned = memberData.BannedUntil is not null; - var wasKicked = memberData.Kicked; - - if (wasMuted || wasBanned || wasKicked) - { - builder.Append("### ") - .AppendLine(Markdown.Bold(Messages.UserInfoPunishments)); - - embedColor = AppendPunishmentsInformation(wasMuted, wasKicked, wasBanned, memberData, - builder, embedColor, communicationDisabledUntil); - } - - if (!guildMemberResult.IsSuccess && !wasBanned) - { - builder.Append("### ") - .AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild)); - - embedColor = ColorsList.Default; - } - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.InformationAbout, target.GetTag()), bot) - .WithDescription(builder.ToString()) - .WithColour(embedColor) - .WithLargeUserAvatar(target) - .WithFooter($"ID: {target.ID.ToString()}") - .Build(); - - return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); - } - - private static Color AppendPunishmentsInformation(bool wasMuted, bool wasKicked, bool wasBanned, - MemberData memberData, StringBuilder builder, Color embedColor, DateTimeOffset? communicationDisabledUntil) - { - if (wasMuted) - { - AppendMuteInformation(memberData, communicationDisabledUntil, builder); - embedColor = ColorsList.Red; - } - - if (wasKicked) - { - builder.AppendBulletPointLine(Messages.UserInfoKicked); - } - - if (wasBanned) - { - AppendBanInformation(memberData, builder); - embedColor = ColorsList.Black; - } - - return embedColor; - } - - private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder) - { - if (guildMember.Nickname.IsDefined(out var nickname)) - { - builder.AppendBulletPointLine(Messages.UserInfoGuildNickname) - .AppendLine(Markdown.InlineCode(nickname)); - } - - builder.AppendBulletPointLine(Messages.UserInfoGuildMemberSince) - .AppendLine(Markdown.Timestamp(guildMember.JoinedAt)); - - if (guildMember.PremiumSince.IsDefined(out var premiumSince)) - { - builder.AppendBulletPointLine(Messages.UserInfoGuildMemberPremiumSince) - .AppendLine(Markdown.Timestamp(premiumSince.Value)); - color = ColorsList.Magenta; - } - - if (guildMember.Roles.Count > 0) - { - builder.AppendBulletPointLine(Messages.UserInfoGuildRoles); - for (var i = 0; i < guildMember.Roles.Count - 1; i++) - { - builder.Append($"<@&{guildMember.Roles[i]}>, "); - } - - builder.AppendLine($"<@&{guildMember.Roles[^1]}>"); - } - - return color; - } - - private static void AppendBanInformation(MemberData memberData, StringBuilder builder) - { - if (memberData.BannedUntil < DateTimeOffset.MaxValue) - { - builder.AppendBulletPointLine(Messages.UserInfoBanned) - .AppendSubBulletPointLine(string.Format( - Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.BannedUntil.Value))); - return; - } - - builder.AppendBulletPointLine(Messages.UserInfoBannedPermanently); - } - - private static void AppendMuteInformation( - MemberData memberData, DateTimeOffset? communicationDisabledUntil, StringBuilder builder) - { - builder.AppendBulletPointLine(Messages.UserInfoMuted); - if (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) - { - builder.AppendSubBulletPointLine(Messages.UserInfoMutedByMuteRole) - .AppendSubBulletPointLine(string.Format( - Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.MutedUntil.Value))); - } - - if (communicationDisabledUntil is not null) - { - builder.AppendSubBulletPointLine(Messages.UserInfoMutedByTimeout) - .AppendSubBulletPointLine(string.Format( - Messages.DescriptionActionExpiresAt, Markdown.Timestamp(communicationDisabledUntil.Value))); - } - } - - /// - /// A slash command that shows guild information. - /// - /// - /// Information in the output: - /// - /// Guild description - /// Creation date - /// Guild's language - /// Guild's owner - /// Boost level - /// Boost count - /// - /// - /// - /// A feedback sending result which may or may not have succeeded. - /// - [Command("guildinfo")] - [DiscordDefaultDMPermission(false)] - [Description("Shows info about current guild")] - [UsedImplicitly] - public async Task ExecuteGuildInfoAsync() - { - if (!_context.TryGetContextIDs(out var guildId, out _, out _)) - { - return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); - } - - var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!botResult.IsDefined(out var bot)) - { - return ResultExtensions.FromError(botResult); - } - - var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); - if (!guildResult.IsDefined(out var guild)) - { - return ResultExtensions.FromError(guildResult); - } - - var data = await _guildData.GetData(guildId, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(data.Settings); - - return await ShowGuildInfoAsync(bot, guild, CancellationToken); - } - - private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct) - { - var description = new StringBuilder().AppendLine($"## {guild.Name}"); - - if (guild.Description is not null) - { - description.AppendBulletPointLine(Messages.GuildInfoDescription) - .AppendLine(Markdown.InlineCode(guild.Description)); - } - - description.AppendBulletPointLine(Messages.GuildInfoCreatedAt) - .AppendLine(Markdown.Timestamp(guild.ID.Timestamp)) - .AppendBulletPointLine(Messages.GuildInfoOwner) - .AppendLine(Mention.User(guild.OwnerID)); - - var embedColor = ColorsList.Cyan; - - if (guild.PremiumTier > PremiumTier.None) - { - description.Append("### ").AppendLine(Messages.GuildInfoServerBoost) - .AppendBulletPoint(Messages.GuildInfoBoostTier) - .Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumTier.ToString())) - .AppendBulletPoint(Messages.GuildInfoBoostCount) - .Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumSubscriptionCount.ToString())); - embedColor = ColorsList.Magenta; - } - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.InformationAbout, guild.Name), bot) - .WithDescription(description.ToString()) - .WithColour(embedColor) - .WithLargeGuildIcon(guild) - .WithGuildBanner(guild) - .WithFooter($"ID: {guild.ID.ToString()}") - .Build(); - - return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); - } - /// /// A slash command that generates a random number using maximum and minimum numbers. /// @@ -405,17 +132,6 @@ public class ToolsCommandGroup : CommandGroup return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } - private static readonly TimestampStyle[] AllStyles = - [ - TimestampStyle.ShortDate, - TimestampStyle.LongDate, - TimestampStyle.ShortTime, - TimestampStyle.LongTime, - TimestampStyle.ShortDateTime, - TimestampStyle.LongDateTime, - TimestampStyle.RelativeTime - ]; - /// /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. /// @@ -533,11 +249,6 @@ public class ToolsCommandGroup : CommandGroup return await AnswerEightBallAsync(bot, CancellationToken); } - private static readonly string[] AnswerTypes = - [ - "Positive", "Questionable", "Neutral", "Negative" - ]; - private Task AnswerEightBallAsync(IUser bot, CancellationToken ct) { var typeNumber = Random.Shared.Next(0, 4); From d03e2504fc0640f8dd2d3a4b335ca9718af75c79 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 23 May 2024 17:47:51 +0500 Subject: [PATCH 299/329] Seal implicitly used classes (#309) Apparently the `[UsedImplicitly]` annotation suppresses the "Class has no inheritors and can be marked sealed" warning. Cool to know. Signed-off-by: Octol1ttle --- TeamOctolings.Octobot/Commands/AboutCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Commands/BanCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/ClearCommandGroup.cs | 2 +- .../Commands/Events/ErrorLoggingPostExecutionEvent.cs | 2 +- .../Commands/Events/LoggingPreparationErrorEvent.cs | 2 +- TeamOctolings.Octobot/Commands/InfoCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/KickCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/MuteCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/PingCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/RemindCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs | 2 +- TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs | 2 +- .../Responders/GuildMemberJoinedResponder.cs | 2 +- TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs | 2 +- TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs | 2 +- TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs | 2 +- TeamOctolings.Octobot/Responders/MessageEditedResponder.cs | 2 +- TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs | 2 +- 19 files changed, 21 insertions(+), 21 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs index 9f05af3..dbb8b12 100644 --- a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs @@ -25,7 +25,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles the command to show information about this bot: /about. /// [UsedImplicitly] -public class AboutCommandGroup : CommandGroup +public sealed class AboutCommandGroup : CommandGroup { private static readonly (string Username, Snowflake Id)[] Developers = [ @@ -36,9 +36,9 @@ public class AboutCommandGroup : CommandGroup private readonly ICommandContext _context; private readonly IFeedbackService _feedback; + private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; - private readonly IDiscordRestGuildAPI _guildApi; public AboutCommandGroup( ICommandContext context, GuildDataService guildData, diff --git a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs index 8d90286..69be80f 100644 --- a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs @@ -26,7 +26,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles commands related to ban management: /ban and /unban. /// [UsedImplicitly] -public class BanCommandGroup : CommandGroup +public sealed class BanCommandGroup : CommandGroup { private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; diff --git a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs index 8a8cb2f..38d864b 100644 --- a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs @@ -23,7 +23,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles the command to clear messages in a channel: /clear. /// [UsedImplicitly] -public class ClearCommandGroup : CommandGroup +public sealed class ClearCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; diff --git a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 7ffc4fe..7409d3b 100644 --- a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -18,7 +18,7 @@ namespace TeamOctolings.Octobot.Commands.Events; /// Handles error logging for slash command groups. /// [UsedImplicitly] -public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent +public sealed class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { private readonly IFeedbackService _feedback; private readonly ILogger _logger; diff --git a/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs b/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs index 10a6a1f..9e69a7f 100644 --- a/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs @@ -11,7 +11,7 @@ namespace TeamOctolings.Octobot.Commands.Events; /// Handles error logging for slash commands that couldn't be successfully prepared. /// [UsedImplicitly] -public class LoggingPreparationErrorEvent : IPreparationErrorEvent +public sealed class LoggingPreparationErrorEvent : IPreparationErrorEvent { private readonly ILogger _logger; diff --git a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs index 65ddd53..d7798f3 100644 --- a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs @@ -23,7 +23,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles info commands: /userinfo, /guildinfo. /// [UsedImplicitly] -public class InfoCommandGroup : CommandGroup +public sealed class InfoCommandGroup : CommandGroup { private readonly ICommandContext _context; private readonly IFeedbackService _feedback; diff --git a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs index 4252232..a8fea2a 100644 --- a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs @@ -22,7 +22,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles the command to kick members of a guild: /kick. /// [UsedImplicitly] -public class KickCommandGroup : CommandGroup +public sealed class KickCommandGroup : CommandGroup { private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; diff --git a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs index 8e449f7..282afe8 100644 --- a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs @@ -26,7 +26,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles commands related to mute management: /mute and /unmute. /// [UsedImplicitly] -public class MuteCommandGroup : CommandGroup +public sealed class MuteCommandGroup : CommandGroup { private readonly AccessControlService _access; private readonly ICommandContext _context; diff --git a/TeamOctolings.Octobot/Commands/PingCommandGroup.cs b/TeamOctolings.Octobot/Commands/PingCommandGroup.cs index 70b9f23..01a1ee2 100644 --- a/TeamOctolings.Octobot/Commands/PingCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/PingCommandGroup.cs @@ -22,7 +22,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping /// [UsedImplicitly] -public class PingCommandGroup : CommandGroup +public sealed class PingCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly DiscordGatewayClient _client; diff --git a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs index f40ba6b..bf59d67 100644 --- a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs @@ -25,13 +25,13 @@ namespace TeamOctolings.Octobot.Commands; /// Handles commands to manage reminders: /remind, /listremind, /delremind /// [UsedImplicitly] -public class RemindCommandGroup : CommandGroup +public sealed class RemindCommandGroup : CommandGroup { private readonly IInteractionCommandContext _context; private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; - private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestInteractionAPI _interactionApi; + private readonly IDiscordRestUserAPI _userApi; public RemindCommandGroup( IInteractionCommandContext context, GuildDataService guildData, IFeedbackService feedback, diff --git a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs index 56584bf..0acaa88 100644 --- a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs @@ -26,7 +26,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles the commands to list and modify per-guild settings: /settings and /settings list. /// [UsedImplicitly] -public class SettingsCommandGroup : CommandGroup +public sealed class SettingsCommandGroup : CommandGroup { /// /// Represents all options as an array of objects implementing . diff --git a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs index 6af3040..b4c3488 100644 --- a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs @@ -22,7 +22,7 @@ namespace TeamOctolings.Octobot.Commands; /// Handles tool commands: /random, /timestamp, /8ball. /// [UsedImplicitly] -public class ToolsCommandGroup : CommandGroup +public sealed class ToolsCommandGroup : CommandGroup { private static readonly TimestampStyle[] AllStyles = [ diff --git a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs index 0c71a06..cebb1ea 100644 --- a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs @@ -19,7 +19,7 @@ namespace TeamOctolings.Octobot.Responders; /// has enabled /// [UsedImplicitly] -public class GuildLoadedResponder : IResponder +public sealed class GuildLoadedResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; private readonly GuildDataService _guildData; diff --git a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs index 6964fe7..c1f1da0 100644 --- a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs @@ -18,7 +18,7 @@ namespace TeamOctolings.Octobot.Responders; /// /// [UsedImplicitly] -public class GuildMemberJoinedResponder : IResponder +public sealed class GuildMemberJoinedResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildAPI _guildApi; diff --git a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs index 4f6150c..9774899 100644 --- a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs @@ -15,7 +15,7 @@ namespace TeamOctolings.Octobot.Responders; /// /// [UsedImplicitly] -public class GuildMemberLeftResponder : IResponder +public sealed class GuildMemberLeftResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildAPI _guildApi; diff --git a/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs index a4680d4..c73c134 100644 --- a/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs @@ -12,7 +12,7 @@ namespace TeamOctolings.Octobot.Responders; /// Handles removing guild ID from if the guild becomes unavailable. /// [UsedImplicitly] -public class GuildUnloadedResponder : IResponder +public sealed class GuildUnloadedResponder : IResponder { private readonly GuildDataService _guildData; private readonly ILogger _logger; diff --git a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs index 6b09b8d..88a8de2 100644 --- a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs @@ -18,7 +18,7 @@ namespace TeamOctolings.Octobot.Responders; /// to a guild's if one is set. /// [UsedImplicitly] -public class MessageDeletedResponder : IResponder +public sealed class MessageDeletedResponder : IResponder { private readonly IDiscordRestAuditLogAPI _auditLogApi; private readonly IDiscordRestChannelAPI _channelApi; diff --git a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs index 6134214..2968562 100644 --- a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs @@ -20,7 +20,7 @@ namespace TeamOctolings.Octobot.Responders; /// to a guild's if one is set. /// [UsedImplicitly] -public class MessageEditedResponder : IResponder +public sealed class MessageEditedResponder : IResponder { private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; diff --git a/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs b/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs index 34a8f5c..24d53a5 100644 --- a/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs @@ -11,7 +11,7 @@ namespace TeamOctolings.Octobot.Responders; /// Handles sending replies to easter egg messages. /// [UsedImplicitly] -public class MessageCreateResponder : IResponder +public sealed class MessageCreateResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; From ea9302e1858807d1a79293ae7e9ed43773373329 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 23 May 2024 18:21:52 +0500 Subject: [PATCH 300/329] Use MemberData roles when checking permissions & interactions (#312) Closes #311 This change fixes unexpected behavior when a member's Discord roles get desynchronized with their MemberData roles (e.g. when a member gets role-muted). In addition this results in less API requests being made when there are cache misses (commands should execute faster) --------- Signed-off-by: Octol1ttle --- .../Services/AccessControlService.cs | 57 ++++++------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/TeamOctolings.Octobot/Services/AccessControlService.cs b/TeamOctolings.Octobot/Services/AccessControlService.cs index b5b98ea..d39c9e5 100644 --- a/TeamOctolings.Octobot/Services/AccessControlService.cs +++ b/TeamOctolings.Octobot/Services/AccessControlService.cs @@ -20,18 +20,17 @@ public sealed class AccessControlService _userApi = userApi; } - private static bool CheckPermission(IEnumerable roles, GuildData data, Snowflake memberId, - IGuildMember member, + private static bool CheckPermission(IEnumerable roles, GuildData data, MemberData memberData, DiscordPermission permission) { var moderatorRole = GuildSettings.ModeratorRole.Get(data.Settings); - if (!moderatorRole.Empty() && data.GetOrCreateMemberData(memberId).Roles.Contains(moderatorRole.Value)) + if (!moderatorRole.Empty() && memberData.Roles.Contains(moderatorRole.Value)) { return true; } return roles - .Where(r => member.Roles.Contains(r.ID)) + .Where(r => memberData.Roles.Contains(r.ID.Value)) .Any(r => r.Permissions.HasPermission(permission) ); @@ -80,38 +79,23 @@ public sealed class AccessControlService return Result.FromError(botResult); } - var botMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); - if (!botMemberResult.IsDefined(out var botMember)) - { - return Result.FromError(botMemberResult); - } - - var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); - if (!targetMemberResult.IsDefined(out var targetMember)) - { - return Result.FromSuccess(null); - } - var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); if (!rolesResult.IsDefined(out var roles)) { return Result.FromError(rolesResult); } + var data = await _data.GetData(guildId, ct); + var targetData = data.GetOrCreateMemberData(targetId); + var botData = data.GetOrCreateMemberData(bot.ID); + if (interacterId is null) { - return CheckInteractions(action, guild, roles, targetMember, botMember, botMember); + return CheckInteractions(action, guild, roles, targetData, botData, botData); } - var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); - if (!interacterResult.IsDefined(out var interacter)) - { - return Result.FromError(interacterResult); - } - - var data = await _data.GetData(guildId, ct); - - var hasPermission = CheckPermission(roles, data, interacterId.Value, interacter, + var interacterData = data.GetOrCreateMemberData(interacterId.Value); + var hasPermission = CheckPermission(roles, data, interacterData, action switch { "Ban" => DiscordPermission.BanMembers, @@ -121,31 +105,26 @@ public sealed class AccessControlService }); return hasPermission - ? CheckInteractions(action, guild, roles, targetMember, botMember, interacter) + ? CheckInteractions(action, guild, roles, targetData, botData, interacterData) : Result.FromSuccess($"UserCannot{action}Members".Localized()); } private static Result CheckInteractions( - string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember botMember, - IGuildMember interacter) + string action, IGuild guild, IReadOnlyList roles, MemberData targetData, MemberData botData, + MemberData interacterData) { - if (!targetMember.User.IsDefined(out var targetUser)) - { - return new ArgumentNullError(nameof(targetMember.User)); - } - - if (botMember.User == targetMember.User) + if (botData.Id == targetData.Id) { return Result.FromSuccess($"UserCannot{action}Bot".Localized()); } - if (targetUser.ID == guild.OwnerID) + if (targetData.Id == guild.OwnerID) { return Result.FromSuccess($"UserCannot{action}Owner".Localized()); } - var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); - var botRoles = roles.Where(r => botMember.Roles.Contains(r.ID)); + var targetRoles = roles.Where(r => targetData.Roles.Contains(r.ID.Value)).ToList(); + var botRoles = roles.Where(r => botData.Roles.Contains(r.ID.Value)); var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); if (targetBotRoleDiff >= 0) @@ -153,7 +132,7 @@ public sealed class AccessControlService return Result.FromSuccess($"BotCannot{action}Target".Localized()); } - var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); + var interacterRoles = roles.Where(r => interacterData.Roles.Contains(r.ID.Value)); var targetInteracterRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); return targetInteracterRoleDiff < 0 From daef4f1d48874b9f2cec25fad70414061f46aa0c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 30 May 2024 15:32:27 +0500 Subject: [PATCH 301/329] Upgrade NuGet dependencies (#313) Signed-off-by: Octol1ttle --- .../Extensions/ChannelApiExtensions.cs | 7 ++++--- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs b/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs index 2767f96..82f8889 100644 --- a/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs @@ -12,11 +12,12 @@ public static class ChannelApiExtensions public static async Task CreateMessageWithEmbedResultAsync(this IDiscordRestChannelAPI channelApi, Snowflake channelId, Optional message = default, Optional nonce = default, Optional isTextToSpeech = default, Optional> embedResult = default, - Optional allowedMentions = default, Optional messageRefenence = default, + Optional allowedMentions = default, Optional messageReference = default, Optional> components = default, Optional> stickerIds = default, Optional>> attachments = default, - Optional flags = default, CancellationToken ct = default) + Optional flags = default, Optional enforceNonce = default, + Optional poll = default, CancellationToken ct = default) { if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed)) { @@ -24,6 +25,6 @@ public static class ChannelApiExtensions } return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed }, - allowedMentions, messageRefenence, components, stickerIds, attachments, flags, ct); + allowedMentions, messageReference, components, stickerIds, attachments, flags, enforceNonce, poll, ct); } } diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 9c3d58b..19e37f9 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -22,16 +22,16 @@ - + - - - - + + + + From 29a1eb986987d9a633b1acf21fc8b74598e3f04e Mon Sep 17 00:00:00 2001 From: Fakeintxsh <95250141+mctaylors@users.noreply.github.com> Date: Fri, 31 May 2024 21:46:12 +0500 Subject: [PATCH 302/329] Add description to /clear's author to clarify its functionality (#315) Closes #314 What? The title speaks for itself. --- TeamOctolings.Octobot/Commands/ClearCommandGroup.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs index 38d864b..7c1b516 100644 --- a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs @@ -64,6 +64,7 @@ public sealed class ClearCommandGroup : CommandGroup public async Task ExecuteClear( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] int amount, + [Description("Ignore messages except from the specified author")] IUser? author = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) From 2b0c4b62d3e8af97ff96215d6e6b05aa3f44b323 Mon Sep 17 00:00:00 2001 From: Fakeintxsh <95250141+mctaylors@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:28:06 +0300 Subject: [PATCH 303/329] README: Refer to the Octobot's Wiki in Building Octobot (#316) Signed-off-by: Fakeintxsh <95250141+mctaylors@users.noreply.github.com> --- docs/README.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index 7056857..ccc3b83 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,23 +15,16 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr * Reminding everyone about that new event you made * Renaming those annoying self-hoisting members * Log everything from joining the server to deleting messages -* Listen to music! +* Listen to Inkantation! *...a-a-and more!* ## Building Octobot -1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) -2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents! -3. Clone this repository and open `Octobot` folder. -``` -git clone https://github.com/TeamOctolings/Octobot -cd Octobot -``` -4. Run Octobot using `dotnet` with `BOT_TOKEN` variable. -``` -dotnet run BOT_TOKEN='ENTER_TOKEN_HERE' -``` +Check out the Octobot's Wiki for details. + +| [Windows](https://github.com/TeamOctolings/Octobot/wiki/Installing-Windows) | [Linux/macOS](https://github.com/TeamOctolings/Octobot/wiki/Installing-Unix) | +| --- | --- | ## Contributing From a953053f1d7ad7012f80d2bb338c7ee107e5bf9c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 22 Jun 2024 00:28:29 +0500 Subject: [PATCH 304/329] Handle audit log entries for message deletion being empty (#317) In order to determine who deleted a message, Octobot fetches the audit log with a filter on the action "Message Delete", gets the latest entry and uses its author if the timestamps roughly match. However, if the filter returns no entries (as in, no message deletions are present in the audit log), `Single()` will throw an exception with the message `Sequence contains no elements`. To fix this, this PR replaces `Single()` with `SingleOrDefault()` and adds a null-check on `auditLog` in the form of a pattern access Signed-off-by: Octol1ttle --- TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs index 88a8de2..f0e3d22 100644 --- a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs @@ -66,10 +66,10 @@ public sealed class MessageDeletedResponder : IResponder return ResultExtensions.FromError(auditLogResult); } - var auditLog = auditLogPage.AuditLogEntries.Single(); - var deleterResult = Result.FromSuccess(message.Author); - if (auditLog.UserID is not null + + var auditLog = auditLogPage.AuditLogEntries.SingleOrDefault(); + if (auditLog is { UserID: not null } && auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { From a0e7b3a6116c6ff44e1ea2877ffb33d35b931bf8 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 25 Jun 2024 15:09:45 +0500 Subject: [PATCH 305/329] Always default cancellation tokens (#319) This PR makes sure that a cancellation token is never *required* to use an `async` method. This does not affect user experience in any way, only code quality. --------- Signed-off-by: Octol1ttle --- TeamOctolings.Octobot/Commands/InfoCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/MuteCommandGroup.cs | 6 +++--- TeamOctolings.Octobot/Commands/RemindCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs | 6 +++--- .../Responders/GuildLoadedResponder.cs | 2 +- .../Responders/GuildMemberJoinedResponder.cs | 2 +- TeamOctolings.Octobot/Services/GuildDataService.cs | 4 ++-- .../Services/Update/MemberUpdateService.cs | 12 ++++++------ .../Services/Update/ScheduledEventUpdateService.cs | 12 ++++++------ TeamOctolings.Octobot/Utility.cs | 2 +- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs index d7798f3..f07b210 100644 --- a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs @@ -288,7 +288,7 @@ public sealed class InfoCommandGroup : CommandGroup return await ShowGuildInfoAsync(bot, guild, CancellationToken); } - private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct) + private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct = default) { var description = new StringBuilder().AppendLine($"## {guild.Name}"); diff --git a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs index 282afe8..46e8d84 100644 --- a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs @@ -170,7 +170,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task SelectMuteMethodAsync( IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, - IUser bot, DateTimeOffset until, CancellationToken ct) + IUser bot, DateTimeOffset until, CancellationToken ct = default) { var muteRole = GuildSettings.MuteRole.Get(data.Settings); @@ -186,7 +186,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task RoleMuteUserAsync( IUser executor, IUser target, string reason, Snowflake guildId, GuildData data, - DateTimeOffset until, Snowflake muteRole, CancellationToken ct) + DateTimeOffset until, Snowflake muteRole, CancellationToken ct = default) { var assignRoles = new List { muteRole }; var memberData = data.GetOrCreateMemberData(target.ID); @@ -208,7 +208,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task TimeoutUserAsync( IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, - IUser bot, DateTimeOffset until, CancellationToken ct) + IUser bot, DateTimeOffset until, CancellationToken ct = default) { if (duration.TotalDays >= 28) { diff --git a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs index bf59d67..be53ed7 100644 --- a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs @@ -78,7 +78,7 @@ public sealed class RemindCommandGroup : CommandGroup return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), guildId, executor, bot, CancellationToken); } - private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct) + private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct = default) { if (data.Reminders.Count == 0) { @@ -353,7 +353,7 @@ public sealed class RemindCommandGroup : CommandGroup } private Task DeleteReminderAsync(MemberData data, int index, IUser bot, - CancellationToken ct) + CancellationToken ct = default) { if (index >= data.Reminders.Count) { diff --git a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs index b4c3488..2936392 100644 --- a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs @@ -90,7 +90,7 @@ public sealed class ToolsCommandGroup : CommandGroup } private Task SendRandomNumberAsync(long first, long? secondNullable, - IUser executor, CancellationToken ct) + IUser executor, CancellationToken ct = default) { const long secondDefault = 0; var second = secondNullable ?? secondDefault; @@ -187,7 +187,7 @@ public sealed class ToolsCommandGroup : CommandGroup return await SendTimestampAsync(offset, executor, CancellationToken); } - private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct) + private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct = default) { var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds(); @@ -249,7 +249,7 @@ public sealed class ToolsCommandGroup : CommandGroup return await AnswerEightBallAsync(bot, CancellationToken); } - private Task AnswerEightBallAsync(IUser bot, CancellationToken ct) + private Task AnswerEightBallAsync(IUser bot, CancellationToken ct = default) { var typeNumber = Random.Shared.Next(0, 4); var embedColor = typeNumber switch diff --git a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs index cebb1ea..b420db2 100644 --- a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs @@ -94,7 +94,7 @@ public sealed class GuildLoadedResponder : IResponder GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, ct: ct); } - private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct) + private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct = default) { var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); if (!channelResult.IsDefined(out var channel)) diff --git a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs index c1f1da0..ae9f174 100644 --- a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs @@ -81,7 +81,7 @@ public sealed class GuildMemberJoinedResponder : IResponder } private async Task TryReturnRolesAsync( - JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct) + JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct = default) { if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { diff --git a/TeamOctolings.Octobot/Services/GuildDataService.cs b/TeamOctolings.Octobot/Services/GuildDataService.cs index 866ee08..a7af7c9 100644 --- a/TeamOctolings.Octobot/Services/GuildDataService.cs +++ b/TeamOctolings.Octobot/Services/GuildDataService.cs @@ -27,7 +27,7 @@ public sealed class GuildDataService : BackgroundService return SaveAsync(ct); } - private Task SaveAsync(CancellationToken ct) + private Task SaveAsync(CancellationToken ct = default) { var tasks = new List(); var datas = _datas.Values.ToArray(); @@ -44,7 +44,7 @@ public sealed class GuildDataService : BackgroundService return Task.WhenAll(tasks); } - private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct) + private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct = default) { var tempFilePath = path + ".tmp"; await using (var tempFileStream = File.Create(tempFilePath)) diff --git a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs index 51cf647..0c49c24 100644 --- a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs @@ -62,7 +62,7 @@ public sealed partial class MemberUpdateService : BackgroundService } } - private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct) + private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct = default) { var guildData = await _guildData.GetData(guildId, ct); var defaultRole = GuildSettings.DefaultRole.Get(guildData.Settings); @@ -79,7 +79,7 @@ public sealed partial class MemberUpdateService : BackgroundService private async Task TickMemberDataAsync(Snowflake guildId, GuildData guildData, Snowflake defaultRole, MemberData data, - CancellationToken ct) + CancellationToken ct = default) { var failedResults = new List(); var id = data.Id.ToSnowflake(); @@ -144,7 +144,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task TryAutoUnbanAsync( - Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) + Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default) { if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil) { @@ -169,7 +169,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task TryAutoUnmuteAsync( - Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) + Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default) { if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil) { @@ -188,7 +188,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, - CancellationToken ct) + CancellationToken ct = default) { var currentNickname = member.Nickname.IsDefined(out var nickname) ? nickname @@ -226,7 +226,7 @@ public sealed partial class MemberUpdateService : BackgroundService private static partial Regex IllegalChars(); private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData data, Snowflake guildId, - CancellationToken ct) + CancellationToken ct = default) { if (DateTimeOffset.UtcNow < reminder.At) { diff --git a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs index ce9c212..ef145aa 100644 --- a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs @@ -46,7 +46,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } } - private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct) + private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct = default) { var failedResults = new List(); var data = await _guildData.GetData(guildId, ct); @@ -133,7 +133,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task TickScheduledEventAsync( Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, - CancellationToken ct) + CancellationToken ct = default) { if (GuildSettings.AutoStartEvents.Get(data.Settings) && DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime @@ -160,7 +160,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task AutoStartEventAsync( - Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct) + Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct = default) { return (Result)await _eventApi.ModifyGuildScheduledEventAsync( guildId, scheduledEvent.ID, @@ -319,7 +319,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data, - CancellationToken ct) + CancellationToken ct = default) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { @@ -351,7 +351,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendScheduledEventCancelledMessage(ScheduledEventData eventData, GuildData data, - CancellationToken ct) + CancellationToken ct = default) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { @@ -405,7 +405,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendEarlyEventNotificationAsync( - IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { diff --git a/TeamOctolings.Octobot/Utility.cs b/TeamOctolings.Octobot/Utility.cs index 463212b..f337d93 100644 --- a/TeamOctolings.Octobot/Utility.cs +++ b/TeamOctolings.Octobot/Utility.cs @@ -125,7 +125,7 @@ public sealed class Utility } } - public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct) + public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct = default) { var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings); if (!privateFeedback.Empty()) From f1a3e6bd244dbc1926eca20a9d367eba8b7fb800 Mon Sep 17 00:00:00 2001 From: mctaylors Date: Wed, 3 Jul 2024 00:58:35 +0500 Subject: [PATCH 306/329] Initial commit Signed-off-by: mctaylors --- .gitignore | 1 + assets/css/fonts.css | 11 ++ assets/css/styles.css | 196 ++++++++++++++++++++++++++++++ assets/ico/octobot.ico | Bin 0 -> 120000 bytes assets/png/mem-cake-mole.png | Bin 0 -> 8853 bytes assets/png/mem-cake-octoling.png | Bin 0 -> 9680 bytes assets/png/mem-cake-sardinium.png | Bin 0 -> 8728 bytes assets/png/octobot-web-logo.png | Bin 0 -> 23219 bytes assets/png/tapes-transparent.png | Bin 0 -> 760509 bytes assets/svg/add-circle-white.svg | 1 + assets/svg/card-header.svg | 4 + assets/svg/github-mark-white.svg | 1 + assets/svg/splatoon.svg | 1 + assets/woff2/BlitzBold.woff2 | Bin 0 -> 68576 bytes assets/woff2/BlitzMain.woff2 | Bin 0 -> 59252 bytes index.html | 91 ++++++++++++++ 16 files changed, 306 insertions(+) create mode 100644 .gitignore create mode 100644 assets/css/fonts.css create mode 100644 assets/css/styles.css create mode 100644 assets/ico/octobot.ico create mode 100644 assets/png/mem-cake-mole.png create mode 100644 assets/png/mem-cake-octoling.png create mode 100644 assets/png/mem-cake-sardinium.png create mode 100644 assets/png/octobot-web-logo.png create mode 100644 assets/png/tapes-transparent.png create mode 100644 assets/svg/add-circle-white.svg create mode 100644 assets/svg/card-header.svg create mode 100644 assets/svg/github-mark-white.svg create mode 100644 assets/svg/splatoon.svg create mode 100644 assets/woff2/BlitzBold.woff2 create mode 100644 assets/woff2/BlitzMain.woff2 create mode 100644 index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/assets/css/fonts.css b/assets/css/fonts.css new file mode 100644 index 0000000..6d38cb2 --- /dev/null +++ b/assets/css/fonts.css @@ -0,0 +1,11 @@ +@font-face { + font-family: 'BlitzBold'; + font-weight: normal; + src: url(/assets/woff2/BlitzBold.woff2); +} + +@font-face { + font-family: 'BlitzMain'; + font-weight: normal; + src: url(/assets/woff2/BlitzMain.woff2); +} \ No newline at end of file diff --git a/assets/css/styles.css b/assets/css/styles.css new file mode 100644 index 0000000..bc415f3 --- /dev/null +++ b/assets/css/styles.css @@ -0,0 +1,196 @@ +/* + Octobot for Discord. Made by mctaylors. + Inspired by splatoon3.ink. +*/ + +@import url(fonts.css); + +:root { + color: #eee; + background-color: #000; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOIAAADiBAMAAAChPgbkAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAqUExURRcXF0dwTBUVFRYWFgAAABoaGhkZGRUVFRgYGAAAAAAAAB4eHiQkJBISEkRDNy8AAAAOdFJOUxYAGBcDEwoMFQUBEQcOBwHpdgAAAjdJREFUeNrt271NBDEUReGnjUlGIoGICCKkbYIKqIASqIEm6IGMDiiBjtj5XXvGz9k9Cde5dfTFvo6Xc6TnacjP/Xd+7/TbuRi3r52b752bXw/5xedecaCRMdDIGGjkpQgjL0UYORZZ5FhkkVMRRU5FFDkXSeRcJJFLEUQuRRC5FjnkWuSQWxFDbkUMeS1SyGuRQhZFCFkUIWRZZJBlkUFWRQRZFRFkXSSQdZFA7ooAclcEkPuiHrkv6pGHohx5KMqRx6IaeSyqkY2iGNkoipGtohbZKmqRzaIU2SxKke2iEtkuKpFJUYhMikJkVtQhs6IOmRZlyLQoQ+ZFFTIvqpCdogjZKYqQvaIGGW80Mh5pZJxoZBhppJFGGmmkkUYaaaSRRhpppJFGGmmkkUYaaeR/Rp5pZHzQyPikkXFHI2OgkTHQyBho5KUII8f3RxY5Flnk9MaKIqciipzfkUnkXCSRy1s5iFyKIHLdA3DItcght80DhtyKGPK666CQ1yKFLLYrELIoQshyn8MgyyKDrDZICLIqIsh6Z0Ug6yKB3G3JAOSuCCD3ezk9cl/UIw+bQDnyUJQjj7tHNfJYVCMb204xslEUI1v7VS2yVdQimxtdKbJZlCLbO2Qlsl1UIpOttRCZFIXIbE+uQ2ZFHTLdzMuQaVGGzP8FqJB5UYXs/H0QITtFEbL3v0OD7BU1yPihkXFDIwNHhpFGGmmkkUYaaaSRRhpppJFGGmmkkUYaaaSRRhoJIgNH/gHUGcb6oO4YfwAAAABJRU5ErkJggg=="); + font-family: BlitzMain, sans-serif; +} + +a, a:visited { + color: chartreuse; +} + +a:hover { + color: aquamarine; +} + +a:active { + color: darkcyan; +} + +a.alternative { + text-decoration: none; +} + +.highlight { + font-family: BlitzBold, sans-serif; + font-size: 32px; +} + +.header { + font-size: 24px; + padding: 16px; + position: fixed; + width: calc(100% - 48px); + z-index: 10; +} + +.header > .left { + float: left; +} + +.header > .right { + float: right; +} + +.header > .left img { + margin: 0 8px; + height: 64px; +} + +.header > .right .social img { + height: 32px; + width: 32px; + border: #999 1px solid; + border-radius: 16px; + background-color: #0009; + padding: 8px; + transition: 200ms; +} + +.header > .right .social img:hover { + border-color: #eee; +} + +.content { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 32px 64px; + padding: 100px 16px 80px; +} + +.content > .card { + mask-image: url("/assets/svg/card-header.svg"); + mask-size: 2000px auto; + mask-position: top; + background-image: url("/assets/png/tapes-transparent.png"); + background-size: contain; + width: 480px; + min-height: 520px; + padding: 48px 16px 8px; + border-radius: 16px; +} + +.content > .card.first { + background-color: #1bbeab; /* Splatoon 3 TurquoisePink Alpha */ + rotate: -2deg; +} + +.content > .card.second { + background-color: #c43a6e; /* Splatoon 3 TurquoisePink Bravo */ + rotate: 2deg; +} + +.content > .card * { + text-align: left; +} + +.content > .card span { + line-height: 1em; + filter: drop-shadow(1px 1px #000); +} + +.content > .card > .title { + margin: 8px 0; +} + +.content > .card > .title > * { + vertical-align: middle; +} + +.content > .card > .title > span { + font-size: 24px; +} + +.content > .card > .title > img { + height: 32px; + width: 32px; +} + +.content > .card > .frame { + padding: 4px 8px 8px; + border-radius: 8px; + background-color: #0009; + backdrop-filter: blur(4px); +} + +.content > .card > .frame > ul { + padding: 0 0 0 24px; +} + +.invite { + margin-top: 8px; + padding: 12px 0; + width: 100%; + font-family: BlitzBold, sans-serif; + font-size: 20px; + color: white; + background-color: #4d5058; + border-radius: 4px; + border: 0; + display: flex; + justify-content: center; + gap: 8px; + transition: 200ms; +} + +.invite:hover { + background-color: #6d6f78; + cursor: pointer; +} + +.invite:active { + background-color: #80848e; +} + +.invite > img { + height: 24px; + width: 24px; +} + +.invite > span { + filter: none !important; +} + +.invite > * { + vertical-align: middle; +} + +.footer { + position: fixed; + left: 0; + bottom: 0; + width: 100%; + padding: 8px; + color: #999; + font-size: 14px; + text-align: center; + background-color: #0009; + backdrop-filter: blur(4px); +} + +.footer img { + vertical-align: sub; +} + +.splatoon { + height: 24px; + filter: brightness(75%); +} \ No newline at end of file diff --git a/assets/ico/octobot.ico b/assets/ico/octobot.ico new file mode 100644 index 0000000000000000000000000000000000000000..147b71659fea572ce955ba6efb8dc7314f893caf GIT binary patch literal 120000 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 RC7>g9JYD@<);T3K0RUua%(Vaj literal 0 HcmV?d00001 diff --git a/assets/png/mem-cake-mole.png b/assets/png/mem-cake-mole.png new file mode 100644 index 0000000000000000000000000000000000000000..6c7c2447472a842563aa98e4e3d8480ce8b73907 GIT binary patch literal 8853 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4mJh`hQoG=rx_TOVmw_OLn`LHom*KU61wlZ z{qkMbFLmbj=BVf>s=SF2a}5&|b&UydocnsmxffrxditzhY5cP4GqSRJZ1%3NugiG4 zr08BIEiMh^j)K&Qt`3KUzD;nLq?Vmude45e7b88Pj`EB!6rS#m&;N`z+UhC{% z+cITRUP-Km?A?aE5}jR~7mi-^ZP4q>V=5^u5nj-}cs4_<%w3ijk6x%bh`XCJ?y?|4 z$r{syz7@zyGMaHvIWm9Qf*%t+SV-QOj9+!HL?)Hiu*oL|kFZjIUE{nwp-o{X3?)$on| zq*L#hU*>3^Nc-Pd@|ZXGSn$==Od(u~@0jC)MM4_ntv{Z7sO}7MB{TnI<(3~D5}Ego zRW9|)doQ$ZXNK%^-y4ow&fZ>99TIjsa{pHO)7N^7GR@)>+ismf!#3I(z%lC1(!q z3D*O;w6v4abMlmYua)-}RRw;}tJrk*_9c-QzyD0R%F3ZAmhh@(!CfnP1>K!Z&*lHf zubcI+e&I}~J1qj{j9=u+ymxO|ZfUWUV}bOq_l(PCd|uvn|6bnQ^~cw*j~AQ9;-sojpI=rDV(UlV7LJDfp0_sOGBLIVJSSCG+1F&wX#m zoyjjyT@vkN`yt+2L*q=s`-NL}y>|7=bGo(SyH4Em(A3oG*?0DBJErN(VDHDSXQ+P8 zqT0B*xZiB&rpZ&w^mackbDkAGb%(9k;cFW=Zn#4QbKQcR{=?JoS*-YW{vo|!WxchF_>gnUs7xj4`cyY}yrXAlVnJQ*-W-eyP z%MLu6+u-NluxDfO$Db;zPWc9LwHzpjss3nnc;}G{1ts}!2H9-0uR5iM#%zA&%pi8+ zZSB3n=icNeBf?Vu-4Mh_^M~#`$-+*J8i?X zRxfUT?(Qvgv)X_+-BqWWf7!=Be?A|I+g|b1<68df;}<8XoYGF3b@jn(y}#FHD2P0K zVQ9hnBE+dNI91SswU>o$j%ixD$HBeqmm~IS_HUhCd^2pq(dzSiKv_!t{*H#D+U^Rq z%iQ^rmBS))pP#djV{SP9`tdKx`$oTCP0hJ;uU-7Pzw*UE-C^v1ky-E(4-FWe3+ z{`4T^fu_mh%X~*yZeaVtudvZl{MVIT$6rs3xT)34*`smRwIn7iLBwM1)3uqFeymJ$ z?%v_Po2Q-EZ+1e}tavrYgn2=ZRbAUmLta%LmTsQWax6wP)YaFQ;^#J{Mohy-akv+Lk4jHy&?h z>Yj2@ch!oi7cw~?gl;uIyQ;(Z%geC+C3dPCCrRvXe<2}mHk-Zwc>Wsog~w74Ki(Mh zc+V51ZS~IP%B!@kTAT%>5stOU~?0Rt!FOLvYGh_G?0` z&3Rb@%?oCoRXdZ-vAJ`W!m)J`i*y~@UKKxWGSK9>WnNaiT-jm&M*bK6@0lK*N!?_8 zs^a_1>5Lm@Y&Gru@pn;9Tw46zNk6-lPNy(6$v9sXdKR;H&X0(2k9o@Lt`*dGFA_>F ze_V2`=wWWT^Nx}k+r+RhGc%taIeyJ- z?WQV$JQGF_4{6I#ubuu%^BIgBf9rm!9!!uH6myJy!&uy?98)xEjeeRIcN zezVJSQh)ut|6ynCcg9qP+*I!yD|?*&vh48W_;~%m+OI#uSZ+$Dp4piv`PfY~VcO%8 zpxaj!zMPHSmd3IE-{+VvZ`snH%btJSwdZ$>KsbYR{U;UXeV?m&Y$X zmHPO@#uTm9Z>Cs2F5JEL%C6UcHn&W0aj)#$y4$1Z%7mF=en~Z-TkU#Vk7qypurY;C zTs^SDV(OzyET{aBJ^Cn7A5`t5Yb&JP*61m_1cprp5aG6}+z;+>X8N-Ede}hiVZV||5P;8ocu1(;e zzqas#MHQbvF0!33!sgdFyK%vW756KW|1@g^|Ly(g@+v2o!KuUiZ+7GThw_}eln$Q>CoAa=G}Y9a%1~D&R@N?42nl) zAK?AN!eLNVVHO^1)b!+tgkRV$i=sCw?R`^7Ivx$d5bP>BZPr(?DaNA;_}x5rur|61PQ(mpp^Pu_R_ zJWmz%V-Gs|j|t`<&kDRzzoIAlef)~hOHOZNf-g?k;5o72iG!u&NyC)%?}xHV3!_>j zRJFszKfK_MXFKm2($spP_wckGJ1hdOTD07F5bUyUrN;&9P_Lhj$Dd0pwp_|!Z{eHF z@`}Zw@!bCK`TsA-2G2CDwMcgRrnWv_OEQx$DS6A)|Jn8Nx7X$7YIU7nJ>&0z<8SWl zTzs$Sx!);u84tm3|2JD2Gd zZQa7K>)dH5w#V~NKYm)-zy4g~;fZsd&)cVONEBi4cxk)Tz3Sgn zbCs07l_E|#3n$LyS6DiWTW8V4Z)|V(@t%>l4`=WUI@RmY7x&59!oKxL2*(^yf8`SY6a}wzR;| za}t;P@7ys73YPui)vLPJ-R9%Qz=*YCX8#LjOuzTDZPF#J1rJRdr%ae7bo_IFtXGzo zj9GW!x`i5Cv$x45-P;@=5c%|2?k07P8|o|*CW`r&r7^X4`xWgt(RuWWzv$D}r>@rj z(&cZ77zt}|%p|ZihmbROH7&1*uuZY;`6fdg0PHR`vaXfSNxhNdC;b~vRfcW-{RPucDpanuh;)~ z__yFs$-Me6mo~64tXNaD@xkFM?`Ru#@{yPUB={s-l{eIqV_6-T? zRjU@Q%DV8u)hlBE&UZUQ=DXYOt3R6Pa8;J~-1N1297e}qmHw_jTzmi8$rXQp|2zHl z>Ab3^+Vd(a+&C`x%M@>V!Ts3gddQvMH-v;_%YHro!oX-)_I6`-*4{U^6-~EpS?NDK z?CSTf{*mw|W0UX}joGZ}uDYsALuE9Jini=Xop+ew{JwAbMweuIKJmZY&E5a``SpLY zL3zi1I!l^q80W~|&$T--agkN`vQLZNciHMKmtJgp^gHj?rB}ro8I9Fq*W7!^kayB? zfz5=BkM8fLoOtWO7QLoa=FI7r2Wz*_^^Nn(GI)EhvO9AB&xFahk{o9I_`i(hGwavz z>rScu34!AB;z#Fh_fa&|>slWF+uZfvSKkAxEVPvyx6j)vmCeGv==J-U0~3|hgS__W zEzkb{?0Wr#s0&ViS&bV699>pxHFpeK^4`++Ln5_v-J~XMnZdy5 z&Zl@pe|Gfo34eIj%`#+a5M5oqIV<*g_p+;TGm0OpG^>R4ACHgm3yi#Lw)M?}>+XB! zc@@u@?Udk{{QkO@a>Ep(SPsK8bLKtRAe_EEuw>7_sh^eiTiR=|%&MrWe^>Ftxy+<^ zeXUmVkqbHJtcx9P?CGDD#~au*RX$2o=^5)k4}(R8hcgTrbvOhT`6pag`19+}YCXAw zYp?gZhlb7(-MZ(?x2$PvZ?!7*ENb^mF7ov3I{f_dGHqAEKabMW3*IMpN8j3Vu92-w zQEV>f9ohXeB(`O(V%p!ES(tBQ_t+Zn0*9|34_5N)vNDjCu!ne3BwfN_k4{6TQ+vU<8Ukmu%KIKiPZU4Jp1^s#} zFLK-4MrE_ycx%tl#JYl4Tr@kt2bF)xx8)JwwH?W)>7XxwE6%3vN5_W<8o6c zLWk>P;l(q%3M}$%zi;lyFZX*d^6Z{qW%8Ey37I#({E%y&6#RRhv_w$(#g^9ja=lWF zC3?4CiZwoT?sCddnH2O_;s>u?Yu98wu5WVc%gedaKK@mG&YODs+%DF*nU^gld1`+A z6%w+)=D?n`7bO!IOSduazAF`GJLkoQ&yVEw9{)5C+x%xXlf!H^eVb~7?J>sN?!8L) zlV|gYo4MYXxysNf=={c+cHJH8thPP1^Y>l(#<<4pxB362o@es|MAp2od62YP>73i= zl)Gw6?kvNKhYOXKQeXRrG~ zSD(-06A+cYzreL%@moeVAC9hydQKVUdoJ8JEJSw%b9`JkrKo?(Czf07lFbntcQqX1 zIDGwz@#p6M(=D?<{Os|c`eK9o9q#fl3+=4PeXAB6Ec#e@G4Z;GeC@WW(eJ9~|7AZs zyML~w+4TK3RgpK2E)|%@ts_>===6lqw@AO=gCY3&{+lnPN-O@fidNX{oA4z&t(UK? zAhWXchUNc|JDhv|e3L4B*C(^nM9A(}j`sR9@rPF=-t3W*yC)%LGI?kCycw=_`wjN@ z3RH@z{xUXcpXfGOL5Wdhz01c_+H1OgtPT}q;d>a|!+wlevDy9MLg77M&zGwGwNp_& zQt-tEB4~=AS?Qb+foZN`x5y{mAq$QW^c57fsX7uVkMeS-$ANnwN*2 zb=t-2c7@*7-@bWqha_X^`zITB-uZk#owZi-(80*S({t|c^AG&!DYvuSYVz^tlGj)s zE_lCw`u)gcgA7&yr(gz8&G$_6#GP8!a7vkQeHA{Sw{%V5S~oH4H)VNy6Ky6Re=a01 z?mKhytoQruPc;4i8#nv&VF3kW+s5nj|M`RJZ@CLO77T8h(E*w~wqs!~Qq1P94t~nK`Ala!d(o>w3{po#y}W(8}iKY1W;5*1_k`JU_d9 zeaf}ghpm#XN7a9p{QS0|^6SHGuhl=io~<@v%96F>Z_2d!@7CoV`IPKdrnmjj-Bxu_ z;!6C?#Xs%S#dN8i%l+i8ys3_J}X>%eEdsBg@6p-OASj4sLY{^Mhu;qEj0 z?4h^&=N$QN-JhkrAuERC=be45qQ}kqySjXj$L0z=WZ?B?GJoxV$n)))8J4vc+gm@r z$>hul;}$;t9;?(>v)!5$(DNN^6#v>mABbF8BJpy z#d^+33=fe%Vg=>`Awm1UoXg^q(R=z! z``86z+fUM#7WU1o`mv{9l}7dKIhp+1ykPaS$0w)nOF8!!(~{e-TZ60afyT4^4=FmFEGyh^Xp+hFH3;P<5O`Qjkd?y zUhU6y2-=tUT5;K2(+!@vhRK&^)J3f2ikn-zLMZp(yeY9~>-HR&df>ZL$AekQN{nUe zE9cnwO6`c|_*lK)emEfLUrygABx+TzW``xueoj2q5dd8i% z)E{bTXRlcKm4#o^G)!eQ@t@t&-L8six4e>^cnmLw2?-^=cwDY?_MDH3 z@>#*ax4CWSU3NWWcFz4fUFZJ2B&HjS*FJf=WK;3>!j0)0kFvH1K4U6uSGyhOYq~n=(G#h>_d5 zTklYf^Y2&J+$^HoR%tzcWO>-U7diO4Ol=XQOt()Q6o7l!PRJ#kW|az9iG zg5K*XtoM@_xMzE`#rHN};csX8;K-{V@)j<=u~ogqo4Mdz%LM+e?b=msH&?R6oMh&Z z=zsC>^Gcn(wlzn3B&t(RE|zEeTWrCk!F=3)1&@SzcwmUg`UUe@f6nt2ym9Ev^q^4Q z;|CWn-Lt#Exg%%)*1(!QMiTKJ{adbBG_TpaH}TuMsjCw1{|n%~Kc(>RM$W_!Uw_v; z?f)9Z*Z53tz5SV8LhG-usNr6rRnq$F)o1BR%r-ggd!0H0d8ZgFES^}L9IY_v#@4+8 zezV2>LT$1l$~sQne*5v`rQ5yS4TWn|{+LhTljrSiSXY((b&WxdzIh6-!3I5nhldaR zH-7&8{7Xl#Af1mJFDACnHg@}#_G!nq+n-nyHnu#^%Q@_vFk8v^tmFTEwl_B4&zQIU zLH&O78;%Tfj5X);+ucm^h~AbZet8)WSGI%nfj#@zBwXI6tW)#X%DL)fr&ifp-ca{y z+t8}o-H8(e?<*NB`TYOS1y`Q+%M&lu3A()%w6a>T^o>W1S+0ct{=6^WbZsI|eF|k| z6Z-S#(?y-geRHFxU)=t9ofn2if{j-&Ege5=jQVi*DN}GZt3MVWf9ei2$9xPTISzvc6GcrlRxtO^TRbO z9$qcq|FmuQ|D*fvs`9*?e{ylT|KYUF3pJQr1w*dy`1#^%MSf3N<+f}D9Z1yuoYf(P!~-|tJuY>!WIUEK3=xykZWo0Y>nMhC3b@fq~_0;Z@Z{H`1a$_ta<7dR{V@^{HN27 zOwif)|KJLaTRXlQaNFON3YmNSxa;Q^wwHJOzUZ20U2wk6H*NAnvGeEkWlwI|%qEyB z^>km{?&;sB8JjF_!*ORa^Ly0 zjAA@JJlTRK8INik4PS2f5XJj=^3G;yCkM0o>t`qLNI2!7bGGAi@-bok8p%6hoUzq< zHt7cC|F6%~UANz~@VavMjZLxY>#jxm{Cy=p`H)mG%d+hfyLA^Gow9k+wCVTbgp(MT zzCXX%eqa29e%S@bgAY_U7V)lSRGPiQOz`}^jaC-@TUkIsck$1!H=Mcq|K@0=#fc~e znS6K@y?W=1-Lf8noW)yCr1 zxY)kVEu`cU|HqS+C%^5O_wk_{L;vZQ-zv3iSDk;nIU;$@wiib-j%IClSW)(F+v{WQ zCbHQa`sd>hA5v9Q_NcGU+goN69&5D7c}e}(hbf`D)Biu3yQ_u6ZS~EWojx|U?FWCp zUF8*Sw=3ED?it{9#xSJOS_~#v-Dv> z+Wi|9s~0|9%^k7fM1DzDk+Fc-!6zGc##je)_e%e=xmD?4FL+|@x{GZ$8@Ane`t9Mz z9|0mON-GO@Z$190B6LrjyxsgGkH0?rlp$VWo3|&9KW;wj^yA&P-yVMY?NMR%%F^nO z6*X6O?GX{3ty0Dtq04?tK6!iQBqw{uzTaOt?Jm#qj-P)$_@UjV$IZ6uRxPf2>fFGf zesNA!pNV?N8wQj2!R)-p=W;&N?pl<>yK7Z_Owj)$3A60Cr=L$ri?f&^=v*~%<~xOB zd5f1_b9Qa*KIFXgjeW?mbGAp<+%0K%^{S(&Cy%3-QB?J|mp9jR*-w?L&g!Q;n7UK= zhuAE6?FxnuENZDv5-lM&vQN}MWA{k-_h$C?x%Sik1UK(~R#AF#UuyfRxxbZWD*kL} zZ%A!yna0xH=VYw4gG)kaoo&X&m8bu@3;nBT%(Q&|)$62{^5)qLYnj8ZZhZA=5${p; z?)~fa)f{v`C?rhq-X6B)gUSN?SsPxkMEUG0a!SjT$+*w5Pv1+2!|Mk1bxbYulrO($b%D>037&Y}nhqWwN$Y^oQ0QQ7f&O8j%my2~jbh4m|ns z9x}uPo=Su2Zdm_Z-sk7okXIsO nRmrn0Z;L4r>o8E1{HYIlHt)mwO&d2IA$(}Ev;L#k5ro(q%Ki=?hul|pB20v`?zh;?V zd?5XIPCdip&oh;|4NkVRI0-3fWQQkh-jZnMGw)^9{}(gw&YUMxrpjnv`)l#scduT( zS`}{}`t{0Z8`ULK_Vj<(n<5~%hWGAA4T5es(`>6CLTl*E}uHwORq36k{KqpoGLG4pnE=kj~jEH^eiD>6x4 zz-)8zxJUOpEMlRtJvO)-8-P@K`_poG)2PBR9WJY-oX?DWU9pzMZ$m*HY2 z8^xtHA{9=-PbMCDJ$sw&w|D9edp0qcxR!cq+H?OgZdl|Q{^_%q8bi>jdOog?`xDca zI2`(|6TbA5_JcP+I(b-TIE7?HH_m7NFK4aF$P<)!%-}UkMbX#PPW$|TmLKj5_AT+* ztnkllpU0C^9arQ^XRq8%85>cs3X8B>Q`E)vK+P5j-yJr=bM7fE z{SzIkRsAS;!#lQ;p%k>!4HTxfJ92xz;YxeEgfmK%tR=fkW^s!|i1I^P#OPy!O;JaWFJY`P4m2 z>3#E~uWXx*IC@@w_h;s7nx_2q_X+VJuaz!N*O|XF=G^({V_2#3x9+~e6OpD1`;W~R zoZUEu+1Rt;;I}Su-tJvs)_{ z-Z%E!pMJ)nz3w(h`$x`MOy@nFtCq`7GL@=vzE>_A@Q>zw-*K+>do+u$_(zUAS*i z)3sM#XO_J=&*5%ztcJ15R<0&;*RrIKHy&6!=lAeSf7KQ~`pjH;)q|F;4a-?_w4U6H znm)Oems2d6^%jR1SMs}q0WHqQ+h%aQh%YK$8l7JedUm}HXEt|ooxOXT+orxiu7m3~ zDxP0k9dFoG)w%CcmCAgZp0~EjLI+etmrP0LQZ#dQ{kFB?$Rm^RU773G``Km3tSPh> z_#7n=y6ej1+T#=E+Gc(}&F)rpTSx6&PWGc0A2fL1alJ_^PP9}PSaK;b`Q{xlw&Q9R zMKR`z7kGX?`L*ZKO8$!DHRUolE>2ZDnfEE@Pl<*#<0C#HL*?#0fzOLB#f3XB>k2bp zbFAc{WyiH+OFj!tm}~1&_eY^lK;$V+{vDuezB^4i&l~MVi z_Tl@KM9UPlT@9}rPSps9TffabzdC!`)2~nc6E97#zcra5eT&JHEBPvqJ{{ZHne@r* z)RH89X~u0n+I8#npV)P4)~afx&kMWo`RRt`3%;l`pUXHo|H13mQQ1lLatyythqe`Y zH+*i~%(YW-)zbNDTE(-tt2SJV-DWk@^u`&rsMVi^r(I23{WEDU%d|P2+zyr;3iINM zEBCbj^IE9=ZQlgPNtr?RXS7@{uSn~?dHj<5|2@{O=>=K~cHZh;#bIGbQ(2 zv>%hmogWPU0y+{bN-M+oc%GKlKe?IfB3H9&hWmUG0jGeM8;&n=$cu@xUM{9l)p)j} zNa(?XmQ5oON)?ljnAYp1IMw0p2s0IGkMd zVEMHeJyQp-O2LFct{5AkhU3qdADp{qkz{SAI#2(?yol}WppHb%}eYbeVi+PLQ z$oN^VVAV;Dzi6<&EPi@A?*+CmPrT;yK7GX$_SA2Z;W z)4CaQm)?ot_X7!&9T>Yr{>K%`HC%8qjwB|gi z|Nkaw+aCcn=AW5mi=C9UO!QXd7A-y=7!z`;`1Gfjo|>Pp{gqBMT)8D==jp&6$;LGQ z{b7NA70rq!krC!Qu7nEryYARHC4G9fK*R^7?T3>V-g~=b-6E#$mrp(^Oji?r*jBYC zeXi${Ra}=p7>MpJOO}$GH}Uz;riD`i40mog@UYLgSeJ8Q5%^J}C6)ldh*N!aN~LTenu2#q=!Q&HjDeU5@(7a5;6R*5fpG{1E^JlBix`?=G>A#&8E;@UBc5dH}i62~?in${WlzkY-~X71o(f2Lm}QhNB7 zk*Kb%Qs~s4m`xj39P3xMSl-)tb+xzF@eIz#oU0cdyS+Yx|KTLg1zehe@zz`T(?fk##;9$ z$CmR)cPCX|tm26`n6l;B{Leo*-2>bn7rSd;2s-lkv@&Ch0P}&YbsY$FI zr@Bm5E?>A#Kl1X-rjJ{n8}9pNW0IXU^HXbTfxvg|+Z$wUxnA6LEbx2ySlsv2q&;nW zE*32MwZTAmm4ug?gUy3YQ;d`uFP;7Dn$7&gpetm$nEt%p3v0q`q^}8P=%1V6CM$h> z=WpLva{rV=^=l>)=hKEJ z!Lwfzj2t`v{h7GR>zIB-ci4j9HX)uQ+ai(M6Bo}omK5hcD{k7!r`i|RT+`gR*7A+d znuxdy3jze0b|0Oi#OkxkDS2u4y@?xpzW22?KfG9AaHd4&*rFWP(~HKdPMjI(K)hOwo?N)wgvw z|5+hlz3HnSMQ`p&2$oioa#h>0nElz7qJ)Ej}Kf*_$JMw7O@N zWIvoI=cwhK*}x%92^H!r&ohk`A$xY*OTnHrm{^!_|e%Uo=7 zX8_;I?@u=7Ej}9)sP{-!_`%DTw+R>SPW|H$ls+SGfpT@fqXpAWCaWhqz0!_EAJ}`? z;Q7nBTS9ghM3w5t?>^gfHs?^LY4FP{yIZu{MK>qT5t?(`sUVNrC`mvvQsu;j9sTp| z_s>7(6%l+uV)LP8?k1it+Y&_C_ayH#Ki1yMTc`40-EXl5Q;uie7YjXxe@xDgnS=Lz z>%8zy#;wm+?Ipt{N587gasKVw8?4)!4g~UjQw?7exa8Pl`4wFUvdjWAjT1NC*cg!$ zp;M-n8<#bw?2XvoRj*|7WWtR3x;fjgvZS?o)=${pC-;11Ym4y&w4eLH zRneDb8&16ceqsqftLo997ZnNuKQ0Hn_c)~4s}uYoMk7ON#)qfzic9ZkFa6Uh@JRpU zg&&;~p2`2^@BR~e$Ug5xh5wJnL*KvbtPM$hqO!r@_gNO6C;3k%x14k^$gO3xse3cG zM6&wD58OtTuZ<1gFCnQQA)-Ww`KsyNzN2#NhE44RT# zy4XoD=GC^s#>-8sxLf^}w3QiCRI&i(am!LjMTS?~9; zek+r{6&A|USTMuxyv5&noJCg;L|9CD;abe$)qQ-9sn?Cm+3QulxxHR8<-FrV>*{4T z0*eTdZ*jSI&Dt?T!M|2;M_z#Lt2*T@U#)Ue|Mh_qoD(=}A8M3;@lUuDutX_w z{-N_b5+({Q;4{b!tT||~FQFqaK52GZee?SFm8Iv;eSG{e!Q(sA{*$X4KHll>5K5aI z^l!z4_9ur@6i%cloVd6!FiR_E%k`@%J;D?Cx3QnT{ysi*YMo7X?#iPZcl5qUVR(Pr zz0*%^cH!0;q1Se=u`!-`Z_jIQ;k^cokG*Cb2?+b?#@YG!bFTNTvUin<{%@WyTz@wH zSSs(YUv7rdUQdHxUuQ@cn^QcmqrdF&tMf@0VvjZ6w=63@wR76}+#`E`eOY<`qNn}b z?f=bMS>8_ja%^qOxGAc9TpV&@O8u_xijDUigAQzbQ4-M@lpvJA^>!?8ARqzfW!R%98KW zxR^U@RWC>AZ(XmFeOgmj2Gssk2xq^p(_*3j{*KoH4?(H@7JpJVN%wk482Y?DwD?%$ z^gf65Rr~H&yk|d@yd~3EUN^qUlxhA1r#EbNY)^cf*02aTO-Xwn!r->|-w(ITtcQb+ zt^7Z`u^{Nr`;PQUi~oODbo=+kO4yp;@Y5T;zI%Ta{;f-4%6M`4kj1K$mePkd;(Jc9 z@Wuq4YS(CB*?EjX)v{KsNpO9CflAlXh7*y6J5Ild`f+?C!}FWEH-yhz{u|4DnRnj8 z)m&MV!fP+b|K2r$efAZpYYJ}D|5iF~q9;Z#Ivms+4neJZO>FRPyey^R~ne^NK z=d0@1ozE5HFz7wFku%C~c5~?>1Mi!xE1AD<+3}6pd&j3eFS%w0v!|b3o1A`~!T#Q+ z?#>%}I&%-~=Q(70)xf-6@SEfl|MB+pd(Y1Qp8We;m|)_>vo3R2C4_$2dbF`%%DjRpdmBBnzT_|bz?nV4*!|a+ zPft6h#pQYA{`xy#`qk5S=VD)bTo60wwc|(71IbwlH9sYHRDb3c5fo%(TALkz4eoBdSYK}8RlZ|G>Rz$;Z}|QhC9d4fR-(Pu zf6~DOmFpi?7OX!Sq^R?zy39OAV0Nw4n_s;FqMfTe*3Ex@Xz7js-)k!u?_D{~+P*OG z+Og?Qo>u4V7WZ1)*9y-6eR}DZked^nROD5@zi`s7TlQtAO2hIsOSKvHIty=!E0EjsAuI0XUE#-fK0o|?!)Q^z{Ie_J1>feF ziTEzAu9%a4&%!CYx0_#QUbpGC`~GHYtXI!1P2XCalF$;)_if|q>d#gWeM<>xQ_8eX25wd$O;$eh*Nr=PZ;@G9ur zjlRQsHawSQkoz;=HfBqS+;Y?F4>rAKTi!U~-_lz;YR^nV0w*SP+`1Uy>YglmJ#Irw zfy?6SkB>g)eZbwu*`jpgjn-7Z)%%uR3^{u5_B*|=S5J3&T@|XIc~-=+VR5Q{Amfed z>*pgr=Eu9o-f?`f{Qpd2sTI4oiwBpRPcD9@GS@yo@v{hH%|QeIH#SoY`)6@k+wb~! zY0LF9YOR%Dr=D3o|6Aq+wb}{SL*Aw&Y^lh7rt?7NLau@^dy^#Rf>++Rs~2m&yJRdq zH9mM*ul6y%H|a_B;_r^i-uU-Xtu3yqx9k5OVWW#Nrvze7{(7aiC--t*)U1GrdlwJo z%I|G(kGFMLpI&oH`Ff(M^3nr&hJmj)J>C*&v3IFjrrbhF-AO!G10%B!TkaPPZe>0_ zZEc3}KI#0#jv0&VzLe!TewXvwq;^h=amke3{v{m!^V;X7oczC&vtbu^xT1cH=aZSa zF7daPO7|{vJS#7k_(b_+)`g|TuOqFt&x>I>mtJ`-?D3Jf=Z9{Fv?k4qV>DT6SZRIn z#kXqVvs0rQ{&rq3E@fma>Aw8=bj-RvhF>oAO(|Z+n{ALA(fr2Nt>k>-7V~e)yV)nH zIG@#CZ@6if&8#<^^&N|k+n!mye$n4Md~l~`uP_r4a&!l&G^v1UOhxw~7|KuIjpQFQ&iNPx#-vKeo+jLb3M>^F2IMAGcZHRY1!i`|sm~WYE%>0ZNMuFCjN&7L69Rev zf8=D_CbuX%;nu{Gy5L_MOpYFywZTjBW4lC^?O&Fab%&*{n0yrQW0@!aO4@Lam!wIm z>Vaiz`na)_da70T zeC2*N%j;Rp_l28mw@1i)>-*jJLtSXCZ|#yo`P1d+ik4iM>u|dDRj9X7-@5kuZ_+Bh zTelnw{w`hdW$NFa%G1Yhir@G9`^fh9Z_Z^uG96A|ogPqFRef__EYH8}1=$9(_I%oG z9=mG}c&s&qbJde~1t!0`L?#_se8=F{m924g3-7%@-akX^-;H&8dyWT*$BO7SU4Q-f z`|WQ6ny33bcFjEyyuSR=N4KDBOm5%42=TTo_gm4$8EUxkM|AwLknnDUKeIDeI9-zb z>YHbI@3AAloS=E!4X%u1R}VC;IOSCMO`oFg}{?yM5gEJqb?`l5J^(|eqUUa0Fi_s}^JIsP8LH=incT5BfroIV(7+njK#UhYBm=jn;LYaBTW&$n&+ zHrvUT*=b7UEaizi#Pt(izBNx?yWVXH%Y+F$`|mcFC+K`*IvM`GO#bowy7?O>cTJfQ z+UnKLyZvz2)ucm-hKZYJY>f7qaplP$=7k&&SG?P5{Eqw4$CtCCjvn3_zblDxepLCl zkY7i(uaCOYvwE-D&Mk)0hwk0CKl`v+ULzy9qeh|gY|HGd#S=rjo2UOdZ2oPz=O@!7 zp#wMWu?i{(ZjURydO3V;%+ACd?@!xI1>-m6-Fp$0x`yHC1%0#n3|a1fzaC!>)_nX~ z)u)W#c$fWOw~73FzL+s|M`f>ExgqfBmHr?>9`_g0bDw-T!0~+EHLs=0XG8oQ4zJ?V znR>bT+L5I#4<2yD?ys5g`22o1m1SE+yuPjTl6vLNss6hyti0qx%~7^rZ#TSpwL19f zvJL$$vByriF)XP18<}qVx4U}Z=As`v!zC*N>&rH>6zE1CXqvr#=_L)l%K{3IxG(ipGwg2%dT ziL%4jO{yXD_lEqhzZq2b>#)4vpRcmzOV$hgFyS%p@6{%U8Qbd|a1z2%u7akZP5=dc)r?^+R8$CevjFYtX5>&7o_pOz>xID9u=DRzG1inc6) zmz&qMUp^4@azWx&zb7Ru9!(-Xa#ig4;rx@??<*QTRF>ip;AhfWBX(x0-W%Uii|5-f z7986$J??p*Y|P)Q>*a22O!r^rcyWD$%Kmdj!VC`{?d0XHe_Uo2ZvXFewX{_D+Bv%f zZ0x)eK3FWAXn5#x+?E86g{x+0$A;+4=`OC5zGM(}zMbKwio?1II>voBUf+d zY;nd*ckNl37oMHmbWYa#$HDqP?)UlY|Gv(VR(sEC*Sydxt%-pld=bA%jQs6C2Uo4H zec9cc9j05(l;9cmxQ$0YWsUsV1<~SdTYDCyPrA7GZ%gnal_l;gTLdh(6&XfJsxQiN zWETBAd2U?sN5z~C$|gs}tl9d?*vsuVroP#|f5-jLf24PB_H9=A)$xDwl1^ERU={}3 zl%M?_x|Z9vnndpVwp8CFZ(mWd^YK5G!JS@;4ha=ak9Tt!2WhD@9un-G&i`dYYPtB_ zX1RBJzAc*bns;mZ`J?v?WJOiunZE2VR51GR?BI!S*SE?2{;s~QqD=m6pLyE4@CvRU zY0J8eJsB<>p8Bul$&arSf1W+N?ez6^YwvE!3jLbtE$*T{OX|$IV{E~$$<3APbHSX15XH@&6IIsG(M*lk-lczRJ8KmO3mb~6(h^h- ze7IfB{F}d=^?u!B-TV6v#~z#>ck|)>t5>I5^q&{>F6EMYV5YEI^sSGuLf_XHu};hh zTYCLI&0bkz+5FEduZ77P~)ISOM-=!X-mN|9^2eBW3kCyXLAlEnkE}+&j?|ieX}Jpbb}hpsu?^l z+7$Bl727OM``A8ze~VSQn70CJ$m*HT_~*#|@4Wv${Wf#`oIkhg|JI$q-EJK$ZZv6P zH-p@R=Lh~;eqv1PXDG~J+viZEQ~j7Z*wofAWM|B)_}JeYE$9D#ay&MD-o_J51r@(9 zsyMltoMkCEpM3jf@dt%C%iU>_*W)%!Is061&aYezF@lgazjhsb)$mS(IEFI#E30F65j@e%K zwCx&i_qlSO{>o(@0_q1!@7GJH--}dts9ic)zAmrowVg8Kv$!jLn%fRuzkL7i*9Xg@ z^E{U?_jj>k@?rSeso->bn17}GQe}sVJLk>+fBU9ArpKldUG}H8C3Gm@!zh71Q;n@4y{2vAJ@iAFTl@CN8h@G-?4b z>JDZh3(A$JFZP~j@kHJ3%h9Q}i>>sVvSz9}%zMATEKoasXO8H;1~>om<>fUsCMBoO z*Be!AS$=v)-rR_X!UxW#7+jmdIp@p0gOLWDe-z5*u+~WKmA2H}@YO-z_OS2`OS50E z+v{%4Jg&Qbk@-K)qbpq+T=sCr@JHmB-Hu=QS}slIYaYAyGxMSGp`QS;8JZcnQC`IPfn{8ek7 z=2-oSJi5|^&7*sF{@i~*YU|$nmzB!ymwT{UKJr=inziK?mtMSLw~gDy=6>_0<$?b) zue^7u@ixvn|1#szw6m8FUfT5XAEVj-v%Kv0?e^FQ?!4S8SNd%C3u)^)0dr0@AL!|r zobdYG^BFT<)O}>%{`Ya~!2pe{tr1sBSUiq&=pRb9=KJtxTKs$F02$Lhukfumvo@|& zEOtJ;-EJde`k`&EJ2@^~Exxn=;jz{H$LjtuS}0z-GV{Yal?KM|_TMJ|-;tD8oXzT> z#}vV|YwLy9_L2zBhebH<{so$z%JCMIXK7?*4UqpS^dluF}CA z2f-$*?ai#drgQE^oo-qtWtCOBxpi%OIe*=+Y0+$k8d+9WdPaAP&#yggZCzdYWTN=T zmQ|a?6~f}vw^>y_`53=WQ0?v1g9nAhGE`aLS8;9t8DIHP@ZQBcp~tdkP2uUQl$81W z=~G?o+&rVmof(DN(&yh~q*$q(vwe{lvhLiuxb*b&_vhs*pB!yE^)YPy^zQTf-~3~j z`}yoG65_wtN&+-dIW>Yp!*J+ei?TYI4P=oHng zwKLCqE_idYtz4FoA?j81igGJ&&g-V4nXgW1|7UtIRp`?$zKFm>e~r7!W5pR3gqc3( zVP9{teBq;;&b3jK0^?jvbFSost=x9tUHJbE@*VLp-FB^q#d4SI-u`-Dv0waELo2hA zk2CbQ@F<8lUQ|)iNez;n<=eHa%OgZ|;tA>XwpAi`?cY})Q0uyLzg+F(I%yWBn%~m9 zj=Yn9^X%pH?Hlv&2d@_`sIK|@_tB5e+yDQuGyHkE)V(tE_2vD{o4&rD`9^x4lle`n zg%$cQ7%x36{F3^tMqvGfV&joJXq7I_ zTF$uK%1g0e*DlSk%@#Lb{f_l-_&!ng{ZzvPzfUy$=YJ}(q2KUKmns7T1B0ilpUXO@ GgeCyk{ifFd literal 0 HcmV?d00001 diff --git a/assets/png/mem-cake-sardinium.png b/assets/png/mem-cake-sardinium.png new file mode 100644 index 0000000000000000000000000000000000000000..ecf358b7ff30c10a6b20eef287659d10077bae52 GIT binary patch literal 8728 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 literal 0 HcmV?d00001 diff --git a/assets/png/octobot-web-logo.png b/assets/png/octobot-web-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4f8aa1963210be3fe89456ea1839e9f374ad0632 GIT binary patch literal 23219 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 gvg>FVdQ&MBb@0Psung8%>k literal 0 HcmV?d00001 diff --git a/assets/png/tapes-transparent.png b/assets/png/tapes-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..6e107587a72de80e6b7736c6c6856b2d82f74e5e GIT binary patch literal 760509 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%0DaH1e|PC%YH{#|NEAtE)~+aQ$J64 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 z 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

aaPH~S)#o`^$1SMc6I!-z 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�&raih>RIo5f_ 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 +
+
+
+ Octobot icon + Veemo! +
+
+ I'm a general-purpose bot for moderation written by Team Octolings in C# and Remora.Discord! +
+
+ Mem Cake (Sardinium) + Features +
+
+
    +
  • Banning, muting, kicking, etc.
  • +
  • Reminding you about something if you wish
  • +
  • Reminding everyone about that new event you made
  • +
  • Renaming those annoying self-hoisting members
  • +
  • Log everything from joining the server to deleting messages
  • +
  • Listen to Inkantation!
  • + ...a-a-and more! +
+
+ + + +
+
+
+ Mem Cake (Rival Octoling) + Bug Report / Feature Request +
+
+ If you find some bug or want some new feature in Octobot, you can always use the Issues menu in our GitHub repository. + +
+
+ Mem Cake (Mole) + Building Octobot +
+
+ Want to make your own Octobot with, for example, even more features? Then, Octobot's Wiki is at your service! + +
+
+
+ + + From eeedd24fc920d36a223b3d07ffa9d733f8a49f2e Mon Sep 17 00:00:00 2001 From: mctaylors Date: Wed, 3 Jul 2024 01:08:41 +0500 Subject: [PATCH 307/329] Use relative paths to assets i LOVE github pages <3 Signed-off-by: mctaylors --- assets/css/fonts.css | 4 ++-- assets/css/styles.css | 4 ++-- index.html | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/assets/css/fonts.css b/assets/css/fonts.css index 6d38cb2..01a61b0 100644 --- a/assets/css/fonts.css +++ b/assets/css/fonts.css @@ -1,11 +1,11 @@ @font-face { font-family: 'BlitzBold'; font-weight: normal; - src: url(/assets/woff2/BlitzBold.woff2); + src: url(../woff2/BlitzBold.woff2); } @font-face { font-family: 'BlitzMain'; font-weight: normal; - src: url(/assets/woff2/BlitzMain.woff2); + src: url(../woff2/BlitzMain.woff2); } \ No newline at end of file diff --git a/assets/css/styles.css b/assets/css/styles.css index bc415f3..42412b1 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -77,10 +77,10 @@ a.alternative { } .content > .card { - mask-image: url("/assets/svg/card-header.svg"); + mask-image: url("../svg/card-header.svg"); mask-size: 2000px auto; mask-position: top; - background-image: url("/assets/png/tapes-transparent.png"); + background-image: url("../png/tapes-transparent.png"); background-size: contain; width: 480px; min-height: 520px; diff --git a/index.html b/index.html index b881673..7fba4b1 100644 --- a/index.html +++ b/index.html @@ -3,19 +3,19 @@ - - + + Octobot for Discord
- Octobot Web logo + Octobot Web logo
@@ -23,14 +23,14 @@
- Octobot icon + Octobot icon Veemo!
I'm a general-purpose bot for moderation written by Team Octolings in C# and Remora.Discord!
- Mem Cake (Sardinium) + Mem Cake (Sardinium) Features
@@ -47,14 +47,14 @@
- Mem Cake (Rival Octoling) + Mem Cake (Rival Octoling) Bug Report / Feature Request
@@ -70,7 +70,7 @@
- Mem Cake (Mole) + Mem Cake (Mole) Building Octobot
@@ -83,7 +83,7 @@
From 400b8acf5884ae51e7fb7905f86ffc9812e9dcff Mon Sep 17 00:00:00 2001 From: mctaylors Date: Wed, 3 Jul 2024 01:21:15 +0500 Subject: [PATCH 308/329] Add The Open Graph protocol support Signed-off-by: mctaylors --- assets/png/octo.png | Bin 0 -> 28582 bytes index.html | 10 ++++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 assets/png/octo.png diff --git a/assets/png/octo.png b/assets/png/octo.png new file mode 100644 index 0000000000000000000000000000000000000000..a5a7b5e1ab56b004ac3c563c0736937d57c1a3bd GIT binary patch literal 28582 zcmeAS@N?(olHy`uVBq!ia0y~yU~&Or4mJh`hVKuoqZk+%I14-?iy0V9uYfRPTeJ;e5*+`EcHu|=pV(81*t!=f948x6%`q^F&~xA%I{+p4#F zzh__1c3htvD;gpo!XheR)#RbSu*D>(%shIZec9rUDZ0kR%l7Pl{_tUyVsV_p%{|r6 z&R8-C(icp0-r>Top!9k|%|9K6w+tdGb$Of%Og`RU+W9|k;*W{U3q%{<9H|#q(+i&S z7d;R-Ak}aG}l z1!=*VEZl+UwiCbC@?U9uPW`K6@HN#N;29&8tp` zGHn)|crK!&Bgf^_&uZnkf?Mi!_qabk3f3~3?i<1!!Iq%c;M+OhoN2~lt)k`=2`*DC z#gvq|C)NCO>=2(9#=w^Iz4^$D$v*+ z{)ySKyI=Wo1W$t2qwV*WG5wgj{a(kJ&eo0-Dk}5ub0^p{C^M9J|GC5carwp*cF%IZ zKUP1Iztgm<*TqH0#ihx^uAZIYFoR6zkEG|yg7qPD=csZ#2tSfOVHsn@Dy>g88z*+m zX>aW~p)l$Eecl5b8LF5q1V2V9ESkHL^@C-@dxkT?qLHF=+E09o=;$a>`uV4pL8iHa z(Tek<`u>hf8$}P)Fyu2uEYSKSv$3$_O?zvHhw`NJ@k|dC7>+Yr@HwP!>^yPIOkv-} zB}@s6R-IUVbQIQ_fFQ>&nUtF!1+k}ge=zQikoD_lopA2 z*wyPb{CBS5U+|x~yh82sO4piiFJc+wS9k64ERa+3PEb_3#eDMl`_3QAAH*4N#23gc z3OUPUz<7WqbYi`TY}<)75gi?6PM>~$XEtE_xYa?&F-2UI;U4pcd8ydw(;LV$24D!qs;h|4fE;=mGdCbYBZ<>bVbOwpeA9@#E{6hF2$TF;7 z*<}-4BB3N0(Q%?sW&VEQN5+onjjA0P?$V4s%m#f=JLa^scE~t>`pGSDU*==Pk!}IO zw>R?{<~i3melxn5*zx6vlgl=>pMRVg{s@0OdQr9Mu|Q;~$N}??^D=uEYZZkQ$SIxO zC@}G@hh4q#$_|+|cb(c+eiG;T-Q)3*@yXq z*`uc`0!t(o`D_rF_|((x|Jn}mg$EaH7O)J_eRT9iW$46uQQ0;PSsj-Y&q?R)ISy=P zsCn>F(k0D^@nf>{y--n0H7lOLO%WX@f>h?myY1nSRkY$3e0!~m!Cz^gcG=Ds1}@cy zoLrWv|NQfl;TThefWyiM7A{j}2|PO9dgoMsC( zsA%P`7YZ)cNs3CBC)WInV|d3@AjQys!^CAtvpl2C8m&$7B?61mbX-!TCY`TmXsBnf z2rdy*GCIO_g5{R8Dn&{6wlh5}{d^Eb^RU)ReY43GGeK&1acE#7pib}p7cK@Bakmxw79O_4g{eKC1a z^MtJ%1p?hv=GVs`>As-3XtTh?#a9`2goHlfcVSn|PEuU7Ma6Hye*OcY44YVvD=KAe zV>sZtO6k9n6-OjDx8PErPe1h?_V7&YJm=)Hh5NhmzZG>6B?6n+#FV^dJ^9Sf@`0n` z3Y%c$VKD~tRb79Uy--N0({Y(1a`Jh-&;ip$#R7s$6NMff^$?Y9In&+RQRe;WXFt;i zqlUbu#R7td{#}WY}aWSP| z(kGwq=lE!JC9pt5Nont%&L6p3bQd*Gu-qVUQC?;Ke%pra3_U_~TRToLNHg?=ivFy= z=;&c1ro_bHVfSB@;T>y$k`=e$#DnwL3bwC05xS^(f`yI?pZd=~svY7BwO!H`m4q^w z8-hbz|9O@O2sO8!IH)rJe+I+*#?4MHE@$Of3-+%%VY;Y!!p4XW8IMmtqnRpX4zw=X zEFd`ZLJ>oH@wO9@cNQ^E*r+nU-k?GLpplEq7HwNbpNTd9R=ct*J}ziG;ixkIe+|QW zh7TR*oLo`_gBj;6)B5Cg(a|Gb$AwSf=O0!E5r*R(=bT(p9HRyGUGI1DIsHmh++=z3 z`F;71Mpq(B#FUh}cgr7`yGqI3g*xWaty$Ma>ho zMReqNeEP}FSi|~2;9_J)M^1+Hqoo;>UMMV)JtuRc=gDV&_6>YG&Mq!hnT!dGo_zlN zAzS((&%|Dpc}mr>C45Rs>_520lAHxo6%xg|FdZE^bQ@Jk9|+fH}D7wc246>5V|fJDb797)zfZ2 z|HtF@(cR~qTwLlaoId@$FC^O(bUZpuOy%dF?MnL`B?JX$Uglv)4t0%Fwc=21Z$06p zGT)y2fH%WA@wu%X9p5te8Vq$@uN`no5$rVj!q_m`HC<6@(GpX|xUPKyvMpu(ttXgN z=GQYkN?o!2g^r8M6r*WM+z%FIO`cry?=OQ7%V`0@iJ_^CA3k%M6elYh8J>J@&vc-i z!3%6mkL|=8F4KKFwx99Q3T#tT*teZkQ1Iq)ZiaP%qB}iH1bzvpcldaG`l-(dGA1^b z@d5YA=jX*`TmBt!N->&rzTWgv|Fz9Lf`T`%YcTxxmD%s2b|5-UP3h;K?Fp83?*|GcOJ942`^C#VY|aVHY zxRwYkf5bB}Vbb~k0ugN;9dp*nH@sJwzrXi|g5Oa|rADFD7iu8&Crm7YRXDgODuAMP z3%8Cd(}!s@877_ouK?1O@x0=<55qYDNuN(Y^VvQucp>BBl2WyqL1v*=(cBjbeg~XV z6izE|Ey=G2akjv!-}kC(YU(&s1|IV|F-mQ_b=VVg(#r1SL#4ciYGxwyEzl;(aQcv109kFkr@q?&(!8GTqTIDt~i zrqjf;=dlo(PNRDmmb^gn62xlG3F>mIE506ZZ?t9`W1J z=8-(<{Cm~~4Ir0%dC2%-p=kFj9N3jr6Glx$tb8ik*Kb}nkZ!OA_c zO=bRnE{5sd=Nw&J`ZkD1beSA>S|WJzxjol`&0rrcp2P5f`J&>RhE@;eN$2fZKK6?L z{O;-E;%2Q_Xd;r@;6F3f z^Z}QX&*inOIzl%Jq&^b^Md?IK5ypy`Pd_)ivTsxpTV&#C_g{hGJJ>N><~lR{aWCP} zPEz!8Uc}=oI=8u_BSZb%^qPO%F6|qn#1?(AVP4=+!l9(JD7GT*(@*v;zC=|W7tKjE z|I!&`bgb9~1uy13VXAQd^t0H7eIu6`s7>?Vl3|?~$RrKU-%J&*B^;|C*tvL4s`@Jih#XkD$a#k3b%D2Tk23mpGW-H(=P3&{8k$30=P6os zNEWtbe4od5T0l_nX0!Bx&6Cd0*S6}A+$iwYDaky`F3I1;rOZg;z~)8HGX%LWuG_0& z#ULm+k?-8(ntuluInUtbzUb#+_n(`gtrHY;OPKW=%vI;xdzT0pCo0}*eDc|wK~D(e zPY?IM3^5v3Ei*r8Zhq@ww_o}|NkLghN5>m|u7n-nmRJ(o9K~O&v%!h++yl0P^)Cd{ z4oWU!Ir;oO!-H~$N>J%7DEUGubfWBjM#J=ueB8IVz@J>tD}QMVzt&Mi;Idb1;2PM?gbU4N?B#=pM171+H_=NtH-CG>`l8% zgaie1*Q?9?+Iw+hE5`v{hE1;R3Q9^wGuu_>-!HsSu&3 zGuvKNfKs;L(pQNW8G9Zqar)Bmg2BbbMXY4jlh5VCvQGJZ8ZML0+e zC$ph^zA~3!gSIQXf|Amv4ZBt6@AoL-u)PTmF#%A0=jh^+lDL!MSQpb5yMw;&OHy zCMSKou6E>owUD6TL^HQlT{fO29BXe@w!M%6J38~N%tgj83E!PuTwGjkS-F4u>FwHn zVU{lU#3!HSnG$AzL(1$)8?U--PGkqe9BnI5s_eb_F4Oq7?y6}u|E4jRF`p0v)j}tZ z@db)j&Uzsbwec~}#hu{Dymb5LBIb;feXS?TzMFP-bojhrI{92*K-Ninn-(Zc1P%m% zeIawmEW0c>w1Z)e9@v8B8_y1e?-pHT3{Fx-6PktjjU(sfas@sri!_JmI3~8)Wt*l8 zgPwqFLr2Gn#+;7^ljm|x)Roc_6r6ZeM&sunTbK3|Q*=Rb&d+=y3RKZ3wM!{lIV{Tw z>|mIq3b81%?q*jk*F}koj2#^vGC4Dze16`=H<9zSxYEhz@>~lRg9Cu?S=-U=`!?1n zG8uS*(^Qh8P;YBTM~BCrx<$+>hg7W`)Kup0mpBl1WAP0^LBYto9HA4{yZI*atrq8$ zU-Lg56wBR=i6Cz zuI(OERY7rB&Tt_T?DJbI8y7JzIW?)3oGG5(!Lz9!Y{3?9S(W+w=e`hFxgkaoTyoy(0F^;5yGs|mU;qg=xqdfSP*N(A z@Sdxdc+}}iJUEbUDW4N~*<&3dEGRfLUj8CuP~uUiE%PBpx;LxLw|6h;NQqn};0fyd z$GSdOP*PfCTf``#V&$?)C$xj%4LFJfZ}wm13e*ROnMP)_E4# zQtDQxD`8*@y`;D&`dK);xVWUqPP2?Wl+~&c4v~oBhbS}&fb!=(`Mh03R!MT2rc2mQ zZdE0vMf*1#nHW7y)8)Y%kpEvE@oHTW0#Q6u^_;@iAVn=P6I3|3d`nph&c{o*#Xw1+$ObocDYzM;b(J8*G1pWezSsc@*oj|RT-iO^rg84qBLLlLsDeVGIKNppt{ArP|lD0Y>xx1>fqhn3-mT$?&vojZ;yIq0w*In<$A*H>@1^I?&C+^*PpKb}vvmewm5dr*}B{pS?*XZ+40 z+ucH+WL;DYO5EwRM61Z>)6Y-_XV-Ma-(5$QI?QsNO}6lE{9xuXd)v|dfv!v)W>;Ss zuQ<&+-J1Q~n`s4=MGwwpah8X1@7Ub+;M23?+@g^|I%3W`G4p@SS>;(HcU3`X72_hs zTDH=i4?jIQ^fs4qy1KMpzT%z8)(=(sGI7@^YQ zu|n!%0I$+PE*Iyk9j&ifSvr-(GF_Hz{P=$Vv!cUu?{d9+Gws8(QV)@(OplF&f)@GS z7RX)Ys_}X`|IH_{1CuXHy?c|iV{`XIPmw~U-Zi3- zaxa*1j-nMyWN=1pJtIfcm=dWi|`S#&CJ-4vL z>pygbg_OiN7p=_WUF3U}!#sy!k?$hqw}Cn_{RKPZR>Xfk{{Q(oy*BIBs(V^vADq-n zb6n(8qh{r>YE4wfwKqSv3;fj-TIIH+(D=~TY(d`8&c4N+5h@J@ceNNCRut*oxw-bi zS^Z}9?YuTSbC0wYH+Za2`)IjU>f(f@)4bjUnS}IatdM&6>&M|e@8$$me7<#X@^!v> zbA%4QJdrf5gKvV?YOzHt7co!x>Dm;1F=V^c#A9bWT3@>yzZh#0(i*Vn%}$%j6ImB! z@BQ&BNEheyOOu}%z8gge=^1-QZ(u6958^yE=k%7M<_+K%sq^IpZ!vM9}!sv*)Tm9P=jh?(nXF#ysrgT#kJhfo3~`)+oJ;O;=2p} zM(rr}ns|4U_@1A#F6~Bh3;tS}#6`{%_9boPI8>S~xGL_1q~ctc*;#@&Y?$B85qh%a6~BMp$`>BHx>>Y9 z0pXF*{X*lLNUl@xTceiO*Ohix#NYEO?)wOCk<6c4e9$Q++xK?ZMh;00sWmQ-k7<4o zD|7ertdGw7CedxC7-1>@(8^n{c*C3sm4*k`ZgPa`JNdm7sQ7%VyU*pf3dTyx5BBb$)Mp!!vPd{8N?r`&kZzU9|Lw_T73~aLpWUyH`1UAP-IB+x+Eb=EVYA zrsz6HlM~aqg9|Dr#O~4G6#Y$TZp)Ufk)cn@7CA3Dt!w3=7UH^&<-_V1GD~_r7BKNj zt0_iUvg14rGv-GXF z-a45?G#={>zOW^C>I;n}yr<{JZ?m-)QSyFQ?ohZfL!z-OlH;S;OjQ@A=nFd%l@9ej zi@PnFcc-O|SJ+ZJg>|DrQ*`>mg*9AB6DPbdnId^kfb-kt7YtM8d90A)eZBwR<0EC8 z+9FIk4lG;A5vJ>;leSMtdQDWrfn_TlrreKNt0J|=;qbAO9V`(^x>=2@PMmgWKhd&O zJk)%0&A*)tY#rwuPiC)*?oBxAbJ6h9wCS^Z_g0m-{4P_?xWA1h^{rPx#Dw-qVY&eU z6S&tOXI^jqPg$U&KKWkQ^^RG$tgc)&PrCdpE9X{FtMBa98*+YHg++MmwP1y-omcdBm%2K>u5K)=Wx0 zw?#WUylG-PzuWnDsW*1JyT7@*Sh*`&EX(THtkT&VpKLdc2#;%=`AX=IQMKl)S8F%^ z)80`kw#fJPgy2o{UuaC(Q|`)sV#<&0rBV}W{w-#xQMO`#8>b`Ym~gqjgT<`7Z&UQo zO3N4f^Z8{;k0kKiuaE!oB1}!?_^(e~e@s4ypNafnt>!bQGq?2F-?aCio^fltPCOvS zqLnsl7thwL$PVYW_w1Pm?nzY6Xfa!wbn5Y^snQ0Cmwf*0D`MU^&qR2lM~FvXO64bO zrR`@8cEprL+)b>G4RMXrwsKJeb#oj({XEQIv#9vOxuf~%XWW()HowrYep;M0rd z!V`_ZA9ggeE18=|mn_-eaWrX$ZNB6(pSj%suiGa+UgmXi;=glG9rJE8^WN>WxU)g9 zQTKA=jGy~9rl(dG{yN$Ict_xG=U4l;YvyU^=?ic?oUFe*@nnAX$B*Kd1-NE@u5s{> zt9~+#ul4j~Wh<_?w?aZE@{7tU1wZ@6b++Nj=lxs{oGx0vc%L*;LCVa}aZTpz7aH}K zx0i>{vH7Ji$L3eU7L7CFvg(=tIE_F2{2}=xao0^b&2q)%DGHmqs$SUz1>enL*?!I} zRPxlRPmg{C9xk{Oc(~w(<6qa~o>2x8hmObl9Ny8dmi*G_^WKx?mE05J&6<{;*pnA3 z`qS~EqEYH{rxMeKbeHD~_8VnOTD|qMf|`;gHwP zMP6(#qq@#))7ZbI)VoK{l;?S`tyJpU=+Z-goD|m}`c6>DS&5{xce%$N%Js-gd2{kJ({= zP|7l+kXyS{*X{iy(qs3F;qu+Ivsb!8pH}>Td=i8?*v=MOpPd`pvgcd-Mv{T-k8<+>cd7+UMh z5byJY(|^wg#Z}p_T#q~E&1Qa}`(nw>ud`lVQ;7K3op#+jJke2Z-Od^wKkJtUPoCUZ z&G9zl@UgWO-=6v#{JR~0K|)~v_Q}?(IVw}#4?I05B-{EZE==@i&_%^93EQ1Y%p2;v z&pj+j({f(iZ+Ae@HN*WYFQd)JJx%5R)a#VX*Lnr+sFP)iuDhfVnC55yvmmDUr^J*k zMb|RlN}J^!efxcHpL~6siWQgl#GNvWv_6?YjJ+azfa{{=9K8)c)mx;mah|=fJY4)v z+1-dY_B;A7$Sl&0-^sZB&S$A-x-oisEvK9JY!dvCvfbym(8K^KRx^6pnS))2(_j#3*&*K>u%zq){ zm)d4j@@Y2*Z!c^uz>DU>+h3DgZB4@>_ww%7kMIBFJ2hf+p76El`L4;y0=Qwbqqpc{`t*7FJ8926VYhUwEj= z=1$e!h&e@v6lcX(U!JD*XJ3--wyJ`*89km&8J@FR(#xg{^Pm-YD!X~gH%J$Gw{u9(e^b1h zXG6MT71sfK*XIR`mgQW;v8JQi8~5!| zTKsyep5~#0>|dPU@3ngTfcdd~cA~edY2J>{sb_wqZdAMOJ@s^S#~(iTgHQeHb{kx@ z4v?>Lu(v6B?m8=X$&QqpQ8o6ta$#%lc4%IHws4cyW_G??TXS0v^Gh}4#okga3oM|*n3GP_Ca>!otIFOtFt>j@nyD_o%+vDMgU6abzBlKm$?5RU zyHj=|d;T3CZyhn`gwOql&Z+Hw{j9j~nWnPQzCNpb$+T78A74(lPdfUlh-5*}nAAc-f+WGGB z+ny`+`6rIcb!^;sW?$g>S-+*%?S0Cs_Iz0)zfOytGq&=WV^Osm=w!>(VoFZ*Q#CkNRpvR-khe4g4GX`w`blJ^1oBR^Bwo{ z*@qHqettb)@#$!Pi@r*3*o67r^|7^5HR>`=Z{B>Fw`JwJm4`#k?Yd{a3hKT5HZt!v z^TV_6cbtwF|LSs+(fQlKVE(wyu%jfCuK$8cICO7r71+wU!Lx)-MdAet#!gdLJ$&3}6LaYH|DCM4w=@2mgl#@6dUeN>S?V8N z-F_Sx8od0r#Z0c&Sq3kETmDG>CiLd=>f%$~^?T#^-FhXOmK8{Be*SM+@Ts|-N6W!@kux-F*!Y4&+>i6ozlxYKP)k9-t^&m=#E!y=^y_cpX^&&bEN+UL&x?wJKF#yz~eDF<7!fGA!-jwA> zubc%9<}O(C{st3htT{3Kq6b5-^nUX{v42h+R<-&2VCsjeufMO<{%+1Myt`Ik>R-~l z&aY2JEB-&$JaPVXqlv;-^*lbATjIIi-G-I33XcmORbo_-YBK+S{^%h}ZZ?~Hp@7PrH(W0z$#N|P^(E%Tl?$nmg+}kyA3}Tocs_Z(#;~+%~H18t=GuN zGxUkOOZ$c9T<*X}+al`=CcB+&l{Qxl$-HCk%kW`tfri|!*Gd&OzjS8AY)w=CS9$sK zkFu^EwNtiiD*G!|^XI6EOx*>BEn0aKFCBk(eYD6AeJ5-KC`WrgW=S#zO`%x>t4u|3E!|` z-k5P=!$&s$8`9_PzwzJQ_9A|3n)10FAL=ga&GB=ub!NElH?vofxq9iI&tDtrYOa*YY5Igs^?_7UomF?F^Fs4 z?<-pUE_KB!tIe{_XIJb~Y*@eVrzHEk`{rwdu1Ihso%?gY)N4C4b9&o3`_~dNr5`&r z>+duj)h>K?^t-aema}#x^&BkDKavjd^jMSxoZ8%V^23h15~WWB&dmEAeaw2Nx~>${ zqKEs}1TOB`yHIPB5@=RsZ({SR6NU`yJI^JGUo5b73jNG0y*@co^WEORHFr;4xK%nk zH@a=sRdbetd!qdb{}x?2Z?o^C{PM>y1ix9Szq&Eex$v`T`jS6|++ypGREFCmoi@6% zX4Nvj^w>`gdHljDm3u4R>^mKB%j&OlUGUW{xt2+RzyHnV{IKl>L)E5IMXw6ch8@bA za#cc7@9dg%sL96W|BI`;=AOMG!uR->b=;;>$*i#Z0tZ^d?c27#He3E@K@BJAJZSE+Z%mb zc+G}?yBsFZSf%wT{-8; zkCi5B$W=VDb)T{@?X#HP@knv|6}P|s{_?76_Ilgk?XTZ!+}XhQ@Yt36JQwcYs4C+q za8UG0P&^f|;?@q~OXqDLuh>!^=XWfYfA7;3Mptsq{ki|~z~uK!{}k$Seg63A^7WS! zPFS3`s}Z%DHg(ybLVi0-zRk}hb|x;hJQKY`rrh?+w7l!~pmzSh8;eVK|Lg0i`MYVQ z)CPf@&8tpKW{}vwbdzt3w7K-Q=NAH2n&<8Qr*n6mef>qQp5Gap@~)fj?zfxYwbD3f z|5wYD1pPUN&p2d-UcPTz?SD9to!8^%<9)vq`R@ID5G^Yd3F>yd{&no(v+VOn>S}kt zF8lk|wOMqI>ZvogS1GZ(v|pI5%oTX(^djbnse$j;MTtCn{O`8@g}1*N9XPh%J8k-H z`@P>%t5yZqANkuSeeT%4+FhCQ9`1j?y=>z?Z)x_lfAy+$hv!ZVmre*}rSbzKLcJWnl87C~vzD{|;;8x(H7$o{r^$7ox#Ud@z=2B+XC0CECFUo#i zwk!7j|F1kc(Rpt+3Z!kR^fryFc)NLu{khj)AKW*ex7JzduT9Z+)lA!^KMrNjPy9SB zOI9ed+p1pD?swL%yzB2H!n(BetQ@z$&exY(Ye^242vz+3hd=Su*ZG+ zY#)`7+#7E$1oOu%eEn6v`kiy|{3v}VtIaM953D~1T)uDh`toY-bGFJ~=dNh{@;~m) zs)dFf?enYRuN%tke{U4K{=SaTjTVm;$0lsdc_hC43?CyOXl(yF!;bJ0zSSA4BC}3d zh|eq5xwZXrzVXMlV+wZv4qdyQz9jXI`{cm;6Mk8pe50eE_xpN;tWf6jW%cvsT|1t8 zE9U6Ug>!$@9O7|V9C&x#{f%oxrq9&}r2x~b62`A{h0Qzp5>(tp7qxt6jQFfk%ty@+3+`7*YOh|;{~-ClN$l|*U;bQtaYga#TEQUEb@AO9H!Od9r6#nk zIuQ(={9bh;TR`7+i?rL^@Upw2b}5y6clCYT+1Kr~Ht!OD@uhz^r2iFKZQAtZ?{@Fw zrqh?lZ}|H-J!8g^IJ=)g_noH9pFa8G{#dOZoBoT3-U-Sc$mr8{;Vf_1?6O;Q-R1wR zi_^dJS-kwwTq3)8p<&0(&(mI7wN2HH-6qCeA3OEh@iHClqY?Kxa+y*Q z{PcClF5U0F-Fr7H?d(t9+1g)fKfU`Ac(`Y#s?me5XCI}l=(2Gv0Z-+trgg@N&po_G z>EPN9vzPlneAx8a{k7e(>yy)7*|lB$d3?EjbJ@k6#}%!uy$_qbJbm)yA$xu4$={B@ z{#mgz_Ro*W`>KELxcjc>AMa$*+*OV$8(hB^>b&v0$mmj#qUg0lmcd-=Ca3Z}+tQ5t zQj7cT9KIK-`OjVbTOw?GO60HXqqnAsC4aei*u-b@zQ^vo&a*E)+G1}vc|ZH|A7z{V zu`iuh@aDsozDUo|%Vx)IY^JV?4{twTYoFzgvIrkb^mn& zcN7{Y8UCF&XS2cach{faaGFwJ-4JzA@r+8$eyNVuQrW{RZZ4d4t7=X3`OJ?gIzM&) zMjqU@^r(cg{F=mBS9&M%yY1e#_R_x}KL7ssZOS^UcIE2p|1H-(F6O&XKTSi*Y01GE zP3emBtxC%{3Mv%6&IlFiUMy2${LAlP^R&g8XQph_&RMs=sAs=zJJa=e#>MuGDJr%z z_0FCDa{j%_{`D9B|B&uK-aFa-b^NBoz0#%fhm-#4S~Ym|>A7&4H>S^F?6|LI{AkVm z4T=;9&EU4QLZYriX}-f_Bbd{e0ekN*UzArH;y<> zG0`iblTUHhV{So) z6eGf z&OLNuz4rai_Lp{Tg8UOB4&<&h`qEQfr9_*Ad39X$rgI-=`+o6BRFtTby1}p4r#i0l?oB`L`-VGo z;(cM+Lpl*1+ZJnm%Ch+2V9n5B`tm;WSG(;w?n_l&A*R$$K7I1!u?LNNO?FFId*{{@Z2r=wr2V+oQRGxr_d*o z3~|D94{L1P@bB``z2D!h`7L3c6&uuY>6$LD{PnJ1-!85bkT7+=;(qwNrtbegW!E;Y z3AudpNQ&ggUxHVE`Yt^A$GL>TI7#u=R_%uqqSM`UR)__5FeEH!+aCJH>(&14%dUT} zxU*}$vXspR@jY?5-)*F99B%Jtw|;eVug{4cZhPun<`nlFf4xe{Us(1~PeezVV&UsI zZWk?k7JRo}EEjwFLfGP?>sMO+I8f(wZ0jUP=5Lq&%!)d9Ponhi!RDSwp1jm{Srdm7 zJ953dFD_oaFZGO~vCHcNlO@YKTQkf#yC$HT8S9 z|J%Or;$prFe`Zy!KY8)-FJ=D8Ci|D&?a*>zD7pW|SZdX^ufNm`!$MQ$-QCHp<5FEF z+fcd4`Gu-j?~8qk$-lQfn)mJ1t(|ip{+5sQ+`VmWNu+)9QKv2O_upmQc01#Z!1=v2zG5ZFcw>68z8z<=)l%R#V=gn-&ACf zP$+2QU^pjHyyL>hvpHWb-a606_)9d?Yn^XI0z>UMMS+>a=Ampv8r+OAD-eLIs0(}$H^YyR(!?|vL!_erjSH-GNS zz5fLm-rNno?|b#zk_`&}+h+=BTcAaxqA+CsyNlUbfgd2((O*+mkQr$a$&5H~BOgC-b zv#W1n&+BzxI{Ol=!eeU|t-JpI(#o|<*WG?IfBL*NufMK6!4q%a%38VW{1>T>Ee8@V zhq~6qmT;_2Q1o(4n*BN3<@tpm9U&3fg90E$_vAK7{hYyk(f9e>J>5|+KA-ho|Jv;J zot3&zlKHNb@@QMWzM|ZEp3n61Z<(zTDob8}<=w{0XI-iotmC2?B>Gd7LBHeN!l@Y% z_c`?^`pTn;+BsNrv6qmLC1_y*K@}o!{)B+H1eI7$$uGXq{&vy_V(8i5#=s z+aix&d7m*(4vSgxet+lLdkXW8N2jh#RP;K3l_9NzZ((bM%l*&QUS4O4Gt%Pn_~iDz zPP%B=visMh$tCks85u!&B$Fb~Ei|olK7TjBu{HIlL zy}Fsjldra+6XnHZ4|PR!Tw0>_Dd&jy#MGNPRTi1%@zu+e^XHc8Xdk`IADeiiLbqhL zqEQ}qtNXis60z3XjdN@^OYDgEzf*qu=c43*S7Ho1B1} zWuCXGy-hf`+aK#AO4H{3*dG!4Wa}d52vP39XL)QN*1iyU+2Q)Nev#5?mi{~Mea`2} ze)#5XzF5INyhP;g(`PAenMTtOM&7b*;l3%P+uov&YQ@b(qv;e~ubBPaCNoPNwnHMDb)Ayp{n!Z*K z<}h7X!@1%i%nmA2S8IEGc&1j}-X@Zop8l})_&?1xJgm*XYLYqGj_hg9*K>6{6C|-# z!CqY7zE+mc{>Gi|6TKC-hL1`ElY>KCx4o^L?Utl?sbkd%t%lDo&pkYS{)t&A5aILR^=kCCsO0#5#=ljM zHkZFUxZq!PXWyia2brrMt`*NaR9Cy3`|L%D3(qx=ozmKPTpLvQ{rmmstVB*D(?<2f z-|vgxulvRE>-}x#k||3o;H-ShDe?}^h}&k2|C_CAZ9oUx&bp&h)vNx(W$@lxZe6XJ)smTHJyuI=FJJ|mF( z{=WIVX8(KpZU=34jkVr>`1Sd}-2eA|lu4;P`!VJIHLu!xXS?SqWjmip zj*FV#Ogg>vx4>(&kQa7sa=*U4>?)7j#VTKGC+fd%!GcxG{7RP0J@u)o;IO^l@&9uz zy-KF+kKC5DSMPf4uisX&>t?TWZvTG#`TMU2v)}g|Kb>FK>2P6Af3Em;odZwB=QM2g zm(Dz8+cs%qX7SX2%{zST{R`%Fb#*H2lFxUS@7U-+|2x0~z3=_Sui_ph6#wL0tJ{13wOs-FhO{^Of;aapN%Ozp{&7XMx( zYUrF0p5*BLS$yq*mCx@k{%vtnm3d-p%BkB==QDgzJ$Fe(c7uTAfi+84tjsfQ5;ZQB zf4*+^`J*$J_gtuzQLeSkRNMD2Pi)!WEdr)nzwYV_y`m)loBv&N@Dpc)M*-P68zLh2 z^0Lbebsp!v zUaGUW_RajR=y#RA-wTVp+7(uBnEmeYk{7}Z=_|T&&i&qKcF@#?EkLw#<&)3-tRF-f zitm;i9@zBs*BVv9moJptAH7g+SCZ*_uv`9F!qZp1Grk;_ITO9Z=5hA(Cn<^Z<6<}X zuh+A9vw+d!MMK>QvzLMzyN%+T_qX4TtG~!~IbVr`L2o*D@)KtRH#?Sv%M70B$MpH{ zpVguxW5m9f`{J9O`(IYo8cpYw-oB`2vf<_?wV8@heLs$M>o~I|DGDhBcIh!G ze9L4kpO)x+cIN-xiOCZqvajbJ`&Cr?eg3Tth1%EF-CpxrB1NuP{Kamy+-kk`AC{Y6 zXSQ$)O*x)_Gs#3u$uY#WPWV8plJSxIoRXUx3U2vx^GLQIG0r+WWA6237vm*={(4x% z9qYMC?&D0wea#{btsV1SGet zD-zc_V5B0#JuzXG(teJKOHV{6oUoX!y^6OcXPV2bjDSGHEE%Efi<3fc#3Y^xk}5Cr zc0P8qlZ!z)qU}hMq7mc7r6+O&6qy=c?$ zhpt#pjV;RtMjhv6hn!N3Rw?c8tI#xhGO=dy`7`Pm_-6sOLh5;h(USGN7iNs5~`^0S=1CSj}XdhF+y z)AGEs=ilZg9x+(WD07DO%Co~xDM6iQrOz`6Tq%~Bew%yx@g1!#`*q{n-`AVuO;nLo z7T}QlcXsv7mYyJwKs@yhnZHJ!9qT}khxwm3i|Fb0Dx7b-6^}6!sQU=W8#aHC$PPGCH3*C`LTO?%q7*bbDDpg30Nv8uAaM5Akt%z zh%8^P_r%-L2|wCWu9|+Wc(aqAS9bCi^WUrir$HenG|rc{^dtTn!nRN<>*9i3p;URisaMH@-FT-G&et6bT@kAsyiR%uAH6u z17t#Qh$~-4siP4e_rz9JuZtWR0bP&2U+A?x#>}8JQRCm|T|4u3Uzk_>-7sgfi`bU~!tWJ@44)iXw7(#I>y=j%UVlGml#;3ABDP%XlUPPbm(9<&#{5&- zJ_@IQ^5Ex=+>>JZuJVcOMZ>b!@7nJlTco<$<@f@X+AlvteC8@Xi_q^k+wAxFQ=l4h!SW-rezIeXIgm(%SZwZHuAdfej_|7Gz9$^Szx8fNg%t9ht=B~N(EzZ*|m z)wi2W*51STAZeOnb%Nrii7uK}VwuyFrk`f@U-xl$nECGyo8>>eeRA{AvE`u9mHBR$ z^)5Qqde{Gt-#@&*bahek1-(7XYudk0dEe6eUiZzUlkNZAL!WR*w7qF=J&`8VU$rc@ zYg1Iob+dr3ygi>&b>g==X0YAbdHa1mbLrRKMafG*3#Pu?f1LM1VvF!=vz&vI^);M~ zQ$&}Z{J~EhZ*xg^%~goBfp%U0(U>Xum=A zyZlATOH!BZx>95uT^1gGzhiY?P1v2aMhtrSb-UPi?OUbw>FFvZ>FOI&FZ)_gEE8Iq zVbU-~Jg|a$SC+g6D|dZtYxNJ_GbVq8(;ghX{&!Jw%9FYOeUBV@A9~SH!O3j?>8Gc@YTvs`>(kMyCyE~xWEwSNkQm=Ko^Tc^R7)`$n%S_$&KgP}bS_V1n?v{eSIOtTg?X!O=C_ zG+pyu^_$p>h7V5W&VKdsn>>SscWBDv@;;Tr8w4yJ7KzMdQFvMQQ1``}_ysDiiw*A- zonIZXt!D2FjlRnlB#un|U&W>!HnqEN(Mn$F?T_Z1mfLjaQGs>Dwi;W#ozFZk8Zs>T z^UF-|Kx@scGyftwWRwbDeG+7tRK<2P`aTVQ?_D|Twe2W z$FWWOlI@*qwymfr-+%pSk+K-4g4CTXpB;BIa(EuSexLU6!o$B8cP;TKk%``1Et$1# z>n1T!9siJ>zvcSr>5jIh(-k+rahZR0k@cRRo^Ma>mpCv{$NAhrNu~Bc(Mr`xb2Lxs z-4idE`8aq<<+2ywMdxT<@>UQia*@n3t9#@VQ}uQ-2aD69#W$wiJSD)8vi+9+%=_CU z4hZTvpG)d{UzW1MMN^7*L0qjUd)?{RZ4Y_1NXzKn-XCvPzkG3LrEb;NH!4BZ=e7X5w@rsC;?9q@z4>>?xltmXc_#`%6)@Q%f3CdhQzxqGC ztY0r8o4R~c)E9@a#OZgZF8sfut7dj$Lgx0aVOtVs6x zUn#h5_gmMHt8czZ@)q8?sqv!1=giZaPv2Y!37uGO>FfteuuKe7^yS2LUt5=HH7sVB z^5(~Dz3uD;>57+jT>MesnUTfyc=P&Yi&s@if4jY%{Zmn~i@TBR)6Mb)Z`bNu)$I+~ zv@PuFqtBei6P|uedQfNUdfagCs#jJvU!GXp`Tt`#2g}*rrH+29|9yC}^=7E#Dwcwb zpN|T8-O`kbAMptOZGG}tp8dh;*uPbOcYW+=J-gS`{q-az(2^brt+yXLh1?_dmI=Sx z@t^mPO?BrBjcKMKuX^3z?`zrY%DMTO&e5&g_ja6(K2t9JSO3mUjWT{{1uBL|2<7?B91c z@QPA=iAo}J2U?37WYZ#?HLbTwEn&ISg*Z; z(1v64QdxiQ-0|v>aN)t5$GiHHR&Y*Ed$cp$ZtD@XY46@7J(()oc(aJ(M zx{mb*Cv1BwTb8tf`*=TgcmDU1^FHVIomb#_ea1lb_682-6+ICh zp01UOI)U$Zg-)xtH6;e;jI&V1(H%OKiQzKtYlz(fMN6d%h~@{ zyjqx8^**{ZFVyz_`Q`S!#rfg3%-_pwzlU74eE)ax{byfS$7fr6<$Qbl`pNO_cTPY5 zfA#s&v%SXRH#@jgqn_=Zm{fV;;=|<)$vy91?NY2{6;+a)^q6%z(|xHo8`sU~nUF_SMgS&eU|fd)M~PxA|@BV=J9sO}m%zD#CN=->2Nuthd|yJ-^nj zt0uKO)m`^t?DV}eH=i#sD%Q$fBnU)|7mLe{~6Km z4S2V`Ird`etn`4{wZ8)R|F8ad*WTBte@Q#nM4rtrUi>Dz`{zU6@=u4Q-#vPGeBL(MbSJK* zX<5bRq`s80{^5#P8Jub)BB~@g@iA*U<95Dp5#Q%Mzxh2W`JA7%Tz>86l`@4ln9kXM z7g^>rFU6WqVBWrn-$$zIN*>?+^PtW8a&!}0#=KIgr?=K9%D=z0weRojx`_#kDoo5n zD^Jh%d#3w*-WkDm-JBUWubAiGJjkEFr(3=LvyAphxmLILKV{rimXg2pi!y)wiIeIwXIHAr9TxN!zhJo4cH-Z6x38aEKkv?g zyZdXV-2VT%?wZ}kdA8Nx)i=FY30b~wfz{6qg7df6pLQ;J8!_8qQHqIqXynd60e*HL zGyE(+X7t%56(;?@w=dcJyX_B_{gr9T_Pajn$wlwh+gPi<^irLMuk3NYuOFWGzTanm z(q@t!$W49XCd{uJUpCwBd;Zh<#ohV)UPl~zG{Lpl;*IHNi&r09Wh#FtrTNT0^T>5` z=E~muus7$<>6zJV@ARF${qqmu*A;KVUspUjnI8H3H81b>po+ayqMilKwtOKH-hArQ z>s3)}3w|v899j28@yxud2R|+5PYlY=b-KQ#w5)XVx6ZFQH+MB3pC57lvHzOq7mv?> z_Oi6smj9}Y=Www}ygyJbtu(%oB^y5;A;ZxlQ{clWmq znV-wb-rqdF%=fl*x$Q;^(NkOWDn1viUu}Ou>zk33mWz>EpZFgpiIQ!b=he=>vHN4X zL$V%&wwUOtEoUqzCzZZgE_--Wt@0PMcU6%|{{L;loOc}l@os{%yArc#F8hQlmFENa zr_5S@*~E7D8OAigZo=U=qDC>UqMXGT+N{9-b07z&`1e(#z#eD#=MT-}4^a zS}3>PC(31M?Fr6{D&<@z=PeTNq^^ixS!s3F*w3Z;)}{5A>LjLab8f3XWqPcWPiWWnaYjJ`krdJGC6!c6FMdO*|pT@r?+&bDlr~1x^iV>(vIa{800-Q`chiB zCicwycz5f8(!;jPtx6@APHUd#I9VohR$0v5Qvv&nUjJDk;u$wJ>RHg-T{TOV$EMeO zzwY^D;+qqtibZ=8dLOgy{FiUQD4#kbaFNqOhtEBeHLgBWneHLwc6rO=6Sq8P6h0L_2h8oEV~zH`z8f0omOdb-Xhb4`$gR1iwidv z?@2KGApC&sIQO}2bC0*hh$$WC7Gn-~`M-;+;>O3O?>D`tMAh4066R`EI&16~qIs%w znWTD9X6%F$`?MyDT{8K*!{wW5S!?PG7p~So*-dux<@44Q?s-u3!0^GD7yau>_9&F^ z-%=de!EyS+fnv+ajpr@nKA0ZozO>V4E9wBC(8cN1%_ADOGEEu%l#g$oL{-gTjo{g(T1%ffhL?86uB?>WdULgA$gMSR;vWTP z#(U*HTXk`o;)NF%R#?Vu=qd<#G1uAVsn2olI_rZT*Pgyw<#L>R)d8!77pdFgzsWt` zz1Zr;^SO^%&oeFFH+Mzx?o9z2~=he#X)%?HXc1*sv?(Me4 z9~%0&F{^7@$Wo84_X^OKp=7ge|LHimsK zDh`^<>PwVKg)lx?wb5p$)z5-i>GOD6gJLRG7?<9f)28XnaI}c~>}LPPcXnMU6PdZ- z)|<=6quLn$1*T?H^ogG-+Vj8*WKz`0iz}k@xF$YkRcCrH^|(Kl?ar%;gEN(F4WF-* zd(A&3`q?F;?_WhuO@4Gj*>Lq1{iSBFr?hE0|Lt0uHYfcG|$k17rt9?(5-H2Kd(`_erM;@eA}EfX+2l^x4gsH5e2*l6Exg@Vfw_3I*L zx}3hV*(5x$OGGR8O4@wCVE1J4zE@9{nw+=lb=tU*Yc`LrOrcD<GV|G2d4F>C(n;3aKSe|E2u)z4s>$MUN5{CEBa?w*9-vHRaj1MyaijSB=45}y(>?Fst-Jh9%1_R%LvKM{D#wGG2e(#ac&hwL zEqU*fC*An+^FVW-_?n)?+nHw1 zx>v`q4bZq~^VcG4bNK5Aslf|fN?#d1WL%xO>a9s>VAedh(|0znER)H$_FC6^ec@ck z;NwPIM|a)Hw0d~o8WiE#jawaLvM1Hv`qWl)#avTgEI$(8reL#fpKaq`6}{7~FCKHg zTP^w_w2R@#`yFO6>&`AqeHUqR-p2Ll3b`&(t+(EpGt2p=ZaeyQ*F5D?uGUA5Ng;EK zM0);T%kKoGu6dPQy2a-g#mts`zAmq?_;IFyulDEQ1(pZzs#Vtb`l$KOw2b?Z_U25~ zE!I6(f4uA0eY0E9BF><{z{9lcZuPvS#kZ4p`PQ%L3e{T6y~58nP3iN#l(6`G^SQU> zmQ`P``c-AR`&fj?>fn`rmQ6+8G1K$pQZ|0Pn?C7Q*O+!n6iKKtC;{)>C7 zZENq&+#R!M-Kn*oHMi?5xHBt4km*KBjk)aRmSYD#Hkk{a>rQ`twr20g*z1?}B~RSx z^rYgT(u2&?hDEVmGWt7sJ!)1?zW2_yGsE2$<&yJJ zX9B__W3(3vs`ag6`2VoNG0yv9a)gG<(IReN+0&V?BIINbT)VirJo%ELl+(g1PL-FO zKyJ^pTh6)Khl@KkV%Piy)$iSY?JB#~m+HE*QtYWG_e!6r6zfxyw=v(ippeRO=wE^1 zhw^E~SA4~|FaF;ss}RJv?{&pN>E8GElC6CX$!(Th@Sow@^~;{$-+e848|VK1-`Q=+ zM^^>pEV^PeyXdu&*I8pD%k~Mmi7(?Ex8_^g{*J5q8IpT!I*X>6?{cF|#i!FM3-9Wh z$z|uVdqkPMd=z>^-}&#q1|HExK7Wt(#OZ9j`_#lwpYi$Al_HjL1&X{D)j9G0>UXQ> zFR#9CRkeFt`lBsT3vakoUSc}6mRmDZFgUyFTkfLl>&9Q++@I1pak10N)}&ISr_(H_ zM2iP$m_12<-re)Q?s{3r5{tM3n}@Nby=C2R3y+omKVH^Rm2o2cC(E9TWetvJ80wx> z98^43UVf8r@`UF(>=Hf;LzWt43QqOxe)0PC;_cbL8;h>oo;j&0WKBTcOz(|%+*YRY zEq#@8yOTx3)ZgCVcTxUzzg^$=Ih?=O`{m7jroD5gUG8<-=(WhyH8!%dtl2mSqCYSd=WfosOQ0gG}uIomw^?`~htAasp$&4;~aK38y_lvi;Hs4z(`{nDK z_uj33@%o-`eE#(o{+!k%VG(T+?a3O`qBTQqX-tb=th8k21p_V>c|)_?ruYinznyMpeY_SXwJlx@hR%C%ID>!@1nk2%3di@Ysv znV(?VT)X!n>#e=9w-?vHZ`-%7**s{4OvPUjud@Mb0yM%jMKRe{p)xW7YSe<$2v_st6pa)#H71gwabO_yvp zk!z1v)Nak#_?F?Z$z+~6<+3L}HgRdWC<*Nk-dr$iS^Sssos&wYDSEsw{L!#RWRZ#a z<U_7&RZgNxI`HxNe zH^?sXv$@UaqGDcmx~!q9V9x{P1FqYOeJJD_VYTBz z8H3=--uqi+6BaGH|66|Z+{J!DwKnIR6r<9t9b{`ye{4$5ZI{qL&V7zu?8Z4orJaVi zKQ>*NrgWTJ=E3Q0#a9AX^&gMaaM_&0zGAW319eS3SNScn4lYH{lWsE}JN&WfaO3R; z9bJ{_uO1hQaa|0$&G;(E>cjg!@iX!tXYwBT*wo!HIp?{+#5=d$VmJ8&npF>+zZBH9DEjT@BEA@xOV5-0#7%DU8A@Gz|8>6Lzj^0pg~<1Z zc1VTSYg!#Cn-KUz;)B#}MHiLr(} zRz_)3wS38*2XD7I^Qi7h(>R^V5maa6cR5Ovy|c~Z`^|F`8}=T^m%P9CWZ8s98GQ@h z9Wx7>OaujQmoeuX?5m#sR)N_vu0ZG^>lNK?n^hL1)tFB`{&Y*fKHoaB&E2Cc_USR%H!+FA^$D6nUcm3J?n05Q@eY*EQ zHkAuZTexS>L01*qU)~p&nmJpvn=j#@!Cj2No^s(u5V|rt#=lk7^OTO)x^Udi>wsnJQ za6`UT+=tgM7CpOR8CSsffn&l{)9WrOzxCMV3il-V3RxHLdGK9E|A*-Z*Ps|4wH4>f z8a{gccD>r&xIE{127`3N>c-W*@9jd?_gcjTFx>O7VE!uk_13zM4%Tvpc(Z-gv9{)~ zDh_fz2znrt5M{f4Zjg7+d%IlgBNZzIQqRvl@b|#>#^1f~%X*)-FBaO;Q}FaDcVmbT zpQe(LS9as;hWCv9Quk#qceFibOoCzb~l2kj1pK5}(&IWjNy zSo!Uoc8T>{?x(V8<}FrIQo1CuDWGHCc3IHjWfd378ahunDOO!91D$=Jxcn`H@WfW9 zlKpQPl$4Yvy#pn>i*C0WU0hsL?jC=vBeE#OJV;P*Vvonyj~3jmFT%GmcXV`cWaqNa z=(r-T^mzAq#`B;IrZg#8U-G{1*Vy|j^<5?&&t(@76g;_c{tZ5Zz(`FOdr;k3P+m`wG0tK!Ho}HO`NVs^t}Hzq4B?@l9H0g5VBnVm-4OtCZIOoX@%v{)lpgQC-53vXk@9|% zeY?+=byS`3QS7QY4)X9snUiM<&AVGO=D!tCQc|+qATPS;n8;kS-xJ@;fcPKeBQ0au zB(+^8zQ4_b^f))qhD%_3r|dfZ4Bt1g{L|6`gcJPbYQ*`3l<&$BV==ZLUk)|0{l5anh9CP9l};8xEFnWJ>JjbZ?bo z(Az%8RmF3@>Aq_94{RY_Pg40A z@#YGVr?Gmc1)l5x`ROm`hvVCdJ4F98&$%7nCZiw0JSWF|QirIz>-zeh_jYa(UH@+9 zscd{wd#s%Kwqnl7Qbi_%eZNB=yw7<)!6lYeCcozUMaKHFiE9m%cpn^iegDV1=e)BC(D{+zZIUDWaMuHJ+4lP5)r+*TvL!y@XW;`fqMIRbs+HO(LROg#7ZGtckgdhaNsZ^604`lg3b$JYZ) zw-sB0+h)(a@v-SV^Lt-|{v*#{A1nWVp{&Dgk>DaniTi(RALQpepTHT(J}0r}dw!di zv*294Y##XyvJMP8a(1xyzTd}hAzPs~(L`wa{xC_a8C(|ydfwN~DTtGrYwbUOHs^b# z-{&9Bds5-ZE4s*S!@VExwze#GNxGieC%%Q}JImwU_RM|kd0uLhu3W5pAoO_m{4KIe z1Xl|19{%xeYRl&BdE4i@x;>RV^!ed)iTkyMBIoxr%#bQplf3C4|26i_N``#-m!8MU z%1Sj2ruebArtd_s5y+m16o z-pMBaR6Nejb--+aZC`*64$$Gf*n-nSL|vA@(`3M(8!D7ju4X!iB`*v^t&o85&!EUzba@w|B z!+q8dzqdbTUERRXls~gVMY6T@fO^mSy1Qi)Oo9|=DIDis#xS4ZUaqxw+g+xboE@Bx zj~)8hbbZm)J<^T&HQ&qQ{9?Sw9Jq?Gt~)JWJ?|GW+kHAMaj%bYsI~R_z9R^%(o0W6uL0G#@Kpzg<>E z+r%j({aE>W=4rxydEYvoT4a8*`4IU)K*aptux+h1i*`*g8-%7-7Dd{S5JGjS3z2DM@DvF|xoHjS;+_~F5it2Umi zHj?WTpW|rZIrGNWJ3rnzJKMgfXlxD(&u3XKb$_q%ySXl>Tle%9Jj?c-VYMTk>Ayhj zi)j)o1XPcFYvHJ*)b)AMf5**_nKt`yO+R;>p@mj(H~gelMFi?@7hM z*p};aI&9|~?E5`YrTj(3L8U{frFsreGo&Ai=*JuETP^LF6t+!PL4T3>XV;6lN1qF> zd$;#Y-YOScuZoZNr+(d4oI2;+#vS>bz26qS^e_Hk<2q?g#5zT(zK?g)8_qM#FWi$L z`Qi70nNMDSf2!PKE?eU*?xpUo>>_3f>T!2myxFqaJ^ifhfvpGD9xIoARHb#C+l*;* z&hrx+w;n6+mpCiD_-W`oCb6T^o^2a1{&**ITajT=(U+w(9ir%rt=VnX*_Z%K3BKcFu(?{eLY4IH*`eXQL4+FyO8 z&WjuqpR_$@-OikTHStNS z-9xp?hTNoG@#uZCyW8w$Q!h-aDT39Px%YTX1E2+=EFO zqMweqaHs84Jz3l3VYX7hGcISdtioeG!+qJQms}q2E@yndY2$ktnA4P|S@Rz*5 z_f{Iiop+TS=gF>voT=8hx2tsb8a-Qb(x z7|-pQvzI`Xb zJMpo}y0M<|mX7zO@kGHu>wTZo%98GdF?iB7x;QKJ=tqf-+!w#k$cYnNd-gbD& z)W@vHnbxz{Nn2bjnzQ7L@xIy9zjtG3$NCy6a_qR&#B=0_P9mYDYGYnC>$0bj4A6OIq z_on{h_urYyh4s9Pwtu|4PNkXCBJP4uMc}uSEv%WTUoLQ7bLu@Gmk|Bs{Bp_rwU^Q) zCN5bi(CGqdeJo=RZjAb}S9FHZ-`^q&Y=6z-F>(DL(>d?DlSR-4H;cfy-x_7NdEYSZ zTJ%1egWo55(fa4T@9PeHWD>u@nq$5%`_+f*Q)gJl1u%&*c`sVO@<2|4TH?W6)f!pf zu75kden_qAN&0Nf5pQi>`E%>$_JSz|>>KQV?PI>-^<&m8uOCvkj-7tGoa+vMLe2K# zr21sRfdCUte#8sa*Ep;Y>8)g7VBkxQ@J#ddWzb?^VBlbYfJ?!XK@3k + + + + + + Octobot for Discord @@ -23,11 +29,11 @@
- Octobot icon + Octobot icon Veemo!
- I'm a general-purpose bot for moderation written by Team Octolings in C# and Remora.Discord! + I'm a general-purpose Discord bot for moderation written by Team Octolings in C# and Remora.Discord!
Mem Cake (Sardinium) From 930b7ca6eda76a09883296d8657cf82a4e3e2d21 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 3 Jul 2024 22:09:39 +0500 Subject: [PATCH 309/329] Bump InspectCode from 1.11.10 to 1.11.12 (#323) I <3 breaking changes. --- .github/workflows/build-pr.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index c92386e..b2991db 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -22,8 +22,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.301' + - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.10 + uses: muno92/resharper_inspectcode@1.11.12 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 07e8784d2e10e0db5dd8b1645b95d674dfbaa1a6 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:12:32 +0500 Subject: [PATCH 310/329] Redesign reminder-related commands (#321) In this PR, I redesigned the reminder-related commands and they will now have a quote block instead of a inline code block (to avoid some visual bugs). Except /listremind. It just has the inline code block removed. ![image](https://github.com/TeamOctolings/Octobot/assets/95250141/3521af97-ee11-405f-8cc2-7bf9a747e757) --- TeamOctolings.Octobot/Commands/RemindCommandGroup.cs | 10 +++++----- .../Extensions/MarkdownExtensions.cs | 12 ++++++++++++ .../Services/Update/MemberUpdateService.cs | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs index be53ed7..3188d27 100644 --- a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs @@ -94,7 +94,7 @@ public sealed class RemindCommandGroup : CommandGroup { var reminder = data.Reminders[i]; builder.AppendBulletPointLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString()))) - .AppendSubBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) + .AppendSubBulletPointLine(string.Format(Messages.ReminderText, reminder.Text)) .AppendSubBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))) .AppendSubBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); } @@ -182,7 +182,7 @@ public sealed class RemindCommandGroup : CommandGroup }); var builder = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(text))) + .AppendLine(MarkdownExtensions.Quote(text)) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderCreated, executor.GetTag()), executor) @@ -279,7 +279,7 @@ public sealed class RemindCommandGroup : CommandGroup data.Reminders.RemoveAt(index); var builder = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(oldReminder.Text))) + .AppendLine(MarkdownExtensions.Quote(oldReminder.Text)) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderEdited, executor.GetTag()), executor) @@ -309,7 +309,7 @@ public sealed class RemindCommandGroup : CommandGroup data.Reminders.RemoveAt(index); var builder = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(value))) + .AppendLine(MarkdownExtensions.Quote(value)) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(oldReminder.At))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderEdited, executor.GetTag()), executor) @@ -367,7 +367,7 @@ public sealed class RemindCommandGroup : CommandGroup var reminder = data.Reminders[index]; var description = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) + .AppendLine(MarkdownExtensions.Quote(reminder.Text)) .AppendBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); data.Reminders.RemoveAt(index); diff --git a/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs index 202cd37..30ddff5 100644 --- a/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs @@ -13,4 +13,16 @@ public static class MarkdownExtensions { return $"- {text}"; } + + /// + /// Formats a string to use Markdown Quote formatting. + /// + /// The input text to format. + /// + /// A markdown-formatted quote string. + /// + public static string Quote(string text) + { + return $"> {text}"; + } } diff --git a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs index 0c49c24..3170060 100644 --- a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs @@ -234,7 +234,7 @@ public sealed partial class MemberUpdateService : BackgroundService } var builder = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) + .AppendLine(MarkdownExtensions.Quote(reminder.Text)) .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); From d6d2660fb02b8eb3979f2acc16869d037c7d8b04 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:13:29 +0300 Subject: [PATCH 311/329] Show an error when entering the same value from the settings (#326) Closes #324 --- .../Commands/SettingsCommandGroup.cs | 21 +++++++++++++++++++ .../Data/Options/BoolOption.cs | 10 +++++++++ .../Data/Options/GuildOption.cs | 12 ++++++++++- .../Data/Options/IGuildOption.cs | 1 + .../Data/Options/LanguageOption.cs | 5 ++--- .../Data/Options/TimeSpanOption.cs | 10 +++++++++ TeamOctolings.Octobot/Messages.Designer.cs | 6 ++++++ TeamOctolings.Octobot/Messages.resx | 3 +++ TeamOctolings.Octobot/Messages.ru.resx | 3 +++ 9 files changed, 67 insertions(+), 4 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs index 0acaa88..15aa42b 100644 --- a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs @@ -202,6 +202,27 @@ public sealed class SettingsCommandGroup : CommandGroup IGuildOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot, CancellationToken ct = default) { + var equalsResult = option.ValueEquals(data.Settings, value); + if (!equalsResult.IsSuccess) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot) + .WithDescription(equalsResult.Error.Message) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + if (equalsResult.Entity) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot) + .WithDescription(Messages.SettingValueEquals) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + var setResult = option.Set(data.Settings, value); if (!setResult.IsSuccess) { diff --git a/TeamOctolings.Octobot/Data/Options/BoolOption.cs b/TeamOctolings.Octobot/Data/Options/BoolOption.cs index 6a3c899..3b81abb 100644 --- a/TeamOctolings.Octobot/Data/Options/BoolOption.cs +++ b/TeamOctolings.Octobot/Data/Options/BoolOption.cs @@ -12,6 +12,16 @@ public sealed class BoolOption : GuildOption return Get(settings) ? Messages.Yes : Messages.No; } + public override Result ValueEquals(JsonNode settings, string value) + { + if (!TryParseBool(value, out var boolean)) + { + return new ArgumentInvalidError(nameof(value), Messages.InvalidSettingValue); + } + + return Value(settings).Equals(boolean.ToString()); + } + public override Result Set(JsonNode settings, string from) { if (!TryParseBool(from, out var value)) diff --git a/TeamOctolings.Octobot/Data/Options/GuildOption.cs b/TeamOctolings.Octobot/Data/Options/GuildOption.cs index 5d9f1a2..ea9c30e 100644 --- a/TeamOctolings.Octobot/Data/Options/GuildOption.cs +++ b/TeamOctolings.Octobot/Data/Options/GuildOption.cs @@ -21,9 +21,19 @@ public class GuildOption : IGuildOption public string Name { get; } + protected virtual string Value(JsonNode settings) + { + return Get(settings).ToString() ?? throw new InvalidOperationException(); + } + public virtual string Display(JsonNode settings) { - return Markdown.InlineCode(Get(settings).ToString() ?? throw new InvalidOperationException()); + return Markdown.InlineCode(Value(settings)); + } + + public virtual Result ValueEquals(JsonNode settings, string value) + { + return Value(settings).Equals(value); } /// diff --git a/TeamOctolings.Octobot/Data/Options/IGuildOption.cs b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs index a8c3e6e..9920281 100644 --- a/TeamOctolings.Octobot/Data/Options/IGuildOption.cs +++ b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs @@ -7,6 +7,7 @@ public interface IGuildOption { string Name { get; } string Display(JsonNode settings); + Result ValueEquals(JsonNode settings, string value); Result Set(JsonNode settings, string from); Result Reset(JsonNode settings); } diff --git a/TeamOctolings.Octobot/Data/Options/LanguageOption.cs b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs index 15ab6ff..f58e011 100644 --- a/TeamOctolings.Octobot/Data/Options/LanguageOption.cs +++ b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Text.Json.Nodes; -using Remora.Discord.Extensions.Formatting; using Remora.Results; namespace TeamOctolings.Octobot.Data.Options; @@ -16,9 +15,9 @@ public sealed class LanguageOption : GuildOption public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { } - public override string Display(JsonNode settings) + protected override string Value(JsonNode settings) { - return Markdown.InlineCode(settings[Name]?.GetValue() ?? "en"); + return settings[Name]?.GetValue() ?? "en"; } /// diff --git a/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs index 3501f09..7e21343 100644 --- a/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs +++ b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs @@ -8,6 +8,16 @@ public sealed class TimeSpanOption : GuildOption { public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { } + public override Result ValueEquals(JsonNode settings, string value) + { + if (!TimeSpanParser.TryParse(value).IsDefined(out var span)) + { + return new ArgumentInvalidError(nameof(value), Messages.InvalidSettingValue); + } + + return Value(settings).Equals(span.ToString()); + } + public override TimeSpan Get(JsonNode settings) { var property = settings[Name]; diff --git a/TeamOctolings.Octobot/Messages.Designer.cs b/TeamOctolings.Octobot/Messages.Designer.cs index bbc1366..ce59f1e 100644 --- a/TeamOctolings.Octobot/Messages.Designer.cs +++ b/TeamOctolings.Octobot/Messages.Designer.cs @@ -1196,5 +1196,11 @@ namespace TeamOctolings.Octobot { return ResourceManager.GetString("SettingsModeratorRole", resourceCulture); } } + + internal static string SettingValueEquals { + get { + return ResourceManager.GetString("SettingValueEquals", resourceCulture); + } + } } } diff --git a/TeamOctolings.Octobot/Messages.resx b/TeamOctolings.Octobot/Messages.resx index 47e7d4f..059584a 100644 --- a/TeamOctolings.Octobot/Messages.resx +++ b/TeamOctolings.Octobot/Messages.resx @@ -681,4 +681,7 @@ Moderator role + + The setting value is the same as the input value. + diff --git a/TeamOctolings.Octobot/Messages.ru.resx b/TeamOctolings.Octobot/Messages.ru.resx index 2eef257..fc8a594 100644 --- a/TeamOctolings.Octobot/Messages.ru.resx +++ b/TeamOctolings.Octobot/Messages.ru.resx @@ -681,4 +681,7 @@ Роль модератора + + Значение настройки такое же, как и вводное значение. + From e457b4609ed7630ac8920cea90c9e1282b4914ca Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 31 Jul 2024 23:57:21 +0500 Subject: [PATCH 312/329] Don't log stack traces for cancelled operations (#327) This PR fixes an issue where the `LogResultStackTrace` method would log stack traces for results that encountered an error due to a cancelled operation/task. The `LoggerExtensions` class already skipped `TaskCanceledException`s, but didn't skip `OperationCanceledException`s (which is a parent of `TaskCanceledException`). The patch specifically does not affect *inner* results which are canceled. Skipping logging these could hide the true cause of an error which appears important --- TeamOctolings.Octobot/Extensions/LoggerExtensions.cs | 2 +- TeamOctolings.Octobot/Extensions/ResultExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs index 76fa386..fac4dda 100644 --- a/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs @@ -25,7 +25,7 @@ public static class LoggerExtensions if (result.Error is ExceptionError exe) { - if (exe.Exception is TaskCanceledException) + if (exe.Exception is OperationCanceledException) { return; } diff --git a/TeamOctolings.Octobot/Extensions/ResultExtensions.cs b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs index d7ef7d7..6872d34 100644 --- a/TeamOctolings.Octobot/Extensions/ResultExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs @@ -23,7 +23,7 @@ public static class ResultExtensions private static void LogResultStackTrace(Result result) { - if (result.IsSuccess) + if (result.IsSuccess || result.Error is ExceptionError { Exception: OperationCanceledException }) { return; } From d1133124b8eaddb7f9a6891ef9bd678de0a046f0 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 24 Aug 2024 20:48:47 +0500 Subject: [PATCH 313/329] Apply new inspection fixes (#329) Rider and R# 2024.2 have introduced new inspections, causing current builds to fail. This PR applies fixes for issues caught by these inspections. --- TeamOctolings.Octobot/Commands/AboutCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/BanCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Commands/ClearCommandGroup.cs | 2 +- .../Commands/Events/ErrorLoggingPostExecutionEvent.cs | 2 +- TeamOctolings.Octobot/Commands/KickCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/MuteCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Data/Reminder.cs | 10 +++++----- .../Responders/GuildLoadedResponder.cs | 2 +- .../Services/Update/ScheduledEventUpdateService.cs | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs index dbb8b12..4575259 100644 --- a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs @@ -131,7 +131,7 @@ public sealed class AboutCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton }) + new ActionRowComponent([repositoryButton, wikiButton, issuesButton]) }), ct); } } diff --git a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs index 69be80f..1d6b26c 100644 --- a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs @@ -62,7 +62,7 @@ public sealed class BanCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user - /// was banned and vice-versa. + /// was banned and vice versa. /// /// [Command("ban", "бан")] @@ -219,7 +219,7 @@ public sealed class BanCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user - /// was unbanned and vice-versa. + /// was unbanned and vice versa. /// /// /// diff --git a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs index 7c1b516..7f29581 100644 --- a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs @@ -51,7 +51,7 @@ public sealed class ClearCommandGroup : CommandGroup /// The user whose messages will be cleared. /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages - /// were cleared and vice-versa. + /// were cleared and vice versa. /// [Command("clear", "очистить")] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] diff --git a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 7409d3b..ff7339f 100644 --- a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -81,7 +81,7 @@ public sealed class ErrorLoggingPostExecutionEvent : IPostExecutionEvent return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent(new[] { issuesButton }) + new ActionRowComponent([issuesButton]) }), ct) ); } diff --git a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs index a8fea2a..3011375 100644 --- a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs @@ -57,7 +57,7 @@ public sealed class KickCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was kicked and vice-versa. + /// was kicked and vice versa. /// [Command("kick", "кик")] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] diff --git a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs index 46e8d84..5dce0b6 100644 --- a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs @@ -59,7 +59,7 @@ public sealed class MuteCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was muted and vice-versa. + /// was muted and vice versa. /// /// [Command("mute", "мут")] @@ -235,7 +235,7 @@ public sealed class MuteCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was unmuted and vice-versa. + /// was unmuted and vice versa. /// /// /// diff --git a/TeamOctolings.Octobot/Data/Reminder.cs b/TeamOctolings.Octobot/Data/Reminder.cs index c3936da..40f29e1 100644 --- a/TeamOctolings.Octobot/Data/Reminder.cs +++ b/TeamOctolings.Octobot/Data/Reminder.cs @@ -1,9 +1,9 @@ namespace TeamOctolings.Octobot.Data; -public struct Reminder +public sealed record Reminder { - public DateTimeOffset At { get; init; } - public string Text { get; init; } - public ulong ChannelId { get; init; } - public ulong MessageId { get; init; } + public required DateTimeOffset At { get; init; } + public required string Text { get; init; } + public required ulong ChannelId { get; init; } + public required ulong MessageId { get; init; } } diff --git a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs index b420db2..b24ef0b 100644 --- a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs @@ -120,6 +120,6 @@ public sealed class GuildLoadedResponder : IResponder ); return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, - components: new[] { new ActionRowComponent(new[] { issuesButton }) }, ct: ct); + components: new[] { new ActionRowComponent([issuesButton]) }, ct: ct); } } diff --git a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs index ef145aa..389a6a8 100644 --- a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs @@ -229,7 +229,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.EventNotificationChannel.Get(settings), roleMention, embedResult: embed, - components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); + components: new[] { new ActionRowComponent([button]) }, ct: ct); } private static Result GetExternalScheduledEventCreatedEmbedDescription( From afd0141c13808dd4f06fba9103b2aaebcb1e062a Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:50:32 +0300 Subject: [PATCH 314/329] /about: Replace repo link with website link (#328) A some sort of UX change. Repository link will be still accessible from the website. --- TeamOctolings.Octobot/BuildInfo.cs | 4 +++- TeamOctolings.Octobot/Commands/AboutCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Messages.Designer.cs | 4 ++-- TeamOctolings.Octobot/Messages.resx | 4 ++-- TeamOctolings.Octobot/Messages.ru.resx | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/TeamOctolings.Octobot/BuildInfo.cs b/TeamOctolings.Octobot/BuildInfo.cs index 4b9a09f..a91e7f3 100644 --- a/TeamOctolings.Octobot/BuildInfo.cs +++ b/TeamOctolings.Octobot/BuildInfo.cs @@ -2,7 +2,9 @@ public static class BuildInfo { - public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; + public const string WebsiteUrl = "https://teamoctolings.github.io/Octobot"; + + private const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; public const string IssuesUrl = $"{RepositoryUrl}/issues"; diff --git a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs index 4575259..28caccf 100644 --- a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs @@ -106,9 +106,9 @@ public sealed class AboutCommandGroup : CommandGroup var repositoryButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonOpenRepository, + Messages.ButtonOpenWebsite, new PartialEmoji(Name: "\ud83c\udf10"), // 'GLOBE WITH MERIDIANS' (U+1F310) - URL: BuildInfo.RepositoryUrl + URL: BuildInfo.WebsiteUrl ); var wikiButton = new ButtonComponent( diff --git a/TeamOctolings.Octobot/Messages.Designer.cs b/TeamOctolings.Octobot/Messages.Designer.cs index ce59f1e..1a81e02 100644 --- a/TeamOctolings.Octobot/Messages.Designer.cs +++ b/TeamOctolings.Octobot/Messages.Designer.cs @@ -633,9 +633,9 @@ namespace TeamOctolings.Octobot { } } - internal static string ButtonOpenRepository { + internal static string ButtonOpenWebsite { get { - return ResourceManager.GetString("ButtonOpenRepository", resourceCulture); + return ResourceManager.GetString("ButtonOpenWebsite", resourceCulture); } } diff --git a/TeamOctolings.Octobot/Messages.resx b/TeamOctolings.Octobot/Messages.resx index 059584a..e4107fb 100644 --- a/TeamOctolings.Octobot/Messages.resx +++ b/TeamOctolings.Octobot/Messages.resx @@ -399,8 +399,8 @@ Developers: - - Octobot's source code + + Open Website About {0} diff --git a/TeamOctolings.Octobot/Messages.ru.resx b/TeamOctolings.Octobot/Messages.ru.resx index fc8a594..d942cec 100644 --- a/TeamOctolings.Octobot/Messages.ru.resx +++ b/TeamOctolings.Octobot/Messages.ru.resx @@ -399,8 +399,8 @@ Разработчики: - - Исходный код Octobot + + Открыть веб-сайт О боте {0} From 0fc53990b96cf9c901790c8c936a1e70d7e0ac37 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:11:54 +0300 Subject: [PATCH 315/329] =?UTF-8?q?Update=20song=20list=20with=20new=20Spl?= =?UTF-8?q?atoon=E2=84=A2=20soundtracks=20(#266)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes in the list: 1. Fly Octo Fly ~ Ebb & Flow (Octo) / Off the Hook → Spectrum Obligato ~ Ebb & Flow (Out of Order) / Off the Hook feat. Dedf1sh 2. `#47` onward / Dedf1sh feat. Off the Hook 3. EchΘ Θnslaught / Free Association 4. Short Order / Off the Hook 5. Fins in the Air / Deep Cut Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- TeamOctolings.Octobot/Services/Update/SongUpdateService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs index b07256f..d0b46ae 100644 --- a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs @@ -29,7 +29,11 @@ public sealed class SongUpdateService : BackgroundService ("Callie", "Bomb Rush Blush", new TimeSpan(0, 2, 18)), ("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)), ("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)), - ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5)) + ("Off the Hook feat. Dedf1sh", "Spectrum Obligato ~ Ebb & Flow (Out of Order)", new TimeSpan(0, 4, 30)), + ("Dedf1sh feat. Off the Hook", "#47 onward", new TimeSpan(0, 4, 40)), + ("Free Association", "EchΘ Θnslaught", new TimeSpan(0, 2, 52)), + ("Off the Hook", "Short Order", new TimeSpan(0, 3, 36)), + ("Deep Cut", "Fins in the Air", new TimeSpan(0, 3, 1)) ]; private static readonly (string Author, string Name, TimeSpan Duration)[] SpecialSongList = From 086cb672f001d3c194311371b9f16ea796201974 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 25 Aug 2024 16:03:31 +0500 Subject: [PATCH 316/329] Fix wrong private key file name (#330) The key used for deploy is Ed25519, not RSA Signed-off-by: Octol1ttle --- .github/workflows/build-push.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index a757eb2..1b63a9d 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -27,8 +27,8 @@ jobs: - name: Setup SSH key run: | - install -m 600 -D /dev/null ~/.ssh/id_rsa - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + install -m 600 -D /dev/null ~/.ssh/id_ed25519 + echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts shell: bash env: From 8028d47ba1b5de1d0dd5eb9c0eb52e90a6f70d51 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:55:58 +0300 Subject: [PATCH 317/329] Fix redundant type specification (#333) This PR fixes failing checks for #332 --- TeamOctolings.Octobot/Services/Update/SongUpdateService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs index d0b46ae..8eaa4c2 100644 --- a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs @@ -41,7 +41,7 @@ public sealed class SongUpdateService : BackgroundService ("Squid Sisters", "Maritime Memory", new TimeSpan(0, 2, 47)) ]; - private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)]; + private readonly List _activityList = [new("with Remora.Discord", ActivityType.Game)]; private readonly DiscordGatewayClient _client; private readonly GuildDataService _guildData; From 450f1da54df36611894ce23876d9bc698efdf8c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:59:32 +0000 Subject: [PATCH 318/329] Bump muno92/resharper_inspectcode from 1.11.12 to 1.12.0 (#332) --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index b2991db..2260918 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -28,7 +28,7 @@ jobs: dotnet-version: '8.0.301' - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.12 + uses: muno92/resharper_inspectcode@1.12.0 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 9e44d49039792c2b94a7a87745780b108bb22a67 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 3 Oct 2024 23:18:25 +0500 Subject: [PATCH 319/329] Update dependencies (#334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this is supposed to be Dependabot's job, but ig something happened to it Bumps `JetBrains.Annotations` from `2023.3.0` to `2024.2.0`
Changelog • Added DefaultEqualityUsageAttribute for equality members usage analysis. • MustDisposeResourceAttribute is now allowed on struct types. • Added ability to specify the description for UsedImplicitlyAttribute (new 'Reason' property). • Added copyright information to nuspec.
Bumps `Remora.Commands` from `10.0.5` to `10.0.6`
Changelog Upgrade Remora.Sdk. Upgrade nuget packages.
Bumps `Remora.Discord.Extensions` from `5.3.5` to `5.3.6` Bumps `Remora.Discord.Interactivity` from `4.5.4` to `5.0.0`
Changelog Update dependencies. BREAKING: Rework deletion logic for data leases to prevent deadlocks.
The breaking change in Remora should not affect us. Signed-off-by: Octol1ttle --- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 19e37f9..5c6da6d 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -24,14 +24,14 @@ - + - + - + - + From 84dc248038241c9783dfa7b034d20d505e5f2f30 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 27 Oct 2024 21:34:01 +0500 Subject: [PATCH 320/329] Add Dockerfile and compose.yaml (#335) This PR adds Dockerfile, to run Octobot within a Docker container, and compose.yaml, to run the Octobot container along with any services that the user may require. --------- Signed-off-by: Octol1ttle Signed-off-by: mctaylors Co-authored-by: mctaylors --- .github/workflows/build-push.yml | 50 ++++++++++++++++++++------------ .gitignore | 1 + Dockerfile | 15 ++++++++++ compose.example.yaml | 17 +++++++++++ 4 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 Dockerfile create mode 100644 compose.example.yaml diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 1b63a9d..ae355cd 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -8,29 +8,43 @@ on: branches: [ "master" ] jobs: - upload-solution: - name: Upload Octobot to production + upload-image: + name: Upload Octobot Docker image runs-on: ubuntu-latest permissions: - actions: read - contents: read + packages: write environment: production steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Publish solution - run: dotnet publish $PUBLISH_FLAGS - env: - PUBLISH_FLAGS: ${{vars.PUBLISH_FLAGS}} + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + push: true + tags: ghcr.io/${{vars.NAMESPACE}}/${{vars.IMAGE_NAME}}:latest + build-args: | + BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 + PUBLISH_OPTIONS=${{vars.PUBLISH_OPTIONS}} + update-production: + name: Update Octobot on production + runs-on: ubuntu-latest + environment: production + needs: upload-image + + steps: - name: Setup SSH key run: | install -m 600 -D /dev/null ~/.ssh/id_ed25519 echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts - shell: bash + shell: bash {0} env: SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}} SSH_HOST: ${{secrets.SSH_HOST}} @@ -38,26 +52,26 @@ jobs: - name: Stop currently running instance run: | ssh $SSH_USER@$SSH_HOST $STOP_COMMAND - shell: bash + shell: bash {0} env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} STOP_COMMAND: ${{vars.STOP_COMMAND}} - - name: Upload published solution + - name: Update Docker image run: | - scp -r $UPLOAD_FROM $SSH_USER@$SSH_HOST:$UPLOAD_TO - shell: bash + ssh $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest + shell: bash {0} env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} - UPLOAD_FROM: ${{vars.UPLOAD_FROM}} - UPLOAD_TO: ${{vars.UPLOAD_TO}} + NAMESPACE: ${{vars.NAMESPACE}} + IMAGE_NAME: ${{vars.IMAGE_NAME}} - name: Start new instance run: | ssh $SSH_USER@$SSH_HOST $START_COMMAND - shell: bash + shell: bash {0} env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} diff --git a/.gitignore b/.gitignore index f97f6b8..fcda727 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ riderModule.iml /.vs/ GuildData/ Logs/ +compose.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ef831a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build-env +WORKDIR /Octobot + +# Copy everything +COPY . ./ +# Load build argument with publish options +ARG PUBLISH_OPTIONS="-c Release" +# Build and publish a release +RUN dotnet publish ./TeamOctolings.Octobot $PUBLISH_OPTIONS -o out + +# Build runtime image +FROM mcr.microsoft.com/dotnet/runtime:8.0@sha256:a335dccd3231f7f9e2122691b21c634f96e187d3840c8b7dbad61ee09500e408 +WORKDIR /Octobot +COPY --from=build-env /Octobot/out . +ENTRYPOINT ["./TeamOctolings.Octobot"] diff --git a/compose.example.yaml b/compose.example.yaml new file mode 100644 index 0000000..522281f --- /dev/null +++ b/compose.example.yaml @@ -0,0 +1,17 @@ +services: + octobot: + container_name: octobot + build: + context: . + args: + - PUBLISH_OPTIONS + environment: + - BOT_TOKEN + volumes: + - guild-data:/Octobot/GuildData + - logs:/Octobot/Logs + restart: unless-stopped + +volumes: + guild-data: + logs: From 8a42ecd1ab30172399ffd4c4f9190f75592282f8 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 28 Oct 2024 09:51:24 +0500 Subject: [PATCH 321/329] Retrieve SSH port from environment secrets (#337) Fixes current deployment failure due to use of non-standard SSH port on our production host. `ssh-keyscan` command was moved out to its own step to help troubleshooting in the future. Signed-off-by: Octol1ttle --- .github/workflows/build-push.yml | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index ae355cd..af3c4cb 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -39,30 +39,38 @@ jobs: needs: upload-image steps: - - name: Setup SSH key + - name: Copy SSH key run: | install -m 600 -D /dev/null ~/.ssh/id_ed25519 echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 - ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts - shell: bash {0} + shell: bash env: SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}} + + - name: Generate SSH known hosts file + run: | + ssh-keyscan -H $SSH_HOST -p $SSH_PORT > ~/.ssh/known_hosts + shell: bash + env: SSH_HOST: ${{secrets.SSH_HOST}} - + SSH_PORT: ${{secrets.SSH_PORT}} + - name: Stop currently running instance run: | - ssh $SSH_USER@$SSH_HOST $STOP_COMMAND - shell: bash {0} + ssh -p $SSH_PORT $SSH_USER@$SSH_HOST $STOP_COMMAND + shell: bash env: + SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} STOP_COMMAND: ${{vars.STOP_COMMAND}} - name: Update Docker image run: | - ssh $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest - shell: bash {0} + ssh -p $SSH_PORT $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest + shell: bash env: + SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} NAMESPACE: ${{vars.NAMESPACE}} @@ -70,9 +78,10 @@ jobs: - name: Start new instance run: | - ssh $SSH_USER@$SSH_HOST $START_COMMAND - shell: bash {0} + ssh -p $SSH_PORT $SSH_USER@$SSH_HOST $START_COMMAND + shell: bash env: + SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} START_COMMAND: ${{vars.START_COMMAND}} From bfee88914951dce168f9e4f02797b60932d60814 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 28 Oct 2024 10:04:08 +0500 Subject: [PATCH 322/329] ssh-keyscan is a dumb command (#338) Signed-off-by: Octol1ttle --- .github/workflows/build-push.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index af3c4cb..e7afe8e 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -5,7 +5,7 @@ concurrency: on: push: - branches: [ "master" ] + branches: [ "master", "deploy-test" ] jobs: upload-image: @@ -49,7 +49,7 @@ jobs: - name: Generate SSH known hosts file run: | - ssh-keyscan -H $SSH_HOST -p $SSH_PORT > ~/.ssh/known_hosts + ssh-keyscan -H -p $SSH_PORT $SSH_HOST > ~/.ssh/known_hosts shell: bash env: SSH_HOST: ${{secrets.SSH_HOST}} From 9fc97bc9089239d1559f1bfae55dba1ac591075f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:35:06 +0000 Subject: [PATCH 323/329] Bump GitInfo from 3.3.5 to 3.5.0 (#340) --- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 5c6da6d..a8bcbc5 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -22,7 +22,7 @@ - + From ed298202fc07d98d07b2b571287c7aa3dc7f5bbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:41:36 +0000 Subject: [PATCH 324/329] Bump JetBrains.Annotations from 2024.2.0 to 2024.3.0 (#339) --- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index a8bcbc5..ec09b3d 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -24,7 +24,7 @@ - + From 5c235b9f981714513af76b9e219540979a4afd1a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 1 Dec 2024 21:48:25 +0500 Subject: [PATCH 325/329] Downgrade GitInfo from 3.5.0 to 3.3.5 (#343) This pull request downgrades GitInfo from 3.5.0 to 3.3.5 due to a decision from the maintainers to lock features behind a paywall. ![image](https://github.com/user-attachments/assets/f90eba84-1a1e-43eb-950e-e233a02feb9a) --- .github/dependabot.yml | 1 + TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 57eea90..b961b81 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -36,5 +36,6 @@ updates: - "Remora.Discord.*" # For all packages, ignore all patch updates ignore: + - dependency-name: "GitInfo" - dependency-name: "*" update-types: [ "version-update:semver-patch" ] diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index ec09b3d..858d9d2 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -22,7 +22,7 @@ - + From bf818401d829e54b3dbcdc630d27bad9f80e6c25 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 1 Dec 2024 21:52:47 +0500 Subject: [PATCH 326/329] Bump TargetFramework from 8.0 to 9.0 (#344) Simple and self-explanatory. Closes #342 --------- Signed-off-by: Octol1ttle --- .github/workflows/build-pr.yml | 4 ++-- Dockerfile | 4 ++-- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 2260918..84ec975 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -25,10 +25,10 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.301' + dotnet-version: '9.0.x' - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.12.0 + uses: muno92/resharper_inspectcode@1.12.3 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor diff --git a/Dockerfile b/Dockerfile index 0ef831a..6cfeac6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build-env +FROM mcr.microsoft.com/dotnet/sdk:9.0@sha256:7d24e90a392e88eb56093e4eb325ff883ad609382a55d42f17fd557b997022ca AS build-env WORKDIR /Octobot # Copy everything @@ -9,7 +9,7 @@ ARG PUBLISH_OPTIONS="-c Release" RUN dotnet publish ./TeamOctolings.Octobot $PUBLISH_OPTIONS -o out # Build runtime image -FROM mcr.microsoft.com/dotnet/runtime:8.0@sha256:a335dccd3231f7f9e2122691b21c634f96e187d3840c8b7dbad61ee09500e408 +FROM mcr.microsoft.com/dotnet/runtime:9.0@sha256:1e5eb0ed94ca96a34a914456db80e48bd1bb7bc3e3c8eda5e2c3d89c153c3081 WORKDIR /Octobot COPY --from=build-env /Octobot/out . ENTRYPOINT ["./TeamOctolings.Octobot"] diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 858d9d2..418803a 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable 2.0.0 @@ -26,7 +26,7 @@ - + From 4785d162a23653f7ed122248c4e2a5a39fc1e75e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 3 Feb 2025 16:58:57 +0500 Subject: [PATCH 327/329] Handle temporary files being present when loading guild data (#345) This PR fixes catastrophic guild data loading errors that appear when there are lingering temporary files. In normal operation, temporary files are deleted as soon as they are copied to the main file. It is also expected that temporary files are valid JSON files. However, due to a yesterday's DoS attack, something:tm: happened and a bunch of empty temporary files got written to disk. When Octobot recovered from the attack, it was unable to load any guild data because of the temporary files. This PR addresses this issue by changing the data loading logic: 1) Check if there's a temporary file. If it exists, try loading it. 2) If it is successfully loaded, move the temp file to the main file and resume operation as normal 3) If it could not be loaded, try loading the main file 4) If it is successfully loaded, delete the temporary file and resume operation as normal 5) If it is not, throw an error (like before) This PR was tested on production data and managed to load every guild without errors. Signed-off-by: Octol1ttle --- .../GuildScheduledEventExtensions.cs | 2 +- .../Services/GuildDataService.cs | 189 +++++++++++++----- 2 files changed, 144 insertions(+), 47 deletions(-) diff --git a/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs index b8eb2d1..7822d9b 100644 --- a/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs @@ -10,7 +10,7 @@ public static class GuildScheduledEventExtensions out string? location) { endTime = default; - location = default; + location = null; if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) { return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); diff --git a/TeamOctolings.Octobot/Services/GuildDataService.cs b/TeamOctolings.Octobot/Services/GuildDataService.cs index a7af7c9..88edb5f 100644 --- a/TeamOctolings.Octobot/Services/GuildDataService.cs +++ b/TeamOctolings.Octobot/Services/GuildDataService.cs @@ -75,78 +75,48 @@ public sealed class GuildDataService : BackgroundService { var path = $"GuildData/{guildId}"; var memberDataPath = $"{path}/MemberData"; + var settingsPath = $"{path}/Settings.json"; + var scheduledEventsPath = $"{path}/ScheduledEvents.json"; MigrateDataDirectory(guildId, path); Directory.CreateDirectory(path); - if (!File.Exists(settingsPath)) - { - await File.WriteAllTextAsync(settingsPath, "{}", ct); - } - - if (!File.Exists(scheduledEventsPath)) - { - await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); - } - var dataLoadFailed = false; - await using var settingsStream = File.OpenRead(settingsPath); - JsonNode? jsonSettings = null; - try - { - jsonSettings = await JsonNode.ParseAsync(settingsStream, cancellationToken: ct); - } - catch (Exception e) - { - _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath); - dataLoadFailed = true; - } - + var jsonSettings = await LoadGuildSettings(settingsPath, ct); if (jsonSettings is not null) { FixJsonSettings(jsonSettings); } - - await using var eventsStream = File.OpenRead(scheduledEventsPath); - Dictionary? events = null; - try + else { - events = await JsonSerializer.DeserializeAsync>( - eventsStream, cancellationToken: ct); + dataLoadFailed = true; } - catch (Exception e) + + var events = await LoadScheduledEvents(scheduledEventsPath, ct); + if (events is null) { - _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath); dataLoadFailed = true; } var memberData = new Dictionary(); - foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles()) + foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles() + .Where(dataFileInfo => + !memberData.ContainsKey( + ulong.Parse(dataFileInfo.Name.Replace(".json", "").Replace(".tmp", ""))))) { - await using var dataStream = dataFileInfo.OpenRead(); - MemberData? data; - try + var data = await LoadMemberData(dataFileInfo, memberDataPath, true, ct); + + if (data == null) { - data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); - } - catch (Exception e) - { - _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath, - dataFileInfo.Name); dataLoadFailed = true; continue; } - if (data is null) - { - continue; - } - - memberData.Add(data.Id, data); + memberData.TryAdd(data.Id, data); } var finalData = new GuildData( @@ -160,6 +130,133 @@ public sealed class GuildDataService : BackgroundService return finalData; } + private async Task LoadMemberData(FileInfo dataFileInfo, string memberDataPath, bool loadTmp, + CancellationToken ct = default) + { + MemberData? data; + var temporaryPath = $"{dataFileInfo.FullName}.tmp"; + var usedInfo = loadTmp && File.Exists(temporaryPath) ? new FileInfo(temporaryPath) : dataFileInfo; + + var isTmp = usedInfo.Extension is ".tmp"; + try + { + await using var dataStream = usedInfo.OpenRead(); + data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); + if (isTmp) + { + usedInfo.CopyTo(usedInfo.FullName.Replace(".tmp", ""), true); + usedInfo.Delete(); + } + } + catch (Exception e) + { + if (isTmp) + { + _logger.LogWarning(e, + "Unable to load temporary member data file, deleting: {MemberDataPath}/{FileName}", memberDataPath, + usedInfo.Name); + usedInfo.Delete(); + return await LoadMemberData(dataFileInfo, memberDataPath, false, ct); + } + + _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath, + usedInfo.Name); + return null; + } + + return data; + } + + private async Task?> LoadScheduledEvents(string scheduledEventsPath, + CancellationToken ct = default) + { + var tempScheduledEventsPath = $"{scheduledEventsPath}.tmp"; + + if (!File.Exists(scheduledEventsPath) && !File.Exists(tempScheduledEventsPath)) + { + return new Dictionary(); + } + + if (File.Exists(tempScheduledEventsPath)) + { + _logger.LogWarning("Found temporary scheduled events file, will try to parse and copy to main: ${Path}", + tempScheduledEventsPath); + try + { + await using var tempEventsStream = File.OpenRead(tempScheduledEventsPath); + var events = await JsonSerializer.DeserializeAsync>( + tempEventsStream, cancellationToken: ct); + File.Copy(tempScheduledEventsPath, scheduledEventsPath, true); + File.Delete(tempScheduledEventsPath); + + _logger.LogInformation("Successfully loaded temporary scheduled events file: ${Path}", + tempScheduledEventsPath); + return events; + } + catch (Exception e) + { + _logger.LogError(e, "Unable to load temporary scheduled events file: {Path}, deleting", + tempScheduledEventsPath); + File.Delete(tempScheduledEventsPath); + } + } + + try + { + await using var eventsStream = File.OpenRead(scheduledEventsPath); + return await JsonSerializer.DeserializeAsync>( + eventsStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath); + return null; + } + } + + private async Task LoadGuildSettings(string settingsPath, CancellationToken ct = default) + { + var tempSettingsPath = $"{settingsPath}.tmp"; + + if (!File.Exists(settingsPath) && !File.Exists(tempSettingsPath)) + { + return new JsonObject(); + } + + if (File.Exists(tempSettingsPath)) + { + _logger.LogWarning("Found temporary settings file, will try to parse and copy to main: ${Path}", + tempSettingsPath); + try + { + await using var tempSettingsStream = File.OpenRead(tempSettingsPath); + var jsonSettings = await JsonNode.ParseAsync(tempSettingsStream, cancellationToken: ct); + + File.Copy(tempSettingsPath, settingsPath, true); + File.Delete(tempSettingsPath); + + _logger.LogInformation("Successfully loaded temporary settings file: ${Path}", tempSettingsPath); + return jsonSettings; + } + catch (Exception e) + { + _logger.LogError(e, "Unable to load temporary settings file: {Path}, deleting", tempSettingsPath); + File.Delete(tempSettingsPath); + } + } + + try + { + await using var settingsStream = File.OpenRead(settingsPath); + return await JsonNode.ParseAsync(settingsStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath); + return null; + } + } + private void MigrateDataDirectory(Snowflake guildId, string newPath) { var oldPath = $"{guildId}"; From f3330c47cc7958e09d27d80124c45476069d3162 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:50:08 +0500 Subject: [PATCH 328/329] Bump Remora.Discord with 5 updates (#346) Bumps the remora group with 5 updates: | Package | From | To | | --- | --- | --- | | [Remora.Discord.Caching](https://github.com/Remora/Remora.Discord) | `39.0.0` | `40.0.0` | | [Remora.Commands](https://github.com/Remora/Remora.Commands) | `10.0.6` | `11.0.1` | | [Remora.Discord.Extensions](https://github.com/Remora/Remora.Discord) | `5.3.6` | `6.0.0` | | [Remora.Discord.Interactivity](https://github.com/Remora/Remora.Discord) | `5.0.0` | `6.0.0` | | [Remora.Discord.Hosting](https://github.com/Remora/Remora.Discord) | `6.0.10` | `7.0.0` | Signed-off-by: dependabot[bot] Signed-off-by: Octol1ttle Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Octol1ttle --- .../Responders/MessageEditedResponder.cs | 31 ++++++------------- .../TeamOctolings.Octobot.csproj | 10 +++--- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs index 2968562..e3d1c58 100644 --- a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs @@ -36,40 +36,29 @@ public sealed class MessageEditedResponder : IResponder public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.ID.IsDefined(out var messageId)) - { - return new ArgumentNullError(nameof(gatewayEvent.ID)); - } - - if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) - { - return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); - } - if (!gatewayEvent.GuildID.IsDefined(out var guildId) - || !gatewayEvent.Author.IsDefined(out var author) - || !gatewayEvent.EditedTimestamp.IsDefined(out var timestamp) - || !gatewayEvent.Content.IsDefined(out var newContent)) + || !gatewayEvent.EditedTimestamp.HasValue + || gatewayEvent.Author.IsBot.OrDefault(false)) { return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); - if (author.IsBot.OrDefault(false) || GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { return Result.Success; } - var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); + var cacheKey = new KeyHelpers.MessageCacheKey(gatewayEvent.ChannelID, gatewayEvent.ID); var messageResult = await _cacheService.TryGetValueAsync( cacheKey, ct); if (!messageResult.IsDefined(out var message)) { - _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + _ = _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); return Result.Success; } - if (message.Content == newContent) + if (message.Content == gatewayEvent.Content) { return Result.Success; } @@ -83,22 +72,22 @@ public sealed class MessageEditedResponder : IResponder // We don't need to await this since the result is not needed // NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously - _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + _ = _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); - var diff = InlineDiffBuilder.Diff(message.Content, newContent); + var diff = InlineDiffBuilder.Diff(message.Content, gatewayEvent.Content); Messages.Culture = GuildSettings.Language.Get(cfg); var builder = new StringBuilder() .AppendLine(diff.AsMarkdown()) .AppendLine(string.Format(Messages.DescriptionActionJumpToMessage, - $"https://discord.com/channels/{guildId}/{channelId}/{messageId}") + $"https://discord.com/channels/{guildId}/{gatewayEvent.ChannelID}/{gatewayEvent.ID}") ); var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) .WithDescription(builder.ToString()) - .WithTimestamp(timestamp.Value) + .WithTimestamp(gatewayEvent.EditedTimestamp.Value) .WithColour(ColorsList.Yellow) .Build(); diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 418803a..b67eaf8 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -27,11 +27,11 @@ - - - - - + + + + + From 5a351cbd97ff4ce11c628960ab8b7386c9f9228f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 08:33:14 +0000 Subject: [PATCH 329/329] Bump muno92/resharper_inspectcode from 1.12.3 to 1.13.0 (#348) --- .github/workflows/build-pr.yml | 2 +- TeamOctolings.Octobot/Program.cs | 51 +++++++++---------- .../Responders/GuildMemberLeftResponder.cs | 10 ++-- TeamOctolings.Octobot/Utility.cs | 4 +- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 84ec975..07d5b90 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -28,7 +28,7 @@ jobs: dotnet-version: '9.0.x' - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.12.3 + uses: muno92/resharper_inspectcode@1.13.0 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor diff --git a/TeamOctolings.Octobot/Program.cs b/TeamOctolings.Octobot/Program.cs index d1d6220..8cdbdcf 100644 --- a/TeamOctolings.Octobot/Program.cs +++ b/TeamOctolings.Octobot/Program.cs @@ -39,8 +39,7 @@ public sealed class Program private static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) - .AddDiscordService( - services => + .AddDiscordService(services => { var configuration = services.GetRequiredService(); @@ -49,25 +48,22 @@ public sealed class Program "No bot token has been provided. Set the " + "BOT_TOKEN environment variable to a valid token."); } - ).ConfigureServices( - (_, services) => + ).ConfigureServices((_, services) => { - services.Configure( - options => - { - options.Intents |= GatewayIntents.MessageContents - | GatewayIntents.GuildMembers - | GatewayIntents.GuildPresences - | GatewayIntents.GuildScheduledEvents; - }); - services.Configure( - cSettings => - { - cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); - cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); - cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); - cSettings.SetSlidingExpiration(TimeSpan.FromDays(7)); - }); + services.Configure(options => + { + options.Intents |= GatewayIntents.MessageContents + | GatewayIntents.GuildMembers + | GatewayIntents.GuildPresences + | GatewayIntents.GuildScheduledEvents; + }); + services.Configure(cSettings => + { + cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); + cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); + cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); + cSettings.SetSlidingExpiration(TimeSpan.FromDays(7)); + }); services.AddTransient() // Init @@ -87,14 +83,13 @@ public sealed class Program .AddHostedService() .AddHostedService(); } - ).ConfigureLogging( - c => c.AddConsole() - .AddFile("Logs/Octobot-{Date}.log", - outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") - .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + ).ConfigureLogging(c => c.AddConsole() + .AddFile("Logs/Octobot-{Date}.log", + outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) ); } } diff --git a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs index 9774899..957a107 100644 --- a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs @@ -36,13 +36,9 @@ public sealed class GuildMemberLeftResponder : IResponder var cfg = data.Settings; var memberData = data.GetOrCreateMemberData(user.ID); - if (memberData.BannedUntil is not null || memberData.Kicked) - { - return Result.Success; - } - - if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() - || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") + if (memberData.BannedUntil is not null || memberData.Kicked + || GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() + || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") { return Result.Success; } diff --git a/TeamOctolings.Octobot/Utility.cs b/TeamOctolings.Octobot/Utility.cs index f337d93..a2f7aca 100644 --- a/TeamOctolings.Octobot/Utility.cs +++ b/TeamOctolings.Octobot/Utility.cs @@ -67,8 +67,8 @@ public sealed class Utility builder.Append($"{Mention.Role(role)} "); } - builder = subscribers.Where( - subscriber => !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value)) + builder = subscribers.Where(subscriber => + !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value)) .Aggregate(builder, (current, subscriber) => current.Append($"{Mention.User(subscriber.User)} ")); return builder.ToString(); }

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 z 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%0Ae{X)^zxB2HTa)>{igHzI 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�UO`N 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 \ No newline at end of file diff --git a/assets/svg/card-header.svg b/assets/svg/card-header.svg new file mode 100644 index 0000000..5dad4bf --- /dev/null +++ b/assets/svg/card-header.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/svg/github-mark-white.svg b/assets/svg/github-mark-white.svg new file mode 100644 index 0000000..d5e6491 --- /dev/null +++ b/assets/svg/github-mark-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/splatoon.svg b/assets/svg/splatoon.svg new file mode 100644 index 0000000..75094c7 --- /dev/null +++ b/assets/svg/splatoon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/woff2/BlitzBold.woff2 b/assets/woff2/BlitzBold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9756e0c422aca1f5f7df53e232308a260716dc45 GIT binary patch literal 68576 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 literal 0 HcmV?d00001 diff --git a/assets/woff2/BlitzMain.woff2 b/assets/woff2/BlitzMain.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..59feb454ee620a013b1030a69c6aeb4a2053ebdb GIT binary patch literal 59252 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 J;X^;jg#bZV82tbM literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..b881673 --- /dev/null +++ b/index.html @@ -0,0 +1,91 @@ + + + + + + + + Octobot for Discord + + +

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 z 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%0DNDXV*9wWBZT~e{HvX?YGFyA{wy2or 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

50#I3;dtoumAry`~Cj^|FYu0 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 z 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 Y{CVzs>aNq*%mdl(>FVdQ&MBb@0O{!Q+W-In diff --git a/locale/Messages.resx b/locale/Messages.resx index 4d84198..7b9a471 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -120,14 +120,14 @@ {0}, welcome to {1} - - Bah! + + Veemo! - - Bop! + + Woomy! - - Beep! + + Ngyes! I do not have permission to execute this command! @@ -520,13 +520,13 @@ Developers: - Boyfriend's source code + Octobot's source code - About Boyfriend + About Octobot - developer & designer, Boyfriend's Wiki creator + developer & designer, Octobot's Wiki creator main developer @@ -544,7 +544,7 @@ You asked me to remind you {0} - Boyfriend's Settings + Octobot's Settings Setting successfully changed diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 4900e53..3e31f8f 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -120,14 +120,14 @@ {0}, добро пожаловать на сервер {1} - - Бап! + + Виимо! - - Боп! + + Вууми! - - Бип! + + Нгьес! У меня недостаточно прав для выполнения этой команды! @@ -520,10 +520,10 @@ Разработчики: - Исходный код Boyfriend + Исходный код Octobot - О Boyfriend + Об Octobot разработчик @@ -532,7 +532,7 @@ основной разработчик - разработчик и дизайнер, создатель Boyfriend's Wiki + разработчик и дизайнер, создатель Octobot's Wiki Напоминание для {0} создано @@ -544,7 +544,7 @@ Вы просили напомнить вам {0} - Настройки Boyfriend + Настройки Octobot Настройка успешно изменена diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 5a08755..df98622 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -120,14 +120,14 @@ {0}, добро пожаловать на сервер {1} - - брах! + + вииимо! - - брох! + + вуууми! - - брух! + + нгьес! у меня прав нету, сделай что нибудь. @@ -520,13 +520,13 @@ девелоперы: - репа Boyfriend (тык) + репа Octobot (тык) - немного о Boyfriend + немного об Octobot - скучный девелопер + дизайнер создавший Boyfriend's Wiki + скучный девелопер + дизайнер создавший Octobot's Wiki ВАЖНЫЙ соучастник кодинг-стримов @Octol1ttle @@ -544,7 +544,7 @@ ты хотел чтоб я напомнил тебе {0} - приколы Boyfriend + приколы Octobot прикол редактирован diff --git a/src/ColorsList.cs b/src/ColorsList.cs index fc32274..cd40313 100644 --- a/src/ColorsList.cs +++ b/src/ColorsList.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Boyfriend; +namespace Octobot; ///

Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.8.3

What's Changed

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.8.2...1.8.3