Skip to content

Commit 78b98d5

Browse files
Lioness100vladfrangufavna
authored
feat: precondition guides (#91)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com> Co-authored-by: Jeroen Claassens <jeroen.claassens@live.nl> Co-authored-by: Jeroen Claassens <support@favware.tech>
1 parent af751ae commit 78b98d5

File tree

8 files changed

+474
-11
lines changed

8 files changed

+474
-11
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"**/projects/": true,
1818
},
1919
"cSpell.words": [
20-
"favna"
20+
"favna",
21+
"nsfw"
2122
]
2223
}
Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
11
---
2-
sidebar_position: 3
2+
sidebar_position: 4
33
title: Setting the types of channel a command can run in
44
---
55

6-
## TODO
6+
The [`runIn`][runin] command option can be used to specify the types of channels a command can run in. This can be
7+
useful if you're developing a command that, for example, displays the roles of a user. In that scenario, you'll want to
8+
make sure that the command can only be run in guild channels.
9+
10+
:::info
11+
12+
You can view the valid `runIn` values [here][runintypes].
13+
14+
:::
15+
16+
```typescript ts2esm2cjs|{7}|{7}
17+
import { Command, CommandOptionsRunTypeEnum } from '@sapphire/framework';
18+
19+
export class PingCommand extends Command {
20+
public constructor(context: Command.Context) {
21+
super(context, {
22+
// ...
23+
runIn: CommandOptionsRunTypeEnum.GuildAny // Only run in guild channels
24+
});
25+
}
26+
}
27+
```
28+
29+
If you try to run a command in direct messages, you'll now find that nothing happens.
30+
31+
:::tip
32+
33+
To learn how to send a message to the command executor when a precondition fails, see [Reporting Precondition
34+
Failure][reporting-precondition-failure].
35+
36+
:::
37+
38+
[runin]: ../../Documentation/api-framework/interfaces/CommandOptions#runin
39+
[runintypes]: ../../Documentation/api-framework/enums/CommandOptionsRunTypeEnum
40+
[reporting-precondition-failure]: ./reporting-precondition-failure
Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,120 @@
11
---
2-
sidebar_position: 0
2+
sidebar_position: 3
33
title: Configuring command cooldowns
44
---
55

6-
## TODO
6+
Cooldowns are of vital importance for many bots to avoid excessive command usage, API ratelimits, and so on. Luckily,
7+
Sapphire makes it easy to integrate them into your commands! At its simplest level, cooldowns can be used in specific
8+
commands via the [`cooldownDelay`][cooldowndelay] property in the command's options. This value is amount of
9+
milliseconds that a user will have to wait after using a command to use it again. Here's a basic example:
10+
11+
```typescript ts2esm2cjs|{7}|{7}
12+
import { Command } from '@sapphire/framework';
13+
14+
export class PingCommand extends Command {
15+
public constructor(context: Command.Context) {
16+
super(context, {
17+
// ...
18+
cooldownDelay: 10_000 // 10_000 milliseconds (10 seconds)
19+
});
20+
}
21+
}
22+
```
23+
24+
If you now try to run this command, and then run it again within 10_000 milliseconds (10 seconds), the command won't
25+
execute, and an error will be thrown. You can learn how to process that error [here][reporting-precondition-failure].
26+
27+
:::info
28+
29+
`cooldownDelay` only accepts a value of milliseconds, which is not known to be the easiest to read or calculate. To
30+
help, you can use [@sapphire/time-utilities][timeutils], which includes utilities for time transformers.
31+
32+
```typescript ts2esm2cjs|{8}|{8}
33+
import { Command } from '@sapphire/framework';
34+
import { Time } from '@sapphire/time-utilities';
35+
36+
export class PingCommand extends Command {
37+
public constructor(context: Command.Context) {
38+
super(context, {
39+
// ...
40+
cooldownDelay: Time.Second * 10 // Much easier for humans to read
41+
});
42+
}
43+
}
44+
```
45+
46+
You can view the docs [here][timeenum].
47+
48+
:::
49+
50+
## User Exceptions
51+
52+
It's very common to not want cooldowns to apply to certain people, for example, the bot owner. This can be achieved by
53+
adding [`cooldownFilteredUsers`][cooldownfilteredusers] to the options. This option should be an array of users ID that
54+
the bot can ignore when calculating cooldowns.
55+
56+
```typescript ts2esm2cjs|{8}|{8}
57+
import { Command } from '@sapphire/framework';
58+
59+
export class PingCommand extends Command {
60+
public constructor(context: Command.Context) {
61+
super(context, {
62+
// ...
63+
cooldownDelay: 10_000 // 10_000 milliseconds (10 seconds)
64+
cooldownFilteredUsers: ['YOUR_ID'] // Ignore the bot owner
65+
});
66+
}
67+
}
68+
```
69+
70+
## Advanced Usage
71+
72+
Accompanying `cooldownDelay`, you also have access to the options [`cooldownLimit`][cooldownlimit] and
73+
[`cooldownScope`][cooldownscope].
74+
75+
`cooldownLimit` will define how many times a command can be used before a cooldown is put into affect. This value is set
76+
to `1` by default. For example, a `cooldownDelay` of `10_000` milliseconds and a `cooldownLimit` of 2 will effectively
77+
mean that you'd be able to use the command twice every 10 seconds.
78+
79+
Another useful option is `cooldownScope`, which will define the scope of the cooldown. This is useful if you want to
80+
have a cooldown that applies per guild, for example, instead of per user. Valid scopes can be found [here][scopes].
81+
82+
## Client-wide Cooldowns
83+
84+
Sometimes you'll find a use case where you want specific cooldown options to apply to all commands in your client. This
85+
can be achieved by adding [`defaultCooldown`][defaultcooldown] to your [`SapphireClient`][sapphire] options. You can use
86+
any of the properties shown above with this option.
87+
88+
```typescript ts2esm2cjs|{5-10}|{5-10}
89+
import { SapphireClient, BucketScope } from '@sapphire/framework';
90+
91+
const client = new SapphireClient({
92+
intents: ['GUILDS', 'GUILD_MESSAGES'],
93+
defaultCooldown: {
94+
cooldownDelay: 10_000, // 10_000 milliseconds
95+
cooldownFilteredUsers: ['YOUR_ID'], // Ignore the bot owner
96+
cooldownLimit: 2, // Allow 2 uses before ratelimiting
97+
cooldownScope: BucketScope.Channel // Scope cooldown to channel
98+
}
99+
});
100+
101+
void client.login('your-token-goes-here');
102+
```
103+
104+
:::tip
105+
106+
To learn how to send a message to the command executor when a precondition fails, see [Reporting Precondition
107+
Failure][reporting-precondition-failure].
108+
109+
:::
110+
111+
[cooldowndelay]: ../../Documentation/api-framework/interfaces/CommandOptions#cooldowndelay
112+
[cooldownfilteredusers]: ../../Documentation/api-framework/interfaces/CommandOptions#cooldownfilteredusers
113+
[cooldownlimit]: ../../Documentation/api-framework/interfaces/CommandOptions#cooldownlimit
114+
[cooldownscope]: ../../Documentation/api-framework/interfaces/CommandOptions#cooldownscope
115+
[defaultcooldown]: ../../Documentation/api-framework/interfaces/SapphireClientOptions#defaultcooldown
116+
[sapphire]: ../../Documentation/api-framework/classes/SapphireClient
117+
[scopes]: ../../Documentation/api-framework/enums/BucketScope
118+
[reporting-precondition-failure]: ./reporting-precondition-failure
119+
[timeutils]: ../../Documentation/api-utilities/modules/sapphire_time_utilities
120+
[timeenum]: ../../Documentation/api-utilities/enums/sapphire_time_utilities.Time
Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,124 @@
11
---
2-
sidebar_position: 4
2+
sidebar_position: 1
33
title: Creating your own preconditions
44
---
55

6-
## TODO
6+
Just as we did in both [Creating Commands][creating-commands] and [Creating Listeners][creating-listeners], we will
7+
start by creating a `preconditions` subdirectory in your project's entry point directory. For this guide, we'll be
8+
building out an `OwnerOnly` precondition, that prevents anyone but the application owner from running the command.
9+
10+
Your directory should now look something like this:
11+
12+
```bash {9-10}
13+
├── node_modules
14+
├── package.json
15+
└── src
16+
├── commands
17+
│ └── ping.js
18+
├── index.js
19+
├── listeners
20+
│ └── ready.js
21+
└── preconditions
22+
└── OwnerOnly.js
23+
```
24+
25+
The purpose of our `OwnerOnly` precondition is just as the name suggests: to check if the user is the bot owner. It can
26+
be used for developer commands, such as commands that evaluate expressions or present internal debugging information.
27+
28+
## Creating a Precondition class
29+
30+
Preconditions are made by extending the Sapphire [`Precondition`][precondition] class and exporting it.
31+
32+
```typescript ts2esm2cjs
33+
import { Precondition } from '@sapphire/framework';
34+
35+
export class OwnerOnlyPrecondition extends Precondition {}
36+
```
37+
38+
Next, we can create a [`run`][preconditionrun] function to execute our logic. This function should either return
39+
[`this.ok()`][preconditionok] to signify the condition has passed, or [`this.error(...)`][preconditionerror] to signify
40+
the command should be denied.
41+
42+
```typescript ts2esm2cjs|{4-8}|{4-8}
43+
import { Precondition, Message } from '@sapphire/framework';
44+
45+
export class OwnerOnlyPrecondition extends Precondition {
46+
public run(message: Message) {
47+
return message.author.id === 'YOUR_ID'
48+
? this.ok()
49+
: this.error({ message: 'Only the bot owner can use this command!' });
50+
}
51+
}
52+
```
53+
54+
## Using Preconditions in Commands
55+
56+
To attach a precondition to a command, you simply have to input its name in an array in the command's
57+
[`preconditions`][preconditions-option] option.
58+
59+
```typescript ts2esm2cjs|{7}|{7}
60+
import { Command } from '@sapphire/framework';
61+
62+
export class PingCommand extends Command {
63+
public constructor(context: Command.Context) {
64+
super(context, {
65+
// ...
66+
preconditions: ['OwnerOnly']
67+
});
68+
}
69+
}
70+
```
71+
72+
Now, if someone who is not the bot owner executes the `ping` command, nothing will happen!
73+
74+
:::caution
75+
76+
For TypeScript users, there's an extra step to make this work. To increase the security of Sapphire's types, you'll need
77+
to augment Sapphire's [`Preconditions`][preconditions-interface] interface. Please see an official example
78+
[here][preconditions-augment].
79+
80+
<!--
81+
TODO: Once we have a dedicated TypeScript page this should link to that instead of a code example.
82+
Including why augmenting with `never` works and the difference between that and augmenting with an object.
83+
-->
84+
85+
:::
86+
87+
By default, no error message will be sent or logged when a command is denied because of a precondition. To learn how to
88+
configure this, please read [Reporting Precondition Failures][reporting-precondition-failure].
89+
90+
## Advanced Usage
91+
92+
Sapphire also has a builtin system for advanced conditional precondition logic through nested arrays. By default, all
93+
preconditions in the given array must pass for the command to be run. However, you can use nested arrays to create `OR`
94+
functionality. This could be useful if you'd like a command to be run if the user is either a moderator _or_ an admin.
95+
96+
Furthermore, if you create a nested array within a nested array, you'll receive `AND` functionality once more. Arrays
97+
can be nested infinitely with the same pattern for optimal control over your preconditions.
98+
99+
Consider the following array of preconditions:
100+
101+
:::warning
102+
103+
None of the following preconditions are bundled with Sapphire; as such you'd have to create them yourself!
104+
105+
:::
106+
107+
```js
108+
[['AdminOnly', ['ModOnly', 'OwnerOnly']], 'InVoiceChannel'];
109+
```
110+
111+
For a command with these preconditions to pass the denial checks, the `InVoiceChannel` precondition must pass, as well
112+
as `AdminOnly` _or_ both `OwnerOnly` and `ModOnly`.
113+
114+
[creating-commands]: ../getting-started/creating-a-basic-command
115+
[creating-listeners]: ../listeners/creating-your-own-listeners
116+
[precondition]: ../../Documentation/api-framework/classes/Precondition
117+
[preconditionrun]: ../../Documentation/api-framework/classes/Precondition#run
118+
[preconditionok]: ../../Documentation/api-framework/classes/Precondition#ok
119+
[preconditionerror]: ../../Documentation/api-framework/classes/Precondition#error
120+
[preconditions-option]: ../../Documentation/api-framework/interfaces/CommandOptions#preconditions
121+
[preconditions-interface]: ../../Documentation/api-framework/interfaces/Preconditions
122+
[preconditions-augment]:
123+
https://github.com/sapphiredev/examples/blob/main/examples/with-typescript-recommended/src/preconditions/OwnerOnly.ts#L13-L17
124+
[reporting-precondition-failure]: ./reporting-precondition-failure
Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,61 @@
11
---
2-
sidebar_position: 2
2+
sidebar_position: 5
33
title: Handling permissions
44
---
55

6-
## TODO
6+
One of the most basic needs of a Discord bot is to be able to deny command access to users based on their permissions or
7+
lack thereof. A common example would be moderation commands. Most people wouldn't want regular users to be able to ban
8+
people, so we can restrict usage using the [`requiredUserPermissions`][requireduserpermissions] option.
9+
10+
<!-- TODO: info block redirecting to subcommands ts guide for using corresponding decorators -->
11+
12+
```typescript ts2esm2cjs|{7}|{7}
13+
import { Command } from '@sapphire/framework';
14+
15+
export class BanCommand extends Command {
16+
public constructor(context: Command.Context) {
17+
super(context, {
18+
// ...
19+
requiredUserPermissions: ['BAN_MEMBERS']
20+
});
21+
}
22+
}
23+
```
24+
25+
Users without the `BAN_MEMBERS` permission will now be unable to use the command!
26+
27+
## Handling Client Permissions
28+
29+
It's also a good idea to verify the inverse: does the _bot_ have the `BAN_MEMBERS` permission? We can use the
30+
[`requiredClientPermissions`][requiredclientpermissions] option the same way to prevent the command from being used if
31+
not.
32+
33+
<!-- TODO: info block redirecting to subcommands ts guide for using corresponding decorators -->
34+
35+
```typescript ts2esm2cjs|{8}|{8}
36+
import { Command } from '@sapphire/framework';
37+
38+
export class BanCommand extends Command {
39+
public constructor(context: Command.Context) {
40+
super(context, {
41+
// ...
42+
requiredUserPermissions: ['BAN_MEMBERS'],
43+
requiredClientPermissions: ['BAN_MEMBERS']
44+
});
45+
}
46+
}
47+
```
48+
49+
With these changes, `BanCommand` now requires the command executor _and_ the client to have the `BAN_MEMBERS` permission
50+
to execute!
51+
52+
:::tip
53+
54+
To learn how to send a message to the command executor when a precondition fails, see [Reporting Precondition
55+
Failure][reporting-precondition-failure].
56+
57+
:::
58+
59+
[requireduserpermissions]: ../../Documentation/api-framework/interfaces/CommandOptions#requireduserpermissions
60+
[requiredclientpermissions]: ../../Documentation/api-framework/interfaces/CommandOptions#requiredclientpermissions
61+
[reporting-precondition-failure]: ./reporting-precondition-failure
Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
---
2-
sidebar_position: 1
2+
sidebar_position: 6
33
title: Locking commands to NSFW channels
44
---
55

6-
## TODO
6+
Sometimes it can be necessary to lock certain commands to NSFW (not safe for work) channels. This can be simply achieved
7+
by setting the `nsfw` option in the command.
8+
9+
```typescript ts2esm2cjs|{7}|{7}
10+
import { Command } from '@sapphire/framework';
11+
12+
export class NSFWCommand extends Command {
13+
public constructor(context: Command.Context) {
14+
super(context, {
15+
// ...
16+
nsfw: true
17+
});
18+
}
19+
}
20+
```
21+
22+
:::tip
23+
24+
To learn how to send a message to the command executor when a precondition fails, see [Reporting Precondition
25+
Failure][reporting-precondition-failure].
26+
27+
:::
28+
29+
[reporting-precondition-failure]: ./reporting-precondition-failure

0 commit comments

Comments
 (0)