Skip to content

feat: precondition guides #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Dec 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
41444dd
feat: precondition guides
Lioness100 Nov 14, 2021
c19f2aa
fix line highlighting
Lioness100 Nov 14, 2021
7809b3a
fix links
Lioness100 Nov 14, 2021
3e801b1
refactors and fixes
Lioness100 Nov 15, 2021
1185820
style: formatting
favna Nov 20, 2021
b6d0889
implement favna's fixes
Lioness100 Nov 25, 2021
60bee92
links
Lioness100 Nov 27, 2021
a6bfe36
fixes
Lioness100 Nov 28, 2021
cfc0f4b
test
Lioness100 Nov 28, 2021
33f5512
prefer enums
Lioness100 Nov 28, 2021
02e7df0
add "what are pre" page
Lioness100 Nov 28, 2021
b2ece16
feat: precondition guides
Lioness100 Nov 14, 2021
dbba8dc
fix line highlighting
Lioness100 Nov 14, 2021
897af5b
fix links
Lioness100 Nov 14, 2021
2b31056
refactors and fixes
Lioness100 Nov 15, 2021
40a75b8
style: formatting
favna Nov 20, 2021
ba0428c
implement favna's fixes
Lioness100 Nov 25, 2021
7409dec
fixes
Lioness100 Nov 28, 2021
659cf9a
prefer enums
Lioness100 Nov 28, 2021
bf980ac
add "what are pre" page
Lioness100 Nov 28, 2021
f64466e
chore: insert commit message here
favna Nov 28, 2021
4939401
Update docs/Guide/preconditions/creating-your-own-preconditions.mdx
favna Nov 28, 2021
c4fc58f
Update docs/Guide/preconditions/handling-permissions.mdx
favna Nov 28, 2021
bce7094
chore: should work i guess...
favna Nov 28, 2021
c917c1a
Convert tabs to plugin
Lioness100 Dec 6, 2021
728a386
Merge branch 'feat/preconditionGuides' of https://github.com/Lioness1…
Lioness100 Dec 6, 2021
87cd8d7
Merge branch 'feat/preconditionGuides' of https://github.com/Lioness1…
Lioness100 Dec 6, 2021
04f1646
Merge branch 'feat/preconditionGuides' of https://github.com/Lioness1…
Lioness100 Dec 6, 2021
b3c131c
conflicts
Lioness100 Dec 6, 2021
814c6b0
vladdy fixes
Lioness100 Dec 6, 2021
997b983
Apply suggestions from favna
Lioness100 Dec 7, 2021
b294935
convert from tabs & prettify
Lioness100 Dec 7, 2021
527fed0
Merge branch 'feat/preconditionGuides' of https://github.com/Lioness1…
Lioness100 Dec 7, 2021
4e67b82
chore: formatting
favna Dec 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"**/projects/": true,
},
"cSpell.words": [
"favna"
"favna",
"nsfw"
]
}
38 changes: 36 additions & 2 deletions docs/Guide/preconditions/channel-types.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
---
sidebar_position: 3
sidebar_position: 4
title: Setting the types of channel a command can run in
---

## TODO
The [`runIn`][runin] command option can be used to specify the types of channels a command can run in. This can be
useful if you're developing a command that, for example, displays the roles of a user. In that scenario, you'll want to
make sure that the command can only be run in guild channels.

:::info

You can view the valid `runIn` values [here][runintypes].

:::

```typescript ts2esm2cjs|{7}|{7}
import { Command, CommandOptionsRunTypeEnum } from '@sapphire/framework';

export class PingCommand extends Command {
public constructor(context: Command.Context) {
super(context, {
// ...
runIn: CommandOptionsRunTypeEnum.GuildAny // Only run in guild channels
});
}
}
```

If you try to run a command in direct messages, you'll now find that nothing happens.

:::tip

To learn how to send a message to the command executor when a precondition fails, see [Reporting Precondition
Failure][reporting-precondition-failure].

:::

[runin]: ../../Documentation/api-framework/interfaces/CommandOptions#runin
[runintypes]: ../../Documentation/api-framework/enums/CommandOptionsRunTypeEnum
[reporting-precondition-failure]: ./reporting-precondition-failure
118 changes: 116 additions & 2 deletions docs/Guide/preconditions/command-cooldown.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,120 @@
---
sidebar_position: 0
sidebar_position: 3
title: Configuring command cooldowns
---

## TODO
Cooldowns are of vital importance for many bots to avoid excessive command usage, API ratelimits, and so on. Luckily,
Sapphire makes it easy to integrate them into your commands! At its simplest level, cooldowns can be used in specific
commands via the [`cooldownDelay`][cooldowndelay] property in the command's options. This value is amount of
milliseconds that a user will have to wait after using a command to use it again. Here's a basic example:

```typescript ts2esm2cjs|{7}|{7}
import { Command } from '@sapphire/framework';

export class PingCommand extends Command {
public constructor(context: Command.Context) {
super(context, {
// ...
cooldownDelay: 10_000 // 10_000 milliseconds (10 seconds)
});
}
}
```

If you now try to run this command, and then run it again within 10_000 milliseconds (10 seconds), the command won't
execute, and an error will be thrown. You can learn how to process that error [here][reporting-precondition-failure].

:::info

`cooldownDelay` only accepts a value of milliseconds, which is not known to be the easiest to read or calculate. To
help, you can use [@sapphire/time-utilities][timeutils], which includes utilities for time transformers.

```typescript ts2esm2cjs|{8}|{8}
import { Command } from '@sapphire/framework';
import { Time } from '@sapphire/time-utilities';

export class PingCommand extends Command {
public constructor(context: Command.Context) {
super(context, {
// ...
cooldownDelay: Time.Second * 10 // Much easier for humans to read
});
}
}
```

You can view the docs [here][timeenum].

:::

## User Exceptions

It's very common to not want cooldowns to apply to certain people, for example, the bot owner. This can be achieved by
adding [`cooldownFilteredUsers`][cooldownfilteredusers] to the options. This option should be an array of users ID that
the bot can ignore when calculating cooldowns.

```typescript ts2esm2cjs|{8}|{8}
import { Command } from '@sapphire/framework';

export class PingCommand extends Command {
public constructor(context: Command.Context) {
super(context, {
// ...
cooldownDelay: 10_000 // 10_000 milliseconds (10 seconds)
cooldownFilteredUsers: ['YOUR_ID'] // Ignore the bot owner
});
}
}
```

## Advanced Usage

Accompanying `cooldownDelay`, you also have access to the options [`cooldownLimit`][cooldownlimit] and
[`cooldownScope`][cooldownscope].

`cooldownLimit` will define how many times a command can be used before a cooldown is put into affect. This value is set
to `1` by default. For example, a `cooldownDelay` of `10_000` milliseconds and a `cooldownLimit` of 2 will effectively
mean that you'd be able to use the command twice every 10 seconds.

Another useful option is `cooldownScope`, which will define the scope of the cooldown. This is useful if you want to
have a cooldown that applies per guild, for example, instead of per user. Valid scopes can be found [here][scopes].

## Client-wide Cooldowns

Sometimes you'll find a use case where you want specific cooldown options to apply to all commands in your client. This
can be achieved by adding [`defaultCooldown`][defaultcooldown] to your [`SapphireClient`][sapphire] options. You can use
any of the properties shown above with this option.

```typescript ts2esm2cjs|{5-10}|{5-10}
import { SapphireClient, BucketScope } from '@sapphire/framework';

const client = new SapphireClient({
intents: ['GUILDS', 'GUILD_MESSAGES'],
defaultCooldown: {
cooldownDelay: 10_000, // 10_000 milliseconds
cooldownFilteredUsers: ['YOUR_ID'], // Ignore the bot owner
cooldownLimit: 2, // Allow 2 uses before ratelimiting
cooldownScope: BucketScope.Channel // Scope cooldown to channel
}
});

void client.login('your-token-goes-here');
```

:::tip

To learn how to send a message to the command executor when a precondition fails, see [Reporting Precondition
Failure][reporting-precondition-failure].

:::

[cooldowndelay]: ../../Documentation/api-framework/interfaces/CommandOptions#cooldowndelay
[cooldownfilteredusers]: ../../Documentation/api-framework/interfaces/CommandOptions#cooldownfilteredusers
[cooldownlimit]: ../../Documentation/api-framework/interfaces/CommandOptions#cooldownlimit
[cooldownscope]: ../../Documentation/api-framework/interfaces/CommandOptions#cooldownscope
[defaultcooldown]: ../../Documentation/api-framework/interfaces/SapphireClientOptions#defaultcooldown
[sapphire]: ../../Documentation/api-framework/classes/SapphireClient
[scopes]: ../../Documentation/api-framework/enums/BucketScope
[reporting-precondition-failure]: ./reporting-precondition-failure
[timeutils]: ../../Documentation/api-utilities/modules/sapphire_time_utilities
[timeenum]: ../../Documentation/api-utilities/enums/sapphire_time_utilities.Time
122 changes: 120 additions & 2 deletions docs/Guide/preconditions/creating-your-own-preconditions.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,124 @@
---
sidebar_position: 4
sidebar_position: 1
title: Creating your own preconditions
---

## TODO
Just as we did in both [Creating Commands][creating-commands] and [Creating Listeners][creating-listeners], we will
start by creating a `preconditions` subdirectory in your project's entry point directory. For this guide, we'll be
building out an `OwnerOnly` precondition, that prevents anyone but the application owner from running the command.

Your directory should now look something like this:

```bash {9-10}
├── node_modules
├── package.json
└── src
├── commands
│ └── ping.js
├── index.js
├── listeners
│ └── ready.js
└── preconditions
└── OwnerOnly.js
```

The purpose of our `OwnerOnly` precondition is just as the name suggests: to check if the user is the bot owner. It can
be used for developer commands, such as commands that evaluate expressions or present internal debugging information.

## Creating a Precondition class

Preconditions are made by extending the Sapphire [`Precondition`][precondition] class and exporting it.

```typescript ts2esm2cjs
import { Precondition } from '@sapphire/framework';

export class OwnerOnlyPrecondition extends Precondition {}
```

Next, we can create a [`run`][preconditionrun] function to execute our logic. This function should either return
[`this.ok()`][preconditionok] to signify the condition has passed, or [`this.error(...)`][preconditionerror] to signify
the command should be denied.

```typescript ts2esm2cjs|{4-8}|{4-8}
import { Precondition, Message } from '@sapphire/framework';

export class OwnerOnlyPrecondition extends Precondition {
public run(message: Message) {
return message.author.id === 'YOUR_ID'
? this.ok()
: this.error({ message: 'Only the bot owner can use this command!' });
}
}
```

## Using Preconditions in Commands

To attach a precondition to a command, you simply have to input its name in an array in the command's
[`preconditions`][preconditions-option] option.

```typescript ts2esm2cjs|{7}|{7}
import { Command } from '@sapphire/framework';

export class PingCommand extends Command {
public constructor(context: Command.Context) {
super(context, {
// ...
preconditions: ['OwnerOnly']
});
}
}
```

Now, if someone who is not the bot owner executes the `ping` command, nothing will happen!

:::caution

For TypeScript users, there's an extra step to make this work. To increase the security of Sapphire's types, you'll need
to augment Sapphire's [`Preconditions`][preconditions-interface] interface. Please see an official example
[here][preconditions-augment].

<!--
TODO: Once we have a dedicated TypeScript page this should link to that instead of a code example.
Including why augmenting with `never` works and the difference between that and augmenting with an object.
-->

:::

By default, no error message will be sent or logged when a command is denied because of a precondition. To learn how to
configure this, please read [Reporting Precondition Failures][reporting-precondition-failure].

## Advanced Usage

Sapphire also has a builtin system for advanced conditional precondition logic through nested arrays. By default, all
preconditions in the given array must pass for the command to be run. However, you can use nested arrays to create `OR`
functionality. This could be useful if you'd like a command to be run if the user is either a moderator _or_ an admin.

Furthermore, if you create a nested array within a nested array, you'll receive `AND` functionality once more. Arrays
can be nested infinitely with the same pattern for optimal control over your preconditions.

Consider the following array of preconditions:

:::warning

None of the following preconditions are bundled with Sapphire; as such you'd have to create them yourself!

:::

```js
[['AdminOnly', ['ModOnly', 'OwnerOnly']], 'InVoiceChannel'];
```

For a command with these preconditions to pass the denial checks, the `InVoiceChannel` precondition must pass, as well
as `AdminOnly` _or_ both `OwnerOnly` and `ModOnly`.

[creating-commands]: ../getting-started/creating-a-basic-command
[creating-listeners]: ../listeners/creating-your-own-listeners
[precondition]: ../../Documentation/api-framework/classes/Precondition
[preconditionrun]: ../../Documentation/api-framework/classes/Precondition#run
[preconditionok]: ../../Documentation/api-framework/classes/Precondition#ok
[preconditionerror]: ../../Documentation/api-framework/classes/Precondition#error
[preconditions-option]: ../../Documentation/api-framework/interfaces/CommandOptions#preconditions
[preconditions-interface]: ../../Documentation/api-framework/interfaces/Preconditions
[preconditions-augment]:
https://github.com/sapphiredev/examples/blob/main/examples/with-typescript-recommended/src/preconditions/OwnerOnly.ts#L13-L17
[reporting-precondition-failure]: ./reporting-precondition-failure
59 changes: 57 additions & 2 deletions docs/Guide/preconditions/handling-permissions.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,61 @@
---
sidebar_position: 2
sidebar_position: 5
title: Handling permissions
---

## TODO
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
lack thereof. A common example would be moderation commands. Most people wouldn't want regular users to be able to ban
people, so we can restrict usage using the [`requiredUserPermissions`][requireduserpermissions] option.

<!-- TODO: info block redirecting to subcommands ts guide for using corresponding decorators -->

```typescript ts2esm2cjs|{7}|{7}
import { Command } from '@sapphire/framework';

export class BanCommand extends Command {
public constructor(context: Command.Context) {
super(context, {
// ...
requiredUserPermissions: ['BAN_MEMBERS']
});
}
}
```

Users without the `BAN_MEMBERS` permission will now be unable to use the command!

## Handling Client Permissions

It's also a good idea to verify the inverse: does the _bot_ have the `BAN_MEMBERS` permission? We can use the
[`requiredClientPermissions`][requiredclientpermissions] option the same way to prevent the command from being used if
not.

<!-- TODO: info block redirecting to subcommands ts guide for using corresponding decorators -->

```typescript ts2esm2cjs|{8}|{8}
import { Command } from '@sapphire/framework';

export class BanCommand extends Command {
public constructor(context: Command.Context) {
super(context, {
// ...
requiredUserPermissions: ['BAN_MEMBERS'],
requiredClientPermissions: ['BAN_MEMBERS']
});
}
}
```

With these changes, `BanCommand` now requires the command executor _and_ the client to have the `BAN_MEMBERS` permission
to execute!

:::tip

To learn how to send a message to the command executor when a precondition fails, see [Reporting Precondition
Failure][reporting-precondition-failure].

:::

[requireduserpermissions]: ../../Documentation/api-framework/interfaces/CommandOptions#requireduserpermissions
[requiredclientpermissions]: ../../Documentation/api-framework/interfaces/CommandOptions#requiredclientpermissions
[reporting-precondition-failure]: ./reporting-precondition-failure
27 changes: 25 additions & 2 deletions docs/Guide/preconditions/nsfw-filter.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
---
sidebar_position: 1
sidebar_position: 6
title: Locking commands to NSFW channels
---

## TODO
Sometimes it can be necessary to lock certain commands to NSFW (not safe for work) channels. This can be simply achieved
by setting the `nsfw` option in the command.

```typescript ts2esm2cjs|{7}|{7}
import { Command } from '@sapphire/framework';

export class NSFWCommand extends Command {
public constructor(context: Command.Context) {
super(context, {
// ...
nsfw: true
});
}
}
```

:::tip

To learn how to send a message to the command executor when a precondition fails, see [Reporting Precondition
Failure][reporting-precondition-failure].

:::

[reporting-precondition-failure]: ./reporting-precondition-failure
Loading