Skip to content

Commit

Permalink
feat: added auto-preconditions (#199)
Browse files Browse the repository at this point in the history
feat(command): added `CommandOptions.nsfw`
feat(command): added `CommandOptions.cooldownBucket`
feat(command): added `CommandOptions.cooldownDuration`
feat(command): added `CommandOptions.runIn`
feat(identifiers): added `Identifiers.PreconditionNewsOnly`
feat(identifiers): added `Identifiers.PreconditionTextOnly`
BREAKING CHANGE: Changed `CommandOptions.preconditions` to always require an array
  • Loading branch information
kyranet committed May 16, 2021
1 parent f28a0f6 commit 7e79e15
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 7 deletions.
4 changes: 3 additions & 1 deletion src/lib/errors/Identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const enum Identifiers {
PreconditionCooldown = 'preconditionCooldown',
PreconditionDMOnly = 'preconditionDmOnly',
PreconditionGuildOnly = 'preconditionGuildOnly',
PreconditionNewsOnly = 'preconditionNewsOnly',
PreconditionNSFW = 'preconditionNsfw',
PreconditionPermissions = 'preconditionPermissions'
PreconditionPermissions = 'preconditionPermissions',
PreconditionTextOnly = 'preconditionTextOnly'
}
111 changes: 107 additions & 4 deletions src/lib/structures/Command.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { AliasPiece, AliasPieceOptions, Awaited, PieceContext } from '@sapphire/pieces';
import { isNullish } from '@sapphire/utilities';
import type { Message } from 'discord.js';
import * as Lexure from 'lexure';
import { Args } from '../parsers/Args';
import type { IPreconditionContainer } from '../utils/preconditions/IPreconditionContainer';
import { PreconditionArrayResolvable, PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';
import { PreconditionContainerArray, PreconditionEntryResolvable } from '../utils/preconditions/PreconditionContainerArray';
import { FlagStrategyOptions, FlagUnorderedStrategy } from '../utils/strategies/FlagUnorderedStrategy';

export abstract class Command<T = Args> extends AliasPiece {
Expand Down Expand Up @@ -47,7 +48,6 @@ export abstract class Command<T = Args> extends AliasPiece {
super(context, { ...options, name: (name ?? context.name).toLowerCase() });
this.description = options.description ?? '';
this.detailedDescription = options.detailedDescription ?? '';
this.preconditions = new PreconditionContainerArray(options.preconditions);
this.strategy = new FlagUnorderedStrategy(options.strategyOptions);
this.lexer.setQuotes(
options.quotes ?? [
Expand All @@ -64,10 +64,12 @@ export abstract class Command<T = Args> extends AliasPiece {

this.aliases = [...this.aliases, ...dashLessAliases];
}

this.preconditions = new PreconditionContainerArray(this.resolveConstructorPreConditions(options));
}

/**
* The pre-parse method. This method can be overriden by plugins to define their own argument parser.
* The pre-parse method. This method can be overridden by plugins to define their own argument parser.
* @param message The message that triggered the command.
* @param parameters The raw parameters as a single string.
* @param context The command-context used in this execution.
Expand Down Expand Up @@ -96,6 +98,79 @@ export abstract class Command<T = Args> extends AliasPiece {
strategy: this.strategy
};
}

protected resolveConstructorPreConditions(options: CommandOptions): readonly PreconditionEntryResolvable[] {
const preconditions = options.preconditions?.slice() ?? [];
if (options.nsfw) preconditions.push(CommandPreConditions.NotSafeForWork);

const runIn = this.resolveConstructorPreConditionsRunType(options.runIn);
if (runIn !== null) preconditions.push(runIn);

const cooldownBucket = options.cooldownBucket ?? 1;
if (cooldownBucket && options.cooldownDuration) {
preconditions.push({ name: CommandPreConditions.Cooldown, context: { bucket: cooldownBucket, cooldown: options.cooldownDuration } });
}

return preconditions;
}

private resolveConstructorPreConditionsRunType(runIn: CommandOptions['runIn']): CommandPreConditions[] | null {
if (isNullish(runIn)) return null;
if (typeof runIn === 'string') {
switch (runIn) {
case 'dm':
return [CommandPreConditions.DirectMessageOnly];
case 'text':
return [CommandPreConditions.TextOnly];
case 'news':
return [CommandPreConditions.NewsOnly];
case 'guild':
return [CommandPreConditions.GuildOnly];
default:
return null;
}
}

// If there's no channel it can run on, throw an error:
if (runIn.length === 0) {
throw new Error(`${this.constructor.name}[${this.name}]: "runIn" was specified as an empty array.`);
}

const dm = runIn.includes('dm');
const text = runIn.includes('text');
const news = runIn.includes('news');
const guild = text && news;

// If runs everywhere, optimise to null:
if (dm && guild) return null;

const array: CommandPreConditions[] = [];
if (dm) array.push(CommandPreConditions.DirectMessageOnly);
if (guild) array.push(CommandPreConditions.GuildOnly);
else if (text) array.push(CommandPreConditions.TextOnly);
else if (news) array.push(CommandPreConditions.NewsOnly);

return array;
}
}

/**
* The allowed values for [[CommandOptions.runIn]].
* @since 2.0.0
*/
export type CommandOptionsRunType = 'dm' | 'text' | 'news' | 'guild';

/**
* The available command pre-conditions.
* @since 2.0.0
*/
export const enum CommandPreConditions {
Cooldown = 'Cooldown',
NotSafeForWork = 'NSFW',
DirectMessageOnly = 'DMOnly',
TextOnly = 'TextOnly',
NewsOnly = 'NewsOnly',
GuildOnly = 'GuildOnly'
}

/**
Expand Down Expand Up @@ -130,7 +205,7 @@ export interface CommandOptions extends AliasPieceOptions {
* @since 1.0.0
* @default []
*/
preconditions?: PreconditionArrayResolvable;
preconditions?: readonly PreconditionEntryResolvable[];

/**
* The options for the lexer strategy.
Expand All @@ -150,6 +225,34 @@ export interface CommandOptions extends AliasPieceOptions {
* ]
*/
quotes?: [string, string][];

/**
* Sets whether or not the command should be treated as NSFW. If set to true, the `NSFW` precondition will be added to the list.
* @since 2.0.0
* @default false
*/
nsfw?: boolean;

/**
* Sets the bucket of the cool-down, if set to a non-zero value alongside {@link CommandOptions.cooldownDuration}, the `Cooldown` precondition will be added to the list.
* @since 2.0.0
* @default 1
*/
cooldownBucket?: number;

/**
* Sets the duration of the tickets in the cool-down, if set to a non-zero value alongside {@link CommandOptions.cooldownBucket}, the `Cooldown` precondition will be added to the list.
* @since 2.0.0
* @default 0
*/
cooldownDuration?: number;

/**
* 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
* @default null
*/
runIn?: CommandOptionsRunType | readonly CommandOptionsRunType[] | null;
}

export interface CommandContext extends Record<PropertyKey, unknown> {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/structures/ExtendedArgument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export abstract class ExtendedArgument<K extends keyof ArgType, T> extends Argum
}

public async run(parameter: string, context: ArgumentContext<T>): AsyncArgumentResult<T> {
const result = await this.base.run(parameter, (context as unknown) as ArgumentContext<ArgType[K]>);
const result = await this.base.run(parameter, context as unknown as ArgumentContext<ArgType[K]>);
// If the result was successful (i.e. is of type `Ok<ArgType[K]>`), pass its
// value to [[ExtendedArgument#handle]] for further parsing. Otherwise, return
// the error as is; it'll provide contextual information from the base argument.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/structures/StoreRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class StoreRegistry extends Collection<Key, Value> {
* @param store The store to register.
*/
public register<T extends Piece>(store: Store<T>): this {
this.set(store.name as Key, (store as unknown) as Value);
this.set(store.name as Key, store as unknown as Value);
return this;
}

Expand Down
11 changes: 11 additions & 0 deletions src/preconditions/NewsOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Message } from 'discord.js';
import { Identifiers } from '../lib/errors/Identifiers';
import { Precondition, PreconditionResult } from '../lib/structures/Precondition';

export class CorePrecondition extends Precondition {
public run(message: Message): PreconditionResult {
return message.channel.type === 'news'
? this.error({ identifier: Identifiers.PreconditionNewsOnly, message: 'You can only run this command in news channels.' })
: this.ok();
}
}
11 changes: 11 additions & 0 deletions src/preconditions/TextOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Message } from 'discord.js';
import { Identifiers } from '../lib/errors/Identifiers';
import { Precondition, PreconditionResult } from '../lib/structures/Precondition';

export class CorePrecondition extends Precondition {
public run(message: Message): PreconditionResult {
return message.channel.type === 'text'
? this.error({ identifier: Identifiers.PreconditionTextOnly, message: 'You can only run this command in text channels.' })
: this.ok();
}
}

0 comments on commit 7e79e15

Please sign in to comment.