-
-
Notifications
You must be signed in to change notification settings - Fork 4k
feat(guide): port legacy guide #10938
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
base: main
Are you sure you want to change the base?
Changes from 18 commits
2760e9a
32a1058
bd18237
e2039cf
b809b4e
bf08ce4
d3ca602
71dd5b2
f39897c
64725cc
8f95419
e6a5a8d
cbd1c6d
b1b1694
5daaaa3
d658632
c37b6d0
205a609
4f23fb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# Contributing | ||
|
||
## Local development | ||
|
||
Clone the repo into your desired folder, `cd` into it, and install the dependencies. | ||
|
||
```bash | ||
git clone https://github.com/discordjs/discord.js.git | ||
cd discord.js/apps/guide | ||
pnpm --filter guide install | ||
``` | ||
|
||
You can use `pnpm dev` to serve a local version of the guide. | ||
|
||
## Adding pages | ||
|
||
To add a new page to the guide, create a `filename.mdx` file in the folder of your choice located under `/content`. Fumadocs will pick it up and route appropriately. To list the section in the sidebar, make sure it is listed in the `meta.json` file of the directory you placed it in under the `pages` key. The order in the `pages` array determines the order pages have in the sidebar. | ||
|
||
## Layout | ||
|
||
The guide uses the fumadocs documention framework for Next.js. You can find examples as well as documentation for many components you can use at <https://fumadocs.dev/docs/ui>. | ||
|
||
## General guidelines | ||
|
||
Because we want to keep everything as consistent and clean as possible, here are some guidelines we strongly recommend you try to follow when making a contribution. | ||
|
||
### Spelling, grammar, and wording | ||
|
||
Improper grammar, strange wording, and incorrect spelling are all things that may lead to confusion when a user reads a guide page. It's important to attempt to keep the content clear and consistent. Re-read what you've written and place yourself in the shoes of someone else for a moment to see if you can fully understand everything without any confusion. | ||
|
||
Don't worry if you aren't super confident with your grammar/spelling/wording skills; all pull requests get thoroughly reviewed, and comments are left in areas that need to be fixed or could be done better/differently. | ||
|
||
#### "You"/"your" instead of "we"/"our" | ||
|
||
When explaining parts of a guide, it's recommended to use "you" instead of "we" in most situations. For example: | ||
|
||
```diff | ||
- To check our Node version, we can run `node -v`. | ||
+ To check your Node version, you can run `node -v`. | ||
|
||
- To delete a message, we can do: `message.delete();` | ||
+ To delete a message, you can do: `message.delete();` | ||
|
||
- Our final code should look like this: ... | ||
+ Your final code should look like this: ... | ||
|
||
- Before we can actually do this, we need to update our configuration file. | ||
+ Before you can actually do this, you need to update your configuration file. | ||
``` | ||
|
||
#### "We" instead of "I" | ||
|
||
When referring to yourself, use "we" (as in "the writers of this guide") instead of "I". For example: | ||
|
||
```diff | ||
- If you don't already have this package installed, I would highly recommend doing so. | ||
+ If you don't already have this package installed, we would highly recommend doing so. | ||
# Valid alternative: | ||
+ If you don't already have this package installed, it's highly recommended that you do so. | ||
|
||
- In this section, I'll be covering how to do that really cool thing everyone's asking about. | ||
+ In this section, we'll be covering how to do that really cool thing everyone's asking about. | ||
``` | ||
|
||
### Inclusive language | ||
|
||
Try to avoid gendered and otherwise non-inclusive language. Examples are: | ||
|
||
- Whitelist -> Allowlist | ||
- Blacklist -> Denylist | ||
- Master/Slave -> Leader/follower, primary/replica, primary/secondary, primary/standby | ||
- Gendered pronouns (e.g. he/him/his) -> They, them, their | ||
- Gendered terms (e.g. guys) -> Folks, people | ||
- Sanity check -> Quick check, confidence check, coherence check | ||
- Dummy value -> Placeholder, sample value | ||
|
||
### Paragraph structure | ||
|
||
Tied in with the section above, try to keep things as neatly formatted as possible. If a paragraph gets long, split it up into multiple paragraphs so that it adds some spacing and is easier on the eyes. |
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,104 @@ | ||||||
--- | ||||||
title: Cooldowns | ||||||
--- | ||||||
|
||||||
Spam is something you generally want to avoid, especially if one of your commands require calls to other APIs or takes a bit of time to build/send. | ||||||
|
||||||
<Callout> | ||||||
This section assumes you followed the [Command Handling](/guide/legacy/app-creation/handling-commands) part of the | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we use relative paths wherever possible? |
||||||
guide. | ||||||
</Callout> | ||||||
|
||||||
First, add a cooldown property to your command. This will determine how long the user would have to wait (in seconds) before using the command again. | ||||||
|
||||||
```js title="commands/utility/ping.js" | ||||||
const { SlashCommandBuilder } = require('discord.js'); | ||||||
|
||||||
module.exports = { | ||||||
cooldown: 5, // [!code ++] | ||||||
data: new SlashCommandBuilder().setName('ping').setDescription('Replies with Pong!'), | ||||||
async execute(interaction) { | ||||||
// ... | ||||||
}, | ||||||
}; | ||||||
``` | ||||||
|
||||||
In your main file, initialize a [Collection](/additional-info/collections.md) to store cooldowns of commands: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
```js | ||||||
client.cooldowns = new Collection(); | ||||||
``` | ||||||
|
||||||
The key will be the command names, and the values will be Collections associating the user's id (key) to the last time (value) this user used this command. Overall the logical path to get a user's last usage of a command will be `cooldowns > command > user > timestamp`. | ||||||
|
||||||
In your `InteractionCreate` event handler, add the following code: | ||||||
|
||||||
```js title="index.js / interactionCreate.js (if you followed the event handler section)" | ||||||
// ... | ||||||
// [!code ++:14] | ||||||
const { cooldowns } = interaction.client; | ||||||
|
||||||
if (!cooldowns.has(command.data.name)) { | ||||||
cooldowns.set(command.data.name, new Collection()); | ||||||
} | ||||||
|
||||||
const now = Date.now(); | ||||||
const timestamps = cooldowns.get(command.data.name); | ||||||
const defaultCooldownDuration = 3; | ||||||
const cooldownAmount = (command.cooldown ?? defaultCooldownDuration) * 1_000; | ||||||
|
||||||
if (timestamps.has(interaction.user.id)) { | ||||||
// ... | ||||||
} | ||||||
``` | ||||||
|
||||||
You check if the `cooldowns` Collection already has an entry for the command being used. If this is not the case, you can add a new entry, where the value is initialized as an empty Collection. Next, create the following variables: | ||||||
|
||||||
1. `now`: The current timestamp. | ||||||
2. `timestamps`: A reference to the Collection of user ids and timestamp key/value pairs for the triggered command. | ||||||
3. `cooldownAmount`: The specified cooldown for the command, converted to milliseconds for straightforward calculation. If none is specified, this defaults to three seconds. | ||||||
|
||||||
If the user has already used this command in this session, get the timestamp, calculate the expiration time, and inform the user of the amount of time they need to wait before using this command again. Note the use of the `return` statement here, causing the code below this snippet to execute only if the user has not used this command in this session or the wait has already expired. | ||||||
|
||||||
Continuing with your current setup, this is the complete `if` statement: | ||||||
|
||||||
```js title="index.js / interactionCreate.js (if you followed the event handler section)" | ||||||
const defaultCooldownDuration = 3; | ||||||
const cooldownAmount = (command.cooldown ?? defaultCooldownDuration) * 1_000; | ||||||
|
||||||
// [!code focus:13] | ||||||
if (timestamps.has(interaction.user.id)) { | ||||||
// ... // [!code --] | ||||||
// [!code ++:9] | ||||||
const expirationTime = timestamps.get(interaction.user.id) + cooldownAmount; | ||||||
|
||||||
if (now < expirationTime) { | ||||||
const expiredTimestamp = Math.round(expirationTime / 1_000); | ||||||
return interaction.reply({ | ||||||
content: `Please wait, you are on a cooldown for \`${command.data.name}\`. You can use it again <t:${expiredTimestamp}:R>.`, | ||||||
flags: MessageFlags.Ephemeral, | ||||||
}); | ||||||
} | ||||||
} | ||||||
``` | ||||||
|
||||||
Since the `timestamps` Collection has the user's id as the key, you can use the `get()` method on it to get the value and sum it up with the `cooldownAmount` variable to get the correct expiration timestamp and further check to see if it's expired or not. | ||||||
|
||||||
The previous user check serves as a precaution in case the user leaves the guild. You can now use the `setTimeout` method, which will allow you to execute a function after a specified amount of time and remove the timeout. | ||||||
|
||||||
```js | ||||||
// [!code focus] | ||||||
if (timestamps.has(interaction.user.id)) { | ||||||
const expiredTimestamp = Math.round(expirationTime / 1_000); | ||||||
return interaction.reply({ | ||||||
content: `Please wait, you are on a cooldown for \`${command.data.name}\`. You can use it again <t:${expiredTimestamp}:R>.`, | ||||||
flags: MessageFlags.Ephemeral, | ||||||
}); | ||||||
} // [!code focus] | ||||||
|
||||||
// [!code ++:2] [!code focus:2] | ||||||
timestamps.set(interaction.user.id, now); | ||||||
setTimeout(() => timestamps.delete(interaction.user.id), cooldownAmount); | ||||||
``` | ||||||
|
||||||
This line causes the entry for the user under the specified command to be deleted after the command's cooldown time has expired for them. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
--- | ||
title: Reloading Commands | ||
--- | ||
|
||
When writing your commands, you may find it tedious to restart your bot every time for testing the smallest changes. With a command handler, you can eliminate this issue and reload your commands while your bot is running. | ||
|
||
<Callout> | ||
ESM does not support require and clearing import cache. You can use [hot-esm](https://www.npmjs.com/package/hot-esm) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean there's always |
||
to import files without cache. Windows support is experimental per [this | ||
issue](https://github.com/vinsonchuong/hot-esm/issues/33). | ||
</Callout> | ||
|
||
<Callout> | ||
This section assumes you followed the [Command Handling](/guide/legacy/app-creation/handling-commands) part of the | ||
guide. | ||
</Callout> | ||
|
||
```js title="commands/utility/reload.js" | ||
const { SlashCommandBuilder } = require('discord.js'); | ||
|
||
module.exports = { | ||
data: new SlashCommandBuilder() | ||
.setName('reload') | ||
.setDescription('Reloads a command.') | ||
.addStringOption((option) => option.setName('command').setDescription('The command to reload.').setRequired(true)), | ||
async execute(interaction) { | ||
// ... | ||
}, | ||
}; | ||
``` | ||
|
||
First off, you need to check if the command you want to reload exists. You can do this check similarly to getting a command. | ||
|
||
```js {4-9} | ||
module.exports = { | ||
// ... | ||
// [!code focus:10] | ||
async execute(interaction) { | ||
// ... // [!code --] | ||
// [!code ++:6] | ||
const commandName = interaction.options.getString('command', true).toLowerCase(); | ||
const command = interaction.client.commands.get(commandName); | ||
|
||
if (!command) { | ||
return interaction.reply(`There is no command with name \`${commandName}\`!`); | ||
} | ||
}, | ||
}; | ||
``` | ||
|
||
<Callout type="warn"> | ||
The reload command ideally should not be used by every user. You should deploy it as a guild command in a private | ||
guild. | ||
</Callout> | ||
|
||
To build the correct file path, you will need the file name. You can use `command.data.name` for doing that. | ||
|
||
In theory, all there is to do is delete the previous command from `client.commands` and require the file again. In practice, you cannot do this easily as `require()` caches the file. If you were to require it again, you would load the previously cached file without any changes. You first need to delete the file from `require.cache`, and only then should you require and set the command file to `client.commands`: | ||
|
||
```js {1,4-6} | ||
delete require.cache[require.resolve(`./${command.data.name}.js`)]; | ||
|
||
try { | ||
const newCommand = require(`./${command.data.name}.js`); | ||
interaction.client.commands.set(newCommand.data.name, newCommand); | ||
await interaction.reply(`Command \`${newCommand.data.name}\` was reloaded!`); | ||
} catch (error) { | ||
console.error(error); | ||
await interaction.reply( | ||
`There was an error while reloading a command \`${command.data.name}\`:\n\`${error.message}\``, | ||
); | ||
} | ||
``` | ||
|
||
The snippet above uses a `try...catch` block to load the command file and add it to `client.commands`. In case of an error, it will log the full error to the console and notify the user about it with the error's message component `error.message`. Note that you never actually delete the command from the commands Collection and instead overwrite it. This behavior prevents you from deleting a command and ending up with no command at all after a failed `require()` call, as each use of the reload command checks that Collection again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a fan of this butchering to have 1 word that represents a list of allowed/disallowed values, imo a phrase is better but 🤷