docs: Update desired properties docs (#3940)

* Update docs for desired properties

* Update examples to use createBot desiredProperties

The BigBot and reaction roles examples use the Discordeno CLI

* Migrate examples to v19 stable

* Docs work
This commit is contained in:
Fleny
2024-11-19 16:40:01 -06:00
committed by GitHub
parent 54a7463dbb
commit 4939822434
44 changed files with 1369 additions and 1103 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Beginner Tips",
"position": 4,
"position": 5,
"link": {
"type": "generated-index",
"description": "Guides for basic knowledge to use discordeno."
+24 -42
View File
@@ -20,49 +20,30 @@ import { createBot } from '@discordeno/bot'
export const BOT = createBot({
token,
events: {},
})
```
Now that our bot manager is created, we need to implement our event handlers. First, we can make another file like `services/bot/events/index.ts` and paste the code below.
Awesome, now we need to implement an event handler. For example, let's implement the ready event. So we make a file like `services/bot/events/ready.ts` and paste the code below:
```ts
import type { EventHandlers } from '@discordeno/bot'
import { BOT } from '../bot.js'
export const events: Partial<EventHandlers> = {
// Fill this in the next section
export const ready: typeof BOT.events.ready = async ({ shardId }) => {
BOT.logger.info(`[READY] Shard ID #${shardId} is ready.`)
}
```
Now, we go back to the bot file and pass this events object to the createBot function.
Now that we have a ready event handler, let's go ahead and add it to our bot.
```ts
import { createBot } from '@discordeno/bot'
import { events } from './events/index.js'
import { ready } from './events/ready.js'
export const BOT = createBot({
token,
events,
})
```
Awesome, now the only thing left is we need to implement an event handler. For example, let's implement the ready event. So we make a file like `services/bot/events/ready.ts` and paste the code below:
```ts
export const ready: EventHandlers['ready'] = async function (payload, shardId) {
logger.info(`[READY] Shard ID #${shardId} is ready.`)
}
```
Now that we have a ready event handler, let's go ahead and add it to our events.
```ts
import type { EventHandlers } from '@discordeno/bot'
import { ready } from './ready.ts'
export const events: Partial<EventHandlers> = {
ready,
}
BOT.events.ready = ready;
```
There you go. You now have an event handler working perfectly.
@@ -112,10 +93,12 @@ Now that we have the basic code setup complete for our listener, we can begin ad
```ts
try {
// OPTIONAL: Runs the raw event handler if you need it
bot.events.raw(bot, req.body.payload, req.body.shardId);
// Runs the event handler if available
if (message.t) bot.events.[snakeToCamelCase(message.t.toLowerCase())]?.(req.body.payload, req.body.shardId);
// Trigger the raw event, you may remove this if you don't need it
bot.events.raw?.(req.body.payload, req.body.shardId)
if (data.t) {
bot.handlers[data.t]?.(bot, req.body.payload, req.body.shardId)
}
res.status(200).json({ success: true })
}
@@ -128,15 +111,12 @@ Alright, now we need to start making our connection to the rest proxy work. That
```ts
export const BOT = createBot({
token,
events,
})
BOT.rest = createRestManager({
token: process.env.TOKEN,
proxy: {
baseUrl: process.env.REST_URL,
authorization: process.env.AUTHORIZATION,
},
rest: {
proxy: {
baseUrl: process.env.REST_URL,
authorization: process.env.AUTHORIZATION,
},
}
})
```
@@ -192,9 +172,11 @@ Threading or workers or clusters, however you wish to call it can be used here.
With server splitting, we are going to split the amount of events that are handled by a bot process across several bot processes. So let's say we buy a couple servers for our bot processes. We can throw this process on both of them. Then go back to our `shards` in step 3 and make each shard send it to the appropriate server. If you think back, we already coded step 3 with this in mind.
```ts
async message(shrd, payload) {
await fetch(getUrlFromShardId(req.body.totalShards, shrd.id), {
method: 'POST',
async message(shard, payload) {
await fetch(getUrlFromShardId(req.body.totalShards, shard.id), {
method: 'POST',
})
}
```
Here we were using a function to determine which url it should send to.
+147
View File
@@ -0,0 +1,147 @@
---
sidebar_position: 4
sidebar_label: Desired Properties
---
# Desired Properties
The `desiredProperties` feature in Discordeno gives developers full control over memory utilization. This enables a highly lightweight setup, where only essential data is stored.
With `desiredProperties`, you can specify which properties to cache for each object type—such as users, members, channels, and guilds. This flexibility allows you to tailor caching to the exact needs of your bot, preserving only the data you truly require.
## Benefits
- **Memory Efficiency**: Only relevant data is stored, leading to substantial memory savings, especially for larger bots.
- **Improved Performance**: By storing minimal data, bots experience faster processing times and reduced resource usage.
- **Customizable**: Developers can enable specific properties on a per-object basis, eliminating unnecessary bloat.
## Example: The Memory Impact of Channel Topics
Consider the `channel.topic` property, which stores a text description for each channel.
While a single topic might not seem memory-intensive, this property can quickly become costly at scale:
- **Single Channel Topic**: A typical `channel.topic` can occupy hundreds of bytes.
- **Large Bot Scale**: If your bot operates across millions of servers with hundreds of millions of channels, storing every `channel.topic` would consume vast amounts of memory.
By choosing to store only the properties relevant to your bots functionality — like omitting `channel.topic` when its unnecessary — you can save gigabytes of memory.
Desired Properties is thus an essential tool for bots needing scalable and efficient caching, allowing for minimal resource usage without sacrificing performance.
:::tip
Check the [TypeScript](#typescript) section if you are using typescript
:::
## Configuring
To configure desired proprieties you can use the `desiredProperties` option on the `createBot` function
The objects inside `desiredProperties` contains all the names of the objects that have desired proprieties and in them you will find all the properties of the objects.
:::info[Flags and toggles]
Usually flags and toggles will be stored in a BitField to save on memory, Discordeno does provide getters on the objects for these flags however they aren't in desired properties with their individual names, instead you will find them as `toggles` and / or `flags` most of the cases.
:::
:::danger[NOT RECOMMENDED - Changing the default for Desired Properties]
You can change the default value for desired properties, using `desiredProprieties: createDesiredPropertiesObject({}, true) as CompleteDesiredProprieties<{}, true>` in the `createBot` function, however this will negate all the benefits desired proprieties provide.
The reason why this is not recommended is because while Desired Proprieties can be an annoyance at first, they have a significant performance impact on both CPU and memory usage.
Again, this is **NOT** RECOMMENDED, especially if you plan to ship your bot to production.
:::
### Computed values
Some values in these object may depend on some other value, notable examples are `user.bot` and `interaction.respond`. If you do not include all the values they depend on these require you might face undefined behavior using these values.
### Examples
In this example we will configure desired properties to have `user.id`, `user.bot` and `user.username`.
```ts
const bot = createBot({
// Your usual createBot options, such as token and intents
desiredProperties: {
user: {
id: true,
toggles: true, // Toggles includes the "bot" flag
username: true,
},
},
})
```
## TypeScript
Discordeno will give change the types of the supported objects to match your desired proprieties, for this reason you might get an error when incorrectly typing your functions.
Along side `desiredProperties` in the bot option that is explained above, `desiredPropertiesBehavior` is a configuration option for how should typescript threat proprieties that are not desired in your configuration.
Discordeno does expose the customized type according to your desired properties in the `bot.transformers.$inferredTypes` object, in these you will find all the types to be used in your functions / variables / ...
:::info
The value `bot.transformers.$inferredTypes` only exists for typescript. It will be `undefined` if tried to access at runtime, as it is not intended to provide any value at runtime, and it is intended to be used along side the `typeof` operator in typescript
:::
### Example
```ts
const bot = createBot({
// Your usual createBot options, such as token and intents
desiredProperties: {
message: {
id: true,
author: true,
}
user: {
id: true,
toggles: true, // Toggles includes the "bot" flag
username: true,
},
},
})
bot.events.messageCreate = (message) => {
processMessage(message)
}
function processMessage(message: typeof bot.transformers.$inferredTypes.message) {
bot.logger.info(`Message with id ${message.id} has author @${message.author.username}, whose has id ${message.author.id} and ${message.author.bot ? 'is' : "isn't"} a bot`)
// Do some other work with the message
}
```
### Configuring
There are 2 behaviors, `ChangeType` and `RemoveKey`. The default behavior is `RemoveKey`.
An example where the behavior is changed to `ChangeType` is:
```ts
const bot = createBot({
// Your usual createBot options, such as token and intents
desiredPropertiesBehavior: DesiredPropertiesBehavior.ChangeType,
desiredProperties: {
user: {
id: true,
toggles: true, // Toggles includes the "bot" flag
username: true,
},
},
})
```
Following is the explanation of each behavior:
#### `RemoveKey`
All the "undesired" properties will be removed from the type of the object. This will prevent you from using them at all since they "don't exist anymore".
The caveats of this behavior are the following:
- You don't know all the properties available on the object
- If a value requires other values to be enabled you won't know them without searching it up (when a computed value is missing a dependency it won't be shown)
#### `ChangeType`
All the "undesired" properties will be typed with a string that will explain why the property is disabled, this may also include the dependencies for said property if those are present.
The caveats of this behavior are the following:
- Typescript may not always error on the usage of undesired proprieties as in some context the string will be a valid option
+1 -1
View File
@@ -20,7 +20,7 @@ This is how you can use it to create a bot that logs into discord:
```ts
import { load } from 'https://deno.land/std@0.212.0/dotenv/mod.ts'
import { createBot } from 'npm:@discordeno/bot@19.0.0-next.d81b28a'
import { createBot } from 'npm:@discordeno/bot@19.0.0'
const env = await load()
+103 -91
View File
@@ -26,9 +26,8 @@ import { createBot } from '@discordeno/bot'
import { config } from 'dotenv'
config()
const bot = createBot({
export const bot = createBot({
token: process.env.TOKEN,
events: {},
})
await bot.start()
@@ -380,11 +379,10 @@ We need to parse all the options Discord has provided us. In the `/roles reactio
```ts
import commands from '../commands/index.js'
import { bot } from '../bot.js'
import { commandOptionsParser } from '@discordeno/bot'
export const event: EventHandlers['interactionCreate'] = async function (
interaction,
) {
export const event: typeof bot.events.interactionCreate = async (interaction) => {
if (interaction.type === InteractionTypes.ApplicationCommand) {
if (!interaction.data) return
@@ -399,12 +397,12 @@ export const event: EventHandlers['interactionCreate'] = async function (
Now we need to create the `src/events/index.ts` file to collect all of our events and give it to the bot object.
```ts
import type { EventHandlers } from '@discordeno/bot'
import type { bot } from '../bot.js'
import { event as interactionCreateEvent } from './interactionCreate.js'
export const events = {
interactionCreate: interactionCreateEvent,
} as Partial<EventHandlers>
} as typeof bot.events
export default events
```
@@ -416,20 +414,18 @@ To tell Discordeno to run the events, we need another change. Go back to the `sr
```ts
import { createBot } from '@discordeno/bot'
import { config } from 'dotenv'
// insert-next-line
import events from './events/index.js'
config()
const bot = createBot({
export const bot = createBot({
token: process.env.TOKEN,
// remove-next-line
events: {},
// in this line we only use `events` but for javascript this will traduce to `events: events`
// insert-next-line
events,
})
// insert-next-line
bot.events = events
// ... REST OF THE FILE ...
```
@@ -607,48 +603,36 @@ This also applies to the `interaction.respond` function that we call. It too has
If you save and then run the bot, you might noticed that Discord still says that the application did not respond, but how is that possibile?
Although we just added the code to respond to the interaction, we have forgot a Discordeno concept called `desired properties`. This is an optimization Discordeno uses to make your code more performant but can be found annoying or unnecessary.
To explain how the `desired properties` work we need to talk about how Discord sends us data. Discord uses its own way to require/give data to who consumes the API. This guide won't go deep into this, but if you are interested can refer to the official documentation.
The way Discord sends us data is not the way that we (might) want it and for that reason Discordeno needs to map it from the Discord format to the Discordeno format. This is done via `transformers` defined in the `bot.transformers` object to tell Discordeno what we need from the pile of data Discord provides us.
Although we just added the code to respond to the interaction, we have forgot a Discordeno concept called `desired properties`. This is an optimization Discordeno uses to make your code more performant however it requires you do write some code. You can learn more on the [desired properties page](../desired-properties.md).
Looking through the code we have written so far we can see that
- We use `interaction.type` and `interaction.data` in the `src/events/interactionCreate.ts` file.
- We use `interaction.channelId`, `interaction.id`, `interaction.token`, and `role.id` in our command.
We need to add all of properties that we use to the `desired properties` list, and to do so we go back to `src/index.ts` and add a few lines:
We need to add all of properties that we use to the `desiredProperties` list, and to do so we go back to `src/index.ts` and add a few lines:
```ts
// REST OF YOUR CODE
const bot = createBot({
export const bot = createBot({
token,
events,
// insert-start
desiredProperties: {
interaction: {
id: true,
data: true,
type: true,
token: true,
channelId: true,
}
}
// insert-end
})
// insert-start
bot.transformers.desiredProperties.interaction.id = true
bot.transformers.desiredProperties.interaction.data = true
bot.transformers.desiredProperties.interaction.type = true
bot.transformers.desiredProperties.interaction.token = true
bot.transformers.desiredProperties.interaction.channelId = true
bot.transformers.desiredProperties.role.id = true
// insert-end
// REST OF YOUR CODE
```
:::tip
As said before the code you are creating is your code, and in being so you can structure it how you find it better for you. This means that if you don't like having to specify the `bot.transformers.desiredProperties` lines in your index.ts nothing is preventing you from moving them somewhere else and make your code call them in a way or another, a way is for example moving to a file apart and creating a function that will edit all the values.
:::
:::note
If you want, you can disable the `desired properties` with the `defaultDesiredPropertiesValue` option in the createBot object, it isn't recommended but it's there to allow the developer to choose, keep in mind this will give you a warning in the console when you run the bot and memory/cpu usage WILL be higher.
:::
If we try again now we'll finally see our message with 3 buttons. But if we click any of the buttons, they don't do anything! This is expected, since we did not write any code to handle buttons. So let's talk about how to react to users' interactions beyond just commands
### Handling interaction beyond commands
@@ -698,7 +682,7 @@ import ItemCollector from '../collector.js'
// insert-next-line
export const collectors = new Set<ItemCollector>()
export const event: EventHandlers['interactionCreate'] = async interaction => {
export const event: typeof bot.event.interactionCreate = async interaction => {
// insert-next-line
for (const collector of collectors) {
// insert-next-line
@@ -712,10 +696,6 @@ export const event: EventHandlers['interactionCreate'] = async interaction => {
In here you are defining a `Set` (for what we use, we can see it exactly the same as an array with a few helpful methods) of these collectors and when we receive an interaction from Discord we collect in all the collectors that have been added to then handle the interaction, so if the have just received the button click interaction we will now able to respond to it.
:::tip
As already said: you don't like how the collection is done in this example? You are free to change it and have fun in experimenting what you find to be the better way
:::
To do this we need to update the command, `src/events/roles.ts`:
```ts
@@ -777,16 +757,25 @@ You might remember from before that we discussed the desired properties, and we
```ts
// REST OF YOUR CODE
// insert-next-line
bot.transformers.desiredProperties.message.id = true
bot.transformers.desiredProperties.interaction.id = true
bot.transformers.desiredProperties.interaction.data = true
bot.transformers.desiredProperties.interaction.type = true
bot.transformers.desiredProperties.interaction.token = true
// insert-next-line
bot.transformers.desiredProperties.interaction.message = true
bot.transformers.desiredProperties.interaction.channelId = true
export const bot = createBot({
token,
desiredProperties: {
interaction: {
id: true,
data: true,
type: true,
token: true,
// insert-next-line
message: true,
channelId: true,
},
// insert-start
message: {
id: true,
}
// insert-end
}
})
// REST OF YOUR CODE
```
@@ -1382,23 +1371,34 @@ In this final piece of code, we use some desired properties. Let's go to the `sr
```ts
// REST OF YOUR CODE
// insert-next-line
bot.transformers.desiredProperties.user.id = true
bot.transformers.desiredProperties.message.id = true
bot.transformers.desiredProperties.interaction.id = true
bot.transformers.desiredProperties.interaction.data = true
bot.transformers.desiredProperties.interaction.type = true
// insert-next-line
bot.transformers.desiredProperties.interaction.user = true
bot.transformers.desiredProperties.interaction.token = true
bot.transformers.desiredProperties.interaction.message = true
// insert-next-line
bot.transformers.desiredProperties.interaction.guildId = true
bot.transformers.desiredProperties.interaction.channelId = true
bot.transformers.desiredProperties.role.id = true
export const bot = createBot({
token,
desiredProperties: {
interaction: {
id: true,
data: true,
type: true,
// insert-next-line
user: true,
token: true,
message: true,
// insert-next-line
guildId: true,
channelId: true,
},
message: {
id: true,
},
// insert-start
user: {
id: true,
},
role: {
id: true,
},
// insert-end
}
})
// REST OF YOUR CODE
```
@@ -1464,25 +1464,37 @@ And now let's add the desired properties. In the `src/index.ts` we need just a f
```ts
// REST OF YOUR CODE
bot.transformers.desiredProperties.user.id = true
bot.transformers.desiredProperties.message.id = true
// insert-next-line
bot.transformers.desiredProperties.member.roles = true
bot.transformers.desiredProperties.interaction.id = true
bot.transformers.desiredProperties.interaction.data = true
bot.transformers.desiredProperties.interaction.type = true
bot.transformers.desiredProperties.interaction.user = true
bot.transformers.desiredProperties.interaction.token = true
// insert-next-line
bot.transformers.desiredProperties.interaction.member = true
bot.transformers.desiredProperties.interaction.message = true
bot.transformers.desiredProperties.interaction.guildId = true
bot.transformers.desiredProperties.interaction.channelId = true
bot.transformers.desiredProperties.role.id = true
export const bot = createBot({
token,
desiredProperties: {
interaction: {
id: true,
data: true,
type: true,
user: true,
token: true,
// insert-next-line
member: true,
message: true,
guildId: true,
channelId: true,
},
message: {
id: true,
},
user: {
id: true,
},
role: {
id: true,
},
// insert-start
member: {
roles: true,
}
// insert-end
}
})
// REST OF YOUR CODE
```
+17 -18
View File
@@ -34,7 +34,7 @@ const bot = createBot({
intents: Intents.Guilds | Intents.GuildMessages, // Or other intents that you might needs.
events: {
ready: data => {
console.log(`The shard ${data.shardId} is ready!`)
bot.logger.info(`The shard ${data.shardId} is ready!`)
},
},
})
@@ -88,34 +88,33 @@ Discordeno's methods always perform one action only. A method will never call mu
## Understanding Desired Properties in Discordeno
:::tip
For more details checkout [the desired properties docs](./desired-properties.md)
:::
If we ran the code above (for example, by putting the code inside the ready event), the bot will send a message in the channel you specified (as long as it has the permissions to do so) with the content "Hello world. This is test message from Discordeno.". However, if we log to the console the message object that Discordeno return, we won't see many, if any, values on it. This is also the case for other events such as `MessageCreate`. But why is that? Well, this is because of a Discordeno feature called `Desired properties`.
Desired properties is a feature that reduce the memory usage of your application by removing properties that you don't use. For example, you might not be interested in knowing the topic of a channel, but Discord will always return it, therefore consuming more memory. This is why Discordeno requires you to explicitly set the properties that you want to keep and use.
You can set which property you want to keep in the `bot.transformers.desiredProperties` object. Discordeno will default everything to false and you can set the values you want to keep to true manually like this:
The main way to configure desired properties is using the `desiredProperties` object in `createBot`:
```ts
bot.transformers.desiredProperties.message.id = true
bot.transformers.desiredProperties.message.content = true
bot.transformers.desiredProperties.message.channelId = true
const bot = createBot({
// ... your exiting code
desiredProperties: {
message: {
id: true,
content: true,
channelId: true,
},
},
})
```
With the above 3 lines of code, we will be able to get the message ID, the channel ID and the message content\* of a specific message. The same thing can be said for the return object of `sendMessage` method and the `messageCreate` event
We can also set the properties we want to keep with `{ id: true, content: true, channelId: true }`, but that would require us to specify all others keys in the object, and doing that would get extremely annoying.
With the above, we will be able to get the message ID, the channel ID and the message content\* of messages, this means that if we now log the message object from before we will find these 3 values.
\*: As long as the required privileged intent is enabled.
:::danger[Changing the default for Desired Properties]
THIS IS NOT RECOMMENDED IF YOU PLAN TO SHIP YOUR BOT TO PRODUCTION.
While not recommended, you can add `defaultDesiredPropertiesValue: true` to the first parameter object of the `createBot` function. This will set every desired property to true by default (you can still disable some if you want to). The reason why this is not recommended and considered deprecated is because while Desired Properties DO slow you down during development (needing to make sure you aren't using something that you won't have at runtime), they have a significant performance impact on both CPU and memory usage.
:::
:::warning[Typescript and Desired Properties]
We are aware that TypeScript has no idea which properties will be missing. We are working on fixing this issue.
:::
## Additional information on Discordeno
Here are some nice to know things about Discordeno that you might be interested, though some may be for more advanced use-cases.