mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
feat(guide): port remaining prs/issues from legacy (#10974)
* chore: remove await wait placeholder prefer using an explanatory placeholder rather than this artificial example original issue: https://github.com/discordjs/guide/issues/1360 * chore: remove implicit grant guide and add disclaimer issue: https://github.com/discordjs/guide/issues/1370/ pr: https://github.com/discordjs/guide/pull/1543/ * chore(sharding): improve broadcast sample and use of context argument original PR: https://github.com/discordjs/guide/pull/1624 * feat: add page about setup with proxy original PR: https://github.com/discordjs/guide/pull/1623 * chore: clarify hiding of commands original PR: https://github.com/discordjs/guide/pull/1617 * feat(voice): seeking original PR: https://github.com/discordjs/guide/pull/1483 * chore(oauth2): typo * chore: align with rest of the guide remove abstraction layers in ws proxy handling in favour of directly setting globals * chore: branding over grammar * Apply suggestions from code review Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com> * chore: remove now obsolete example explanation from comments --------- Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"pages": ["async-await", "collections", "es6-syntax", "notation", "rest-api"]
|
"pages": ["async-await", "collections", "es6-syntax", "notation", "rest-api", "proxy"]
|
||||||
}
|
}
|
||||||
|
|||||||
73
apps/guide/content/docs/legacy/additional-info/proxy.mdx
Normal file
73
apps/guide/content/docs/legacy/additional-info/proxy.mdx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
title: Using a proxy
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide will show you how to set up a proxy with discord.js. This may be necessary if you are deploying your bot to a server with a firewall only allowing outside traffic through the proxy.
|
||||||
|
|
||||||
|
Proxying discord.js requires two components: a REST proxy and a WebSocket proxy.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
To achieve these two components you can utilize the `undici` and `global-agent` packages:
|
||||||
|
|
||||||
|
```sh tab="npm"
|
||||||
|
npm install undici global-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh tab="yarn"
|
||||||
|
yarn add undici global-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh tab="pnpm"
|
||||||
|
pnpm add undici global-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh tab="bun"
|
||||||
|
bun add undici global-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting up the proxy for REST calls
|
||||||
|
|
||||||
|
The `@discordjs/rest` package handling HTTP requests in discord.js uses the `undici` package. Accordingly, you can provide a custom `ProxyAgent` configuration to the client constructor:
|
||||||
|
|
||||||
|
```js title="index.js" lineNumbers
|
||||||
|
const { ProxyAgent } = require('undici'); // [!code word:ProxyAgent]
|
||||||
|
const { Client } = require('discord.js');
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
// ...other client options
|
||||||
|
rest: {
|
||||||
|
agent: new ProxyAgent('http://my-proxy-server:port'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login('your-token-goes-here');
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout>
|
||||||
|
For further information on the `undici` `ProxyAgent`, please refer to the [undici
|
||||||
|
documentation](https://undici.nodejs.org/#/docs/api/ProxyAgent.md).
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Setting up the proxy for the WebSocket connection
|
||||||
|
|
||||||
|
To set up a proxy for WebSocket, you can use the `global-agent` package. You will need to import and call the `bootstrap()` function and set the required `GLOBAL_AGENT` globals as shown below:
|
||||||
|
|
||||||
|
```js title="index.js" lineNumbers
|
||||||
|
const { ProxyAgent } = require('undici');
|
||||||
|
const { Client } = require('discord.js');
|
||||||
|
const { bootstrap } = require('global-agent'); // [!code ++:5]
|
||||||
|
|
||||||
|
bootstrap(); // [!code word:bootstrap]
|
||||||
|
global.GLOBAL_AGENT.HTTP_PROXY = 'http://my-proxy-server:port';
|
||||||
|
global.GLOBAL_AGENT.HTTPS_PROXY = 'https://my-proxy-server:port';
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
// ...other client options
|
||||||
|
rest: {
|
||||||
|
agent: new ProxyAgent('http://my-proxy-server:port'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login('your-token-goes-here');
|
||||||
|
```
|
||||||
@@ -75,56 +75,11 @@ The `identify` scope will allow your application to get basic user information f
|
|||||||
|
|
||||||
### Implicit grant flow
|
### Implicit grant flow
|
||||||
|
|
||||||
You have your website, and you have a URL. Now you need to use those two things to get an access token. For basic applications like [SPAs](https://en.wikipedia.org/wiki/Single-page_application), getting an access token directly is enough. You can do so by changing the `response_type` in the URL to `token`. However, this means you will not get a refresh token, which means the user will have to explicitly re-authorize when this access token has expired.
|
<Callout type="error">
|
||||||
|
Implicit grant flow, as previously covered in this section, is vulnerable to token leakage and replay attacks. Please
|
||||||
After you change the `response_type`, you can test the URL right away. Visiting it in your browser, you will be directed to a page that looks like this:
|
use the **authorization grant** flow instead. You can find out more about the best implementation practices in the
|
||||||
|
[Oauth2 RFC](https://datatracker.ietf.org/doc/html/rfc9700).
|
||||||

|
</Callout>
|
||||||
|
|
||||||
You can see that by clicking `Authorize`, you allow the application to access your username and avatar. Once you click through, it will redirect you to your redirect URL with a [fragment identifier](https://en.wikipedia.org/wiki/Fragment_identifier) appended to it. You now have an access token and can make requests to Discord's API to get information on the user.
|
|
||||||
|
|
||||||
Modify `index.html` to add your OAuth2 URL and to take advantage of the access token if it exists. Even though [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) is for working with query strings, it can work here because the structure of the fragment follows that of a query string after removing the leading "#".
|
|
||||||
|
|
||||||
```html title="index.html" lineNumbers=11
|
|
||||||
<div id="info">Hoi!</div>
|
|
||||||
<a id="login" style="display: none;" href="your-oauth2-URL-here">Identify Yourself</a>
|
|
||||||
<script>
|
|
||||||
window.onload = () => {
|
|
||||||
const fragment = new URLSearchParams(window.location.hash.slice(1));
|
|
||||||
const [accessToken, tokenType] = [fragment.get('access_token'), fragment.get('token_type')];
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
return (document.getElementById('login').style.display = 'block');
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('https://discord.com/api/users/@me', {
|
|
||||||
headers: {
|
|
||||||
authorization: `${tokenType} ${accessToken}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((result) => result.json())
|
|
||||||
.then((response) => {
|
|
||||||
const { username, discriminator } = response;
|
|
||||||
document.getElementById('info').innerText += ` ${username}#${discriminator}`;
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
Here you grab the access token and type from the URL if it's there and use it to get info on the user, which is then used to greet them. The response you get from the [`/api/users/@me` endpoint](https://discord.com/developers/docs/resources/user#get-current-user) is a [user object](https://discord.com/developers/docs/resources/user#user-object) and should look something like this:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "123456789012345678",
|
|
||||||
"username": "User",
|
|
||||||
"discriminator": "0001",
|
|
||||||
"avatar": "1cc0a3b14aec3499632225c708451d67",
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In the following sections, we'll go over various details of Discord and OAuth2.
|
|
||||||
|
|
||||||
## More details
|
## More details
|
||||||
|
|
||||||
|
|||||||
@@ -79,14 +79,24 @@ You can access them later as usual via `process.argv`, which contains an array o
|
|||||||
|
|
||||||
## Eval arguments
|
## Eval arguments
|
||||||
|
|
||||||
There may come the point where you will want to pass arguments from the outer scope into a `.broadcastEval()` call.
|
There may come the point where you will want to pass arguments from the outer scope into a `.broadcastEval()` call. The `context` parameter is useful for this purpose.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function funcName(c, { arg }) {
|
// [!code word:context]
|
||||||
|
function funcName(client, context) {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|
||||||
client.shard.broadcastEval(funcName, { context: { arg: 'arg' } });
|
// Evaluate on all shards
|
||||||
|
client.shard.broadcastEval(funcName, {
|
||||||
|
context: { arg: 'arg' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evaluate on a specific shard
|
||||||
|
client.shard.broadcastEval(funcName, {
|
||||||
|
shard: 0,
|
||||||
|
context: { arg: 'arg' },
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
The `BroadcastEvalOptions` typedef was introduced in discord.js v13 as the second parameter in `.broadcastEval()`. It accepts two properties: `shard` and `context`. The `context` property will be sent as the second argument to your function.
|
The `BroadcastEvalOptions` typedef was introduced in discord.js v13 as the second parameter in `.broadcastEval()`. It accepts two properties: `shard` and `context`. The `context` property will be sent as the second argument to your function.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ The slash command permissions for guilds are defaults only and can be altered by
|
|||||||
|
|
||||||
## Member permissions
|
## Member permissions
|
||||||
|
|
||||||
You can use `SlashCommandBuilder#setDefaultMemberPermissions` to set the default permissions required for a member to run the command. Setting it to `0` will prohibit anyone in a guild from using the command unless a specific overwrite is configured or the user has the Administrator permission flag.
|
You can use `SlashCommandBuilder#setDefaultMemberPermissions` to set the default permissions required for a member to run the command. Setting it to `0` will hide the command from and prohibit anyone in a guild from using the command unless a specific overwrite is configured or the user has the Administrator permission flag.
|
||||||
|
|
||||||
For this, you'll introduce two common and simple moderation commands: `ban` and `kick`. For a ban command, a sensible default is to ensure that users already have the Discord permission `BanMembers` in order to use it.
|
For this, you'll introduce two common and simple moderation commands: `ban` and `kick`. For a ban command, a sensible default is to ensure that users already have the Discord permission `BanMembers` in order to use it.
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,12 @@ After you've sent an initial response, you may want to edit that response for va
|
|||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const wait = require('node:timers/promises').setTimeout;
|
|
||||||
|
|
||||||
client.on(Events.InteractionCreate, async (interaction) => {
|
client.on(Events.InteractionCreate, async (interaction) => {
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
if (interaction.commandName === 'ping') {
|
if (interaction.commandName === 'ping') {
|
||||||
await interaction.reply('Pong!');
|
await interaction.reply('Pong!');
|
||||||
await wait(2_000);
|
// do something that requires time (database queries, api requests, ...)
|
||||||
await interaction.editReply('Pong again!'); // [!code word:editReply]
|
await interaction.editReply('Pong again!'); // [!code word:editReply]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -71,16 +69,13 @@ As previously mentioned, Discord requires an acknowledgement from your bot withi
|
|||||||
In this case, you can make use of the `ChatInputCommandInteraction#deferReply()` method, which triggers the `<application> is thinking...` message. This also acts as the initial response, to confirm to Discord that the interaction was received successfully and gives you a **15-minute timeframe to complete your tasks** before responding.
|
In this case, you can make use of the `ChatInputCommandInteraction#deferReply()` method, which triggers the `<application> is thinking...` message. This also acts as the initial response, to confirm to Discord that the interaction was received successfully and gives you a **15-minute timeframe to complete your tasks** before responding.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const wait = require('node:timers/promises').setTimeout;
|
|
||||||
|
|
||||||
client.on(Events.InteractionCreate, async (interaction) => {
|
client.on(Events.InteractionCreate, async (interaction) => {
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
if (interaction.commandName === 'ping') {
|
if (interaction.commandName === 'ping') {
|
||||||
await interaction.deferReply(); // [!code word:deferReply]
|
await interaction.deferReply(); // [!code word:deferReply]
|
||||||
// you can take up to 15 minutes! We take 4 seconds to demonstrate this
|
// you can do things that take time here (database queries, api requests, ...) that you need for the initial response
|
||||||
// since it is barely above the 3-second threshold for the initial response
|
// you can take up to 15 minutes, then the interaction token becomes invalid!
|
||||||
await wait(4_000);
|
|
||||||
await interaction.editReply('Pong!'); // [!code word:editReply]
|
await interaction.editReply('Pong!'); // [!code word:editReply]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"life-cycles",
|
"life-cycles",
|
||||||
"voice-connections",
|
"voice-connections",
|
||||||
"audio-player",
|
"audio-player",
|
||||||
"audio-resources"
|
"audio-resources",
|
||||||
|
"seeking"
|
||||||
],
|
],
|
||||||
"root": true,
|
"root": true,
|
||||||
"icon": "Volume2"
|
"icon": "Volume2"
|
||||||
|
|||||||
64
apps/guide/content/docs/voice/seeking.mdx
Normal file
64
apps/guide/content/docs/voice/seeking.mdx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: Seeking
|
||||||
|
---
|
||||||
|
|
||||||
|
There is no built-in method for seeking audio resources in discord.js. However external libraries such as `prism-media` can be used to tackle this issue.
|
||||||
|
|
||||||
|
## Seeking in a stream
|
||||||
|
|
||||||
|
To seek resource, you can create a new function with the following code:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Require necessary package
|
||||||
|
const prism = require('prism-media'); // [!code word:prism]
|
||||||
|
|
||||||
|
function createFFmpegStream(stream, seek) {
|
||||||
|
let seekPosition = '0';
|
||||||
|
if (seek) seekPosition = String(seek);
|
||||||
|
const transcoder = new prism.FFmpeg({
|
||||||
|
args: [
|
||||||
|
'-analyzeduration',
|
||||||
|
'0',
|
||||||
|
'-loglevel',
|
||||||
|
'0',
|
||||||
|
'-f',
|
||||||
|
's16le',
|
||||||
|
'-ar',
|
||||||
|
'48000',
|
||||||
|
'-ac',
|
||||||
|
'2',
|
||||||
|
'-ss',
|
||||||
|
seekPosition,
|
||||||
|
'-ab',
|
||||||
|
'320',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const s16le = stream.pipe(transcoder);
|
||||||
|
const opus = s16le.pipe(new prism.opus.Encoder({ rate: 48000, channels: 2, frameSize: 960 }));
|
||||||
|
return opus;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This function takes two arguments: the audio stream and the desired seek position, expressed as duration within the duration of the full stream. It returns the seeked stream.
|
||||||
|
|
||||||
|
<Callout>
|
||||||
|
You can find configuration options in the [prism media documentation](https://amishshah.github.io/prism-media/).
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Using seek with the audio player
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { createAudioResource, createAudioPlayer } = require('@discordjs/voice');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const player = createAudioPlayer();
|
||||||
|
const normalAudioResource = createAudioResource('Your audio file path');
|
||||||
|
|
||||||
|
player.play(normalAudioResource);
|
||||||
|
|
||||||
|
// [!code word:createFFmpegStream]
|
||||||
|
const seekedAudioStream = createFFmpegStream(fs.createReadStream('Your audio file path'), 10); // Seek to 10s
|
||||||
|
const seekedAudioResource = createAudioResource(seekedAudioStream);
|
||||||
|
|
||||||
|
player.play(seekedAudioResource);
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user