Skip to content

Commit 7e79e15

Browse files
authored
feat: added auto-preconditions (#199)
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
1 parent f28a0f6 commit 7e79e15

File tree

6 files changed

+134
-7
lines changed

6 files changed

+134
-7
lines changed

src/lib/errors/Identifiers.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const enum Identifiers {
3535
PreconditionCooldown = 'preconditionCooldown',
3636
PreconditionDMOnly = 'preconditionDmOnly',
3737
PreconditionGuildOnly = 'preconditionGuildOnly',
38+
PreconditionNewsOnly = 'preconditionNewsOnly',
3839
PreconditionNSFW = 'preconditionNsfw',
39-
PreconditionPermissions = 'preconditionPermissions'
40+
PreconditionPermissions = 'preconditionPermissions',
41+
PreconditionTextOnly = 'preconditionTextOnly'
4042
}

src/lib/structures/Command.ts

+107-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { AliasPiece, AliasPieceOptions, Awaited, PieceContext } from '@sapphire/pieces';
2+
import { isNullish } from '@sapphire/utilities';
23
import type { Message } from 'discord.js';
34
import * as Lexure from 'lexure';
45
import { Args } from '../parsers/Args';
56
import type { IPreconditionContainer } from '../utils/preconditions/IPreconditionContainer';
6-
import { PreconditionArrayResolvable, PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';
7+
import { PreconditionContainerArray, PreconditionEntryResolvable } from '../utils/preconditions/PreconditionContainerArray';
78
import { FlagStrategyOptions, FlagUnorderedStrategy } from '../utils/strategies/FlagUnorderedStrategy';
89

910
export abstract class Command<T = Args> extends AliasPiece {
@@ -47,7 +48,6 @@ export abstract class Command<T = Args> extends AliasPiece {
4748
super(context, { ...options, name: (name ?? context.name).toLowerCase() });
4849
this.description = options.description ?? '';
4950
this.detailedDescription = options.detailedDescription ?? '';
50-
this.preconditions = new PreconditionContainerArray(options.preconditions);
5151
this.strategy = new FlagUnorderedStrategy(options.strategyOptions);
5252
this.lexer.setQuotes(
5353
options.quotes ?? [
@@ -64,10 +64,12 @@ export abstract class Command<T = Args> extends AliasPiece {
6464

6565
this.aliases = [...this.aliases, ...dashLessAliases];
6666
}
67+
68+
this.preconditions = new PreconditionContainerArray(this.resolveConstructorPreConditions(options));
6769
}
6870

6971
/**
70-
* The pre-parse method. This method can be overriden by plugins to define their own argument parser.
72+
* The pre-parse method. This method can be overridden by plugins to define their own argument parser.
7173
* @param message The message that triggered the command.
7274
* @param parameters The raw parameters as a single string.
7375
* @param context The command-context used in this execution.
@@ -96,6 +98,79 @@ export abstract class Command<T = Args> extends AliasPiece {
9698
strategy: this.strategy
9799
};
98100
}
101+
102+
protected resolveConstructorPreConditions(options: CommandOptions): readonly PreconditionEntryResolvable[] {
103+
const preconditions = options.preconditions?.slice() ?? [];
104+
if (options.nsfw) preconditions.push(CommandPreConditions.NotSafeForWork);
105+
106+
const runIn = this.resolveConstructorPreConditionsRunType(options.runIn);
107+
if (runIn !== null) preconditions.push(runIn);
108+
109+
const cooldownBucket = options.cooldownBucket ?? 1;
110+
if (cooldownBucket && options.cooldownDuration) {
111+
preconditions.push({ name: CommandPreConditions.Cooldown, context: { bucket: cooldownBucket, cooldown: options.cooldownDuration } });
112+
}
113+
114+
return preconditions;
115+
}
116+
117+
private resolveConstructorPreConditionsRunType(runIn: CommandOptions['runIn']): CommandPreConditions[] | null {
118+
if (isNullish(runIn)) return null;
119+
if (typeof runIn === 'string') {
120+
switch (runIn) {
121+
case 'dm':
122+
return [CommandPreConditions.DirectMessageOnly];
123+
case 'text':
124+
return [CommandPreConditions.TextOnly];
125+
case 'news':
126+
return [CommandPreConditions.NewsOnly];
127+
case 'guild':
128+
return [CommandPreConditions.GuildOnly];
129+
default:
130+
return null;
131+
}
132+
}
133+
134+
// If there's no channel it can run on, throw an error:
135+
if (runIn.length === 0) {
136+
throw new Error(`${this.constructor.name}[${this.name}]: "runIn" was specified as an empty array.`);
137+
}
138+
139+
const dm = runIn.includes('dm');
140+
const text = runIn.includes('text');
141+
const news = runIn.includes('news');
142+
const guild = text && news;
143+
144+
// If runs everywhere, optimise to null:
145+
if (dm && guild) return null;
146+
147+
const array: CommandPreConditions[] = [];
148+
if (dm) array.push(CommandPreConditions.DirectMessageOnly);
149+
if (guild) array.push(CommandPreConditions.GuildOnly);
150+
else if (text) array.push(CommandPreConditions.TextOnly);
151+
else if (news) array.push(CommandPreConditions.NewsOnly);
152+
153+
return array;
154+
}
155+
}
156+
157+
/**
158+
* The allowed values for [[CommandOptions.runIn]].
159+
* @since 2.0.0
160+
*/
161+
export type CommandOptionsRunType = 'dm' | 'text' | 'news' | 'guild';
162+
163+
/**
164+
* The available command pre-conditions.
165+
* @since 2.0.0
166+
*/
167+
export const enum CommandPreConditions {
168+
Cooldown = 'Cooldown',
169+
NotSafeForWork = 'NSFW',
170+
DirectMessageOnly = 'DMOnly',
171+
TextOnly = 'TextOnly',
172+
NewsOnly = 'NewsOnly',
173+
GuildOnly = 'GuildOnly'
99174
}
100175

101176
/**
@@ -130,7 +205,7 @@ export interface CommandOptions extends AliasPieceOptions {
130205
* @since 1.0.0
131206
* @default []
132207
*/
133-
preconditions?: PreconditionArrayResolvable;
208+
preconditions?: readonly PreconditionEntryResolvable[];
134209

135210
/**
136211
* The options for the lexer strategy.
@@ -150,6 +225,34 @@ export interface CommandOptions extends AliasPieceOptions {
150225
* ]
151226
*/
152227
quotes?: [string, string][];
228+
229+
/**
230+
* Sets whether or not the command should be treated as NSFW. If set to true, the `NSFW` precondition will be added to the list.
231+
* @since 2.0.0
232+
* @default false
233+
*/
234+
nsfw?: boolean;
235+
236+
/**
237+
* 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.
238+
* @since 2.0.0
239+
* @default 1
240+
*/
241+
cooldownBucket?: number;
242+
243+
/**
244+
* 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.
245+
* @since 2.0.0
246+
* @default 0
247+
*/
248+
cooldownDuration?: number;
249+
250+
/**
251+
* 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).
252+
* @since 2.0.0
253+
* @default null
254+
*/
255+
runIn?: CommandOptionsRunType | readonly CommandOptionsRunType[] | null;
153256
}
154257

155258
export interface CommandContext extends Record<PropertyKey, unknown> {

src/lib/structures/ExtendedArgument.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export abstract class ExtendedArgument<K extends keyof ArgType, T> extends Argum
6666
}
6767

6868
public async run(parameter: string, context: ArgumentContext<T>): AsyncArgumentResult<T> {
69-
const result = await this.base.run(parameter, (context as unknown) as ArgumentContext<ArgType[K]>);
69+
const result = await this.base.run(parameter, context as unknown as ArgumentContext<ArgType[K]>);
7070
// If the result was successful (i.e. is of type `Ok<ArgType[K]>`), pass its
7171
// value to [[ExtendedArgument#handle]] for further parsing. Otherwise, return
7272
// the error as is; it'll provide contextual information from the base argument.

src/lib/structures/StoreRegistry.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export class StoreRegistry extends Collection<Key, Value> {
6666
* @param store The store to register.
6767
*/
6868
public register<T extends Piece>(store: Store<T>): this {
69-
this.set(store.name as Key, (store as unknown) as Value);
69+
this.set(store.name as Key, store as unknown as Value);
7070
return this;
7171
}
7272

src/preconditions/NewsOnly.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Message } from 'discord.js';
2+
import { Identifiers } from '../lib/errors/Identifiers';
3+
import { Precondition, PreconditionResult } from '../lib/structures/Precondition';
4+
5+
export class CorePrecondition extends Precondition {
6+
public run(message: Message): PreconditionResult {
7+
return message.channel.type === 'news'
8+
? this.error({ identifier: Identifiers.PreconditionNewsOnly, message: 'You can only run this command in news channels.' })
9+
: this.ok();
10+
}
11+
}

src/preconditions/TextOnly.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Message } from 'discord.js';
2+
import { Identifiers } from '../lib/errors/Identifiers';
3+
import { Precondition, PreconditionResult } from '../lib/structures/Precondition';
4+
5+
export class CorePrecondition extends Precondition {
6+
public run(message: Message): PreconditionResult {
7+
return message.channel.type === 'text'
8+
? this.error({ identifier: Identifiers.PreconditionTextOnly, message: 'You can only run this command in text channels.' })
9+
: this.ok();
10+
}
11+
}

0 commit comments

Comments
 (0)