From fce4ae01c3656e4b94f4128a3d9798e60da4b1d0 Mon Sep 17 00:00:00 2001 From: Awesome Stickz Date: Sun, 7 Jul 2024 21:06:16 +0530 Subject: [PATCH] docs: update big bot guide (#3718) --- website/docs/bigbot/step-1-decisions.md | 6 +- website/docs/bigbot/step-2-rest.md | 41 ++-- website/docs/bigbot/step-3-gateway.md | 221 ++++++++++++------ website/docs/bigbot/step-4-bot.md | 45 ++-- .../bigbotguide/GatewayFlowChart.tsx | 155 ++++++++++++ 5 files changed, 349 insertions(+), 119 deletions(-) create mode 100644 website/src/components/bigbotguide/GatewayFlowChart.tsx diff --git a/website/docs/bigbot/step-1-decisions.md b/website/docs/bigbot/step-1-decisions.md index 01c8f5593..5fc6c1ebd 100644 --- a/website/docs/bigbot/step-1-decisions.md +++ b/website/docs/bigbot/step-1-decisions.md @@ -9,10 +9,10 @@ This guide is going to skip the basics as it expects you to have the ability to ## Choose A Runtime -Discordeno supports several platforms including Node.JS, Bun, and Deno. This means you get to make the fun decision of choosing which runtime to go with. As my personal recommendation at the time of writing this guide I would highly recommend you chose Node.JS +Discordeno supports several platforms including Node.JS, Bun, and Deno. This means you get to make the fun decision of choosing which runtime to go with. As my personal recommendation at the time of writing this guide, I would highly recommend you chose Node.JS -This guide will proceed with Node.JS but you should apply the same concepts in your own runtime. Should you need help with another runtime, please contact us on Discord. +This guide will proceed with Node.JS but you should apply the same concepts in your own runtime. Should you need help with another runtime, please contact us on [Discord](https://discord.gg/ddeno). ## Choose A Communication System -One of the main things to decide, is how you will communicate across your separater processes. Some developers like to use redis pubsub. Others prefer rabbitmq. There are too many systems available to cover in this guide so we will be simply opting for one solution at this time. You should look to optimize this for your specific bot as this is a critical bottleneck in handling events. +One of the main things to decide, is how you will communicate across your separated processes. Some developers like to use Redis Pub/Sub, some prefer RabbitMQ. There are too many systems available to cover in this guide so we will be simply opting for one solution at this time - HTTP Requests. You should look to optimize this for your specific bot, as this is a critical bottleneck in handling events. diff --git a/website/docs/bigbot/step-2-rest.md b/website/docs/bigbot/step-2-rest.md index 776034136..9e48dbee8 100644 --- a/website/docs/bigbot/step-2-rest.md +++ b/website/docs/bigbot/step-2-rest.md @@ -10,7 +10,7 @@ Awesome, if you have reached this far you know that this guide will be using the - Node.JS Runtime - Class based approach -Remember if you need any help with an alternative to the above listed items, please contact us on Discord. +Remember if you need any help with an alternative to the above listed items, please contact us on [Discord](https://discord.gg/ddeno). ## Creating Your REST Manager @@ -40,6 +40,7 @@ Now you can make another file like `services/rest/index.ts`. Then paste the code ```ts import dotenv from 'dotenv' import express from 'express' + dotenv.config() import { REST } from './rest.ts' @@ -51,7 +52,7 @@ const app = express() app.use( express.urlencoded({ extended: true, - }) + }), ) app.use(express.json()) @@ -82,10 +83,10 @@ app.listen(REST_PORT, () => { }) ``` -Now let's take a minute to explain this part very briefly. The majority of this code is about creating a http listener in Node.JS to listen for requests coming into this process. When any request comes in it first checks if the authorization header matches. This is a small security challenge, to help prevent unknown users from making requests with your token should they find the url your hosting this listener on. This guide, uses `dotenv` package to handle private secrets but you can use anything you like. +Now let's take a minute to explain this part very briefly. The majority of this code is about creating a http listener in Node.JS to listen for requests coming into this process. When any request comes in it first checks if the authorization header matches. This is a small security challenge, to help prevent unknown users from making requests with your token should they find the url your hosting this listener on. This guide uses `dotenv` package to handle private secrets but you can use anything you like. :::tip -Take the time to go back to the rest.ts file we made earlier and adjust the `token` property to use dotenv as well, should you want to optimize your code. +Take the time to go back to the `rest.ts` file we made earlier and adjust the `token` property to use dotenv as well, should you want to optimize your code. ::: If a request was not authorized, it will be ignored. If a request comes with a proper authorization header, we will proceed to making the request. This request is forwarded to our REST we made earlier in rest.ts file. We add on the discord base url to the route and forward it. The manager will handle all rate limits and queues and anything else to make the request. When it responds, it will either return a valid response or an error. This successful response or error is than handled as needed and sent back to original process that called this listener. @@ -139,7 +140,7 @@ Now, make sure to scroll to this line as we are going to work around this line n const result = await REST.makeRequest( req.method, `${REST.baseUrl}${req.url}`, - req.body + req.body, ) ``` @@ -150,12 +151,12 @@ Influx?.writePoint( .stringField('type', 'REQUEST_FETCHING') .tag('method', options.method) .tag('url', options.url) - .tag('bucket', options.bucketId ?? 'NA') + .tag('bucket', options.bucketId ?? 'NA'), ) const result = await REST.makeRequest( req.method, `${REST.baseUrl}${req.url}`, - req.body + req.body, ) ``` @@ -168,12 +169,12 @@ Influx?.writePoint( .stringField('type', 'REQUEST_FETCHING') .tag('method', options.method) .tag('url', options.url) - .tag('bucket', options.bucketId ?? 'NA') + .tag('bucket', options.bucketId ?? 'NA'), ) const result = await REST.makeRequest( req.method, `${REST.baseUrl}${req.url}`, - req.body + req.body, ) Influx?.writePoint( new Point('restEvents') @@ -183,7 +184,7 @@ Influx?.writePoint( .tag('url', options.url) .tag('bucket', options.bucketId ?? 'NA') .intField('status', response.status) - .tag('statusText', response.statusText) + .tag('statusText', response.statusText), ) ``` @@ -214,15 +215,15 @@ Now you will be able to take this data and implement it into Grafana. In a futur ### Multiple Custom Bot Proxy Rest -For bot's that allow servers to buy custom bot's, you can create a separate manager for each bot's token/authorization. As a request comes in, either get a cached rest manager or create one if none exists in the cache. +For bots that allow servers to buy custom bots, you can create a separate manager for each bot's token/authorization. As a request comes in, either get a cached rest manager or create one if none exists in the cache. -The plan in this guide is to create a custom header that is sent on every request to the rest process. This will contain the custom instances bot id so we can find it a collection on the rest process, which can be then be used to determine which bot token we will use. +The plan in this guide is to create a custom header that is sent on every request to the rest process. This will contain the custom instances' bot id, so we can find it a collection on the rest process, which can be then be used to determine which bot token we will use. :::caution -Having multiple bot's sending requests from one source will impact your global rate limit due to the global ip rate limit. +Having multiple bots sending requests from one source will impact your global rate limit due to the global ip rate limit. ::: -In order to send the bot id inside of the request headers we first have to override the `createBaseHeaders()` function. +In order to send the bot id inside of the request headers we first have to override the `createBaseHeaders()` function in our `services/bot/bot.ts` file. ```ts BOT.rest = createRestManager({ @@ -237,7 +238,7 @@ BOT.rest.createBaseHeaders = () => { } ``` -Create this MANAGERS collection somewhere near the top of the file. Then we can begin implementing this in our request handler. +Create this MANAGERS collection somewhere near the top of your `services/rest/rest.ts` file. Then we can begin implementing this in our request handler. ```ts const MANAGERS = new Collection() @@ -273,7 +274,7 @@ try { ### Evals -One of the last things we should do, is make it possible to run commands on this process. To do this, we simply create a small bot on this process with an eval command that listens for our messages only on our developer server. This way we can dynamically update any properties we may need to. For example, if discord updates the API version, we can easily switch the api version with a simple command. +One of the last things we should do, is make it possible to run commands on this process. To do this, we simply create a small bot on this process with an eval command that listens for our messages only on our developer server. This way we can dynamically update any properties we may need to. For example, if discord updates the API version, we can easily switch the api version with a simple command without having to restart our rest service. Let's make a small bot on this process. Make a file called `services/rest/bot.ts`. Then paste the code below. @@ -313,7 +314,7 @@ const bot = createBot({ } const response = ['```ts'] - const regex = new RegExp(Gamer.token, 'gi') + const regex = new RegExp(process.env.token, 'gi') if (result && typeof result.then === 'function') { // We returned a promise? @@ -327,13 +328,13 @@ const bot = createBot({ util .inspect(value, inspectOptions) .replace(regex, 'YOU WISH!') - .substring(0, 1985) + .substring(0, 1985), ) } else { response.push( String(util.inspect(result)) .replace(regex, 'YOU WISH!') - .substring(0, 1985) + .substring(0, 1985), ) } @@ -345,4 +346,4 @@ const bot = createBot({ }) ```` -Now that you have an eval command available on ur `REST` service, whenever you need to modify something quickly you can easily do so from ur developer server where this bot is. For example, should you want to switch to a newer api version, it is as simple as `.eval REST.version = xxx` where xxx is the new API version. +Now that you have an eval command available on ur `REST` service, whenever you need to modify something quickly, you can easily do so from ur developer server where this bot is. For example, should you want to switch to a newer api version, it is as simple as `.eval REST.version = xxx` where xxx is the new API version. diff --git a/website/docs/bigbot/step-3-gateway.md b/website/docs/bigbot/step-3-gateway.md index 8d54d108a..542b5085d 100644 --- a/website/docs/bigbot/step-3-gateway.md +++ b/website/docs/bigbot/step-3-gateway.md @@ -3,6 +3,9 @@ sidebar_position: 4 sidebar_label: Step 3 - Gateway --- +import BrowserOnly from '@docusaurus/BrowserOnly' +import FlowChart from '@site/src/components/bigbotguide/GatewayFlowChart' + # Standalone Gateway Sweet, it is time to start our gateway code. By now, you should have already built your rest process, as we will need it shortly. The gateway portion is the hardest and most complex part of making a bot. This is where most of your time will be spent to optimize your setup. @@ -20,11 +23,19 @@ In Discordeno, we have 2 main portions of the gateway system. We have what we ca Let's say the bot process needs to execute some code on some shard such as fetching members, changing bot's status, or anything else. The ideal way to do this is the bot process sends a request to the gateway manager process which sends a request to the shard process which can send it back to the bot process directly. All of this will help make it easily scale horizontally, which we will start to see below as we code. +## Understanding the Flow + +In this example, we're proceeding with the understanding that we have 5,000 shards, 5,000,000 Discord servers, which we'll be scaling horizontally across 10 separate dedicated servers with 500 shards each. Each server will have 50 worker threads that contain 10 shards each to split the load evenly among all of the servers properly. Take a look at the following diagram to get a better understanding of this: + +{() => } + +Now let's proceed with the next steps. + ## Creating The Manager ## Connecting To REST -Before we begin making a gateway manager, we need to first prepare a connection to our REST manager. Go ahead and make a file called `services/gateway/rest/index.ts`. +Before we begin making a gateway manager, we need to first prepare a connection to our REST manager. Go ahead and make a file called `services/gateway/rest.ts`. ```ts import { createRestManager } from '@discordeno/rest' @@ -43,12 +54,12 @@ The `baseUrl` should be pointed at the server where you are hosting your REST ma ## Preparing Our Gateway Manager -We are going to proceed with the understanding that we have 5,000 shards _5,000,000 servers_. Let's make a file called `services/gateway/manager.ts` +We are going to proceed with the understanding that we have 5,000 shards, 5,000,000 Discord servers. Let's make a file called `services/gateway/manager.ts` ```ts import { createGatewayManager } from '@discordeno/gateway' import { logger } from '@discordeno/utils' -import { REST } from '../rest/index.ts' +import { REST } from '../rest.ts' export const GATEWAY = createGatewayManager({ token: process.env.TOKEN, @@ -65,7 +76,7 @@ Now let's break it down. ### Worker & Server Confusion -The `shardsPerWorker` property represents how many shards we will run per **server**. This property is called `perWorker` because for mid sized bots that don't require separate dedicated servers it uses _worker threads_ to mitigate the load. Here we are going to be aiming to scale much much larger so we need to think bigger. In our case, what we are telling our gateway manager, is that it should create 500 shards per **server**. Sounds like a lot? Yes, but no problem! Those shards will then be split across _worker threads_ on each server. +The `shardsPerWorker` property represents how many shards we will run per **server** in this eaxmple. This property is called `perWorker` because for mid sized bots that don't require separate dedicated servers, it can use _worker threads_ to mitigate the load in that single server. Here, we are going to be aiming to scale much much larger so we need to think bigger. In our case, what we are telling our gateway manager is that it should create 500 shards per **server**. Sounds like a lot? Yes, but no problem! Those shards will then be split across _worker threads_ on each server. The `totalWorkers` property represents the number of **servers** we have available for shards. For example, if we have 10 dedicated servers available to us, this will allow the manager to spread out the load across 10 total **servers**. @@ -81,6 +92,7 @@ Let's make a file called `services/gateway/index.ts` and paste the following cod import { logger } from '@discordeno/utils' import dotenv from 'dotenv' import express from 'express' + dotenv.config() const AUTHORIZATION = process.env.AUTHORIZATION as string @@ -90,7 +102,7 @@ const app = express() app.use( express.urlencoded({ extended: true, - }) + }), ) app.use(express.json()) @@ -108,7 +120,7 @@ app.all('/*', async (req, res) => { } default: logger.error( - `[Shard] Unknown request received. ${JSON.stringify(req.body)}` + `[Shard] Unknown request received. ${JSON.stringify(req.body)}`, ) return res .status(404) @@ -142,7 +154,7 @@ GATEWAY.tellWorkerToIdentify = async function (workerId, shardId, bucketId) { const url = process.env[`SERVER_URL_${workerId}`] if (!url) return logger.error( - `No server URL found for server #${workerId}. Unable to start Shard #${shardId}` + `No server URL found for server #${workerId}. Unable to start Shard #${shardId}`, ) await fetch(url, { @@ -160,8 +172,7 @@ GATEWAY.tellWorkerToIdentify = async function (workerId, shardId, bucketId) { GATEWAY.spawnShards() ``` -Here, we are overriding the built in method on the gateway manager called `tellWorkerToIdentify`. Internally, this function just simply starts a new shard as by default the lib supports small bots. For our case, we are going to make it get the server url from a `.env` file -and then send the request to identify it. +Here, we are overriding the built in method on the gateway manager called `tellWorkerToIdentify`. Internally, this function just simply starts a new shard, as by default, the lib supports small bots. For our case, we are going to make it get the server url from a `.env` file and then send the request to identify it. :::tip For the purposes of the guide, we are using `fetch` to communicate between servers but you can use any communication system you prefer. I highly recommend taking the time to optimize this portion with a more performant communication system. Ideal recommendation would be gRPC. @@ -171,37 +182,34 @@ Now, let's go ahead and set up the server where we will receive this and start a ### Setting Up Sharder -Just like before, we are going to make another http listener to listen for incoming events and delegate them outwords. Make a file called `services/gateway/sharding/index.ts` +Now, we need to setup a sharder process to spawn all our shards in each server. Since we will have 500 shards in each server, we need to split them evenly with `worker_threads`, so we'll be setting up a master process and worker for our sharder. + +### Setting Up Sharding Master Process + +Just like before, we are going to make another http listener to listen for incoming requests from the gateway manager and delegate them outwards to different workers. Make a file called `services/gateway/sharding/index.ts` ```ts -import { DiscordenoShard } from '@discordeno/gateway' -import { logger } from '@discordeno/utils' -import { Intents } from '@discordeno/types' -import events from './events.js' import dotenv from 'dotenv' import express from 'express' +import { Worker } from 'worker_threads' + dotenv.config() const AUTHORIZATION = process.env.AUTHORIZATION as string -const SHARDS = new Collection() + +// Create workers +const WORKERS = new Collection() const app = express() app.use( express.urlencoded({ extended: true, - }) + }), ) app.use(express.json()) -function getUrlFromShardId(totalShards: number, shardId: number) { - const urls = process.env.EVENT_HANDLER_URLS?.split(',') ?? [] - const index = totalShards % shardId - - return urls[index] ?? urls[0] -} - app.all('/*', async (req, res) => { if (!AUTHORIZATION || AUTHORIZATION !== req.headers.authorization) { return res.status(401).json({ error: 'Invalid authorization key.' }) @@ -211,40 +219,26 @@ app.all('/*', async (req, res) => { // Identify A Shard switch (req.body.type) { case 'IDENTIFY_SHARD': { - logger.info( - `[Shard] identifying ${ - SHARDS.has(req.body.shardId) ? 'existing' : 'new' - } shard (${req.body.shardId})` - ) - const shard = - SHARDS.get(req.body.shardId) ?? - new DiscordenoShard({ - id: req.body.shardId, - connection: { - compress: req.body.compress, - intents: req.body.intents, - properties: req.body.properties, - token: req.body.token, - totalShards: req.body.totalShards, - url: req.body.url, - version: req.body.version, - }, - // TODO: Enable this in the next portion of the guide. - // events, - }) + const workerId = Math.floor(req.body.shardId / 10) + let worker = WORKERS.get(workerId) - SHARDS.set(shard.id, shard) - await shard.identify() - return res.status(200).json({ - identified: true, - shardId: req.body.shardId, - workerId: process.env.WORKER_ID, - }) + if (!worker) { + worker = new Worker('./worker.js', { workerData: { workerId } }) + + WORKERS.set(workerId, worker) + } + + worker.postMessage(req.body) + + return res.status(200).send() } default: logger.error( - `[Shard] Unknown request received. ${JSON.stringify(req.body)}` + `[Sharding Master] Unknown request received. ${JSON.stringify( + req.body, + )}`, ) + return res .status(404) .json({ message: 'Unknown request received.', status: 404 }) @@ -256,14 +250,93 @@ app.all('/*', async (req, res) => { }) app.listen(process.env.SHARD_SERVER_PORT, () => { - console.log(`Listening at ${process.env.SERVER_URL}`) + console.log(`[Sharding Master] Listening at ${process.env.SERVER_URL}`) }) ``` -Most of this code is another http listener again. The part we are going to focus on is the part after the authorization check. Each request is past into a switch statement which determines which type of request to handle. If it is the `identify` request, it will begin identifying the shard. First it checks if an existing shard exists and triggers identify on that which will internally handle this cleanly by closing existing shard, and opening a new one. If no shard exists, we create this shard and save it to our SHARDS cache. Using this method, you can support many different types of communication between your gateway manager and your shards. +Most of this code is another http listener again. The part we are going to focus on is the part after the authorization check. Each request is passed into a switch statement which determines which type of request to handle. If it is the `identify` request, it will send that request to the corresponding worker by finding the worker id responsible for this shard and then getting the worker object from the `WORKERS` collection or spawning the worker if it doesn't already exist, and then it forwards the request to that worker. + +### Setting Up Sharding Worker Process + +Now that we have our sharding master process ready, create a file called `services/gateway/sharding/worker.ts` for it to spawn and forward requests to. + +```ts +import { DiscordenoShard } from '@discordeno/gateway' +import { logger } from '@discordeno/utils' +import { Intents } from '@discordeno/types' +import { parentPort, workerData } from 'worker_threads' +import dotenv from 'dotenv' + +dotenv.config() + +if (!parentPort) throw new Error('Parent port is null') + +const SHARDS = new Collection() + +function getUrlFromShardId(totalShards: number, shardId: number) { + const urls = process.env.EVENT_HANDLER_URLS?.split(',') ?? [] + const index = totalShards % shardId + + return urls[index] ?? urls[0] +} + +parentPort.on('message', async data => { + try { + switch (data.type) { + // Identify A Shard + case 'IDENTIFY_SHARD': { + logger.info( + `[Sharding Worker #${workerData.workerId}] identifying ${ + SHARDS.has(data.shardId) ? 'existing' : 'new' + } shard (${data.shardId})`, + ) + + const shard = + SHARDS.get(data.shardId) ?? + new DiscordenoShard({ + id: data.shardId, + connection: { + compress: data.compress, + intents: data.intents, + properties: data.properties, + token: data.token, + totalShards: data.totalShards, + url: data.url, + version: data.version, + }, + // Enable this in the next portion of the guide. + // events, + }) + + SHARDS.set(shard.id, shard) + + await shard.identify() + } + default: + logger.error( + `[Sharding Worker #${ + workerData.workerId + }] Unknown request received. ${JSON.stringify(data)}`, + ) + } + } catch (error: any) { + console.log(error) + } +}) + +console.log( + `[Sharding Worker #${workerData.workerId}] Sharding Worker Started.`, +) +``` + +Here, we listen to the master process for message event, through which we'll receive requests to identify shards or any other requests from the gateway manager. Each message is passed into a switch statement which determines which type of request to handle. If it is the `identify` request, it will begin identifying the shard. First it checks if an existing shard exists and triggers identify on that which will internally handle this cleanly by closing existing shard, and opening a new one. If no shard exists, we create this shard and save it to our SHARDS cache. Using this method, you can support many different types of communication between your gateway manager and your shards. Next, we will focus on the `events` portion which we had commented out above. Each shard handles many events and this will be the portion where we tell the shard how to handle those events. For this guide, we will only cover the `message` event, but you can implement any other events you require as you need following the same method. +:::info +Shard events are NOT the same as your regular bot events. **All** your bot events will be received on the `shard.events.message` function. +::: + ```diff const shard = SHARDS.get(req.body.shardId) ?? @@ -298,12 +371,12 @@ const shard = Now, whenever the shard gets an event, it will send that payload to the event listener url. The event listener is also known as the "bot". :::tip -If you have the $, I would recommend writing the event listener(bot), in a serverless friendly manner. If that code is deployed to something like cloudflare workers, your bot will have unlimited scalability and your shards would simply route these events to be sent there. +If you have the money, I would recommend writing the event listener (bot), in a serverless friendly manner. If that code is deployed to something like cloudflare workers, your bot will have unlimited scalability and your shards would simply route these events to be sent there. ::: At this point, what we need next is the bot process which is listening to these events. However, before we go make that, let us take some time and really improve our gateway logic a bit more. -### Analytics +## Analytics There is a lot of analytics you can build here. But we will specifically cover just the analytics for a couple of things. You should take the time to implement much more of the analytics. Go ahead and make a file: `services/gateway/analytics.ts` @@ -337,7 +410,7 @@ if (!saveAnalyticsId) { } ``` -Now we can begin implementing influxdb in our sharder. Go back to `services/gateway/sharding/index.ts`. +Now, we can begin implementing influxdb in our sharder. Go back to `services/gateway/sharding/worker.ts`. ```ts events: { @@ -389,21 +462,21 @@ events: { }, ``` -### Further Optimizations +## Further Optimizations There are a few things we can improve in our gateway proxy, should we require certain features in our bot. You can choose to implement these should you need them. -#### GUILD_LOADED +### GUILD_LOADED -This is for bots who need to take certain actions when the bot is added to or removed from a server. There are a few different things to keep in mind here and its important for you to understand, whether you need to have **GUILD_LOADED_DD** added or if you dont need it added. Discord is really odd when it comes to **GUILD_CREATE** and **GUILD_DELETE** events. **GUILD_CREATE** event are emitted in various different circumstances. +This is for bots who need to take certain actions when the bot is added to or removed from a server. There are a few different things to keep in mind here and its important for you to understand whether you need to have **GUILD_LOADED_DD** added or not. Discord is really odd when it comes to **GUILD_CREATE** and **GUILD_DELETE** events. **GUILD_CREATE** event are emitted in various different circumstances: - A guild was added - Shard resumed - Unavailable guild became available - Initial loading when connecting to discord -- Insert anything i didnt think of here etc... +- Insert anything I didn't think of here... -Let's start by creating a small local cache at the top of the file. +Due to this, we can't find if a **GUILD_CREATE** event was sent because the bot was added to a guild or not without a few more steps. Let's start by creating a small local cache at the top of the file. ```ts const cache = { @@ -412,7 +485,7 @@ const cache = { } ``` -Next, let's handle the fact that some guilds are actually new guilds the bot gets added to. To make this possible, we need to store which guilds already have the bot in it when it starts. A list of guild ids are sent in the READY event telling the bot that the bot is already in these guilds. So let's store them to cache. +Next, let's handle the fact that some guilds are actually new guilds the bot gets added to. To make this possible, we need to store which guilds already have the bot in it when it starts. A list of guild ids are sent in the READY event, telling the bot that the bot is already in these guilds. So let's store them to cache. ```ts events: { @@ -443,7 +516,7 @@ events: { }, ``` -Now we need to make sure that this will handle it correctly by changing any guild creates that are not new guilds, to a private event. +Now, we need to make sure that this will handle it correctly by changing any guild creates that are not new guilds, to a private event. ```ts if (payload.t === 'READY') { @@ -471,7 +544,7 @@ if (payload.t === 'GUILD_CREATE') { } ``` -This will make it so whenever a GUILD_CREATE arrives from the initial batch it will rename the event to **GUILD_LOADED_DD**. Any other **GUILD_CREATE** that arrive can be safely ignored as those are just guilds being resumed or becoming available. Should you need these events feel free to edit the code above and create custom events just as **GUILD_CREATE_RESUMED** or **GUILD_CREATE_AVAILABLE**. This way when your bot receives a **GUILD_CREATE**, it will be automatically known that this is the bot being added to a new guild. +This will make it so whenever a **GUILD_CREATE** arrives from the initial batch from the **READY** event, it will rename the event to **GUILD_LOADED_DD**. Any other **GUILD_CREATE** that arrive can be safely ignored, as those are just guilds being resumed or becoming available. Should you need these events, feel free to edit the code above and create custom events just as **GUILD_CREATE_RESUMED** or **GUILD_CREATE_AVAILABLE**. This way, when your bot receives a **GUILD_CREATE**, it will be automatically known that this is the bot being added to a new guild. One last bit before you are done, simply add the following to make it ignore any useless **GUILD_DELETE** events as well. You can also choose rename it should you like to something like **GUILD_UNAVAILABLE**. @@ -481,14 +554,14 @@ if (payload.t === 'GUILD_DELETE') { } ``` -#### MESSAGE_UPDATE +### MESSAGE_UPDATE One other area where we can optimize is for the **MESSAGE_UPDATE** event, assuming you have the MessageContent intent enabled. You can save a ton of your CPU for gateway and bot by adding this in. This event can spam for no reason whatsoever and we can use the following code to ignore useless events. For example, any message that is sent with an embed will have a - MESSAGE_CREATE - MESSAGE_UPDATE -The update event is sent just to reflect that this message had an embed in it. This is how discord loads embeds by internally editing it and that sends a MESSAGE_UPDATE event out. Imagine every time your bot sends a message you are processing double the event load. Further, it's not just your bot but every bot. Even when a bot farm or abusive user decides to make self bots send embeds it will do the same. +The update event is sent just to reflect that this message had an embed in it. This is how discord loads embeds by internally editing it and that sends a MESSAGE_UPDATE event out. Imagine every time your bot sends a message, you are processing double the event load. Further, it's not just your bot but every bot. Even when a bot farm or abusive user decides to make self bots send embeds, it will do the same. It gets even worse when you realize that any `link` in a message will trigger the same. So if a user sends a message with 5 urls. That will trigger: @@ -507,7 +580,7 @@ Then let's say the user actually edits the message just a tiny bit. - MESSAGE_UPDATE - MESSAGE_UPDATE -Imagine having to process all these events, sending them through your queue system and causing a waste of CPU processing power. +Imagine having to process all these events, sending them through your queue system and causing a waste of CPU processing power so let's go ahead and change our code to: ```ts if (payload.t === 'MESSAGE_UPDATE') { @@ -539,20 +612,20 @@ We should take the time here to implement a small queue where we can store event RabbitMQ setup guide here. -### Resharding +## Resharding -Now let's enable resharding on our bot so we don't need to deal with it. Remember, Discord stops allowing your bot to be added to new servers when you max out your existing max shards. Consider a bot started with 150 shards operating on 150,000 servers. Your shards support a maximum of 150 \* 2500 = 375,000 servers. Your bot will be unable to join new servers once it reaches this point until it re-shards. Discordeno proxy provides 2 types of re-sharding. Automated and manual. You can also have both. +Now let's enable resharding on our bot so we don't need to deal with it. Remember, Discord stops allowing your bot to be added to new servers when you max out your existing max shards. Consider a bot started with 150 shards operating on 150,000 servers. Your shards support a maximum of 150 \* 2500 = 375,000 servers. Your bot will be unable to join new servers once it reaches this point until it re-shards. Discordeno provides 2 types of re-sharding: Automated and Manual. You can also have both. - Automated: This system will automatically begin a Zero-downtime resharding process behind the scenes when you reach 80% of your maximum servers allowed by your shards. For example, since 375,000 was the max, at 300,000 we would begin re-sharding behind the scenes with ZERO DOWNTIME. - 80% of maximum servers reached (The % of 80% is customizable.) - Identify limits have room to allow re-sharding. (Also customizable) -- Manual: You can also trigger this manually should you choose. +- Manual: You can also trigger this manually, should you choose. - When discord releases a new API version, updates your gateways to new version with no downtime. -### Evals +## Evals -One of the last things we should do, is make it possible to run commands on this process. To do this, we simply create a small bot on this process with an eval command that listens for our messages only on our developer server. This way we can dynamically update any properties we may need to. For example, if discord updates the API version, we can easily switch the api version with a simple command. +One of the last things we should do, is make it possible to run commands on this process. To do this, we simply create a small bot on this process with an eval command that listens for our messages only on our developer server. This way, we can dynamically update any properties we may need to. For example, if discord updates the API version, we can easily switch the api version with a simple command. -Please review the Evals section in Step 2 - REST portion of this guide to see how we made an eval in that process. The entire setup would be repeated here for the bot portion as well. That way you can dynamically change anything in your gateway. +Please review the [Evals section in Step 2 - REST](./step-2-rest.md#evals) portion of this guide to see how we made an eval in that process. The entire setup would be repeated here for the gateway portion as well. That way, you can dynamically change anything in your gateway, should you need. diff --git a/website/docs/bigbot/step-4-bot.md b/website/docs/bigbot/step-4-bot.md index 9fa7ea36e..15a4e51c8 100644 --- a/website/docs/bigbot/step-4-bot.md +++ b/website/docs/bigbot/step-4-bot.md @@ -5,10 +5,10 @@ sidebar_label: Step 4 - Bot # Event Handler (Bot) -Woah! You go through the most difficult part already which was the gateway. WOOT! Let's go ahead and jump into the event handler portion so we can proceed. The event handler will be called as the `bot` process going forward. It's purpose is generally to listen for events coming from the shards and process them accordingly. +Woah! You went through the most difficult part already which was the gateway. WOOT! Let's go ahead and jump into the event handler portion so we can proceed. The event handler will be called as the `bot` process going forward. Its purpose is generally to listen for events coming from the shards and process them accordingly. :::tip -If you have the $, I would recommend writing the event listener(bot), in a serverless friendly manner. If that code is deployed to something like cloudflare workers, your bot will have unlimited scalability and your shards would simply route these events to be sent there. +If you have the money, I would recommend writing the event listener(bot), in a serverless friendly manner. If that code is deployed to something like cloudflare workers, your bot will have unlimited scalability and your shards would simply route these events to be sent there. ::: ## Creating Bot Manager @@ -30,7 +30,7 @@ Now that our bot manager is created, we need to implement our event handlers. Fi import type { EventHandlers } from '@discordeno/bot' export const events: Partial = { - // TODO: fill this in the next section + // Fill this in the next section } ``` @@ -46,7 +46,7 @@ export const BOT = createBot({ }) ``` -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. +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) { @@ -54,7 +54,7 @@ export const ready: EventHandlers['ready'] = async function (payload, shardId) { } ``` -Now that we have a ready event handler. Let's go ahead and add it to our events. +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' @@ -74,6 +74,7 @@ Once, again we are going to create a quick http listener that will listen for ev ```ts import dotenv from 'dotenv' import express from 'express' + dotenv.config() const AUTHORIZATION = process.env.AUTHORIZATION as string @@ -83,7 +84,7 @@ const app = express() app.use( express.urlencoded({ extended: true, - }) + }), ) app.use(express.json()) @@ -94,7 +95,7 @@ app.all('/*', async (req, res) => { } try { - // TODO: Add the code here in next portion + // Add the code here in the next section res.status(200).json({ success: true }) } catch (error: any) { console.log(error) @@ -107,22 +108,22 @@ app.listen(BOT_PORT, () => { }) ``` -Now that we have the basic code setup complete for our listener. We can begin adding the code necessary for handling the events. +Now that we have the basic code setup complete for our listener, we can begin adding the code necessary for handling the events. ```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); + // 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); - res.status(200).json({ success: true }) - } + res.status(200).json({ success: true }) +} ``` ## Connecting To REST -Alright, now we need to start making our connection to the rest proxy work. That way when our bot needs to make a rest request, it will use the proxy. +Alright, now we need to start making our connection to the rest proxy work. That way, when our bot needs to make a rest request, it will use the proxy. ```ts export const BOT = createBot({ @@ -141,13 +142,13 @@ BOT.rest = createRestManager({ ## Communication With Gateway -This portion of the guide is only necessary for the bots that require sending a request to their gateway manager. For example, should your bot need to connect to a voice channel, edit it's status, or fetch members through the gateway; it will need to send a request to the gateway manager process. +This portion of the guide is only necessary for the bots that require sending a request to their gateway manager. For example, should your bot need to connect to a voice channel, edit its status, or fetch members through the gateway, it will need to send a request to the gateway manager process. :::tip -Take the time to implement this properly should you need it with something like websocket, IPC, gRPC, rabbitMQ etc... +Take the time to implement this properly, should you need it, with something like WebSocket, IPC, gRPC, RabbitMQ etc. ::: -To tell, the Bot that we need to make a request to the proxy gateway manager what we do is override the gateway in the bot. +To tell the bot that we need to make a request to the proxy gateway manager, what we do is override the gateway in the bot. ```ts BOT.gateway.requestMembers = async function (guildId, options) { @@ -176,11 +177,11 @@ This will now send a request to our gateway manager whenever the bot makes a req ## Optimizing For Scale -Now think back at how many shards we created in this guide. We had built this with the idea that we had 5,000 shards, in other words 5,000,000 servers. This means that there are 5,000 shards or 5,000,000 servers sending events all to this one little bot listener. This needs to scale much better. So we have several options available. Remember if you went the serverless route, none of this is needed because that is already scaled. However, should you not have the $ to afford serverless infrastructure, we will make it work. +Now think back at how many shards we created in this guide. We had built this with the idea that we had 5,000 shards, in other words 5,000,000 servers. This means that there are 5,000 shards or 5,000,000 servers sending events all to this one little bot listener. This needs to scale much better. So we have several options available. Remember if you went the serverless route, none of this is needed because that is already scaled. However, should you not have the money to afford serverless infrastructure, we will make it work. ### Threading -Threading or workers or clusters however you wish to call it can be used here. You can take the time to implement a main thread and child threads to be created and have the workload be delegated to the thread. You should look into using something that can provide you a thread pool to optimizing the thread load management. However, there is another option which is server splitting. +Threading or workers or clusters, however you wish to call it can be used here. You can take the time to implement a main thread and child threads to be created and have the workload be delegated to the thread. You should look into using something that can provide you a thread pool to optimizing the thread load management. However, there is another option, which is, server splitting. ### Server Splitting @@ -203,6 +204,6 @@ function getUrlFromShardId(totalShards: number, shardId: number) { } ``` -This function is simply making it so that it determines what the event handler url should be where that specific shard should send the event to. This means if you have 5,000 shards and you receive an event in shard #4565 and you had 10 server urls in the configs. This would make this event be sent to the 5th url in the array. +This function is simply making it so that it determines what the event handler url should be and where that specific shard should send the event to. This means if you have 5,000 shards and you receive an event in shard #4565 and you had 10 server urls in the configs, this would make this event be sent to the 5th url in the array. -Now that all your processes are fully functioning, we can get into the nitty gritty part of finally beginning to code our features/commands/etc... +Woohoo! You've reached the end of this guide. By this point, you'll have your rest, gateway and the bot process fully functioning and you can finally begin to code your features/commands etc. If you need any help, please contact us on [Discord](https://discord.gg/ddeno). Good Luck! diff --git a/website/src/components/bigbotguide/GatewayFlowChart.tsx b/website/src/components/bigbotguide/GatewayFlowChart.tsx new file mode 100644 index 000000000..17ec411b6 --- /dev/null +++ b/website/src/components/bigbotguide/GatewayFlowChart.tsx @@ -0,0 +1,155 @@ +import ReactFlow, { + Background, + Controls, + Edge, + Node, + Position, +} from 'reactflow' +import 'reactflow/dist/style.css' + +export const defaultNodeOptions = { + targetPosition: Position.Top, + sourcePosition: Position.Bottom, + draggable: false, + style: { width: '70px', height: '50px', padding: '10px 0' }, +} + +const genServer = (x: number, id: number) => { + const server: Node[] = [ + { + id: `s${id + 1}`, + data: { label: `Server ${id + 1}` }, + position: { x: x - 42.5, y: 100 }, + }, + ] + + for (let i = 0; i < 4; i++) { + if (i == 2) { + server.push( + { + id: `baseLineNodeText-${id}-${i}`, + type: 'baseLineNodeText', + position: { x: x - 130, y: 307.5 }, + data: { + label: '.....', + }, + }, + { + id: `baseLineNodeText-${id}-${i}s`, + type: 'baseLineNodeText', + position: { x: x - 130, y: 200 }, + data: { + label: '.....', + }, + }, + ) + continue + } + server.push( + ...[ + { + id: `w${id * 50 + (i == 3 ? 49 : i) + 1}`, + data: { label: `Worker ${id * 50 + (i == 3 ? 49 : i)}` }, + position: { x: x - 112.5 + 75 * i, y: 200 }, + ...defaultNodeOptions, + ...{ style: { ...defaultNodeOptions.style, padding: '5px 10px' } }, + }, + { + id: `w${id * 50 + (i == 3 ? 49 : i) + 1}s`, + data: { + label: `Shard ${id * 500 + (i == 3 ? 49 : i) * 10}-${ + id * 500 + (i == 3 ? 49 : i) * 10 + 9 + }`, + }, + position: { x: x - 112.5 + 75 * i, y: 300 }, + type: 'output', + ...defaultNodeOptions, + ...{ + style: { + ...defaultNodeOptions.style, + ...(id == 1 || (id == 0 && i == 3) + ? { padding: '5px 10px' } + : { padding: `5px 5px` }), + ...(id == 0 && i != 3 ? { padding: '5px 15px' } : {}), + }, + }, + }, + ], + ) + } + + return server +} + +const nodes = [ + { + id: 'gwm', + data: { label: 'Gateway Manager' }, + position: { x: -42.5, y: 0 }, + type: 'input', + }, + ...genServer(-300, 0), + ...genServer(0, 1), + ...genServer(300, 9), + { + id: 'baseLineNodeText-6', + type: 'baseLineNodeText', + position: { x: -15, y: 100 }, + data: { + label: '...............', + }, + }, +] + +const edges: Edge[] = [ + { id: 'gwm-s1', source: 'gwm', target: 's1', type: 'step' }, + { id: 'gwm-s2', source: 'gwm', target: 's2', type: 'step' }, + { id: 'gwm-s10', source: 'gwm', target: 's10', type: 'step' }, + { id: 's1-w1', source: 's1', target: 'w1', type: 'step' }, + { id: 's1-w2', source: 's1', target: 'w2', type: 'step' }, + { id: 's1-w50', source: 's1', target: 'w50', type: 'step' }, + { id: 's2-w51', source: 's2', target: 'w51', type: 'step' }, + { id: 's2-w52', source: 's2', target: 'w52', type: 'step' }, + { id: 's2-w100', source: 's2', target: 'w100', type: 'step' }, + { id: 's10-w451', source: 's10', target: 'w451', type: 'step' }, + { id: 's10-w452', source: 's10', target: 'w452', type: 'step' }, + { id: 's10-w500', source: 's10', target: 'w500', type: 'step' }, + { id: 'w1-w1s', source: 'w1', target: 'w1s', type: 'step' }, + { id: 'w2-w2s', source: 'w2', target: 'w2s', type: 'step' }, + { id: 'w50-w50s', source: 'w50', target: 'w50s', type: 'step' }, + { id: 'w51-w51s', source: 'w51', target: 'w51s', type: 'step' }, + { id: 'w52-w52s', source: 'w52', target: 'w52s', type: 'step' }, + { id: 'w100-w100s', source: 'w100', target: 'w100s', type: 'step' }, + { id: 'w451-w451s', source: 'w451', target: 'w451s', type: 'step' }, + { id: 'w452-w452s', source: 'w452', target: 'w452s', type: 'step' }, + { id: 'w500-w500s', source: 'w500', target: 'w500s', type: 'step' }, +] + +function Flow() { + return ( +
+ ( +
+

{n.data.label}

+
+ ), + }} + fitView + > + + +
+
+ ) +} + +export default Flow