From 201a1ce07933f2d78a93e7ba88fb99158db836bb Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 16 May 2023 00:11:11 +0500 Subject: [PATCH] =?UTF-8?q?Remora.Discord=20part=201=20out=20of=20?= =?UTF-8?q?=E2=88=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Octol1ttle --- .gitignore | 1 - Boyfriend.cs | 220 +++++++----------------- Boyfriend.csproj | 18 +- CommandProcessor.cs | 321 ------------------------------------ Commands/BanCommand.cs | 48 ------ Commands/ClearCommand.cs | 30 ---- Commands/HelpCommand.cs | 21 --- Commands/ICommand.cs | 7 - Commands/KickCommand.cs | 34 ---- Commands/MuteCommand.cs | 71 -------- Commands/PingCommand.cs | 19 --- Commands/RemindCommand.cs | 34 ---- Commands/SettingsCommand.cs | 164 ------------------ Commands/UnbanCommand.cs | 25 --- Commands/UnmuteCommand.cs | 36 ---- Data/GuildData.cs | 142 ---------------- Data/MemberData.cs | 38 ----- Data/Reminder.cs | 7 - EventHandler.cs | 238 -------------------------- EventResponders.cs | 31 ++++ Extensions.cs | 35 ++++ Messages.Designer.cs | 2 +- ReplyEmojis.cs | 19 --- Utils.cs | 168 ------------------- 24 files changed, 141 insertions(+), 1588 deletions(-) delete mode 100644 CommandProcessor.cs delete mode 100644 Commands/BanCommand.cs delete mode 100644 Commands/ClearCommand.cs delete mode 100644 Commands/HelpCommand.cs delete mode 100644 Commands/ICommand.cs delete mode 100644 Commands/KickCommand.cs delete mode 100644 Commands/MuteCommand.cs delete mode 100644 Commands/PingCommand.cs delete mode 100644 Commands/RemindCommand.cs delete mode 100644 Commands/SettingsCommand.cs delete mode 100644 Commands/UnbanCommand.cs delete mode 100644 Commands/UnmuteCommand.cs delete mode 100644 Data/GuildData.cs delete mode 100644 Data/MemberData.cs delete mode 100644 Data/Reminder.cs delete mode 100644 EventHandler.cs create mode 100644 EventResponders.cs create mode 100644 Extensions.cs delete mode 100644 ReplyEmojis.cs delete mode 100644 Utils.cs 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..de0a7f7 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -1,180 +1,78 @@ -using System.Text; -using System.Timers; -using Boyfriend.Data; -using Discord; -using Discord.Rest; -using Discord.WebSocket; -using Timer = System.Timers.Timer; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.Caching.Extensions; +using Remora.Discord.Caching.Services; +using Remora.Discord.Gateway.Extensions; +using Remora.Discord.Hosting.Extensions; namespace Boyfriend; -public static class Boyfriend { - public static readonly StringBuilder StringBuilder = new(); +public class Boyfriend { + public static ILogger Logger = null!; + public static IConfiguration GuildConfiguration = null!; - 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 - }; + private static readonly Dictionary ReflectionMessageCache = new(); - private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; - private static uint _nextSongIndex; + public static async Task Main(string[] args) { + var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); - 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)) - }; + var services = host.Services; + Logger = services.GetRequiredService>(); + GuildConfiguration = services.GetRequiredService().AddJsonFile("guild_configs.json") + .Build(); - 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) => { + var responderTypes = typeof(Boyfriend).Assembly + .GetExportedTypes() + .Where(t => t.IsResponder()); + foreach (var responderType in responderTypes) services.AddResponder(responderType); - if (now >= _nextSongAt) { - var nextSong = ActivityList[_nextSongIndex]; - await Client.SetActivityAsync(nextSong.Song); - _nextSongAt = now.Add(nextSong.Duration); - _nextSongIndex++; - if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0; - } + services.AddDiscordCaching(); + services.Configure( + settings => { settings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); }); - 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(); + services.AddSingleton(); + } + ).ConfigureLogging( + c => c.AddConsole() + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + ); } - 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; + public static string GetLocalized(string key) { + var propertyName = key; + key = $"{Messages.Culture}/{key}"; + if (ReflectionMessageCache.TryGetValue(key, out var cached)) return cached; - case LogSeverity.Verbose: - case LogSeverity.Debug: - default: return Task.CompletedTask; + var toReturn = + typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) + ?.ToString(); + if (toReturn is null) { + Logger.LogError("Could not find localized property: {Name}", propertyName); + return key; } - 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); + ReflectionMessageCache.Add(key, toReturn); + return toReturn; } } diff --git a/Boyfriend.csproj b/Boyfriend.csproj index 0192abe..9aff7e3 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -19,8 +19,20 @@ - - - + + + + + + + + + + + + + + + diff --git a/CommandProcessor.cs b/CommandProcessor.cs deleted file mode 100644 index 3ee91b6..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 SettingsCommand(), new UnbanCommand(), new UnmuteCommand(), - new RemindCommand() - }; - - 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/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/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/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/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/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/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/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/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/GuildData.cs b/Data/GuildData.cs deleted file mode 100644 index 8403e2c..0000000 --- a/Data/GuildData.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using Discord; -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; - - [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 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 - => Boyfriend.Client.GetGuild(_id) - .GetTextChannel( - ulong.Parse(Preferences["PublicFeedbackChannel"])); - - 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); - 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 deleted file mode 100644 index 137375a..0000000 --- a/Data/MemberData.cs +++ /dev/null @@ -1,38 +0,0 @@ -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/Data/Reminder.cs b/Data/Reminder.cs deleted file mode 100644 index c64ebbd..0000000 --- a/Data/Reminder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Boyfriend.Data; - -public struct Reminder { - public DateTimeOffset RemindAt; - public string ReminderText; - public ulong ReminderChannel; -} 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..767472b --- /dev/null +++ b/EventResponders.cs @@ -0,0 +1,31 @@ +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend; + +public class ReadyResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + + public ReadyResponder(IDiscordRestChannelAPI channelApi) { + _channelApi = channelApi; + } + + public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // is IAvailableGuild + + var guild = gatewayEvent.Guild.AsT0; + if (guild.GetConfigBool("SendReadyMessages").IsDefined(out var enabled) + && enabled + && guild.GetChannel("PrivateFeedbackChannel").IsDefined(out var channel)) { + Messages.Culture = guild.GetCulture(); + var i = Random.Shared.Next(1, 4); + + return (Result)await _channelApi.CreateMessageAsync( + channel.ID, string.Format(Messages.Ready, Boyfriend.GetLocalized($"Beep{i}")), ct: ct); + } + + return Result.FromSuccess(); + } +} diff --git a/Extensions.cs b/Extensions.cs new file mode 100644 index 0000000..6605d44 --- /dev/null +++ b/Extensions.cs @@ -0,0 +1,35 @@ +using System.Globalization; +using Microsoft.Extensions.Configuration; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Results; + +namespace Boyfriend; + +public static class Extensions { + private static readonly Dictionary CultureInfoCache = new() { + { "en", new CultureInfo("en-US") }, + { "ru", new CultureInfo("ru-RU") }, + { "mctaylors-ru", new CultureInfo("tt-RU") } + }; + + public static Result GetConfigBool(this IGuild guild, string key) { + var value = Boyfriend.GuildConfiguration.GetValue($"GuildConfigs:{guild.ID}:{key}"); + return value is not null ? Result.FromSuccess(value.Value) : Result.FromError(new NotFoundError()); + } + + public static Result GetChannel(this IGuildCreate.IAvailableGuild guild, string key) { + var value = Boyfriend.GuildConfiguration.GetValue($"GuildConfigs:{guild.ID}:{key}"); + if (value is null) return Result.FromError(new NotFoundError()); + + var match = guild.Channels.SingleOrDefault(channel => channel!.ID.Equals(value.Value), null); + return match is not null + ? Result.FromSuccess(match) + : Result.FromError(new NotFoundError()); + } + + public static CultureInfo GetCulture(this IGuild guild) { + var value = Boyfriend.GuildConfiguration.GetValue($"GuildConfigs:{guild.ID}:Language"); + return value is not null ? CultureInfoCache[value] : CultureInfoCache["en"]; + } +} diff --git a/Messages.Designer.cs b/Messages.Designer.cs index 54a7eab..ed4cc9e 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -21,7 +21,7 @@ namespace Boyfriend { [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 { + public class Messages { private static global::System.Resources.ResourceManager resourceMan; 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/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(); -}