diff --git a/src/index.ts b/src/index.ts index 6216f512e..020a763b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,10 +42,12 @@ export * from './lib/utils/logger/Logger'; export * from './lib/utils/preconditions/conditions/IPreconditionCondition'; export * from './lib/utils/preconditions/conditions/PreconditionConditionAnd'; export * from './lib/utils/preconditions/conditions/PreconditionConditionOr'; -export * from './lib/utils/preconditions/containers/PermissionsPrecondition'; +export * from './lib/utils/preconditions/containers/ClientPermissionsPrecondition'; +export * from './lib/utils/preconditions/containers/UserPermissionsPrecondition'; export * from './lib/utils/preconditions/IPreconditionContainer'; export * from './lib/utils/preconditions/PreconditionContainerArray'; export * from './lib/utils/preconditions/PreconditionContainerSingle'; export type { CooldownContext } from './preconditions/Cooldown'; +export { CorePrecondition as ClientPermissionsCorePrecondition } from './preconditions/ClientPermissions'; export const version = '[VI]{version}[/VI]'; diff --git a/src/lib/errors/Identifiers.ts b/src/lib/errors/Identifiers.ts index a40a253d6..a4a84e945 100644 --- a/src/lib/errors/Identifiers.ts +++ b/src/lib/errors/Identifiers.ts @@ -41,6 +41,7 @@ export const enum Identifiers { PreconditionGuildPublicThreadOnly = 'preconditionGuildPublicThreadOnly', PreconditionGuildTextOnly = 'preconditionGuildTextOnly', PreconditionNSFW = 'preconditionNsfw', - PreconditionPermissions = 'preconditionPermissions', + PreconditionClientPermissions = 'preconditionClientPermissions', + PreconditionUserPermissions = 'preconditionUserPermissions', PreconditionThreadOnly = 'preconditionThreadOnly' } diff --git a/src/lib/structures/Command.ts b/src/lib/structures/Command.ts index ac740b3b1..49d0cd88f 100644 --- a/src/lib/structures/Command.ts +++ b/src/lib/structures/Command.ts @@ -112,6 +112,7 @@ export abstract class Command extends AliasPiece { this.parseConstructorPreConditionsRunIn(options); this.parseConstructorPreConditionsNsfw(options); this.parseConstructorPreConditionsRequiredClientPermissions(options); + this.parseConstructorPreConditionsRequiredUserPermissions(options); this.parseConstructorPreConditionsCooldown(options); } @@ -135,14 +136,26 @@ export abstract class Command extends AliasPiece { } /** - * Appends the `Permissions` precondition when {@link CommandOptions.requiredClientPermissions} resolves to a + * Appends the `ClientPermissions` precondition when {@link CommandOptions.requiredClientPermissions} resolves to a * non-zero bitfield. * @param options The command options given from the constructor. */ protected parseConstructorPreConditionsRequiredClientPermissions(options: CommandOptions) { const permissions = new Permissions(options.requiredClientPermissions); if (permissions.bitfield !== 0n) { - this.preconditions.append({ name: CommandPreConditions.Permissions, context: { permissions } }); + this.preconditions.append({ name: CommandPreConditions.ClientPermissions, context: { permissions } }); + } + } + + /** + * Appends the `UserPermissions` precondition when {@link CommandOptions.requiredUserPermissions} resolves to a + * non-zero bitfield. + * @param options The command options given from the constructor. + */ + protected parseConstructorPreConditionsRequiredUserPermissions(options: CommandOptions) { + const permissions = new Permissions(options.requiredUserPermissions); + if (permissions.bitfield !== 0n) { + this.preconditions.append({ name: CommandPreConditions.UserPermissions, context: { permissions } }); } } @@ -269,7 +282,8 @@ export const enum CommandPreConditions { GuildTextOnly = 'GuildTextOnly', GuildThreadOnly = 'GuildThreadOnly', NotSafeForWork = 'NSFW', - Permissions = 'Permissions' + ClientPermissions = 'ClientPermissions', + UserPermissions = 'UserPermissions' } /** @@ -361,6 +375,13 @@ export interface CommandOptions extends AliasPieceOptions, FlagStrategyOptions { */ requiredClientPermissions?: PermissionResolvable; + /** + * The required permissions for the user. + * @since 2.0.0 + * @default 0 + */ + requiredUserPermissions?: PermissionResolvable; + /** * The channels the command should run in. If set to `null`, no precondition entry will be added. Some optimizations are applied when given an array to reduce the amount of preconditions run (e.g. `'text'` and `'news'` becomes `'guild'`, and if both `'dm'` and `'guild'` are defined, then no precondition entry is added as it runs in all channels). * @since 2.0.0 diff --git a/src/lib/structures/Precondition.ts b/src/lib/structures/Precondition.ts index 5b77687c7..83fe02ff8 100644 --- a/src/lib/structures/Precondition.ts +++ b/src/lib/structures/Precondition.ts @@ -92,7 +92,10 @@ export interface Preconditions { GuildTextOnly: never; GuildThreadOnly: never; NSFW: never; - Permissions: { + ClientPermissions: { + permissions: Permissions; + }; + UserPermissions: { permissions: Permissions; }; } diff --git a/src/lib/utils/preconditions/containers/ClientPermissionsPrecondition.ts b/src/lib/utils/preconditions/containers/ClientPermissionsPrecondition.ts new file mode 100644 index 000000000..20f8a7980 --- /dev/null +++ b/src/lib/utils/preconditions/containers/ClientPermissionsPrecondition.ts @@ -0,0 +1,39 @@ +import { PermissionResolvable, Permissions } from 'discord.js'; +import type { PreconditionSingleResolvableDetails } from '../PreconditionContainerSingle'; + +/** + * Constructs a contextful permissions precondition requirement. + * @since 1.0.0 + * @example + * ```typescript + * export class CoreCommand extends Command { + * public constructor(context: PieceContext) { + * super(context, { + * preconditions: [ + * 'GuildOnly', + * new ClientPermissionsPrecondition('ADD_REACTIONS') + * ] + * }); + * } + * + * public run(message: Message, args: Args) { + * // ... + * } + * } + * ``` + */ +export class ClientPermissionsPrecondition implements PreconditionSingleResolvableDetails<'ClientPermissions'> { + public name: 'ClientPermissions'; + public context: { permissions: Permissions }; + + /** + * Constructs a precondition container entry. + * @param permissions The permissions that will be required by this command. + */ + public constructor(permissions: PermissionResolvable) { + this.name = 'ClientPermissions'; + this.context = { + permissions: new Permissions(permissions) + }; + } +} diff --git a/src/lib/utils/preconditions/containers/PermissionsPrecondition.ts b/src/lib/utils/preconditions/containers/UserPermissionsPrecondition.ts similarity index 78% rename from src/lib/utils/preconditions/containers/PermissionsPrecondition.ts rename to src/lib/utils/preconditions/containers/UserPermissionsPrecondition.ts index c609d8a82..d106e5878 100644 --- a/src/lib/utils/preconditions/containers/PermissionsPrecondition.ts +++ b/src/lib/utils/preconditions/containers/UserPermissionsPrecondition.ts @@ -11,7 +11,7 @@ import type { PreconditionSingleResolvableDetails } from '../PreconditionContain * super(context, { * preconditions: [ * 'GuildOnly', - * new PermissionsPrecondition('ADD_REACTIONS') + * new UserPermissionsPrecondition('ADD_REACTIONS') * ] * }); * } @@ -22,8 +22,8 @@ import type { PreconditionSingleResolvableDetails } from '../PreconditionContain * } * ``` */ -export class PermissionsPrecondition implements PreconditionSingleResolvableDetails<'Permissions'> { - public name: 'Permissions'; +export class UserPermissionsPrecondition implements PreconditionSingleResolvableDetails<'UserPermissions'> { + public name: 'UserPermissions'; public context: { permissions: Permissions }; /** @@ -31,7 +31,7 @@ export class PermissionsPrecondition implements PreconditionSingleResolvableDeta * @param permissions The permissions that will be required by this command. */ public constructor(permissions: PermissionResolvable) { - this.name = 'Permissions'; + this.name = 'UserPermissions'; this.context = { permissions: new Permissions(permissions) }; diff --git a/src/preconditions/Permissions.ts b/src/preconditions/ClientPermissions.ts similarity index 82% rename from src/preconditions/Permissions.ts rename to src/preconditions/ClientPermissions.ts index db5c19f74..0be449ee7 100644 --- a/src/preconditions/Permissions.ts +++ b/src/preconditions/ClientPermissions.ts @@ -4,17 +4,18 @@ import type { Command } from '../lib/structures/Command'; import { Precondition, PreconditionContext, PreconditionResult } from '../lib/structures/Precondition'; export class CorePrecondition extends Precondition { - private readonly dmChannelPermissions = new Permissions([ - Permissions.FLAGS.VIEW_CHANNEL, - Permissions.FLAGS.SEND_MESSAGES, - Permissions.FLAGS.SEND_TTS_MESSAGES, - Permissions.FLAGS.EMBED_LINKS, - Permissions.FLAGS.ATTACH_FILES, - Permissions.FLAGS.READ_MESSAGE_HISTORY, - Permissions.FLAGS.MENTION_EVERYONE, - Permissions.FLAGS.USE_EXTERNAL_EMOJIS, - Permissions.FLAGS.ADD_REACTIONS - ]).freeze(); + private readonly dmChannelPermissions = new Permissions( + ~new Permissions([ + // + 'ADD_REACTIONS', + 'ATTACH_FILES', + 'EMBED_LINKS', + 'READ_MESSAGE_HISTORY', + 'SEND_MESSAGES', + 'USE_EXTERNAL_EMOJIS', + 'VIEW_CHANNEL' + ]).bitfield & Permissions.ALL + ).freeze(); public run(message: Message, _command: Command, context: PreconditionContext): PreconditionResult { const required = (context.permissions as Permissions) ?? new Permissions(); @@ -25,7 +26,7 @@ export class CorePrecondition extends Precondition { return missing.length === 0 ? this.ok() : this.error({ - identifier: Identifiers.PreconditionPermissions, + identifier: Identifiers.PreconditionClientPermissions, message: `I am missing the following permissions to run this command: ${missing .map((perm) => CorePrecondition.readablePermissions[perm]) .join(', ')}`, @@ -33,7 +34,7 @@ export class CorePrecondition extends Precondition { }); } - protected static readonly readablePermissions = { + public static readonly readablePermissions = { ADD_REACTIONS: 'Add Reactions', ADMINISTRATOR: 'Administrator', ATTACH_FILES: 'Attach Files', diff --git a/src/preconditions/UserPermissions.ts b/src/preconditions/UserPermissions.ts new file mode 100644 index 000000000..1dee47734 --- /dev/null +++ b/src/preconditions/UserPermissions.ts @@ -0,0 +1,38 @@ +import { Message, NewsChannel, Permissions, TextChannel } from 'discord.js'; +import { Identifiers } from '../lib/errors/Identifiers'; +import type { Command } from '../lib/structures/Command'; +import { Precondition, PreconditionContext, PreconditionResult } from '../lib/structures/Precondition'; +import { CorePrecondition as ClientPermissionsPrecondition } from './ClientPermissions'; + +export class CorePrecondition extends Precondition { + private readonly dmChannelPermissions = new Permissions( + ~new Permissions([ + 'ADD_REACTIONS', + 'ATTACH_FILES', + 'EMBED_LINKS', + 'READ_MESSAGE_HISTORY', + 'SEND_MESSAGES', + 'USE_EXTERNAL_EMOJIS', + 'VIEW_CHANNEL', + 'USE_EXTERNAL_STICKERS', + 'MENTION_EVERYONE' + ]).bitfield & Permissions.ALL + ).freeze(); + + public run(message: Message, _command: Command, context: PreconditionContext): PreconditionResult { + const required = (context.permissions as Permissions) ?? new Permissions(); + const channel = message.channel as TextChannel | NewsChannel; + + const permissions = message.guild ? channel.permissionsFor(message.client.id!)! : this.dmChannelPermissions; + const missing = permissions.missing(required); + return missing.length === 0 + ? this.ok() + : this.error({ + identifier: Identifiers.PreconditionUserPermissions, + message: `You are missing the following permissions to run this command: ${missing + .map((perm) => ClientPermissionsPrecondition.readablePermissions[perm]) + .join(', ')}`, + context: { missing } + }); + } +}