Skip to content

Commit

Permalink
Merge pull request #546 from aternosorg/new-slash-command-permissions
Browse files Browse the repository at this point in the history
New slash command permissions
  • Loading branch information
JulianVennen authored Dec 21, 2022
2 parents 3d48aea + 2678b4e commit 695e3de
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 126 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "modbot",
"version": "3.1.2",
"version": "3.2.0",
"description": "Discord Bot for the Aternos Discord server",
"main": "index.js",
"type": "module",
Expand Down
5 changes: 4 additions & 1 deletion src/bot/Bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import MessageDeleteEmbed from '../embeds/MessageDeleteEmbed.js';

export class Bot {
/**
* @type {Client}
* @type {import('discord.js').Client}
*/
#client;

Expand Down Expand Up @@ -44,6 +44,9 @@ export class Bot {
});
}

/**
* @return {import('discord.js').Client}
*/
get client() {
return this.#client;
}
Expand Down
3 changes: 2 additions & 1 deletion src/commands/Command.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export default class Command extends ExecutableCommand {
* Permissions that members need to execute this command by default.
* Null: no permissions required. Empty bitfield: disabled by default
*
* This is not checked by ModBot and is only used to register commands on discord
* For slash commands this is not checked by ModBot and is only used to register commands on Discord
* For context menus, buttons and other interactions ModBot emulate Discord's permission system
* @return {?import('discord.js').PermissionsBitField}
*/
getDefaultMemberPermissions() {
Expand Down
127 changes: 4 additions & 123 deletions src/commands/CommandManager.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import bot from '../bot/Bot.js';
import {
ApplicationCommandPermissionType,
ApplicationCommandType,
hyperlink,
PermissionFlagsBits,
RESTJSONErrorCodes
} from 'discord.js';
import {AUTOCOMPLETE_OPTIONS_LIMIT} from '../util/apiLimits.js';
Expand All @@ -16,7 +14,6 @@ import ExportCommand from './bot/ExportCommand.js';
import ImportCommand from './bot/ImportCommand.js';
import InfoCommand, {GITHUB_REPOSITORY} from './bot/InfoCommand.js';
import UserInfoCommand from './user/UserInfoCommand.js';
import MemberWrapper from '../discord/MemberWrapper.js';
import BanCommand from './user/BanCommand.js';
import UnbanCommand from './user/UnbanCommand.js';
import VideoCommand from './external/VideoCommand.js';
Expand All @@ -43,6 +40,7 @@ import BadWordCommand from './settings/BadWordCommand.js';
import {replyOrFollowUp} from '../util/interaction.js';
import logger from '../bot/Logger.js';
import SafeSearchCommand from './settings/SafeSearchCommand.js';
import SlashCommandPermissionManagers from '../discord/permissions/SlashCommandPermissionManagers.js';

const cooldowns = new Cache();

Expand Down Expand Up @@ -399,126 +397,9 @@ export class CommandManager {
* @return {Promise<boolean>}
*/
async hasPermission(interaction, command) {
const member = await (new MemberWrapper(interaction.user, interaction.guild)).fetchMember();

if (interaction.memberPermissions.has(PermissionFlagsBits.Administrator)) {
return true;
}

if (!interaction.memberPermissions.has(PermissionFlagsBits.UseApplicationCommands)) {
return false;
}

// Check permissions for specific command if they exist
const commandPermissions = await this.fetchCommandOverrides(interaction.guild, command.id);
if (commandPermissions.length) {
return this.hasPermissionInOverrides(member, interaction.channel, commandPermissions);
}

// Fallback to global permissions if they exist
const globalPermissions = await this.fetchCommandOverrides(interaction.guild, bot.client.user.id);
if (globalPermissions.length) {
return this.hasPermissionInOverrides(member, interaction.channel, globalPermissions);
}

// Fallback to default permissions
switch (command.getDefaultMemberPermissions()) {
case null:
return true;
case 0:
return false;
default:
return interaction.memberPermissions.has(command.getDefaultMemberPermissions());
}
}

/**
*
* @param {import('discord.js').GuildMember} member
* @param {import('discord.js').GuildTextBasedChannel} channel
* @param {import('discord.js').ApplicationCommandPermissions[]} overrides
* @return {Promise<?boolean>}
*/
async hasPermissionInOverrides(member, channel, overrides) {
let permission = null;
// https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-constants
const everyoneRoleId = member.guild.id;
const allChannelsId = (BigInt(member.guild.id) - 1n).toString();

const everyoneOverride = overrides.find(override =>
override.type === ApplicationCommandPermissionType.Role
&& override.id === everyoneRoleId
) ?? null;

const roleOverrides = overrides.filter(override =>
override.type === ApplicationCommandPermissionType.Role
&& override.id !== everyoneRoleId
&& member.roles.resolve(override.id)
);

const memberOverride = overrides.find(override =>
override.type === ApplicationCommandPermissionType.User
&& override.id === member.id
);

const globalChannelOverride = overrides.find(override =>
override.type === ApplicationCommandPermissionType.Channel
&& override.id === allChannelsId
) ?? null;

const channelOverride = overrides.find(override =>
override.type === ApplicationCommandPermissionType.Channel
&& override.id === channel.id
) ?? null;

// check channel permissions
if (channelOverride && !channelOverride.permission) {
return false;
}
if (!channelOverride && globalChannelOverride && !globalChannelOverride.permission) {
return false;
}

// Apply permissions for the default role (@everyone).
if (everyoneOverride) {
permission = everyoneOverride.permission;
}

// Apply denies for all additional roles the guild member has at once.
if (roleOverrides.some(override => !override.permission)) {
permission = false;
}

// Apply allows for all additional roles the guild member has at once.
if (roleOverrides.some(override => override.permission)) {
permission = true;
}

// Apply permissions for the specific guild member if they exist.
if (memberOverride) {
permission = memberOverride.permission;
}

return permission;
}

/**
*
* @param {import('discord.js').Guild} guild
* @param {import('discord.js').Snowflake} commandId
* @return {Promise<import('discord.js').ApplicationCommandPermissions[]>}
*/
async fetchCommandOverrides(guild, commandId) {
try {
return await guild.commands.permissions.fetch({command: commandId});
}
catch (e) {
if (e.code === RESTJSONErrorCodes.UnknownApplicationCommandPermissions) {
return [];
} else {
throw e;
}
}
return SlashCommandPermissionManagers
.getManager(interaction)
.hasPermission(interaction, command);
}
}

Expand Down
44 changes: 44 additions & 0 deletions src/discord/permissions/SlashCommandPermissionManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {RESTJSONErrorCodes} from 'discord.js';
import SlashCommandPermissionOverrides from './SlashCommandPermissionOverrides.js';
import bot from '../../bot/Bot.js';

/**
* Emulate Discord Slash Command Permissions
* @abstract
*/
export default class SlashCommandPermissionManager {

/**
* Calculates if a member has the permission to execute a command in a guild
* Uses the older V2 Permission system: https://discord.com/developers/docs/change-log#updated-command-permissions
* @param {import('discord.js').Interaction<"cached">} interaction
* @param {Command} command
* @return {Promise<boolean>}
*/
async hasPermission(interaction, command) {
throw new Error('Not implemented');
}

/**
* fetch the overrides for a command or application
* @param {import('discord.js').Interaction<"cached">} interaction
* @param {import('discord.js').Snowflake} [commandId] leave empty to fetch global permissions
* @return {Promise<SlashCommandPermissionOverrides>}
*/
async fetchOverrides(interaction, commandId = bot.client.user.id) {
let overrides = [];
try {
overrides = await interaction.guild.commands.permissions.fetch({command: commandId});

}
catch (e) {
if (e.code === RESTJSONErrorCodes.UnknownApplicationCommandPermissions) {
overrides = [];
} else {
throw e;
}
}

return new SlashCommandPermissionOverrides(overrides, interaction.guild, interaction.member, interaction.channel);
}
}
85 changes: 85 additions & 0 deletions src/discord/permissions/SlashCommandPermissionManagerV2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {PermissionFlagsBits} from 'discord.js';
import SlashCommandPermissionManager from './SlashCommandPermissionManager.js';

export default class SlashCommandPermissionManagerV2 extends SlashCommandPermissionManager {
/**
* Calculates if a member has the permission to execute a command in a guild
* Uses the older V2 Permission system: https://discord.com/developers/docs/change-log#updated-command-permissions
* @param {import('discord.js').Interaction<"cached">} interaction
* @param {Command} command
* @return {Promise<boolean>}
*/
async hasPermission(interaction, command) {
if (interaction.memberPermissions.has(PermissionFlagsBits.Administrator)) {
return true;
}

if (!interaction.memberPermissions.has(PermissionFlagsBits.UseApplicationCommands)) {
return false;
}

// Check permissions for specific command if they exist
const commandPermissions = await this.fetchOverrides(interaction, command.id);
if (commandPermissions.rawOverrides.length) {
return this.hasPermissionInOverrides(commandPermissions);
}

// Fallback to global permissions if they exist
const globalPermissions = await this.fetchOverrides(interaction);
if (globalPermissions.rawOverrides.length) {
return this.hasPermissionInOverrides(globalPermissions);
}

// Fallback to default permissions
switch (command.getDefaultMemberPermissions()) {
case null:
return true;
case 0:
return false;
default:
return interaction.memberPermissions.has(command.getDefaultMemberPermissions());
}
}

/**
* @param {SlashCommandPermissionOverrides} overrides
* @return {Promise<?boolean>}
*/
async hasPermissionInOverrides(overrides) {
let permission = null;

// check channel permissions
if (overrides.channelOverride) {
if (!overrides.channelOverride.permission) {
return false;
}
}
else if (overrides.allChannelsOverride) {
if (!overrides.allChannelsOverride.permission) {
return false;
}
}

// Apply permissions for the default role (@everyone).
if (overrides.everyoneOverride) {
permission = overrides.everyoneOverride.permission;
}

// Apply denies for all additional roles the guild member has at once.
if (overrides.memberRoleOverrides.some(override => !override.permission)) {
permission = false;
}

// Apply allows for all additional roles the guild member has at once.
if (overrides.memberRoleOverrides.some(override => override.permission)) {
permission = true;
}

// Apply permissions for the specific guild member if they exist.
if (overrides.memberOverride) {
permission = overrides.memberOverride.permission;
}

return permission;
}
}
Loading

0 comments on commit 695e3de

Please sign in to comment.