Skip to content

Commit

Permalink
refactor: move preconditions resolvers to dedicated files (#679)
Browse files Browse the repository at this point in the history
  • Loading branch information
favna authored Oct 23, 2023
1 parent 832c979 commit d9bbb28
Show file tree
Hide file tree
Showing 22 changed files with 515 additions and 139 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export * from './lib/parsers/Args';
export * from './lib/plugins/Plugin';
export * from './lib/plugins/PluginManager';
export * from './lib/plugins/symbols';
export * as PreconditionResolvers from './lib/precondition-resolvers/index';
export type { EmojiObject } from './lib/resolvers/emoji';
export * as Resolvers from './lib/resolvers/index';
export type { MessageResolverOptions } from './lib/resolvers/message';
Expand Down
19 changes: 19 additions & 0 deletions src/lib/precondition-resolvers/clientPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PermissionsBitField, type PermissionResolvable } from 'discord.js';
import { CommandPreConditions } from '../types/Enums';
import type { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';

/**
* Appends the `ClientPermissions` precondition when {@link Command.Options.requiredClientPermissions} resolves to a
* non-zero bitfield.
* @param requiredClientPermissions The required client permissions.
* @param preconditionContainerArray The precondition container array to append the precondition to.
*/
export function parseConstructorPreConditionsRequiredClientPermissions(
requiredClientPermissions: PermissionResolvable | undefined,
preconditionContainerArray: PreconditionContainerArray
) {
const permissions = new PermissionsBitField(requiredClientPermissions);
if (permissions.bitfield !== 0n) {
preconditionContainerArray.append({ name: CommandPreConditions.ClientPermissions, context: { permissions } });
}
}
42 changes: 42 additions & 0 deletions src/lib/precondition-resolvers/cooldown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { container } from '@sapphire/pieces';
import type { Command } from '../structures/Command';
import { BucketScope, CommandPreConditions } from '../types/Enums';
import { type PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';

/**
* Appends the `Cooldown` precondition when {@link Command.Options.cooldownLimit} and
* {@link Command.Options.cooldownDelay} are both non-zero.
*
* @param command The command to parse cooldowns for.
* @param cooldownLimit The cooldown limit to use.
* @param cooldownDelay The cooldown delay to use.
* @param cooldownScope The cooldown scope to use.
* @param cooldownFilteredUsers The cooldown filtered users to use.
* @param preconditionContainerArray The precondition container array to append the precondition to.
*/
export function parseConstructorPreConditionsCooldown<P, O extends Command.Options>(
command: Command<P, O>,
cooldownLimit: number | undefined,
cooldownDelay: number | undefined,
cooldownScope: BucketScope | undefined,
cooldownFilteredUsers: string[] | undefined,
preconditionContainerArray: PreconditionContainerArray
) {
const { defaultCooldown } = container.client.options;

// We will check for whether the command is filtered from the defaults, but we will allow overridden values to
// be set. If an overridden value is passed, it will have priority. Otherwise, it will default to 0 if filtered
// (causing the precondition to not be registered) or the default value with a fallback to a single-use cooldown.
const filtered = defaultCooldown?.filteredCommands?.includes(command.name) ?? false;
const limit = cooldownLimit ?? (filtered ? 0 : defaultCooldown?.limit ?? 1);
const delay = cooldownDelay ?? (filtered ? 0 : defaultCooldown?.delay ?? 0);

if (limit && delay) {
const scope = cooldownScope ?? defaultCooldown?.scope ?? BucketScope.User;
const filteredUsers = cooldownFilteredUsers ?? defaultCooldown?.filteredUsers;
preconditionContainerArray.append({
name: CommandPreConditions.Cooldown,
context: { scope, limit, delay, filteredUsers }
});
}
}
5 changes: 5 additions & 0 deletions src/lib/precondition-resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './clientPermissions';
export * from './cooldown';
export * from './nsfw';
export * from './runIn';
export * from './userPermissions';
11 changes: 11 additions & 0 deletions src/lib/precondition-resolvers/nsfw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CommandPreConditions } from '../types/Enums';
import type { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';

/**
* Appends the `NSFW` precondition if {@link SubcommandMappingMethod.nsfw} is set to true.
* @param nsfw Whether this command is NSFW or not.
* @param preconditionContainerArray The precondition container array to append the precondition to.
*/
export function parseConstructorPreConditionsNsfw(nsfw: boolean | undefined, preconditionContainerArray: PreconditionContainerArray) {
if (nsfw) preconditionContainerArray.append(CommandPreConditions.NotSafeForWork);
}
46 changes: 46 additions & 0 deletions src/lib/precondition-resolvers/runIn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { isNullish } from '@sapphire/utilities';
import type { ChannelType } from 'discord.js';
import { Command } from '../structures/Command';
import type { CommandRunInUnion, CommandSpecificRunIn } from '../types/CommandTypes';
import { CommandPreConditions } from '../types/Enums';
import type { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';

/**
* Appends the `RunIn` precondition based on the values passed, defaulting to `null`, which doesn't add a
* precondition.
* @param runIn The command's `runIn` option field from the constructor.
* @param resolveConstructorPreConditionsRunType The function to resolve the run type from the constructor.
* @param preconditionContainerArray The precondition container array to append the precondition to.
*/
export function parseConstructorPreConditionsRunIn(
runIn: CommandRunInUnion | CommandSpecificRunIn,
resolveConstructorPreConditionsRunType: (types: CommandRunInUnion) => readonly ChannelType[] | null,
preconditionContainerArray: PreconditionContainerArray
) {
// Early return if there's no runIn option:
if (isNullish(runIn)) return;

if (Command.runInTypeIsSpecificsObject(runIn)) {
const messageRunTypes = resolveConstructorPreConditionsRunType(runIn.messageRun);
const chatInputRunTypes = resolveConstructorPreConditionsRunType(runIn.chatInputRun);
const contextMenuRunTypes = resolveConstructorPreConditionsRunType(runIn.contextMenuRun);

if (messageRunTypes !== null || chatInputRunTypes !== null || contextMenuRunTypes !== null) {
preconditionContainerArray.append({
name: CommandPreConditions.RunIn,
context: {
types: {
messageRun: messageRunTypes ?? [],
chatInputRun: chatInputRunTypes ?? [],
contextMenuRun: contextMenuRunTypes ?? []
}
}
});
}
} else {
const types = resolveConstructorPreConditionsRunType(runIn);
if (types !== null) {
preconditionContainerArray.append({ name: CommandPreConditions.RunIn, context: { types } });
}
}
}
19 changes: 19 additions & 0 deletions src/lib/precondition-resolvers/userPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PermissionsBitField, type PermissionResolvable } from 'discord.js';
import { CommandPreConditions } from '../types/Enums';
import type { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';

/**
* Appends the `UserPermissions` precondition when {@link Command.Options.requiredUserPermissions} resolves to a
* non-zero bitfield.
* @param requiredUserPermissions The required user permissions.
* @param preconditionContainerArray The precondition container array to append the precondition to.
*/
export function parseConstructorPreConditionsRequiredUserPermissions(
requiredUserPermissions: PermissionResolvable | undefined,
preconditionContainerArray: PreconditionContainerArray
) {
const permissions = new PermissionsBitField(requiredUserPermissions);
if (permissions.bitfield !== 0n) {
preconditionContainerArray.append({ name: CommandPreConditions.UserPermissions, context: { permissions } });
}
}
82 changes: 21 additions & 61 deletions src/lib/structures/Command.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ArgumentStream, Lexer, Parser, type IUnorderedStrategy } from '@sapphire/lexure';
import { AliasPiece } from '@sapphire/pieces';
import { isNullish, isObject, type Awaitable } from '@sapphire/utilities';
import {
ChannelType,
ChatInputCommandInteraction,
ContextMenuCommandInteraction,
PermissionsBitField,
type AutocompleteInteraction,
type Message
} from 'discord.js';
import { ChannelType, ChatInputCommandInteraction, ContextMenuCommandInteraction, type AutocompleteInteraction, type Message } from 'discord.js';
import { Args } from '../parsers/Args';
import {
parseConstructorPreConditionsCooldown,
parseConstructorPreConditionsNsfw,
parseConstructorPreConditionsRequiredClientPermissions,
parseConstructorPreConditionsRequiredUserPermissions,
parseConstructorPreConditionsRunIn
} from '../precondition-resolvers/index';
import type {
AutocompleteCommand,
ChatInputCommand,
Expand All @@ -22,7 +22,7 @@ import type {
DetailedDescriptionCommand,
MessageCommand
} from '../types/CommandTypes';
import { BucketScope, CommandPreConditions, RegisterBehavior } from '../types/Enums';
import { RegisterBehavior } from '../types/Enums';
import { acquire, getDefaultBehaviorWhenNotIdentical, handleBulkOverwrite } from '../utils/application-commands/ApplicationCommandRegistries';
import type { ApplicationCommandRegistry } from '../utils/application-commands/ApplicationCommandRegistry';
import { getNeededRegistryParameters } from '../utils/application-commands/getNeededParameters';
Expand Down Expand Up @@ -362,7 +362,7 @@ export class Command<PreParseReturn = Args, O extends Command.Options = Command.
* @param options The command options given from the constructor.
*/
protected parseConstructorPreConditionsNsfw(options: Command.Options) {
if (options.nsfw) this.preconditions.append(CommandPreConditions.NotSafeForWork);
parseConstructorPreConditionsNsfw(options.nsfw, this.preconditions);
}

/**
Expand All @@ -371,32 +371,7 @@ export class Command<PreParseReturn = Args, O extends Command.Options = Command.
* @param options The command options given from the constructor.
*/
protected parseConstructorPreConditionsRunIn(options: Command.Options) {
// Early return if there's no runIn option:
if (isNullish(options.runIn)) return;

if (Command.runInTypeIsSpecificsObject(options.runIn)) {
const messageRunTypes = this.resolveConstructorPreConditionsRunType(options.runIn.messageRun);
const chatInputRunTypes = this.resolveConstructorPreConditionsRunType(options.runIn.chatInputRun);
const contextMenuRunTypes = this.resolveConstructorPreConditionsRunType(options.runIn.contextMenuRun);

if (messageRunTypes !== null || chatInputRunTypes !== null || contextMenuRunTypes !== null) {
this.preconditions.append({
name: CommandPreConditions.RunIn,
context: {
types: {
messageRun: messageRunTypes ?? [],
chatInputRun: chatInputRunTypes ?? [],
contextMenuRun: contextMenuRunTypes ?? []
}
}
});
}
} else {
const types = this.resolveConstructorPreConditionsRunType(options.runIn);
if (types !== null) {
this.preconditions.append({ name: CommandPreConditions.RunIn, context: { types } });
}
}
parseConstructorPreConditionsRunIn(options.runIn, this.resolveConstructorPreConditionsRunType.bind(this), this.preconditions);
}

/**
Expand All @@ -405,10 +380,7 @@ export class Command<PreParseReturn = Args, O extends Command.Options = Command.
* @param options The command options given from the constructor.
*/
protected parseConstructorPreConditionsRequiredClientPermissions(options: Command.Options) {
const permissions = new PermissionsBitField(options.requiredClientPermissions);
if (permissions.bitfield !== 0n) {
this.preconditions.append({ name: CommandPreConditions.ClientPermissions, context: { permissions } });
}
parseConstructorPreConditionsRequiredClientPermissions(options.requiredClientPermissions, this.preconditions);
}

/**
Expand All @@ -417,10 +389,7 @@ export class Command<PreParseReturn = Args, O extends Command.Options = Command.
* @param options The command options given from the constructor.
*/
protected parseConstructorPreConditionsRequiredUserPermissions(options: Command.Options) {
const permissions = new PermissionsBitField(options.requiredUserPermissions);
if (permissions.bitfield !== 0n) {
this.preconditions.append({ name: CommandPreConditions.UserPermissions, context: { permissions } });
}
parseConstructorPreConditionsRequiredUserPermissions(options.requiredUserPermissions, this.preconditions);
}

/**
Expand All @@ -429,23 +398,14 @@ export class Command<PreParseReturn = Args, O extends Command.Options = Command.
* @param options The command options given from the constructor.
*/
protected parseConstructorPreConditionsCooldown(options: Command.Options) {
const { defaultCooldown } = this.container.client.options;

// We will check for whether the command is filtered from the defaults, but we will allow overridden values to
// be set. If an overridden value is passed, it will have priority. Otherwise, it will default to 0 if filtered
// (causing the precondition to not be registered) or the default value with a fallback to a single-use cooldown.
const filtered = defaultCooldown?.filteredCommands?.includes(this.name) ?? false;
const limit = options.cooldownLimit ?? (filtered ? 0 : defaultCooldown?.limit ?? 1);
const delay = options.cooldownDelay ?? (filtered ? 0 : defaultCooldown?.delay ?? 0);

if (limit && delay) {
const scope = options.cooldownScope ?? defaultCooldown?.scope ?? BucketScope.User;
const filteredUsers = options.cooldownFilteredUsers ?? defaultCooldown?.filteredUsers;
this.preconditions.append({
name: CommandPreConditions.Cooldown,
context: { scope, limit, delay, filteredUsers }
});
}
parseConstructorPreConditionsCooldown(
this,
options.cooldownLimit,
options.cooldownDelay,
options.cooldownScope,
options.cooldownFilteredUsers,
this.preconditions
);
}

/**
Expand Down
26 changes: 26 additions & 0 deletions tests/precondition-resolvers/clientPermissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PermissionFlagsBits } from 'discord.js';
import { parseConstructorPreConditionsRequiredClientPermissions } from '../../src/lib/precondition-resolvers/clientPermissions';
import { CommandPreConditions } from '../../src/lib/types/Enums';
import { PreconditionContainerArray } from '../../src/lib/utils/preconditions/PreconditionContainerArray';
import type { PreconditionContainerSingle } from '../../src/lib/utils/preconditions/PreconditionContainerSingle';
import type { PermissionPreconditionContext } from '../../src/preconditions/ClientPermissions';

describe('parseConstructorPreConditionsRequiredClientPermissions', () => {
test('GIVEN valid permissions THEN appends to preconditionContainerArray', () => {
const preconditionContainerArray = new PreconditionContainerArray();
parseConstructorPreConditionsRequiredClientPermissions(PermissionFlagsBits.Administrator, preconditionContainerArray);
expect(preconditionContainerArray.entries.length).toBe(1);
expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).name).toBe(CommandPreConditions.ClientPermissions);
expect(
((preconditionContainerArray.entries[0] as PreconditionContainerSingle).context as PermissionPreconditionContext).permissions?.has(
PermissionFlagsBits.Administrator
)
).toBe(true);
});

test('GIVEN no permissions THEN does not append to preconditionContainerArray', () => {
const preconditionContainerArray = new PreconditionContainerArray();
parseConstructorPreConditionsRequiredClientPermissions(undefined, preconditionContainerArray);
expect(preconditionContainerArray.entries.length).toBe(0);
});
});
76 changes: 76 additions & 0 deletions tests/precondition-resolvers/cooldown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { CooldownOptions } from '../../src/lib/SapphireClient';
import { parseConstructorPreConditionsCooldown } from '../../src/lib/precondition-resolvers/cooldown';
import { BucketScope } from '../../src/lib/types/Enums';
import { PreconditionContainerArray } from '../../src/lib/utils/preconditions/PreconditionContainerArray';
import type { PreconditionContainerSingle } from '../../src/lib/utils/preconditions/PreconditionContainerSingle';

describe('parseConstructorPreConditionsCooldown', () => {
vi.mock('@sapphire/pieces', async () => {
const mod = await vi.importActual<typeof import('@sapphire/pieces')>('@sapphire/pieces');
const { BucketScope } = await import('../../src/lib/types/Enums');

return {
...mod,
container: {
client: {
options: {
defaultCooldown: {
limit: 1,
delay: 2,
scope: BucketScope.User,
filteredCommands: undefined,
filteredUsers: undefined
} as CooldownOptions
}
}
}
};
});

afterAll(() => {
vi.restoreAllMocks();
});

test('when limit and delay are undefined, sets limit to default limit and delay to default delay', () => {
const preconditionContainerArray = new PreconditionContainerArray();

parseConstructorPreConditionsCooldown({ name: 'test' } as any, undefined, undefined, undefined, undefined, preconditionContainerArray);

expect(preconditionContainerArray.entries.length).toBe(1);
expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).name).toBe('Cooldown');
expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).context).toMatchObject({
scope: BucketScope.User,
limit: 1,
delay: 2,
filteredUsers: undefined
});
});

test('when limit and delay are defined, sets limit to passed limit and delay to passed delay', () => {
const preconditionContainerArray = new PreconditionContainerArray();
parseConstructorPreConditionsCooldown({ name: 'test' } as any, 5, 10, undefined, undefined, preconditionContainerArray);

expect(preconditionContainerArray.entries.length).toBe(1);
expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).name).toBe('Cooldown');
expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).context).toMatchObject({
scope: BucketScope.User,
limit: 5,
delay: 10,
filteredUsers: undefined
});
});

test('when scope, filteredUsers, limit, and delay are defined, sets all values to passed values', () => {
const preconditionContainerArray = new PreconditionContainerArray();
parseConstructorPreConditionsCooldown({ name: 'test' } as any, 5, 10, BucketScope.Guild, ['user1', 'user2'], preconditionContainerArray);

expect(preconditionContainerArray.entries.length).toBe(1);
expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).name).toBe('Cooldown');
expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).context).toMatchObject({
scope: BucketScope.Guild,
limit: 5,
delay: 10,
filteredUsers: ['user1', 'user2']
});
});
});
Loading

0 comments on commit d9bbb28

Please sign in to comment.