Skip to content

Commit

Permalink
Implement block check within a precondition
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
NoiTheCat committed Mar 12, 2022
1 parent 85b23e2 commit d700cd8
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 37 deletions.
2 changes: 1 addition & 1 deletion ApplicationCommands/BirthdayModule.cs
Expand Up @@ -4,7 +4,7 @@

namespace BirthdayBot.ApplicationCommands;

[RequireContext(ContextType.Guild)]
[RequireGuildContext]
[Group("birthday", HelpCmdBirthday)]
public class BirthdayModule : BotModuleBase {
public const string HelpCmdBirthday = "Commands relating to birthdays.";
Expand Down
1 change: 0 additions & 1 deletion ApplicationCommands/BirthdayOverrideModule.cs
Expand Up @@ -3,7 +3,6 @@

namespace BirthdayBot.ApplicationCommands;

[RequireContext(ContextType.Guild)]
[RequireBotModerator]
[Group("override", HelpCmdOverride)]
public class BirthdayOverrideModule : BotModuleBase {
Expand Down
1 change: 1 addition & 0 deletions ApplicationCommands/BotModuleBase.cs
Expand Up @@ -9,6 +9,7 @@ namespace BirthdayBot.ApplicationCommands;
/// <summary>
/// Base class for our interaction module classes. Contains common data for use in implementing classes.
/// </summary>
[EnforceBlocking]
public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionContext> {
protected const string HelpPfxModOnly = "Bot moderators only: ";
protected const string ErrGuildOnly = ":x: This command can only be run within a server.";
Expand Down
1 change: 0 additions & 1 deletion ApplicationCommands/ConfigModule.cs
Expand Up @@ -4,7 +4,6 @@

namespace BirthdayBot.ApplicationCommands;

[RequireContext(ContextType.Guild)]
[RequireBotModerator]
[Group("config", HelpCmdConfig)]
public class ConfigModule : BotModuleBase {
Expand Down
38 changes: 38 additions & 0 deletions ApplicationCommands/Preconditions/EnforceBlocking.cs
@@ -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();
}
}
Expand Up @@ -3,29 +3,27 @@

namespace BirthdayBot.ApplicationCommands;

// Contains preconditions used by our interaction modules.

/// <summary>
/// Precondition requiring the executing user be considered a bot moderator.
/// That is, they must either have the Manage Server permission or be a member of the designated bot moderator role.
/// Precondition requiring the executing user be recognized as a bot moderator.<br/>
/// A bot moderator has either the Manage Server permission or is a member of the designated bot moderator role.
/// </summary>
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 override string ErrorMessage => FailMsg;
public override string ErrorMessage => Error;

public override async Task<PreconditionResult> CheckRequirementsAsync(
IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) {
if (context.User is not SocketGuildUser user) {
return PreconditionResult.FromError("Failed due to non-guild context.");
}
// A bot moderator can only exist in a guild context, so we must do this check.
// 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();
var gconf = await ((SocketGuild)context.Guild).GetConfigAsync().ConfigureAwait(false);
if (gconf.ModeratorRole.HasValue && user.Roles.Any(r => r.Id == gconf.ModeratorRole.Value))
return PreconditionResult.FromSuccess();

return PreconditionResult.FromError(FailMsg);
return PreconditionResult.FromError(Error);
}
}
}
16 changes: 16 additions & 0 deletions ApplicationCommands/Preconditions/RequireGuildContext.cs
@@ -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) { }
}
2 changes: 1 addition & 1 deletion BirthdayBot.csproj
Expand Up @@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>3.3.0</Version>
<Version>3.3.1</Version>
<Authors>NoiTheCat</Authors>
</PropertyGroup>

Expand Down
13 changes: 11 additions & 2 deletions Data/GuildConfiguration.cs
Expand Up @@ -71,12 +71,19 @@ class GuildConfiguration {
}

/// <summary>
/// Checks if the given user exists in the block list.
/// If the server is in moderated mode, this always returns true.
/// Checks if the specified user is blocked by current guild policy (block list or moderated mode).
/// </summary>
[Obsolete("Block lists should be reimplemented in a more resource-efficient manner later.", false)]
public async Task<bool> IsUserBlockedAsync(ulong userId) {
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 c = db.CreateCommand();
c.CommandText = $"select * from {BackingTableBans} "
Expand All @@ -92,6 +99,7 @@ class GuildConfiguration {
/// <summary>
/// Adds the specified user to the block list corresponding to this guild.
/// </summary>
[Obsolete("Block lists will be reimplemented in a more practical manner later.", false)]
public async Task BlockUserAsync(ulong userId) {
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
using var c = db.CreateCommand();
Expand All @@ -109,6 +117,7 @@ class GuildConfiguration {
/// Removes the specified user from the block list corresponding to this guild.
/// </summary>
/// <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) {
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
using var c = db.CreateCommand();
Expand Down
36 changes: 16 additions & 20 deletions ShardInstance.cs
Expand Up @@ -170,26 +170,15 @@ public sealed class ShardInstance : IDisposable {
private async Task DiscordClient_InteractionCreated(SocketInteraction 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 {
await _interactionService.ExecuteCommandAsync(context, _services).ConfigureAwait(false);
} catch (Exception e) {
Log(nameof(DiscordClient_InteractionCreated), $"Unhandled exception. {e}");
// TODO when implementing proper error logging, see here
if (arg.Type == InteractionType.ApplicationCommand)
if (arg.HasResponded) await arg.ModifyOriginalResponseAsync(prop => prop.Content = ":warning: An unknown error occured.");
// TODO when implementing proper application error logging, see here
if (arg.Type == InteractionType.ApplicationCommand) {
if (arg.HasResponded) await arg.ModifyOriginalResponseAsync(prop => prop.Content = InternalError);
else await arg.RespondAsync(InternalError);
}
}
}

Expand All @@ -205,17 +194,24 @@ public sealed class ShardInstance : IDisposable {

if (result.Error != null) {
// 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
if (result.Error == InteractionCommandError.UnmetPrecondition && result.ErrorReason == RequireBotModeratorAttribute.FailMsg) {
await context.Interaction.RespondAsync(RequireBotModeratorAttribute.Reply, ephemeral: true).ConfigureAwait(false);
if (result.Error == InteractionCommandError.UnmetPrecondition) {
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 {
// Generic error response
// TODO when implementing proper application error logging, see here
var ia = context.Interaction;
if (ia.HasResponded) await ia.ModifyOriginalResponseAsync(p => p.Content = InternalError).ConfigureAwait(false);
else await ia.RespondAsync(InternalError).ConfigureAwait(false);
// TODO when implementing proper error logging, see here
}
}

Expand Down

0 comments on commit d700cd8

Please sign in to comment.