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.
This commit is contained in:
Noikoio 2018-12-04 19:41:42 -08:00
parent 530a4ae1b6
commit 47cc8425b2
4 changed files with 221 additions and 0 deletions

View file

@ -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.
/// </summary>
protected Task LogAsync(ulong guild, string message) => Kerobot.GuildLogAsync(guild, Name, message);
/// <summary>
/// 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.
/// </summary>
/// <returns>A structure containing results of the ban operation.</returns>
/// <param name="guild">The guild in which to attempt the action.</param>
/// <param name="source">The user, module, or service which is requesting this action to be taken.</param>
/// <param name="targetUser">The user which to perform the action to.</param>
/// <param name="purgeDays">Number of days of prior post history to delete on ban. Must be between 0-7.</param>
/// <param name="reason">Reason for the action. Sent to the Audit Log and user (if specified).</param>
/// <param name="dmMsg">
/// 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.
/// </param>
protected Task<BanKickResult> 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<BanKickResult> 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();
}
/// <summary>
/// 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.
/// </summary>
/// <returns>A structure containing results of the ban operation.</returns>
/// <param name="guild">The guild in which to attempt the action.</param>
/// <param name="source">The user, if any, which requested the action to be taken.</param>
/// <param name="targetUser">The user which to perform the action to.</param>
/// <param name="reason">Reason for the action. Sent to the Audit Log and user (if specified).</param>
/// <param name="dmMsg">
/// 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.
/// </param>
protected Task<BanKickResult> KickAsync(SocketGuild guild, string source, ulong targetUser, string reason, string dmMsg)
=> Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, dmMsg);
protected Task<BanKickResult> KickAsync(SocketGuild guild, SocketUser user, string reason, string dmMsg)
{
// TODO requires EntityCache lookup. Do this when that feature gets implemented.
throw new NotImplementedException();
}
}
/// <summary>

View file

@ -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.
/// <summary>
/// Contains information on various success/failure outcomes for a ban or kick operation.
/// </summary>
public class BanKickResult
{
private readonly bool _userNotFound;
internal BanKickResult(HttpException error, bool notifySuccess, bool errorNotFound)
{
OperationError = error;
MessageSendSuccess = notifySuccess;
_userNotFound = errorNotFound;
}
/// <summary>
/// Gets a value indicating whether the kick or ban succeeded.
/// </summary>
public bool OperationSuccess { get => OperationError == null; }
/// <summary>
/// The exception thrown, if any, when attempting to kick or ban the target.
/// </summary>
public HttpException OperationError { get; }
/// <summary>
/// Indicates if the operation failed due to being unable to find the user.
/// </summary>
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;
}
}
/// <summary>
/// Indicates if the operation failed due to a permissions issue.
/// </summary>
public bool ErrorForbidden
{
get
{
if (OperationSuccess) return false;
return OperationError.HttpCode == System.Net.HttpStatusCode.Forbidden;
}
}
/// <summary>
/// Gets a value indicating whether the user was able to receive the ban or kick message.
/// </summary>
/// <value>
/// <see langword="false"/> if an error was encountered when attempting to send the target a DM. Will always
/// return <see langword="true"/> otherwise, including cases in which no message was sent.
/// </value>
public bool MessageSendSuccess { get; }
}
}

View file

@ -0,0 +1,86 @@
using System.Threading.Tasks;
using Discord.Net;
using Discord.WebSocket;
using static Kerobot.Kerobot;
namespace Kerobot.Services.CommonFunctions
{
/// <summary>
/// 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.
/// </summary>
internal class CommonFunctionsService : Service
{
public CommonFunctionsService(Kerobot kb) : base(kb) { }
#region Guild member removal
/// <summary>
/// Common processing for kicks and bans. Called by DoKickAsync and DoBanAsync.
/// </summary>
/// <param name="logReason">The reason to insert into the Audit Log.</param>
/// <param name="dmTemplate">
/// The message to send out to the target. Leave null to not perform this action.
/// Instances of "%r" within it are replaced with <paramref name="logReason"/> and instances of "%g"
/// are replaced with the server name.
/// </param>
internal async Task<BanKickResult> 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<bool> 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
}
}

View file

@ -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 }
/// <summary>
/// See <see cref="ModuleBase.BanAsync(SocketGuild, string, ulong, int, string, string)"/>
/// and related methods.
/// </summary>
public Task<BanKickResult> BanOrKickAsync(RemovalType t, SocketGuild guild, string source,
ulong target, int banPurgeDays, string logReason, string dmTemplate)
=> _svcCommonFunctions.BanOrKickAsync(t, guild, source, target, banPurgeDays, logReason, dmTemplate);
}
}