Skip to content

Commit

Permalink
Add Command#clientPermissions
Browse files Browse the repository at this point in the history
Similar to Command#callerPermissions, but represents permissions the bot is requesting that the command will need in order to function. It's basically completely arbitrary but will allow you to have the bot automatically make sure permissions you will need are present for the command
  • Loading branch information
zajrik committed Apr 19, 2017
1 parent 60310fa commit 977e463
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 34 deletions.
70 changes: 45 additions & 25 deletions src/command/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class Command<T extends Client>
public hidden: boolean;
public argOpts: ArgOpts;
public callerPermissions: PermissionResolvable[];
public clientPermissions: PermissionResolvable[];
public roles: string[];
public ownerOnly: boolean;
public overloads: string;
Expand Down Expand Up @@ -113,6 +114,17 @@ export class Command<T extends Client>
* @type {external:PermissionResolvable[]}
*/

/**
* Array of permissions required by the client
* to be able to execute the command in the guild
* the command is called in.
*
* If any permissions are provided the command's `guildOnly`
* property will be automatically overridden to true
* @name Command#clientPermissions
* @type {external:PermissionResolvable[]}
*/

/**
* Array of roles required to use the command. If the command caller
* has any of the roles in the array, they will be able to use the command
Expand Down Expand Up @@ -163,47 +175,40 @@ export class Command<T extends Client>
}

/**
* Assert {@link Command#action} is typeof Function, finishing the
* command creation process.<br>Called by {@link CommandRegistry#register}
* Make necessary asserts for Command validity. Called by the CommandLoader when
* adding Commands to the CommandRegistry via {@link CommandRegistry#register}
* @returns {void}
*/
public register(): void
{
// Set defaults if not present
if (!this.aliases) this.aliases = [];
if (!this.group) this.group = 'base';
if (!this.guildOnly) this.guildOnly = false;
if (!this.hidden) this.hidden = false;
if (!this.argOpts) this.argOpts = {};
if (!this.argOpts.separator) this.argOpts.separator = ' ';
if (!this.callerPermissions) this.callerPermissions = [];
if (!this.roles) this.roles = [];
if (!this.ownerOnly) this.ownerOnly = false;
if (typeof this.aliases === 'undefined') this.aliases = [];
if (typeof this.group === 'undefined') this.group = 'base';
if (typeof this.guildOnly === 'undefined') this.guildOnly = false;
if (typeof this.hidden === 'undefined') this.hidden = false;
if (typeof this.argOpts === 'undefined') this.argOpts = {};
if (typeof this.argOpts.separator === 'undefined') this.argOpts.separator = ' ';
if (typeof this.callerPermissions === 'undefined') this.callerPermissions = [];
if (typeof this.clientPermissions === 'undefined') this.clientPermissions = [];
if (typeof this.roles === 'undefined') this.roles = [];
if (typeof this.ownerOnly === 'undefined') this.ownerOnly = false;

// Make necessary asserts
if (!this.name) throw new Error(`A command is missing a name`);
if (!this.description) throw new Error(`You must provide a description for command: ${this.name}`);
if (!this.usage) throw new Error(`You must provide usage information for command: ${this.name}`);
if (!this.group) throw new Error(`You must provide a group for command: ${this.name}`);
if (this.aliases && !Array.isArray(this.aliases)) throw new Error(`Aliases for command "${this.name}" must be an array`);
if (this.callerPermissions && !Array.isArray(this.callerPermissions)) throw new Error(`callerPermissions for Command "${this.name}" must be an array`);
if (this.callerPermissions && this.callerPermissions.length > 0)
for (const [index, perm] of this.callerPermissions.entries())
{
try
{
Permissions.resolve(perm);
}
catch (err)
{
throw new Error(`Command "${this.name}" caller permission "${this.callerPermissions[index]}" at "${this.name}".callerPermissions[${index}] is not a valid permission.\n\n${err}`);
}
}
if (this.clientPermissions && !Array.isArray(this.clientPermissions)) throw new Error(`clientPermissions for Command "${this.name}" must be an array`);
if (this.callerPermissions && this.callerPermissions.length) this._validatePermissions('callerPermissions', this.callerPermissions);
if (this.clientPermissions && this.clientPermissions.length) this._validatePermissions('clientPermissions', this.clientPermissions);
if (this.roles && !Array.isArray(this.roles)) throw new Error(`Roles for command ${this.name} must be an array`);
if (this.overloads && this.group !== 'base') throw new Error('Commands may only overload commands in group "base"');

// Default guildOnly to true if permissions/roles are given
if (this.callerPermissions.length > 0 || this.roles.length > 0) this.guildOnly = true;
if (!this.guildOnly && (this.callerPermissions.length
|| this.clientPermissions.length
|| this.roles.length)) this.guildOnly = true;

if (!this.action) throw new Error(`Command "${this.name}".action: expected Function, got: ${typeof this.action}`);
if (!(this.action instanceof Function)) throw new Error(`Command "${this.name}".action: expected Function, got: ${typeof this.action}`);
Expand Down Expand Up @@ -261,4 +266,19 @@ export class Command<T extends Client>
if (code) return message.channel.sendCode(code, response);
return message.channel.sendMessage(response);
}

/**
* Validate permissions resolvables in the given array, throwing an error
* for any that are invalid
* @private
*/
private _validatePermissions(method: string, permissions: PermissionResolvable[]): void
{
let errString: (i: number, err: any) => string = (i: number, err: any) =>
`Command "${this.name}" permission "${permissions[i]}" at "${this.name}".${method}[${i}] is not a valid permission.\n\n${err}`;

for (const [index, perm] of permissions.entries())
try { Permissions.resolve(perm); }
catch (err) { throw new Error(errString(index, err)); }
}
}
39 changes: 34 additions & 5 deletions src/command/CommandDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export class CommandDispatcher<T extends Client>
// Check ratelimits
if (!this.checkRateLimits(message, command)) return;

// Alert for missing client permissions
const missingClientPermissions: PermissionResolvable[] = this.checkClientPermissions(command, message, dm);
if (missingClientPermissions.length > 0)
{
message.channel.send(this.missingClientPermissionsError(missingClientPermissions));
return;
}

// Remove clientuser from message.mentions if only mentioned one time as a prefix
if (!(!dm && prefix === await message.guild.storage.settings.get('prefix')) && prefix !== ''
&& (message.content.match(new RegExp(`<@!?${this._client.user.id}>`, 'g')) || []).length === 1)
Expand Down Expand Up @@ -143,8 +151,8 @@ export class CommandDispatcher<T extends Client>
&& (await storage.settings.get('disabledGroups')).includes(command.group)) return false;

if (dm && command.guildOnly) throw this.guildOnlyError();
let missingPermissions: PermissionResolvable[] = this.checkPermissions(command, message, dm);
if (missingPermissions.length > 0) throw this.missingPermissionsError(missingPermissions);
const missingCallerPermissions: PermissionResolvable[] = this.checkCallerPermissions(command, message, dm);
if (missingCallerPermissions.length > 0) throw this.missingCallerPermissionsError(missingCallerPermissions);
if (!(await this.checkLimiter(command, message, dm))) throw await this.failedLimiterError(command, message);
if (!this.hasRoles(command, message, dm)) throw this.missingRolesError(command);

Expand Down Expand Up @@ -198,10 +206,20 @@ export class CommandDispatcher<T extends Client>
return passedRateLimiters;
}

/**
* Check that the client has the permissions requested by the
* command in the channel the command is being called in
*/
private checkClientPermissions(command: Command<T>, message: Message, dm: boolean): PermissionResolvable[]
{
return dm ? [] : command.clientPermissions.filter(a =>
!(<TextChannel> message.channel).permissionsFor(this._client.user).has(a));
}

/**
* Compare user permissions to the command's requisites
*/
private checkPermissions(command: Command<T>, message: Message, dm: boolean): PermissionResolvable[]
private checkCallerPermissions(command: Command<T>, message: Message, dm: boolean): PermissionResolvable[]
{
return this._client.selfbot || dm ? [] : command.callerPermissions.filter(a =>
!(<TextChannel> message.channel).permissionsFor(message.author).has(a));
Expand Down Expand Up @@ -282,9 +300,20 @@ export class CommandDispatcher<T extends Client>
}

/**
* Return an error for missing permissions
* Return an error for missing caller permissions
*/
private missingClientPermissionsError(missing: PermissionResolvable[]): string
{
return `**I must be given the following permission`
+ `${missing.length > 1 ? 's' : ''} `
+ `for that command to be usable in this channel:**\n\`\`\`css\n`
+ `${missing.join(', ')}\n\`\`\``;
}

/**
* Return an error for missing caller permissions
*/
private missingPermissionsError(missing: PermissionResolvable[]): string
private missingCallerPermissionsError(missing: PermissionResolvable[]): string
{
return `**You're missing the following permission`
+ `${missing.length > 1 ? 's' : ''} `
Expand Down
2 changes: 2 additions & 0 deletions src/types/CommandInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* @property {boolean} [hidden=false] See: {@link Command#hidden}
* @property {ArgOpts} [argOpts] See: {@link Command#argOpts}, {@link ArgOpts}
* @property {PermissionResolvable[]} [callerPermissions=[]] See: {@link Command#callerPermissions}
* @property {PermissionResolvable[]} [clientPermissions=[]] See: {@link Command#clientPermissions}
* @property {string[]} [roles=[]] See: {@link Command#roles}
* @property {boolean} [ownerOnly=false] See: {@link Command#ownerOnly}
* @property {string} [overloads=null] See: {@link Command#overloads}
Expand All @@ -32,6 +33,7 @@ export type CommandInfo = {
hidden?: boolean;
argOpts?: ArgOpts;
callerPermissions?: PermissionResolvable[];
clientPermissions?: PermissionResolvable[];
roles?: string[];
ownerOnly?: boolean;
overloads?: BaseCommandName;
Expand Down
14 changes: 10 additions & 4 deletions test/commands/test_command.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { Client, Command, Message, CommandDecorators } from '../../bin';
const { using, guildOnly } = CommandDecorators;
import { Client, Command, Message, CommandDecorators, Logger, logger } from '../../bin';
const { using, guildOnly, group } = CommandDecorators;
import * as util from 'util';

@guildOnly
@group('test')
export default class extends Command<Client>
{
@logger private readonly logger: Logger;
public constructor(client: Client)
{
super(client, {
name: 'test',
description: 'test command',
usage: '<prefix>test',
group: 'test'
clientPermissions: ['MANAGE_GUILD'],
ratelimit: '2/10s'
});
}

@using((message, args) => [message, args.map(a => a.toUpperCase())])
public action(message: Message, args: string[]): void
{
message.channel.send(args.join(' '));
message.channel.send(args.join(' ') || 'MISSING ARGS');
this.logger.debug('Command:test', util.inspect(this.clientPermissions));
this.logger.debug('Command:test', util.inspect(this.group));
}
}

0 comments on commit 977e463

Please sign in to comment.