From 47cc8425b2e602506086a1627dc48cccbcb033d7 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 4 Dec 2018 19:41:42 -0800 Subject: [PATCH] Added CommonFunctionsService Experimental feature. An attempt to standardize some common functions to ensure consistent execution of those functions, and also to notify services ahead of time that the events they may raise originated from within the bot. This is the initial commit. Implementation may change later. --- Kerobot/ModuleBase.cs | 48 +++++++++++ .../Services/CommonFunctions/BanKickResult.cs | 66 ++++++++++++++ .../CommonFunctions/CommonFunctionsService.cs | 86 +++++++++++++++++++ .../Services/CommonFunctions/Kerobot_hooks.cs | 21 +++++ 4 files changed, 221 insertions(+) create mode 100644 Kerobot/Services/CommonFunctions/BanKickResult.cs create mode 100644 Kerobot/Services/CommonFunctions/CommonFunctionsService.cs create mode 100644 Kerobot/Services/CommonFunctions/Kerobot_hooks.cs diff --git a/Kerobot/ModuleBase.cs b/Kerobot/ModuleBase.cs index 96c8e7c..1ac49f5 100644 --- a/Kerobot/ModuleBase.cs +++ b/Kerobot/ModuleBase.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json.Linq; using System; using System.Diagnostics; using System.Threading.Tasks; +using static Kerobot.Kerobot; namespace Kerobot { @@ -73,6 +74,53 @@ namespace Kerobot /// Appends a message to the log for the specified guild. /// protected Task LogAsync(ulong guild, string message) => Kerobot.GuildLogAsync(guild, Name, message); + + /// + /// Attempts to ban the given user from the specified guild. It is greatly preferred to call this method + /// instead of manually executing the equivalent method found in Discord.Net. It notifies other services + /// that the action originated from the bot, and allows them to handle the action appropriately. + /// + /// A structure containing results of the ban operation. + /// The guild in which to attempt the action. + /// The user, module, or service which is requesting this action to be taken. + /// The user which to perform the action to. + /// Number of days of prior post history to delete on ban. Must be between 0-7. + /// Reason for the action. Sent to the Audit Log and user (if specified). + /// + /// Message to DM the target user. Set null to disable. Instances of "%s" are replaced with the guild name + /// and instances of "%r" are replaced with the reason. + /// + protected Task BanAsync(SocketGuild guild, string source, ulong targetUser, int purgeDays, string reason, string dmMsg) + => Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, purgeDays, reason, dmMsg); + + protected Task BanAsync(SocketGuild guild, SocketGuildUser source, string targetSearch, string reason, string dmMsg) + { + // TODO requires EntityCache lookup. Do this when that feature gets implemented. + throw new NotImplementedException(); + } + + /// + /// Attempts to ban the given user from the specified guild. It is greatly preferred to call this method + /// instead of manually executing the equivalent method found in Discord.Net. It notifies other services + /// that the action originated from the bot, and allows them to handle the action appropriately. + /// + /// A structure containing results of the ban operation. + /// The guild in which to attempt the action. + /// The user, if any, which requested the action to be taken. + /// The user which to perform the action to. + /// Reason for the action. Sent to the Audit Log and user (if specified). + /// + /// Message to DM the target user. Set null to disable. Instances of "%s" are replaced with the guild name + /// and instances of "%r" are replaced with the reason. + /// + protected Task KickAsync(SocketGuild guild, string source, ulong targetUser, string reason, string dmMsg) + => Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, dmMsg); + + protected Task KickAsync(SocketGuild guild, SocketUser user, string reason, string dmMsg) + { + // TODO requires EntityCache lookup. Do this when that feature gets implemented. + throw new NotImplementedException(); + } } /// diff --git a/Kerobot/Services/CommonFunctions/BanKickResult.cs b/Kerobot/Services/CommonFunctions/BanKickResult.cs new file mode 100644 index 0000000..5ce880a --- /dev/null +++ b/Kerobot/Services/CommonFunctions/BanKickResult.cs @@ -0,0 +1,66 @@ +using System; +using Discord.Net; + +namespace Kerobot +{ + // Instances created by CommonFunctionService, but are meant to be sent to modules. Hence its namespace. + + /// + /// Contains information on various success/failure outcomes for a ban or kick operation. + /// + public class BanKickResult + { + private readonly bool _userNotFound; + + internal BanKickResult(HttpException error, bool notifySuccess, bool errorNotFound) + { + OperationError = error; + MessageSendSuccess = notifySuccess; + _userNotFound = errorNotFound; + } + + /// + /// Gets a value indicating whether the kick or ban succeeded. + /// + public bool OperationSuccess { get => OperationError == null; } + + /// + /// The exception thrown, if any, when attempting to kick or ban the target. + /// + public HttpException OperationError { get; } + + /// + /// Indicates if the operation failed due to being unable to find the user. + /// + public bool ErrorNotFound + { + get + { + if (_userNotFound) return true; // TODO I don't like this. + if (OperationSuccess) return false; + return OperationError.HttpCode == System.Net.HttpStatusCode.NotFound; + } + } + + /// + /// Indicates if the operation failed due to a permissions issue. + /// + public bool ErrorForbidden + { + get + { + if (OperationSuccess) return false; + return OperationError.HttpCode == System.Net.HttpStatusCode.Forbidden; + } + } + + /// + /// Gets a value indicating whether the user was able to receive the ban or kick message. + /// + /// + /// if an error was encountered when attempting to send the target a DM. Will always + /// return otherwise, including cases in which no message was sent. + /// + public bool MessageSendSuccess { get; } + } +} diff --git a/Kerobot/Services/CommonFunctions/CommonFunctionsService.cs b/Kerobot/Services/CommonFunctions/CommonFunctionsService.cs new file mode 100644 index 0000000..1a6678c --- /dev/null +++ b/Kerobot/Services/CommonFunctions/CommonFunctionsService.cs @@ -0,0 +1,86 @@ +using System.Threading.Tasks; +using Discord.Net; +using Discord.WebSocket; +using static Kerobot.Kerobot; + +namespace Kerobot.Services.CommonFunctions +{ + /// + /// Implements certain common actions that modules may want to perform. Using this service to perform those + /// functions may help enforce a sense of consistency across modules when performing common actions, and may + /// inform services which provide any additional features the ability to respond to those actions ahead of time. + /// + /// This is currently an experimental section. If it turns out to not be necessary, this service will be removed and + /// modules may resume executing common actions on their own. + /// + internal class CommonFunctionsService : Service + { + public CommonFunctionsService(Kerobot kb) : base(kb) { } + + #region Guild member removal + /// + /// Common processing for kicks and bans. Called by DoKickAsync and DoBanAsync. + /// + /// The reason to insert into the Audit Log. + /// + /// The message to send out to the target. Leave null to not perform this action. + /// Instances of "%r" within it are replaced with and instances of "%g" + /// are replaced with the server name. + /// + internal async Task BanOrKickAsync( + RemovalType t, SocketGuild guild, string source, ulong target, int banPurgeDays, + string logReason, string dmTemplate) + { + if (string.IsNullOrWhiteSpace(logReason)) logReason = "Reason not specified."; + var dmSuccess = true; + + SocketGuildUser utarget = guild.GetUser(target); + // Can't kick without obtaining user object. Quit here. + if (t == RemovalType.Kick && utarget == null) return new BanKickResult(null, false, true); + +#if DEBUG +#warning "Services are NOT NOTIFIED" of bans/kicks during debug." +#else + // TODO notify services here as soon as we get some who will want to listen to this +#endif + + // Send DM notification + if (dmTemplate != null) + { + if (utarget != null) dmSuccess = await BanKickSendNotificationAsync(utarget, dmTemplate, logReason); + else dmSuccess = false; + } + + // Perform the action + try + { +#if DEBUG +#warning "Actual kick/ban is DISABLED during debug." +#else + if (t == RemovalType.Ban) await guild.AddBanAsync(target, banPurgeDays); + else await utarget.KickAsync(logReason); +#endif + } + catch (HttpException ex) + { + return new BanKickResult(ex, false, false); + } + + return new BanKickResult(null, dmSuccess, false); + } + + private async Task BanKickSendNotificationAsync(SocketGuildUser target, string dmTemplate, string reason) + { + if (dmTemplate == null) return true; + + var dch = await target.GetOrCreateDMChannelAsync(); + string output = dmTemplate.Replace("%r", reason).Replace("%s", target.Guild.Name); + + try { await dch.SendMessageAsync(output); } + catch (HttpException) { return false; } + + return true; + } +#endregion + } +} diff --git a/Kerobot/Services/CommonFunctions/Kerobot_hooks.cs b/Kerobot/Services/CommonFunctions/Kerobot_hooks.cs new file mode 100644 index 0000000..300c48b --- /dev/null +++ b/Kerobot/Services/CommonFunctions/Kerobot_hooks.cs @@ -0,0 +1,21 @@ +using Discord.WebSocket; +using System.Threading.Tasks; +using Kerobot.Services.CommonFunctions; + +namespace Kerobot +{ + partial class Kerobot + { + private CommonFunctionsService _svcCommonFunctions; + + public enum RemovalType { Ban, Kick } + + /// + /// See + /// and related methods. + /// + public Task BanOrKickAsync(RemovalType t, SocketGuild guild, string source, + ulong target, int banPurgeDays, string logReason, string dmTemplate) + => _svcCommonFunctions.BanOrKickAsync(t, guild, source, target, banPurgeDays, logReason, dmTemplate); + } +}