Implement block check within a precondition

With more preconditions in use, command logging has been modified to also be better able to respond to users in the event of an error. As a result, the bot is now able to respond to users and notify them properly if they fail any preconditions.
This commit is contained in:
Noi 2022-03-11 21:52:46 -08:00
parent 85b23e255c
commit d700cd8ce9
10 changed files with 93 additions and 37 deletions

View file

@ -4,7 +4,7 @@ using System.Text;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
[RequireContext(ContextType.Guild)] [RequireGuildContext]
[Group("birthday", HelpCmdBirthday)] [Group("birthday", HelpCmdBirthday)]
public class BirthdayModule : BotModuleBase { public class BirthdayModule : BotModuleBase {
public const string HelpCmdBirthday = "Commands relating to birthdays."; public const string HelpCmdBirthday = "Commands relating to birthdays.";

View file

@ -3,7 +3,6 @@ using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
[RequireContext(ContextType.Guild)]
[RequireBotModerator] [RequireBotModerator]
[Group("override", HelpCmdOverride)] [Group("override", HelpCmdOverride)]
public class BirthdayOverrideModule : BotModuleBase { public class BirthdayOverrideModule : BotModuleBase {

View file

@ -9,6 +9,7 @@ namespace BirthdayBot.ApplicationCommands;
/// <summary> /// <summary>
/// Base class for our interaction module classes. Contains common data for use in implementing classes. /// Base class for our interaction module classes. Contains common data for use in implementing classes.
/// </summary> /// </summary>
[EnforceBlocking]
public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionContext> { public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionContext> {
protected const string HelpPfxModOnly = "Bot moderators only: "; protected const string HelpPfxModOnly = "Bot moderators only: ";
protected const string ErrGuildOnly = ":x: This command can only be run within a server."; protected const string ErrGuildOnly = ":x: This command can only be run within a server.";

View file

@ -4,7 +4,6 @@ using System.Text;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
[RequireContext(ContextType.Guild)]
[RequireBotModerator] [RequireBotModerator]
[Group("config", HelpCmdConfig)] [Group("config", HelpCmdConfig)]
public class ConfigModule : BotModuleBase { public class ConfigModule : BotModuleBase {

View file

@ -0,0 +1,38 @@
using BirthdayBot.Data;
using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands;
/// <summary>
/// Only users not on the blocklist or affected by moderator mode may use the command.<br/>
/// This is used in the <see cref="BotModuleBase"/> base class. Manually using it anywhere else is unnecessary.
/// </summary>
class EnforceBlockingAttribute : PreconditionAttribute {
public const string FailModerated = "Guild has moderator mode enabled.";
public const string FailBlocked = "User is in the guild's block list.";
public const string ReplyModerated = ":x: This bot is in moderated mode, preventing you from using any bot commands in this server.";
public const string ReplyBlocked = ":x: You have been blocked from using bot commands in this server.";
public override async Task<PreconditionResult> CheckRequirementsAsync(
IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) {
// Not in guild context, unaffected by blocking
if (context.Guild is not SocketGuild guild) return PreconditionResult.FromSuccess();
// Manage Guild permission overrides any blocks
var user = (SocketGuildUser)context.User;
if (user.GuildPermissions.ManageGuild) return PreconditionResult.FromSuccess();
var gconf = await guild.GetConfigAsync().ConfigureAwait(false);
// Bot moderators override any blocks
if (gconf.ModeratorRole.HasValue && user.Roles.Any(r => r.Id == gconf.ModeratorRole.Value)) return PreconditionResult.FromSuccess();
// Moderated mode check
if (gconf.IsModerated) return PreconditionResult.FromError(FailModerated);
// Block list check
if (await gconf.IsUserInBlocklistAsync(user.Id)) return PreconditionResult.FromError(FailBlocked);
return PreconditionResult.FromSuccess();
}
}

View file

@ -3,29 +3,27 @@ using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
// Contains preconditions used by our interaction modules.
/// <summary> /// <summary>
/// Precondition requiring the executing user be considered a bot moderator. /// Precondition requiring the executing user be recognized as a bot moderator.<br/>
/// That is, they must either have the Manage Server permission or be a member of the designated bot moderator role. /// A bot moderator has either the Manage Server permission or is a member of the designated bot moderator role.
/// </summary> /// </summary>
class RequireBotModeratorAttribute : PreconditionAttribute { class RequireBotModeratorAttribute : PreconditionAttribute {
public const string FailMsg = "User did not pass the mod check."; public const string Error = "User did not pass the mod check.";
public const string Reply = ":x: You must be a moderator to use this command."; public const string Reply = ":x: You must be a moderator to use this command.";
public override string ErrorMessage => FailMsg; public override string ErrorMessage => Error;
public override async Task<PreconditionResult> CheckRequirementsAsync( public override async Task<PreconditionResult> CheckRequirementsAsync(
IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) { IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) {
if (context.User is not SocketGuildUser user) { // A bot moderator can only exist in a guild context, so we must do this check.
return PreconditionResult.FromError("Failed due to non-guild context."); // This check causes this precondition to become a functional equivalent to RequireGuildContextAttribute...
} if (context.User is not SocketGuildUser user) return PreconditionResult.FromError(RequireGuildContextAttribute.Error);
if (user.GuildPermissions.ManageGuild) return PreconditionResult.FromSuccess(); if (user.GuildPermissions.ManageGuild) return PreconditionResult.FromSuccess();
var gconf = await ((SocketGuild)context.Guild).GetConfigAsync().ConfigureAwait(false); var gconf = await ((SocketGuild)context.Guild).GetConfigAsync().ConfigureAwait(false);
if (gconf.ModeratorRole.HasValue && user.Roles.Any(r => r.Id == gconf.ModeratorRole.Value)) if (gconf.ModeratorRole.HasValue && user.Roles.Any(r => r.Id == gconf.ModeratorRole.Value))
return PreconditionResult.FromSuccess(); return PreconditionResult.FromSuccess();
return PreconditionResult.FromError(FailMsg); return PreconditionResult.FromError(Error);
} }
} }

View file

@ -0,0 +1,16 @@
using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands;
/// <summary>
/// Implements the included precondition from Discord.Net, requiring a guild context while using our custom error message.<br/><br/>
/// Combining this with <see cref="RequireBotModeratorAttribute"/> is redundant. If possible, only use the latter instead.
/// </summary>
class RequireGuildContextAttribute : RequireContextAttribute {
public const string Error = "Command not received within a guild context.";
public const string Reply = ":x: This command is only available within a server.";
public override string ErrorMessage => Error;
public RequireGuildContextAttribute() : base(ContextType.Guild) { }
}

View file

@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>3.3.0</Version> <Version>3.3.1</Version>
<Authors>NoiTheCat</Authors> <Authors>NoiTheCat</Authors>
</PropertyGroup> </PropertyGroup>

View file

@ -71,12 +71,19 @@ class GuildConfiguration {
} }
/// <summary> /// <summary>
/// Checks if the given user exists in the block list. /// Checks if the specified user is blocked by current guild policy (block list or moderated mode).
/// If the server is in moderated mode, this always returns true.
/// </summary> /// </summary>
[Obsolete("Block lists should be reimplemented in a more resource-efficient manner later.", false)]
public async Task<bool> IsUserBlockedAsync(ulong userId) { public async Task<bool> IsUserBlockedAsync(ulong userId) {
if (IsModerated) return true; if (IsModerated) return true;
return await IsUserInBlocklistAsync(userId).ConfigureAwait(false);
}
/// <summary>
/// Checks if the given user exists in the block list.
/// </summary>
[Obsolete("Block lists should be reimplemented in a more resource-efficient manner later.", false)]
public async Task<bool> IsUserInBlocklistAsync(ulong userId) {
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false); using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
using var c = db.CreateCommand(); using var c = db.CreateCommand();
c.CommandText = $"select * from {BackingTableBans} " c.CommandText = $"select * from {BackingTableBans} "
@ -92,6 +99,7 @@ class GuildConfiguration {
/// <summary> /// <summary>
/// Adds the specified user to the block list corresponding to this guild. /// Adds the specified user to the block list corresponding to this guild.
/// </summary> /// </summary>
[Obsolete("Block lists will be reimplemented in a more practical manner later.", false)]
public async Task BlockUserAsync(ulong userId) { public async Task BlockUserAsync(ulong userId) {
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false); using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
using var c = db.CreateCommand(); using var c = db.CreateCommand();
@ -109,6 +117,7 @@ class GuildConfiguration {
/// Removes the specified user from the block list corresponding to this guild. /// Removes the specified user from the block list corresponding to this guild.
/// </summary> /// </summary>
/// <returns>True if a user has been removed, false if the requested user was not in this list.</returns> /// <returns>True if a user has been removed, false if the requested user was not in this list.</returns>
[Obsolete("Block lists will be reimplemented in a more practical manner later.", false)]
public async Task<bool> UnblockUserAsync(ulong userId) { public async Task<bool> UnblockUserAsync(ulong userId) {
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false); using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
using var c = db.CreateCommand(); using var c = db.CreateCommand();

View file

@ -170,26 +170,15 @@ public sealed class ShardInstance : IDisposable {
private async Task DiscordClient_InteractionCreated(SocketInteraction arg) { private async Task DiscordClient_InteractionCreated(SocketInteraction arg) {
var context = new SocketInteractionContext(DiscordClient, arg); var context = new SocketInteractionContext(DiscordClient, arg);
// Blocklist/moderated check
// TODO convert to precondition
var gconf = await GuildConfiguration.LoadAsync(context.Guild.Id, false);
if (context.Channel is SocketGuildChannel) { // Check only if in a guild context
if (!gconf!.IsBotModerator((SocketGuildUser)arg.User)) { // Moderators exempted from this check
if (await gconf.IsUserBlockedAsync(arg.User.Id)) {
Log("Interaction", $"Interaction blocked per guild policy for {context.Guild}!{context.User}");
await arg.RespondAsync(BotModuleBase.AccessDeniedError, ephemeral: true).ConfigureAwait(false);
return;
}
}
}
try { try {
await _interactionService.ExecuteCommandAsync(context, _services).ConfigureAwait(false); await _interactionService.ExecuteCommandAsync(context, _services).ConfigureAwait(false);
} catch (Exception e) { } catch (Exception e) {
Log(nameof(DiscordClient_InteractionCreated), $"Unhandled exception. {e}"); Log(nameof(DiscordClient_InteractionCreated), $"Unhandled exception. {e}");
// TODO when implementing proper error logging, see here // TODO when implementing proper application error logging, see here
if (arg.Type == InteractionType.ApplicationCommand) if (arg.Type == InteractionType.ApplicationCommand) {
if (arg.HasResponded) await arg.ModifyOriginalResponseAsync(prop => prop.Content = ":warning: An unknown error occured."); if (arg.HasResponded) await arg.ModifyOriginalResponseAsync(prop => prop.Content = InternalError);
else await arg.RespondAsync(InternalError);
}
} }
} }
@ -205,17 +194,24 @@ public sealed class ShardInstance : IDisposable {
if (result.Error != null) { if (result.Error != null) {
// Additional log information with error detail // Additional log information with error detail
logresult += Enum.GetName(typeof(InteractionCommandError), result.Error) + ": " + result.ErrorReason; logresult += " " + Enum.GetName(typeof(InteractionCommandError), result.Error) + ": " + result.ErrorReason;
// Specific responses to errors, if necessary // Specific responses to errors, if necessary
if (result.Error == InteractionCommandError.UnmetPrecondition && result.ErrorReason == RequireBotModeratorAttribute.FailMsg) { if (result.Error == InteractionCommandError.UnmetPrecondition) {
await context.Interaction.RespondAsync(RequireBotModeratorAttribute.Reply, ephemeral: true).ConfigureAwait(false); string errReply = result.ErrorReason switch {
RequireBotModeratorAttribute.Error => RequireBotModeratorAttribute.Reply,
EnforceBlockingAttribute.FailBlocked => EnforceBlockingAttribute.ReplyBlocked,
EnforceBlockingAttribute.FailModerated => EnforceBlockingAttribute.ReplyModerated,
RequireGuildContextAttribute.Error => RequireGuildContextAttribute.Reply,
_ => result.ErrorReason
};
await context.Interaction.RespondAsync(errReply, ephemeral: true).ConfigureAwait(false);
} else { } else {
// Generic error response // Generic error response
// TODO when implementing proper application error logging, see here
var ia = context.Interaction; var ia = context.Interaction;
if (ia.HasResponded) await ia.ModifyOriginalResponseAsync(p => p.Content = InternalError).ConfigureAwait(false); if (ia.HasResponded) await ia.ModifyOriginalResponseAsync(p => p.Content = InternalError).ConfigureAwait(false);
else await ia.RespondAsync(InternalError).ConfigureAwait(false); else await ia.RespondAsync(InternalError).ConfigureAwait(false);
// TODO when implementing proper error logging, see here
} }
} }